document-processor/app/templates/documents/detail.html

510 lines
29 KiB
HTML

<!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">
{% include "partials/sidebar.html" %}
<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>
<form method="post" action="/documents/{{ document.document_id }}/save-document-type" style="margin-top: 1rem;">
<div style="display:flex; align-items:flex-end; gap:0.6rem; flex-wrap:wrap;">
<div style="position:relative;">
<label for="document_type_input">Document type</label>
<input
id="document_type_input"
type="text"
name="document_type"
value="{{ document.document_type or '' }}"
placeholder="receipt"
autocomplete="off"
style="min-width:160px; max-width:260px;"
>
<div id="document-type-suggestions" style="display:none; position:absolute; top:100%; left:0; right:0; z-index:20; background:#fff; border:1px solid #d7dce5; border-radius:12px; margin-top:0.35rem; max-height:220px; overflow-y:auto; box-shadow:0 10px 24px rgba(15,23,42,0.10);"></div>
</div>
<button type="submit" style="height:38px;">Save</button>
</div>
</form>
<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 == 'additional-fields' %} active{% endif %}" type="button" data-tab="additional-fields">Additional 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 == 'additional-fields' %} active{% endif %}" data-panel="additional-fields">
<h2 class="card-title">Additional fields</h2>
{% if current_additional %}
<p>Current additional fields last updated at {{ current_additional.updated_at }}</p>
{% else %}
<p class="empty-state">No additional fields saved yet.</p>
{% endif %}
{% if presets %}
<form method="get" action="/documents/{{ document.document_id }}" style="margin-bottom: 1rem;">
<input type="hidden" name="tab" value="additional-fields">
<div class="button-row">
<select name="preset_id">
<option value="">Select preset</option>
{% for preset in presets %}
<option value="{{ preset.id }}" {% if selected_preset_id == preset.id %}selected{% endif %}>{{ preset.name }}</option>
{% endfor %}
</select>
<button type="submit">Apply preset</button>
<a class="button-link" href="/presets/">Manage presets</a>
</div>
</form>
{% else %}
<div class="button-row" style="margin-bottom: 1rem;">
<a class="button-link" href="/presets/">Create first preset</a>
</div>
{% endif %}
<form method="post" action="/documents/{{ document.document_id }}/save-additional-fields">
<div class="form-grid">
<div class="form-field">
<label>Primary owner</label>
<input type="text" name="owner_primary" value="{{ additional_form.owner_primary }}">
</div>
<div class="form-field">
<label>Secondary owner</label>
<input type="text" name="owner_secondary" value="{{ additional_form.owner_secondary }}">
</div>
<div class="form-field">
<label>Paid by person</label>
<input type="text" name="paid_by_person" value="{{ additional_form.paid_by_person }}">
</div>
<div class="form-field full">
<label>Covered people</label>
<input type="text" name="covered_people" value="{{ additional_form.covered_people }}" placeholder="Full Name, Full Name">
</div>
<div class="form-field full">
<label>Attendees</label>
<input type="text" name="attendees" value="{{ additional_form.attendees }}" placeholder="Full Name, Full Name">
</div>
<div class="form-field full">
<label>Occasion note</label>
<input type="text" name="occasion_note" value="{{ additional_form.occasion_note }}" placeholder="Dinner with Camie and friends">
</div>
<div class="form-field">
<label><input type="checkbox" name="is_shared_expense" value="1" {% if additional_form.is_shared_expense %}checked{% endif %}> Shared expense</label>
</div>
<div class="form-field full">
<label>Reimbursement expected from</label>
<input type="text" name="reimbursement_expected_from" value="{{ additional_form.reimbursement_expected_from }}" placeholder="Full Name, Full Name">
</div>
<div class="form-field">
<label>Reimbursement paid by</label>
<input type="text" name="reimbursement_paid_by" value="{{ additional_form.reimbursement_paid_by }}">
</div>
<div class="form-field">
<label>Reimbursement paid to</label>
<input type="text" name="reimbursement_paid_to" value="{{ additional_form.reimbursement_paid_to }}">
</div>
<div class="form-field">
<label>Reimbursement paid amount</label>
<input type="text" name="reimbursement_paid_amount" value="{{ additional_form.reimbursement_paid_amount }}">
</div>
<div class="form-field">
<label>Reimbursement paid date</label>
<input type="date" name="reimbursement_paid_date" value="{{ additional_form.reimbursement_paid_date }}">
</div>
<div class="form-field full">
<label>Reimbursement note</label>
<textarea name="reimbursement_note" rows="4">{{ additional_form.reimbursement_note }}</textarea>
</div>
</div>
<div class="button-row" style="margin-top: 1rem;">
<button class="primary" type="submit">Save additional 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();
}
})();
const documentTypeInput = document.getElementById("document_type_input");
const documentTypeSuggestions = document.getElementById("document-type-suggestions");
const existingDocumentTypes = {{ existing_document_types|tojson }};
if (documentTypeInput && documentTypeSuggestions) {
function renderDocumentTypeSuggestions() {
const value = (documentTypeInput.value || "").trim().toLowerCase();
let matches = existingDocumentTypes.filter(function (item) {
return item && (!value || item.toLowerCase().includes(value));
});
if (value) {
matches = matches.filter(function (item) {
return item.toLowerCase() !== value;
});
}
matches = matches.slice(0, 8);
if (!matches.length) {
documentTypeSuggestions.style.display = "none";
documentTypeSuggestions.innerHTML = "";
return;
}
documentTypeSuggestions.innerHTML = matches.map(function (item) {
return '<button type="button" class="doc-type-option" data-value="' + item.replace(/"/g, '&quot;') + '" style="display:block; width:100%; text-align:left; padding:0.7rem 0.85rem; border:0; background:#fff; cursor:pointer;">' + item + '</button>';
}).join("");
documentTypeSuggestions.style.display = "block";
documentTypeSuggestions.querySelectorAll(".doc-type-option").forEach(function (btn) {
btn.addEventListener("click", function () {
documentTypeInput.value = btn.getAttribute("data-value") || "";
documentTypeSuggestions.style.display = "none";
documentTypeSuggestions.innerHTML = "";
documentTypeInput.focus();
});
});
}
documentTypeInput.addEventListener("input", renderDocumentTypeSuggestions);
documentTypeInput.addEventListener("focus", renderDocumentTypeSuggestions);
document.addEventListener("click", function (e) {
if (!documentTypeSuggestions.contains(e.target) && e.target !== documentTypeInput) {
documentTypeSuggestions.style.display = "none";
}
});
}
</script>
</body>
</html>