Categories are kept only as the label each supplier carries (suppliers.category_id + the material_categories list it points at). Everything that treated category as a shared key across materials, inventory, quotes and unit conversions is removed — including two features that were never populated (0 rows on every volume). Removed (backend): - routers category_attributes, category_ref_units, unit_conversions + registration - ?category_id= filters on /materials, /inventory and /quotes (the /suppliers one stays — it filters suppliers by their own label) - GET /inventory/materials/options: required a category and had no caller - the material-payload category derivation (expand_service.category_by_material, the inventory serializer lookup) and the material category on /pm/on-the-way; the wire keys stay, now always null, so the payload shape is unchanged - startup DDL: DROP the empty category_attributes / category_reference_units / unit_conversions tables (idempotent) and their CREATE statements; seed.py truncate list updated Removed (frontend): - the category-keyed conversion path: refUnitForCategory / findConversion / refConverted, the categoryUnits + unitConversions store slices, api clients, hooks and the "≈ converted quantity" line on quote detail and project pages (it needed rows in those tables to render, and there were none) - material.supplier_category and its read sites (the edit modal grew one yesterday to restore labels; the label lives on the supplier now) - the inventory page's category chips, the row filter and the «دسته» column, plus the lookup's categories fetch and catName helper Kept: supplier label + its list and picker, /suppliers?category_id=, the global standard_conversions feature and the units page (material_units conversions). Verified against a restore of the production dump: startup DDL drops exactly the three tables, suppliers keep their label (3/3), material_categories still serves its 8 rows, the removed endpoints 404, and materials/inventory/quotes/units/ standard-conversions all return 200 with quote line items intact.
167 lines
6.7 KiB
TypeScript
167 lines
6.7 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 { Header, PageLayout } from "../../components/layout/Header";
|
|
import { StatsBar, type StatusFilter } 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 = { supplier: "", status: "", search: "" };
|
|
|
|
export function DashboardPage() {
|
|
useStoreData();
|
|
|
|
const quotes = useQuotes();
|
|
const suppliers = useSuppliers();
|
|
const materials = useMaterials();
|
|
const projects = useProjects();
|
|
const { 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, standardConversions, topics }),
|
|
[quotes, suppliers, materials, projects, 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 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 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}
|
|
arrived={stats.arrived}
|
|
activeFilter={activeStatusFilter}
|
|
onFilterChange={handleStatusFilterChange}
|
|
/>
|
|
<FilterToolbar
|
|
filters={filters}
|
|
onChange={setFilters}
|
|
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>
|
|
);
|
|
} |