diff --git a/frontend/src/components/StatsBar.tsx b/frontend/src/components/StatsBar.tsx index 44a1029..5fd3931 100644 --- a/frontend/src/components/StatsBar.tsx +++ b/frontend/src/components/StatsBar.tsx @@ -1,15 +1,27 @@ import { ClipboardList, Inbox, CheckCircle2, Timer, type LucideIcon } from "lucide-react"; import { fmtNum } from "../lib/formatters"; +export type StatusFilter = "all" | "offered" | "accepted"; + export interface StatsBarProps { total: number; offered: number; accepted: number; avgLeadTime: number | null; // روز + activeFilter?: StatusFilter; + onFilterChange?: (filter: StatusFilter) => void; } -export function StatsBar({ total, offered, accepted, avgLeadTime }: StatsBarProps) { +export function StatsBar({ + total, + offered, + accepted, + avgLeadTime, + activeFilter = "all", + onFilterChange, +}: StatsBarProps) { const stats: { + key?: StatusFilter; label: string; shortLabel: string; value: string; @@ -18,6 +30,7 @@ export function StatsBar({ total, offered, accepted, avgLeadTime }: StatsBarProp icon: LucideIcon; }[] = [ { + key: "all", label: "کل پیشنهادها", shortLabel: "کل", value: fmtNum(total), @@ -25,6 +38,7 @@ export function StatsBar({ total, offered, accepted, avgLeadTime }: StatsBarProp icon: ClipboardList, }, { + key: "offered", label: "پیشنهاد باز", shortLabel: "باز", value: fmtNum(offered), @@ -32,6 +46,7 @@ export function StatsBar({ total, offered, accepted, avgLeadTime }: StatsBarProp icon: Inbox, }, { + key: "accepted", label: "پذیرفته‌شده", shortLabel: "پذیرفته", value: fmtNum(accepted), @@ -48,39 +63,61 @@ export function StatsBar({ total, offered, accepted, avgLeadTime }: StatsBarProp }, ]; + const handleClick = (key?: StatusFilter) => { + if (!key || !onFilterChange) return; + if (key === "all") { + onFilterChange("all"); + } else { + // در صورت کلیک مجدد روی همان کارت، فیلتر ریست شده و به کل برمی‌گردد + onFilterChange(activeFilter === key ? "all" : key); + } + }; + return (
- {stats.map((s) => ( -
- {/* عنوان کارت */} -
- - {s.shortLabel} - {s.label} - + {stats.map((s) => { + const isClickable = Boolean(s.key && onFilterChange); + const isActive = s.key && activeFilter === s.key; - {/* آیکون در دسکتاپ */} - - - -
- - {/* مقدار عددی به همراه واحد روز */} + return (
handleClick(s.key)} + className={`rounded-xl border p-2 text-center transition-all sm:rounded-lg sm:p-3.5 sm:text-right ${ + isClickable ? "cursor-pointer select-none" : "" + } ${ + isActive + ? "border-accent ring-2 ring-accent/20 bg-accent/5 shadow-sm" + : "border-line bg-panel hover:border-accent/40" + }`} > - {s.value} - {s.unit && ( - - {s.unit} + {/* عنوان کارت */} +
+ + {s.shortLabel} + {s.label} - )} + + {/* آیکون در دسکتاپ */} + + + +
+ + {/* مقدار عددی به همراه واحد روز */} +
+ {s.value} + {s.unit && ( + + {s.unit} + + )} +
-
- ))} + ); + })}
); } \ No newline at end of file diff --git a/frontend/src/lib/quoteUtils.ts b/frontend/src/lib/quoteUtils.ts index 455e351..ca5d8fd 100644 --- a/frontend/src/lib/quoteUtils.ts +++ b/frontend/src/lib/quoteUtils.ts @@ -288,8 +288,13 @@ export function filterQuotes( "پیشنهاد شده": "offered", "پذیرفته شده": "accepted", "رسیده": "arrived", + "باز": "offered", + "پذیرفته": "accepted", + "offered": "offered", + "accepted": "accepted", + "arrived": "arrived", }; - const stKey = STATUS_BY_LABEL[stVal] || ""; + const stKey = STATUS_BY_LABEL[stVal] || stVal; return store.quotes.filter((quote) => { const supObj = supplierById(store.suppliers, quote.supplier_id); @@ -313,11 +318,16 @@ export function filterQuotes( export function isExpired(q: Quote): boolean { return ( q.status === "offered" && + !quoteHasArrivedOrAccepted(q) && !!q.expiry_date && new Date(q.expiry_date) < new Date(new Date().toDateString()) ); } +function quoteHasArrivedOrAccepted(q: Quote): boolean { + return q.status === "accepted" || q.status === "arrived"; +} + // آماده‌سازی نمایش ترم‌شیت در حالت expanded export function termSheetBlocks( store: Store, @@ -347,4 +357,4 @@ export function termSheetBlocks( }); return { ts, isAccepted, isUsed, itemsDesc, milestones }; }); -} +} \ No newline at end of file diff --git a/frontend/src/pages/Dashboard/DashboardPage.tsx b/frontend/src/pages/Dashboard/DashboardPage.tsx index 7d88728..ba5fd5a 100644 --- a/frontend/src/pages/Dashboard/DashboardPage.tsx +++ b/frontend/src/pages/Dashboard/DashboardPage.tsx @@ -13,7 +13,7 @@ import { useAppStore } from "../../store"; import { filterQuotes } from "../../lib/quoteUtils"; import { daysBetween } from "../../lib/formatters"; import { Header, PageLayout } from "../../components/layout/Header"; -import { StatsBar } from "../../components/StatsBar"; +import { StatsBar, type StatusFilter } from "../../components/StatsBar"; import { FilterToolbar, type Filters } from "./FilterToolbar"; import { LedgerTable } from "./LedgerTable"; import { AppSwitcherMenu } from "../../components/AppSwitcherMenu"; @@ -68,6 +68,21 @@ export function DashboardPage() { return { total, offered, accepted, avgLead }; }, [quotes]); + // تعیین وضعیت فعال در StatsBar بر اساس مقدار فعلی filters.status + const activeStatusFilter: StatusFilter = useMemo(() => { + if (filters.status === "offered") return "offered"; + if (filters.status === "accepted") return "accepted"; + return "all"; + }, [filters.status]); + + // تغییر فیلتر وضعیت هنگام کلیک روی کارت‌ها + const handleStatusFilterChange = (status: StatusFilter) => { + setFilters((prev) => ({ + ...prev, + status: status === "all" ? "" : status, + })); + }; + const categoryOptions = useMemo(() => [...new Set(materials.map((m) => m.category))], [materials]); const supplierOptions = useMemo(() => suppliers.map((s) => ({ id: s.id, name: s.name })), [suppliers]); @@ -87,7 +102,7 @@ export function DashboardPage() { {/* دکمه پروژه‌ها */} پروژه‌ها @@ -96,7 +111,7 @@ export function DashboardPage() { {/* دکمه ضرایب تبدیل */} ضرایب تبدیل @@ -120,6 +135,8 @@ export function DashboardPage() { offered={stats.offered} accepted={stats.accepted} avgLeadTime={stats.avgLead} + activeFilter={activeStatusFilter} + onFilterChange={handleStatusFilterChange} />