diff --git a/apps/api/src/integrations/tahesab/balance.service.ts b/apps/api/src/integrations/tahesab/balance.service.ts index 3d2dc2f..04c69c7 100644 --- a/apps/api/src/integrations/tahesab/balance.service.ts +++ b/apps/api/src/integrations/tahesab/balance.service.ts @@ -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, diff --git a/apps/api/src/integrations/tahesab/tahesab-methods.ts b/apps/api/src/integrations/tahesab/tahesab-methods.ts index 103ddcc..be673b4 100644 --- a/apps/api/src/integrations/tahesab/tahesab-methods.ts +++ b/apps/api/src/integrations/tahesab/tahesab-methods.ts @@ -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> } | 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; + 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 { diff --git a/apps/web/src/components/person-assets/PersonAssetsPanel.vue b/apps/web/src/components/person-assets/PersonAssetsPanel.vue index 8a57b8b..619f4cc 100644 --- a/apps/web/src/components/person-assets/PersonAssetsPanel.vue +++ b/apps/web/src/components/person-assets/PersonAssetsPanel.vue @@ -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 = { + 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 = { + 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) { + 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 = { '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) { + 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([ + ...ASNAD_HEAD_KEYS, + ...INVOICE_COLS.map((c) => c.key), + ...INVOICE_HEAD_KEYS, + 'No_Kar', + 'Sharh1', + 'Jens_Felez', +]); + +function invoiceCols(rows: Record[]) { + return INVOICE_COLS.filter((c) => rows.some((r) => !isZeroish(r[c.key]))); +} + +function invoiceExtra(r: Record) { + 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 = { - مالی: [], - 'فلزات/وزنی': [], - سکه: [], - کالا: [], - ارز: [], - سایر: [], - }; - 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( + localStorage.getItem(ASNAD_VIEW_KEY) === 'rows' ? 'rows' : 'grouped', +); +watch(asnadView, (v) => localStorage.setItem(ASNAD_VIEW_KEY, v)); + +function sanadNo(item: Record) { + return dash(item.Sh_Factor ?? item.Sh_factor, ''); +} + +/** ته‌حساب هر ردیف سند را جدا برمی‌گرداند؛ ردیف‌های هم‌شماره و هم‌تاریخ یک سند هستند (فقط دسته‌بندی، بدون محاسبه) */ +const asnadGroups = computed(() => { + const map = new Map[] }>(); + 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 () => {