Merge remote-tracking branch 'origin/prod_v2' into integration
This commit is contained in:
@@ -11,9 +11,8 @@ import {
|
||||
} from "../../hooks";
|
||||
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";
|
||||
@@ -56,18 +55,31 @@ export function DashboardPage() {
|
||||
return [...list].sort((a, b) => (sortDir === "desc" ? createdTs(b) - createdTs(a) : createdTs(a) - createdTs(b)));
|
||||
}, [store, filters, sortDir]);
|
||||
|
||||
// شمارش تفکیکی پیشنهادها بر اساس پایپلاین خرید
|
||||
const stats = useMemo(() => {
|
||||
const total = quotes.length;
|
||||
const offered = quotes.filter((q) => q.status === "offered").length;
|
||||
const accepted = quotes.filter((q) => q.status === "accepted").length;
|
||||
const leadTimes = quotes
|
||||
.filter((q) => q.status === "arrived" && q.accepted_date && q.arrived_date)
|
||||
.map((q) => daysBetween(q.arrived_date, q.accepted_date))
|
||||
.filter((x): x is number => x != null);
|
||||
const avgLead = leadTimes.length ? Math.round(leadTimes.reduce((a, b) => a + b, 0) / leadTimes.length) : null;
|
||||
return { total, offered, accepted, avgLead };
|
||||
const arrived = quotes.filter((q) => q.status === "arrived").length;
|
||||
return { total, offered, accepted, arrived };
|
||||
}, [quotes]);
|
||||
|
||||
// وضعیت فعال در StatsBar بر اساس مقدار فعلی filters.status
|
||||
const activeStatusFilter: StatusFilter = useMemo(() => {
|
||||
if (filters.status === "offered") return "offered";
|
||||
if (filters.status === "accepted") return "accepted";
|
||||
if (filters.status === "arrived") return "arrived";
|
||||
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]);
|
||||
|
||||
@@ -119,7 +131,9 @@ export function DashboardPage() {
|
||||
total={stats.total}
|
||||
offered={stats.offered}
|
||||
accepted={stats.accepted}
|
||||
avgLeadTime={stats.avgLead}
|
||||
arrived={stats.arrived}
|
||||
activeFilter={activeStatusFilter}
|
||||
onFilterChange={handleStatusFilterChange}
|
||||
/>
|
||||
<FilterToolbar
|
||||
filters={filters}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import { SlidersHorizontal, X, ArrowUpDown, RotateCcw } from "lucide-react";
|
||||
import { Combobox } from "../../components/Combobox";
|
||||
import { STATUS_LABEL, STATUS_ORDER } from "../../lib/constants";
|
||||
|
||||
export interface Filters {
|
||||
category: string;
|
||||
@@ -10,8 +9,33 @@ export interface Filters {
|
||||
search: string;
|
||||
}
|
||||
|
||||
// لیست وضعیتها از dictionary واحد (constants) — نه آرایهی تکراری
|
||||
const STATUS_ITEMS = STATUS_ORDER.map((s) => STATUS_LABEL[s]);
|
||||
// گزینههای استاندارد و یکپارچه وضعیت هماهنگ با کارتهای بالای صفحه
|
||||
const STATUS_OPTIONS = [
|
||||
{ id: "offered", label: "پیشنهاد باز" },
|
||||
{ id: "accepted", label: "پذیرفتهشده" },
|
||||
{ id: "arrived", label: "تحویلشده" },
|
||||
];
|
||||
|
||||
const STATUS_MAP: Record<string, string> = {
|
||||
offered: "پیشنهاد باز",
|
||||
accepted: "پذیرفتهشده",
|
||||
arrived: "تحویلشده",
|
||||
};
|
||||
|
||||
const STATUS_TO_KEY: Record<string, string> = {
|
||||
"پیشنهاد باز": "offered",
|
||||
"پذیرفتهشده": "accepted",
|
||||
"تحویلشده": "arrived",
|
||||
"پیشنهاد شده": "offered",
|
||||
"پذیرفته شده": "accepted",
|
||||
"رسیده": "arrived",
|
||||
"باز": "offered",
|
||||
"پذیرفته": "accepted",
|
||||
"تحویل": "arrived",
|
||||
offered: "offered",
|
||||
accepted: "accepted",
|
||||
arrived: "arrived",
|
||||
};
|
||||
|
||||
export interface FilterToolbarProps {
|
||||
filters: Filters;
|
||||
@@ -40,6 +64,14 @@ export function FilterToolbar({
|
||||
onChange({ ...filters, category: "", supplier: "", status: "" });
|
||||
};
|
||||
|
||||
// تبدیل کلید انگلیسی به لیبل فارسی برای نمایش داخل کامبوباکس
|
||||
const statusDisplayValue = STATUS_MAP[filters.status] || filters.status;
|
||||
|
||||
const handleStatusChange = (val: string) => {
|
||||
const key = STATUS_TO_KEY[val] || val;
|
||||
set({ status: key });
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* ===== نمای موبایل (صفحهنمایشهای زیر md) ===== */}
|
||||
@@ -148,12 +180,12 @@ export function FilterToolbar({
|
||||
<div className="field">
|
||||
<label className="mb-1 block text-xs font-semibold text-muted">وضعیت</label>
|
||||
<Combobox
|
||||
value={filters.status}
|
||||
onChange={(it: { id: string; label: string }) => set({ status: it.label })}
|
||||
items={STATUS_ITEMS}
|
||||
value={statusDisplayValue}
|
||||
onChange={(it: { id: string; label: string }) => handleStatusChange(it.label)}
|
||||
items={STATUS_OPTIONS}
|
||||
allowAdd={false}
|
||||
placeholder="همه وضعیتها"
|
||||
onInputChange={(v) => set({ status: v })}
|
||||
onInputChange={handleStatusChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -197,12 +229,12 @@ export function FilterToolbar({
|
||||
<div className="field min-w-[150px] flex-1">
|
||||
<label>وضعیت</label>
|
||||
<Combobox
|
||||
value={filters.status}
|
||||
onChange={(it: { id: string; label: string }) => set({ status: it.label })}
|
||||
items={STATUS_ITEMS}
|
||||
value={statusDisplayValue}
|
||||
onChange={(it: { id: string; label: string }) => handleStatusChange(it.label)}
|
||||
items={STATUS_OPTIONS}
|
||||
allowAdd={false}
|
||||
placeholder="همه"
|
||||
onInputChange={(v) => set({ status: v })}
|
||||
onInputChange={handleStatusChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="field min-w-[200px] flex-1">
|
||||
|
||||
Reference in New Issue
Block a user