fix(suppliers): enforce Category → Supplier hierarchy, remove rogue-category bug
Domain model (v2.6): suppliers.category_id NOT NULL (1:N from the 8 fixed categories), materials are global and link to suppliers via supplier_materials. This fixes three regressions of the old inverted tree: - remove the intermediate 'یک متریال را انتخاب کنید' screen: a category card now routes straight to that category's supplier list (/suppliers/category/:category); TopicPage is deleted and legacy /topics/:topic[/:category] URLs redirect to the new hierarchy - kill the accidental-category bug: api.materials.create silently POSTed /material-categories for any unknown 'category' (a material name!), which is how the rogue «پریز» category (+ junk material) appeared. Unknown categories now fail loudly instead of creating catalog rows (DB cleaned: exactly the 8 canonical categories remain) - fix inverted wording: supplier list is «تأمینکنندههای این دسته» with category-first breadcrumbs; the legacy material multi-select on the add-supplier form is gone (a supplier gets the page's category) - supplier details screen logic untouched — only its breadcrumb link targets were updated to the new routes
This commit is contained in:
@@ -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 ? <Navigate to={`/topics/${encodeURIComponent(topic)}`} replace /> : <Navigate to="/suppliers" replace />;
|
||||
}
|
||||
if (!cat) return <Navigate to="/suppliers" replace />;
|
||||
|
||||
const sups = suppliersOfCategory(topic, matched, suppliers);
|
||||
const sups = suppliersOfCategory(cat, cat, suppliers);
|
||||
|
||||
// متریالهای خریداریشده از هر تأمینکننده
|
||||
// متریالهای خریداریشده از هر تأمینکننده (برای نمایش روی کارت)
|
||||
const supMats = useMemo(() => {
|
||||
const map = new Map<string, string[]>();
|
||||
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 (
|
||||
<div className="min-h-screen">
|
||||
<Header title={matched} sub={`تأمینکنندههای این متریال — ${topic}`} backTo="/suppliers" backLabel="موضوعها" />
|
||||
<Header title={cat} sub="تأمینکنندههای این دسته" backTo="/suppliers" backLabel="دستهبندیها" />
|
||||
<PageLayout className="flex flex-col gap-4">
|
||||
{!sups.length ? (
|
||||
<EmptyState icon={Inbox} text="هنوز تأمینکنندهای برای این متریال ثبت نشده است." />
|
||||
<EmptyState icon={Inbox} text="هنوز تأمینکنندهای برای این دسته ثبت نشده است." />
|
||||
) : (
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4">
|
||||
{sups.map((s) => (
|
||||
<SupplierCard key={s.id} s={s} topic={topic} deletable onDelete={removeSup} materials={supMats.get(s.id) || []} />
|
||||
<SupplierCard key={s.id} s={s} topic={cat} deletable onDelete={removeSup} materials={supMats.get(s.id) || []} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -130,15 +122,6 @@ export function CategoryPage() {
|
||||
onInputChange={(v) => setForm((f) => ({ ...f, name: v }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label>متریال (ها)</label>
|
||||
<MultiSelectCombobox
|
||||
values={form.categories}
|
||||
onChange={(cats) => setForm((f) => ({ ...f, categories: cats }))}
|
||||
options={topicCategories(topic, materials, suppliers)}
|
||||
placeholder="متریال را انتخاب یا تایپ کنید..."
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label>موقعیت (شهر - منطقه)</label>
|
||||
<input
|
||||
|
||||
Reference in New Issue
Block a user