- | حساب |
+ معین |
+ تفصیل (کی گرفته) |
شرح ردیف |
- بابت |
بدهکار |
بستانکار |
|
@@ -416,8 +477,9 @@ export function Vouchers() {
+
+
+ |
updateLine(line.id, { description: e.target.value })}
- placeholder="شرح"
+ placeholder="شرح ردیف"
/>
|
-
- {resolveVoucherRegarding(draft, data.treasury)}
- |
- {draft.lines.some((l) => l.accountId) && (
+ {draft.lines.some((l) => l.accountId || l.partyId) && (
- حسابهای انتخابشده: {draft.lines.filter((l) => l.accountId).map((l) => accountName(l.accountId)).join('، ')}
+ {draft.lines
+ .filter((l) => l.accountId)
+ .map((l) => {
+ const moein = accountName(l.accountId)
+ const tafsir = partyLabel(partiesSorted, l.partyId)
+ return tafsir !== '—' ? `${moein} / ${tafsir}` : moein
+ })
+ .join(' · ')}
)}
diff --git a/src/pages/sales/salesUtils.ts b/src/pages/sales/salesUtils.ts
index de4aafa..afe6a6b 100644
--- a/src/pages/sales/salesUtils.ts
+++ b/src/pages/sales/salesUtils.ts
@@ -228,9 +228,16 @@ export function buildSalesVoucher(
const label = `${documentTypeLabels[docType]} #${invoice.number} - ${party?.name ?? ''}`
const regarding = invoiceRegarding(invoice)
- const makeLine = (accountId: string, debit: number, credit: number, desc: string): VoucherLine => ({
+ const makeLine = (
+ accountId: string,
+ debit: number,
+ credit: number,
+ desc: string,
+ partyId?: string | null,
+ ): VoucherLine => ({
id: createId('vl-'),
accountId,
+ partyId: partyId ?? null,
description: desc,
debit,
credit,
@@ -253,7 +260,7 @@ export function buildSalesVoucher(
status: 'posted',
source: withdrawalId ? `withdrawal:${withdrawalId}` : voucherSourceForInvoice(invoice),
lines: [
- makeLine(payableId, net, 0, `برداشت کیف پول - ${label}`),
+ makeLine(payableId, net, 0, `برداشت کیف پول - ${label}`, party?.id),
makeLine(bankId, 0, net, `پرداخت از بانک - ${label}`),
],
}
@@ -298,7 +305,7 @@ export function buildSalesVoucher(
source: voucherSourceForInvoice(invoice),
lines: [
makeLine(bankId, net, 0, `مانده بانکی پس از لغو بلیت - ${cancelLabel}`),
- makeLine(customerPayableId, 0, net, `بدهی کیف پول مشتری پس از لغو - ${cancelLabel}`),
+ makeLine(customerPayableId, 0, net, `بدهی کیف پول مشتری پس از لغو - ${cancelLabel}`, party?.id),
],
}
}
@@ -331,7 +338,13 @@ export function buildSalesVoucher(
status: 'posted',
source: voucherSourceForInvoice(invoice),
lines: [
- makeLine(debitAccountId, split.gross, 0, `${debitLabel} - ${label}`),
+ makeLine(
+ debitAccountId,
+ split.gross,
+ 0,
+ `${debitLabel} - ${label}`,
+ fromWallet ? party?.id : null,
+ ),
makeLine(ownerPayableId, 0, split.ownerNet, `سهم تامینکننده (مالک) - ${label}`),
makeLine(platformIncomeId, 0, split.platformProfit, `سود پلتفرم (۱۴٪) - ${label}`),
makeLine(taxAccountId, 0, split.tax, `مالیات (۱۰٪) - ${label}`),
@@ -346,10 +359,10 @@ export function buildSalesVoucher(
const lines: VoucherLine[] = isReturn
? [
makeLine(salesIncomeAccountId, net, 0, `برگشت فروش - ${label}`),
- makeLine(receivableAccountId, 0, net, `کاهش مطالبات - ${label}`),
+ makeLine(receivableAccountId, 0, net, `کاهش مطالبات - ${label}`, party?.id),
]
: [
- makeLine(receivableAccountId, net, 0, `مطالبات مشتری - ${label}`),
+ makeLine(receivableAccountId, net, 0, `مطالبات مشتری - ${label}`, party?.id),
makeLine(salesIncomeAccountId, 0, net, `درآمد فروش - ${label}`),
]
@@ -382,9 +395,16 @@ export function buildPurchaseVoucher(
const party = data.parties.find((p) => p.id === invoice.partyId)
const label = `فاکتور خرید #${invoice.number} - ${party?.name ?? ''}`
- const makeLine = (accountId: string, debit: number, credit: number, desc: string): VoucherLine => ({
+ const makeLine = (
+ accountId: string,
+ debit: number,
+ credit: number,
+ desc: string,
+ partyId?: string | null,
+ ): VoucherLine => ({
id: createId('vl-'),
accountId,
+ partyId: partyId ?? null,
description: desc,
debit,
credit,
@@ -399,7 +419,7 @@ export function buildPurchaseVoucher(
source: voucherSourceForInvoice(invoice),
lines: [
makeLine(inventoryAccountId, net, 0, `افزایش موجودی - ${label}`),
- makeLine(payableAccountId, 0, net, `بدهی به تأمینکننده - ${label}`),
+ makeLine(payableAccountId, 0, net, `بدهی به تأمینکننده - ${label}`, party?.id),
],
}
}
@@ -520,9 +540,16 @@ export function buildAdjustmentVoucher(
const kindLabel = adjustment.kind === 'debit' ? 'بدهکار' : 'بستانکار'
const label = `اعلامیه ${kindLabel} #${adjustment.number} - ${party?.name ?? ''}`
- const makeLine = (accountId: string, debit: number, credit: number, desc: string): VoucherLine => ({
+ const makeLine = (
+ accountId: string,
+ debit: number,
+ credit: number,
+ desc: string,
+ partyId?: string | null,
+ ): VoucherLine => ({
id: createId('vl-'),
accountId,
+ partyId: partyId ?? null,
description: desc,
debit,
credit,
@@ -532,12 +559,12 @@ export function buildAdjustmentVoucher(
const lines: VoucherLine[] =
adjustment.kind === 'debit'
? [
- makeLine(receivableAccountId, amount, 0, `افزایش مطالبات - ${label}`),
+ makeLine(receivableAccountId, amount, 0, `افزایش مطالبات - ${label}`, party?.id),
makeLine(salesIncomeAccountId, 0, amount, `اصلاح درآمد - ${label}`),
]
: [
makeLine(salesIncomeAccountId, amount, 0, `اصلاح درآمد - ${label}`),
- makeLine(receivableAccountId, 0, amount, `کاهش مطالبات - ${label}`),
+ makeLine(receivableAccountId, 0, amount, `کاهش مطالبات - ${label}`, party?.id),
]
return {
@@ -579,9 +606,16 @@ export function buildTreasuryVoucher(
const kindLabel = txn.kind === 'receipt' ? 'دریافت' : 'پرداخت'
const label = `${kindLabel} #${txn.number}${party ? ` - ${party.name}` : ''}`
- const makeLine = (accountId: string, debit: number, credit: number, desc: string): VoucherLine => ({
+ const makeLine = (
+ accountId: string,
+ debit: number,
+ credit: number,
+ desc: string,
+ partyId?: string | null,
+ ): VoucherLine => ({
id: createId('vl-'),
accountId,
+ partyId: partyId ?? null,
description: desc,
debit,
credit,
@@ -591,6 +625,7 @@ export function buildTreasuryVoucher(
const tax = txn.taxAmount ?? 0
const profit = txn.platformProfitAmount ?? 0
const ownerNet = txn.ownerNetAmount ?? 0
+ const partyId = party?.id ?? null
const source = txn.source ?? ''
const isWalletBySource =
@@ -613,7 +648,7 @@ export function buildTreasuryVoucher(
source: voucherSourceForTreasury(txn),
lines: [
makeLine(bankId, amount, 0, `واریز شارژ کیف پول - ${label}`),
- makeLine(customerPayableId, 0, amount, `بدهی به مشتری (کیف پول) - ${label}`),
+ makeLine(customerPayableId, 0, amount, `بدهی به مشتری (کیف پول) - ${label}`, partyId),
],
}
}
@@ -630,7 +665,7 @@ export function buildTreasuryVoucher(
status: 'posted',
source: voucherSourceForTreasury(txn),
lines: [
- makeLine(customerPayableId, amount, 0, `برداشت کیف پول مشتری - ${label}`),
+ makeLine(customerPayableId, amount, 0, `برداشت کیف پول مشتری - ${label}`, partyId),
makeLine(bankId, 0, amount, `پرداخت از بانک - ${label}`),
],
}
@@ -654,7 +689,13 @@ export function buildTreasuryVoucher(
status: 'posted',
source: voucherSourceForTreasury(txn),
lines: [
- makeLine(debitAccountId, amount, 0, `${debitLabel} - ${label}`),
+ makeLine(
+ debitAccountId,
+ amount,
+ 0,
+ `${debitLabel} - ${label}`,
+ txn.method === 'cash' ? partyId : null,
+ ),
makeLine(ownerPayableId!, 0, ownerNet, `سهم مالک - ${label}`),
makeLine(platformIncomeId!, 0, profit, `سود پلتفرم (۱۴٪) - ${label}`),
makeLine(taxAccountId!, 0, tax, `مالیات (۱۰٪) - ${label}`),
@@ -668,10 +709,10 @@ export function buildTreasuryVoucher(
txn.kind === 'receipt'
? [
makeLine(bankId, amount, 0, `واریز - ${label}`),
- makeLine(counterAccountId, 0, amount, `تسویه مطالبات - ${label}`),
+ makeLine(counterAccountId, 0, amount, `تسویه مطالبات - ${label}`, partyId),
]
: [
- makeLine(counterAccountId, amount, 0, `تسویه بدهی - ${label}`),
+ makeLine(counterAccountId, amount, 0, `تسویه بدهی - ${label}`, partyId),
makeLine(bankId, 0, amount, `برداشت - ${label}`),
]
diff --git a/src/pages/sales/usePendingCustomerWithdrawals.ts b/src/pages/sales/usePendingCustomerWithdrawals.ts
index ed493f0..1a153bc 100644
--- a/src/pages/sales/usePendingCustomerWithdrawals.ts
+++ b/src/pages/sales/usePendingCustomerWithdrawals.ts
@@ -1,9 +1,10 @@
import { useCallback, useEffect, useRef, useState } from 'react'
-import { apiGet, apiPatch } from '../../api/client'
+import { apiGet, apiPatch, withCacheBust } from '../../api/client'
import { ENDPOINTS } from '../../api/config'
import type { ApiWithdrawal, WalletTxnStatus } from '../../api/types'
const REFRESH_INTERVAL_MS = 30_000
+const FETCH_TIMEOUT_MS = 20_000
function sortWithdrawalsNewestFirst(list: ApiWithdrawal[]): ApiWithdrawal[] {
return [...list].sort(
@@ -19,6 +20,7 @@ export function usePendingCustomerWithdrawals(enabled = true) {
const [error, setError] = useState(null)
const inFlightRef = useRef(false)
const enabledRef = useRef(enabled)
+ const abortRef = useRef(null)
enabledRef.current = enabled
const load = useCallback(async (opts?: { soft?: boolean }) => {
@@ -28,15 +30,32 @@ export function usePendingCustomerWithdrawals(enabled = true) {
inFlightRef.current = true
if (!soft) setLoading(true)
setError(null)
+
+ abortRef.current?.abort()
+ const controller = new AbortController()
+ abortRef.current = controller
+ let timedOut = false
+ const timeoutId = window.setTimeout(() => {
+ timedOut = true
+ controller.abort()
+ }, FETCH_TIMEOUT_MS)
+
try {
- const res = await apiGet<{ withdrawals: ApiWithdrawal[] }>(ENDPOINTS.WITHDRAWALS)
+ // Cache-bust query defeats Arvan/CDN GETs that ignore Cache-Control.
+ const res = await apiGet<{ withdrawals: ApiWithdrawal[] }>(
+ withCacheBust(ENDPOINTS.WITHDRAWALS),
+ { signal: controller.signal },
+ )
+ if (controller.signal.aborted) return
setWithdrawals(sortWithdrawalsNewestFirst(res.withdrawals ?? []))
} catch (err) {
+ // Cleanup abort: ignore. Timeout/network: surface error (keep list on soft).
+ if (controller.signal.aborted && !timedOut) return
setError(err instanceof Error ? err.message : 'خطا در بارگذاری برداشتها')
- // Keep last known list on soft refresh / transient CDN errors so the UI
- // does not flash empty while production caches catch up.
if (!soft) setWithdrawals([])
} finally {
+ window.clearTimeout(timeoutId)
+ if (abortRef.current === controller) abortRef.current = null
inFlightRef.current = false
setLoading(false)
}
@@ -59,32 +78,40 @@ export function usePendingCustomerWithdrawals(enabled = true) {
document.removeEventListener('visibilitychange', softReload)
window.removeEventListener('focus', softReload)
window.clearInterval(intervalId)
+ abortRef.current?.abort()
+ abortRef.current = null
+ inFlightRef.current = false
}
}, [enabled, load])
- const toggleStatus = useCallback(async (withdrawal: ApiWithdrawal) => {
- const nextStatus: WalletTxnStatus =
- withdrawal.status === 'pending' ? 'completed' : 'pending'
+ const toggleStatus = useCallback(
+ async (withdrawal: ApiWithdrawal) => {
+ const nextStatus: WalletTxnStatus =
+ withdrawal.status === 'pending' ? 'completed' : 'pending'
- setUpdatingId(withdrawal.id)
- setError(null)
- try {
- await apiPatch(ENDPOINTS.UPDATE_WITHDRAWAL_STATUS(withdrawal.id), {
- status: nextStatus,
- user_type: withdrawal.user_type,
- })
- setWithdrawals((prev) =>
- prev.map((w) => (w.id === withdrawal.id ? { ...w, status: nextStatus } : w)),
- )
- return nextStatus
- } catch (err) {
- const message = err instanceof Error ? err.message : 'خطا در بروزرسانی وضعیت برداشت'
- setError(message)
- throw err instanceof Error ? err : new Error(message)
- } finally {
- setUpdatingId(null)
- }
- }, [])
+ setUpdatingId(withdrawal.id)
+ setError(null)
+ try {
+ await apiPatch(ENDPOINTS.UPDATE_WITHDRAWAL_STATUS(withdrawal.id), {
+ status: nextStatus,
+ user_type: withdrawal.user_type,
+ })
+ setWithdrawals((prev) =>
+ prev.map((w) => (w.id === withdrawal.id ? { ...w, status: nextStatus } : w)),
+ )
+ // Re-fetch soon so production CDN/proxy cannot leave the desk on a stale list.
+ window.setTimeout(() => void load({ soft: true }), 400)
+ return nextStatus
+ } catch (err) {
+ const message = err instanceof Error ? err.message : 'خطا در بروزرسانی وضعیت برداشت'
+ setError(message)
+ throw err instanceof Error ? err : new Error(message)
+ } finally {
+ setUpdatingId(null)
+ }
+ },
+ [load],
+ )
const reload = useCallback(() => void load(), [load])
diff --git a/src/parties/useFunZoneParties.ts b/src/parties/useFunZoneParties.ts
index 61e853a..6d312de 100644
--- a/src/parties/useFunZoneParties.ts
+++ b/src/parties/useFunZoneParties.ts
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
-import { apiGet, asList } from '../api/client'
+import { apiGet, asList, withCacheBust } from '../api/client'
import { ENDPOINTS } from '../api/config'
import type { ApiCustomer, ApiOwner } from '../api/types'
import { useStore } from '../store/AppStore'
@@ -117,9 +117,17 @@ export function usePartyWithdrawals(kind: PartyKind) {
useEffect(() => {
let cancelled = false
const userType = kind === 'supplier' ? 'owner' : 'customer'
+ let inFlight = false
const load = () => {
- apiGet<{ withdrawals: import('../api/types').ApiWithdrawal[] }>(ENDPOINTS.WITHDRAWALS)
+ if (cancelled || inFlight) return
+ inFlight = true
+ const controller = new AbortController()
+ const timeoutId = window.setTimeout(() => controller.abort(), 20_000)
+ apiGet<{ withdrawals: import('../api/types').ApiWithdrawal[] }>(
+ withCacheBust(ENDPOINTS.WITHDRAWALS),
+ { signal: controller.signal },
+ )
.then((res) => {
if (cancelled) return
setWithdrawals((res.withdrawals ?? []).filter((w) => w.user_type === userType))
@@ -127,6 +135,10 @@ export function usePartyWithdrawals(kind: PartyKind) {
.catch(() => {
// Keep last known list; avoid wiping party stats on a transient/CDN miss.
})
+ .finally(() => {
+ window.clearTimeout(timeoutId)
+ inFlight = false
+ })
}
load()
diff --git a/src/types/index.ts b/src/types/index.ts
index f658744..701a645 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -14,7 +14,13 @@ export interface Account {
export interface VoucherLine {
id: ID
+ /** حساب معین (leaf account in the chart). */
accountId: ID
+ /**
+ * تفصیل / طرف حساب — who this line is with («کی گرفته»).
+ * Optional for accounts that do not need a party (cash, tax, income, …).
+ */
+ partyId?: ID | null
description: string
debit: number
credit: number
@@ -27,7 +33,10 @@ export interface Voucher {
number: number
date: string
description: string
- /** Event or subject the voucher relates to (بابت). */
+ /**
+ * بابت / موضوع سند — why the entry exists («برای چی گرفته»).
+ * Event name, wallet, or free-text purpose.
+ */
regarding?: string
status: VoucherStatus
lines: VoucherLine[]
diff --git a/src/utils/voucherFilters.ts b/src/utils/voucherFilters.ts
index 42ec502..154e83b 100644
--- a/src/utils/voucherFilters.ts
+++ b/src/utils/voucherFilters.ts
@@ -81,7 +81,20 @@ export function filterVouchers(
if (query) {
const regarding = resolveVoucherRegarding(voucher, data.treasury).toLowerCase()
- const haystack = [voucher.description, regarding, String(voucher.number), voucher.source ?? '']
+ const tafsirNames = voucher.lines
+ .map((line) =>
+ line.partyId ? data.parties.find((p) => p.id === line.partyId)?.name ?? '' : '',
+ )
+ .join(' ')
+ const lineDescriptions = voucher.lines.map((line) => line.description).join(' ')
+ const haystack = [
+ voucher.description,
+ regarding,
+ tafsirNames,
+ lineDescriptions,
+ String(voucher.number),
+ voucher.source ?? '',
+ ]
.join(' ')
.toLowerCase()
if (!haystack.includes(query)) return false
|