chore: remove dead PDF artifact imports and clean backup artifacts
This commit is contained in:
parent
0617ab58c4
commit
c19753ec04
|
|
@ -20,8 +20,6 @@ from pypdf import PdfReader
|
|||
from app.core.storage_settings import get_default_save_root
|
||||
from app.db.deps import get_db
|
||||
from app.logic.document_outputs import (
|
||||
create_field_enriched_pdf_version,
|
||||
create_ocr_corrected_pdf_version,
|
||||
save_field_enriched_pdf_current,
|
||||
save_ocr_corrected_pdf_current,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,383 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{{ document.document_id }}</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; margin: 1rem; }
|
||||
textarea { font-family: monospace; }
|
||||
input[type="text"], input[type="date"] { padding: 0.25rem; }
|
||||
.editor-wrap {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.line-numbers {
|
||||
font-family: monospace;
|
||||
white-space: pre;
|
||||
text-align: right;
|
||||
color: #666;
|
||||
user-select: none;
|
||||
padding-top: 2px;
|
||||
min-width: 3rem;
|
||||
}
|
||||
.line-warning {
|
||||
color: #8a5a00;
|
||||
font-weight: 600;
|
||||
}
|
||||
.error-box {
|
||||
background: #ffe8e8;
|
||||
color: #8b0000;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #cc9999;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.split-layout {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
.preview-pane {
|
||||
flex: 1 1 52%;
|
||||
min-width: 420px;
|
||||
}
|
||||
.editor-pane {
|
||||
flex: 1 1 48%;
|
||||
min-width: 420px;
|
||||
}
|
||||
.preview-box {
|
||||
border: 1px solid #ccc;
|
||||
padding: 0.5rem;
|
||||
background: #fafafa;
|
||||
}
|
||||
.preview-box iframe,
|
||||
.preview-box img {
|
||||
width: 100%;
|
||||
height: 900px;
|
||||
border: none;
|
||||
background: white;
|
||||
}
|
||||
.section-box {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
table { border-collapse: collapse; width: 100%; margin-bottom: 1rem; }
|
||||
th, td { border: 1px solid #ccc; padding: 0.4rem; text-align: left; vertical-align: top; }
|
||||
th { background: #f3f3f3; }
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.split-layout {
|
||||
flex-direction: column;
|
||||
}
|
||||
.preview-pane,
|
||||
.editor-pane {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.preview-box iframe,
|
||||
.preview-box img {
|
||||
height: 700px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p>
|
||||
<a href="/documents/">Back to documents</a> |
|
||||
<a href="/queue/">Open review queue</a> |
|
||||
<a href="/trash/">Open trash</a>
|
||||
</p>
|
||||
|
||||
<h1>{{ document.document_id }}</h1>
|
||||
|
||||
{% if error == "line_count_mismatch" %}
|
||||
<div class="error-box">
|
||||
Could not save reviewed OCR because line count did not match OCR layout.
|
||||
Expected {{ error_expected }}, got {{ error_actual }}.
|
||||
</div>
|
||||
{% elif error == "save_ocr_corrected_failed" %}
|
||||
<div class="error-box">
|
||||
Could not save OCR-corrected PDF. Check that reviewed OCR line count matches raw OCR line count.
|
||||
</div>
|
||||
{% elif error == "rerun_ocr_failed" %}
|
||||
<div class="error-box">
|
||||
OCR rerun failed.
|
||||
</div>
|
||||
{% elif error == "save_field_enriched_failed" %}
|
||||
<div class="error-box">
|
||||
Could not save field-enriched PDF.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="/documents/{{ document.document_id }}/move-to-trash" style="margin-bottom: 1rem;">
|
||||
<button type="submit">Move to trash</button>
|
||||
</form>
|
||||
|
||||
<div class="section-box">
|
||||
<h2>Document metadata</h2>
|
||||
<ul>
|
||||
<li>Type: {{ document.document_type }}</li>
|
||||
<li>Source path: {{ document.source_path }}</li>
|
||||
<li>Current path: {{ document.current_path }}</li>
|
||||
<li>Share path: {{ document.share_path or "" }}</li>
|
||||
<li>App URL: <a href="{{ app_url }}">{{ app_url }}</a></li>
|
||||
<li>Original filename: {{ document.original_filename }}</li>
|
||||
<li>Canonical filename: {{ document.canonical_filename }}</li>
|
||||
<li>MIME type: {{ document.mime_type }}</li>
|
||||
<li>File size: {{ document.file_size }}</li>
|
||||
<li>Page count: {{ document.page_count }}</li>
|
||||
<li>Storage status: {{ document.storage_status }}</li>
|
||||
<li>Review status: {{ document.review_status }}</li>
|
||||
<li>Created at: {{ document.created_at }}</li>
|
||||
<li>Updated at: {{ document.updated_at }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="section-box">
|
||||
<h2>Saved PDF outputs</h2>
|
||||
<form method="post" action="/documents/{{ document.document_id }}/save-ocr-corrected-pdf" style="display:inline;">
|
||||
<button type="submit">Save OCR-corrected PDF</button>
|
||||
</form>
|
||||
<form method="post" action="/documents/{{ document.document_id }}/save-field-enriched-pdf" style="display:inline; margin-left: 1rem;">
|
||||
<button type="submit">Save field-enriched PDF</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="split-layout">
|
||||
<div class="preview-pane">
|
||||
<div class="section-box">
|
||||
<h2>Document preview</h2>
|
||||
<div class="preview-box">
|
||||
{% if file_url %}
|
||||
{% if document.mime_type == "application/pdf" %}
|
||||
<iframe src="{{ file_url }}"></iframe>
|
||||
{% elif document.mime_type in ["image/jpeg", "image/png"] %}
|
||||
<img src="{{ file_url }}" alt="Document image">
|
||||
{% else %}
|
||||
<p><a href="{{ file_url }}" target="_blank">Open file</a></p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p>No preview available.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-box">
|
||||
<h2>Document versions</h2>
|
||||
{% if document.versions %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Version</th>
|
||||
<th>Type</th>
|
||||
<th>Path</th>
|
||||
<th>Created</th>
|
||||
<th>Notes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for version in document.versions %}
|
||||
<tr>
|
||||
<td>v{{ version.version_number }}</td>
|
||||
<td>{{ version.version_type }}</td>
|
||||
<td>{{ version.file_path }}</td>
|
||||
<td>{{ version.created_at }}</td>
|
||||
<td>{{ version.notes or "" }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>No versions found.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editor-pane">
|
||||
<div class="section-box">
|
||||
<h2>Raw OCR</h2>
|
||||
<form method="post" action="/documents/{{ document.document_id }}/rerun-ocr">
|
||||
<button type="submit">Re-run OCR</button>
|
||||
</form>
|
||||
|
||||
{% if raw_ocr %}
|
||||
<p>
|
||||
<strong>Text version:</strong> v{{ raw_ocr.version_number }}<br>
|
||||
<strong>OCR engine:</strong> {{ raw_ocr.ocr_engine or "unknown" }}<br>
|
||||
<strong>OCR engine version:</strong> {{ raw_ocr.ocr_engine_version or "unknown" }}<br>
|
||||
<strong>Rerun source:</strong> {{ raw_ocr.rerun_source or "unknown" }}<br>
|
||||
<strong>Quality score:</strong> {{ raw_ocr.quality_score if raw_ocr.quality_score is not none else "not scored yet" }}<br>
|
||||
<strong>Quality flags:</strong> {{ raw_ocr.quality_flags if raw_ocr.quality_flags else [] }}<br>
|
||||
<strong>Quality note:</strong> {{ raw_ocr.quality_note or "" }}
|
||||
</p>
|
||||
{% else %}
|
||||
<p>No raw OCR text found.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="section-box">
|
||||
<h2>Reviewed OCR</h2>
|
||||
{% if reviewed_ocr %}
|
||||
<p>
|
||||
Current reviewed version saved at {{ reviewed_ocr.created_at }} —
|
||||
v{{ reviewed_ocr.version_number }}
|
||||
</p>
|
||||
{% else %}
|
||||
<p>No reviewed OCR saved yet.</p>
|
||||
{% endif %}
|
||||
|
||||
<p>
|
||||
Expected OCR lines: <span id="expected-lines">{{ expected_line_count }}</span><br>
|
||||
Current editor lines: <span id="actual-lines">{{ actual_line_count }}</span>
|
||||
<br><span id="line-warning" class="line-warning" {% if expected_line_count == actual_line_count %}style="display:none;"{% endif %}>
|
||||
Line count mismatch may affect corrected PDF layout.
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<form method="post" action="/documents/{{ document.document_id }}/review-text">
|
||||
<div>
|
||||
<label for="reviewed_text">Edit reviewed OCR text (one line per OCR line):</label>
|
||||
</div>
|
||||
|
||||
<div class="editor-wrap">
|
||||
<div class="line-numbers" id="line-numbers">{% for n in line_numbers %}{{ n }}
|
||||
{% endfor %}</div>
|
||||
<textarea id="reviewed_text" name="reviewed_text" rows="{{ [actual_line_count + 2, 20]|max }}" cols="90">{{ review_text_value }}</textarea>
|
||||
</div>
|
||||
|
||||
<h3>Quality flags</h3>
|
||||
<div>
|
||||
{% for flag in quality_flag_options %}
|
||||
<label style="display:block;">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="quality_flags"
|
||||
value="{{ flag }}"
|
||||
{% if flag in current_quality_flags %}checked{% endif %}
|
||||
>
|
||||
{{ flag }}
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<h3>Quality note</h3>
|
||||
<div>
|
||||
<textarea id="quality_note" name="quality_note" rows="4" cols="90">{{ current_quality_note }}</textarea>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 1rem;">
|
||||
<button type="submit" id="save-reviewed-btn">Save reviewed OCR</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="section-box">
|
||||
<h2>Extracted fields</h2>
|
||||
|
||||
{% if current_extracted %}
|
||||
<p>Current extracted fields last updated at {{ current_extracted.updated_at }}</p>
|
||||
{% else %}
|
||||
<p>No extracted fields saved yet.</p>
|
||||
{% endif %}
|
||||
|
||||
<form method="get" action="/documents/{{ document.document_id }}">
|
||||
<input type="hidden" name="autofill_extracted" value="1">
|
||||
<button type="submit">Auto-extract fields</button>
|
||||
</form>
|
||||
|
||||
<form method="post" action="/documents/{{ document.document_id }}/save-extracted-fields" style="margin-top: 1rem;">
|
||||
<div>
|
||||
<label>Merchant raw:</label><br>
|
||||
<input type="text" name="merchant_raw" value="{{ extracted_form.merchant_raw }}" size="80">
|
||||
</div>
|
||||
<div>
|
||||
<label>Merchant normalized:</label><br>
|
||||
<input type="text" name="merchant_normalized" value="{{ extracted_form.merchant_normalized }}" size="80">
|
||||
</div>
|
||||
<div>
|
||||
<label>Transaction date:</label><br>
|
||||
<input type="date" name="transaction_date" value="{{ extracted_form.transaction_date }}">
|
||||
</div>
|
||||
<div>
|
||||
<label>Transaction time:</label><br>
|
||||
<input type="text" name="transaction_time" value="{{ extracted_form.transaction_time }}" size="20">
|
||||
</div>
|
||||
<div>
|
||||
<label>Subtotal:</label><br>
|
||||
<input type="text" name="subtotal" value="{{ extracted_form.subtotal }}" size="20">
|
||||
</div>
|
||||
<div>
|
||||
<label>Tax:</label><br>
|
||||
<input type="text" name="tax" value="{{ extracted_form.tax }}" size="20">
|
||||
</div>
|
||||
<div>
|
||||
<label>Total:</label><br>
|
||||
<input type="text" name="total" value="{{ extracted_form.total }}" size="20">
|
||||
</div>
|
||||
<div>
|
||||
<label>Currency:</label><br>
|
||||
<input type="text" name="currency" value="{{ extracted_form.currency }}" size="10">
|
||||
</div>
|
||||
<div>
|
||||
<label>Payment method:</label><br>
|
||||
<input type="text" name="payment_method" value="{{ extracted_form.payment_method }}" size="40">
|
||||
</div>
|
||||
<div>
|
||||
<label>Receipt number:</label><br>
|
||||
<input type="text" name="receipt_number" value="{{ extracted_form.receipt_number }}" size="40">
|
||||
</div>
|
||||
<div>
|
||||
<label>Location:</label><br>
|
||||
<input type="text" name="location" value="{{ extracted_form.location }}" size="80">
|
||||
</div>
|
||||
<div>
|
||||
<label>Counterparty:</label><br>
|
||||
<input type="text" name="counterparty" value="{{ extracted_form.counterparty }}" size="80">
|
||||
</div>
|
||||
<div>
|
||||
<label>Extra JSON:</label><br>
|
||||
<textarea name="extra_json" rows="8" cols="90">{{ extracted_form.extra_json }}</textarea>
|
||||
</div>
|
||||
<div style="margin-top: 1rem;">
|
||||
<button type="submit">Save extracted fields</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const textarea = document.getElementById("reviewed_text");
|
||||
const expectedLines = parseInt(document.getElementById("expected-lines").textContent || "0", 10);
|
||||
const actualLinesEl = document.getElementById("actual-lines");
|
||||
const warningEl = document.getElementById("line-warning");
|
||||
const saveBtn = document.getElementById("save-reviewed-btn");
|
||||
const lineNumbersEl = document.getElementById("line-numbers");
|
||||
|
||||
function countLines(text) {
|
||||
if (text.length === 0) return 0;
|
||||
return text.split('\\n').length;
|
||||
}
|
||||
|
||||
function rebuildLineNumbers(lineCount) {
|
||||
let nums = "";
|
||||
for (let i = 1; i <= lineCount; i++) {
|
||||
nums += i + "\\n";
|
||||
}
|
||||
lineNumbersEl.textContent = nums;
|
||||
}
|
||||
|
||||
function updateEditorState() {
|
||||
const actual = countLines(textarea.value);
|
||||
actualLinesEl.textContent = actual.toString();
|
||||
rebuildLineNumbers(Math.max(actual, expectedLines));
|
||||
|
||||
const mismatch = expectedLines > 0 && actual !== expectedLines;
|
||||
warningEl.style.display = mismatch ? "inline" : "none";
|
||||
saveBtn.disabled = mismatch;
|
||||
}
|
||||
|
||||
textarea.addEventListener("input", updateEditorState);
|
||||
updateEditorState();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,362 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{{ document.document_id }}</title>
|
||||
<link rel="stylesheet" href="/static/app.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-shell" id="app-shell">
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-top">
|
||||
<div class="sidebar-toggle" id="menu-toggle" aria-label="Toggle navigation" role="button" tabindex="0">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
<div class="brand">Document Processor</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-section-title">Workspace</div>
|
||||
<nav class="nav-list">
|
||||
<a class="nav-link active" href="/documents/" title="Documents"><span class="nav-link-short">D</span><span class="nav-link-text">Documents</span></a>
|
||||
<a class="nav-link" href="/line-items/" title="Line Items"><span class="nav-link-short">L</span><span class="nav-link-text">Line Items</span></a>
|
||||
<a class="nav-link" href="/queue/" title="Review Queue"><span class="nav-link-short">Q</span><span class="nav-link-text">Review Queue</span></a>
|
||||
<a class="nav-link" href="/trash/" title="Trash"><span class="nav-link-short">T</span><span class="nav-link-text">Trash</span></a>
|
||||
<a class="nav-link" href="/ingest/" title="Ingest"><span class="nav-link-short">I</span><span class="nav-link-text">Ingest</span></a>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<main class="main">
|
||||
{% if error == "line_count_mismatch" %}
|
||||
<div class="error-box">
|
||||
Could not save reviewed OCR because line count did not match OCR layout.
|
||||
Expected {{ error_expected }}, got {{ error_actual }}.
|
||||
</div>
|
||||
{% elif error == "save_ocr_corrected_failed" %}
|
||||
<div class="error-box">
|
||||
Could not save OCR-corrected PDF. Check that reviewed OCR line count matches raw OCR line count.
|
||||
</div>
|
||||
{% elif error == "rerun_ocr_failed" %}
|
||||
<div class="error-box">OCR rerun failed.</div>
|
||||
{% elif error == "save_field_enriched_failed" %}
|
||||
<div class="error-box">Could not save field-enriched PDF.</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="detail-sticky-header">
|
||||
<div class="topbar">
|
||||
<div>
|
||||
<h1 class="page-title">{{ document.document_id }}</h1>
|
||||
<p class="page-subtitle">{{ document.original_filename or document.canonical_filename or document.document_type }}</p>
|
||||
</div>
|
||||
<div class="badges">
|
||||
<span class="badge {% if document.review_status == 'reviewed' %}reviewed{% else %}pending{% endif %}">{{ document.review_status }}</span>
|
||||
<span class="badge">{{ document.document_type }}</span>
|
||||
<span class="badge">{{ document.mime_type }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 0;">
|
||||
<div class="button-row">
|
||||
<form method="post" action="/documents/{{ document.document_id }}/rerun-ocr">
|
||||
<button type="submit">Re-run OCR</button>
|
||||
</form>
|
||||
<form method="post" action="/documents/{{ document.document_id }}/save-ocr-corrected-pdf">
|
||||
<button class="primary" type="submit">Save OCR-corrected PDF</button>
|
||||
</form>
|
||||
<form method="post" action="/documents/{{ document.document_id }}/save-field-enriched-pdf">
|
||||
<button type="submit">Save field-enriched PDF</button>
|
||||
</form>
|
||||
<form method="post" action="/documents/{{ document.document_id }}/move-to-trash">
|
||||
<button class="danger" type="submit">Move to trash</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="queue-nav-row">
|
||||
{% if prev_doc %}
|
||||
<a class="button-link" href="/documents/{{ prev_doc.document_id }}">← Previous</a>
|
||||
{% endif %}
|
||||
{% if next_doc %}
|
||||
<a class="button-link" href="/documents/{{ next_doc.document_id }}">Next →</a>
|
||||
{% endif %}
|
||||
{% if next_ocr_doc %}
|
||||
<a class="button-link" href="/documents/{{ next_ocr_doc.document_id }}">Next OCR review</a>
|
||||
{% endif %}
|
||||
{% if next_fields_doc %}
|
||||
<a class="button-link" href="/documents/{{ next_fields_doc.document_id }}">Next field extraction</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="workspace-grid">
|
||||
<section>
|
||||
<div class="card preview-card">
|
||||
<h2 class="card-title">Document preview</h2>
|
||||
{% if file_url %}
|
||||
{% if document.mime_type == "application/pdf" %}
|
||||
<iframe class="preview-frame" src="{{ file_url }}"></iframe>
|
||||
{% elif document.mime_type in ["image/jpeg", "image/png"] %}
|
||||
<img class="preview-image" src="{{ file_url }}" alt="Document image">
|
||||
{% else %}
|
||||
<p><a href="{{ file_url }}" target="_blank">Open file</a></p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p class="empty-state">No preview available.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="card">
|
||||
<div class="right-pane-tabs">
|
||||
<button class="tab-button{% if active_tab == 'ocr-review' %} active{% endif %}" type="button" data-tab="ocr-review">OCR Review</button>
|
||||
<button class="tab-button{% if active_tab == 'extracted-fields' %} active{% endif %}" type="button" data-tab="extracted-fields">Extracted Fields</button>
|
||||
<button class="tab-button{% if active_tab == 'versions' %} active{% endif %}" type="button" data-tab="versions">Versions</button>
|
||||
<button class="tab-button{% if active_tab == 'raw-ocr' %} active{% endif %}" type="button" data-tab="raw-ocr">Raw OCR</button>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel{% if active_tab == 'ocr-review' %} active{% endif %}" data-panel="ocr-review">
|
||||
<h2 class="card-title">Reviewed OCR</h2>
|
||||
{% if reviewed_ocr %}
|
||||
<p>Current reviewed version saved at {{ reviewed_ocr.created_at }} — v{{ reviewed_ocr.version_number }}</p>
|
||||
{% else %}
|
||||
<p class="empty-state">No reviewed OCR saved yet.</p>
|
||||
{% endif %}
|
||||
|
||||
<p>
|
||||
Expected OCR lines: <span id="expected-lines">{{ expected_line_count }}</span><br>
|
||||
Current editor lines: <span id="actual-lines">{{ actual_line_count }}</span><br>
|
||||
<span id="line-warning" class="line-warning" {% if expected_line_count == actual_line_count %}style="display:none;"{% endif %}>
|
||||
Line count mismatch may affect corrected PDF layout.
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<form method="post" action="/documents/{{ document.document_id }}/review-text">
|
||||
<div class="form-field full">
|
||||
<label for="reviewed_text">Edit reviewed OCR text (one line per OCR line)</label>
|
||||
<div class="editor-wrap">
|
||||
<pre class="line-numbers" id="line-numbers">{% for n in line_numbers %}{{ n }}
|
||||
{% endfor %}</pre>
|
||||
<textarea id="reviewed_text" name="reviewed_text" rows="34" spellcheck="false">{{ review_text_value }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-field full">
|
||||
<label>Quality flags</label>
|
||||
<div>
|
||||
{% for flag in quality_flag_options %}
|
||||
<label style="display:block; margin-bottom: 0.25rem;">
|
||||
<input type="checkbox" name="quality_flags" value="{{ flag }}" {% if flag in current_quality_flags %}checked{% endif %}>
|
||||
{{ flag }}
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-field full">
|
||||
<label for="quality_note">Quality note</label>
|
||||
<textarea id="quality_note" name="quality_note" rows="4">{{ current_quality_note }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="button-row">
|
||||
<button class="primary" type="submit" id="save-reviewed-btn">Save reviewed OCR</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel{% if active_tab == 'extracted-fields' %} active{% endif %}" data-panel="extracted-fields">
|
||||
<h2 class="card-title">Extracted fields</h2>
|
||||
|
||||
{% if current_extracted %}
|
||||
<p>Current extracted fields last updated at {{ current_extracted.updated_at }}</p>
|
||||
{% else %}
|
||||
<p class="empty-state">No extracted fields saved yet.</p>
|
||||
{% endif %}
|
||||
|
||||
<form method="get" action="/documents/{{ document.document_id }}">
|
||||
<input type="hidden" name="autofill_extracted" value="1">
|
||||
<input type="hidden" name="tab" value="extracted-fields">
|
||||
<div class="button-row">
|
||||
<button type="submit">Auto-extract fields</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form method="post" action="/documents/{{ document.document_id }}/save-extracted-fields" style="margin-top: 1rem;">
|
||||
<div class="form-grid">
|
||||
<div class="form-field"><label>Merchant raw</label><input type="text" name="merchant_raw" value="{{ extracted_form.merchant_raw }}"></div>
|
||||
<div class="form-field"><label>Merchant normalized</label><input type="text" name="merchant_normalized" value="{{ extracted_form.merchant_normalized }}"></div>
|
||||
<div class="form-field"><label>Transaction date</label><input type="date" name="transaction_date" value="{{ extracted_form.transaction_date }}"></div>
|
||||
<div class="form-field"><label>Transaction time</label><input type="text" name="transaction_time" value="{{ extracted_form.transaction_time }}"></div>
|
||||
<div class="form-field"><label>Subtotal</label><input type="text" name="subtotal" value="{{ extracted_form.subtotal }}"></div>
|
||||
<div class="form-field"><label>Tax</label><input type="text" name="tax" value="{{ extracted_form.tax }}"></div>
|
||||
<div class="form-field"><label>Total</label><input type="text" name="total" value="{{ extracted_form.total }}"></div>
|
||||
<div class="form-field"><label>Currency</label><input type="text" name="currency" value="{{ extracted_form.currency }}"></div>
|
||||
<div class="form-field"><label>Payment method</label><input type="text" name="payment_method" value="{{ extracted_form.payment_method }}"></div>
|
||||
<div class="form-field"><label>Reference number</label><input type="text" name="receipt_number" value="{{ extracted_form.receipt_number }}"></div>
|
||||
<div class="form-field full"><label>Location</label><input type="text" name="location" value="{{ extracted_form.location }}"></div>
|
||||
<div class="form-field full"><label>Counterparty</label><input type="text" name="counterparty" value="{{ extracted_form.counterparty }}"></div>
|
||||
<div class="form-field full"><label>Extra JSON</label><textarea name="extra_json" rows="8">{{ extracted_form.extra_json }}</textarea></div>
|
||||
</div>
|
||||
|
||||
<div class="button-row" style="margin-top: 1rem;">
|
||||
<button class="primary" type="submit">Save extracted fields</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel{% if active_tab == 'versions' %} active{% endif %}" data-panel="versions">
|
||||
<h2 class="card-title">Document versions</h2>
|
||||
{% if document.versions %}
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Version</th>
|
||||
<th>Type</th>
|
||||
<th>Path</th>
|
||||
<th>Created</th>
|
||||
<th>Notes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for version in document.versions %}
|
||||
<tr>
|
||||
<td>v{{ version.version_number }}</td>
|
||||
<td>{{ version.version_type }}</td>
|
||||
<td>{{ version.file_path }}</td>
|
||||
<td>{{ version.created_at }}</td>
|
||||
<td>{{ version.notes or "" }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="empty-state">No versions found.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="tab-panel{% if active_tab == 'raw-ocr' %} active{% endif %}" data-panel="raw-ocr">
|
||||
<h2 class="card-title">Raw OCR</h2>
|
||||
{% if raw_ocr %}
|
||||
<div class="meta-grid">
|
||||
<div class="meta-item"><span class="meta-label">Text version</span>v{{ raw_ocr.version_number }}</div>
|
||||
<div class="meta-item"><span class="meta-label">OCR engine</span>{{ raw_ocr.ocr_engine or "unknown" }}</div>
|
||||
<div class="meta-item"><span class="meta-label">Engine version</span>{{ raw_ocr.ocr_engine_version or "unknown" }}</div>
|
||||
<div class="meta-item"><span class="meta-label">Rerun source</span>{{ raw_ocr.rerun_source or "unknown" }}</div>
|
||||
<div class="meta-item"><span class="meta-label">Quality score</span>{{ raw_ocr.quality_score if raw_ocr.quality_score is not none else "not scored yet" }}</div>
|
||||
<div class="meta-item"><span class="meta-label">Quality note</span>{{ raw_ocr.quality_note or "" }}</div>
|
||||
</div>
|
||||
<p><strong>Quality flags:</strong> {{ raw_ocr.quality_flags if raw_ocr and raw_ocr.quality_flags else [] }}</p>
|
||||
<pre class="codeblock">{{ raw_ocr.text_content }}</pre>
|
||||
{% else %}
|
||||
<p class="empty-state">No raw OCR text found.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 class="card-title">Metadata</h2>
|
||||
<div class="meta-grid">
|
||||
<div class="meta-item"><span class="meta-label">Type</span>{{ document.document_type }}</div>
|
||||
<div class="meta-item"><span class="meta-label">Review status</span>{{ document.review_status }}</div>
|
||||
<div class="meta-item"><span class="meta-label">Source path</span>{{ document.source_path }}</div>
|
||||
<div class="meta-item"><span class="meta-label">Current path</span>{{ document.current_path }}</div>
|
||||
<div class="meta-item"><span class="meta-label">Original filename</span>{{ document.original_filename }}</div>
|
||||
<div class="meta-item"><span class="meta-label">Canonical filename</span>{{ document.canonical_filename }}</div>
|
||||
<div class="meta-item"><span class="meta-label">MIME type</span>{{ document.mime_type }}</div>
|
||||
<div class="meta-item"><span class="meta-label">File size</span>{{ document.file_size }}</div>
|
||||
<div class="meta-item"><span class="meta-label">Page count</span>{{ document.page_count }}</div>
|
||||
<div class="meta-item"><span class="meta-label">Share path</span>{{ document.share_path or "" }}</div>
|
||||
<div class="meta-item"><span class="meta-label">Created at</span>{{ document.created_at }}</div>
|
||||
<div class="meta-item"><span class="meta-label">Updated at</span>{{ document.updated_at }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const appShell = document.getElementById("app-shell");
|
||||
const menuToggle = document.getElementById("menu-toggle");
|
||||
if (appShell && menuToggle) {
|
||||
menuToggle.addEventListener("click", function () {
|
||||
appShell.classList.toggle("nav-open");
|
||||
});
|
||||
menuToggle.addEventListener("keydown", function (e) {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
appShell.classList.toggle("nav-open");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const tabButtons = document.querySelectorAll("[data-tab]");
|
||||
const tabPanels = document.querySelectorAll("[data-panel]");
|
||||
|
||||
function activateTab(target) {
|
||||
tabButtons.forEach(function (b) {
|
||||
b.classList.toggle("active", b.getAttribute("data-tab") === target);
|
||||
});
|
||||
tabPanels.forEach(function (p) {
|
||||
p.classList.toggle("active", p.getAttribute("data-panel") === target);
|
||||
});
|
||||
}
|
||||
|
||||
tabButtons.forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
const target = btn.getAttribute("data-tab");
|
||||
activateTab(target);
|
||||
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set("tab", target);
|
||||
window.history.replaceState({}, "", url.toString());
|
||||
});
|
||||
});
|
||||
|
||||
const textarea = document.getElementById("reviewed_text");
|
||||
const expectedLinesEl = document.getElementById("expected-lines");
|
||||
const actualLinesEl = document.getElementById("actual-lines");
|
||||
const warningEl = document.getElementById("line-warning");
|
||||
const saveBtn = document.getElementById("save-reviewed-btn");
|
||||
const lineNumbersEl = document.getElementById("line-numbers");
|
||||
|
||||
if (textarea && expectedLinesEl && actualLinesEl && warningEl && saveBtn && lineNumbersEl) {
|
||||
const expectedLines = parseInt(expectedLinesEl.textContent || "0", 10);
|
||||
|
||||
function countLines(text) {
|
||||
if (text.length === 0) return 0;
|
||||
return text.split('\n').length;
|
||||
}
|
||||
|
||||
function rebuildLineNumbers(lineCount) {
|
||||
let nums = "";
|
||||
for (let i = 1; i <= lineCount; i++) nums += i + "\n";
|
||||
lineNumbersEl.textContent = nums;
|
||||
}
|
||||
|
||||
function syncScroll() {
|
||||
lineNumbersEl.scrollTop = textarea.scrollTop;
|
||||
}
|
||||
|
||||
function updateEditorState() {
|
||||
const actual = countLines(textarea.value);
|
||||
actualLinesEl.textContent = actual.toString();
|
||||
rebuildLineNumbers(Math.max(actual, expectedLines));
|
||||
const mismatch = expectedLines > 0 && actual !== expectedLines;
|
||||
warningEl.style.display = mismatch ? "inline" : "none";
|
||||
saveBtn.disabled = mismatch;
|
||||
syncScroll();
|
||||
}
|
||||
|
||||
textarea.addEventListener("input", updateEditorState);
|
||||
textarea.addEventListener("scroll", syncScroll);
|
||||
|
||||
updateEditorState();
|
||||
syncScroll();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Documents</title>
|
||||
<link rel="stylesheet" href="/static/app.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-shell" id="app-shell">
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-top">
|
||||
<div class="sidebar-toggle" id="menu-toggle" aria-label="Toggle navigation" role="button" tabindex="0">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
<div class="brand">Document Processor</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-section-title">Workspace</div>
|
||||
<nav class="nav-list">
|
||||
<a class="nav-link active" href="/documents/" title="Documents"><span class="nav-link-short">D</span><span class="nav-link-text">Documents</span></a>
|
||||
<a class="nav-link" href="/line-items/" title="Line Items"><span class="nav-link-short">L</span><span class="nav-link-text">Line Items</span></a>
|
||||
<a class="nav-link" href="/queue/" title="Review Queue"><span class="nav-link-short">Q</span><span class="nav-link-text">Review Queue</span></a>
|
||||
<a class="nav-link" href="/trash/" title="Trash"><span class="nav-link-short">T</span><span class="nav-link-text">Trash</span></a>
|
||||
<a class="nav-link" href="/ingest/" title="Ingest"><span class="nav-link-short">I</span><span class="nav-link-text">Ingest</span></a>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<main class="main">
|
||||
<div class="topbar">
|
||||
<div>
|
||||
<h1 class="page-title">Documents</h1>
|
||||
<p class="page-subtitle">Active documents available for review and processing.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="button-row">
|
||||
<a class="button-link primary" href="/ingest/">Open ingest</a>
|
||||
<a class="button-link" href="/queue/">Open review queue</a>
|
||||
<a class="button-link" href="/trash/">Open trash</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 class="card-title">All documents</h2>
|
||||
{% if documents %}
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Document</th>
|
||||
<th>Type</th>
|
||||
<th>Review status</th>
|
||||
<th>Current path</th>
|
||||
<th>Updated</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for doc in documents %}
|
||||
<tr>
|
||||
<td><a href="/documents/{{ doc.document_id }}">{{ doc.document_id }}</a></td>
|
||||
<td>{{ doc.document_type }}</td>
|
||||
<td><span class="badge {% if doc.review_status == 'reviewed' %}reviewed{% else %}pending{% endif %}">{{ doc.review_status }}</span></td>
|
||||
<td>{{ doc.current_path }}</td>
|
||||
<td>{{ doc.updated_at }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="empty-state">No documents found.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const appShell = document.getElementById("app-shell");
|
||||
const menuToggle = document.getElementById("menu-toggle");
|
||||
if (!appShell || !menuToggle) return;
|
||||
menuToggle.addEventListener("click", function () {
|
||||
appShell.classList.toggle("nav-open");
|
||||
});
|
||||
menuToggle.addEventListener("keydown", function (e) {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
appShell.classList.toggle("nav-open");
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Ingest</title>
|
||||
<link rel="stylesheet" href="/static/app.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-shell" id="app-shell">
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-top">
|
||||
<div class="sidebar-toggle" id="menu-toggle" aria-label="Toggle navigation" role="button" tabindex="0">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
<div class="brand">Document Processor</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-section-title">Workspace</div>
|
||||
<nav class="nav-list">
|
||||
<a class="nav-link" href="/documents/" title="Documents"><span class="nav-link-short">D</span><span class="nav-link-text">Documents</span></a>
|
||||
<a class="nav-link" href="/line-items/" title="Line Items"><span class="nav-link-short">L</span><span class="nav-link-text">Line Items</span></a>
|
||||
<a class="nav-link" href="/queue/" title="Review Queue"><span class="nav-link-short">Q</span><span class="nav-link-text">Review Queue</span></a>
|
||||
<a class="nav-link" href="/trash/" title="Trash"><span class="nav-link-short">T</span><span class="nav-link-text">Trash</span></a>
|
||||
<a class="nav-link active" href="/ingest/" title="Ingest"><span class="nav-link-short">I</span><span class="nav-link-text">Ingest</span></a>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<main class="main">
|
||||
<div class="topbar">
|
||||
<div>
|
||||
<h1 class="page-title">Ingest</h1>
|
||||
<p class="page-subtitle">Upload files or ingest from server-side paths.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 class="card-title">Upload files</h2>
|
||||
<form method="post" action="/ingest/upload-files" enctype="multipart/form-data">
|
||||
<div class="form-field full">
|
||||
<label>Select file(s)</label>
|
||||
<input type="file" name="uploaded_files" multiple required>
|
||||
</div>
|
||||
<div class="button-row" style="margin-top: 1rem;">
|
||||
<button class="primary" type="submit">Upload and ingest</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 class="card-title">Server-side ingest</h2>
|
||||
|
||||
<form method="post" action="/ingest/server-file" style="margin-bottom: 1.25rem;">
|
||||
<div class="form-field full">
|
||||
<label>Ingest one server file</label>
|
||||
<input type="text" name="file_path" placeholder="/mnt/storage/.../file.pdf" required>
|
||||
</div>
|
||||
<div class="button-row" style="margin-top: 1rem;">
|
||||
<button type="submit">Ingest file</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form method="post" action="/ingest/server-directory" style="margin-bottom: 1.25rem;">
|
||||
<div class="form-field full">
|
||||
<label>Ingest one server directory</label>
|
||||
<input type="text" name="directory_path" placeholder="/mnt/storage/.../incoming" required>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label><input type="checkbox" name="recursive" value="1"> Recursive</label>
|
||||
</div>
|
||||
<div class="button-row" style="margin-top: 1rem;">
|
||||
<button type="submit">Ingest directory</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form method="post" action="/ingest/inbox">
|
||||
<div class="form-field full">
|
||||
<label>Inbox root</label>
|
||||
<input type="text" value="{{ inbox_root }}" readonly>
|
||||
</div>
|
||||
<div class="button-row" style="margin-top: 1rem;">
|
||||
<button type="submit">Ingest inbox</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const appShell = document.getElementById("app-shell");
|
||||
const menuToggle = document.getElementById("menu-toggle");
|
||||
if (!appShell || !menuToggle) return;
|
||||
menuToggle.addEventListener("click", function () {
|
||||
appShell.classList.toggle("nav-open");
|
||||
});
|
||||
menuToggle.addEventListener("keydown", function (e) {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
appShell.classList.toggle("nav-open");
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,161 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Line Items</title>
|
||||
<link rel="stylesheet" href="/static/app.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-shell" id="app-shell">
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-top">
|
||||
<div class="sidebar-toggle" id="menu-toggle" aria-label="Toggle navigation" role="button" tabindex="0">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
<div class="brand">Document Processor</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-section-title">Workspace</div>
|
||||
<nav class="nav-list">
|
||||
<a class="nav-link" href="/documents/" title="Documents"><span class="nav-link-short">D</span><span class="nav-link-text">Documents</span></a>
|
||||
<a class="nav-link active" href="/line-items/" title="Line Items"><span class="nav-link-short">L</span><span class="nav-link-text">Line Items</span></a>
|
||||
<a class="nav-link" href="/line-items/summary" title="Line Item Summary"><span class="nav-link-short">S</span><span class="nav-link-text">Line Item Summary</span></a>
|
||||
<a class="nav-link" href="/queue/" title="Review Queue"><span class="nav-link-short">Q</span><span class="nav-link-text">Review Queue</span></a>
|
||||
<a class="nav-link" href="/trash/" title="Trash"><span class="nav-link-short">T</span><span class="nav-link-text">Trash</span></a>
|
||||
<a class="nav-link" href="/ingest/" title="Ingest"><span class="nav-link-short">I</span><span class="nav-link-text">Ingest</span></a>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<main class="main">
|
||||
<div class="topbar">
|
||||
<div>
|
||||
<h1 class="page-title">Line Items</h1>
|
||||
<p class="page-subtitle">Search extracted purchase lines across documents</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<form method="get" action="/line-items/">
|
||||
<div class="form-grid">
|
||||
<div class="form-field">
|
||||
<label for="q">Item contains</label>
|
||||
<input id="q" type="text" name="q" value="{{ q }}" placeholder="margarita">
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="merchant">Merchant contains</label>
|
||||
<input id="merchant" type="text" name="merchant" value="{{ merchant }}" placeholder="El Canelo">
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="category">Category</label>
|
||||
<input id="category" type="text" name="category" value="{{ category }}" placeholder="cocktail">
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="date_from">Date from</label>
|
||||
<input id="date_from" type="date" name="date_from" value="{{ date_from }}">
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="date_to">Date to</label>
|
||||
<input id="date_to" type="date" name="date_to" value="{{ date_to }}">
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="rating_min">Min rating</label>
|
||||
<input id="rating_min" type="text" name="rating_min" value="{{ rating_min }}" placeholder="7">
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="rating_max">Max rating</label>
|
||||
<input id="rating_max" type="text" name="rating_max" value="{{ rating_max }}" placeholder="10">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-row" style="margin-top: 1rem;">
|
||||
<button class="primary" type="submit">Search</button>
|
||||
<a class="button-link" href="/line-items/">Clear</a>
|
||||
<a class="button-link" href="/line-items/summary?q={{ q }}">Summary view</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 class="card-title">Results</h2>
|
||||
|
||||
{% if rows %}
|
||||
{% for row in rows %}
|
||||
<div class="card" style="margin-bottom: 1rem;">
|
||||
<div class="topbar" style="margin-bottom: 0.75rem;">
|
||||
<div>
|
||||
<div class="page-subtitle">{{ row.transaction_date }} · {{ row.merchant }}</div>
|
||||
<h3 class="card-title" style="margin: 0.2rem 0 0 0;">{{ row.description }}</h3>
|
||||
<div class="page-subtitle">{{ row.raw_description }}</div>
|
||||
</div>
|
||||
<div class="badges">
|
||||
{% if row.category %}
|
||||
<span class="badge">{{ row.category }}</span>
|
||||
{% endif %}
|
||||
{% if row.quantity %}
|
||||
<span class="badge">Qty {{ row.quantity }}</span>
|
||||
{% endif %}
|
||||
{% if row.line_total %}
|
||||
<span class="badge">${{ row.line_total }}</span>
|
||||
{% endif %}
|
||||
{% if row.confidence %}
|
||||
<span class="badge">Conf {{ row.confidence }}</span>
|
||||
{% endif %}
|
||||
{% if row.quality_rating %}
|
||||
<span class="badge reviewed">Rating {{ row.quality_rating }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" action="/line-items/{{ row.line_item_id }}/review">
|
||||
<input type="hidden" name="q" value="{{ q }}">
|
||||
<input type="hidden" name="merchant" value="{{ merchant }}">
|
||||
<input type="hidden" name="category" value="{{ category }}">
|
||||
<input type="hidden" name="date_from" value="{{ date_from }}">
|
||||
<input type="hidden" name="date_to" value="{{ date_to }}">
|
||||
<input type="hidden" name="rating_min" value="{{ rating_min }}">
|
||||
<input type="hidden" name="rating_max" value="{{ rating_max }}">
|
||||
|
||||
<div class="form-grid">
|
||||
<div class="form-field">
|
||||
<label for="quality_rating_{{ row.line_item_id }}">Quality rating</label>
|
||||
<input id="quality_rating_{{ row.line_item_id }}" type="text" name="quality_rating" value="{{ row.quality_rating }}" placeholder="e.g. 8.5 or 4/5">
|
||||
</div>
|
||||
<div class="form-field full">
|
||||
<label for="quality_note_{{ row.line_item_id }}">Quality note</label>
|
||||
<textarea id="quality_note_{{ row.line_item_id }}" name="quality_note" rows="3" placeholder="Taste, portion, texture, service notes...">{{ row.quality_note }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-row" style="margin-top: 1rem;">
|
||||
<button class="primary" type="submit">Save rating/note</button>
|
||||
<a class="button-link" href="/documents/{{ row.document_id }}?tab=extracted-fields">Open document</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="empty-state">No line items found for the current filters.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const appShell = document.getElementById("app-shell");
|
||||
const menuToggle = document.getElementById("menu-toggle");
|
||||
if (appShell && menuToggle) {
|
||||
menuToggle.addEventListener("click", function () {
|
||||
appShell.classList.toggle("nav-open");
|
||||
});
|
||||
menuToggle.addEventListener("keydown", function (e) {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
appShell.classList.toggle("nav-open");
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Line Item Summary</title>
|
||||
<link rel="stylesheet" href="/static/app.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-shell" id="app-shell">
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-top">
|
||||
<div class="sidebar-toggle" id="menu-toggle" aria-label="Toggle navigation" role="button" tabindex="0">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
<div class="brand">Document Processor</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-section-title">Workspace</div>
|
||||
<nav class="nav-list">
|
||||
<a class="nav-link" href="/documents/" title="Documents"><span class="nav-link-short">D</span><span class="nav-link-text">Documents</span></a>
|
||||
<a class="nav-link" href="/line-items/" title="Line Items"><span class="nav-link-short">L</span><span class="nav-link-text">Line Items</span></a>
|
||||
<a class="nav-link active" href="/line-items/summary" title="Line Item Summary"><span class="nav-link-short">S</span><span class="nav-link-text">Line Item Summary</span></a>
|
||||
<a class="nav-link" href="/queue/" title="Review Queue"><span class="nav-link-short">Q</span><span class="nav-link-text">Review Queue</span></a>
|
||||
<a class="nav-link" href="/trash/" title="Trash"><span class="nav-link-short">T</span><span class="nav-link-text">Trash</span></a>
|
||||
<a class="nav-link" href="/ingest/" title="Ingest"><span class="nav-link-short">I</span><span class="nav-link-text">Ingest</span></a>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<main class="main">
|
||||
<div class="topbar">
|
||||
<div>
|
||||
<h1 class="page-title">Line Item Summary</h1>
|
||||
<p class="page-subtitle">Aggregate prices and ratings across extracted line items</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<form method="get" action="/line-items/summary">
|
||||
<div class="form-grid">
|
||||
<div class="form-field">
|
||||
<label for="q">Item contains</label>
|
||||
<input id="q" name="q" value="{{ q }}" placeholder="margarita">
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-row" style="margin-top: 1rem;">
|
||||
<button class="primary" type="submit">Search</button>
|
||||
<a class="button-link" href="/line-items/summary">Clear</a>
|
||||
<a class="button-link" href="/line-items/?q={{ q }}">Detailed view</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 class="card-title">Summary Results</h2>
|
||||
|
||||
{% if rows %}
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Item</th>
|
||||
<th>Count</th>
|
||||
<th>Avg Price</th>
|
||||
<th>Min Price</th>
|
||||
<th>Max Price</th>
|
||||
<th>Rated Count</th>
|
||||
<th>Avg Rating</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in rows %}
|
||||
<tr>
|
||||
<td>{{ row.item }}</td>
|
||||
<td>{{ row.count }}</td>
|
||||
<td>{{ row.avg_price }}</td>
|
||||
<td>{{ row.min_price }}</td>
|
||||
<td>{{ row.max_price }}</td>
|
||||
<td>{{ row.rated_count }}</td>
|
||||
<td>{{ row.avg_rating }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="empty-state">No summary rows found for the current search.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const appShell = document.getElementById("app-shell");
|
||||
const menuToggle = document.getElementById("menu-toggle");
|
||||
if (appShell && menuToggle) {
|
||||
menuToggle.addEventListener("click", function () {
|
||||
appShell.classList.toggle("nav-open");
|
||||
});
|
||||
menuToggle.addEventListener("keydown", function (e) {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
appShell.classList.toggle("nav-open");
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,134 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Review Queue</title>
|
||||
<link rel="stylesheet" href="/static/app.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-shell" id="app-shell">
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-top">
|
||||
<div class="sidebar-toggle" id="menu-toggle" aria-label="Toggle navigation" role="button" tabindex="0">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
<div class="brand">Document Processor</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-section-title">Workspace</div>
|
||||
<nav class="nav-list">
|
||||
<a class="nav-link" href="/documents/" title="Documents"><span class="nav-link-short">D</span><span class="nav-link-text">Documents</span></a>
|
||||
<a class="nav-link" href="/line-items/" title="Line Items"><span class="nav-link-short">L</span><span class="nav-link-text">Line Items</span></a>
|
||||
<a class="nav-link active" href="/queue/" title="Review Queue"><span class="nav-link-short">Q</span><span class="nav-link-text">Review Queue</span></a>
|
||||
<a class="nav-link" href="/trash/" title="Trash"><span class="nav-link-short">T</span><span class="nav-link-text">Trash</span></a>
|
||||
<a class="nav-link" href="/ingest/" title="Ingest"><span class="nav-link-short">I</span><span class="nav-link-text">Ingest</span></a>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<main class="main">
|
||||
<div class="topbar">
|
||||
<div>
|
||||
<h1 class="page-title">Review Queue</h1>
|
||||
<p class="page-subtitle">Work through OCR review and field extraction in order.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="button-row">
|
||||
{% if next_ocr %}
|
||||
<a class="button-link primary" href="/documents/{{ next_ocr.document_id }}?queue=ocr">Next needing OCR review</a>
|
||||
{% endif %}
|
||||
{% if next_fields %}
|
||||
<a class="button-link" href="/documents/{{ next_fields.document_id }}?queue=fields">Next needing field extraction</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 class="card-title">Needs OCR review ({{ needs_ocr_review|length }})</h2>
|
||||
{% if needs_ocr_review %}
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>Document</th><th>Type</th><th>Review status</th><th>Updated</th></tr></thead>
|
||||
<tbody>
|
||||
{% for doc in needs_ocr_review %}
|
||||
<tr>
|
||||
<td><a href="/documents/{{ doc.document_id }}?queue=ocr">{{ doc.document_id }}</a></td>
|
||||
<td>{{ doc.document_type }}</td>
|
||||
<td><span class="badge pending">{{ doc.review_status }}</span></td>
|
||||
<td>{{ doc.updated_at }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="empty-state">No documents currently need OCR review.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 class="card-title">Needs field extraction ({{ needs_field_extraction|length }})</h2>
|
||||
{% if needs_field_extraction %}
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>Document</th><th>Type</th><th>Review status</th><th>Updated</th></tr></thead>
|
||||
<tbody>
|
||||
{% for doc in needs_field_extraction %}
|
||||
<tr>
|
||||
<td><a href="/documents/{{ doc.document_id }}?queue=fields">{{ doc.document_id }}</a></td>
|
||||
<td>{{ doc.document_type }}</td>
|
||||
<td><span class="badge reviewed">{{ doc.review_status }}</span></td>
|
||||
<td>{{ doc.updated_at }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="empty-state">No reviewed documents are waiting on field extraction.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 class="card-title">Recently updated</h2>
|
||||
{% if recently_updated %}
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>Document</th><th>Type</th><th>Review status</th><th>Current path</th><th>Updated</th></tr></thead>
|
||||
<tbody>
|
||||
{% for doc in recently_updated %}
|
||||
<tr>
|
||||
<td><a href="/documents/{{ doc.document_id }}?queue=recent">{{ doc.document_id }}</a></td>
|
||||
<td>{{ doc.document_type }}</td>
|
||||
<td><span class="badge {% if doc.review_status == 'reviewed' %}reviewed{% else %}pending{% endif %}">{{ doc.review_status }}</span></td>
|
||||
<td>{{ doc.current_path }}</td>
|
||||
<td>{{ doc.updated_at }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const appShell = document.getElementById("app-shell");
|
||||
const menuToggle = document.getElementById("menu-toggle");
|
||||
if (!appShell || !menuToggle) return;
|
||||
menuToggle.addEventListener("click", function () {
|
||||
appShell.classList.toggle("nav-open");
|
||||
});
|
||||
menuToggle.addEventListener("keydown", function (e) {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
appShell.classList.toggle("nav-open");
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Trash</title>
|
||||
<link rel="stylesheet" href="/static/app.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-shell" id="app-shell">
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-top">
|
||||
<div class="sidebar-toggle" id="menu-toggle" aria-label="Toggle navigation" role="button" tabindex="0">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
<div class="brand">Document Processor</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-section-title">Workspace</div>
|
||||
<nav class="nav-list">
|
||||
<a class="nav-link" href="/documents/" title="Documents"><span class="nav-link-short">D</span><span class="nav-link-text">Documents</span></a>
|
||||
<a class="nav-link" href="/line-items/" title="Line Items"><span class="nav-link-short">L</span><span class="nav-link-text">Line Items</span></a>
|
||||
<a class="nav-link" href="/queue/" title="Review Queue"><span class="nav-link-short">Q</span><span class="nav-link-text">Review Queue</span></a>
|
||||
<a class="nav-link active" href="/trash/" title="Trash"><span class="nav-link-short">T</span><span class="nav-link-text">Trash</span></a>
|
||||
<a class="nav-link" href="/ingest/" title="Ingest"><span class="nav-link-short">I</span><span class="nav-link-text">Ingest</span></a>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<main class="main">
|
||||
<div class="topbar">
|
||||
<div>
|
||||
<h1 class="page-title">Trash</h1>
|
||||
<p class="page-subtitle">Soft-deleted documents can be restored or removed permanently.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
{% if documents %}
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Document</th>
|
||||
<th>Type</th>
|
||||
<th>Review status</th>
|
||||
<th>Trashed at</th>
|
||||
<th>Current path</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for doc in documents %}
|
||||
<tr>
|
||||
<td><a href="/documents/{{ doc.document_id }}">{{ doc.document_id }}</a></td>
|
||||
<td>{{ doc.document_type }}</td>
|
||||
<td><span class="badge trashed">{{ doc.review_status }}</span></td>
|
||||
<td>{{ doc.trashed_at }}</td>
|
||||
<td>{{ doc.current_path }}</td>
|
||||
<td>
|
||||
<div class="button-row">
|
||||
<form method="post" action="/trash/{{ doc.document_id }}/restore">
|
||||
<button type="submit">Restore</button>
|
||||
</form>
|
||||
<form method="post" action="/trash/{{ doc.document_id }}/delete">
|
||||
<button class="danger" type="submit">Delete permanently</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="empty-state">Trash is empty.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const appShell = document.getElementById("app-shell");
|
||||
const menuToggle = document.getElementById("menu-toggle");
|
||||
if (!appShell || !menuToggle) return;
|
||||
menuToggle.addEventListener("click", function () {
|
||||
appShell.classList.toggle("nav-open");
|
||||
});
|
||||
menuToggle.addEventListener("keydown", function (e) {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
appShell.classList.toggle("nav-open");
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue