Files
ERP/frontend/src/pages/Suppliers/CategoryPage.tsx
T

188 lines
7.5 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 { daysBetween } from "../../lib/formatters";
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";
// صفحه‌ی تأمین‌کننده‌های یک دسته + فرم افزودن تأمین‌کننده
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: "",
});
// همه‌ی تأمین‌کننده‌ها برای انتخاب سریع
const supplierOptions = useMemo(
() => suppliers.map((s) => ({ id: s.id, name: s.name })),
[suppliers],
);
// محاسبه میانگین زمان تحویل اختصاصی هر تأمین‌کننده بر اساس سفارش‌های تحویل‌شده
const supLeadTimes = useMemo(() => {
const map = new Map<string, number | null>();
suppliers.forEach((s) => {
const supQuotes = quotes.filter(
(q) => q.supplier_id === s.id && q.status === "arrived" && q.accepted_date && q.arrived_date
);
const times = supQuotes
.map((q) => daysBetween(q.arrived_date, q.accepted_date))
.filter((x): x is number => x != null);
const avg = times.length ? Math.round(times.reduce((a, b) => a + b, 0) / times.length) : null;
map.set(s.id, avg);
});
return map;
}, [quotes, 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) || []}
avgLeadTime={supLeadTimes.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>
);
}