@ -69,76 +69,139 @@ export function documentHtml(doc: Doc, client: Client, company: Record<string, s
const termsText = doc . payload . terms ? ? tpl ( 'terms' )
// Bank details belong on documents payment is made against — tax and proforma invoices.
const showBank = ( doc . docType === 'INVOICE' || doc . docType === 'PROFORMA' ) && get ( 'bank' ) !== ''
const companyMeta = [
get ( 'address' ) ,
get ( 'gstin' ) !== '' ? ` GSTIN: ${ get ( 'gstin' ) } ` : '' ,
[ get ( 'phone' ) , get ( 'email' ) ] . filter ( ( s ) = > s !== '' ) . join ( ' · ' ) ,
] . filter ( ( s ) = > s !== '' )
// Accent = the single chromatic voice. Validate-or-fallback BEFORE interpolation so a
// raw setting string can never be injected into the CSS (`#334155` is professional slate-indigo).
const accentRaw = tpl ( 'accent' )
const accent = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/ . test ( accentRaw ) ? accentRaw : '#334155'
const isDraft = ( doc . docNo ? ? '' ) === ''
const phoneEmail = [ get ( 'phone' ) , get ( 'email' ) ] . filter ( ( s ) = > s !== '' ) . join ( ' · ' )
return ` <!doctype html>
< html >
< head >
< meta charset = "utf-8" >
< title > $ { esc ( doc . docNo ? ? 'Draft' ) } < / title >
< style >
: root {
-- accent :$ { accent } ;
-- ink : # 1 f2937 ; -- ink - strong : # 111827 ; -- muted : # 6 b7280 ; -- faint : # 9 ca3af ;
-- line : # e5e7eb ; -- panel : # f8fafc ;
}
@page { size :A4 ; margin :14mm ; }
/ * O n s c r e e n ( l i v e p r e v i e w ) p u p p e t e e r i s n o t i n v o l v e d , s o @ p a g e m a r g i n s d o n o t
apply — mimic the 14 mm print margin and a white page . page . pdf renders the
print media , so these screen - only rules never affect the actual PDF . * /
@media screen { body { padding : 14mm ; background : # fff ; } }
apply — mimic the 14 mm print margin on a white A4 card over a desk - grey
backdrop . page . pdf renders the print media , so these screen - only rules never
affect the actual PDF . * /
@media screen {
html { background : # eceef1 ; }
body { padding :14mm ; background : # fff ; max - width :210mm ; margin :0 auto ; box - shadow :0 1 px 4 px rgba ( 0 , 0 , 0 , . 18 ) ; }
}
* { box - sizing :border - box ; }
body { font - family : 'Segoe UI' , Arial , sans - serif ; font - size : 11px ; color : # 1 a1a1a ; margin : 0 ; }
. letterhead { border - bottom : 2px solid # 1 a1a1a ; padding - bottom : 8px ; }
. letterhead h1 { font - size : 20px ; margin : 0 0 2 px ; letter - spacing : 0.5px ; }
. letterhead p { margin : 1px 0 ; color : # 444 ; }
. doc - title { text - align : center ; font - size : 13px ; font - weight : 700 ; letter - spacing : 2px ; margin : 12px 0 8 px ; }
. meta { display : flex ; justify - content : space - between ; margin - bottom : 10px ; }
. meta . box { width : 48 % ; }
. meta h2 { font - size : 10px ; text - transform : uppercase ; color : # 666 ; margin : 0 0 3 px ; }
. meta p { margin : 1px 0 ; }
table . lines { width : 100 % ; border - collapse : collapse ; margin - bottom : 8px ; }
table . lines th , table . lines td { border : 1px solid # 999 ; padding : 4px 6 px ; }
table . lines th { background : # f0f0f0 ; font - size : 10px ; text - transform : uppercase ; }
td . r , th . r { text - align : right ; } td . c , th . c { text - align : center ; }
ul . line - content { margin : 3px 0 0 ; padding - left : 16px ; color : # 555 ; font - size : 10px ; }
body {
font - family : 'Segoe UI' , 'Inter' , 'Helvetica Neue' , Arial , sans - serif ;
font - size :10.5px ; line - height :1.45 ; color :var ( -- ink ) ; margin :0 ;
- webkit - print - color - adjust :exact ; print - color - adjust :exact ;
}
strong { font - weight :600 ; }
. eyebrow { font - size :8.5px ; font - weight :600 ; letter - spacing :.9px ; text - transform :uppercase ; color :var ( -- muted ) ; margin :0 0 3 px ; }
/* Letterhead band */
. letterhead { display :flex ; justify - content :space - between ; align - items :flex - end ; gap :16px ; padding - bottom :10px ; border - bottom :2px solid var ( -- accent ) ; }
. letterhead . lh - brand { flex :0 0 auto ; }
. letterhead . logo { max - height :60px ; max - width :200px ; display :block ; }
. letterhead . lh - company { text - align :right ; }
. letterhead . no - logo . lh - brand { display :none ; }
. letterhead . no - logo . lh - company { text - align :left ; }
. lh - company h1 { font - size :21px ; font - weight :600 ; letter - spacing :.2px ; color :var ( -- ink - strong ) ; margin :0 0 3 px ; line - height :1.15 ; }
. lh - company p { margin :1px 0 ; color :var ( -- muted ) ; font - size :10px ; }
. lh - company . gstin { color :var ( -- ink - strong ) ; font - weight :600 ; }
/* Document title */
. doc - title { text - align :center ; margin :14px 0 12 px ; }
. doc - title span { display :inline - block ; font - size :14px ; font - weight :700 ; letter - spacing :3px ; text - transform :uppercase ; color :var ( -- accent ) ; padding - bottom :4px ; border - bottom :2px solid var ( -- accent ) ; }
/* Bill-to / meta */
. meta { display :flex ; justify - content :space - between ; gap :16px ; margin - bottom :14px ; }
. meta . party { flex :1 1 55 % ; }
. party . party - name { font - size :12px ; font - weight :600 ; color :var ( -- ink - strong ) ; margin :0 0 2 px ; }
. party p { margin :1px 0 ; }
. meta . docmeta { flex :0 0 38 % ; align - self :flex - start ; border :1px solid var ( -- line ) ; border - left :3px solid var ( -- accent ) ; border - radius :2px ; padding :8px 10 px ; background :var ( -- panel ) ; }
. docmeta . row { display :flex ; justify - content :space - between ; gap :10px ; margin :2px 0 ; }
. docmeta . k { color :var ( -- muted ) ; }
. docmeta . v { font - weight :600 ; color :var ( -- ink - strong ) ; font - variant - numeric :tabular - nums ; }
. chip - draft { display :inline - block ; font - size :8.5px ; font - weight :700 ; letter - spacing :1px ; color : # fff ; background :var ( -- accent ) ; padding :1px 6 px ; border - radius :2px ; }
/* Line table */
table . lines { width :100 % ; border - collapse :collapse ; margin - bottom :14px ; }
table . lines thead { display :table - header - group ; }
table . lines th { font - size :9px ; font - weight :600 ; letter - spacing :.5px ; text - transform :uppercase ; color :var ( -- ink - strong ) ; text - align :left ; padding :7px 8 px ; background : # f1f5f9 ; border - bottom :2px solid var ( -- accent ) ; }
table . lines td { padding :7px 8 px ; border - bottom :1px solid var ( -- line ) ; vertical - align :top ; }
table . lines tbody tr { break - inside :avoid ; page - break - inside :avoid ; }
table . lines td . c , table . lines th . c { text - align :center ; }
table . lines td . r , table . lines th . r { text - align :right ; font - variant - numeric :tabular - nums ; font - feature - settings : "tnum" 1 ; }
table . lines td :first - child , table . lines th :first - child { padding - left :2px ; }
table . lines td :last - child , table . lines th :last - child { padding - right :2px ; }
table . lines td :last - child { font - weight :600 ; }
ul . line - content { margin :4px 0 0 ; padding - left :15px ; color :var ( -- muted ) ; font - size :9.5px ; }
ul . line - content li { margin :1px 0 ; }
. bottom { display : flex ; justify - content : space - between ; align - items : flex - start ; }
. words { width : 55 % ; }
table . totals { width : 40 % ; border - collapse : collapse ; }
table . totals td { padding : 3px 6 px ; border - bottom : 1px solid # ddd ; }
table . totals tr . grand td { border - top : 2px solid # 1 a1a1a ; border - bottom : none ; font - weight : 700 ; font - size : 13px ; }
. bank , . terms { margin - top : 12px ; border : 1px solid # ccc ; padding : 6px 8 px ; }
. bank h3 , . terms h3 { font - size : 10px ; text - transform : uppercase ; margin : 0 0 3 px ; color : # 666 ; }
. letterhead . logo { max - height : 56px ; margin - bottom : 4px ; display : block ; }
. declaration { margin - top : 10px ; font - size : 10px ; color : # 555 ; }
. jurisdiction { margin - top : 6px ; font - size : 10px ; color : # 666 ; }
. footer - note { margin - top : 16px ; padding - top : 6px ; border - top : 1px solid # ddd ; text - align : center ; font - size : 10px ; color : # 666 ; }
. sign { margin - top : 28px ; text - align : right ; }
@supports ( background : color - mix ( in srgb , red , blue ) ) {
table . lines th { background : color - mix ( in srgb , var ( -- accent ) 7 % , # ffffff ) ; }
}
/* Totals + amount in words */
. bottom { display :flex ; justify - content :space - between ; align - items :flex - start ; gap :20px ; margin - bottom :6px ; break - inside :avoid ; }
. words { flex :1 1 55 % ; border - left :2px solid var ( -- accent ) ; background :var ( -- panel ) ; padding :8px 12 px ; border - radius :0 2 px 2 px 0 ; }
. words . amt - words { font - style :italic ; font - weight :500 ; color :var ( -- ink - strong ) ; }
table . totals { flex :0 0 40 % ; border - collapse :collapse ; }
table . totals td { padding :5px 10 px ; border - bottom :1px solid var ( -- line ) ; font - variant - numeric :tabular - nums ; }
table . totals td :first - child { color :var ( -- muted ) ; }
table . totals td . r { text - align :right ; color :var ( -- ink - strong ) ; font - weight :500 ; }
table . totals tr . grand td { background :var ( -- accent ) ; color : # fff ; border :none ; font - size :14px ; font - weight :700 ; padding :8px 10 px ; letter - spacing :.3px ; }
/* Supporting blocks */
. panels { display :flex ; gap :14px ; margin - top :14px ; break - inside :avoid ; }
. panels > * { flex :1 ; }
. bank , . terms { border :1px solid var ( -- line ) ; border - radius :2px ; padding :8px 10 px ; }
. bank p , . terms p { margin :2px 0 0 ; }
. declaration { margin - top :12px ; font - size :9.5px ; color :var ( -- muted ) ; }
. jurisdiction { margin - top :4px ; font - size :9.5px ; color :var ( -- faint ) ; }
. sign { margin - top :26px ; text - align :right ; break - inside :avoid ; }
. sign . for { color :var ( -- ink - strong ) ; }
. sign . label { margin - top :34px ; font - weight :600 ; color :var ( -- ink - strong ) ; }
. footer - note { margin - top :18px ; padding - top :8px ; border - top :1px solid var ( -- line ) ; text - align :center ; font - size :9px ; color :var ( -- faint ) ; }
/* OPTIONAL draft watermark (only un-issued) */
. watermark { position :fixed ; top :50 % ; left :50 % ; transform :translate ( - 50 % , - 50 % ) rotate ( - 24 deg ) ; font - size :120px ; font - weight :800 ; letter - spacing :8px ; color :var ( -- accent ) ; opacity :.05 ; z - index : - 1 ; pointer - events :none ; }
< / style >
< / head >
< body >
< header class = "letterhead" >
$ { logo !== '' ? ` <img class="logo" src=" ${ esc ( logo ) } " alt=""> ` : '' }
$ { isDraft ? ` <div class="watermark">DRAFT</div> ` : '' }
< header class = "letterhead ${logo !== '' ? 'has-logo' : 'no-logo'}" >
< div class = "lh-brand" > $ { logo !== '' ? ` <img class="logo" src=" ${ esc ( logo ) } " alt=""> ` : '' } < / div >
< div class = "lh-company" >
< h1 > $ { esc ( get ( 'name' ) ) } < / h1 >
$ { companyMeta . map ( ( line ) = > ` <p> ${ esc ( line ) } </p> ` ) . join ( '\n ' ) }
$ { get ( 'address' ) !== '' ? ` <p> ${ esc ( get ( 'address' ) ) } </p> ` : '' }
$ { get ( 'gstin' ) !== '' ? ` <p class="gstin">GSTIN: ${ esc ( get ( 'gstin' ) ) } </p> ` : '' }
$ { phoneEmail !== '' ? ` <p> ${ esc ( phoneEmail ) } </p> ` : '' }
< / div >
< / header >
< div class = "doc-title" > $ { esc ( title ) } < / div >
< div class = "doc-title" > < span > $ { esc ( title ) } < / span > < / div >
< div class = "meta" >
< div class = "box" >
< h2 > $ { doc . docType === 'QUOTATION' ? 'To' : 'Bill To' } < / h2 >
< p > < strong > $ { esc ( client . name ) } < / strong > ( $ { esc ( client . code ) } ) < / p >
< section class = "meta" >
< div class = " party ">
< div class = "eyebrow" > $ { doc . docType === 'QUOTATION' ? 'To' : 'Bill To' } < / div >
< p class = "party-name" > $ { esc ( client . name ) } < span style = "color:var(--muted);font-weight:400" > ( $ { esc ( client . code ) } ) < / span > < / p >
$ { client . address !== '' ? ` <p> ${ esc ( client . address ) } </p> ` : '' }
$ { client . gstin !== undefined ? ` <p>GSTIN: ${ esc ( client . gstin ) } </p> ` : '' }
< p > State Code : $ { esc ( client . stateCode ) } < / p >
< / div >
< div class = "box" >
< h2 > Document < / h2 >
< p > No : < strong > $ { esc ( doc . docNo ? ? 'DRAFT' ) } < / strong > < / p >
< p > Date : $ { displayDate ( doc . docDate ) } < / p >
< p > FY : $ { esc ( doc . fy ) } < / p >
$ { client . gstin !== undefined ? ` <p><strong>GSTIN:</strong> ${ esc ( client . gstin ) } </p> ` : '' }
< p > < strong > State Code : < / strong > $ { esc ( client . stateCode ) } < / p >
< / div >
< div class = "docmeta" >
< div class = "row" > < span class = "k" > Document No < / span > < span class = "v" > $ { doc . docNo !== null ? esc ( doc . docNo ) : '<span class="chip-draft">DRAFT</span>' } < / span > < / div >
< div class = "row" > < span class = "k" > Date < / span > < span class = "v" > $ { displayDate ( doc . docDate ) } < / span > < / div >
< div class = "row" > < span class = "k" > FY < / span > < span class = "v" > $ { esc ( doc . fy ) } < / span > < / div >
< / div >
< / section >
< table class = "lines" >
< thead >
@ -154,24 +217,26 @@ export function documentHtml(doc: Doc, client: Client, company: Record<string, s
< div class = "bottom" >
< div class = "words" >
< strong > Amount in words : < / strong > < br > $ { esc ( rupeesInWords ( doc . payablePaise ) ) }
< div class = "eyebrow" > Amount in Words < / div >
< div class = "amt-words" > $ { esc ( rupeesInWords ( doc . payablePaise ) ) } < / div >
< / div >
< table class = "totals" >
$ { totalsRows ( doc ) }
< tr class = "grand" > < td > Total< / td > < td class = "r" > $ { formatINR ( doc . payablePaise ) } < / td > < / tr >
< tr class = "grand" > < td > Grand Total< / td > < td class = "r" > $ { formatINR ( doc . payablePaise ) } < / td > < / tr >
< / table >
< / div >
$ { showBank ? ` <div class="bank"><h3>Bank Details</h3><p> ${ esc ( get ( 'bank' ) ) } </p></div> ` : '' }
$ { termsText !== '' ? ` <div class="terms"><h3>Terms</h3><p> ${ esc ( termsText ) } </p></div> ` : '' }
$ { ( showBank || termsText !== '' ) ? ` <div class="panels">
$ { showBank ? ` <div class="bank"><div class="eyebrow">Bank Details</div><p> ${ esc ( get ( 'bank' ) ) } </p></div> ` : '' }
$ { termsText !== '' ? ` <div class="terms"><div class="eyebrow">Terms & Conditions</div><p> ${ esc ( termsText ) } </p></div> ` : '' }
< / div > ` : ''}
$ { tpl ( 'declaration' ) !== '' ? ` <div class="declaration"> ${ esc ( tpl ( 'declaration' ) ) } </div> ` : '' }
$ { tpl ( 'jurisdiction' ) !== '' ? ` <p class="jurisdiction"> ${ esc ( tpl ( 'jurisdiction' ) ) } </p> ` : '' }
< div class = "sign" >
< p > For < strong > $ { esc ( get ( 'name' ) ) } < / strong > < / p >
< p style= "margin-top: 36px; "> $ { esc ( tpl ( 'signatory_label' ) !== '' ? tpl ( 'signatory_label' ) : 'Authorised Signatory' ) } < / p >
< p class = "for" > For < strong > $ { esc ( get ( 'name' ) ) } < / strong > < / p >
< p class= "label "> $ { esc ( tpl ( 'signatory_label' ) !== '' ? tpl ( 'signatory_label' ) : 'Authorised Signatory' ) } < / p >
< / div >
$ { tpl ( 'footer_note' ) !== '' ? ` <div class="footer-note"> ${ esc ( tpl ( 'footer_note' ) ) } </div> ` : '' }