New shared materialLabel() renders the global materials.name with brand/specs appended only when not already part of the name (deduped; «؟» fallback kept). Replaces the category-first composer in five places: supplier card badges (CategoryPage), dashboard item lines (QuoteCard), ledger items summary, project rows and purchase history. Also fixes the stale Units subtitle «ضریب تبدیل همان دسته» → «ضریب تبدیل متریال» (category model kept).
165 lines
6.9 KiB
TypeScript
165 lines
6.9 KiB
TypeScript
import { useMemo, useState } from "react";
|
|
import { Navigate, useParams } from "react-router-dom";
|
|
import { toast } from "sonner";
|
|
import { Inbox, Plus } from "lucide-react";
|
|
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 { topicByName } from "../../lib/topics";
|
|
import { Header, PageLayout } from "../../components/layout/Header";
|
|
import { EmptyState } from "../../components/EmptyState";
|
|
import { Combobox } from "../../components/Combobox";
|
|
import { SupplierCard, suppliersOfCategory } from "./supplierUtils";
|
|
import { materialLabel } from "../../lib/quoteUtils";
|
|
import { Button } from "../../components/ui/button";
|
|
|
|
// صفحهی تأمینکنندههای یک دسته + فرم افزودن تأمینکننده
|
|
// (سلسلهمراتب دامنه: دسته → تأمینکننده؛ در v2.6 دسته = تاپیک)
|
|
export function CategoryPage() {
|
|
useStoreData();
|
|
const { category: catParam = "" } = useParams();
|
|
|
|
const materials = useMaterials();
|
|
const suppliers = useSuppliers();
|
|
const quotes = useQuotes();
|
|
const addSupplier = useAddSupplier();
|
|
const removeSupplier = useRemoveSupplier();
|
|
const topics = useAppStore((s) => s.topics);
|
|
|
|
const cat = useMemo(() => topicByName(catParam, topics)?.name ?? null, [catParam, topics]);
|
|
|
|
const [form, setForm] = useState<{ name: string; location: string; phone: string; contact: string }>({
|
|
name: "",
|
|
location: "",
|
|
phone: "",
|
|
contact: "",
|
|
});
|
|
|
|
// همهی تأمینکنندهها برای انتخاب سریع (task: کومبوکس کششده)
|
|
const supplierOptions = useMemo(
|
|
() => suppliers.map((s) => ({ id: s.id, name: s.name })),
|
|
[suppliers],
|
|
);
|
|
|
|
if (!cat) return <Navigate to="/suppliers" replace />;
|
|
|
|
const sups = suppliersOfCategory(cat, cat, suppliers);
|
|
|
|
// متریالهای خریداریشده از هر تأمینکننده (برای نمایش روی کارت)
|
|
const supMats = useMemo(() => {
|
|
const map = new Map<string, string[]>();
|
|
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 = materialLabel(m);
|
|
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]);
|
|
|
|
const addSup = () => {
|
|
const name = form.name.trim();
|
|
if (!name) {
|
|
toast.error("نام تأمینکننده را وارد کنید.");
|
|
return;
|
|
}
|
|
// تأمینکننده دقیقاً زیر همین دسته ثبت میشود (۱ دسته به ازای هر تأمینکننده)
|
|
const s: Supplier = {
|
|
id: nextId("s", suppliers),
|
|
topic: cat,
|
|
categories: [cat],
|
|
name,
|
|
location: form.location.trim(),
|
|
phone: form.phone.trim(),
|
|
contact: form.contact.trim(),
|
|
};
|
|
void addSupplier.mutateAsync(s);
|
|
setForm({ name: "", location: "", phone: "", contact: "" });
|
|
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);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen">
|
|
<Header title={cat} sub="تأمینکنندههای این دسته" backTo="/suppliers" backLabel="دستهبندیها" />
|
|
<PageLayout className="flex flex-col gap-4">
|
|
{!sups.length ? (
|
|
<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={cat} deletable onDelete={removeSup} materials={supMats.get(s.id) || []} />
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
<div className="add-panel">
|
|
<h4>
|
|
<Plus className="me-1 inline h-4 w-4 text-accent" /> افزودن تأمینکنندهی جدید
|
|
</h4>
|
|
<div className="grid max-w-2xl grid-cols-2 gap-3 max-md:grid-cols-1">
|
|
<div className="field">
|
|
<label>نام تأمینکننده *</label>
|
|
<Combobox
|
|
value={form.name}
|
|
onChange={(it: { id: string; label: string }) => setForm((f) => ({ ...f, name: it.label }))}
|
|
items={supplierOptions}
|
|
allowAdd
|
|
addSuffix="به عنوان تأمینکنندهٔ جدید"
|
|
placeholder="تایپ کنید یا از تأمینکنندهها انتخاب کنید"
|
|
onInputChange={(v) => setForm((f) => ({ ...f, name: v }))}
|
|
/>
|
|
</div>
|
|
<div className="field">
|
|
<label>موقعیت (شهر - منطقه)</label>
|
|
<input
|
|
type="text"
|
|
className="h-9 w-full rounded-lg border border-line bg-panel px-3 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40"
|
|
placeholder="مثلاً تهران - ظفر"
|
|
value={form.location}
|
|
onChange={(e) => setForm((f) => ({ ...f, location: e.target.value }))}
|
|
/>
|
|
</div>
|
|
<div className="field">
|
|
<label>تلفن</label>
|
|
<input
|
|
type="text"
|
|
className="h-9 w-full rounded-lg border border-line bg-panel px-3 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40"
|
|
placeholder="۰۲۱-..."
|
|
value={form.phone}
|
|
onChange={(e) => setForm((f) => ({ ...f, phone: e.target.value }))}
|
|
/>
|
|
</div>
|
|
<div className="field">
|
|
<label>شخص رابط</label>
|
|
<input
|
|
type="text"
|
|
className="h-9 w-full rounded-lg border border-line bg-panel px-3 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40"
|
|
placeholder="نام فرد"
|
|
value={form.contact}
|
|
onChange={(e) => setForm((f) => ({ ...f, contact: e.target.value }))}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<Button size="sm" className="mt-3" onClick={addSup}>
|
|
ذخیره تأمینکننده
|
|
</Button>
|
|
</div>
|
|
</PageLayout>
|
|
</div>
|
|
);
|
|
}
|