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
79 lines
2.6 KiB
TypeScript
79 lines
2.6 KiB
TypeScript
import { Link } from "react-router-dom";
|
||
import { MapPin, Phone, User, Plus } from "lucide-react";
|
||
import type { Material, Supplier } from "../../lib/types";
|
||
import { normalize } from "../../lib/normalize";
|
||
export function topicCategories(topic: string, materials: Material[], suppliers: Supplier[]): string[] {
|
||
const out = new Set<string>();
|
||
materials
|
||
.filter((m) => topic && normalize(m.topic) === normalize(topic) && m.category)
|
||
.forEach((m) => out.add(m.category));
|
||
suppliers
|
||
.filter((s) => topic && normalize(s.topic) === normalize(topic))
|
||
.forEach((s) => (s.categories || []).forEach((c) => c && out.add(c)));
|
||
return [...out].sort((a, b) => a.localeCompare(b, "fa"));
|
||
}
|
||
|
||
export function suppliersOfCategory(topic: string, cat: string, suppliers: Supplier[]): Supplier[] {
|
||
return suppliers.filter(
|
||
(s) => normalize(s.topic) === normalize(topic) && (s.categories || []).some((c) => normalize(c) === normalize(cat)),
|
||
);
|
||
}
|
||
|
||
// کارت تأمینکننده
|
||
export function SupplierCard({
|
||
s,
|
||
topic,
|
||
deletable,
|
||
onDelete,
|
||
materials = [],
|
||
}: {
|
||
s: Supplier;
|
||
topic: string;
|
||
deletable?: boolean;
|
||
onDelete?: (id: string) => void;
|
||
// متریالهای خریداریشده از این تأمینکننده (بهجای دستهبندی)
|
||
materials?: string[];
|
||
}) {
|
||
return (
|
||
<div className="sup-card">
|
||
<div className="sc-head">
|
||
<Link to={`/suppliers/${s.id}`} className="sc-name-link">
|
||
{s.name}
|
||
</Link>
|
||
{deletable && onDelete && (
|
||
<button type="button" className="sup-del" title="حذف تأمینکننده" onClick={() => onDelete(s.id)}>
|
||
×
|
||
</button>
|
||
)}
|
||
</div>
|
||
<div className="sc-meta">
|
||
<span>
|
||
<MapPin className="me-1 inline h-3 w-3" /> {s.location || "—"}
|
||
</span>
|
||
<span>
|
||
<Phone className="me-1 inline h-3 w-3" /> {s.phone || "—"}
|
||
</span>
|
||
<span>
|
||
<User className="me-1 inline h-3 w-3" /> {s.contact || "—"}
|
||
</span>
|
||
</div>
|
||
{materials.length > 0 && (
|
||
<div className="mt-2 border-t border-line pt-1.5">
|
||
<div className="mb-1 flex flex-wrap gap-1">
|
||
{materials.map((m, i) => (
|
||
<span key={i} className="spec-chip">
|
||
{m}
|
||
</span>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
<div className="sc-actions">
|
||
<Link to={`/wizard?topic=${encodeURIComponent(topic)}&supplier=${encodeURIComponent(s.id)}`} className="btn-line-file no-underline">
|
||
<Plus className="h-3 w-3" />
|
||
پیشنهاد جدید
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
);
|
||
} |