From 92d7afc10371b4a68490b67fa33a34062039991b Mon Sep 17 00:00:00 2001 From: Shayan Azadi Date: Tue, 4 Aug 2026 19:21:37 +0330 Subject: [PATCH] =?UTF-8?q?Harden=20withdrawals=20freshness=20and=20add=20?= =?UTF-8?q?=D9=85=D8=B9=DB=8C=D9=86/=D8=AA=D9=81=D8=B5=DB=8C=D9=84=20on=20?= =?UTF-8?q?accounting=20vouchers.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cache-bust and soft-refresh withdrawal lists for CDN, and capture party detail (تفصیل) plus بابت purpose on journal documents. Co-authored-by: Cursor --- index.html | 2 + nginx.conf | 9 ++ public/favicon.ico | Bin 0 -> 1150 bytes public/favicon.svg | 4 + src/accounting/voucherSync.ts | 17 ++- src/api/client.ts | 18 ++- src/pages/Vouchers.tsx | 115 +++++++++++++++--- src/pages/sales/salesUtils.ts | 75 +++++++++--- .../sales/usePendingCustomerWithdrawals.ts | 79 ++++++++---- src/parties/useFunZoneParties.ts | 16 ++- src/types/index.ts | 11 +- src/utils/voucherFilters.ts | 15 ++- 12 files changed, 292 insertions(+), 69 deletions(-) create mode 100644 public/favicon.ico create mode 100644 public/favicon.svg diff --git a/index.html b/index.html index 514bc22..a3fa7cc 100644 --- a/index.html +++ b/index.html @@ -3,6 +3,8 @@ + + سپیدار فان‌زون | سیستم حسابداری vYhz#zuJz@P!dKp~(AL>x#lH~{6)L0%dE|4}>|2BT?UWTpZ1 I@_ + + + diff --git a/src/accounting/voucherSync.ts b/src/accounting/voucherSync.ts index 763e223..25fee73 100644 --- a/src/accounting/voucherSync.ts +++ b/src/accounting/voucherSync.ts @@ -40,8 +40,21 @@ function assertMapping(map: VoucherAccountMap): void { } } -function makeLine(accountId: string, debit: number, credit: number, description: string): VoucherLine { - return { id: createId('vl-'), accountId, debit, credit, description } +function makeLine( + accountId: string, + debit: number, + credit: number, + description: string, + partyId?: string | null, +): VoucherLine { + return { + id: createId('vl-'), + accountId, + partyId: partyId ?? null, + debit, + credit, + description, + } } function makeVoucher( diff --git a/src/api/client.ts b/src/api/client.ts index 2208ed3..2546a57 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -21,6 +21,12 @@ export class ApiError extends Error { } } +/** Append a unique query so CDNs that ignore Cache-Control cannot reuse a GET body. */ +export function withCacheBust(path: string): string { + const sep = path.includes('?') ? '&' : '?' + return `${path}${sep}_=${Date.now()}` +} + async function request(path: string, init?: RequestInit, baseUrl: string = getApiBaseUrl()): Promise { const token = getToken() let response: Response @@ -38,7 +44,10 @@ async function request(path: string, init?: RequestInit, baseUrl: string = ge ...(init?.headers ?? {}), }, }) - } catch { + } catch (err) { + if (err instanceof DOMException && err.name === 'AbortError') { + throw new ApiError('درخواست زمان‌بر شد. دوباره تلاش کنید.', 0) + } throw new ApiError('عدم دسترسی به سرور. آدرس سرور و اتصال شبکه را بررسی کنید.', 0) } @@ -59,10 +68,11 @@ async function request(path: string, init?: RequestInit, baseUrl: string = ge return (text ? JSON.parse(text) : undefined) as T } -export const apiGet = (path: string): Promise => request(path) +export const apiGet = (path: string, init?: RequestInit): Promise => + request(path, init) -export const apiPatch = (path: string, body: unknown): Promise => - request(path, { method: 'PATCH', body: JSON.stringify(body) }) +export const apiPatch = (path: string, body: unknown, init?: RequestInit): Promise => + request(path, { ...init, method: 'PATCH', body: JSON.stringify(body) }) /** Client bound to the standalone accounting backend (shares the admin token). */ export const acct = { diff --git a/src/pages/Vouchers.tsx b/src/pages/Vouchers.tsx index fe131e3..e9ef518 100644 --- a/src/pages/Vouchers.tsx +++ b/src/pages/Vouchers.tsx @@ -38,6 +38,7 @@ import { const newLine = (): VoucherLine => ({ id: createId('vl-'), accountId: '', + partyId: null, description: '', debit: 0, credit: 0, @@ -55,6 +56,33 @@ function createDraft(items: Voucher[]): Voucher { } } +function partyLabel( + parties: { id: string; name: string; kind: string }[], + partyId: string | null | undefined, +): string { + if (!partyId) return '—' + const party = parties.find((p) => p.id === partyId) + if (!party) return '—' + return party.name +} + +/** Compact تفصیل summary for the documents list. */ +function voucherTafsilSummary( + voucher: Voucher, + parties: { id: string; name: string }[], +): string { + const names = [ + ...new Set( + voucher.lines + .map((line) => (line.partyId ? parties.find((p) => p.id === line.partyId)?.name : null)) + .filter((name): name is string => Boolean(name)), + ), + ] + if (names.length === 0) return '—' + if (names.length === 1) return names[0] + return `${names[0]} +${names.length - 1}` +} + export function Vouchers() { const { data, upsertVoucher, removeVoucher } = useStore() const { sync, loading: syncLoading, error: syncError, stats: syncStats, lastSyncedAt } = @@ -72,6 +100,10 @@ export function Vouchers() { () => data.accounts.filter((account) => !account.isGroup), [data.accounts], ) + const partiesSorted = useMemo( + () => [...data.parties].sort((a, b) => a.name.localeCompare(b.name, 'fa')), + [data.parties], + ) const accountName = (id: string) => leafAccounts.find((a) => a.id === id)?.name ?? '—' const displayedVouchers = useMemo( @@ -123,7 +155,12 @@ export function Vouchers() { const handleSave = (status: Voucher['status']) => { if (!draft) return - const cleanLines = draft.lines.filter((line) => line.accountId && (line.debit > 0 || line.credit > 0)) + const cleanLines = draft.lines + .filter((line) => line.accountId && (line.debit > 0 || line.credit > 0)) + .map((line) => ({ + ...line, + partyId: line.partyId || null, + })) if (cleanLines.length < 2) return const candidate: Voucher = { ...draft, lines: cleanLines, status } if (status === 'posted' && !voucherBalance(candidate).balanced) return @@ -168,6 +205,20 @@ export function Vouchers() { ), }, + { + key: 'tafsil', + header: 'تفصیل', + truncate: true, + className: 'min-w-0', + render: (v) => { + const summaryText = voucherTafsilSummary(v, data.parties) + return ( + + {summaryText} + + ) + }, + }, { key: 'desc', header: 'شرح', @@ -227,7 +278,7 @@ export function Vouchers() {