Spaces:
Sleeping
Sleeping
| <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,'&').replace(/</g,'<').replace(/>/g,'>'); | |
| } | |
| 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> |