From a11074187025f930a7e73fe8d8cd5eaf6889230d Mon Sep 17 00:00:00 2001 From: Behrad Date: Wed, 16 Sep 2026 12:12:28 +0330 Subject: [PATCH] feat: mobile UI optimizations --- frontend/src/components/StatsBar.tsx | 69 ++++- frontend/src/components/layout/Header.tsx | 4 +- .../src/pages/Dashboard/DashboardPage.tsx | 22 +- .../src/pages/Dashboard/FilterToolbar.tsx | 257 ++++++++++++---- frontend/src/pages/Dashboard/LedgerTable.tsx | 10 +- frontend/src/pages/Dashboard/QuoteCard.tsx | 173 ++++++----- .../src/pages/Inventory/InventoryPage.tsx | 291 +++++++++++------- 7 files changed, 539 insertions(+), 287 deletions(-) diff --git a/frontend/src/components/StatsBar.tsx b/frontend/src/components/StatsBar.tsx index 56cf40a..a7a8cb4 100644 --- a/frontend/src/components/StatsBar.tsx +++ b/frontend/src/components/StatsBar.tsx @@ -8,31 +8,72 @@ export interface StatsBarProps { avgLeadTime: number | null; // روز } -// نوار ۴ ستونه‌ی آمار export function StatsBar({ total, offered, accepted, avgLeadTime }: StatsBarProps) { - const stats: { label: string; value: string; color: string; icon: LucideIcon }[] = [ - { label: "کل پیشنهادها", value: fmtNum(total), color: "text-ink", icon: ClipboardList }, - // پیشنهاد باز = نارنجی، پذیرفته‌شده = آبی، رسیده = سبز - { label: "پیشنهاد باز", value: fmtNum(offered), color: "text-amber", icon: Inbox }, - { label: "پذیرفته‌شده", value: fmtNum(accepted), color: "text-[#1d4ed8]", icon: CheckCircle2 }, - { label: "میانگین lead time (روز)", value: avgLeadTime == null ? "—" : fmtNum(avgLeadTime), color: "text-green", icon: Timer }, + const stats: { + label: string; + shortLabel: string; + value: string; + color: string; + icon: LucideIcon; + }[] = [ + { + label: "کل پیشنهادها", + shortLabel: "کل", + value: fmtNum(total), + color: "text-ink", + icon: ClipboardList, + }, + { + label: "پیشنهاد باز", + shortLabel: "باز", + value: fmtNum(offered), + color: "text-amber", + icon: Inbox, + }, + { + label: "پذیرفته‌شده", + shortLabel: "پذیرفته", + value: fmtNum(accepted), + color: "text-[#1d4ed8]", + icon: CheckCircle2, + }, + { + label: "میانگین lead time (روز)", + shortLabel: "زمان تحویل", + value: avgLeadTime == null ? "—" : fmtNum(avgLeadTime), + color: "text-green", + icon: Timer, + }, ]; + return ( -
+
{stats.map((s) => (
-
-
{s.label}
- + {/* عنوان کارت */} +
+ + {s.shortLabel} + {s.label} + + + {/* آیکون در موبایل حذف شده تا فضا برای متن باز بماند */} +
-
{s.value}
+ + {/* مقدار عددی */} +
+ {s.value} +
))}
); -} +} \ No newline at end of file diff --git a/frontend/src/components/layout/Header.tsx b/frontend/src/components/layout/Header.tsx index 29a467e..7b6350d 100644 --- a/frontend/src/components/layout/Header.tsx +++ b/frontend/src/components/layout/Header.tsx @@ -38,7 +38,7 @@ export function Header({ title, sub, backTo, backLabel = "بازگشت", actions + + {/* دکمه تغییر جهت سورت */} + - + + {/* باتم‌شیت کشویی فیلترها برای موبایل */} + {isOpen && ( +
+
setIsOpen(false)} aria-hidden="true" /> +
+ {/* هدر کشو */} +
+
+ + فیلترهای پیشرفته +
+
+ {activeFilterCount > 0 && ( + + )} + +
+
+ + {/* دراپ‌داون‌ها داخل کشو */} +
+
+ + set({ category: it.label })} + items={categories} + allowAdd={false} + placeholder="همه دسته‌ها" + onInputChange={(v) => set({ category: v })} + /> +
+ +
+ + set({ supplier: it.label })} + items={suppliers} + allowAdd={false} + placeholder="همه تأمین‌کنندگان" + onInputChange={(v) => set({ supplier: v })} + /> +
+ +
+ + set({ status: it.label })} + items={STATUS_ITEMS} + allowAdd={false} + placeholder="همه وضعیت‌ها" + onInputChange={(v) => set({ status: v })} + /> +
+
+ + {/* دکمه تأیید و بستن */} + +
+
+ )} + + {/* ===== نمای دسکتاپ (md به بالا) ===== */} +
+
+ + set({ category: it.label })} + items={categories} + allowAdd={false} + placeholder="همه" + onInputChange={(v) => set({ category: v })} + /> +
+
+ + set({ supplier: it.label })} + items={suppliers} + allowAdd={false} + placeholder="همه" + onInputChange={(v) => set({ supplier: v })} + /> +
+
+ + set({ status: it.label })} + items={STATUS_ITEMS} + allowAdd={false} + placeholder="همه" + onInputChange={(v) => set({ status: v })} + /> +
+
+ + set({ search: e.target.value })} + /> +
+
+ + +
+
+ ); -} +} \ No newline at end of file diff --git a/frontend/src/pages/Dashboard/LedgerTable.tsx b/frontend/src/pages/Dashboard/LedgerTable.tsx index 29b7ce4..f4a55f5 100644 --- a/frontend/src/pages/Dashboard/LedgerTable.tsx +++ b/frontend/src/pages/Dashboard/LedgerTable.tsx @@ -12,7 +12,7 @@ export interface LedgerTableProps { onEdit: (id: string) => void; } -// دفتر پیشنهادها — فهرست کارت‌ها (هر پیشنهاد یک کارت خوانا) +// دفتر پیشنهادها — فهرست کارت‌ها (واکنش‌گرا و بهینه برای موبایل) export function LedgerTable({ store, quotes, @@ -23,14 +23,14 @@ export function LedgerTable({ }: LedgerTableProps) { if (!quotes.length) { return ( -
+
); } return ( -
+
{quotes.map((q) => ( onEdit(q.id)} /> ))} -
+
هشدار «منقضی» = قیمت پیشنهادی از تاریخ انقضا گذشته؛ برای پیش‌بینی قیمت، شمارش نمی‌شود.
); -} +} \ No newline at end of file diff --git a/frontend/src/pages/Dashboard/QuoteCard.tsx b/frontend/src/pages/Dashboard/QuoteCard.tsx index 88fcf20..7d9de8f 100644 --- a/frontend/src/pages/Dashboard/QuoteCard.tsx +++ b/frontend/src/pages/Dashboard/QuoteCard.tsx @@ -1,5 +1,5 @@ import { Link } from "react-router-dom"; -import { AlarmClock, ChevronDown, Pencil, TriangleAlert, Building2, Package, CreditCard } from "lucide-react"; +import { AlarmClock, ChevronDown, Pencil, TriangleAlert, Building2, Package, CreditCard, Check } from "lucide-react"; import type { Quote, Store } from "../../lib/types"; import { isExpired, @@ -27,16 +27,16 @@ export interface QuoteCardProps { onEdit: () => void; } -function InfoCell({ label, children }: { label: string; children: React.ReactNode }) { +function InfoCell({ label, children, className }: { label: string; children: React.ReactNode; className?: string }) { return ( -
-
{label}
-
{children}
+
+
{label}
+
{children}
); } -// کارت یک پیشنهاد در دفتر — جایگزین جدول ۱۱ ستونه +// کارت پیشنهاد خرید — بهینه‌شده برای کاربری سرپا با موبایل export function QuoteCard({ store, q, expanded, onToggleExpand, onAccept, onEdit }: QuoteCardProps) { const sup = supplierById(store.suppliers, q.supplier_id); const expired = isExpired(q); @@ -46,22 +46,25 @@ export function QuoteCard({ store, q, expanded, onToggleExpand, onAccept, onEdit const unitWarns = unitWarning(store, q); const summary = itemsSummary(store, q); - // ستون پذیرش (با ساعت) + // ستون / دکمه پذیرش let acceptedCell: React.ReactNode; if (q.status === "offered") { acceptedCell = ( - ); } else if (q.accepted_date) { - acceptedCell = {fmtDateTime(q.accepted_date)}; + acceptedCell = {fmtDateTime(q.accepted_date)}; } else { acceptedCell = —; } - // ستون رسید — v2.6: تأیید رسید فقط در «پنل مدیریت انبار» توسط مدیر پروژه انجام می‌شود؛ - // اینجا فقط وضعیت نمایش داده می‌شود. با رسید جزیی: «رسیده X از Y» + «در راه Z». + // وضعیت رسید کالا const activeItems = (q.line_items || []).filter((li) => !li.rejected); const receivedSum = activeItems.reduce((s, li) => s + (li.received_qty || 0), 0); const orderedSum = activeItems.reduce((s, li) => s + (li.quantity || 0), 0); @@ -71,25 +74,24 @@ export function QuoteCard({ store, q, expanded, onToggleExpand, onAccept, onEdit const anyReceived = receivedSum > 0; const fullyDelivered = activeItems.length > 0 && fulfilledItems === activeItems.length && anyReceived; - // نشان پیشرفت تکمیل در هدر کارت (جایگزین نشان وضعیت در حالت پذیرش/رسید): - // در راه (آبی) → رسید: X از Y قلم (کهربایی) → تکمیل شد (سبز) + // نشان پیشرفت تکمیل let fulfillmentBadge: React.ReactNode = null; if (q.status === "accepted" || q.status === "arrived") { if (fullyDelivered || q.status === "arrived") { fulfillmentBadge = ( - - ✓ تکمیل شد + + ✓ تکمیل ); } else if (anyReceived) { fulfillmentBadge = ( - - رسید: {fmtNum(fulfilledItems)} از {fmtNum(activeItems.length)} قلم + + رسید: {fmtNum(fulfilledItems)}/{fmtNum(activeItems.length)} ); } else { fulfillmentBadge = ( - + ⏳ در راه ); @@ -97,19 +99,12 @@ export function QuoteCard({ store, q, expanded, onToggleExpand, onAccept, onEdit } let arrivedCell: React.ReactNode; - // آخرین لحظه‌ی رسید ثبت‌شده بین قلم‌ها (رسیدهای جزیی توسط مدیر پروژه) const lastReceiptAt = activeItems.reduce((acc, li) => { if (!li.arrived_date) return acc; const t = new Date(li.arrived_date).getTime(); if (Number.isNaN(t)) return acc; return !acc || t > acc.getTime() ? new Date(li.arrived_date) : acc; }, null); - const receiptTimeLine = - lastReceiptAt != null ? ( - - آخرین رسید: {fmtDateTime(lastReceiptAt.toISOString())} - - ) : null; if (q.status === "accepted") { if (q.arrived_date) { @@ -117,12 +112,11 @@ export function QuoteCard({ store, q, expanded, onToggleExpand, onAccept, onEdit } else if (receivedSum > 0) { arrivedCell = ( - رسیده {fmtNum(receivedSum)} از {fmtNum(orderedSum)} — در راه {fmtNum(Math.max(0, orderedSum - receivedSum))} - {receiptTimeLine} + {fmtNum(receivedSum)} از {fmtNum(orderedSum)} ); } else { - arrivedCell = در انتظار PM; + arrivedCell = در انتظار انبار; } } else if (q.status === "arrived") { arrivedCell = ( @@ -137,35 +131,42 @@ export function QuoteCard({ store, q, expanded, onToggleExpand, onAccept, onEdit return (
{/* سربرگ کارت */} -
- +
+ {sup.name.slice(0, 1)} -
+
{sup.name} - {sup.location &&
{sup.location}
} + {sup.location &&
{sup.location}
}
-
+
{fulfillmentBadge ?? ( - {STATUS_LABEL[q.status]} + + {STATUS_LABEL[q.status]} + )} -
- {/* اطلاعات کلیدی */} -
+ {/* اطلاعات کلیدی (گرید ۲ ستونه موبایل / ۴ ستونه دسکتاپ) */} +
{summary.count > 1 ? (
- {fmtNum(summary.count)} قلم -
- {store.quotes.find((x) => x.id === q.id)?.line_items?.filter((li) => !li.rejected).map((li) => { + {fmtNum(summary.count)} قلم کالا +
+ {store.quotes.find((x) => x.id === q.id)?.line_items?.filter((li) => !li.rejected).slice(0, 3).map((li) => { const m = materialById(store.materials, li.material_id); return ( - - - {materialLabel(m)} - - {fmtNum(li.quantity)} {li.unit} - + + • {materialLabel(m)} ({fmtNum(li.quantity)} {li.unit}) ); })} + {(q.line_items?.length || 0) > 3 && ( + و {(q.line_items?.length || 0) - 3} قلم دیگر... + )}
) : ( - summary.text + {summary.text} )} {unitWarns.map((w) => ( @@ -213,37 +213,34 @@ export function QuoteCard({ store, q, expanded, onToggleExpand, onAccept, onEdit - + - {q.projects && q.projects.length > 0 ? ( - - {q.projects.map((p, i) => ( - {p} - ))} - - ) : ( - projectCell(q) - )} + + {q.projects && q.projects.length > 0 ? q.projects.join("، ") : projectCell(q)} + - + {pc ? ( - - {pc.num} ({pc.words} تومان) - +
+
+ {pc.num} تومان +
+
({pc.words})
+
) : ( "—" )}
- + {ts.title !== "—" ? (
-
+
{ts.title}
- {ts.sub &&
{ts.sub}
} + {ts.sub &&
{ts.sub}
}
) : ( — @@ -251,30 +248,36 @@ export function QuoteCard({ store, q, expanded, onToggleExpand, onAccept, onEdit
- {/* تاریخ‌ها و اقدامات */} -
- - - پیشنهاد: {fmtDateTime(q.quote_date)} + {/* تاریخ‌ها و اکشن‌ها (۲ ستونه در موبایل، نواری در دسکتاپ) */} +
+ + + تاریخ: + {fmtDate(q.quote_date)} - - - معتبر تا:{" "} + + + + اعتبار: {fmtDate(q.expiry_date)} - {expired && } + {expired && } + - پذیرش: {acceptedCell} + پذیرش: + {acceptedCell} - - رسید: {arrivedCell} + + + رسید: + {arrivedCell}
- {/* جریان (expand) */} + {/* جزئیات تکمیلی بازشونده */} {expanded && }
); -} +} \ No newline at end of file diff --git a/frontend/src/pages/Inventory/InventoryPage.tsx b/frontend/src/pages/Inventory/InventoryPage.tsx index 5b62036..7958914 100644 --- a/frontend/src/pages/Inventory/InventoryPage.tsx +++ b/frontend/src/pages/Inventory/InventoryPage.tsx @@ -1,6 +1,6 @@ import { useMemo, useState } from "react"; import { Link, useLocation, useSearchParams } from "react-router-dom"; -import { Plus, Warehouse as WarehouseIcon } from "lucide-react"; +import { Plus, Warehouse as WarehouseIcon, SlidersHorizontal, ArrowUpDown, X } from "lucide-react"; import { Header, PageLayout } from "../../components/layout/Header"; import { EmptyState } from "../../components/EmptyState"; import { Badge } from "../../components/ui/badge"; @@ -12,6 +12,7 @@ import { useInventoryData, brandName, catName, materialName, unitName, whName } import type { InventoryRow, InventoryTransaction, TransactionType } from "../../lib/types"; import { faNum } from "../../lib/formatters"; import { isoToJalaliStr } from "../../lib/jalali"; +import { cn } from "../../lib/utils"; // رنگ‌بندی معنایی: سبز زمردی = ورود/افزایش، رز = خروج/کاهش، آبی = خنثی/جابه‌جایی const TX_META: Record = { @@ -47,25 +48,23 @@ function specsHash(specs: Record | null | undefined): string { return keys.map((k) => `${k}=${s[k]}`).join("|"); } -// داشبورد انبار — پورت کامل نسخه‌ی ارسالی (گروه‌بندی + خط زمانی + فیلترها) export function InventoryPage() { const location = useLocation(); const user = useAuthStore((s) => s.user); const { warehouses, inventory, transactions, lookup, loading } = useInventoryData(); - const isAdmin = user?.role === "admin" || user?.role === "procurement_manager"; // مدیر تدارکات = ادمین دفتر + const isAdmin = user?.role === "admin" || user?.role === "procurement_manager"; const isPM = user?.role === "pm"; - // پرش عمیق از «انبارها»: /inventory?warehouse= → داشبورد همان انبار از پیش فیلتر شده const [params] = useSearchParams(); const [filters, setFilters] = useState<{ category: string; warehouse: string; material: string; project: string }>({ category: "", warehouse: params.get("warehouse") || "", material: "", project: "", }); const [sort, setSort] = useState<"asc" | "desc" | "">(""); + const [showMobileFilters, setShowMobileFilters] = useState(false); const [expandedKey, setExpandedKey] = useState(null); const [selTx, setSelTx] = useState(null); const [expBrand, setExpBrand] = useState(null); - // ادمین: انبارهای همه پروژه‌ها یا پروژه‌ی انتخابی const visibleWarehouses = useMemo(() => { if (isAdmin && filters.project) return warehouses.filter((w) => w.project_id === filters.project); if (isAdmin) return warehouses; @@ -120,7 +119,7 @@ export function InventoryPage() { .slice() .sort((a, b) => new Date(a.transaction_date).getTime() - new Date(b.transaction_date).getTime()); - const hasFilter = !!(filters.category || filters.warehouse || filters.material || (isAdmin && filters.project)); + const hasFilter = !!(filters.category || filters.warehouse || (isAdmin && filters.project)); if (loading) return
; if (!user || (!isAdmin && !isPM)) { @@ -135,119 +134,171 @@ export function InventoryPage() { } return ( -
+
} menu={} actions={ - <> - +
+ {/* تب‌های تکراری در موبایل مخفی و فقط در دسکتاپ نمایش داده می‌شوند */} +
+ +
{isPM && ( - تغییر موجودی + تغییر موجودی )} - +
} /> - - {/* فیلترها */} -
-
- - setFilters((f) => ({ ...f, material: e.target.value }))} + /> + {filters.material && ( + + )} +
+ + + + + + + {faNum(groups.length)} قلم +
- {isAdmin && ( -
- + + {/* گزینه‌های فیلتر پیشرفته: در موبایل با دکمه باز می‌شود و در دسکتاپ همیشه باز است */} +
+
+
- )} -
- - + + {isAdmin && ( +
+ + +
+ )} + +
+ + +
+ + {hasFilter && ( + + )} + +
+ نمایش {faNum(groups.length)} قلم کالا +
-
- - setFilters((f) => ({ ...f, material: e.target.value }))} - /> -
- - {hasFilter && ( - - )} - {filters.warehouse && ( - 🏬 {whName(warehouses, filters.warehouse)} - )} - {faNum(groups.length)} قلم کالا
- {/* جدول گروه‌بندی‌شده */} -
+ {/* جدول موجودی انبار با اسکرول افقی محافظت‌شده */} +
{!groups.length ? ( ) : ( - +
- - - - - - - - - + + + + + + + + @@ -295,7 +346,9 @@ export function InventoryPage() {
- متریالدسته‌بندیانبارمشخصه فنیتعدادواحدآخرین به‌روزرسانی +
+ متریالدسته‌بندیانبارمشخصه فنیتعدادواحدآخرین به‌روزرسانی
)}
-
+ + {/* متن راهنما فقط در دسکتاپ نمایش داده می‌شود */} +
۱۰ نوع تراکنش استاندارد • سبز زمردی = ورود به انبار · رز = خروج/کاهش · آبی = انتقال و شمارش • کلیک روی هر سطر: خط زمانی با «مانده» لحظه‌به‌لحظه و تفکیک برندها
@@ -317,21 +370,21 @@ function GroupRow({ g, isExp, isAdmin, matName, matCatName, wh, projName, unit, }) { return ( <> - - {isExp ? "▾" : "▸"} - {matName} - {matCatName} - - {wh} - {isAdmin && {projName}} + + {isExp ? "▾" : "▸"} + {matName} + {matCatName} + +
{wh}
+ {isAdmin && {projName}} - + {Object.entries(g.specs || {}).map(([k, v]) => `${k}: ${v}`).join(" · ") || "—"} - {faNum(g.qty)} - {unit} - {isoToJalaliStr(g.updatedAt)} - 🕘 + {faNum(g.qty)} + {unit} + {isoToJalaliStr(g.updatedAt)} + 🕘 {isExp && ( @@ -367,14 +420,14 @@ function Timeline({ g, txs, allTxs, brandTxs, selTx, setSelTx, expBrand, setExpB key={t.id} type="button" onClick={() => setSelTx(sel ? null : t.id)} - className={`flex min-w-[120px] flex-col gap-0.5 rounded-lg border p-2 text-start ${sel ? "border-accent bg-accent/10" : "border-line bg-panel"}`} + className={`flex min-w-[120px] flex-col gap-0.5 rounded-lg border p-2 text-start transition-all ${sel ? "border-accent bg-accent/10 shadow-xs" : "border-line bg-panel hover:border-accent/40"}`} style={{ borderTop: `3px solid ${meta.color}` }} > {meta.icon} {meta.label} - + {faNum(Math.abs(t.quantity_number))} {unit} - مانده: {faNum(bal)} {unit} + مانده: {faNum(bal)} {unit} ); }); @@ -385,7 +438,7 @@ function Timeline({ g, txs, allTxs, brandTxs, selTx, setSelTx, expBrand, setExpB const brandSec = expBrand ? (
جریان برند: {brandNameFn(expBrand)} ({faNum(brandTxs(g, expBrand).length)} تراکنش)
-
{track(brandTxs(g, expBrand))}
+
{track(brandTxs(g, expBrand))}
) : null; @@ -395,21 +448,21 @@ function Timeline({ g, txs, allTxs, brandTxs, selTx, setSelTx, expBrand, setExpB const meta = TX_META[t.transaction_type] || { label: t.transaction_type, color: "#64748b", icon: "•" }; const pairTx = t.transfer_pair_id ? allTxs.find((x) => x.transfer_pair_id === t.transfer_pair_id && x.id !== t.id) : null; return ( -
+
{meta.label}
- تاریخ و ساعت: {isoToJalaliStr(t.transaction_date)} + تاریخ و ساعت: {isoToJalaliStr(t.transaction_date)} {t.transaction_type === "TRANSFER_OUT" || t.transaction_type === "TRANSFER_IN" ? ( <> - از انبار: {t.transaction_type === "TRANSFER_OUT" ? whNameFn(t.warehouse_id) : whNameFn(pairTx?.warehouse_id)} - به انبار: {t.transaction_type === "TRANSFER_IN" ? whNameFn(t.warehouse_id) : whNameFn(pairTx?.warehouse_id)} + از انبار: {t.transaction_type === "TRANSFER_OUT" ? whNameFn(t.warehouse_id) : whNameFn(pairTx?.warehouse_id)} + به انبار: {t.transaction_type === "TRANSFER_IN" ? whNameFn(t.warehouse_id) : whNameFn(pairTx?.warehouse_id)} ) : ( - انبار: {whNameFn(t.warehouse_id)} + انبار: {whNameFn(t.warehouse_id)} )} - {t.brand?.name && برند: {t.brand.name}} - {t.performed_by && انجام‌دهنده: {t.performed_by}} - {t.note && یادداشت: {t.note}} + {t.brand?.name && برند: {t.brand.name}} + {t.performed_by && انجام‌دهنده: {t.performed_by}} + {t.note && یادداشت: {t.note}} {t.transaction_type === "STOCK_COUNT" && t.recorded_qty != null && t.physical_qty != null && ( ثبت‌شده: {faNum(t.recorded_qty)} • شمارش‌شده: {faNum(t.physical_qty)} • اختلاف: {faNum(t.physical_qty - t.recorded_qty)} @@ -437,16 +490,16 @@ function Timeline({ g, txs, allTxs, brandTxs, selTx, setSelTx, expBrand, setExpB key={bid} type="button" onClick={() => setExpBrand(expBrand === bid ? null : bid)} - className={`rounded-lg border px-2 py-1 text-[11.5px] ${expBrand === bid ? "border-accent bg-accent/10 text-accent" : "border-line bg-panel"}`} + className={`rounded-lg border px-2 py-1 text-[11.5px] transition-colors ${expBrand === bid ? "border-accent bg-accent/10 text-accent font-bold" : "border-line bg-panel hover:bg-bg"}`} > - {brandNameFn(bid)}: {faNum(g.brands[bid])} {unit}{expBrand === bid ? " ✕" : ""} + {brandNameFn(bid)}: {faNum(g.brands[bid])} {unit}{expBrand === bid ? " ✕" : ""} ))}
{brandSec}
جریان کلی رکورد — ترکیب همه برندها ({faNum(txs.length)} تراکنش)
-
{track(txs)}
+
{track(txs)}
{selDetail}
); -} +} \ No newline at end of file