feat(tahesab): polish asnad invoice PDF and prior balance

Compute حساب قبل from asnad before this doc date/number, add in-app preview layout, and leave a blank mesghal line for handwriting after print.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-24 21:47:46 +03:30
co-authored by Cursor
parent 1ba352f127
commit 2d690b4c04
6 changed files with 1364 additions and 143 deletions
@@ -18,14 +18,17 @@ import type {
TahesabInvoiceSettingsDto,
TahesabSanadType,
} from '@mento/shared';
import { TAHSAB_SANAD_LABELS } from '@/lib/tahesab';
import { printTahesabInvoicePdf } from '@/lib/tahesab-invoice-pdf';
import { formatTahesabBedBes, TAHSAB_SANAD_LABELS } from '@/lib/tahesab';
import type { TahesabInvoiceGroup } from '@/lib/tahesab-invoice-pdf';
import TahesabInvoiceSettingsModal from '@/components/person-assets/TahesabInvoiceSettingsModal.vue';
import TahesabInvoicePreviewModal from '@/components/person-assets/TahesabInvoicePreviewModal.vue';
const props = defineProps<{
personId: string;
/** نام نمایشی مشتری برای فاکتور PDF */
personName?: string;
/** موبایل مشتری برای فاکتور */
personPhone?: string;
}>();
const emit = defineEmits<{
@@ -44,6 +47,8 @@ const asnadLoading = ref(false);
const asnadItems = ref<Record<string, unknown>[]>([]);
const invoiceSettingsOpen = ref(false);
const invoiceSettings = ref<TahesabInvoiceSettingsDto | null>(null);
const invoicePreviewOpen = ref(false);
const invoicePreviewGroup = ref<TahesabInvoiceGroup | null>(null);
/** مدل JalaliDateInput = تاریخ محلی میلادی YYYY-MM-DD */
const fromDate = ref('');
const toDate = ref('');
@@ -181,6 +186,8 @@ const ASNAD_ORDER = Object.keys(ASNAD_FIELD_LABELS);
const ASNAD_HEAD_KEYS = new Set(['Sh_Factor', 'Sh_factor', 'Tarikh', 'NO']);
/** مقادیر کمّی که جداکنندهٔ هزارگان می‌گیرند؛ بقیه (کدها، شناسه‌ها، عیار، تاریخ) خام */
const ASNAD_AMOUNT_KEYS = new Set(['Vazn', 'TabdilVazn', 'Mali', 'Fi', 'Mazaneh', 'Count']);
/** مبالغ علامت‌دار اسناد/مانده — منفی=بد، مثبت=بس (مثل ته‌حساب) */
const ASNAD_SIGNED_KEYS = new Set(['Mali', 'TabdilVazn']);
function asnadFields(item: Record<string, unknown>) {
const keys = Object.keys(item).filter((k) => !ASNAD_HEAD_KEYS.has(k));
@@ -209,6 +216,7 @@ function asnadCell(k: string, v: unknown): string {
if (!s) return '—';
if (k === 'Jens_Felez') return JENS_FELEZ_LABELS[s] ?? showRaw(s);
if (typeof v === 'boolean') return v ? 'بله' : 'خیر';
if (ASNAD_SIGNED_KEYS.has(k)) return formatTahesabBedBes(s);
return ASNAD_AMOUNT_KEYS.has(k) ? showValue(s) : showRaw(s);
}
@@ -405,41 +413,85 @@ function onInvoiceSettingsSaved(dto: TahesabInvoiceSettingsDto) {
invoiceSettings.value = dto;
}
async function exportAsnadPdf(g: {
no: string;
date: string;
rows: Record<string, unknown>[];
}) {
error.value = '';
// باز کردن هم‌زمان با کلیک — وگرنه بعد از await پاپ‌آپ بلاک/خالی می‌شود
const preview = window.open('about:blank', '_blank');
if (preview) {
try {
preview.document.write(
'<!DOCTYPE html><html lang="fa" dir="rtl"><head><meta charset="utf-8" /><title>فاکتور</title></head><body style="font-family:Tahoma,sans-serif;padding:24px">در حال آماده‌سازی فاکتور…</body></html>',
);
preview.document.close();
} catch {
/* ignore */
}
}
function parseBalanceNum(v: unknown): number | null {
if (v == null) return null;
const s = String(v).trim().replace(/,/g, '');
if (!s || s === 'null' || s === 'undefined' || s === 'NaN') return null;
const n = Number(s);
return Number.isFinite(n) ? n : null;
}
function sumGroupField(g: TahesabInvoiceGroup, key: string): number {
let t = 0;
for (const r of g.rows) {
const n = parseBalanceNum(r[key]);
if (n != null) t += n;
}
return t;
}
function normalizeAsnadDate(d: string): string {
const s = d
.replace(/-/g, '/')
.trim()
.replace(/[۰-۹]/g, (ch) => String('۰۱۲۳۴۵۶۷۸۹'.indexOf(ch)));
const m = /^(\d{4})\/(\d{1,2})\/(\d{1,2})/.exec(s);
if (!m) return s;
return `${m[1]}/${m[2]!.padStart(2, '0')}/${m[3]!.padStart(2, '0')}`;
}
function asnadDocNoNum(no: string): number {
const s = String(no)
.trim()
.replace(/[۰-۹]/g, (ch) => String('۰۱۲۳۴۵۶۷۸۹'.indexOf(ch)));
const n = Number(s.replace(/[^\d.-]/g, ''));
return Number.isFinite(n) ? n : 0;
}
/** سندهای قبل از این فاکتور (تاریخ، و در همان روز شماره سند کوچک‌تر) */
function isAsnadBeforeInvoice(
item: Record<string, unknown>,
g: TahesabInvoiceGroup,
): boolean {
const d = normalizeAsnadDate(dash(item.Tarikh, ''));
const gd = normalizeAsnadDate(g.date);
if (!d || d === '—') return false;
if (d < gd) return true;
if (d > gd) return false;
return asnadDocNoNum(sanadNo(item)) < asnadDocNoNum(g.no);
}
function sumAsnadBeforeInvoice(g: TahesabInvoiceGroup, key: string): number {
let t = 0;
for (const item of asnadItems.value) {
if (!isAsnadBeforeInvoice(item, g)) continue;
const n = parseBalanceNum(item[key]);
if (n != null) t += n;
}
return t;
}
const invoicePriorWeight = ref<number | null>(null);
const invoicePriorRial = ref<number | null>(null);
const invoiceFinalWeight = ref<number | null>(null);
const invoiceFinalRial = ref<number | null>(null);
async function exportAsnadPdf(g: TahesabInvoiceGroup) {
error.value = '';
// حساب قبل = جمع اثر اسناد قبل از تاریخ/شماره این سند (نه مانده فعلی ته‌حساب)
const priorW = sumAsnadBeforeInvoice(g, 'TabdilVazn');
const priorR = sumAsnadBeforeInvoice(g, 'Mali');
const invW = sumGroupField(g, 'TabdilVazn');
const invR = sumGroupField(g, 'Mali');
invoicePriorWeight.value = priorW;
invoicePriorRial.value = priorR;
invoiceFinalWeight.value = priorW + invW;
invoiceFinalRial.value = priorR + invR;
invoicePreviewGroup.value = g;
invoicePreviewOpen.value = true;
if (!invoiceSettings.value) {
await loadInvoiceSettings();
}
const okPrint = printTahesabInvoicePdf({
group: g,
settings: invoiceSettings.value,
customerName: props.personName?.trim() || 'مشتری',
moshtariCode: currentLink.value?.moshtariCode ?? null,
targetWindow: preview,
});
if (!okPrint) {
error.value =
'خروجی فاکتور باز نشد. پاپ‌آپ مرورگر را برای این سایت مجاز کنید و دوباره بزنید.';
preview?.close();
}
}
async function loadAsnad() {
@@ -699,17 +751,17 @@ onMounted(async () => {
<div v-if="mandehFields?.length" class="balance-grid">
<article v-for="f in mandehFields" :key="f.key" class="balance-box">
<span>{{ mandehFieldLabel(f.key) }}</span>
<strong dir="ltr">{{ showValue(f.value) }}</strong>
<strong dir="ltr">{{ formatTahesabBedBes(f.value) }}</strong>
</article>
</div>
<div v-else class="balance-grid">
<article class="balance-box">
<span>مانده وزنی (طلا)</span>
<strong dir="ltr">{{ showValue(assets.goldBalance) }}</strong>
<strong dir="ltr">{{ formatTahesabBedBes(assets.goldBalance) }}</strong>
</article>
<article class="balance-box">
<span>مانده مالی</span>
<strong dir="ltr">{{ showValue(assets.rialBalance) }}</strong>
<strong dir="ltr">{{ formatTahesabBedBes(assets.rialBalance) }}</strong>
</article>
</div>
<p class="muted tiny">
@@ -746,7 +798,7 @@ onMounted(async () => {
>
<span role="cell" class="raw-name">{{ d.Name }}</span>
<span v-for="c in detailCols" :key="c.key" role="cell" dir="ltr" class="raw-num">
{{ showValue(d[c.key]) }}
{{ formatTahesabBedBes(d[c.key]) }}
</span>
</div>
</div>
@@ -755,7 +807,7 @@ onMounted(async () => {
<div class="name">{{ item.Name }}:</div>
<div class="vals">
<template v-for="(v, vi) in item.values" :key="vi">
<span dir="ltr">{{ showValue(v.amount) }}</span>
<span dir="ltr">{{ formatTahesabBedBes(v.amount) }}</span>
<span class="unit">{{ v.unit }}</span>
<span v-if="vi < item.values.length - 1" class="sep">|</span>
</template>
@@ -817,10 +869,10 @@ onMounted(async () => {
<button
type="button"
class="btn inv-pdf-btn"
title="خروجی فاکتور PDF"
title="پیش‌نمایش و چاپ فاکتور"
@click="exportAsnadPdf(g)"
>
خروجی فاکتور PDF
پیش‌نمایش فاکتور
</button>
</div>
<dl class="inv-meta">
@@ -904,7 +956,7 @@ onMounted(async () => {
</div>
<div class="sc-row cash">
<span>مالی</span>
<strong dir="ltr">{{ dash(item.Mali) }}</strong>
<strong dir="ltr">{{ formatTahesabBedBes(item.Mali) }}</strong>
</div>
<div class="sc-row">
<span>شرح</span>
@@ -1033,6 +1085,18 @@ onMounted(async () => {
v-model:open="invoiceSettingsOpen"
@saved="onInvoiceSettingsSaved"
/>
<TahesabInvoicePreviewModal
v-model:open="invoicePreviewOpen"
:group="invoicePreviewGroup"
:settings="invoiceSettings"
:customer-name="personName?.trim() || 'مشتری'"
:customer-phone="personPhone?.trim() || ''"
:moshtari-code="currentLink?.moshtariCode ?? null"
:prior-weight="invoicePriorWeight"
:prior-rial="invoicePriorRial"
:final-weight="invoiceFinalWeight"
:final-rial="invoiceFinalRial"
/>
</div>
</template>
@@ -0,0 +1,705 @@
<script setup lang="ts">
import { computed } from 'vue';
import type { TahesabInvoiceSettingsDto } from '@mento/shared';
import { formatJalaliDateTime, toFaDigits } from '@/lib/jalali';
import { useEscClose } from '@/lib/modal-stack';
import { formatTahesabBedBes } from '@/lib/tahesab';
import {
aggregateSameNameInvoiceRows,
dashInvoice,
fmtInvoiceNum,
pickInvoiceDarsad,
pickInvoiceMazaneh,
printTahesabInvoicePdf,
rowInvoiceDesc,
formatInvoiceWeightBalance,
sumInvoiceMali,
tabdilVaznColumnLabel,
type TahesabInvoiceGroup,
} from '@/lib/tahesab-invoice-pdf';
const props = defineProps<{
open: boolean;
group: TahesabInvoiceGroup | null;
settings: TahesabInvoiceSettingsDto | null;
customerName: string;
customerPhone?: string;
moshtariCode?: number | null;
priorWeight?: number | null;
priorRial?: number | null;
finalWeight?: number | null;
finalRial?: number | null;
}>();
const emit = defineEmits<{
'update:open': [value: boolean];
}>();
useEscClose(
() => props.open,
() => close(),
);
function close() {
emit('update:open', false);
}
const first = computed(() => props.group?.rows[0] ?? {});
const temporary = computed(() => first.value.isSanadMovaghat === true);
const issuerName = computed(
() => dashInvoice(first.value.User, '') || '—',
);
const invoiceRows = computed(() =>
props.group ? aggregateSameNameInvoiceRows(props.group.rows) : [],
);
const total = computed(() => sumInvoiceMali(invoiceRows.value));
const sourceRows = computed(
() => props.group?.rows ?? invoiceRows.value,
);
const totalVaznLabel = computed(() =>
formatInvoiceWeightBalance(sourceRows.value, 'Vazn', false),
);
const totalTabdilLabel = computed(() =>
formatInvoiceWeightBalance(sourceRows.value, 'TabdilVazn', true),
);
const tabdilColLabel = computed(() =>
tabdilVaznColumnLabel(sourceRows.value),
);
const company = computed(
() => props.settings?.companyName?.trim() || 'فاکتور ته‌حساب',
);
const generatedAtLabel = computed(() =>
props.open ? formatJalaliDateTime(new Date()) : '',
);
function printPdf() {
if (!props.group) return;
const ok = printTahesabInvoicePdf({
group: props.group,
settings: props.settings,
customerName: props.customerName,
customerPhone: props.customerPhone,
moshtariCode: props.moshtariCode,
priorWeight: props.priorWeight,
priorRial: props.priorRial,
finalWeight: props.finalWeight,
finalRial: props.finalRial,
});
if (!ok) {
// در مرورگر Cursor پاپ‌آپ کار نمی‌کند؛ پیش‌نمایش همین‌جا کافی است
window.alert(
'چاپ در این مرورگر باز نشد. از Chrome استفاده کنید یا از منوی چاپ سیستم روی همین پیش‌نمایش استفاده کنید.',
);
}
}
</script>
<template>
<div v-if="open && group" class="backdrop" @click.self="close">
<div
class="sheet"
role="dialog"
aria-modal="true"
aria-label="پیش‌نمایش فاکتور ته‌حساب"
>
<header class="sheet-head">
<div>
<h3>پیش‌نمایش فاکتور ته‌حساب</h3>
<p class="muted">
سند {{ toFaDigits(group.no) }} · همین‌جا قابل بررسی است (بدون پاپ‌آپ)
</p>
</div>
<div class="head-actions">
<button type="button" class="btn" @click="printPdf">چاپ / PDF</button>
<button type="button" class="btn ghost" @click="close">بستن</button>
</div>
</header>
<div class="paper">
<div class="top">
<div class="top-side top-start">
<p v-if="settings?.headerText?.trim()" class="header-text">
{{ settings.headerText }}
</p>
<p class="org">{{ company }}</p>
<div v-if="settings?.phone?.trim()" class="dim">
تلفن: <bdi dir="ltr">{{ settings.phone }}</bdi>
</div>
</div>
<div class="top-center">
<img
v-if="settings?.logoDataUrl"
class="logo"
:src="settings.logoDataUrl"
alt=""
/>
</div>
<div class="top-side top-end">
<div class="doc-date">
تاریخ: {{ toFaDigits(group.date) }}
</div>
<div>
شماره سند:
<strong>{{ toFaDigits(group.no) }}</strong>
</div>
<div
v-if="settings?.companySubtitle?.trim()"
class="sub"
dir="ltr"
>
{{ settings.companySubtitle }}
</div>
<span v-if="temporary" class="badge">سند موقت</span>
</div>
</div>
<div class="party">
<span class="party-item">
حساب:
<strong>{{ customerName || '—' }}</strong>
</span>
<span class="party-item">
کد:
<strong class="ltr">{{
moshtariCode != null ? toFaDigits(String(moshtariCode)) : '—'
}}</strong>
</span>
<span class="party-item party-phone">
موبایل:
<strong class="ltr">{{
customerPhone?.trim()
? toFaDigits(customerPhone.trim())
: '—'
}}</strong>
</span>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th class="col-radif">ردیف</th>
<th class="col-desc">شرح</th>
<th class="col-darsad">درصد</th>
<th>عیار</th>
<th>وزن</th>
<th class="col-tabdil">{{ tabdilColLabel }}</th>
<th>مظنه/فی</th>
<th class="col-mali">مبلغ (ریال)</th>
</tr>
</thead>
<tbody>
<tr
v-for="(r, i) in invoiceRows"
:key="String(r.ID ?? r.No_Kar ?? i)"
>
<td class="col-radif">
{{ toFaDigits(dashInvoice(r.Radif, String(i + 1))) }}
</td>
<td class="col-desc">{{ rowInvoiceDesc(r) }}</td>
<td class="ltr col-darsad">
{{ fmtInvoiceNum(pickInvoiceDarsad(r)) }}
</td>
<td class="ltr">{{ fmtInvoiceNum(r.Ayar) }}</td>
<td class="ltr">{{ fmtInvoiceNum(r.Vazn) }}</td>
<td class="ltr col-tabdil">
{{ formatTahesabBedBes(r.TabdilVazn) }}
</td>
<td class="ltr">{{ fmtInvoiceNum(pickInvoiceMazaneh(r)) }}</td>
<td class="ltr col-mali">
{{ formatTahesabBedBes(r.Mali) }}
</td>
</tr>
</tbody>
<tfoot>
<tr class="totals-row">
<td colspan="3" class="totals-label">مجموع کل اجناس</td>
<td></td>
<td class="ltr weight-sum">{{ totalVaznLabel }}</td>
<td class="ltr weight-sum col-tabdil">{{ totalTabdilLabel }}</td>
<td></td>
<td class="ltr col-mali">{{ formatTahesabBedBes(total) }}</td>
</tr>
</tfoot>
</table>
</div>
<div class="invoice-balance">
<div class="ib-when">
<span class="ib-k">تاریخ ایجاد:</span>
{{ generatedAtLabel }}
</div>
<div class="ib-label">مانده این فاکتور:</div>
<div class="ib-cell">
<span class="ib-k">مانده وزنی:</span>
<strong class="ib-val" dir="ltr">{{ totalTabdilLabel }}</strong>
</div>
<div class="ib-cell">
<span class="ib-k">مانده ریالی:</span>
<strong class="ib-val" dir="ltr">{{
formatTahesabBedBes(total)
}}</strong>
</div>
</div>
<div class="account-row">
<div class="mesghal-box">
<span class="ib-k">قیمت یک مثقال نقره خام:</span>
<span class="mesghal-blank" aria-hidden="true" />
</div>
<div class="account-box">
<div class="acc-line">
<span class="acc-title">حساب قبل:</span>
<span class="acc-pair">
<span class="ib-k">وزنی</span>
<strong dir="ltr">{{ formatTahesabBedBes(priorWeight) }}</strong>
</span>
<span class="acc-pair">
<span class="ib-k">ریالی</span>
<strong dir="ltr">{{ formatTahesabBedBes(priorRial) }}</strong>
</span>
</div>
<div class="acc-line">
<span class="acc-title">مانده نهایی:</span>
<span class="acc-pair">
<span class="ib-k">وزنی</span>
<strong dir="ltr">{{ formatTahesabBedBes(finalWeight) }}</strong>
</span>
<span class="acc-pair">
<span class="ib-k">ریالی</span>
<strong dir="ltr">{{ formatTahesabBedBes(finalRial) }}</strong>
</span>
</div>
</div>
</div>
<footer class="page-foot">
<p v-if="settings?.footerText?.trim()" class="note foot">
{{ settings.footerText }}
</p>
<div class="sign-row">
<div class="sign-col">
<span class="ib-k">صادر کننده سند:</span>
<strong class="sign-name">{{ issuerName }}</strong>
<div class="sign-space" aria-hidden="true"></div>
</div>
<div class="sign-addr">
{{ settings?.address?.trim() || '—' }}
</div>
<div class="sign-col">
<span class="ib-k">گیرنده سند:</span>
<strong class="sign-name">{{ customerName || '—' }}</strong>
<div class="sign-space" aria-hidden="true"></div>
</div>
</div>
</footer>
</div>
</div>
</div>
</template>
<style scoped>
.backdrop {
position: fixed;
inset: 0;
z-index: 85;
background: rgba(15, 23, 42, 0.45);
display: grid;
place-items: center;
padding: 12px;
}
.sheet {
width: min(920px, 100%);
max-height: min(92vh, 960px);
overflow: auto;
background: #f3f4f6;
border-radius: 12px;
border: 1px solid #e5e7eb;
padding: 12px;
display: flex;
flex-direction: column;
gap: 12px;
}
.sheet-head {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: flex-start;
flex-wrap: wrap;
background: #fff;
border-radius: 8px;
padding: 10px 12px;
}
.sheet-head h3 {
margin: 0;
font-size: 1rem;
}
.muted {
margin: 4px 0 0;
color: #6b7280;
font-size: 0.85rem;
}
.head-actions {
display: flex;
gap: 8px;
}
.paper {
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 20px 22px;
color: #111827;
font-size: 13px;
}
.top {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 12px 16px;
align-items: center;
margin-bottom: 6px;
border: 1px solid #111827;
border-radius: 10px;
padding: 12px 14px;
}
.top-side {
min-width: 0;
}
.top-start {
text-align: right;
}
.top-end {
text-align: left;
}
.top-center {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.header-text {
margin: 0 0 8px;
white-space: pre-wrap;
font-size: 16px;
font-weight: 800;
line-height: 1.5;
color: #111827;
}
.logo {
max-height: 110px;
max-width: 220px;
object-fit: contain;
}
.org {
font-size: 16px;
font-weight: 800;
margin: 0;
}
.sub {
color: #374151;
margin-top: 8px;
direction: ltr;
unicode-bidi: isolate;
text-align: left;
font-size: 13px;
}
.dim {
color: #6b7280;
margin-top: 2px;
}
.doc-date {
font-weight: 800;
font-size: 15px;
margin-bottom: 4px;
}
.badge {
display: inline-block;
margin-top: 4px;
padding: 2px 8px;
border: 1px solid #d97706;
color: #b45309;
border-radius: 999px;
font-size: 11px;
}
.party {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 20px;
margin-bottom: 14px;
border: 1px solid #111827;
border-radius: 10px;
padding: 8px 14px;
font-size: 13px;
}
.party-item {
white-space: nowrap;
}
.party-phone {
margin-inline-start: auto;
}
.page-foot {
margin-top: 16px;
display: flex;
flex-direction: column;
gap: 10px;
}
.note {
background: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 8px 10px;
margin: 0;
white-space: pre-wrap;
}
.sign-row {
display: grid;
grid-template-columns: 1fr 1.2fr 1fr;
gap: 12px;
align-items: start;
margin-top: 4px;
}
.sign-col {
display: flex;
flex-direction: column;
gap: 4px;
text-align: center;
}
.sign-name {
font-size: 13px;
font-weight: 800;
}
.sign-space {
min-height: 56px;
margin-top: 4px;
border-bottom: 1px dashed #d1d5db;
}
.sign-addr {
color: #6b7280;
font-size: 12px;
text-align: center;
line-height: 1.55;
max-width: 100%;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
padding-top: 2px;
}
.table-wrap {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
th,
td {
padding: 7px 8px;
border: 1px solid #e5e7eb;
text-align: right;
vertical-align: top;
}
th {
background: #f3f4f6;
font-size: 11px;
text-align: center;
vertical-align: middle;
}
.col-radif {
width: 5%;
white-space: nowrap;
padding-inline: 4px;
text-align: center;
vertical-align: middle;
}
.col-desc {
width: 32%;
word-break: break-word;
}
.col-darsad {
width: 6%;
white-space: nowrap;
padding-inline: 4px;
}
.col-tabdil {
width: 14%;
white-space: nowrap;
}
.col-mali {
width: 13%;
white-space: nowrap;
}
.ltr {
direction: ltr;
unicode-bidi: isolate;
text-align: left;
}
.totals-row td {
font-weight: 800;
background: #f9fafb;
vertical-align: middle;
}
.totals-label {
text-align: right;
}
.weight-sum {
font-size: 11px;
white-space: nowrap;
line-height: 1.35;
}
.invoice-balance {
display: grid;
grid-template-columns: 1.2fr 1fr 1fr 1fr;
gap: 0;
margin-top: 10px;
border: 1px solid #111827;
border-radius: 10px;
overflow: hidden;
font-size: 13px;
background: #f3f4f6;
}
.invoice-balance > div {
padding: 8px 12px;
}
.invoice-balance > .ib-cell {
border-inline-start: 1px solid #e5e7eb;
}
.ib-when {
display: flex;
align-items: center;
gap: 6px;
font-weight: 600;
white-space: nowrap;
}
.ib-label {
font-weight: 800;
display: flex;
align-items: center;
justify-content: left;
text-align: left;
}
.ib-cell {
position: relative;
display: flex;
align-items: center;
width: 100%;
min-height: 1.4em;
padding-left: 7.5rem;
box-sizing: border-box;
white-space: nowrap;
}
.ib-k {
color: #6b7280;
font-size: 12px;
}
.ib-val {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
text-align: left;
unicode-bidi: isolate;
font-weight: 800;
}
.account-row {
display: grid;
grid-template-columns: 1fr 1.4fr;
gap: 10px;
margin-top: 10px;
}
.mesghal-box {
border: 1px solid #111827;
border-radius: 10px;
background: transparent;
padding: 8px 12px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
min-height: 2.5rem;
}
.mesghal-blank {
flex: 1;
min-width: 4rem;
border-bottom: 1px dotted #9ca3af;
align-self: flex-end;
margin-bottom: 0.35rem;
margin-inline-start: 0.5rem;
}
.account-box {
border: 1px solid #111827;
border-radius: 10px;
background: transparent;
overflow: hidden;
display: flex;
flex-direction: column;
}
.acc-line {
display: grid;
grid-template-columns: auto 1fr 1fr;
align-items: stretch;
gap: 0;
border-top: 1px solid #e5e7eb;
}
.acc-line:first-child {
border-top: 0;
}
.acc-title {
font-weight: 800;
white-space: nowrap;
padding: 6px 12px;
display: flex;
align-items: center;
}
.acc-pair {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
min-width: 0;
padding: 6px 12px;
border-inline-start: 1px solid #e5e7eb;
}
.acc-pair strong {
font-weight: 800;
unicode-bidi: isolate;
}
.btn {
border: 0;
border-radius: 8px;
padding: 0.45rem 0.8rem;
background: var(--accent, #0d9488);
color: #fff;
font-weight: 600;
cursor: pointer;
}
.btn.ghost {
background: transparent;
color: #111827;
border: 1px solid #e5e7eb;
}
@media (max-width: 720px) {
.party-phone {
margin-inline-start: 0;
}
.top {
grid-template-columns: 1fr;
justify-items: stretch;
}
.top-center {
order: -1;
}
.top-end {
text-align: right;
}
.invoice-balance {
grid-template-columns: 1fr 1fr;
}
.account-row {
grid-template-columns: 1fr;
}
.sign-row {
grid-template-columns: 1fr;
}
}
</style>