Invoice Desk — Invoicing Dashboard

Invoicing Workspace

Invoice Desk

Build, preview, and send professional invoices in one place

Outstanding

₹0

0 invoices

Paid (all time)

0 invoices

Overdue

₹0

0 invoices

Total invoices

0

Create an invoice

Fill in the details — the preview on the right updates as you type
Invoice details
Bill to
Line items
Description Qty Unit price Amount
Subtotal₹0.00
Tax % ₹0.00
Total₹0.00

All invoices

Every invoice you’ve created
Invoice # Client Issued Due Total Status Actions
Showing 0 of 0 invoices

Import & export

Bring in a spreadsheet of invoices, or back up what you have
Load from a spreadsheet Upload a CSV or Excel file with columns like invoice_number, client_name, issue_date, due_date, description, quantity, unit_price, status — each row becomes a line item, grouped by invoice number.
Export your invoices Download everything currently saved as a CSV file — useful as a backup or to open in Excel.
Invoice Desk · everything renders and saves in your browser · no data leaves this page
`); w.document.close(); w.onload = () => { w.focus(); w.print(); }; }); document.getElementById('downloadPdfBtn').addEventListener('click', async () => { if(typeof html2canvas === 'undefined' || typeof window.jspdf === 'undefined'){ alert('PDF tools could not load (this needs an internet connection to fetch them). Try "Print / Save as PDF" instead, which uses your browser\'s built-in print dialog.'); return; } const btn = document.getElementById('downloadPdfBtn'); const original = btn.textContent; btn.textContent = 'Preparing…'; btn.disabled = true; try{ const sheet = document.getElementById('invoiceSheet'); const canvas = await html2canvas(sheet, { scale: 2, backgroundColor: '#FFFFFF' }); const imgData = canvas.toDataURL('image/png'); const { jsPDF } = window.jspdf; const pdf = new jsPDF({ unit: 'pt', format: 'a4' }); const pageWidth = pdf.internal.pageSize.getWidth(); const pageHeight = pdf.internal.pageSize.getHeight(); const imgWidth = pageWidth; const imgHeight = canvas.height * (imgWidth / canvas.width); let heightLeft = imgHeight, position = 0; pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight); heightLeft -= pageHeight; while(heightLeft > 0){ position = heightLeft - imgHeight; pdf.addPage(); pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight); heightLeft -= pageHeight; } const inv = readFormInvoice(); pdf.save(`${inv.number || 'invoice'}.pdf`); } catch(err){ alert('Could not generate the PDF: ' + err.message); } finally { btn.textContent = original; btn.disabled = false; } }); // ---------- Import from CSV / Excel ---------- const fileInput = document.getElementById('fileInput'); document.getElementById('uploadBtn').addEventListener('click', () => fileInput.click()); const uploadStatus = document.getElementById('uploadStatus'); fileInput.addEventListener('change', (e) => { const file = e.target.files[0]; if(!file) return; uploadStatus.className = 'status-msg'; uploadStatus.textContent = `Reading "${file.name}"…`; const reader = new FileReader(); reader.onload = (evt) => { try{ let parsedRows = []; if(file.name.toLowerCase().endsWith('.csv')){ parsedRows = parseCSV(evt.target.result); } else { const wb = XLSX.read(evt.target.result, { type:'array' }); parsedRows = XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]], { defval:'' }); } if(!parsedRows.length) throw new Error('No rows found in file.'); const imported = groupRowsIntoInvoices(parsedRows); if(!imported.length) throw new Error('Could not find an invoice_number column to group rows by.'); invoices = invoices.concat(imported); saveInvoices(); renderRegister(); renderKPIs(); updateStorageNote(); uploadStatus.textContent = `Imported ${imported.length} invoice${imported.length===1?'':'s'} from "${file.name}".`; } catch(err){ uploadStatus.className = 'status-msg error'; uploadStatus.textContent = 'Could not read that file: ' + err.message; } }; reader.onerror = () => { uploadStatus.className = 'status-msg error'; uploadStatus.textContent = 'Could not read that file.'; }; if(file.name.toLowerCase().endsWith('.csv')) reader.readAsText(file); else reader.readAsArrayBuffer(file); fileInput.value = ''; }); function findKey(obj, names){ const keys = Object.keys(obj); for(const n of names){ const hit = keys.find(k => k.trim().toLowerCase().replace(/\s+/g,'_') === n); if(hit) return hit; } for(const n of names){ const hit = keys.find(k => k.trim().toLowerCase().includes(n)); if(hit) return hit; } return null; } function groupRowsIntoInvoices(rawRows){ const sample = rawRows[0]; const km = { number: findKey(sample, ['invoice_number','invoice_no','invoice','number']), client: findKey(sample, ['client_name','client','customer','bill_to']), email: findKey(sample, ['client_email','email']), address: findKey(sample, ['client_address','address']), issue: findKey(sample, ['issue_date','date','invoice_date']), due: findKey(sample, ['due_date','due']), status: findKey(sample, ['status']), desc: findKey(sample, ['description','item','particulars']), qty: findKey(sample, ['quantity','qty']), price: findKey(sample, ['unit_price','price','rate']), tax: findKey(sample, ['tax','tax_rate']), notes: findKey(sample, ['notes','terms']) }; if(!km.number) return []; const groups = {}; rawRows.forEach(r => { const num = String(r[km.number]).trim(); if(!num) return; if(!groups[num]){ groups[num] = { id: genId(), number: num, status: (km.status && r[km.status]) ? String(r[km.status]).trim().toLowerCase() : 'draft', issueDate: km.issue ? normalizeDate(r[km.issue]) : todayISO(), dueDate: km.due ? normalizeDate(r[km.due]) : addDaysISO(todayISO(),14), client: { name: km.client ? String(r[km.client]).trim() : '', email: km.email ? String(r[km.email]).trim() : '', address: km.address ? String(r[km.address]).trim() : '' }, items: [], tax: km.tax ? Number(r[km.tax])||0 : 0, notes: km.notes ? String(r[km.notes]).trim() : '' }; } groups[num].items.push({ desc: km.desc ? String(r[km.desc]).trim() : 'Item', qty: km.qty ? Number(r[km.qty])||1 : 1, price: km.price ? Number(r[km.price])||0 : 0 }); }); return Object.values(groups); } function normalizeDate(v){ if(!v) return todayISO(); const s = String(v).trim(); const parts = s.split(/[-/]/); if(parts.length === 3){ if(parts[0].length === 4) return `${parts[0]}-${parts[1].padStart(2,'0')}-${parts[2].padStart(2,'0')}`; if(parts[2].length === 4) return `${parts[2]}-${parts[1].padStart(2,'0')}-${parts[0].padStart(2,'0')}`; } const d = new Date(s); return isNaN(d) ? todayISO() : d.toISOString().slice(0,10); } function parseCSV(text){ const lines = text.split(/\r?\n/).filter(l => l.trim().length); if(!lines.length) return []; const headers = splitCSVLine(lines[0]); return lines.slice(1).map(line => { const cells = splitCSVLine(line); const obj = {}; headers.forEach((h,i) => obj[h.trim()] = (cells[i] !== undefined ? cells[i].trim() : '')); return obj; }); } function splitCSVLine(line){ const out = []; let cur=''; let inQ=false; for(let i=0;i { if(!invoices.length){ alert('No invoices to export yet.'); return; } const rows = [['invoice_number','client_name','client_email','client_address','issue_date','due_date','status','description','quantity','unit_price','tax_rate','notes']]; invoices.forEach(inv => { inv.items.forEach(it => { rows.push([inv.number, inv.client.name, inv.client.email, inv.client.address, inv.issueDate, inv.dueDate, inv.status, it.desc, it.qty, it.price, inv.tax, inv.notes]); }); }); const csv = rows.map(r => r.map(cell => { const s = String(cell ?? ''); return /[",\n]/.test(s) ? `"${s.replace(/"/g,'""')}"` : s; }).join(',')).join('\n'); const blob = new Blob([csv], { type:'text/csv' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `invoices-export-${todayISO()}.csv`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }); // ---------- Reset everything ---------- document.getElementById('resetBtn').addEventListener('click', () => { if(!confirm('This will permanently delete all saved invoices in this browser. Continue?')) return; invoices = []; if(storageAvailable) localStorage.removeItem(STORAGE_KEY); editingId = null; resetForm(true); renderRegister(); renderKPIs(); updateStorageNote(); }); // ---------- Init ---------- (function init(){ const savedProfile = loadProfile(); if(savedProfile) profile = savedProfile; invoices = loadInvoices(); resetForm(true); renderRegister(); renderKPIs(); updateStorageNote(); })(); })();