feat(tahesab): PDF invoice export for asnad cards
Add per-document PDF print from account details, plus org invoice settings for logo and texts separate from CRM invoices.
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
import type { TahesabInvoiceSettingsDto } from '@mento/shared';
|
||||
import { toFaDigits } from '@/lib/jalali';
|
||||
|
||||
export type TahesabInvoiceGroup = {
|
||||
no: string;
|
||||
date: string;
|
||||
rows: Record<string, unknown>[];
|
||||
};
|
||||
|
||||
function escapeHtml(value: string) {
|
||||
return value
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function dash(v: unknown, fallback = '—'): string {
|
||||
if (v == null) return fallback;
|
||||
const s = String(v).trim();
|
||||
if (!s || s === 'null' || s === 'undefined' || s === 'NaN') return fallback;
|
||||
return s;
|
||||
}
|
||||
|
||||
function fmtNum(v: unknown): string {
|
||||
const s = dash(v, '');
|
||||
if (!s) return '—';
|
||||
const n = Number(String(s).replace(/,/g, ''));
|
||||
if (!Number.isFinite(n)) return toFaDigits(s);
|
||||
return toFaDigits(
|
||||
new Intl.NumberFormat('fa-IR', { maximumFractionDigits: 3 }).format(n),
|
||||
);
|
||||
}
|
||||
|
||||
function metalLabel(v: unknown): string {
|
||||
const s = dash(v, '');
|
||||
if (s === '0') return 'طلا';
|
||||
if (s === '1') return 'پلاتین';
|
||||
if (s === '2') return 'نقره';
|
||||
return s || '—';
|
||||
}
|
||||
|
||||
function rowDesc(r: Record<string, unknown>): string {
|
||||
const parts = [dash(r.NO, ''), dash(r.No_Kar, ''), dash(r.Sharh1, '')].filter(
|
||||
(p) => p && p !== '—',
|
||||
);
|
||||
return parts.join(' · ') || '—';
|
||||
}
|
||||
|
||||
function sumMali(rows: Record<string, unknown>[]): number {
|
||||
let t = 0;
|
||||
for (const r of rows) {
|
||||
const n = Number(String(r.Mali ?? '').replace(/,/g, ''));
|
||||
if (Number.isFinite(n)) t += n;
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
/** خروجی فاکتور سند تهحساب — چاپ / ذخیره PDF از دیالوگ مرورگر */
|
||||
export function printTahesabInvoicePdf(opts: {
|
||||
group: TahesabInvoiceGroup;
|
||||
settings: TahesabInvoiceSettingsDto | null;
|
||||
customerName: string;
|
||||
moshtariCode?: number | null;
|
||||
}) {
|
||||
const { group, settings, customerName, moshtariCode } = opts;
|
||||
const first = group.rows[0] ?? {};
|
||||
const time = dash(first.Tarikh_Time ?? first.ZamanSabt, '');
|
||||
const user = dash(first.User, '');
|
||||
const temporary = first.isSanadMovaghat === true;
|
||||
const total = sumMali(group.rows);
|
||||
|
||||
const rowsHtml = group.rows
|
||||
.map((r, i) => {
|
||||
const idx = dash(r.Radif, String(i + 1));
|
||||
return `<tr>
|
||||
<td>${toFaDigits(idx)}</td>
|
||||
<td>${escapeHtml(rowDesc(r))}</td>
|
||||
<td>${escapeHtml(metalLabel(r.Jens_Felez))}</td>
|
||||
<td class="ltr">${fmtNum(r.Count)}</td>
|
||||
<td class="ltr">${fmtNum(r.Vazn)}</td>
|
||||
<td class="ltr">${fmtNum(r.Mazaneh ?? r.Fi)}</td>
|
||||
<td class="ltr">${fmtNum(r.Mali)}</td>
|
||||
<td>${escapeHtml(dash(r.Arz, 'IRR'))}</td>
|
||||
</tr>`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
const logo = settings?.logoDataUrl
|
||||
? `<img class="logo" src="${escapeHtml(settings.logoDataUrl)}" alt="" />`
|
||||
: '';
|
||||
const company = escapeHtml(settings?.companyName?.trim() || 'فاکتور تهحساب');
|
||||
const subtitle = settings?.companySubtitle?.trim()
|
||||
? `<div class="sub">${escapeHtml(settings.companySubtitle.trim())}</div>`
|
||||
: '';
|
||||
const address = settings?.address?.trim()
|
||||
? `<div class="muted">${escapeHtml(settings.address.trim())}</div>`
|
||||
: '';
|
||||
const phone = settings?.phone?.trim()
|
||||
? `<div class="muted">${escapeHtml(settings.phone.trim())}</div>`
|
||||
: '';
|
||||
const headerText = settings?.headerText?.trim()
|
||||
? `<p class="note">${escapeHtml(settings.headerText.trim())}</p>`
|
||||
: '';
|
||||
const footerText = settings?.footerText?.trim()
|
||||
? `<p class="note foot-note">${escapeHtml(settings.footerText.trim())}</p>`
|
||||
: '';
|
||||
|
||||
const win = window.open('', '_blank', 'noopener,noreferrer,width=900,height=1200');
|
||||
if (!win) return;
|
||||
|
||||
const html = `<!DOCTYPE html>
|
||||
<html lang="fa" dir="rtl">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>فاکتور سند ${escapeHtml(group.no)}</title>
|
||||
<style>
|
||||
@page { size: A4; margin: 12mm; }
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Tahoma, "Segoe UI", sans-serif;
|
||||
color: #111827;
|
||||
font-size: 12px;
|
||||
-webkit-print-color-adjust: exact;
|
||||
print-color-adjust: exact;
|
||||
}
|
||||
.top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 14px;
|
||||
border-bottom: 2px solid #111827;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
.brand { display: flex; gap: 12px; align-items: center; }
|
||||
.logo { max-height: 64px; max-width: 140px; object-fit: contain; }
|
||||
.org { font-size: 18px; font-weight: 800; margin: 0; }
|
||||
.sub { color: #374151; margin-top: 2px; }
|
||||
.muted { color: #6b7280; margin-top: 2px; }
|
||||
.doc { text-align: left; }
|
||||
.doc-type { font-weight: 800; font-size: 15px; }
|
||||
.badge {
|
||||
display: inline-block;
|
||||
margin-top: 4px;
|
||||
padding: 2px 8px;
|
||||
border: 1px solid #d97706;
|
||||
color: #b45309;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
}
|
||||
.meta {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 8px 16px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.lbl { color: #6b7280; font-size: 11px; display: block; margin-bottom: 2px; }
|
||||
.val { font-weight: 600; }
|
||||
.note {
|
||||
background: #f9fafb;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
margin: 0 0 12px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th, td {
|
||||
padding: 7px 8px;
|
||||
border: 1px solid #e5e7eb;
|
||||
text-align: right;
|
||||
vertical-align: top;
|
||||
}
|
||||
th { background: #f3f4f6; font-size: 11px; }
|
||||
.ltr { direction: ltr; unicode-bidi: isolate; text-align: left; }
|
||||
.totals {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.totals-box {
|
||||
min-width: 220px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
.t-row { display: flex; justify-content: space-between; gap: 24px; font-weight: 800; }
|
||||
.foot-note { margin-top: 16px; }
|
||||
@media print {
|
||||
body { font-size: 11px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="top">
|
||||
<div class="brand">
|
||||
${logo}
|
||||
<div>
|
||||
<p class="org">${company}</p>
|
||||
${subtitle}${address}${phone}
|
||||
</div>
|
||||
</div>
|
||||
<div class="doc">
|
||||
<div class="doc-type">فاکتور سند تهحساب</div>
|
||||
<div>شماره: <strong>${toFaDigits(escapeHtml(group.no))}</strong></div>
|
||||
${temporary ? '<span class="badge">سند موقت</span>' : ''}
|
||||
</div>
|
||||
</div>
|
||||
${headerText}
|
||||
<div class="meta">
|
||||
<div><span class="lbl">مشتری</span><div class="val">${escapeHtml(customerName || '—')}</div></div>
|
||||
<div><span class="lbl">کد مشتری</span><div class="val ltr">${
|
||||
moshtariCode != null ? toFaDigits(String(moshtariCode)) : '—'
|
||||
}</div></div>
|
||||
<div><span class="lbl">تاریخ</span><div class="val">${toFaDigits(escapeHtml(group.date))}${
|
||||
time ? ` · ${toFaDigits(escapeHtml(time))}` : ''
|
||||
}</div></div>
|
||||
<div><span class="lbl">کاربر ثبت</span><div class="val">${escapeHtml(user)}</div></div>
|
||||
<div><span class="lbl">تعداد ردیف</span><div class="val">${toFaDigits(String(group.rows.length))}</div></div>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>شرح</th>
|
||||
<th>جنس فلز</th>
|
||||
<th>تعداد</th>
|
||||
<th>وزن</th>
|
||||
<th>مظنه/فی</th>
|
||||
<th>مالی</th>
|
||||
<th>ارز</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>${rowsHtml}</tbody>
|
||||
</table>
|
||||
<div class="totals">
|
||||
<div class="totals-box">
|
||||
<div class="t-row"><span>جمع مالی</span><span class="ltr">${fmtNum(total)}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
${footerText}
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
win.document.open();
|
||||
win.document.write(html);
|
||||
win.document.close();
|
||||
win.focus();
|
||||
setTimeout(() => {
|
||||
win.print();
|
||||
}, 300);
|
||||
}
|
||||
Reference in New Issue
Block a user