Invoice Desk — Invoicing Dashboard
Outstanding
₹0
0 invoices
Create an invoice
Fill in the details — the preview on the right updates as you type
Print / Save as PDF
Download PDF
All invoices
Every invoice you’ve created
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();
})();
})();