feat: improve document detail UI and disable deprecated PDF save routes
This commit is contained in:
parent
aecc7c5679
commit
b1e059fe05
|
|
@ -959,38 +959,10 @@ def rerun_ocr(document_id: str, db: Session = Depends(get_db)):
|
|||
|
||||
@router.post("/{document_id}/save-ocr-corrected-pdf", response_class=RedirectResponse)
|
||||
def save_ocr_corrected_pdf(document_id: str, db: Session = Depends(get_db)):
|
||||
document = (
|
||||
db.query(Document)
|
||||
.options(
|
||||
selectinload(Document.text_versions),
|
||||
selectinload(Document.naming_fields),
|
||||
selectinload(Document.extracted_fields),
|
||||
selectinload(Document.additional_fields),
|
||||
return RedirectResponse(
|
||||
url=f"/documents/{document_id}?error=deprecated_pdf_route_disabled&tab=ocr-review",
|
||||
status_code=303,
|
||||
)
|
||||
.filter(Document.document_id == document_id)
|
||||
.first()
|
||||
)
|
||||
if document is None:
|
||||
return RedirectResponse(url="/documents/", status_code=303)
|
||||
|
||||
save_root = get_default_save_root()
|
||||
naming_row = document.naming_fields[0] if getattr(document, "naming_fields", None) else None
|
||||
output_path = Path(
|
||||
build_proposed_storage_path(
|
||||
document=document,
|
||||
save_root=save_root,
|
||||
naming_row=naming_row,
|
||||
)
|
||||
)
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
try:
|
||||
create_ocr_corrected_pdf_version(db, document, output_path=output_path_obj)
|
||||
except Exception:
|
||||
return RedirectResponse(url=f"/documents/{document.document_id}?error=save_ocr_corrected_failed", status_code=303)
|
||||
|
||||
return RedirectResponse(url=f"/documents/{document.document_id}?tab=ocr-review", status_code=303)
|
||||
|
||||
|
||||
|
||||
@router.post("/{document_id}/save-review-flags", response_class=RedirectResponse)
|
||||
|
|
@ -1109,39 +1081,10 @@ def save_pdf(document_id: str, output_path: str = Form(""), db: Session = Depend
|
|||
|
||||
@router.post("/{document_id}/save-field-enriched-pdf", response_class=RedirectResponse)
|
||||
def save_field_enriched_pdf(document_id: str, db: Session = Depends(get_db)):
|
||||
document = (
|
||||
db.query(Document)
|
||||
.options(
|
||||
selectinload(Document.naming_fields),
|
||||
selectinload(Document.extracted_fields),
|
||||
selectinload(Document.additional_fields),
|
||||
return RedirectResponse(
|
||||
url=f"/documents/{document_id}?error=deprecated_pdf_route_disabled&tab=extracted-fields",
|
||||
status_code=303,
|
||||
)
|
||||
.filter(Document.document_id == document_id)
|
||||
.first()
|
||||
)
|
||||
if document is None:
|
||||
return RedirectResponse(url="/documents/", status_code=303)
|
||||
|
||||
save_root = get_default_save_root()
|
||||
naming_row = document.naming_fields[0] if getattr(document, "naming_fields", None) else None
|
||||
output_path = Path(
|
||||
build_proposed_storage_path(
|
||||
document=document,
|
||||
save_root=save_root,
|
||||
naming_row=naming_row,
|
||||
)
|
||||
)
|
||||
output_path = output_path.with_name(
|
||||
re.sub(r"_v\d+(?=\.[^.]+$)", "", output_path.name)
|
||||
)
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
try:
|
||||
create_field_enriched_pdf_version(db, document, output_path=output_path_obj)
|
||||
except Exception as e:
|
||||
return RedirectResponse(url=f"/documents/{document.document_id}?error=save_field_enriched_failed", status_code=303)
|
||||
|
||||
return RedirectResponse(url=f"/documents/{document.document_id}?tab=extracted-fields", status_code=303)
|
||||
|
||||
|
||||
@router.post("/{document_id}/review-text", response_class=RedirectResponse)
|
||||
|
|
|
|||
|
|
@ -5695,3 +5695,71 @@ table {
|
|||
}
|
||||
/* ===== end polished login page ===== */
|
||||
|
||||
|
||||
/* ===== mobile split mode wide canvas ===== */
|
||||
@media (max-width: 900px) {
|
||||
body.detail-mode-split .document-detail-grid,
|
||||
body.detail-mode-split .detail-layout-grid,
|
||||
body.detail-mode-split .detail-main-grid {
|
||||
display: grid !important;
|
||||
grid-template-columns: 1.2fr 0.8fr !important;
|
||||
gap: 0.75rem !important;
|
||||
width: 300vw !important;
|
||||
min-width: 300vw !important;
|
||||
max-width: 300vw !important;
|
||||
align-items: start !important;
|
||||
}
|
||||
|
||||
body.detail-mode-split .preview-card,
|
||||
body.detail-mode-split .tabbed-review-card,
|
||||
body.detail-mode-split .review-card,
|
||||
body.detail-mode-split .document-preview-card,
|
||||
body.detail-mode-split .document-review-card {
|
||||
width: 100% !important;
|
||||
max-width: none !important;
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
body.detail-mode-split .main,
|
||||
body.detail-mode-split .main-content {
|
||||
overflow-x: auto !important;
|
||||
}
|
||||
}
|
||||
/* ===== end mobile split mode wide canvas ===== */
|
||||
|
||||
.line-item-delete-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
padding: 0;
|
||||
border: 1px solid #e7bcbc;
|
||||
border-radius: 0.75rem;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.line-item-delete-btn:hover {
|
||||
background: #fff5f5;
|
||||
}
|
||||
|
||||
.line-item-delete-btn svg {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.line-item-delete-btn .trash-tile {
|
||||
fill: #fff;
|
||||
stroke: #cf2e2e;
|
||||
stroke-width: 1.6;
|
||||
}
|
||||
|
||||
.line-item-delete-btn .trash-can {
|
||||
fill: none;
|
||||
stroke: #cf2e2e;
|
||||
stroke-width: 1.9;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}Document Processor{% endblock %}</title>
|
||||
<link rel="stylesheet" href="/static/app.css?v=154">
|
||||
<link rel="stylesheet" href="/static/app-shell.css?v=66">
|
||||
<link rel="stylesheet" href="/static/app.css?v=161">
|
||||
<link rel="stylesheet" href="/static/app-shell.css?v=158">
|
||||
</head>
|
||||
<body>
|
||||
<button id="mobile-nav-toggle" class="mobile-nav-toggle" type="button" aria-label="Toggle navigation">
|
||||
|
|
|
|||
|
|
@ -79,6 +79,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||
<div class="success-message">Reviewed OCR saved.</div>
|
||||
{% elif error == "rerun_ocr_failed" %}
|
||||
<div class="error-box">OCR rerun failed.</div>
|
||||
{% elif error == "deprecated_pdf_route_disabled" %}
|
||||
<div class="error-box">This deprecated PDF save route has been disabled. Use Save Document instead.</div>
|
||||
{% elif error == "save_field_enriched_failed" %}
|
||||
<div class="error-box">Could not save field-enriched PDF.</div>
|
||||
{% endif %}
|
||||
|
|
@ -218,7 +220,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||
<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 in ['ocr-review', 'raw-ocr', 'source-options'] %} 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 == 'line-items' %} active{% endif %}" type="button" data-tab="line-items">Line Items</button>
|
||||
|
|
@ -227,7 +229,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||
<button class="tab-button{% if active_tab == 'source-options' %} active{% endif %}" type="button" data-tab="source-options">Source Options</button>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel{% if active_tab == 'ocr-review' %} active{% endif %}" data-panel="ocr-review">
|
||||
<div class="tab-panel{% if active_tab in ['ocr-review', 'raw-ocr', 'source-options'] %} active{% endif %}" data-panel="ocr-review">
|
||||
<div class="ocr-review-header-row">
|
||||
<h2 class="card-title">Reviewed OCR</h2>
|
||||
<form method="post" action="/documents/{{ document.document_id }}/rerun-ocr" class="ocr-rerun-inline-form">
|
||||
|
|
@ -458,7 +460,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||
|
||||
<form method="post" action="/documents/{{ document.document_id }}/save-line-items">
|
||||
{% set base_count = line_items|length %}
|
||||
{% set row_count = base_count + 3 if base_count > 0 else 12 %}
|
||||
{% set row_count = base_count if base_count > 0 else 0 %}
|
||||
<input type="hidden" name="row_count" value="{{ row_count }}">
|
||||
|
||||
<div style="margin-bottom: 0.5rem;">
|
||||
|
|
@ -484,8 +486,21 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||
<tbody>
|
||||
{% for i in range(row_count) %}
|
||||
{% set item = line_items[i] if i < line_items|length else None %}
|
||||
<tr>
|
||||
<td style="padding:0.35rem;">{{ i + 1 }}</td>
|
||||
<tr class="line-item-row">
|
||||
<td style="padding:0.35rem; white-space:nowrap;">
|
||||
<span class="line-item-row-number">{{ i + 1 }}</span>
|
||||
<button type="button" onclick="removeLineItemRow(this)" aria-label="Remove line item" title="Remove line item" style="margin-left:0.45rem; padding:0; border:none; background:transparent; cursor:pointer; display:inline-flex; align-items:center; justify-content:center; width:2.1rem; height:2.1rem; vertical-align:middle;">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true" style="display:block; width:1.9rem; height:1.9rem;">
|
||||
<rect x="1.5" y="1.5" width="21" height="21" rx="5" fill="#ffffff" stroke="#d7dce5" stroke-width="1.2"></rect>
|
||||
<path d="M8 7h8" fill="none" stroke="#cf2e2e" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
<path d="M10 5h4" fill="none" stroke="#cf2e2e" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
<path d="M9 9.25v6.25" fill="none" stroke="#cf2e2e" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
<path d="M12 9.25v6.25" fill="none" stroke="#cf2e2e" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
<path d="M15 9.25v6.25" fill="none" stroke="#cf2e2e" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
<path d="M7.5 7.5l.7 10.2a1 1 0 0 0 1 .9h5.6a1 1 0 0 0 1-.9l.7-10.2" fill="none" stroke="#cf2e2e" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</td>
|
||||
<td style="padding:0.35rem;"><input type="date" name="entry_date_{{ i }}" value="{{ item.entry_date.isoformat() if item and item.entry_date else '' }}" style="width:100%;"></td>
|
||||
<td style="padding:0.35rem;"><input type="text" name="description_{{ i }}" value="{{ item.description if item else '' }}" style="width:100%;"></td>
|
||||
<td style="padding:0.35rem;"><input type="text" name="quantity_{{ i }}" value="{{ (item.quantity | string | replace('.0000', '') | replace('.00', '')) if item and item.quantity is not none else '' }}" style="width:100%;"></td>
|
||||
|
|
@ -506,17 +521,64 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||
</form>
|
||||
|
||||
<script>
|
||||
function renumberLineItemRows(panel) {
|
||||
const tbody = panel.querySelector("tbody");
|
||||
const rowCountInput = panel.querySelector('input[name="row_count"]');
|
||||
const rows = Array.from(tbody.querySelectorAll("tr.line-item-row, tr"));
|
||||
|
||||
rows.forEach((row, i) => {
|
||||
const num = row.querySelector(".line-item-row-number");
|
||||
if (num) {
|
||||
num.textContent = String(i + 1);
|
||||
} else if (row.cells && row.cells[0]) {
|
||||
row.cells[0].textContent = String(i + 1);
|
||||
}
|
||||
|
||||
row.querySelectorAll("input").forEach((input) => {
|
||||
const oldName = input.getAttribute("name");
|
||||
if (!oldName) return;
|
||||
input.setAttribute("name", oldName.replace(/_\d+$/, `_${i}`));
|
||||
});
|
||||
});
|
||||
|
||||
rowCountInput.value = rows.length;
|
||||
}
|
||||
|
||||
function removeLineItemRow(button) {
|
||||
const panel = document.querySelector('[data-panel="line-items"]');
|
||||
if (!panel) return;
|
||||
|
||||
const row = button.closest("tr");
|
||||
if (!row) return;
|
||||
|
||||
row.remove();
|
||||
renumberLineItemRows(panel);
|
||||
}
|
||||
|
||||
function addRow() {
|
||||
const panel = document.querySelector('[data-panel="line-items"]');
|
||||
if (!panel) return;
|
||||
|
||||
const tbody = panel.querySelector("tbody");
|
||||
const rowCountInput = panel.querySelector('input[name="row_count"]');
|
||||
const i = tbody.querySelectorAll("tr").length;
|
||||
|
||||
const row = document.createElement("tr");
|
||||
row.className = "line-item-row";
|
||||
row.innerHTML = `
|
||||
<td style="padding:0.35rem;">${i + 1}</td>
|
||||
<td style="padding:0.35rem; white-space:nowrap;">
|
||||
<span class="line-item-row-number">${i + 1}</span>
|
||||
<button type="button" onclick="removeLineItemRow(this)" aria-label="Remove line item" title="Remove line item" style="margin-left:0.45rem; padding:0; border:none; background:transparent; cursor:pointer; display:inline-flex; align-items:center; justify-content:center; width:2.1rem; height:2.1rem; vertical-align:middle;">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true" style="display:block; width:1.9rem; height:1.9rem;">
|
||||
<rect x="1.5" y="1.5" width="21" height="21" rx="5" fill="#ffffff" stroke="#d7dce5" stroke-width="1.2"></rect>
|
||||
<path d="M8 7h8" fill="none" stroke="#cf2e2e" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
<path d="M10 5h4" fill="none" stroke="#cf2e2e" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
<path d="M9 9.25v6.25" fill="none" stroke="#cf2e2e" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
<path d="M12 9.25v6.25" fill="none" stroke="#cf2e2e" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
<path d="M15 9.25v6.25" fill="none" stroke="#cf2e2e" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
<path d="M7.5 7.5l.7 10.2a1 1 0 0 0 1 .9h5.6a1 1 0 0 0 1-.9l.7-10.2" fill="none" stroke="#cf2e2e" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</td>
|
||||
<td style="padding:0.35rem;"><input type="date" name="entry_date_${i}" style="width:100%;"></td>
|
||||
<td style="padding:0.35rem;"><input type="text" name="description_${i}" style="width:100%;"></td>
|
||||
<td style="padding:0.35rem;"><input type="text" name="quantity_${i}" style="width:100%;"></td>
|
||||
|
|
@ -525,10 +587,21 @@ function addRow() {
|
|||
<td style="padding:0.35rem;"><input type="text" name="tax_amount_${i}" style="width:100%;"></td>
|
||||
<td style="padding:0.35rem;"><input type="text" name="category_${i}" style="width:100%;"></td>
|
||||
<td style="padding:0.35rem;"><input type="text" name="notes_${i}" style="width:100%;"></td>
|
||||
<td style="padding:0.35rem; width:3.25rem; text-align:center;"><button type="button" class="line-item-delete-btn" onclick="deleteRow(this)" aria-label="Delete line item" title="Delete line item">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<rect class="trash-tile" x="1.5" y="1.5" width="21" height="21" rx="5"></rect>
|
||||
<path class="trash-can" d="M8 7h8"></path>
|
||||
<path class="trash-can" d="M10 5h4"></path>
|
||||
<path class="trash-can" d="M9 9.25v6.25"></path>
|
||||
<path class="trash-can" d="M12 9.25v6.25"></path>
|
||||
<path class="trash-can" d="M15 9.25v6.25"></path>
|
||||
<path class="trash-can" d="M7.5 7.5l.7 10.2a1 1 0 0 0 1 .9h5.6a1 1 0 0 0 1-.9l.7-10.2"></path>
|
||||
</svg>
|
||||
</button></td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
|
||||
rowCountInput.value = i + 1;
|
||||
renumberLineItemRows(panel);
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
|
|
@ -585,40 +658,65 @@ function addRow() {
|
|||
<select id="file_action_select" name="file_action">
|
||||
<option value="none" selected>No file change</option>
|
||||
<option value="revert_original">Revert to original file</option>
|
||||
<option value="revert_current_saved">Revert to current saved version</option>
|
||||
<option value="revert_current_version">Revert to latest saved PDF version</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="padding:1rem;">
|
||||
<h3 style="margin-top:0;">Data Reset</h3>
|
||||
<h3 style="margin-top:0;">Restore from version history</h3>
|
||||
<div class="data-reset-grid">
|
||||
<div class="data-reset-row">
|
||||
<label for="ocr_action">OCR</label>
|
||||
<select id="ocr_action" name="ocr_action">
|
||||
<option value="none" {% if selected_ocr_action == "none" %}selected{% endif %}>No change</option>
|
||||
<option value="reset" {% if selected_ocr_action == "reset" %}selected{% endif %}>Reset</option>
|
||||
<label for="ocr_restore_choice">OCR</label>
|
||||
<select id="ocr_restore_choice" name="ocr_restore_choice">
|
||||
<option value="none">No change</option>
|
||||
<option value="original">Reset to original</option>
|
||||
{% for tv in (document.text_versions | sort(attribute='version_number', reverse=True)) %}
|
||||
<option value="version:{{ tv.version_number }}">
|
||||
v{{ tv.version_number }} — {{ tv.version_type }} — {{ tv.created_at }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="data-reset-row">
|
||||
<label for="extracted_action">Extracted fields</label>
|
||||
<select id="extracted_action" name="extracted_action">
|
||||
<option value="none" {% if selected_extracted_action == "none" %}selected{% endif %}>No change</option>
|
||||
<option value="reset" {% if selected_extracted_action == "reset" %}selected{% endif %}>Reset</option>
|
||||
<label for="extracted_restore_choice">Extracted fields</label>
|
||||
<select id="extracted_restore_choice" name="extracted_restore_choice">
|
||||
<option value="none">No change</option>
|
||||
<option value="original">Reset to original</option>
|
||||
{% for v in (document.extracted_field_versions | sort(attribute='version_number', reverse=True)) %}
|
||||
<option value="version:{{ v.version_number }}">
|
||||
v{{ v.version_number }} — {{ v.created_at }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="data-reset-row">
|
||||
<label for="additional_action">Additional fields</label>
|
||||
<select id="additional_action" name="additional_action">
|
||||
<option value="none" {% if selected_additional_action == "none" %}selected{% endif %}>No change</option>
|
||||
<option value="reset" {% if selected_additional_action == "reset" %}selected{% endif %}>Reset</option>
|
||||
<label for="additional_restore_choice">Additional fields</label>
|
||||
<select id="additional_restore_choice" name="additional_restore_choice">
|
||||
<option value="none">No change</option>
|
||||
<option value="original">Reset to original</option>
|
||||
{% for v in (document.additional_field_versions | sort(attribute='version_number', reverse=True)) %}
|
||||
<option value="version:{{ v.version_number }}">
|
||||
v{{ v.version_number }} — {{ v.created_at }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="data-reset-row">
|
||||
<label for="line_items_action">Line items</label>
|
||||
<select id="line_items_action" name="line_items_action">
|
||||
<option value="none" {% if selected_line_items_action == "none" %}selected{% endif %}>No change</option>
|
||||
<option value="reset" {% if selected_line_items_action == "reset" %}selected{% endif %}>Reset</option>
|
||||
<label for="line_item_restore_choice">Line items</label>
|
||||
<select id="line_item_restore_choice" name="line_item_restore_choice">
|
||||
<option value="none">No change</option>
|
||||
<option value="clear">Clear current line items</option>
|
||||
{% if document.line_item_set_versions is defined %}
|
||||
{% for v in (document.line_item_set_versions | sort(attribute='version_number', reverse=True)) %}
|
||||
<option value="version:{{ v.version_number }}">
|
||||
v{{ v.version_number }} — {{ v.created_at }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue