@@ -92,7 +92,7 @@ <h4>Pending UPI payments <span id="pendingCount" class="pending-badge">0</span><
9292</ main >
9393
9494< div id ="modalRoot "> </ div >
95- < footer > New Serchhip North Village Council · v1.29.6 </ footer >
95+ < footer > New Serchhip North Village Council · v1.29.7 </ footer >
9696
9797< script type ="module ">
9898/* ============================================================
@@ -1934,22 +1934,22 @@ <h4>Pending UPI payments <span id="pendingCount" class="pending-badge">0</span><
19341934 return window . XLSX ;
19351935}
19361936
1937- async function ensureJsPDF ( ) {
1938- if ( window . jspdf && window . jspdf . jsPDF ) return window . jspdf . jsPDF ;
1937+ async function ensureHtml2Pdf ( ) {
1938+ if ( window . html2pdf ) return window . html2pdf ;
19391939 try {
19401940 await new Promise ( ( res , rej ) => {
19411941 const s = document . createElement ( "script" ) ;
1942- s . src = "https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5 .1/jspdf.umd .min.js" ;
1942+ s . src = "https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10 .1/html2pdf.bundle .min.js" ;
19431943 s . crossOrigin = "anonymous" ;
19441944 s . onload = res ;
1945- s . onerror = ( ) => rej ( new Error ( "jsPDF load failed" ) ) ;
1945+ s . onerror = ( ) => rej ( new Error ( "html2pdf load failed" ) ) ;
19461946 document . head . appendChild ( s ) ;
19471947 } ) ;
19481948 } catch ( e ) {
1949- console . warn ( "jsPDF load failed:" , e ) ;
1949+ console . warn ( "html2pdf load failed:" , e ) ;
19501950 throw new Error ( "Couldn't load PDF generator. Check your connection." ) ;
19511951 }
1952- return window . jspdf . jsPDF ;
1952+ return window . html2pdf ;
19531953}
19541954
19551955const _num = v => { const n = parseFloat ( v ) ; return isNaN ( n ) ?0 :n ; } ;
@@ -2796,80 +2796,69 @@ <h4>Pending UPI payments <span id="pendingCount" class="pending-badge">0</span><
27962796
27972797async function doExportPdf ( ) {
27982798 await withExportUI ( "exportPdfBtn" , "Export owing list (PDF)" , async ( ) => {
2799- const jsPDF = await ensureJsPDF ( ) ;
2799+ const html2pdf = await ensureHtml2Pdf ( ) ;
28002800 const searchTerm = $ ( "adminSearch" ) . value . trim ( ) . toLowerCase ( ) ;
28012801 const list = filterCitizenList ( allCitizens , searchTerm , adminFilter , periodFilter )
28022802 . filter ( c => getEffectiveBalance ( c ) > 0 ) ;
28032803 if ( ! list . length ) { toast ( "No owing households to export" ) ; return ; }
28042804
2805- const fmtRs = n => "Rs. " + ( Number ( n ) || 0 ) . toLocaleString ( "en-IN" ) ;
28062805 const ledgers = ledgerCache . size ? Object . fromEntries ( ledgerCache ) : await store . getAllLedgers ( ) ;
28072806 const rows = [ ] ;
28082807 for ( const c of list ) {
28092808 const bal = getEffectiveBalance ( c ) ;
28102809 const entries = ledgers [ c . id ] || await store . getLedger ( c . id ) ;
2811- const payments = entries . filter ( e => e . type === "payment" ) . map ( e => fmtRs ( e . amount ) ) ;
2810+ const payments = entries . filter ( e => e . type === "payment" ) . map ( e => fmt ( e . amount ) ) ;
28122811 rows . push ( {
2813- name : c . name || "—" ,
2814- jobCard : c . jobCard || c . id ,
2815- amount : fmtRs ( bal ) ,
2812+ name : esc ( c . name || "—" ) ,
2813+ jobCard : esc ( c . jobCard || c . id ) ,
2814+ amount : fmt ( bal ) ,
28162815 payments : payments . length ? "Payment: " + payments . join ( ", " ) : ""
28172816 } ) ;
28182817 }
28192818
2820- const doc = new jsPDF ( { orientation : "portrait" , unit : "mm" , format : "a4" } ) ;
2821- const pageW = doc . internal . pageSize . getWidth ( ) ;
2822- const margin = 16 ;
2823- const rightX = pageW - margin ;
2824- let y = 22 ;
2825-
2826- doc . setFontSize ( 18 ) ;
2827- doc . setFont ( undefined , "bold" ) ;
2828- doc . text ( "Owing List" , margin , y ) ;
2829- y += 10 ;
2830-
2831- doc . setFontSize ( 10 ) ;
2832- doc . setFont ( undefined , "normal" ) ;
2833- doc . text ( `${ todayISO ( ) } · ${ adminFilter . charAt ( 0 ) . toUpperCase ( ) + adminFilter . slice ( 1 ) } ${ periodFilter ?` · ${ periodFilter } ` :"" } ` , margin , y ) ;
2834- y += 14 ;
2835-
2836- doc . setDrawColor ( 230 , 230 , 230 ) ;
2837- for ( const r of rows ) {
2838- if ( y > doc . internal . pageSize . getHeight ( ) - 28 ) { doc . addPage ( ) ; y = 20 ; }
2839-
2840- // Row 1: name (left) and amount (right)
2841- doc . setFont ( undefined , "bold" ) ;
2842- doc . setTextColor ( 40 , 40 , 40 ) ;
2843- doc . setFontSize ( 11 ) ;
2844- const nameLines = doc . splitTextToSize ( r . name , pageW - margin * 2 - 55 ) ;
2845- doc . text ( nameLines , margin , y ) ;
2846-
2847- const amountW = doc . getTextWidth ( r . amount ) ;
2848- doc . setTextColor ( 180 , 40 , 40 ) ;
2849- doc . text ( r . amount , rightX - amountW , y ) ;
2850-
2851- // Row 2: job card (left) and payment history (right)
2852- y += nameLines . length * 4.5 + 1 ;
2853- doc . setFont ( undefined , "normal" ) ;
2854- doc . setTextColor ( 120 , 120 , 120 ) ;
2855- doc . setFontSize ( 9 ) ;
2856- doc . text ( r . jobCard , margin , y ) ;
2857-
2858- if ( r . payments ) {
2859- const payMaxW = 70 ;
2860- const payLines = doc . splitTextToSize ( r . payments , payMaxW ) ;
2861- doc . setTextColor ( 26 , 125 , 66 ) ;
2862- doc . text ( payLines , rightX , y , { align : "right" } ) ;
2863- }
2819+ const rowHtml = rows . map ( r => `
2820+ <div style="display:flex;justify-content:space-between;align-items:flex-start;border-bottom:1px solid #e5e5e5;padding:10px 0;">
2821+ <div style="flex:1;min-width:0;padding-right:12px;">
2822+ <div style="font-size:14px;font-weight:600;color:#222;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${ r . name } </div>
2823+ <div style="font-size:11px;color:#777;margin-top:2px;">${ r . jobCard } </div>
2824+ </div>
2825+ <div style="text-align:right;max-width:45%;">
2826+ <div style="font-size:14px;font-weight:700;color:#b91c1c;">${ r . amount } </div>
2827+ ${ r . payments ? `<div style="font-size:10px;color:#15803d;margin-top:2px;word-break:break-word;">${ r . payments } </div>` : "" }
2828+ </div>
2829+ </div>
2830+ ` ) . join ( "" ) ;
2831+
2832+ const el = document . createElement ( "div" ) ;
2833+ el . style . position = "fixed" ;
2834+ el . style . left = "-9999px" ;
2835+ el . style . top = "0" ;
2836+ el . style . width = "210mm" ;
2837+ el . style . padding = "16mm" ;
2838+ el . style . background = "#fff" ;
2839+ el . style . fontFamily = "Arial, sans-serif" ;
2840+ el . innerHTML = `
2841+ <div style="font-size:22px;font-weight:700;color:#1b4d3e;margin-bottom:4px;">Owing List</div>
2842+ <div style="font-size:11px;color:#666;margin-bottom:16px;">${ todayISO ( ) } · ${ adminFilter . charAt ( 0 ) . toUpperCase ( ) + adminFilter . slice ( 1 ) } ${ periodFilter ?` · ${ periodFilter } ` :"" } </div>
2843+ ${ rowHtml }
2844+ ` ;
2845+ document . body . appendChild ( el ) ;
28642846
2865- doc . setFontSize ( 11 ) ;
2866- y += 6 ;
2867- doc . line ( margin , y , pageW - margin , y ) ;
2868- y += 8 ;
2847+ try {
2848+ await html2pdf ( ) . set ( {
2849+ margin : 0 ,
2850+ filename : `owing_${ todayISO ( ) } .pdf` ,
2851+ image : { type : "jpeg" , quality : 0.98 } ,
2852+ html2canvas : { scale : 2 , useCORS : true , logging : false } ,
2853+ jsPDF : { unit : "mm" , format : "a4" , orientation : "portrait" }
2854+ } ) . from ( el ) . save ( ) ;
2855+ toast ( "PDF exported" ) ;
2856+ } catch ( err ) {
2857+ console . error ( "PDF export failed:" , err ) ;
2858+ toast ( "PDF export failed" ) ;
2859+ } finally {
2860+ document . body . removeChild ( el ) ;
28692861 }
2870-
2871- doc . save ( `owing_${ todayISO ( ) } .pdf` ) ;
2872- toast ( "PDF exported" ) ;
28732862 } ) ;
28742863}
28752864
0 commit comments