Files
ERP/frontend/src/pages/Wizard/Step1TopicSupplier.tsx
T
whqreza 4dd60d7cd9 fix(quotes): lock status on arrived quotes + trim modal helper copy
- 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).
2026-09-06 19:03:19 +03:30

193 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"));
}