Tahesab: show raw data from Tahesab, invoice-style document view with metal labels

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-08 13:49:54 +03:30
co-authored by Cursor
parent 2f3646d085
commit e6755d0c2e
4 changed files with 608 additions and 100 deletions
@@ -6,7 +6,7 @@ import {
import { Prisma } from '@prisma/client';
import { PrismaService } from '../../prisma/prisma.service';
import { TahesabConnectionsService } from './connections.service';
import { normalizeMandehResponse } from './tahesab-methods';
import { extractMandehRaw, normalizeMandehResponse } from './tahesab-methods';
@Injectable()
export class TahesabBalanceService {
@@ -78,11 +78,14 @@ export class TahesabBalanceService {
data: { lastBalanceSyncedAt: cache.fetchedAt },
});
const rawView = extractMandehRaw(raw);
return {
connectionId: resolvedId,
connectionLabel: link.connection.label,
dbName: link.connection.dbName,
moshtariCode: link.moshtariCode,
mandehFields: rawView.fields,
mandehDetailRaw: rawView.details,
goldBalance: normalized.goldBalance,
rialBalance: normalized.rialBalance,
currencyBalance: normalized.currencyBalance,
@@ -116,11 +119,14 @@ export class TahesabBalanceService {
return this.getAssets(organizationId, personId, connectionId);
}
const c = link.balanceCache;
const rawView = extractMandehRaw(c.rawResponse);
return {
connectionId: resolvedId,
connectionLabel: link.connection.label,
dbName: link.connection.dbName,
moshtariCode: link.moshtariCode,
mandehFields: rawView.fields,
mandehDetailRaw: rawView.details,
goldBalance: c.goldBalance,
rialBalance: c.rialBalance,
currencyBalance: c.currencyBalance,
@@ -72,6 +72,44 @@ function detailStatus(amount: number): 'بس' | 'بد' {
return amount < 0 ? 'بس' : 'بد';
}
function rawStr(v: unknown): string | null {
if (v === null || v === undefined) return null;
const s = String(v);
return s === 'null' || s === 'undefined' ? null : s;
}
/** فیلدهای پاسخ مانده بدون هیچ تبدیل/محاسبه، به ترتیب خود ته‌حساب */
export function extractMandehRaw(raw: unknown): {
fields: Array<{ key: string; value: string | null }>;
details: Array<{
Name: string;
Value: string | null;
Value1: string | null;
Value2: string | null;
Value3: string | null;
Value4: string | null;
}>;
} {
const row = (raw as { MandeHesab?: Array<Record<string, unknown>> } | null)
?.MandeHesab?.[0];
if (!row || typeof row !== 'object') return { fields: [], details: [] };
const fields = Object.entries(row)
.filter(([k, v]) => k !== 'details' && (v === null || typeof v !== 'object'))
.map(([key, v]) => ({ key, value: rawStr(v) }));
const details = (Array.isArray(row.details) ? row.details : []).map((item) => {
const it = (item ?? {}) as Record<string, unknown>;
return {
Name: String(it.Name ?? ''),
Value: rawStr(it.Value),
Value1: rawStr(it.Value1),
Value2: rawStr(it.Value2),
Value3: rawStr(it.Value3),
Value4: rawStr(it.Value4),
};
});
return { fields, details };
}
/** نرمال‌سازی پاسخ getmandehesabbycode مثل همکار */
export function normalizeMandehResponse(raw: unknown): NormalizedAssets {
const root = raw as {
@@ -2,7 +2,6 @@
import { computed, onMounted, ref, watch } from 'vue';
import axios from 'axios';
import { api } from '@/lib/api';
import { formatMoney } from '@/lib/money';
import {
dateToJalali,
formatClockFa,
@@ -79,8 +78,171 @@ const currentLink = computed(() =>
const hasMultipleConnections = computed(() => connections.value.length > 1);
const goldBalance = computed(() => assets.value?.goldBalance ?? null);
const rialBalance = computed(() => assets.value?.rialBalance ?? null);
/** برچسب فارسی فیلدهای شناخته‌شدهٔ پاسخ مانده؛ کلیدهای ناشناخته با همان نام خام نمایش داده می‌شوند */
const MANDEH_FIELD_LABELS: Record<string, string> = {
Code: 'کد حساب',
MandeyeMali: 'مانده مالی',
MandeyeVazni: 'مانده وزنی (طلا)',
MandeyeVazniNoghre: 'مانده وزنی نقره',
MandeMenhayeSeke: 'مانده منهای سکه',
MandeyeKolBeTALA: 'مانده کل به طلا',
MandeyeKolBePool: 'مانده کل به پول',
};
function mandehFieldLabel(key: string) {
if (MANDEH_FIELD_LABELS[key]) return MANDEH_FIELD_LABELS[key]!;
if (key.startsWith('Seke_')) return `سکه ${key.slice(5)}`;
if (key.startsWith('MandeyeArz_')) return `مانده ارز ${key.slice(11)}`;
return key;
}
/** فقط قالب‌بندی نمایشی (جداکنندهٔ هزارگان)؛ مقدار و علامت همان پاسخ ته‌حساب است */
function faDigits(s: string) {
return s.replace(/\d/g, (d) => '۰۱۲۳۴۵۶۷۸۹'[Number(d)]!);
}
function showValue(v: unknown): string {
const s = dash(v, '');
if (!s) return '—';
if (/^-?\d+(\.\d+)?$/.test(s)) {
const [int, frac] = s.replace('-', '').split('.');
const grouped = int!.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
return `${s.startsWith('-') ? '-' : ''}${faDigits(grouped)}${frac ? `٫${faDigits(frac)}` : ''}`;
}
return s;
}
/** شناسه‌ها/کدها بدون جداکنندهٔ هزارگان */
function showRaw(v: unknown): string {
const s = dash(v, '');
return s ? faDigits(s) : '—';
}
const mandehFields = computed(() => assets.value?.mandehFields ?? null);
const mandehDetailRaw = computed(() => assets.value?.mandehDetailRaw ?? null);
const DETAIL_VALUE_COLS = [
{ key: 'Value', label: 'مقدار' },
{ key: 'Value1', label: 'وزن (Value1)' },
{ key: 'Value2', label: 'تعداد (Value2)' },
{ key: 'Value3', label: 'ارزی (Value3)' },
{ key: 'Value4', label: 'ریالی (Value4)' },
] as const;
const detailCols = computed(() =>
DETAIL_VALUE_COLS.filter((c) => (mandehDetailRaw.value ?? []).some((d) => d[c.key] != null)),
);
/** برچسب فیلدهای سند ته‌حساب (DoListAsnad) */
const ASNAD_FIELD_LABELS: Record<string, string> = {
NO: 'نوع',
Radif: 'ردیف',
Vazn: 'وزن',
Ayar: 'عیار',
TabdilVazn: 'وزن تبدیلی',
Count: 'تعداد',
No_Kar: 'نوع کار',
Fi: 'فی',
Mazaneh: 'مظنه',
Mali: 'مالی',
Arz: 'ارز',
Name_Bank: 'بانک',
Tarikh_Cheq: 'تاریخ چک',
Sh_Cheq: 'شماره چک',
Sh_Posht_Check: 'شماره پشت چک',
Operation_Date: 'تاریخ عملیات',
Sh_Sharti: 'شماره شرطی',
Name_az: 'آزمایشگاه',
Baabat: 'بابت',
Sharh1: 'شرح',
Sharh2: 'شرح ۲',
TasirBarMande: 'تأثیر بر مانده',
TahesabVazni: 'حساب وزنی',
TahesabMali: 'حساب مالی',
TahesabArzi: 'حساب ارزی',
Jens_Felez: 'جنس فلز',
Seller: 'فروشنده',
User: 'کاربر ثبت',
Tarikh_Time: 'ساعت',
ZamanSabt: 'زمان ثبت',
ZamanEdit: 'زمان ویرایش',
isSanadMovaghat: 'سند موقت',
MCode: 'کد مشتری',
Factor_Code: 'کد فاکتور',
ID: 'شناسه',
};
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']);
function asnadFields(item: Record<string, unknown>) {
const keys = Object.keys(item).filter((k) => !ASNAD_HEAD_KEYS.has(k));
keys.sort((a, b) => {
const ia = ASNAD_ORDER.indexOf(a);
const ib = ASNAD_ORDER.indexOf(b);
return (ia < 0 ? 999 : ia) - (ib < 0 ? 999 : ib);
});
return keys
.map((k) => {
const v = item[k];
if (typeof v === 'boolean') return { key: k, label: ASNAD_FIELD_LABELS[k] ?? k, value: v ? 'بله' : 'خیر' };
if (v != null && typeof v === 'object') return null;
const s = dash(v, '');
if (!s) return null;
return { key: k, label: ASNAD_FIELD_LABELS[k] ?? k, value: asnadCell(k, s) };
})
.filter((x): x is { key: string; label: string; value: string } => x != null);
}
/** کدهای جنس فلز طبق مستندات ته‌حساب */
const JENS_FELEZ_LABELS: Record<string, string> = { '0': 'طلا', '1': 'پلاتین', '2': 'نقره' };
function asnadCell(k: string, v: unknown): string {
const s = dash(v, '');
if (!s) return '—';
if (k === 'Jens_Felez') return JENS_FELEZ_LABELS[s] ?? showRaw(s);
if (typeof v === 'boolean') return v ? 'بله' : 'خیر';
return ASNAD_AMOUNT_KEYS.has(k) ? showValue(s) : showRaw(s);
}
function isZeroish(v: unknown) {
const s = dash(v, '');
return !s || /^-?0+(\.0+)?$/.test(s);
}
/** جنس فلز فقط برای ردیف‌های وزنی معنا دارد؛ ردیف‌های صرفاً مالی هم Jens_Felez=0 دارند */
function rowMetal(r: Record<string, unknown>) {
if (isZeroish(r.Vazn) && isZeroish(r.TabdilVazn)) return '—';
return asnadCell('Jens_Felez', r.Jens_Felez);
}
const INVOICE_COLS = [
{ key: 'Count', label: 'تعداد' },
{ key: 'Vazn', label: 'وزن' },
{ key: 'Ayar', label: 'عیار' },
{ key: 'TabdilVazn', label: 'وزن تبدیلی' },
{ key: 'Fi', label: 'فی' },
{ key: 'Mazaneh', label: 'مظنه' },
{ key: 'Mali', label: 'مالی' },
{ key: 'Arz', label: 'ارز' },
] as const;
const INVOICE_HEAD_KEYS = ['Tarikh_Time', 'MCode', 'User', 'isSanadMovaghat'] as const;
const INVOICE_SHOWN_KEYS = new Set<string>([
...ASNAD_HEAD_KEYS,
...INVOICE_COLS.map((c) => c.key),
...INVOICE_HEAD_KEYS,
'No_Kar',
'Sharh1',
'Jens_Felez',
]);
function invoiceCols(rows: Record<string, unknown>[]) {
return INVOICE_COLS.filter((c) => rows.some((r) => !isZeroish(r[c.key])));
}
function invoiceExtra(r: Record<string, unknown>) {
return asnadFields(r).filter((f) => !INVOICE_SHOWN_KEYS.has(f.key));
}
const fetchedAtLabel = computed(() => {
const raw = assets.value?.fetchedAt;
@@ -90,57 +252,6 @@ const fetchedAtLabel = computed(() => {
return `${formatJalaliDay(d)} ${formatClockFa(d)}`;
});
const mandehGroups = computed(() => {
const items = assets.value?.mandehDetail ?? [];
const groups: Record<string, typeof items> = {
مالی: [],
'فلزات/وزنی': [],
سکه: [],
کالا: [],
ارز: [],
سایر: [],
};
for (const item of items) {
const name = item.Name || '';
if (/ریال|تومان|مالی|پول|وجه/i.test(name)) groups['مالی']!.push(item);
else if (/سکه/i.test(name)) groups['سکه']!.push(item);
else if (/ارز|دلار|یورو/i.test(name)) groups['ارز']!.push(item);
else if (/طلا|نقره|پلاتین|وزن|آبشده|شمش/i.test(name))
groups['فلزات/وزنی']!.push(item);
else if (/کار|اتیکت|کالا/i.test(name)) groups['کالا']!.push(item);
else groups['سایر']!.push(item);
}
return Object.entries(groups)
.filter(([, list]) => list.length > 0)
.map(([label, list]) => ({ label, items: list }));
});
function balanceTone(v: number | null) {
if (v == null || v === 0) return 'tone-neutral';
return v > 0 ? 'tone-credit' : 'tone-debt';
}
function balanceLabel(v: number | null) {
if (v == null || v === 0) return '';
return v > 0 ? 'بستانکار' : 'بدهکار';
}
function formatAmt(v: number | null, digits = 3) {
if (v == null) return '—';
return Math.abs(v).toLocaleString('fa-IR', {
maximumFractionDigits: digits,
minimumFractionDigits: 0,
});
}
function formatMandehAmount(amount: string | number, unit: string) {
const n = Number(amount);
if (!Number.isFinite(n)) return String(amount);
const abs = Math.abs(n);
if (unit === 'ریال') return formatMoney(abs, 'IRR');
return abs.toLocaleString('fa-IR', { maximumFractionDigits: 3 });
}
/** ته‌حساب گاهی رشتهٔ "null" می‌فرستد؛ ?? کافی نیست */
function dash(v: unknown, fallback = '—'): string {
if (v == null) return fallback;
@@ -149,6 +260,39 @@ function dash(v: unknown, fallback = '—'): string {
return s;
}
type AsnadView = 'grouped' | 'rows';
const ASNAD_VIEW_KEY = 'mento.asnadView';
const asnadView = ref<AsnadView>(
localStorage.getItem(ASNAD_VIEW_KEY) === 'rows' ? 'rows' : 'grouped',
);
watch(asnadView, (v) => localStorage.setItem(ASNAD_VIEW_KEY, v));
function sanadNo(item: Record<string, unknown>) {
return dash(item.Sh_Factor ?? item.Sh_factor, '');
}
/** ته‌حساب هر ردیف سند را جدا برمی‌گرداند؛ ردیف‌های هم‌شماره و هم‌تاریخ یک سند هستند (فقط دسته‌بندی، بدون محاسبه) */
const asnadGroups = computed(() => {
const map = new Map<string, { no: string; date: string; rows: Record<string, unknown>[] }>();
for (const item of asnadItems.value) {
const no = sanadNo(item);
const date = dash(item.Tarikh, '');
const key = `${no}|${date}`;
let g = map.get(key);
if (!g) {
g = { no, date, rows: [] };
map.set(key, g);
}
g.rows.push(item);
}
return [...map.values()].map((g) => ({
key: `${g.no}|${g.date}`,
no: g.no || '—',
date: g.date || '—',
rows: g.rows,
}));
});
/** تبدیل تاریخ محلی میلادی به شمسی YYYY-MM-DD برای API ته‌حساب */
function toShamsiYmd(localIso: string): string | null {
const d = parseLocalInput(localIso);
@@ -488,38 +632,26 @@ onMounted(async () => {
<template v-if="currentLink && assets">
<section class="summary card">
<h3>دارایی</h3>
<div class="balance-grid">
<article class="balance-box" :class="balanceTone(goldBalance)">
<span>دارایی طلایی</span>
<strong dir="ltr">
{{ formatAmt(goldBalance) }}
<em>(گرم)</em>
</strong>
<small v-if="balanceLabel(goldBalance)">{{ balanceLabel(goldBalance) }}</small>
</article>
<article class="balance-box" :class="balanceTone(rialBalance)">
<span>دارایی ریالی</span>
<strong dir="ltr">
{{ rialBalance != null ? formatMoney(Math.abs(rialBalance), 'IRR') : '—' }}
</strong>
<small v-if="balanceLabel(rialBalance)">{{ balanceLabel(rialBalance) }}</small>
</article>
<article
v-if="assets.totalGoldEquiv != null"
class="balance-box"
:class="balanceTone(assets.totalGoldEquiv)"
>
<span>دارایی کل به طلا</span>
<strong dir="ltr">
{{ formatAmt(assets.totalGoldEquiv) }}
<em>(گرم)</em>
</strong>
<small v-if="balanceLabel(assets.totalGoldEquiv)">{{
balanceLabel(assets.totalGoldEquiv)
}}</small>
<h3>مانده حساب (طبق ته‌حساب)</h3>
<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>
</article>
</div>
<div v-else class="balance-grid">
<article class="balance-box">
<span>مانده وزنی (طلا)</span>
<strong dir="ltr">{{ showValue(assets.goldBalance) }}</strong>
</article>
<article class="balance-box">
<span>مانده مالی</span>
<strong dir="ltr">{{ showValue(assets.rialBalance) }}</strong>
</article>
</div>
<p class="muted tiny">
اعداد دقیقاً همان مقادیر ته‌حساب هستند (با همان علامت مثبت/منفی) و CRM هیچ محاسبه یا تبدیلی روی آن‌ها انجام نمی‌دهد.
</p>
<p v-if="fetchedAtLabel" class="muted tiny">آخرین دریافت: {{ fetchedAtLabel }}</p>
</section>
@@ -537,31 +669,37 @@ onMounted(async () => {
</div>
<div v-show="activeTab === 'مانده حساب'" class="detail">
<template v-for="group in mandehGroups" :key="group.label">
<div class="group-title">
<span class="line" />
<strong>{{ group.label }}</strong>
<span class="line" />
<div v-if="mandehDetailRaw?.length" class="raw-table" role="table">
<div class="raw-tr raw-th" role="row" :style="{ '--cols': detailCols.length }">
<span role="columnheader">عنوان</span>
<span v-for="c in detailCols" :key="c.key" role="columnheader">{{ c.label }}</span>
</div>
<div
v-for="(item, idx) in group.items"
:key="idx"
class="mandeh-card card"
v-for="(d, di) in mandehDetailRaw"
:key="di"
class="raw-tr"
role="row"
:style="{ '--cols': detailCols.length }"
>
<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]) }}
</span>
</div>
</div>
<template v-else-if="assets.mandehDetail?.length">
<div v-for="(item, idx) in assets.mandehDetail" :key="idx" class="mandeh-card card">
<div class="name">{{ item.Name }}:</div>
<div class="vals">
<template v-for="(v, vi) in item.values" :key="vi">
<span :class="Number(v.amount) >= 0 ? 'credit' : 'debt'">
{{ formatMandehAmount(v.amount, v.unit) }}
</span>
<span dir="ltr">{{ showValue(v.amount) }}</span>
<span class="unit">{{ v.unit }}</span>
<span class="st">{{ Number(v.amount) >= 0 ? 'بد' : 'بس' }}</span>
<span v-if="vi < item.values.length - 1" class="sep">|</span>
</template>
</div>
</div>
</template>
<p v-if="!mandehGroups.length" class="muted">جزئیات مانده‌ای نیست.</p>
<p v-else class="muted">جزئیات مانده‌ای نیست.</p>
</div>
<div v-show="activeTab === 'جزئیات حساب'" class="asnad">
@@ -578,7 +716,101 @@ onMounted(async () => {
{{ asnadLoading ? '…' : 'نمایش اسناد' }}
</button>
</div>
<div class="asnad-grid">
<div v-if="asnadItems.length" class="view-bar">
<span class="muted">
{{ asnadGroups.length.toLocaleString('fa-IR') }} سند ·
{{ asnadItems.length.toLocaleString('fa-IR') }} ردیف
</span>
<div class="seg" role="tablist" aria-label="نحوه نمایش اسناد">
<button
type="button"
:class="{ on: asnadView === 'grouped' }"
:aria-selected="asnadView === 'grouped'"
@click="asnadView = 'grouped'"
>
هر سند یکجا
</button>
<button
type="button"
:class="{ on: asnadView === 'rows' }"
:aria-selected="asnadView === 'rows'"
@click="asnadView = 'rows'"
>
ردیف‌به‌ردیف
</button>
</div>
</div>
<div v-if="asnadView === 'grouped'" class="asnad-groups">
<article v-for="g in asnadGroups" :key="g.key" class="invoice card">
<header class="inv-head">
<div class="inv-title">
<strong>سند شماره {{ showRaw(g.no) }}</strong>
<span v-if="g.rows[0]?.isSanadMovaghat === true" class="inv-badge">موقت</span>
<span class="sg-count">{{ g.rows.length.toLocaleString('fa-IR') }} ردیف</span>
</div>
<dl class="inv-meta">
<div>
<dt>تاریخ</dt>
<dd>{{ showRaw(g.date) }}</dd>
</div>
<div v-for="k in INVOICE_HEAD_KEYS.filter((x) => x !== 'isSanadMovaghat' && dash(g.rows[0]?.[x], ''))" :key="k">
<dt>{{ ASNAD_FIELD_LABELS[k] }}</dt>
<dd>{{ asnadCell(k, g.rows[0]?.[k]) }}</dd>
</div>
</dl>
</header>
<div class="inv-table-wrap">
<table class="inv-table">
<thead>
<tr>
<th>#</th>
<th class="inv-desc">شرح</th>
<th>جنس فلز</th>
<th v-for="c in invoiceCols(g.rows)" :key="c.key">{{ c.label }}</th>
</tr>
</thead>
<tbody>
<template v-for="(r, ri) in g.rows" :key="String(r.Factor_Code ?? r.ID ?? ri)">
<tr>
<td class="inv-idx">{{ showRaw(r.Radif ?? ri + 1) }}</td>
<td class="inv-desc">
<strong>{{ dash(r.NO) }}</strong>
<small v-if="dash(r.No_Kar, '')">{{ dash(r.No_Kar) }}</small>
<small v-if="dash(r.Sharh1, '')">{{ dash(r.Sharh1) }}</small>
</td>
<td>{{ rowMetal(r) }}</td>
<td v-for="c in invoiceCols(g.rows)" :key="c.key" class="inv-num">
<bdi dir="ltr">{{ asnadCell(c.key, r[c.key]) }}</bdi>
</td>
</tr>
<tr v-if="invoiceExtra(r).length" class="inv-extra-tr">
<td :colspan="3 + invoiceCols(g.rows).length">
<details>
<summary>سایر جزئیات ته‌حساب</summary>
<dl class="kv">
<div
v-for="f in invoiceExtra(r)"
:key="f.key"
class="kv-item"
:class="`k-${f.key}`"
>
<dt>{{ f.label }}</dt>
<dd>{{ f.value }}</dd>
</div>
</dl>
</details>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</article>
</div>
<div v-else class="asnad-grid">
<article
v-for="(item, idx) in asnadItems"
:key="String(item.Factor_Code ?? item.ID ?? idx)"
@@ -606,6 +838,9 @@ onMounted(async () => {
</div>
</article>
</div>
<p v-if="asnadItems.length && asnadView === 'grouped'" class="muted tiny">
همهٔ فیلدهایی که ته‌حساب برای هر ردیف برگردانده نمایش داده می‌شود؛ فیلدهای خالی حذف شده‌اند و هیچ جمع یا تبدیلی در CRM انجام نمی‌شود.
</p>
<p v-if="!asnadLoading && !asnadItems.length" class="muted">
در این بازه سندی یافت نشد.
</p>
@@ -906,12 +1141,165 @@ onMounted(async () => {
gap: 0.65rem;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
}
.view-bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
flex-wrap: wrap;
margin-bottom: 0.65rem;
}
.seg {
display: inline-flex;
padding: 3px;
gap: 3px;
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--bg);
}
.seg button {
border: 0;
background: transparent;
color: var(--text);
padding: 0.35rem 0.75rem;
border-radius: calc(var(--radius) - 2px);
font: inherit;
font-size: 0.85rem;
cursor: pointer;
}
.seg button.on {
background: var(--accent);
color: #fff;
}
.asnad-groups {
display: grid;
gap: 1rem;
}
.invoice {
display: grid;
gap: 0.6rem;
border-top: 3px solid var(--accent);
}
.inv-head {
display: grid;
gap: 0.5rem;
padding: 0.6rem 0.75rem;
border-radius: var(--radius);
background: color-mix(in srgb, var(--accent) 10%, transparent);
border: 1px solid color-mix(in srgb, var(--accent) 25%, transparent);
}
.inv-title strong {
color: var(--accent);
}
.inv-title {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.95rem;
}
.inv-badge {
font-size: 0.72rem;
padding: 0.1rem 0.45rem;
border-radius: 999px;
background: #fef3c7;
color: #92400e;
}
.inv-meta {
margin: 0;
display: flex;
flex-wrap: wrap;
gap: 0.35rem 1.25rem;
}
.inv-meta div {
display: flex;
gap: 0.35rem;
align-items: baseline;
}
.inv-meta dt {
font-size: 0.76rem;
color: var(--muted);
}
.inv-meta dd {
margin: 0;
font-size: 0.86rem;
font-weight: 600;
}
.inv-table-wrap {
overflow-x: auto;
border: 1px solid var(--line);
border-radius: var(--radius);
}
.inv-table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
}
.inv-table th {
background: var(--bg);
color: var(--muted);
font-weight: 600;
font-size: 0.76rem;
text-align: start;
padding: 0.45rem 0.55rem;
white-space: nowrap;
border-bottom: 1px solid var(--line);
}
.inv-table td {
padding: 0.45rem 0.55rem;
border-bottom: 1px solid var(--line);
vertical-align: top;
white-space: nowrap;
}
.inv-table tbody tr:last-child td {
border-bottom: 0;
}
.inv-table tr:has(+ .inv-extra-tr) td {
border-bottom: 0;
}
.inv-num,
.inv-idx {
font-variant-numeric: tabular-nums;
}
.inv-idx {
color: var(--muted);
width: 2rem;
}
.inv-table .inv-desc {
white-space: normal;
min-width: 180px;
}
.inv-desc strong {
display: block;
}
.inv-desc small {
display: block;
color: var(--muted);
font-size: 0.76rem;
}
.inv-extra-tr td {
padding-top: 0;
white-space: normal;
}
.inv-extra-tr summary {
cursor: pointer;
font-size: 0.76rem;
color: var(--muted);
margin-bottom: 0.35rem;
}
.sg-count {
margin-inline-start: auto;
font-size: 0.78rem;
padding: 0.1rem 0.5rem;
border-radius: 999px;
background: var(--bg);
border: 1px solid var(--line);
}
.sc-head {
display: flex;
justify-content: space-between;
gap: 0.5rem;
font-size: 0.85rem;
margin-bottom: 0.4rem;
margin-bottom: 0.5rem;
}
.sc-row {
display: flex;
@@ -928,6 +1316,63 @@ onMounted(async () => {
.sc-row.cash {
background: color-mix(in srgb, #16a34a 12%, transparent);
}
.kv {
margin: 0;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 0.35rem;
}
.kv-item {
min-width: 0;
padding: 0.3rem 0.45rem;
border-radius: calc(var(--radius) - 2px);
background: var(--bg);
}
.kv dt {
font-size: 0.72rem;
color: var(--muted);
}
.kv dd {
margin: 0;
font-size: 0.86rem;
font-weight: 600;
overflow-wrap: anywhere;
font-variant-numeric: tabular-nums;
}
.kv-item.k-Sharh1,
.kv-item.k-Sharh2,
.kv-item.k-Factor_Code {
grid-column: 1 / -1;
}
.raw-table {
display: grid;
border: 1px solid var(--line);
border-radius: var(--radius);
overflow-x: auto;
}
.raw-tr {
display: grid;
grid-template-columns: minmax(150px, 1.4fr) repeat(var(--cols), minmax(110px, 1fr));
gap: 0.5rem;
padding: 0.5rem 0.7rem;
border-top: 1px solid var(--line);
font-size: 0.88rem;
align-items: center;
}
.raw-th {
border-top: 0;
background: var(--bg);
color: var(--muted);
font-size: 0.78rem;
font-weight: 700;
}
.raw-name {
font-weight: 600;
}
.raw-num {
text-align: right;
font-variant-numeric: tabular-nums;
}
.sanad-form {
display: grid;
gap: 0.65rem;
+19
View File
@@ -3415,11 +3415,30 @@ export interface MandehDetailItemDto {
values: MandehValueDto[];
}
/** یک فیلد سطح بالای پاسخ GetMandeHesabByCode، دقیقاً همان مقدار ته‌حساب */
export interface TahesabMandehFieldDto {
key: string;
value: string | null;
}
/** یک ردیف details پاسخ مانده، بدون هیچ تبدیل */
export interface TahesabMandehDetailRawDto {
Name: string;
Value: string | null;
Value1: string | null;
Value2: string | null;
Value3: string | null;
Value4: string | null;
}
export interface PersonAssetsDto {
connectionId: string;
connectionLabel: string;
dbName: string;
moshtariCode: number;
/** فیلدهای خام ته‌حساب به همان ترتیب پاسخ */
mandehFields?: TahesabMandehFieldDto[];
mandehDetailRaw?: TahesabMandehDetailRawDto[];
goldBalance: number | null;
rialBalance: number | null;
currencyBalance: number | null;