- Edit modal: for arrived (رسیده) quotes the status select is disabled with «به دلیل ثبت ورود اقلام به انبار، امکان تغییر وضعیت پیشنهاد وجود ندارد.» so no rollback to accepted/offered is possible; metadata stays editable. - Remove superfluous helper subtexts: parenthetical after «فاکتور رسمی» (wizard step 1) and the MinIO upload explanations under the invoice/proforma sections (Edit + Accept modals).
193 lines
7.8 KiB
TypeScript
193 lines
7.8 KiB
TypeScript
import { useMemo, useState } from "react";
|
||
import type { Material, Supplier } from "../../lib/types";
|
||
import { normalize } from "../../lib/normalize";
|
||
import { useSuppliers } from "../../hooks";
|
||
import { useAppStore } from "../../store";
|
||
import { TopicGrid } from "../../components/TopicGrid";
|
||
import { Combobox } from "../../components/Combobox";
|
||
import { cn } from "../../lib/utils";
|
||
|
||
export interface Step1Props {
|
||
selectedTopic: string;
|
||
onTopicSelect: (topic: string) => void;
|
||
supplierName: string;
|
||
onSupplierName: (name: string) => void;
|
||
newSup: { location: string; phone: string; contact: string };
|
||
onNewSup: (patch: Partial<{ location: string; phone: string; contact: string }>) => void;
|
||
isOfficial: boolean;
|
||
onOfficial: (v: boolean) => void;
|
||
invalid: boolean;
|
||
}
|
||
|
||
// مرحله ۱ — تاپیک + تأمینکننده (چیپهای سریع + فرم تأمینکنندهی جدید)
|
||
export function Step1TopicSupplier({
|
||
selectedTopic,
|
||
onTopicSelect,
|
||
supplierName,
|
||
onSupplierName,
|
||
newSup,
|
||
onNewSup,
|
||
isOfficial,
|
||
onOfficial,
|
||
invalid,
|
||
}: Step1Props) {
|
||
const suppliers = useSuppliers();
|
||
const topics = useAppStore((s) => s.topics);
|
||
const [selectedSupId, setSelectedSupId] = useState<string | null>(null);
|
||
|
||
const topicSups = useMemo(
|
||
() => (selectedTopic ? suppliers.filter((s) => normalize(s.topic) === normalize(selectedTopic)) : []),
|
||
[suppliers, selectedTopic],
|
||
);
|
||
|
||
const counts = useMemo(() => {
|
||
const out: Record<string, number> = {};
|
||
topics.forEach((t) => {
|
||
out[t.name] = suppliers.filter((s) => normalize(s.topic) === normalize(t.name)).length;
|
||
});
|
||
return out;
|
||
}, [topics, suppliers]);
|
||
|
||
// آیا تأمینکنندهی فعلی در لیست هست؟
|
||
const matched = useMemo(() => {
|
||
const n = normalize(supplierName);
|
||
if (!n) return null;
|
||
return topicSups.find((s) => normalize(s.name) === n) || null;
|
||
}, [topicSups, supplierName]);
|
||
|
||
const showNewSupForm = supplierName.trim().length > 0 && !matched;
|
||
|
||
const pickSupplier = (s: Supplier) => {
|
||
onSupplierName(s.name);
|
||
setSelectedSupId(s.id);
|
||
};
|
||
|
||
return (
|
||
<div>
|
||
<h3 className="mb-1 text-[15px] font-bold">
|
||
<span className="me-1 inline-flex h-6 w-6 items-center justify-center rounded-full bg-accent text-[12px] font-bold text-white">۱</span>
|
||
موضوع (دستهبندی کالا)
|
||
</h3>
|
||
<div className="mb-3 text-[12px] text-muted">
|
||
اول موضوع کالای موردنیاز را از بین تصاویر انتخاب کنید. تأمینکنندهها بر اساس موضوع فیلتر میشوند.
|
||
</div>
|
||
<TopicGrid
|
||
topics={topics}
|
||
selectedTopic={selectedTopic}
|
||
onSelect={onTopicSelect}
|
||
showCounts
|
||
counts={counts}
|
||
/>
|
||
|
||
{selectedTopic && (
|
||
<div className="mt-5">
|
||
<h3 className="mb-1 text-[15px] font-bold">
|
||
<span className="me-1 inline-flex h-6 w-6 items-center justify-center rounded-full bg-accent text-[12px] font-bold text-white">۱</span>
|
||
تأمینکننده
|
||
</h3>
|
||
<div className="mb-2 flex flex-wrap gap-1.5">
|
||
{!topicSups.length ? (
|
||
<div className="chip-divider">
|
||
هنوز تأمینکنندهای در این موضوع ثبت نشده — پایین را تایپ کنید تا بهعنوان تأمینکنندهی جدید ذخیره شود.
|
||
</div>
|
||
) : (
|
||
topicSups.map((s) => (
|
||
<button
|
||
key={s.id}
|
||
type="button"
|
||
className={cn("quick-chip", selectedSupId === s.id && "selected")}
|
||
onClick={() => pickSupplier(s)}
|
||
>
|
||
{s.name} <span className="qc-sub">{s.location || ""}</span>
|
||
</button>
|
||
))
|
||
)}
|
||
</div>
|
||
<div className={cn("field max-w-md", invalid && "invalid")}>
|
||
<label>یا نام تأمینکننده را تایپ و انتخاب کنید</label>
|
||
<Combobox
|
||
value={supplierName}
|
||
onChange={(it: { id: string; label: string }) => {
|
||
onSupplierName(it.label);
|
||
setSelectedSupId(it.id === it.label ? null : it.id);
|
||
}}
|
||
items={topicSups.map((s) => ({ id: s.id, name: s.name }))}
|
||
allowAdd
|
||
addSuffix="به عنوان تأمینکنندهٔ جدید"
|
||
placeholder="تایپ کنید... مثلاً زاگرس"
|
||
onInputChange={(v) => {
|
||
onSupplierName(v);
|
||
setSelectedSupId(null);
|
||
}}
|
||
/>
|
||
</div>
|
||
|
||
{showNewSupForm && (
|
||
<div className="mt-3 rounded-xl border border-dashed border-accent/40 bg-blue-bg/20 p-3">
|
||
<div className="mb-2 text-[12px] text-muted">
|
||
این تأمینکننده در لیست نیست. جزئیات را تکمیل کنید تا ذخیره شود:
|
||
</div>
|
||
<div className="grid max-w-2xl grid-cols-2 gap-3 max-md:grid-cols-1">
|
||
<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={newSup.location}
|
||
onChange={(e) => onNewSup({ 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={newSup.phone}
|
||
onChange={(e) => onNewSup({ 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={newSup.contact}
|
||
onChange={(e) => onNewSup({ contact: e.target.value })}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{supplierName.trim() && (
|
||
<label className="mt-3 flex cursor-pointer items-center gap-2 text-[13px]">
|
||
<input
|
||
type="checkbox"
|
||
className="accent-accent"
|
||
checked={isOfficial}
|
||
onChange={(e) => onOfficial(e.target.checked)}
|
||
/>
|
||
فاکتور رسمی
|
||
</label>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// دستههای موجود داخل یک تاپیک (از materials + تأمینکنندهها)
|
||
export function topicCategories(topic: string, suppliers: Supplier[], materials: Material[]): 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"));
|
||
}
|