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>
706 lines
17 KiB
Vue
706 lines
17 KiB
Vue
<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>
|