import { useMemo } from 'react' import { Link } from 'react-router-dom' import { visibleVouchers } from '../accounting/voucherDedupe' import { useStore } from '../store/AppStore' import { Badge, Card, PageHeader, StatCard } from '../components/ui' import { formatMoney, formatMoneyWithUnit, toFa } from '../utils/format' import { balanceSheet, incomeStatement, invoiceTotals, trialBalance, } from '../utils/accounting' interface MonthBucket { label: string sales: number purchases: number } function isSaleInvoice(invoice: ReturnType['data']['invoices'][number]): boolean { return ( invoice.kind === 'sale' && (invoice.documentType ?? 'invoice') === 'invoice' && invoice.status !== 'draft' && invoice.status !== 'cancelled' ) } function monthlyBuckets( invoices: ReturnType['data']['invoices'], ): MonthBucket[] { const formatter = new Intl.DateTimeFormat('fa-IR-u-ca-persian', { month: 'short' }) const buckets: MonthBucket[] = [] const now = new Date() for (let offset = 5; offset >= 0; offset -= 1) { const date = new Date(now.getFullYear(), now.getMonth() - offset, 1) const key = `${date.getFullYear()}-${date.getMonth()}` const inMonth = invoices.filter((invoice) => { const d = new Date(invoice.date) return `${d.getFullYear()}-${d.getMonth()}` === key }) buckets.push({ label: formatter.format(date), sales: inMonth .filter(isSaleInvoice) .reduce((sum, i) => sum + invoiceTotals(i).net, 0), purchases: inMonth .filter((i) => i.kind === 'purchase' && i.status !== 'draft') .reduce((sum, i) => sum + invoiceTotals(i).net, 0), }) } return buckets } export function Dashboard() { const { data } = useStore() const metrics = useMemo(() => { const vouchers = visibleVouchers(data.vouchers, data) const balances = trialBalance(data.accounts, vouchers) const liquidity = balances .filter((b) => ['1001', '1002', '1003'].includes(b.account.code)) .reduce((sum, b) => sum + b.balance, 0) const sales = data.invoices .filter(isSaleInvoice) .reduce((sum, i) => sum + invoiceTotals(i).net, 0) const purchases = data.invoices .filter((i) => i.kind === 'purchase' && i.status !== 'draft') .reduce((sum, i) => sum + invoiceTotals(i).net, 0) const income = incomeStatement(balances) const sheet = balanceSheet(balances) const lowStock = data.products.filter((p) => p.stock <= p.reorderLevel) const unpaidSales = data.invoices.filter( (i) => i.kind === 'sale' && (i.documentType ?? 'invoice') === 'invoice' && i.status === 'confirmed', ) return { liquidity, sales, purchases, income, sheet, lowStock, unpaidSales } }, [data]) const buckets = useMemo(() => monthlyBuckets(data.invoices), [data.invoices]) const chartMax = Math.max(1, ...buckets.flatMap((b) => [b.sales, b.purchases])) return (
i.kind === 'purchase').length)} فاکتور خرید`} icon="🛒" tone="amber" /> = 0 ? '📈' : '📉'} tone={metrics.income.profit >= 0 ? 'green' : 'rose'} />

روند فروش و خرید (۶ ماه اخیر)

فروش خرید
{buckets.map((bucket) => (
{bucket.label}
))}

ترازنامه فشرده

هشدار موجودی انبار

مدیریت انبار
{metrics.lowStock.length === 0 ? (

موجودی همه کالاها در حد مطلوب است.

) : (
    {metrics.lowStock.map((product) => (
  • {product.name} موجودی: {toFa(product.stock)} {product.unit}
  • ))}
)}

فاکتورهای فروش تسویه‌نشده

مرور فروش
{metrics.unpaidSales.length === 0 ? (

همه فاکتورها تسویه شده‌اند.

) : (
    {metrics.unpaidSales.slice(0, 5).map((invoice) => { const party = data.parties.find((p) => p.id === invoice.partyId) return (
  • فاکتور #{toFa(invoice.number)} · {party?.name ?? '—'} {formatMoneyWithUnit(invoiceTotals(invoice).net)}
  • ) })}
)}
) } function Row({ label, value, tone }: { label: string; value: number; tone: string }) { return (
{label}
{formatMoney(value)}
) }