Files
ERP/frontend/src/pages/Dashboard/DashboardPage.tsx
T
MMD 7ead60cd5c Merge main_v2 mobile UI into production, restore mobile bottom tab bar
Merges main_v2 a110741 (mobile UI optimizations) into production and
reinstates the mobile bottom navigation that f0e4243 had removed.

Conflicts in DashboardPage, QuoteCard and InventoryPage resolved in
favour of main_v2's mobile-first markup, keeping production's newer
design system (Button variant="primary" on InventoryPage).

f0e4243's removal was reverted in effect: MobileTabBar is restored and
remounted in App.tsx, and PageLayout regains max-md:pb-24 clearance so
content is not hidden behind the fixed bar.
2026-09-16 14:14:59 +03:30

155 lines
6.5 KiB
TypeScript

import { useMemo, useState } from "react";
import { Link } from "react-router-dom";
import { Users, FolderKanban, Ruler, Plus } from "lucide-react";
import {
useMaterials,
useProjects,
useQuotes,
useStoreData,
useSuppliers,
useUnits,
} 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 { FilterToolbar, type Filters } from "./FilterToolbar";
import { LedgerTable } from "./LedgerTable";
import { AppSwitcherMenu } from "../../components/AppSwitcherMenu";
import { AcceptModal } from "./AcceptModal";
import { EditModal } from "./EditModal";
const EMPTY_FILTERS: Filters = { category: "", supplier: "", status: "", search: "" };
export function DashboardPage() {
useStoreData();
const quotes = useQuotes();
const suppliers = useSuppliers();
const materials = useMaterials();
const projects = useProjects();
const { categoryUnits, unitConversions, standardConversions } = useUnits();
const expandedQuoteIds = useAppStore((s) => s.expandedQuoteIds);
const toggleExpand = useAppStore((s) => s.toggleExpand);
const topics = useAppStore((s) => s.topics);
const [filters, setFilters] = useState<Filters>(EMPTY_FILTERS);
const [sortDir, setSortDir] = useState<"desc" | "asc">("desc");
const [acceptId, setAcceptId] = useState<string | null>(null);
const [editId, setEditId] = useState<string | null>(null);
const store = useMemo(
() => ({ quotes, suppliers, materials, projects, categoryUnits, unitConversions, standardConversions, topics }),
[quotes, suppliers, materials, projects, categoryUnits, unitConversions, standardConversions, topics],
);
// فیلتر + مرتب‌سازی بر اساس زمان ساخت (created_at)
const filtered = useMemo(() => {
const list = filterQuotes(store, filters);
const createdTs = (q: (typeof store.quotes)[number]) => {
const iso = q.created_at || q.quote_date;
const ts = iso ? new Date(iso).getTime() : 0;
return Number.isFinite(ts) ? ts : 0;
};
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 };
}, [quotes]);
const categoryOptions = useMemo(() => [...new Set(materials.map((m) => m.category))], [materials]);
const supplierOptions = useMemo(() => suppliers.map((s) => ({ id: s.id, name: s.name })), [suppliers]);
return (
<div className="min-h-screen pb-20 md:pb-6">
<Header
title="دفتر پیشنهادهای خرید"
brand
menu={<AppSwitcherMenu activeModule="procurement" />}
actions={
<div className="hidden md:flex items-center gap-2">
<Link to="/suppliers" className="btn-line-file no-underline">
<Users className="h-3.5 w-3.5" />
تأمین‌کننده‌ها
</Link>
{/* دکمه پروژه‌ها */}
<Link
to="/projects"
className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-xl border border-gray-200 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-200 text-xs font-semibold hover:bg-gray-50 dark:hover:bg-zinc-700 shadow-2xs transition-all no-underline"
>
<FolderKanban className="h-4 w-4 text-gray-500 dark:text-zinc-400" />
<span>پروژه‌ها</span>
</Link>
{/* دکمه ضرایب تبدیل */}
<Link
to="/units"
className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-xl border border-gray-200 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-200 text-xs font-semibold hover:bg-gray-50 dark:hover:bg-zinc-700 shadow-2xs transition-all no-underline"
>
<Ruler className="h-4 w-4 text-gray-500 dark:text-zinc-400" />
<span>ضرایب تبدیل</span>
</Link>
{/* دکمه اصلی: افزودن پیشنهاد جدید */}
<Link
to="/wizard"
className="inline-flex items-center gap-1.5 px-4 py-2 rounded-xl bg-blue-600 hover:bg-blue-700 active:bg-blue-800 text-white text-xs font-semibold shadow-sm hover:shadow transition-all no-underline"
>
<Plus className="h-4 w-4" />
<span>افزودن پیشنهاد جدید</span>
</Link>
</div>
}
/>
<PageLayout className="flex flex-col gap-3 px-3 md:px-6">
<StatsBar
total={stats.total}
offered={stats.offered}
accepted={stats.accepted}
avgLeadTime={stats.avgLead}
/>
<FilterToolbar
filters={filters}
onChange={setFilters}
categories={categoryOptions}
suppliers={supplierOptions}
sortDir={sortDir}
onToggleSort={() => setSortDir((d) => (d === "desc" ? "asc" : "desc"))}
/>
<LedgerTable
store={store}
quotes={filtered}
expanded={expandedQuoteIds}
onToggleExpand={toggleExpand}
onAccept={setAcceptId}
onEdit={setEditId}
/>
</PageLayout>
{/* دکمه شناور اکشن (FAB) مخصوص نسخه موبایل */}
<Link
to="/wizard"
aria-label="افزودن پیشنهاد جدید"
className="md:hidden fixed bottom-20 left-4 z-40 flex h-14 w-14 items-center justify-center rounded-full bg-accent text-white shadow-xl shadow-accent/30 active:scale-95 transition-transform"
>
<Plus className="h-7 w-7" />
</Link>
<AcceptModal quoteId={acceptId} onClose={() => setAcceptId(null)} />
<EditModal quoteId={editId} onClose={() => setEditId(null)} />
</div>
);
}