feat(frontend): add React UI — procurement office, wizard, suppliers, inventory panel (dash port), role-based login
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
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 { PanelMenu } from "../../components/PanelMenu";
|
||||
import { useLogout } from "../Inventory/panelMenu";
|
||||
import { AcceptModal } from "./AcceptModal";
|
||||
import { EditModal } from "./EditModal";
|
||||
|
||||
const EMPTY_FILTERS: Filters = { category: "", supplier: "", status: "", search: "" };
|
||||
|
||||
// صفحهی اصلی دفتر پیشنهادهای خرید
|
||||
export function DashboardPage() {
|
||||
useStoreData();
|
||||
const logout = useLogout();
|
||||
|
||||
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 [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 }),
|
||||
[quotes, suppliers, materials, projects, categoryUnits, unitConversions, standardConversions],
|
||||
);
|
||||
|
||||
// فیلتر + مرتبسازی بر اساس زمان ساخت (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">
|
||||
<Header
|
||||
title="دفتر پیشنهادهای خرید"
|
||||
sub="دادهها در پایگاه داده ذخیره میشوند"
|
||||
brand
|
||||
menu={
|
||||
<PanelMenu
|
||||
title="پنلها"
|
||||
items={[
|
||||
{ label: "مدیریت انبار", to: "/inventory" },
|
||||
{ label: "مدیریت تدارکات", to: "/", active: true },
|
||||
{ divider: true },
|
||||
{ label: "خروج از حساب", onClick: logout },
|
||||
]}
|
||||
/>
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
<Link to="/suppliers" className="btn-line-file no-underline">
|
||||
<Users className="h-3.5 w-3.5" />
|
||||
تأمینکنندهها
|
||||
</Link>
|
||||
<Link to="/projects" className="btn-line-file no-underline">
|
||||
<FolderKanban className="h-3.5 w-3.5" />
|
||||
پروژهها
|
||||
</Link>
|
||||
<Link to="/units" className="btn-line-file no-underline">
|
||||
<Ruler className="h-3.5 w-3.5" />
|
||||
ضرایب تبدیل
|
||||
</Link>
|
||||
<Link to="/wizard" className="btn-line-file no-underline bg-accent text-white hover:bg-accent-dark">
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
افزودن پیشنهاد جدید
|
||||
</Link>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<PageLayout className="flex flex-col gap-4">
|
||||
<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>
|
||||
|
||||
<AcceptModal quoteId={acceptId} onClose={() => setAcceptId(null)} />
|
||||
<EditModal quoteId={editId} onClose={() => setEditId(null)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user