Loading workspace…
`; const w=window.open('','_blank'); if(w){ w.document.write(html); w.document.close(); } else alert('Please allow pop-ups to generate the PDF.'); } function renderQuotations(){ const stClass={Draft:'tag-gray',Sent:'tag-blue',Accepted:'tag-teal',Rejected:'tag-red',Converted:'tag-amber'}; document.querySelector('#quotesTable tbody').innerHTML=DATA.quotations.map(q=>{ const items=q.items&&q.items.length?q.items:[{service:q.service||'—'}]; const svcLabel=items.length>1?items[0].service+' +'+(items.length-1)+' more':items[0].service; return ` ${q.number}${q.client}${q.clientPhone||'—'}${svcLabel}${fmt(q.amount)} ${q.status} ${q.status==='Draft'?``:''} ${q.status==='Sent'?``:''} ${q.status==='Accepted'?``:''} `; }).join('') || 'No quotations yet'; const total=DATA.quotations.length; const totalValue=DATA.quotations.reduce((s,q)=>s+q.amount,0); const accepted=DATA.quotations.filter(q=>['Accepted','Converted'].includes(q.status)).length; const closed=DATA.quotations.filter(q=>['Accepted','Rejected','Converted'].includes(q.status)).length; const acceptRate=closed?Math.round(100*accepted/closed):0; const avgValue=total?Math.round(totalValue/total):0; const pending=DATA.quotations.filter(q=>['Draft','Sent'].includes(q.status)).length; document.getElementById('quoteKpiRow').innerHTML=`
Total quotations
${total}
Total quoted value
${fmt(totalValue)}
Acceptance rate
${acceptRate}%
Avg quote value
${fmt(avgValue)}
Pending
${pending}
`; destroyChart('quoteStatus'); const statuses=['Draft','Sent','Accepted','Rejected','Converted']; charts.quoteStatus=new Chart(document.getElementById('quoteStatusChart'),{type:'doughnut',data:{labels:statuses,datasets:[{data:statuses.map(s=>DATA.quotations.filter(q=>q.status===s).length),backgroundColor:[COLORS.gray,COLORS.blue,COLORS.teal,COLORS.red,COLORS.amber],borderWidth:2,borderColor:'#fff'}]}, options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{position:'bottom',labels:{boxWidth:10,font:{size:11}}}}}}); destroyChart('quoteService'); const svcTotals={}; DATA.quotations.forEach(q=>{ (q.items&&q.items.length?q.items:[{service:q.service||'Other',price:q.amount,qty:1}]).forEach(it=>{ svcTotals[it.service]=(svcTotals[it.service]||0)+it.price*it.qty; }); }); const svcLabels=Object.keys(svcTotals); const svcColors=[COLORS.blue,COLORS.violet,COLORS.teal,COLORS.coral,COLORS.amber,COLORS.pink]; charts.quoteService=new Chart(document.getElementById('quoteServiceChart'),{type:'bar',data:{labels:svcLabels,datasets:[{data:svcLabels.map(s=>svcTotals[s]),backgroundColor:svcLabels.map((_,i)=>svcColors[i%svcColors.length]),borderRadius:6,maxBarThickness:34}]}, options:{indexAxis:'y',responsive:true,maintainAspectRatio:false,plugins:{legend:{display:false}},scales:{x:{ticks:{callback:v=>'₹'+(v/1000)+'k'},grid:{color:'#EEEFF3'}},y:{grid:{display:false}}}}}); } /* ---------- Client ledger (khatabook) ---------- */ let ledgerOpenClient=null; function ledgerClientNames(){ const fromLedger=DATA.ledger.map(l=>l.client); const fromInvoices=DATA.invoices.map(i=>i.client); const fromContacts=DATA.contacts.filter(c=>c.company).map(c=>c.company); return [...new Set([...fromLedger,...fromInvoices,...fromContacts])].filter(Boolean); } function ledgerEntriesFor(client){ // merge manual ledger entries with invoice-driven entries, oldest first const manual=DATA.ledger.filter(l=>l.client===client).map(l=>({date:l.date,desc:l.desc,type:l.type,amount:l.amount})); const fromInvoices=DATA.invoices.filter(i=>i.client===client).map(i=>({date:i.issued,desc:'Invoice '+i.number,type:'debit',amount:i.amount})); return [...manual,...fromInvoices].sort((a,b)=>new Date(a.date)-new Date(b.date)); } function ledgerTotals(client){ const entries=ledgerEntriesFor(client); const billed=entries.filter(e=>e.type==='debit').reduce((s,e)=>s+e.amount,0); const received=entries.filter(e=>e.type==='credit').reduce((s,e)=>s+e.amount,0); return { billed, received, due: billed-received }; } document.getElementById('addLedgerBtn').addEventListener('click',async()=>{ const client=document.getElementById('ledClient').value.trim(); const amount=Number(document.getElementById('ledAmount').value); if(!client||!amount) return; const date=document.getElementById('ledDate').value?new Date(document.getElementById('ledDate').value).toISOString():todayISO(); DATA.ledger.unshift({id:'led'+Date.now(),client,type:document.getElementById('ledType').value,desc:document.getElementById('ledDesc').value||(document.getElementById('ledType').value==='debit'?'Amount billed':'Payment received'),amount,date}); logActivity('Ledger entry added for '+client+': '+fmt(amount)); document.getElementById('ledClient').value=''; document.getElementById('ledAmount').value=''; document.getElementById('ledDesc').value=''; document.getElementById('ledDate').value=''; await save(); renderLedger(); }); function openClientLedger(client){ ledgerOpenClient=client; const panel=document.getElementById('ledgerDetailPanel'); panel.style.display='block'; const contact=DATA.contacts.find(c=>c.name===client||c.company===client); document.getElementById('ledgerClientName').textContent=client+' — Khata'; const entries=ledgerEntriesFor(client); document.getElementById('ledgerClientSub').textContent=entries.length+' entries'+(contact&&contact.phone?' · '+contact.phone:''); const t=ledgerTotals(client); document.getElementById('ledgerClientKpis').innerHTML=`
Total billed
${fmt(t.billed)}
▲ ${entries.filter(e=>e.type==='debit').length} entries
Total received
${fmt(t.received)}
▲ ${entries.filter(e=>e.type==='credit').length} payments
Balance due
${fmt(t.due)}
${t.due>0?'▼ Outstanding':'✓ Settled'}
`; destroyChart('ledgerClient'); const sortedE=entries.slice(); const debitLabels=sortedE.map(e=>new Date(e.date).toLocaleDateString('en-IN',{day:'numeric',month:'short'})); let run=0; const balanceLine=sortedE.map(e=>{ run+=e.type==='debit'?e.amount:-e.amount; return run; }); charts.ledgerClient=new Chart(document.getElementById('ledgerClientChart'),{type:'line',data:{labels:debitLabels,datasets:[ {label:'Running balance',data:balanceLine,borderColor:COLORS.blue,backgroundColor:'rgba(42,120,214,0.08)',fill:true,tension:.35,pointRadius:3,pointBackgroundColor:COLORS.blue} ]},options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{display:false}},scales:{y:{ticks:{callback:v=>'₹'+(v/1000)+'k'},grid:{color:'#EEEFF3'}},x:{grid:{display:false},ticks:{maxTicksLimit:6}}}}}); let running=0; document.querySelector('#ledgerEntriesTable tbody').innerHTML=entries.map(e=>{ running+=e.type==='debit'?e.amount:-e.amount; const rowBg=e.type==='debit'?'background:#FFF8F8;':'background:#F6FEFA;'; return ` ${new Date(e.date).toLocaleDateString('en-IN',{day:'numeric',month:'short',year:'numeric'})} ${e.desc} ${e.type==='debit'?fmt(e.amount):'—'} ${e.type==='credit'?fmt(e.amount):'—'} ${fmt(running)} `; }).join('') || 'No entries yet — add one below'; panel.scrollIntoView({behavior:'smooth',block:'nearest'}); } document.getElementById('ledgerCloseBtn').addEventListener('click',()=>{ document.getElementById('ledgerDetailPanel').style.display='none'; ledgerOpenClient=null; destroyChart('ledgerClient'); }); document.getElementById('ledgerStatementBtn').addEventListener('click',()=>{ if(ledgerOpenClient) generateStatement(ledgerOpenClient); }); function generateStatement(client){ const entries=ledgerEntriesFor(client); const t=ledgerTotals(client); const contact=DATA.contacts.find(c=>c.name===client||c.company===client); let running=0; const rows=entries.map(e=>{ running+=e.type==='debit'?e.amount:-e.amount; return `${new Date(e.date).toLocaleDateString('en-IN',{day:'numeric',month:'short',year:'numeric'})}${e.desc} ${e.type==='debit'?fmt(e.amount):'—'} ${e.type==='credit'?fmt(e.amount):'—'} ${fmt(running)} `; }).join(''); const html=`Statement — ${client}

Statement of account

${client}${contact&&contact.phone?' · '+contact.phone:''}
${new Date().toLocaleDateString('en-IN',{day:'numeric',month:'long',year:'numeric'})}
Total billed
${fmt(t.billed)}
Total received
${fmt(t.received)}
Balance due
${fmt(t.due)}
${rows}
DateDescriptionDebit (billed)Credit (received)Running balance