InvoiceParser/wwwroot/pending.html

216 lines
7.9 KiB
HTML
Raw Permalink Normal View History

2026-09-16 20:09:19 +00:00
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Pending Invoices</title>
<link rel="stylesheet" href="css/style.css">
<script src="js/jquery-3.5.1.min.js"></script>
</head>
<body>
<h1>
Pending Invoices
2026-09-30 18:50:04 +00:00
<a href="index.html">Home / Upload</a>
<a href="pending.html" class="active">Pending</a>
2026-09-16 20:09:19 +00:00
<a href="archive.html">Archive</a>
2026-09-30 18:50:04 +00:00
<a href="changes.html">Changes</a>
2026-09-16 20:09:19 +00:00
</h1>
<div id="content">
<div id="statusBar"></div>
<div id="emptyState">No pending invoices.</div>
<table id="pendingTable" class="list" style="display:none">
<thead>
<tr>
<th>Updated</th>
<th>Vendor</th>
<th>Invoice #</th>
<th>Date</th>
<th>PO #</th>
<th>Total</th>
<th>Lines</th>
<th>Status</th>
<th>Source</th>
<th></th>
</tr>
</thead>
<tbody id="pendingBody"></tbody>
</table>
</div>
<script>
$(document).ready(function () {
loadPending()
connectEvents()
})
// Live refresh whenever the background worker changes any item's stage.
function connectEvents() {
try {
var es = new EventSource('/api/events')
var refreshTimer = null
es.onmessage = function (e) {
var msg
try { msg = JSON.parse(e.data) } catch (err) { return }
if (msg.type !== 'stage') return
clearTimeout(refreshTimer)
refreshTimer = setTimeout(loadPending, 400) // coalesce bursts
}
} catch (e) { console.warn('SSE unavailable:', e) }
}
async function loadPending() {
try {
var resp = await fetch('/api/pending')
var list = await resp.json()
list.sort(function (a, b) { return String(b.updated_at || '').localeCompare(String(a.updated_at || '')) })
var body = $('#pendingBody')
if (!list.length) {
body.empty()
$('#pendingTable').hide()
$('#emptyState').show()
setStatus('', '')
return
}
var states = await Promise.all(list.map(function (rec) {
return fetch('/api/pending/' + encodeURIComponent(rec.id))
.then(function (r) { return r.json() })
.catch(function () { return { error: true } })
}))
body.empty()
list.forEach(function (rec, i) {
if (!states[i] || (states[i].error && !states[i].stage)) return
body.append(buildRow(rec, states[i]))
})
$('#emptyState').toggle(body.children().length === 0)
$('#pendingTable').toggle(body.children().length > 0)
var active = $('#pendingBody tr').filter(function () {
var s = $(this).data('stage'); return s === 'queued' || s === 'queue' || s === 'processing'
}).length
if (active) setStatus(active + ' invoice(s) queued/processing in the background - this list refreshes automatically. You can leave this page.', 'loading')
else setStatus('', '')
} catch (e) {
setStatus('Could not load pending invoices: ' + e.message, 'error')
}
}
function buildRow(rec, state) {
var h = state.header || {}
var src = state.source || {}
var vendor = h.vendor_name || h.vendor_hint || ''
if (vendor === '-- Select Vendor --') vendor = ''
var stage = state.stage || 'pending'
var stageLabel = stage.replace(/_/g, ' ')
if (stage === 'error' && state.error) stageLabel += ': ' + state.error
var resumeLabel = (stage === 'parsed') ? 'Review'
: (stage === 'queued' || stage === 'processing') ? 'Open'
: 'Resume'
var parseBtn = (stage === 'queued' || stage === 'error')
? '<button class="btn btn-process btn-parsenow" data-id="' + esc(rec.id) + '">Parse now</button>'
: ''
return '<tr data-id="' + esc(rec.id) + '" data-stage="' + esc(stage) + '">' +
'<td>' + esc(formatDate(rec.updated_at || state.updated_at)) + '</td>' +
'<td>' + esc(vendor || '-') + '</td>' +
'<td>' + esc(h.invoice_number || '-') + '</td>' +
'<td>' + esc(h.invoice_date || '-') + '</td>' +
'<td>' + esc(h.po_number || '-') + '</td>' +
'<td>' + esc(h.invoice_total ? '$' + parseFloat(h.invoice_total).toFixed(2) : '-') + '</td>' +
'<td>' + ((state.line_items || []).length) + '</td>' +
'<td><span class="stage stage-' + esc(stage) + '">' + esc(stageLabel) + '</span></td>' +
'<td class="muted">' + esc(src.file_name || ('pending_' + rec.id)) + '</td>' +
'<td style="white-space:nowrap">' +
'<a class="btn" href="index.html?pending=' + encodeURIComponent(rec.id) + '">' + resumeLabel + '</a>' +
parseBtn +
'<button class="btn btn-restore" data-id="' + esc(rec.id) + '">Restore Defaults</button>' +
'<button class="btn btn-delete" data-id="' + esc(rec.id) + '">Delete</button>' +
'</td>' +
'</tr>'
}
$(document).on('click', '.btn-parsenow', async function () {
var btn = $(this)
var id = btn.data('id')
btn.text('Queued...').prop('disabled', true)
try {
var resp = await fetch('/api/pending/' + encodeURIComponent(id) + '/parse', { method: 'POST' })
var result = await resp.json()
if (!result.success) throw new Error(result.error || 'parse failed')
setStatus('Parsing in the background - this list updates automatically.', 'loading')
} catch (e) {
setStatus('Parse failed: ' + e.message, 'error')
}
loadPending()
})
$(document).on('click', '.btn-delete', async function () {
var id = $(this).data('id')
if (!confirm('Delete this pending invoice?')) return
try {
var resp = await fetch('/api/pending/' + encodeURIComponent(id), { method: 'DELETE' })
var result = await resp.json()
if (!result.success) throw new Error(result.error || 'delete failed')
$('tr[data-id="' + id + '"]').remove()
$('#emptyState').toggle($('#pendingBody').children().length === 0)
$('#pendingTable').toggle($('#pendingBody').children().length > 0)
} catch (e) {
setStatus('Delete failed: ' + e.message, 'error')
}
})
$(document).on('click', '.btn-restore', async function () {
var id = $(this).data('id')
if (!confirm('Restore this pending invoice to its original parsed defaults?')) return
try {
var stateResp = await fetch('/api/pending/' + encodeURIComponent(id))
var state = await stateResp.json()
if (state.error && !state.stage) throw new Error(state.error)
if (!state.defaults || !state.defaults.header || !state.defaults.line_items) {
throw new Error('This draft does not have saved defaults.')
}
state.header = state.defaults.header
state.line_items = state.defaults.line_items
state.po_compare_data = state.defaults.po_compare_data || null
state.stage = 'review'
state.updated_at = new Date().toISOString()
var saveResp = await fetch('/api/pending/' + encodeURIComponent(id) + '/state', {
method: 'POST',
body: JSON.stringify(state),
headers: { 'Content-Type': 'application/json; charset=utf-8' }
})
var result = await saveResp.json()
if (!result.success) throw new Error(result.error || 'restore failed')
await loadPending()
} catch (e) {
setStatus('Restore failed: ' + e.message, 'error')
}
})
function setStatus(msg, type) {
var el = $('#statusBar')
el.text(msg).removeClass('status-loading status-error status-success')
if (msg) el.addClass('status-' + type).show()
else el.hide()
}
function formatDate(s) {
if (!s) return ''
var d = new Date(s)
if (!isNaN(d.getTime())) return d.toLocaleString()
return String(s).replace('T', ' ')
}
function esc(s) {
return String(s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
}
</script>
</body>
</html>