InvoiceParser/wwwroot/archive.html

328 lines
12 KiB
HTML
Raw Normal View History

2026-09-16 20:09:19 +00:00
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Invoice Archive</title>
<link rel="stylesheet" href="css/style.css">
<script src="js/jquery-3.5.1.min.js"></script>
</head>
<body class="archive-page">
<h1>
Invoice Archive
<a href="index.html">+ New Invoice</a>
<a href="pending.html">Pending</a>
<a href="archive.html">Archive</a>
</h1>
<div id="filterPanel">
<form id="filterForm" onsubmit="return false">
<div class="filter-field">
<label>Vendor</label>
<select name="vendor" id="fVendor" style="width:210px">
<option value="">All Vendors</option>
</select>
</div>
<div class="filter-field">
<label>Invoice #</label>
<input name="inv" id="fInv" style="width:115px">
</div>
<div class="filter-field">
<label>PO #</label>
<input name="po" id="fPo" style="width:115px">
</div>
<div class="filter-field">
<label>From</label>
<input type="date" name="date_from" id="fDateFrom">
</div>
<div class="filter-field">
<label>To</label>
<input type="date" name="date_to" id="fDateTo">
</div>
<div class="filter-field">
<label>Min $</label>
<input name="amt_min" id="fAmtMin" style="width:80px">
</div>
<div class="filter-field">
<label>Max $</label>
<input name="amt_max" id="fAmtMax" style="width:80px">
</div>
<div class="filter-field">
<label>UPC / Cert / Product</label>
<input name="product" id="fProduct" style="width:180px">
</div>
<div class="filter-actions">
<button class="filter-btn primary" id="btnFilter" type="submit">Filter</button>
<button class="filter-btn" id="btnReset" type="button">Reset</button>
</div>
</form>
</div>
<div id="listPanel">
<table>
<thead>
<tr>
<th>Date</th>
<th>Vendor</th>
<th>Invoice #</th>
<th style="text-align:right">Total</th>
</tr>
</thead>
<tbody id="archiveBody"></tbody>
</table>
</div>
<div id="detailLayout">
<div id="archiveLeft">
<div id="pdfPlaceholder">Select an invoice above to preview.</div>
</div>
<div id="archiveRight">
<div id="detailContent" style="display:none">
<div class="detail-top">
<div class="info-card">
<h2>Invoice Header</h2>
<div class="info-row"><span class="info-label">Vendor</span><span class="info-value" id="dVendor"></span></div>
<div class="info-row"><span class="info-label">Invoice #</span><span class="info-value" id="dInvNum"></span></div>
<div class="info-row"><span class="info-label">Date</span><span class="info-value" id="dDate"></span></div>
<div class="info-row"><span class="info-label">PO #</span><span class="info-value" id="dPO"></span></div>
<div class="info-row"><span class="info-label">Total</span><span class="info-value" id="dTotal"></span></div>
</div>
<div class="info-card" id="batchCard">
<h2>Cost Batch <span id="bFlagged" class="batch-flag" style="display:none"></span></h2>
<div id="batchNone" class="batch-none">No cost changes - this invoice created no batch.</div>
<div id="batchBody" style="display:none">
<div class="info-row"><span class="info-label">Batch #</span><span class="info-value" id="bNo"></span></div>
<div class="info-row"><span class="info-label">Created</span><span class="info-value" id="bCreated"></span></div>
<div class="info-row"><span class="info-label">Items</span><span class="info-value" id="bCount"></span></div>
<div class="batch-scroll">
<table id="batchTable">
<thead>
<tr>
<th>UPC</th>
<th>Description</th>
<th title="Unit cost this batch sets">New</th>
<th title="Unit cost on the product file right now - equal to New once the batch is applied">Now</th>
<th>Retail</th>
<th title="Gross margin at the new cost">GM %</th>
</tr>
</thead>
<tbody id="batchBodyRows"></tbody>
</table>
</div>
</div>
</div>
</div>
<div class="info-card">
<h2>Line Items</h2>
<div style="overflow-x:auto">
<table id="itemsTable">
<thead>
<tr>
<th>UPC</th>
<th>Description</th>
<th>Cert</th>
<th>Qty</th>
<th>Case Cost</th>
<th>Total</th>
<th>Size</th>
<th>UOM</th>
</tr>
</thead>
<tbody id="itemsBody"></tbody>
</table>
</div>
</div>
</div>
<div id="noSelection" style="color:#777;padding:40px 20px;text-align:center">Select an invoice above.</div>
</div>
</div>
<script>
$(document).ready(function () {
loadVendors()
loadList()
$('#btnFilter').on('click', loadList)
$('#btnReset').on('click', function () {
$('#filterForm')[0].reset()
loadList()
})
})
async function loadVendors() {
try {
var vendors = await (await fetch('/api/vendors')).json()
if (vendors.error) throw new Error(vendors.error)
var sel = $('#fVendor')
vendors.forEach(function (v) {
sel.append('<option value="' + v.id + '">' + esc(v.name) + '</option>')
})
} catch (e) { showDbError('Vendor list failed to load: ' + e.message) }
}
async function loadList() {
var params = new URLSearchParams({
vendor: $('#fVendor').val() || '',
inv: $('#fInv').val() || '',
po: $('#fPo').val() || '',
date_from: $('#fDateFrom').val() || '',
date_to: $('#fDateTo').val() || '',
amt_min: $('#fAmtMin').val() || '',
amt_max: $('#fAmtMax').val() || '',
product: $('#fProduct').val() || ''
})
try {
var list = await (await fetch('/api/archive?' + params)).json()
if (list.error) throw new Error(list.error)
var body = $('#archiveBody')
body.empty()
list.forEach(function (r) {
body.append('<tr class="inv-row" data-inv="' + esc(r.invoice_number) + '" data-vendor="' + r.vendor + '">' +
'<td>' + esc(r.invoice_date) + '</td>' +
'<td>' + esc(r.vendor_name) + '</td>' +
'<td>' + esc(r.invoice_number) + '</td>' +
'<td style="text-align:right">$' + (parseFloat(r.vendor_total) || 0).toFixed(2) + '</td>' +
'</tr>')
})
if (!list.length) {
body.append('<tr><td colspan="4" style="color:#777;text-align:center;padding:12px">No invoices found.</td></tr>')
}
} catch (e) {
showDbError('Could not load archive list: ' + e.message)
}
}
function showDbError(msg) {
var el = $('#dbWarning')
if (!el.length) el = $('<div id="dbWarning" class="db-warning"></div>').insertAfter('h1')
el.text(msg).show()
}
$(document).on('click', '.inv-row', function () {
$('.inv-row').removeClass('selected')
$(this).addClass('selected')
loadDetail($(this).data('inv'), $(this).data('vendor'))
})
async function loadDetail(inv, vendor) {
$('#noSelection').hide()
$('#detailContent').show()
$('#itemsBody').empty()
setPdfStatus('Loading...', true)
var q = 'inv=' + encodeURIComponent(inv) + '&v=' + encodeURIComponent(vendor)
try {
var detail = await (await fetch('/api/archive/detail?' + q)).json()
if (detail.error) { setPdfStatus(detail.error, false); return }
renderHeader(detail)
renderItems(detail.items || [])
renderBatch(detail.batch)
} catch (e) {
setPdfStatus('Failed to load invoice: ' + e.message, false)
return
}
// Preview pages are rendered server-side; images are just <img> tags.
try {
var info = await (await fetch('/api/archive/preview?' + q)).json()
if (info.error) { setPdfStatus(info.error, false); return }
var left = $('#archiveLeft')
left.find('img').remove()
$('#pdfPlaceholder').hide()
// version changes only when the archived file does, so revisiting an
// invoice reuses the browser-cached pages instead of refetching them.
for (var p = 1; p <= (info.pages || 1); p++) {
left.append($('<img>').attr('src', '/api/archive/preview/' + p + '?' + q + '&ver=' + (info.version || 0)))
}
} catch (e) {
setPdfStatus('Could not load file.', false)
}
}
function renderHeader(d) {
$('#dVendor').text(d.vendor_name || '-')
$('#dInvNum').text(d.invoice_number || '-')
$('#dDate').text(d.invoice_date || '-')
$('#dPO').text(d.po_number || '-')
$('#dTotal').text(d.vendor_total != null ? '$' + parseFloat(d.vendor_total).toFixed(2) : '-')
}
function renderItems(items) {
var tbody = $('#itemsBody')
tbody.empty()
if (!items.length) {
tbody.append('<tr><td colspan="8" style="color:#777;text-align:center;padding:8px">No line items recorded.</td></tr>')
return
}
items.forEach(function (it) {
tbody.append('<tr>' +
'<td>' + esc(it.upc) + '</td>' +
'<td>' + esc(it.desc) + '</td>' +
'<td>' + esc(it.cert) + '</td>' +
'<td>' + (it.qty != null ? it.qty : '') + '</td>' +
'<td>' + (it.cost != null ? '$' + parseFloat(it.cost).toFixed(2) : '') + '</td>' +
'<td>' + (it.total != null ? '$' + parseFloat(it.total).toFixed(2) : '') + '</td>' +
'<td>' + esc(it.size) + '</td>' +
'<td>' + esc(it.uom) + '</td>' +
'</tr>')
})
}
// The price/cost batch this invoice produced. Null when nothing moved, which
// is the normal case for a re-order at the same cost.
function renderBatch(b) {
var rows = $('#batchBodyRows').empty()
if (!b || !b.batch_no) {
$('#batchBody').hide()
$('#bFlagged').hide()
$('#batchNone').show()
return
}
$('#batchNone').hide()
$('#batchBody').show()
$('#bNo').text(b.batch_no)
$('#bCreated').text(b.created || '-')
var items = b.items || []
var threshold = b.margin_threshold != null ? b.margin_threshold : 15
$('#bCount').text(items.length)
var flagged = 0
items.forEach(function (it) {
var low = it.margin != null && it.margin < threshold
if (low) flagged++
rows.append('<tr' + (low ? ' class="batch-low"' : '') + '>' +
'<td>' + esc(it.upc) + '</td>' +
'<td>' + esc(it.desc) + '</td>' +
'<td>' + money(it.cost) + '</td>' +
'<td>' + money(it.current_cost) + '</td>' +
'<td>' + money(it.retail) + '</td>' +
'<td>' + (it.margin != null ? it.margin.toFixed(1) + '%' : '-') + '</td>' +
'</tr>')
})
if (!items.length) {
rows.append('<tr><td colspan="6" style="color:#777;text-align:center;padding:8px">Batch has no rows.</td></tr>')
}
$('#bFlagged')
.text(flagged ? flagged + ' below ' + threshold + '% margin' : '')
.toggle(flagged > 0)
}
function money(v) {
return v != null && v !== '' ? '$' + parseFloat(v).toFixed(3).replace(/0$/, '') : '-'
}
function setPdfStatus(msg, loading) {
$('#archiveLeft').find('img').remove()
$('#pdfPlaceholder').text(msg).css('color', loading ? '#ffee88' : '#888').show()
}
function esc(s) {
return String(s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
}
</script>
</body>
</html>