diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 54267f1..0b51874 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -10,7 +10,6 @@ const WizardPage = lazy(() => import("./pages/Wizard/WizardPage").then((m) => ({ const SuppliersPage = lazy(() => import("./pages/Suppliers/SuppliersPage").then((m) => ({ default: m.SuppliersPage })), ); -const TopicPage = lazy(() => import("./pages/Suppliers/TopicPage").then((m) => ({ default: m.TopicPage }))); const CategoryPage = lazy(() => import("./pages/Suppliers/CategoryPage").then((m) => ({ default: m.CategoryPage })), ); @@ -93,22 +92,25 @@ export default function App() { } /> + {/* سلسله‌مراتب دامنه: دسته → تأمین‌کننده. کارت دسته مستقیم به + فهرست تأمین‌کننده‌های همان دسته می‌رود (بدون صفحه‌ی واسط متریال). */} - - - } - /> - } /> + {/* مسیرهای قدیمی (درخت معکوس topic → material → supplier) — ریدایرکت */} + } + /> + } + /> => { const lookup = await fetchLookup(); - let categoryId = m.category ? lookup.categoryNameToId.get(norm(m.category)) : undefined; - // اگر دسته در بک‌اند نیست (مثلاً بعد از حذف seed) — بساز - if (!categoryId && m.category) { - const cat = await postData("/material-categories", { - slug: norm(m.category) || "cat", - name: m.category, - is_active: true, - }); - categoryId = cat.id; - lookup.categoryNameToId.set(norm(m.category), cat.id); - } + const categoryId = m.category ? lookup.categoryNameToId.get(norm(m.category)) : undefined; + // دسته‌ها همان ۸ دسته‌ی ثابت سیستم‌اند و هرگز به‌صورت خودکار ساخته + // نمی‌شوند — قبلاً این‌جا برای هر نامِ ناشناخته یک رکورد material_categories + // می‌ساخت و باگ «پریز» (دسته‌ی rogue) را به وجود آورد. if (!categoryId) { - throw new Error(`متریال «${m.category}» در بک‌اند تعریف نشده است`); + throw new Error( + m.category + ? `دسته «${m.category}» در سامانه تعریف نشده است — یکی از ۸ دسته‌ی موجود را انتخاب کنید` + : "دسته‌ی متریال مشخص نیست — یکی از ۸ دسته‌ی موجود را انتخاب کنید", + ); } // واحد مرجع — اگر تعریف نشده، بساز (بک‌اند canonical_unit_id الزامی دارد) const unitName = m.base_unit || "عدد"; diff --git a/frontend/src/pages/Suppliers/CategoryPage.tsx b/frontend/src/pages/Suppliers/CategoryPage.tsx index 33c6af8..eaab3c5 100644 --- a/frontend/src/pages/Suppliers/CategoryPage.tsx +++ b/frontend/src/pages/Suppliers/CategoryPage.tsx @@ -6,18 +6,18 @@ import type { Supplier } from "../../lib/types"; import { nextId } from "../../lib/api"; import { useAddSupplier, useMaterials, useQuotes, useRemoveSupplier, useStoreData, useSuppliers } from "../../hooks"; import { useAppStore } from "../../store"; -import { normalize } from "../../lib/normalize"; +import { topicByName } from "../../lib/topics"; import { Header, PageLayout } from "../../components/layout/Header"; import { EmptyState } from "../../components/EmptyState"; import { Combobox } from "../../components/Combobox"; -import { MultiSelectCombobox } from "../../components/MultiSelectCombobox"; -import { SupplierCard, suppliersOfCategory, topicCategories, topicFromParam } from "./supplierUtils"; +import { SupplierCard, suppliersOfCategory } from "./supplierUtils"; import { Button } from "../../components/ui/button"; -// صفحه‌ی تأمین‌کننده‌های یک متریال + فرم افزودن تأمین‌کننده +// صفحه‌ی تأمین‌کننده‌های یک دسته + فرم افزودن تأمین‌کننده +// (سلسله‌مراتب دامنه: دسته → تأمین‌کننده؛ در v2.6 دسته = تاپیک) export function CategoryPage() { useStoreData(); - const { topic: topicParam = "", category: catParam = "" } = useParams(); + const { category: catParam = "" } = useParams(); const materials = useMaterials(); const suppliers = useSuppliers(); @@ -26,15 +26,10 @@ export function CategoryPage() { const removeSupplier = useRemoveSupplier(); const topics = useAppStore((s) => s.topics); - const topic = useMemo(() => topicFromParam(topicParam, topics), [topicParam, topics]); - const matched = useMemo( - () => (topic ? topicCategories(topic, materials, suppliers).find((c) => normalize(c) === normalize(catParam)) : undefined), - [topic, catParam, materials, suppliers], - ); + const cat = useMemo(() => topicByName(catParam, topics)?.name ?? null, [catParam, topics]); - const [form, setForm] = useState<{ name: string; categories: string[]; location: string; phone: string; contact: string }>({ + const [form, setForm] = useState<{ name: string; location: string; phone: string; contact: string }>({ name: "", - categories: [], location: "", phone: "", contact: "", @@ -46,13 +41,11 @@ export function CategoryPage() { [suppliers], ); - if (!topic || !matched) { - return topic ? : ; - } + if (!cat) return ; - const sups = suppliersOfCategory(topic, matched, suppliers); + const sups = suppliersOfCategory(cat, cat, suppliers); - // متریال‌های خریداری‌شده از هر تأمین‌کننده + // متریال‌های خریداری‌شده از هر تأمین‌کننده (برای نمایش روی کارت) const supMats = useMemo(() => { const map = new Map(); quotes.forEach((q) => { @@ -75,19 +68,18 @@ export function CategoryPage() { toast.error("نام تأمین‌کننده را وارد کنید."); return; } - // اگر متریالی انتخاب نشده، تأمین‌کننده به همین متریالِ صفحه اضافه می‌شود - const cats = form.categories.length ? form.categories : [matched]; + // تأمین‌کننده دقیقاً زیر همین دسته ثبت می‌شود (۱ دسته به ازای هر تأمین‌کننده) const s: Supplier = { id: nextId("s", suppliers), - topic, - categories: cats, + topic: cat, + categories: [cat], name, location: form.location.trim(), phone: form.phone.trim(), contact: form.contact.trim(), }; void addSupplier.mutateAsync(s); - setForm({ name: "", categories: [], location: "", phone: "", contact: "" }); + setForm({ name: "", location: "", phone: "", contact: "" }); toast.success("تأمین‌کننده افزوده شد."); }; @@ -101,14 +93,14 @@ export function CategoryPage() { return (
-
+
{!sups.length ? ( - + ) : (
{sups.map((s) => ( - + ))}
)} @@ -130,15 +122,6 @@ export function CategoryPage() { onInputChange={(v) => setForm((f) => ({ ...f, name: v }))} />
-
- - setForm((f) => ({ ...f, categories: cats }))} - options={topicCategories(topic, materials, suppliers)} - placeholder="متریال را انتخاب یا تایپ کنید..." - /> -
{firstCat ? ( - + ‹ بازگشت به دسته ) : ( - - ‹ بازگشت به موضوع + + ‹ بازگشت به دسته‌بندی‌ها )}
diff --git a/frontend/src/pages/Suppliers/SuppliersPage.tsx b/frontend/src/pages/Suppliers/SuppliersPage.tsx index 348ca79..be803fc 100644 --- a/frontend/src/pages/Suppliers/SuppliersPage.tsx +++ b/frontend/src/pages/Suppliers/SuppliersPage.tsx @@ -25,14 +25,14 @@ export function SuppliersPage() {
navigate(`/topics/${encodeURIComponent(topic)}`)} + onSelect={(topic) => navigate(`/suppliers/category/${encodeURIComponent(topic)}`)} showCounts counts={counts} /> diff --git a/frontend/src/pages/Suppliers/TopicPage.tsx b/frontend/src/pages/Suppliers/TopicPage.tsx deleted file mode 100644 index fd2528c..0000000 --- a/frontend/src/pages/Suppliers/TopicPage.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import { useMemo, useState } from "react"; -import { Navigate, useNavigate, useParams } from "react-router-dom"; -import { toast } from "sonner"; -import { FolderOpen, Plus } from "lucide-react"; -import type { Material } from "../../lib/types"; -import { nextId } from "../../lib/api"; -import { useAddMaterial, useMaterials, useQuotes, useRemoveSupplier, useStoreData, useSuppliers } from "../../hooks"; -import { useAppStore } from "../../store"; -import { normalize } from "../../lib/normalize"; -import { faNum } from "../../lib/formatters"; -import { Header, PageLayout } from "../../components/layout/Header"; -import { EmptyState } from "../../components/EmptyState"; -import { SupplierCard, suppliersOfCategory, topicCategories, topicFromParam } from "./supplierUtils"; -import { Button } from "../../components/ui/button"; - -// صفحه‌ی متریال‌های یک موضوع + تأمین‌کننده‌های بدون متریال + افزودن متریال -export function TopicPage() { - useStoreData(); - const { topic: topicParam = "" } = useParams(); - const navigate = useNavigate(); - - const materials = useMaterials(); - const suppliers = useSuppliers(); - const quotes = useQuotes(); - const addMaterial = useAddMaterial(); - const removeSupplier = useRemoveSupplier(); - const topics = useAppStore((s) => s.topics); - - const topic = useMemo(() => topicFromParam(topicParam, topics), [topicParam, topics]); - const [newCatName, setNewCatName] = useState(""); - - const cats = useMemo( - () => (topic ? topicCategories(topic, materials, suppliers) : []), - [topic, materials, suppliers], - ); - - const uncat = useMemo( - () => - topic - ? suppliers.filter((s) => normalize(s.topic) === normalize(topic) && !(s.categories || []).length) - : [], - [topic, suppliers], - ); - - if (!topic) return ; - - const addCategory = () => { - const name = newCatName.trim(); - if (!name) return; - const dup = materials.some( - (m) => normalize(m.topic) === normalize(topic) && normalize(m.category) === normalize(name), - ); - if (dup) { - toast.error("این متریال قبلاً ثبت شده است."); - return; - } - const m: Material = { - id: nextId("m", materials), - name, - topic, - category: name, - brand: "", - specs: [], - base_unit: "", - }; - void addMaterial.mutateAsync(m); - setNewCatName(""); - toast.success("متریال افزوده شد."); - }; - - const removeSup = (id: string) => { - const s = suppliers.find((x) => x.id === id); - if (!s) return; - if (confirm(`تأمین‌کننده «${s.name}» حذف شود؟ پیشنهادهای قبلی او در دفتر باقی می‌مانند.`)) { - void removeSupplier.mutateAsync(id); - } - }; - - // متریال‌های خریداری‌شده از هر تأمین‌کننده - const supMats = useMemo(() => { - const map = new Map(); - quotes.forEach((q) => { - (q.line_items || []).forEach((li) => { - if (li.rejected) return; - const m = materials.find((x) => x.id === li.material_id); - if (!m) return; - const label = `${m.category}${m.brand ? " " + m.brand : ""}${m.specs.length ? " (" + m.specs.join("، ") + ")" : ""}`; - if (!map.has(q.supplier_id)) map.set(q.supplier_id, []); - const list = map.get(q.supplier_id)!; - if (!list.includes(label)) list.push(label); - }); - }); - return map; - }, [quotes, materials]); - - return ( -
-
- - {cats.length ? ( -
- {cats.map((c) => ( - - ))} -
- ) : ( - - )} - - {uncat.length > 0 && ( -
-

تأمین‌کننده‌های این موضوع

-
- {uncat.map((s) => ( - - ))} -
-
- )} - -
-

- افزودن متریال جدید -

-
-
- - setNewCatName(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter") { - e.preventDefault(); - addCategory(); - } - }} - /> -
- -
-
- بعد از افزودن، داخل همان متریال می‌توانید تأمین‌کننده اضافه یا حذف کنید. -
-
-
-
- ); -} diff --git a/frontend/src/pages/Suppliers/supplierUtils.tsx b/frontend/src/pages/Suppliers/supplierUtils.tsx index 3452f95..e52e862 100644 --- a/frontend/src/pages/Suppliers/supplierUtils.tsx +++ b/frontend/src/pages/Suppliers/supplierUtils.tsx @@ -1,8 +1,7 @@ import { Link } from "react-router-dom"; import { MapPin, Phone, User, Plus } from "lucide-react"; -import type { Material, Supplier, Topic } from "../../lib/types"; +import type { Material, Supplier } from "../../lib/types"; import { normalize } from "../../lib/normalize"; -import { topicByName } from "../../lib/topics"; export function topicCategories(topic: string, materials: Material[], suppliers: Supplier[]): string[] { const out = new Set(); materials @@ -20,12 +19,6 @@ export function suppliersOfCategory(topic: string, cat: string, suppliers: Suppl ); } -export function topicFromParam(param: string | null, topics: Topic[]): string | null { - if (!param) return null; - const t = topicByName(param, topics); - return t ? t.name : null; -} - // کارت تأمین‌کننده export function SupplierCard({ s,