592 lines
22 KiB
TypeScript
592 lines
22 KiB
TypeScript
import { useEffect, useMemo, useState } from "react";
|
||
import { track } from "../../lib/analytics";
|
||
import { Link, useNavigate, useSearchParams } from "react-router-dom";
|
||
import { ChevronRight, ChevronLeft, Check } from "lucide-react";
|
||
import { toast } from "sonner";
|
||
import type { LineItem, Quote } from "../../lib/types";
|
||
import { api, ApiError, looksLikeUuid, nextId } from "../../lib/api";
|
||
import {
|
||
useAddMaterial,
|
||
useAddProject,
|
||
useAddSupplier,
|
||
useCreateQuote,
|
||
useMaterials,
|
||
useProjects,
|
||
useQuotes,
|
||
useStoreData,
|
||
useSuppliers,
|
||
} from "../../hooks";
|
||
import { topicByName } from "../../lib/topics";
|
||
import { useAppStore } from "../../store";
|
||
import { normalize } from "../../lib/normalize";
|
||
import { faNum } from "../../lib/formatters";
|
||
import { matchMaterial } from "../../lib/quoteUtils";
|
||
import { Header, PageLayout } from "../../components/layout/Header";
|
||
import { Stepper } from "./Stepper";
|
||
import { Step1TopicSupplier } from "./Step1TopicSupplier";
|
||
import { Step2LineItems, type ItemDraft, type MaterialRec } from "./Step2LineItems";
|
||
import { Step3Termsheets, effectiveCovered, type TsCardDraft } from "./Step3Termsheets";
|
||
import { Step4Projects } from "./Step4Projects";
|
||
import { Step5Review } from "./Step5Review";
|
||
import { Button } from "../../components/ui/button";
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogFooter,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
} from "../../components/ui/dialog";
|
||
|
||
const TOTAL_STEPS = 5;
|
||
|
||
// ویزارد ۵ مرحلهای افزودن پیشنهاد جدید
|
||
export function WizardPage() {
|
||
useStoreData();
|
||
const navigate = useNavigate();
|
||
const [params] = useSearchParams();
|
||
|
||
const quotes = useQuotes();
|
||
const suppliers = useSuppliers();
|
||
const materials = useMaterials();
|
||
const projects = useProjects();
|
||
const topics = useAppStore((s) => s.topics);
|
||
const createQuote = useCreateQuote();
|
||
const addSupplier = useAddSupplier();
|
||
const addMaterial = useAddMaterial();
|
||
const addProject = useAddProject();
|
||
|
||
// ---------- state ----------
|
||
const [currentStep, setCurrentStep] = useState(1);
|
||
const [highestVisited, setHighestVisited] = useState(1);
|
||
|
||
const [selectedTopic, setSelectedTopic] = useState("");
|
||
const [supplierName, setSupplierName] = useState("");
|
||
const [newSup, setNewSup] = useState({ location: "", phone: "", contact: "" });
|
||
const [isOfficial, setIsOfficial] = useState(false);
|
||
const [supplierInvalid, setSupplierInvalid] = useState(false);
|
||
|
||
const [items, setItems] = useState<ItemDraft[]>(() => [
|
||
{ tempId: "it0", cat: "", brand: "", specs: [], qty: null, unit: "", price: null, total: null },
|
||
]);
|
||
const [cards, setCards] = useState<TsCardDraft[]>(() => [
|
||
{ key: "ts0", milestones: [{ when_iso: null, amount: null }] },
|
||
]);
|
||
const [coverage, setCoverage] = useState<Record<string, string[]>>({});
|
||
const [assignment, setAssignment] = useState<Record<string, string[]>>({});
|
||
|
||
const [quoteDate, setQuoteDate] = useState<string | null>(() => {
|
||
return new Date().toISOString();
|
||
});
|
||
const [expiryDate, setExpiryDate] = useState<string | null>(null);
|
||
const [notes, setNotes] = useState("");
|
||
const [proforma, setProforma] = useState<File | null>(null);
|
||
const [emptyFields, setEmptyFields] = useState<{ label: string; step: number }[]>([]);
|
||
const [confirmOpen, setConfirmOpen] = useState(false);
|
||
|
||
// ---------- prefill از لینکها (?topic=&supplier=) ----------
|
||
useEffect(() => {
|
||
const t = params.get("topic");
|
||
if (!t) return;
|
||
const topic = topicByName(t, topics);
|
||
if (!topic) return;
|
||
setSelectedTopic(topic.name);
|
||
const sid = params.get("supplier");
|
||
if (sid) {
|
||
const sup = suppliers.find((s) => s.id === sid);
|
||
if (sup && normalize(sup.topic) === normalize(topic.name)) {
|
||
setSupplierName(sup.name);
|
||
setSupplierInvalid(false);
|
||
}
|
||
}
|
||
}, [params, suppliers, topics]);
|
||
|
||
// ---------- اعتبارسنجی قبل از ترک هر مرحله ----------
|
||
const completedSteps = useMemo(() => {
|
||
const s = new Set<number>();
|
||
if (selectedTopic && supplierName.trim()) s.add(1);
|
||
if (items.length > 0 && items.some((it) => it.qty != null && it.qty !== 0 && it.price != null && it.price !== 0)) s.add(2);
|
||
if (
|
||
cards.length > 0 &&
|
||
cards.some((card) => (card.milestones || []).some((m) => m.when_iso || (m.amount != null && m.amount !== 0)))
|
||
) {
|
||
s.add(3);
|
||
}
|
||
if (items.length > 0 && items.every((it) => (assignment[it.tempId] || []).length > 0)) s.add(4);
|
||
if (highestVisited >= 5) s.add(5);
|
||
return s;
|
||
}, [selectedTopic, supplierName, items, cards, coverage, assignment, highestVisited]);
|
||
|
||
const goToStep = (n: number) => {
|
||
setCurrentStep(n);
|
||
setHighestVisited((h) => Math.max(h, n));
|
||
track("wizard_step", { step: n, direction: n > currentStep ? "forward" : "back" });
|
||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||
};
|
||
|
||
const next = () => {
|
||
goToStep(currentStep + 1);
|
||
};
|
||
|
||
const rememberMaterial = (_rec: MaterialRec) => {};
|
||
|
||
// ---------- پروژهها ----------
|
||
const projectNames = useMemo(() => {
|
||
const names = new Set<string>(projects.map((p) => p.name));
|
||
quotes.forEach((q) => {
|
||
if (q.project) names.add(q.project);
|
||
(q.projects || []).forEach((n) => names.add(n));
|
||
});
|
||
return [...names];
|
||
}, [projects, quotes]);
|
||
|
||
const assign = (tempId: string, proj: string) => {
|
||
setAssignment((a) => {
|
||
const cur = a[tempId] || [];
|
||
if (cur.includes(proj)) return a;
|
||
return { ...a, [tempId]: [...cur, proj] };
|
||
});
|
||
};
|
||
|
||
const unassign = (tempId: string, proj: string) => {
|
||
setAssignment((a) => {
|
||
const cur = (a[tempId] || []).filter((p) => p !== proj);
|
||
const next = { ...a };
|
||
if (cur.length) next[tempId] = cur;
|
||
else delete next[tempId];
|
||
return next;
|
||
});
|
||
};
|
||
|
||
// ---------- خلاصه مرحله ۵ ----------
|
||
const review = useMemo(() => {
|
||
let grandTotal = 0;
|
||
items.forEach((it) => {
|
||
const t = it.total;
|
||
if (t) grandTotal += t;
|
||
});
|
||
const projSet = new Set<string>();
|
||
items.forEach((it) => (assignment[it.tempId] || []).forEach((p) => projSet.add(p)));
|
||
const coveredTempIds = new Set<string>();
|
||
cards.forEach((card) =>
|
||
effectiveCovered(cards, coverage, items, card.key).forEach((tid) => coveredTempIds.add(tid)),
|
||
);
|
||
const orphanCount = items.filter((it) => !coveredTempIds.has(it.tempId)).length;
|
||
return {
|
||
supplierName: supplierName.trim() || "—",
|
||
topicName: selectedTopic || "—",
|
||
projects: [...projSet],
|
||
itemCount: items.length,
|
||
tsCount: cards.length,
|
||
grandTotal,
|
||
isOfficial,
|
||
orphanCount,
|
||
};
|
||
}, [items, assignment, cards, coverage, supplierName, selectedTopic, isOfficial]);
|
||
|
||
// ---------- ذخیره ----------
|
||
const doSave = async () => {
|
||
const resolveErrorTarget = (err: unknown): { step: number; field?: string } => {
|
||
let field = "";
|
||
let msg = "";
|
||
if (err instanceof ApiError) {
|
||
field = err.field || "";
|
||
msg = err.message || "";
|
||
} else if (err instanceof Error) {
|
||
msg = err.message || "";
|
||
if ("field" in err && typeof (err as any).field === "string") {
|
||
field = (err as any).field;
|
||
}
|
||
}
|
||
|
||
const combined = `${field} ${msg}`.toLowerCase();
|
||
|
||
if (/supplier|تأمین|تامین/i.test(combined)) return { step: 1, field: "supplier_id" };
|
||
if (/line_item|material|canonical_unit|unit_id|quantity|متریال|واحد|قلم|تعداد/i.test(combined)) {
|
||
return { step: 2, field: field || "line_items" };
|
||
}
|
||
if (/termsheet|item_termsheet|schedule_step|due_date|milestone|ترم|پرداخت|پوشش|سررسید/i.test(combined)) {
|
||
return { step: 3, field: field || "termsheets" };
|
||
}
|
||
if (/project|پروژه/i.test(combined)) return { step: 4, field: "project" };
|
||
if (/expiry_date|تاریخ انقضا/i.test(combined)) return { step: 5, field: "expiry_date" };
|
||
if (/quote_date|تاریخ پیشنهاد/i.test(combined)) return { step: 5, field: "quote_date" };
|
||
if (/proforma|پیشفاکتور/i.test(combined)) return { step: 5, field: "proforma" };
|
||
if (/notes|یادداشت/i.test(combined)) return { step: 5, field: "notes" };
|
||
|
||
return { step: 5, field };
|
||
};
|
||
|
||
const sectionError = (stepOrTarget: number | { step: number; field?: string }, title: string, err: unknown) => {
|
||
const target = typeof stepOrTarget === "number" ? { step: stepOrTarget } : stepOrTarget;
|
||
const msg = err instanceof Error ? err.message : "خطای ناشناخته";
|
||
toast.error(`${title}: ${msg}`, {
|
||
duration: 8000,
|
||
action: {
|
||
label: "رفع خطا",
|
||
onClick: () => {
|
||
goToStep(target.step);
|
||
setTimeout(() => {
|
||
const targetAttr = target.field ? `[data-field="${target.field}"]` : "";
|
||
const el = (targetAttr ? document.querySelector(targetAttr) : null) ||
|
||
document.querySelector(`[data-step="${target.step}"]`);
|
||
if (el instanceof HTMLElement) {
|
||
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
||
const input = el.querySelector("input, textarea, select") as HTMLElement | null;
|
||
if (input) input.focus();
|
||
else el.focus?.();
|
||
el.classList.add("ring-2", "ring-red-500", "rounded-xl");
|
||
setTimeout(() => el.classList.remove("ring-2", "ring-red-500", "rounded-xl"), 3500);
|
||
}
|
||
}, 150);
|
||
},
|
||
},
|
||
});
|
||
};
|
||
|
||
const name = supplierName.trim();
|
||
let supplierId = "";
|
||
if (name) {
|
||
const exist = suppliers.find((s) => normalize(s.name) === normalize(name));
|
||
if (exist) {
|
||
supplierId = exist.id;
|
||
} else {
|
||
try {
|
||
const created = await addSupplier.mutateAsync({
|
||
id: nextId("s", suppliers),
|
||
topic: selectedTopic,
|
||
categories: [],
|
||
name,
|
||
location: newSup.location.trim(),
|
||
phone: newSup.phone.trim(),
|
||
contact: newSup.contact.trim(),
|
||
});
|
||
supplierId = created.id;
|
||
} catch (err) {
|
||
sectionError(1, "خطا در بخش تأمینکننده", err);
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
const collected: { li: LineItem; tempId: string }[] = [];
|
||
for (const it of items) {
|
||
const cat = it.cat.trim();
|
||
const brand = it.brand.trim();
|
||
const specs = it.specs;
|
||
const unit = it.unit.trim();
|
||
const qty = it.qty;
|
||
const price = it.price;
|
||
const total = it.total;
|
||
if (!cat && !brand && qty == null && price == null) continue;
|
||
|
||
const wantName = (cat || brand || specs.join(" ") || "").trim() || "کالای جدید";
|
||
let material = materials.find((m) => looksLikeUuid(m.id) && normalize(m.name) === normalize(wantName));
|
||
if (!material) {
|
||
const matched = matchMaterial(
|
||
materials.filter((m) => looksLikeUuid(m.id)),
|
||
cat,
|
||
brand,
|
||
specs,
|
||
);
|
||
if (matched) material = matched;
|
||
}
|
||
let materialId: string;
|
||
if (material && looksLikeUuid(material.id)) {
|
||
materialId = material.id;
|
||
} else {
|
||
try {
|
||
const created = await addMaterial.mutateAsync({
|
||
id: nextId("m", materials),
|
||
name: wantName,
|
||
topic: selectedTopic,
|
||
brand,
|
||
specs,
|
||
base_unit: unit || "عدد",
|
||
});
|
||
materialId = created.id;
|
||
} catch (err) {
|
||
sectionError(2, "خطا در بخش اقلام (متریال)", err);
|
||
return;
|
||
}
|
||
}
|
||
|
||
collected.push({
|
||
li: {
|
||
id: "li-" + (collected.length + 1),
|
||
material_id: materialId,
|
||
quantity: qty,
|
||
unit,
|
||
unit_price: price,
|
||
total_price: total,
|
||
termsheet_ids: [],
|
||
accepted_ts_id: null,
|
||
rejected: false,
|
||
arrived_date: null,
|
||
},
|
||
tempId: it.tempId,
|
||
});
|
||
}
|
||
|
||
const lineItems = collected.map((c) => c.li);
|
||
|
||
const termsheets = cards
|
||
.map((card, i) => ({
|
||
id: "new-ts-" + (i + 1),
|
||
label: "گزینه " + faNum(i + 1),
|
||
milestones: card.milestones.filter((m) => m.when_iso || m.amount != null),
|
||
status: "offered" as const,
|
||
}))
|
||
.filter((t) => t.milestones.length);
|
||
|
||
collected.forEach(({ li, tempId }) => {
|
||
const ids = cards
|
||
.map((card, i) =>
|
||
effectiveCovered(cards, coverage, items, card.key).includes(tempId) ? "new-ts-" + (i + 1) : "",
|
||
)
|
||
.filter(Boolean);
|
||
const valid = ids.filter((tid) => termsheets.some((t) => t.id === tid));
|
||
li.termsheet_ids = valid.length ? valid : termsheets.length ? [termsheets[0].id] : [];
|
||
});
|
||
|
||
const projSet = new Set<string>();
|
||
collected.forEach(({ li, tempId }) => {
|
||
const list = assignment[tempId] || [];
|
||
li.projects = list.map((p) => ({ id: "", name: p }));
|
||
list.forEach((p) => projSet.add(p));
|
||
});
|
||
const projectsList = [...projSet];
|
||
|
||
const quoteIso = quoteDate || new Date().toISOString();
|
||
const quote: Quote = {
|
||
id: nextId("q", quotes),
|
||
supplier_id: supplierId,
|
||
project: projectsList[0] || "",
|
||
projects: projectsList,
|
||
quote_date: quoteIso,
|
||
expiry_date: expiryDate,
|
||
accepted_date: null,
|
||
arrived_date: null,
|
||
status: "offered",
|
||
is_official: isOfficial,
|
||
proformas: [],
|
||
invoices: [],
|
||
events: [{ type: "offered", occurred_at: quoteIso }],
|
||
notes: notes.trim(),
|
||
line_items: lineItems,
|
||
termsheets,
|
||
};
|
||
|
||
try {
|
||
const created = await createQuote.mutateAsync(quote);
|
||
if (proforma) {
|
||
try {
|
||
await api.files.upload(created.id, "proforma", proforma);
|
||
} catch (err) {
|
||
toast.error(`خطا در آپلود پیشفاکتور: ${err instanceof Error ? err.message : "خطای ناشناخته"}`, {
|
||
action: { label: "رفع خطا", onClick: () => goToStep(5) },
|
||
});
|
||
}
|
||
}
|
||
toast.success("پیشنهاد ذخیره شد.");
|
||
track("quote_submitted", { quote_id: created.id, items: lineItems.length });
|
||
navigate("/");
|
||
} catch (e) {
|
||
const target = resolveErrorTarget(e);
|
||
sectionError(target, "خطا در ثبت پیشنهاد", e);
|
||
}
|
||
};
|
||
|
||
const save = () => {
|
||
const incomplete: { label: string; step: number }[] = [];
|
||
if (!completedSteps.has(1)) incomplete.push({ label: "تأمینکننده", step: 1 });
|
||
if (!completedSteps.has(2)) incomplete.push({ label: "اقلام", step: 2 });
|
||
if (!completedSteps.has(3)) incomplete.push({ label: "شرایط پرداخت", step: 3 });
|
||
if (!completedSteps.has(4)) incomplete.push({ label: "پروژه", step: 4 });
|
||
if (incomplete.length > 0) {
|
||
setEmptyFields(incomplete);
|
||
setConfirmOpen(true);
|
||
return;
|
||
}
|
||
void doSave();
|
||
};
|
||
|
||
return (
|
||
<div className="min-h-screen pb-10">
|
||
<Header
|
||
title="افزودن پیشنهاد جدید"
|
||
sub="اطلاعات را از تماس با تأمینکننده وارد کنید"
|
||
backTo="/"
|
||
backLabel="بازگشت به دفتر"
|
||
/>
|
||
<PageLayout>
|
||
{/* نوار مراحل استپر */}
|
||
<Stepper current={currentStep} highestVisited={highestVisited} completedSteps={completedSteps} onGoto={goToStep} />
|
||
|
||
{/* محتوای مراحل با پدینگ ریسپانسیو و بهینه برای موبایل */}
|
||
{currentStep === 1 && (
|
||
<div data-step="1" className="rounded-xl border border-line bg-panel p-3.5 sm:p-5 shadow-xs">
|
||
<Step1TopicSupplier
|
||
selectedTopic={selectedTopic}
|
||
onTopicSelect={(t) => {
|
||
setSelectedTopic(t);
|
||
setSupplierName("");
|
||
setNewSup({ location: "", phone: "", contact: "" });
|
||
setIsOfficial(false);
|
||
setSupplierInvalid(false);
|
||
}}
|
||
supplierName={supplierName}
|
||
onSupplierName={(v) => {
|
||
setSupplierName(v);
|
||
setSupplierInvalid(false);
|
||
}}
|
||
newSup={newSup}
|
||
onNewSup={(p) => setNewSup((s) => ({ ...s, ...p }))}
|
||
isOfficial={isOfficial}
|
||
onOfficial={setIsOfficial}
|
||
invalid={supplierInvalid}
|
||
/>
|
||
</div>
|
||
)}
|
||
{currentStep === 2 && (
|
||
<div data-step="2" className="rounded-xl border border-line bg-panel p-3.5 sm:p-5 shadow-xs">
|
||
<Step2LineItems items={items} onItemsChange={setItems} selectedTopic={selectedTopic} onRemember={rememberMaterial} />
|
||
</div>
|
||
)}
|
||
{currentStep === 3 && (
|
||
<div data-step="3" className="rounded-xl border border-line bg-panel p-3.5 sm:p-5 shadow-xs">
|
||
<Step3Termsheets
|
||
cards={cards}
|
||
onCardsChange={setCards}
|
||
items={items}
|
||
coverage={coverage}
|
||
onCoverageChange={(key, ids) => setCoverage((c) => ({ ...c, [key]: ids }))}
|
||
/>
|
||
</div>
|
||
)}
|
||
{currentStep === 4 && (
|
||
<div data-step="4" className="rounded-xl border border-line bg-panel p-3.5 sm:p-5 shadow-xs">
|
||
<Step4Projects
|
||
items={items}
|
||
projects={projectNames}
|
||
assignment={assignment}
|
||
onAssign={assign}
|
||
onUnassign={unassign}
|
||
onAddProject={(name) => {
|
||
void addProject.mutateAsync({ id: nextId("p", projects), name });
|
||
}}
|
||
/>
|
||
</div>
|
||
)}
|
||
{currentStep === 5 && (
|
||
<div data-step="5" className="rounded-xl border border-line bg-panel p-3.5 sm:p-5 shadow-xs">
|
||
<Step5Review
|
||
quoteDate={quoteDate}
|
||
onQuoteDate={setQuoteDate}
|
||
expiryDate={expiryDate}
|
||
onExpiryDate={setExpiryDate}
|
||
notes={notes}
|
||
onNotes={setNotes}
|
||
proforma={proforma}
|
||
onProforma={setProforma}
|
||
summary={review}
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
{/* پاورقی کنترل مراحل */}
|
||
<div className="mt-4 flex items-center justify-between rounded-xl border border-line bg-panel p-3 sm:p-3.5 shadow-xs">
|
||
<div>
|
||
{currentStep > 1 && (
|
||
<button
|
||
type="button"
|
||
onClick={() => goToStep(currentStep - 1)}
|
||
className="inline-flex items-center gap-1 px-3 sm:px-4 py-2 rounded-xl border border-slate-200 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-slate-700 dark:text-zinc-200 hover:bg-slate-50 text-xs sm:text-sm font-medium transition-colors shadow-2xs"
|
||
>
|
||
<ChevronRight className="h-4 w-4" />
|
||
<span>قبلی</span>
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
<div className="flex items-center gap-2 sm:gap-3">
|
||
<Link
|
||
to="/"
|
||
className="inline-flex items-center justify-center px-3 sm:px-4 py-2 rounded-xl text-xs sm:text-sm font-medium text-slate-500 hover:text-slate-700 no-underline"
|
||
>
|
||
انصراف
|
||
</Link>
|
||
|
||
{currentStep < TOTAL_STEPS && (
|
||
<button
|
||
type="button"
|
||
onClick={next}
|
||
className="inline-flex items-center gap-1 px-4 sm:px-6 py-2 rounded-xl bg-blue-600 hover:bg-blue-700 active:bg-blue-800 text-white text-xs sm:text-sm font-semibold shadow-xs transition-all"
|
||
>
|
||
<span>بعدی</span>
|
||
<ChevronLeft className="h-4 w-4" />
|
||
</button>
|
||
)}
|
||
|
||
{currentStep === TOTAL_STEPS && (
|
||
<button
|
||
type="button"
|
||
onClick={() => void save()}
|
||
className="inline-flex items-center gap-1 px-4 sm:px-6 py-2 rounded-xl bg-emerald-600 hover:bg-emerald-700 active:bg-emerald-800 text-white text-xs sm:text-sm font-semibold shadow-xs transition-all"
|
||
>
|
||
<span>ذخیره پیشنهاد</span>
|
||
<Check className="h-4 w-4" />
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* دیالوگ فیلدهای خالی */}
|
||
<Dialog open={confirmOpen} onOpenChange={setConfirmOpen}>
|
||
<DialogContent>
|
||
<DialogHeader>
|
||
<DialogTitle>فیلدهای خالی</DialogTitle>
|
||
</DialogHeader>
|
||
<div className="text-[13.5px] leading-7 text-gray-700">
|
||
هنوز تعدادی از فیلدها خالی هستند. آیا مطمئنید که میخواهید ادامه دهید؟
|
||
</div>
|
||
<div className="flex flex-wrap gap-x-3 gap-y-1 text-[13px]">
|
||
{emptyFields.map((f) => (
|
||
<button
|
||
key={f.step}
|
||
type="button"
|
||
className="cursor-pointer text-blue-600 underline underline-offset-2 hover:text-blue-800"
|
||
onClick={() => {
|
||
setConfirmOpen(false);
|
||
goToStep(f.step);
|
||
}}
|
||
>
|
||
{f.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
<DialogFooter className="sm:justify-between">
|
||
<Button
|
||
variant="green"
|
||
onClick={() => {
|
||
setConfirmOpen(false);
|
||
void doSave();
|
||
}}
|
||
>
|
||
ذخیره کردن
|
||
</Button>
|
||
<Button
|
||
className="bg-red text-white hover:brightness-95"
|
||
onClick={() => {
|
||
setConfirmOpen(false);
|
||
goToStep(emptyFields[0]?.step ?? 1);
|
||
}}
|
||
>
|
||
تکمیل فیلدها
|
||
</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
</PageLayout>
|
||
</div>
|
||
);
|
||
} |