InvoiceOCR / index.html
abinash73's picture
Add application file
f5858a9
Raw
History Blame Contribute Delete
27.6 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tax Invoice Extractor</title>
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;600;700;900&family=IBM+Plex+Mono:wght@300;400;500&display=swap" rel="stylesheet">
<style>
:root {
--ink: #1a1410;
--paper: #fdf8f0;
--warm: #f5ede0;
--accent: #b5451b;
--accent2: #2b6cb0;
--gold: #c8922a;
--muted: #8a7a68;
--border: #d4c4aa;
--card: #fffcf7;
--success: #276749;
--error: #c0392b;
--rule: #c8b89a;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: var(--paper);
color: var(--ink);
font-family: 'IBM Plex Mono', monospace;
min-height: 100vh;
}
/* Ledger lines */
body::before {
content: '';
position: fixed; inset: 0;
background-image: repeating-linear-gradient(
transparent, transparent 27px,
var(--rule) 27px, var(--rule) 28px
);
opacity: 0.18;
pointer-events: none; z-index: 0;
}
/* Red margin line */
body::after {
content: '';
position: fixed;
left: 72px; top: 0; bottom: 0;
width: 1.5px;
background: rgba(181,69,27,0.15);
pointer-events: none; z-index: 0;
}
.page {
position: relative; z-index: 1;
max-width: 960px;
margin: 0 auto;
padding: 56px 32px 100px 96px;
}
/* ── Header ── */
header { margin-bottom: 52px; animation: fadeUp 0.6s ease both; }
.stamp {
display: inline-block;
font-family: 'IBM Plex Mono', monospace;
font-size: 9px; font-weight: 500;
letter-spacing: 0.22em; text-transform: uppercase;
color: var(--accent);
border: 1.5px solid var(--accent);
padding: 3px 10px; border-radius: 2px;
margin-bottom: 14px;
opacity: 0.85;
}
header h1 {
font-family: 'Playfair Display', serif;
font-size: clamp(32px, 5.5vw, 56px);
font-weight: 900;
line-height: 1.05;
letter-spacing: -0.02em;
color: var(--ink);
}
header h1 em { color: var(--accent); font-style: normal; }
header p {
margin-top: 12px;
font-size: 12px; color: var(--muted);
line-height: 1.8; max-width: 520px;
}
/* ── API bar ── */
.api-bar {
display: flex; align-items: center; gap: 10px;
background: var(--warm); border: 1px solid var(--border);
border-radius: 3px; padding: 9px 14px;
margin-bottom: 28px;
animation: fadeUp 0.6s 0.1s ease both;
}
.api-bar label { font-size: 9px; letter-spacing: 0.2em; text-transform: uppercase; color: var(--muted); white-space: nowrap; }
.api-bar input { flex: 1; background: transparent; border: none; outline: none; font-family: 'IBM Plex Mono', monospace; font-size: 12px; color: var(--ink); }
.reset-btn { font-size: 9px; padding: 3px 9px; border: 1px solid var(--border); border-radius: 2px; background: transparent; cursor: pointer; color: var(--muted); transition: all .15s; font-family: 'IBM Plex Mono', monospace; }
.reset-btn:hover { border-color: var(--ink); color: var(--ink); }
/* ── Upload zone ── */
.upload-section { animation: fadeUp 0.6s 0.15s ease both; margin-bottom: 24px; }
.drop-zone {
border: 2px dashed var(--border); border-radius: 4px;
background: var(--card); padding: 52px 24px;
text-align: center; cursor: pointer;
transition: border-color .2s, background .2s; position: relative;
}
.drop-zone:hover, .drop-zone.dragover { border-color: var(--accent); background: #fff9f3; }
.drop-zone input[type=file] { position: absolute; inset: 0; opacity: 0; cursor: pointer; width: 100%; height: 100%; }
.drop-icon {
font-size: 42px; margin-bottom: 14px; display: block;
filter: grayscale(0.3);
}
.drop-zone h3 {
font-family: 'Playfair Display', serif;
font-size: 18px; font-weight: 700; margin-bottom: 6px;
}
.drop-zone p { font-size: 11px; color: var(--muted); line-height: 1.7; }
kbd { display: inline-block; font-family: 'IBM Plex Mono',monospace; font-size: 10px; background: var(--warm); border: 1px solid var(--border); border-radius: 2px; padding: 1px 5px; color: var(--muted); }
.format-badges { display: flex; justify-content: center; gap: 6px; margin-top: 12px; flex-wrap: wrap; }
.fmt-badge { font-size: 9px; letter-spacing: 0.14em; text-transform: uppercase; padding: 3px 9px; border-radius: 2px; border: 1px solid var(--border); color: var(--muted); background: var(--warm); font-family: 'IBM Plex Mono', monospace; }
.fmt-badge.pdf { border-color: var(--accent); color: var(--accent); background: #fdf0ea; }
/* preview strip */
#preview-strip { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 14px; }
.preview-thumb { position: relative; width: 96px; height: 72px; border-radius: 3px; overflow: hidden; border: 1.5px solid var(--border); animation: popIn .25s ease both; }
.preview-thumb img { width: 100%; height: 100%; object-fit: cover; }
.preview-thumb.pdf-thumb { display: flex; flex-direction: column; align-items: center; justify-content: center; background: #fdf0ea; border-color: var(--accent); }
.preview-thumb.pdf-thumb .pdf-icon { font-size: 24px; line-height: 1; }
.preview-thumb.pdf-thumb .pdf-label { font-size: 8px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--accent); margin-top: 5px; font-family: 'IBM Plex Mono', monospace; }
.preview-thumb .rm { position: absolute; top: 2px; right: 2px; width: 18px; height: 18px; background: rgba(0,0,0,.65); color: #fff; border: none; border-radius: 50%; font-size: 10px; cursor: pointer; display: flex; align-items: center; justify-content: center; z-index: 2; }
.preview-thumb .fname { position: absolute; bottom: 0; left: 0; right: 0; background: rgba(0,0,0,.5); color: #fff; font-size: 8px; padding: 2px 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* ── Submit button ── */
.submit-btn {
display: flex; align-items: center; gap: 10px;
padding: 13px 32px; background: var(--ink); color: var(--paper);
font-family: 'IBM Plex Mono', monospace; font-size: 12px; font-weight: 500;
letter-spacing: 0.12em; text-transform: uppercase;
border: none; border-radius: 3px; cursor: pointer;
transition: background .2s, transform .1s;
animation: fadeUp 0.6s 0.2s ease both;
}
.submit-btn:hover { background: var(--accent); }
.submit-btn:active { transform: scale(.98); }
.submit-btn:disabled { background: var(--muted); cursor: not-allowed; }
.submit-btn .spinner { width: 13px; height: 13px; border: 2px solid rgba(255,255,255,.3); border-top-color: #fff; border-radius: 50%; animation: spin .7s linear infinite; display: none; }
.submit-btn.loading .spinner { display: block; }
.submit-btn.loading .btn-text { opacity: .7; }
/* ── Status ── */
#status { margin-top: 14px; font-size: 11px; padding: 10px 14px; border-radius: 3px; display: none; font-family: 'IBM Plex Mono', monospace; }
#status.info { background: #e8f2ff; color: #1a4a8a; border-left: 3px solid #1a4a8a; }
#status.ok { background: #e8f5ee; color: var(--success); border-left: 3px solid var(--success); }
#status.err { background: #fdecea; color: var(--error); border-left: 3px solid var(--error); }
/* ── Results ── */
#results-section { margin-top: 56px; }
.results-rule {
display: flex; align-items: center; gap: 16px;
margin-bottom: 28px;
}
.results-rule span {
font-family: 'IBM Plex Mono', monospace;
font-size: 9px; font-weight: 500;
letter-spacing: 0.22em; text-transform: uppercase; color: var(--muted);
white-space: nowrap;
}
.results-rule::before, .results-rule::after {
content: ''; flex: 1; height: 1px; background: var(--border);
}
.result-card {
background: var(--card);
border: 1px solid var(--border);
border-top: 3px solid var(--accent);
border-radius: 4px; padding: 28px;
margin-bottom: 28px; position: relative;
animation: popIn .3s ease both;
box-shadow: 0 2px 12px rgba(0,0,0,0.04);
}
/* Invoice header block */
.inv-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 20px; flex-wrap: wrap; gap: 12px; }
.inv-number { font-family: 'Playfair Display', serif; font-size: 24px; font-weight: 700; color: var(--ink); }
.inv-number small { display: block; font-family: 'IBM Plex Mono', monospace; font-size: 10px; font-weight: 400; color: var(--muted); letter-spacing: 0.1em; text-transform: uppercase; margin-bottom: 2px; }
.inv-meta { text-align: right; }
.inv-meta .date { font-family: 'Playfair Display', serif; font-size: 15px; font-weight: 600; }
.inv-meta small { display: block; font-size: 10px; color: var(--muted); letter-spacing: 0.08em; }
.inv-total { font-family: 'Playfair Display', serif; font-size: 28px; font-weight: 900; color: var(--accent); text-align: right; margin-top: 4px; }
/* Section titles */
.sec { margin: 22px 0 12px; padding-bottom: 5px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 10px; }
.sec span { font-size: 9px; font-weight: 500; letter-spacing: 0.2em; text-transform: uppercase; color: var(--gold); }
.sec::after { content: ''; flex: 1; height: 1px; }
/* Party block */
.party-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.party-box { background: var(--warm); border-radius: 3px; padding: 14px 16px; }
.party-box .party-label { font-size: 9px; letter-spacing: 0.18em; text-transform: uppercase; color: var(--muted); margin-bottom: 6px; }
.party-box .party-name { font-family: 'Playfair Display', serif; font-size: 15px; font-weight: 700; margin-bottom: 4px; }
.party-box .party-detail { font-size: 11px; color: var(--muted); line-height: 1.7; }
.party-box .gstin { font-family: 'IBM Plex Mono', monospace; font-size: 11px; font-weight: 500; color: var(--accent2); margin-top: 5px; }
/* Fields grid */
.fields-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px,1fr)); gap: 10px 20px; }
.field { display: flex; flex-direction: column; gap: 3px; }
.field .lbl { font-size: 9px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--muted); }
.field .val { font-size: 12px; color: var(--ink); word-break: break-word; line-height: 1.5; }
.field .val.empty { color: var(--border); font-style: italic; font-size: 11px; }
.field .val.hi { color: var(--accent); font-weight: 500; }
.field .val.blue { color: var(--accent2); font-weight: 500; }
/* Items table */
.items-table { width: 100%; border-collapse: collapse; font-size: 11px; margin-top: 6px; }
.items-table th { background: var(--warm); font-size: 9px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted); padding: 7px 10px; text-align: left; border-bottom: 1px solid var(--border); font-weight: 500; }
.items-table td { padding: 8px 10px; border-bottom: 1px solid #ede8df; vertical-align: top; line-height: 1.5; }
.items-table tr:last-child td { border-bottom: none; }
.items-table tr:hover td { background: var(--warm); }
.items-table .amt { text-align: right; font-weight: 500; }
.no-items { font-size: 11px; color: var(--border); font-style: italic; padding: 10px 0; }
/* Tax summary */
.tax-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px,1fr)); gap: 10px; }
.tax-box { background: var(--warm); border-radius: 3px; padding: 10px 14px; }
.tax-box .t-lbl { font-size: 9px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted); margin-bottom: 3px; }
.tax-box .t-val { font-size: 14px; font-family: 'Playfair Display', serif; font-weight: 700; color: var(--ink); }
.tax-box .t-val.total { color: var(--accent); font-size: 18px; }
/* HSN table */
.hsn-table { width: 100%; border-collapse: collapse; font-size: 11px; margin-top: 6px; }
.hsn-table th { background: var(--warm); font-size: 9px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted); padding: 6px 8px; text-align: left; border-bottom: 1px solid var(--border); font-weight: 500; }
.hsn-table td { padding: 7px 8px; border-bottom: 1px solid #ede8df; }
.hsn-table tr:last-child td { border-bottom: none; }
/* Amount words */
.amount-words { background: var(--warm); border-left: 3px solid var(--gold); padding: 10px 14px; font-size: 12px; line-height: 1.6; border-radius: 0 3px 3px 0; margin-top: 4px; }
/* Copy row */
.copy-row { margin-top: 20px; padding-top: 14px; border-top: 1px solid var(--border); display: flex; gap: 10px; flex-wrap: wrap; }
.copy-btn { font-family: 'IBM Plex Mono', monospace; font-size: 9px; padding: 5px 12px; border: 1px solid var(--border); border-radius: 2px; background: transparent; cursor: pointer; color: var(--muted); transition: all .15s; letter-spacing: 0.1em; text-transform: uppercase; }
.copy-btn:hover { border-color: var(--ink); color: var(--ink); }
.copy-btn.copied { border-color: var(--success); color: var(--success); }
.page-badge { display: inline-flex; align-items: center; gap: 5px; font-size: 9px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted); background: var(--warm); border: 1px solid var(--border); padding: 2px 8px; border-radius: 2px; margin-left: 10px; vertical-align: middle; }
@keyframes fadeUp { from{opacity:0;transform:translateY(16px)} to{opacity:1;transform:translateY(0)} }
@keyframes popIn { from{opacity:0;transform:scale(.97)} to{opacity:1;transform:scale(1)} }
@keyframes spin { to{transform:rotate(360deg)} }
@keyframes pasteFlash { 0%{border-color:var(--accent);background:#fff5ee} 100%{border-color:var(--border);background:var(--card)} }
.drop-zone.paste-flash { animation: pasteFlash .6s ease both; }
@media (max-width: 600px) {
.page { padding-left: 24px; }
body::after { display: none; }
.party-grid { grid-template-columns: 1fr; }
.inv-header { flex-direction: column; }
.inv-meta { text-align: left; }
}
</style>
</head>
<body>
<div class="page">
<header>
<div class="stamp">NVIDIA OCR · NeMo + Nemotron</div>
<h1>Tax Invoice<br><em>Extractor</em></h1>
<p>Upload a GST Tax Invoice image or PDF to extract all structured data — vendor, buyer, line items, HSN summary, GSTIN, taxes, and totals.</p>
</header>
<div class="api-bar">
<label>Endpoint</label>
<input type="text" id="api-url" value="/extract-invoice" spellcheck="false">
<button class="reset-btn" onclick="document.getElementById('api-url').value='/extract-invoice'">Reset</button>
</div>
<div class="upload-section">
<div class="drop-zone" id="drop-zone">
<input type="file" id="file-input" accept="image/jpeg,image/png,image/webp,application/pdf">
<span class="drop-icon">🧾</span>
<h3>Drop, paste, or click to browse</h3>
<p><kbd>Ctrl+V</kbd> / <kbd>⌘V</kbd> to paste a screenshot directly</p>
<div class="format-badges">
<span class="fmt-badge">JPG</span>
<span class="fmt-badge">PNG</span>
<span class="fmt-badge">WEBP</span>
<span class="fmt-badge pdf">PDF</span>
</div>
</div>
<div id="preview-strip"></div>
</div>
<button class="submit-btn" id="submit-btn" onclick="doSubmit()">
<div class="spinner"></div>
<span class="btn-text">Extract Invoice</span>
</button>
<div id="status"></div>
<div id="results-section" style="display:none">
<div class="results-rule"><span>Extracted Data</span></div>
<div id="results-container"></div>
</div>
</div>
<script>
let files = [];
/* ── Drag & drop ── */
const dz = document.getElementById('drop-zone');
dz.addEventListener('dragover', e => { e.preventDefault(); dz.classList.add('dragover'); });
dz.addEventListener('dragleave', () => dz.classList.remove('dragover'));
dz.addEventListener('drop', e => { e.preventDefault(); dz.classList.remove('dragover'); addFiles([...e.dataTransfer.files]); });
document.getElementById('file-input').addEventListener('change', e => { addFiles([...e.target.files]); e.target.value = ''; });
/* ── Clipboard paste ── */
document.addEventListener('paste', e => {
const imgs = [...(e.clipboardData?.items||[])].filter(i => i.type.startsWith('image/'));
if (!imgs.length) return;
e.preventDefault();
const pasted = imgs.map(i => i.getAsFile()).filter(Boolean);
addFiles(pasted);
dz.classList.remove('paste-flash'); void dz.offsetWidth; dz.classList.add('paste-flash');
dz.addEventListener('animationend', () => dz.classList.remove('paste-flash'), {once:true});
setStatus(`✓ ${pasted.length} image${pasted.length>1?'s':''} pasted from clipboard.`, 'ok');
});
function isAccepted(f) {
return f.type.startsWith('image/') || f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
}
function isPDF(f) {
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
}
function addFiles(newFiles) {
const accepted = newFiles.filter(isAccepted);
const rejected = newFiles.length - accepted.length;
if (!accepted.length) { setStatus('No supported file. Use JPG, PNG, WEBP, or PDF.', 'err'); return; }
if (rejected > 0) setStatus(`${rejected} unsupported file(s) skipped.`, 'info');
files = accepted.slice(0, 1); // single upload only
renderPreviews();
}
function renderPreviews() {
const strip = document.getElementById('preview-strip');
strip.innerHTML = '';
files.forEach((f, i) => {
const div = document.createElement('div');
const name = f.name || `file-${i+1}`;
if (isPDF(f)) {
div.className = 'preview-thumb pdf-thumb';
div.innerHTML = `<span class="pdf-icon">📄</span><span class="pdf-label">PDF</span>
<button class="rm" onclick="removeFile(${i})" title="Remove">✕</button>
<div class="fname">${esc(name)}</div>`;
} else {
const url = URL.createObjectURL(f);
div.className = 'preview-thumb';
div.innerHTML = `<img src="${url}" alt="preview">
<button class="rm" onclick="removeFile(${i})" title="Remove">✕</button>
<div class="fname">${esc(name)}</div>`;
}
strip.appendChild(div);
});
}
function removeFile(i) { files.splice(i,1); renderPreviews(); }
function setStatus(msg, type) {
const el = document.getElementById('status');
el.textContent = msg; el.className = type; el.style.display = msg?'block':'none';
}
/* ── Submit ── */
async function doSubmit() {
if (!files.length) { setStatus('Please select or paste an invoice image or PDF.', 'err'); return; }
const url = document.getElementById('api-url').value.trim();
const btn = document.getElementById('submit-btn');
btn.disabled = true; btn.classList.add('loading');
btn.querySelector('.btn-text').textContent = 'Extracting…';
setStatus('Sending to OCR pipeline…', 'info');
document.getElementById('results-section').style.display = 'none';
document.getElementById('results-container').innerHTML = '';
try {
const fd = new FormData();
fd.append('file', files[0], files[0].name || 'invoice.pdf');
const res = await fetch(url, { method: 'POST', body: fd });
let data;
try { data = await res.json(); } catch { throw new Error('Server returned non-JSON.'); }
if (!res.ok) throw new Error(data.detail || `HTTP ${res.status}`);
const pages = data.source_pages || 1;
setStatus(`✓ Extraction complete · ${pages} page${pages>1?'s':''} processed`, 'ok');
renderResult(data);
} catch(err) {
setStatus('Error: ' + err.message, 'err');
} finally {
btn.disabled = false; btn.classList.remove('loading');
btn.querySelector('.btn-text').textContent = 'Extract Invoice';
}
}
/* ── Render ── */
function renderResult(d) {
const c = document.getElementById('results-container');
c.innerHTML = '';
const card = document.createElement('div');
card.className = 'result-card';
/* Items table */
const itemRows = (d.items || []).map(it => `
<tr>
<td>${esc(it.sl_no)}</td>
<td>${esc(it.description)}${it.batch?`<br><span style="color:var(--muted);font-size:10px">Batch: ${esc(it.batch)}</span>`:''}</td>
<td>${esc(it.hsn_sac)}</td>
<td>${esc(it.quantity)}</td>
<td>${esc(it.rate)}</td>
<td class="amt">${esc(it.amount)}</td>
</tr>`).join('');
/* HSN table */
const hsnRows = (d.hsn_summary || []).map(h => `
<tr>
<td>${esc(h.hsn_sac)}</td>
<td>${esc(h.taxable_value)}</td>
<td>${esc(h.cgst_rate)}</td>
<td>${esc(h.cgst_amount)}</td>
<td>${esc(h.sgst_rate)}</td>
<td>${esc(h.sgst_amount)}</td>
<td style="font-weight:500">${esc(h.total_tax)}</td>
</tr>`).join('');
const pages = d.source_pages || 1;
card.innerHTML = `
<!-- Invoice Header -->
<div class="inv-header">
<div>
<div class="inv-number">
<small>Invoice Number</small>
${esc(d.invoice_number)||'<span style="color:var(--muted);font-size:14px">Not found</span>'}
${pages>1?`<span class="page-badge">📄 ${pages} pages</span>`:''}
</div>
${d.eway_bill_number?`<div style="font-size:11px;color:var(--muted);margin-top:4px">e-Way Bill: <span style="color:var(--ink)">${esc(d.eway_bill_number)}</span></div>`:''}
</div>
<div class="inv-meta">
<small>Date</small>
<div class="date">${esc(d.invoice_date)||'—'}</div>
<div class="inv-total">${d.grand_total?'₹'+esc(d.grand_total):'—'}</div>
</div>
</div>
<!-- Vendor + Buyer -->
<div class="sec"><span>Parties</span></div>
<div class="party-grid">
<div class="party-box">
<div class="party-label">Vendor / Seller</div>
<div class="party-name">${esc(d.vendor_name)||'—'}</div>
<div class="party-detail">
${d.vendor_address?esc(d.vendor_address)+'<br>':''}
${d.vendor_state?esc(d.vendor_state)+'<br>':''}
${d.vendor_email?`<a href="mailto:${esc(d.vendor_email)}" style="color:var(--accent2)">${esc(d.vendor_email)}</a>`:''}
</div>
${d.vendor_gstin?`<div class="gstin">GSTIN: ${esc(d.vendor_gstin)}</div>`:''}
</div>
<div class="party-box">
<div class="party-label">Buyer / Customer</div>
<div class="party-name">${esc(d.buyer_name)||'—'}</div>
<div class="party-detail">
${d.buyer_address?esc(d.buyer_address)+'<br>':''}
${d.buyer_state?esc(d.buyer_state):''}
</div>
${d.buyer_gstin?`<div class="gstin">GSTIN: ${esc(d.buyer_gstin)}</div>`:''}
</div>
</div>
<!-- Reference fields -->
<div class="sec"><span>Reference Details</span></div>
<div class="fields-grid">
${fld('Mode of Payment', d.mode_of_payment)}
${fld('Supplier Ref', d.supplier_ref)}
${fld('Buyer Order No.', d.buyer_order_number)}
${fld('Dispatch Doc No.', d.dispatch_document_number)}
${fld('Dispatched Through', d.dispatched_through)}
${fld('Destination', d.destination)}
${fld('Delivery Note', d.delivery_note)}
</div>
<!-- Line items -->
<div class="sec"><span>Line Items</span></div>
${itemRows ? `
<div style="overflow-x:auto">
<table class="items-table">
<thead><tr>
<th>#</th><th>Description</th><th>HSN/SAC</th>
<th>Qty</th><th>Rate</th><th style="text-align:right">Amount</th>
</tr></thead>
<tbody>${itemRows}</tbody>
</table>
</div>` : '<p class="no-items">No line items found</p>'}
<!-- Tax summary -->
<div class="sec"><span>Tax Summary</span></div>
<div class="tax-grid">
${taxBox('Taxable Value', d.taxable_value)}
${taxBox('CGST', d.cgst_amount, d.cgst_rate)}
${taxBox('SGST', d.sgst_amount, d.sgst_rate)}
${d.igst_amount ? taxBox('IGST', d.igst_amount, d.igst_rate) : ''}
${d.output_cgst ? taxBox('Output CGST', d.output_cgst) : ''}
${d.output_sgst ? taxBox('Output SGST', d.output_sgst) : ''}
${taxBox('Total Tax', d.total_tax_amount, '', true)}
${taxBox('Grand Total', d.grand_total, '', true)}
</div>
${d.amount_in_words ? `
<div class="sec"><span>Amount in Words</span></div>
<div class="amount-words">${esc(d.amount_in_words)}</div>` : ''}
${d.tax_amount_in_words ? `
<div style="margin-top:8px"><div class="amount-words" style="border-color:var(--muted)">${esc(d.tax_amount_in_words)}</div></div>` : ''}
<!-- HSN Summary -->
${hsnRows ? `
<div class="sec"><span>HSN/SAC Summary</span></div>
<div style="overflow-x:auto">
<table class="hsn-table">
<thead><tr>
<th>HSN/SAC</th><th>Taxable Value</th>
<th>CGST%</th><th>CGST Amt</th>
<th>SGST%</th><th>SGST Amt</th><th>Total Tax</th>
</tr></thead>
<tbody>${hsnRows}</tbody>
</table>
</div>` : ''}
<!-- Declaration -->
${d.declaration ? `
<div class="sec"><span>Declaration</span></div>
<div style="font-size:11px;color:var(--muted);line-height:1.7">${esc(d.declaration)}</div>` : ''}
${d.authorised_signatory ? `
<div style="margin-top:12px;font-size:11px;color:var(--muted)">
Authorised Signatory: <span style="color:var(--ink)">${esc(d.authorised_signatory)}</span>
</div>` : ''}
<!-- Copy -->
<div class="copy-row">
<button class="copy-btn" onclick="copyJSON(this)">Copy JSON</button>
<button class="copy-btn" onclick="copyFlat(this)">Copy Key Fields</button>
</div>`;
c.appendChild(card);
window._result = d;
document.getElementById('results-section').style.display = 'block';
}
function fld(label, val, hi=false) {
const empty = !val || !String(val).trim();
return `<div class="field">
<span class="lbl">${label}</span>
<span class="val ${empty?'empty':hi?'hi':''}">${empty?'—':esc(String(val))}</span>
</div>`;
}
function taxBox(label, val, rate='', isTotal=false) {
const empty = !val || !String(val).trim();
return `<div class="tax-box">
<div class="t-lbl">${label}${rate?` <span style="font-size:9px">(${esc(rate)})</span>`:''}</div>
<div class="t-val${isTotal?' total':''}">${empty?'—': '₹'+esc(String(val))}</div>
</div>`;
}
function esc(s) {
return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
async function safeCopy(btn, text, label) {
try { await navigator.clipboard.writeText(text); }
catch {
const ta = document.createElement('textarea');
ta.value = text; ta.style.cssText = 'position:fixed;opacity:0';
document.body.appendChild(ta); ta.select(); document.execCommand('copy');
document.body.removeChild(ta);
}
btn.textContent = '✓ Copied!'; btn.classList.add('copied');
setTimeout(() => { btn.textContent = label; btn.classList.remove('copied'); }, 1800);
}
function copyJSON(btn) {
safeCopy(btn, JSON.stringify(window._result, null, 2), 'Copy JSON');
}
function copyFlat(btn) {
const d = window._result;
const lines = [
`Invoice No: ${d.invoice_number}`,
`Date: ${d.invoice_date}`,
`Vendor: ${d.vendor_name}`,
`Vendor GSTIN: ${d.vendor_gstin}`,
`Buyer: ${d.buyer_name}`,
`Buyer GSTIN: ${d.buyer_gstin}`,
`Taxable Value: ${d.taxable_value}`,
`CGST: ${d.cgst_amount}`,
`SGST: ${d.sgst_amount}`,
`Total Tax: ${d.total_tax_amount}`,
`Grand Total: ${d.grand_total}`,
`Amount in Words: ${d.amount_in_words}`,
].join('\n');
safeCopy(btn, lines, 'Copy Key Fields');
}
</script>
</body>
</html>