feat(wizard): add real-time bi-directional sync, debounced auto-save, and bot draft prefill
This commit is contained in:
+1
-1
@@ -11,7 +11,7 @@ services:
|
|||||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
|
||||||
|
|
||||||
ports:
|
ports:
|
||||||
- "5433:5432"
|
- "5455:5432"
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
- default
|
- default
|
||||||
|
|||||||
+15
-3
@@ -1,5 +1,5 @@
|
|||||||
import { lazy, Suspense } from "react";
|
import { lazy, Suspense } from "react";
|
||||||
import { Navigate, Route, Routes } from "react-router-dom";
|
import { Navigate, Route, Routes, useLocation } from "react-router-dom";
|
||||||
import { useAuthStore } from "./store/auth";
|
import { useAuthStore } from "./store/auth";
|
||||||
import { ErrorBoundary } from "./components/ErrorBoundary";
|
import { ErrorBoundary } from "./components/ErrorBoundary";
|
||||||
import { RequirePmProjects } from "./components/RequirePmProjects";
|
import { RequirePmProjects } from "./components/RequirePmProjects";
|
||||||
@@ -52,10 +52,22 @@ function Loading() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// همهی صفحات پشت ورود هستند — بدون نشست، به صفحهی ورود میرویم.
|
// همهی صفحات پشت ورود هستند — به جز پیشنویسهای ویزارد از ربات یا بدون نشست که به صفحهی ورود میروند.
|
||||||
function RequireAuth({ children }: { children: React.ReactNode }) {
|
function RequireAuth({ children }: { children: React.ReactNode }) {
|
||||||
const user = useAuthStore((s) => s.user);
|
const user = useAuthStore((s) => s.user);
|
||||||
if (!user) return <Navigate to="/login" replace />;
|
const location = useLocation();
|
||||||
|
|
||||||
|
// در صورت داشتن شناسه پیشنویس (از ربات بله)، دسترسی مستقیم به ویزارد بدون لاگین مجاز است
|
||||||
|
const searchParams = new URLSearchParams(location.search);
|
||||||
|
const hasDraft = searchParams.has("draft") || searchParams.has("draft_id");
|
||||||
|
if (location.pathname === "/wizard" && hasDraft) {
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
const returnUrl = encodeURIComponent(location.pathname + location.search);
|
||||||
|
return <Navigate to={`/login?returnUrl=${returnUrl}`} replace />;
|
||||||
|
}
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1020,6 +1020,41 @@ export const api = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
drafts: {
|
||||||
|
get: async (
|
||||||
|
id: string,
|
||||||
|
): Promise<{
|
||||||
|
id: string;
|
||||||
|
chat_id: number;
|
||||||
|
kind: string;
|
||||||
|
status: string;
|
||||||
|
payload: Record<string, any>;
|
||||||
|
missing?: { fields?: { ok: boolean; key: string; label: string; blocking: boolean }[] } | null;
|
||||||
|
source?: string;
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
}> => {
|
||||||
|
return getData(`/drafts/${id}`);
|
||||||
|
},
|
||||||
|
update: async (
|
||||||
|
id: string,
|
||||||
|
payload: Record<string, any>,
|
||||||
|
): Promise<void> => {
|
||||||
|
await patchData(`/drafts/${id}`, { payload });
|
||||||
|
},
|
||||||
|
commit: async (
|
||||||
|
id: string,
|
||||||
|
quoteId?: string,
|
||||||
|
): Promise<void> => {
|
||||||
|
await postData(`/drafts/${id}/commit`, { quote_id: quoteId });
|
||||||
|
},
|
||||||
|
cancel: async (
|
||||||
|
id: string,
|
||||||
|
): Promise<void> => {
|
||||||
|
await postData(`/drafts/${id}/cancel`, {});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
suppliers: {
|
suppliers: {
|
||||||
list: async (): Promise<Supplier[]> => {
|
list: async (): Promise<Supplier[]> => {
|
||||||
const raw = await getData<RawSupplier[]>(`/suppliers?page_size=${PAGE_SIZE}`);
|
const raw = await getData<RawSupplier[]>(`/suppliers?page_size=${PAGE_SIZE}`);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { ClipboardList, Eye, EyeOff, Loader2, Lock, ShieldCheck, Wrench, X } from "lucide-react";
|
import { ClipboardList, Eye, EyeOff, Loader2, Lock, ShieldCheck, Wrench, X } from "lucide-react";
|
||||||
import { api, apiV26 } from "../../lib/api";
|
import { api, apiV26 } from "../../lib/api";
|
||||||
@@ -33,6 +33,8 @@ export function LoginPage() {
|
|||||||
const setUser = useAuthStore((s) => s.setUser);
|
const setUser = useAuthStore((s) => s.setUser);
|
||||||
const setDevUser = useAuthStore((s) => s.setDevUser);
|
const setDevUser = useAuthStore((s) => s.setDevUser);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [params] = useSearchParams();
|
||||||
|
const returnUrl = params.get("returnUrl");
|
||||||
|
|
||||||
// ---- فرم اصلی (Casdoor) ----
|
// ---- فرم اصلی (Casdoor) ----
|
||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
@@ -83,8 +85,12 @@ export function LoginPage() {
|
|||||||
identifyUser({ id: user.id, name: user.display_name, role: user.role });
|
identifyUser({ id: user.id, name: user.display_name, role: user.role });
|
||||||
track("login_success", { role: user.role });
|
track("login_success", { role: user.role });
|
||||||
toast.success(`خوش آمدید، ${user.display_name}`);
|
toast.success(`خوش آمدید، ${user.display_name}`);
|
||||||
// مدیر پروژه → پنل انبار؛ مدیر تدارکات/ادمین → دفتر پیشنهادهای خرید
|
if (returnUrl) {
|
||||||
navigate(user.role === "pm" ? "/inventory" : "/");
|
navigate(decodeURIComponent(returnUrl));
|
||||||
|
} else {
|
||||||
|
// مدیر پروژه → پنل انبار؛ مدیر تدارکات/ادمین → دفتر پیشنهادهای خرید
|
||||||
|
navigate(user.role === "pm" ? "/inventory" : "/");
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(err instanceof Error ? err.message : "خطا در ورود");
|
toast.error(err instanceof Error ? err.message : "خطا در ورود");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -125,8 +131,9 @@ export function LoginPage() {
|
|||||||
}
|
}
|
||||||
setDevUser(devRole, devRole === "pm" ? devProjectId : null, devRole === "pm" ? devProject?.name || null : null);
|
setDevUser(devRole, devRole === "pm" ? devProjectId : null, devRole === "pm" ? devProject?.name || null : null);
|
||||||
toast.success(`ورود آزمایشی انجام شد — نقش: ${DEV_ROLE_LABELS[devRole]}`);
|
toast.success(`ورود آزمایشی انجام شد — نقش: ${DEV_ROLE_LABELS[devRole]}`);
|
||||||
// ادمین → پنل مدیریت ادمین؛ مدیر تدارکات → دفتر؛ مدیر پروژه → پنل انبار
|
if (returnUrl) {
|
||||||
if (devRole === "admin") navigate("/admin");
|
navigate(decodeURIComponent(returnUrl));
|
||||||
|
} else if (devRole === "admin") navigate("/admin");
|
||||||
else if (devRole === "pm") navigate("/inventory");
|
else if (devRole === "pm") navigate("/inventory");
|
||||||
else navigate("/");
|
else navigate("/");
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { track } from "../../lib/analytics";
|
import { track } from "../../lib/analytics";
|
||||||
import { Link, useNavigate, useSearchParams } from "react-router-dom";
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
import { ChevronRight, ChevronLeft, Check } from "lucide-react";
|
import { ChevronRight, ChevronLeft, Check } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import type { LineItem, Quote } from "../../lib/types";
|
import type { LineItem, Quote } from "../../lib/types";
|
||||||
@@ -82,6 +82,120 @@ export function WizardPage() {
|
|||||||
const [proforma, setProforma] = useState<File | null>(null);
|
const [proforma, setProforma] = useState<File | null>(null);
|
||||||
const [emptyFields, setEmptyFields] = useState<{ label: string; step: number }[]>([]);
|
const [emptyFields, setEmptyFields] = useState<{ label: string; step: number }[]>([]);
|
||||||
const [confirmOpen, setConfirmOpen] = useState(false);
|
const [confirmOpen, setConfirmOpen] = useState(false);
|
||||||
|
const [loadedDraftId, setLoadedDraftId] = useState<string | null>(null);
|
||||||
|
const draftFetchedRef = useRef(false);
|
||||||
|
const lastRemoteUpdatedAtRef = useRef<string | null>(null);
|
||||||
|
const isSelfUpdatingRef = useRef<boolean>(false);
|
||||||
|
const isCommittedOrCancelledRef = useRef<boolean>(false);
|
||||||
|
const initialLoadedRef = useRef<boolean>(false);
|
||||||
|
|
||||||
|
// ---------- تابع اعمال دادههای پیشنویس به فیلدهای ویزارد ----------
|
||||||
|
const applyDraftPayloadToForm = useCallback(
|
||||||
|
(p: Record<string, any>) => {
|
||||||
|
if (!p) return;
|
||||||
|
|
||||||
|
// ۱. تأمینکننده و وضعیت رسمی
|
||||||
|
if (p.supplier_name !== undefined) {
|
||||||
|
setSupplierName(p.supplier_name || "");
|
||||||
|
setSupplierInvalid(false);
|
||||||
|
}
|
||||||
|
if (p.is_official !== undefined) {
|
||||||
|
setIsOfficial(Boolean(p.is_official));
|
||||||
|
}
|
||||||
|
|
||||||
|
// استنتاج تاپیک: ابتدا از اطلاعات تأمینکننده، سپس متریال، سپس اولین تاپیک موجود
|
||||||
|
let topicToSet = "";
|
||||||
|
if (p.supplier_name) {
|
||||||
|
const sup = suppliers.find((s) => normalize(s.name) === normalize(p.supplier_name));
|
||||||
|
if (sup?.topic) topicToSet = sup.topic;
|
||||||
|
}
|
||||||
|
if (!topicToSet && Array.isArray(p.items) && p.items.length > 0) {
|
||||||
|
for (const it of p.items) {
|
||||||
|
if (it.material_name) {
|
||||||
|
const mat = materials.find((m) => normalize(m.name) === normalize(it.material_name));
|
||||||
|
if (mat?.topic) {
|
||||||
|
topicToSet = mat.topic;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!topicToSet && topics.length > 0) {
|
||||||
|
topicToSet = topics[0].name;
|
||||||
|
}
|
||||||
|
if (topicToSet) {
|
||||||
|
setSelectedTopic(topicToSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ۲. اقلام (Step 2)
|
||||||
|
let newItems: ItemDraft[] = [];
|
||||||
|
if (Array.isArray(p.items) && p.items.length > 0) {
|
||||||
|
newItems = p.items.map((it: any, idx: number) => {
|
||||||
|
const qty = it.quantity != null ? Number(it.quantity) : null;
|
||||||
|
const price = it.unit_price != null ? Number(it.unit_price) : null;
|
||||||
|
return {
|
||||||
|
tempId: "it" + idx,
|
||||||
|
cat: it.material_name || "",
|
||||||
|
brand: it.brand_name || "",
|
||||||
|
specs: Array.isArray(it.specs) ? it.specs : [],
|
||||||
|
qty,
|
||||||
|
unit: it.unit_name || "",
|
||||||
|
price,
|
||||||
|
total: qty != null && price != null ? qty * price : null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
setItems(newItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ۳. شرایط پرداخت / termsheet (Step 3)
|
||||||
|
if (Array.isArray(p.termsheets) && p.termsheets.length > 0) {
|
||||||
|
const newCards: TsCardDraft[] = p.termsheets.map((ts: any, idx: number) => ({
|
||||||
|
key: "ts" + idx,
|
||||||
|
milestones:
|
||||||
|
Array.isArray(ts.milestones) && ts.milestones.length > 0
|
||||||
|
? ts.milestones.map((m: any) => ({
|
||||||
|
when_iso: m.due_date || m.when_iso || null,
|
||||||
|
amount: m.amount != null ? Number(m.amount) : null,
|
||||||
|
}))
|
||||||
|
: [{ when_iso: null, amount: null }],
|
||||||
|
}));
|
||||||
|
setCards(newCards);
|
||||||
|
|
||||||
|
const targetIds = (newItems.length > 0 ? newItems : items).map((i) => i.tempId);
|
||||||
|
const newCov: Record<string, string[]> = {};
|
||||||
|
newCards.forEach((c) => {
|
||||||
|
newCov[c.key] = targetIds;
|
||||||
|
});
|
||||||
|
setCoverage(newCov);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ۴. پروژه (Step 4)
|
||||||
|
const projNames: string[] = [];
|
||||||
|
if (Array.isArray(p.project_names)) {
|
||||||
|
projNames.push(...p.project_names.filter(Boolean));
|
||||||
|
}
|
||||||
|
if (p.project_name && !projNames.includes(p.project_name)) {
|
||||||
|
projNames.push(p.project_name);
|
||||||
|
}
|
||||||
|
if (projNames.length > 0) {
|
||||||
|
const newAssignment: Record<string, string[]> = {};
|
||||||
|
const targetItems = newItems.length > 0 ? newItems : items;
|
||||||
|
targetItems.forEach((it) => {
|
||||||
|
newAssignment[it.tempId] = projNames;
|
||||||
|
});
|
||||||
|
setAssignment(newAssignment);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ۵. یادداشت و تاریخ انقضا (Step 5)
|
||||||
|
if (p.notes !== undefined) {
|
||||||
|
setNotes(p.notes || "");
|
||||||
|
}
|
||||||
|
if (p.expiry_date !== undefined) {
|
||||||
|
setExpiryDate(p.expiry_date || null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[suppliers, materials, topics, items],
|
||||||
|
);
|
||||||
|
|
||||||
// ---------- prefill از لینکها (?topic=&supplier=) ----------
|
// ---------- prefill از لینکها (?topic=&supplier=) ----------
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -100,6 +214,122 @@ export function WizardPage() {
|
|||||||
}
|
}
|
||||||
}, [params, suppliers, topics]);
|
}, [params, suppliers, topics]);
|
||||||
|
|
||||||
|
// ---------- بارگذاری اولیه پیشنویس ربات بله (?draft=&draft_id=) ----------
|
||||||
|
useEffect(() => {
|
||||||
|
const draftId = params.get("draft") || params.get("draft_id");
|
||||||
|
if (!draftId || draftFetchedRef.current) return;
|
||||||
|
|
||||||
|
draftFetchedRef.current = true;
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
const draft = await api.drafts.get(draftId);
|
||||||
|
if (!draft || !draft.payload) return;
|
||||||
|
setLoadedDraftId(draft.id);
|
||||||
|
lastRemoteUpdatedAtRef.current = draft.updated_at || null;
|
||||||
|
|
||||||
|
applyDraftPayloadToForm(draft.payload);
|
||||||
|
setHighestVisited(5);
|
||||||
|
initialLoadedRef.current = true;
|
||||||
|
|
||||||
|
toast.success("پیشنویس از ربات بله بارگذاری شد. لطفاً اطلاعات را بازبینی کنید.", {
|
||||||
|
duration: 6000,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(`خطا در فراخوانی پیشنویس از ربات: ${err instanceof Error ? err.message : "خطای ناشناخته"}`);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [params, applyDraftPayloadToForm]);
|
||||||
|
|
||||||
|
// ---------- پولینگ دورهای برای دریافت تغییرات یا ثبت از سمت ربات بله ----------
|
||||||
|
useEffect(() => {
|
||||||
|
const draftId = params.get("draft") || params.get("draft_id");
|
||||||
|
if (!draftId) return;
|
||||||
|
|
||||||
|
const interval = setInterval(async () => {
|
||||||
|
if (isCommittedOrCancelledRef.current || !initialLoadedRef.current) return;
|
||||||
|
try {
|
||||||
|
const draft = await api.drafts.get(draftId);
|
||||||
|
if (!draft) return;
|
||||||
|
|
||||||
|
if (draft.status === "committed") {
|
||||||
|
isCommittedOrCancelledRef.current = true;
|
||||||
|
toast.success("این پیشنهاد از طریق ربات بله با موفقیت ثبت نهایی شد.", { duration: 8000 });
|
||||||
|
navigate("/");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (draft.status === "cancelled") {
|
||||||
|
isCommittedOrCancelledRef.current = true;
|
||||||
|
toast.error("این پیشنویس از طریق ربات بله لغو شد.", { duration: 6000 });
|
||||||
|
navigate("/");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
draft.updated_at &&
|
||||||
|
lastRemoteUpdatedAtRef.current &&
|
||||||
|
draft.updated_at > lastRemoteUpdatedAtRef.current &&
|
||||||
|
!isSelfUpdatingRef.current
|
||||||
|
) {
|
||||||
|
lastRemoteUpdatedAtRef.current = draft.updated_at;
|
||||||
|
applyDraftPayloadToForm(draft.payload);
|
||||||
|
toast.info("🔄 اصلاحات جدید از ربات بله دریافت و در ویزارد اعمال شد.", { duration: 4000 });
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* خطای موقت شبکه در پولینگ نادیده گرفته میشود */
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [params, navigate, applyDraftPayloadToForm]);
|
||||||
|
|
||||||
|
// ---------- همگامسازی خودکار تغییرات ویزارد به پیشنویس ربات (Debounced Auto-Save) ----------
|
||||||
|
useEffect(() => {
|
||||||
|
const draftId = params.get("draft") || params.get("draft_id");
|
||||||
|
if (!draftId || isCommittedOrCancelledRef.current || !initialLoadedRef.current) return;
|
||||||
|
|
||||||
|
const timer = setTimeout(async () => {
|
||||||
|
isSelfUpdatingRef.current = true;
|
||||||
|
try {
|
||||||
|
const payloadToSave: Record<string, any> = {
|
||||||
|
supplier_name: supplierName.trim() || null,
|
||||||
|
is_official: isOfficial,
|
||||||
|
items: items.map((it) => ({
|
||||||
|
material_name: it.cat.trim() || null,
|
||||||
|
brand_name: it.brand.trim() || null,
|
||||||
|
specs: it.specs,
|
||||||
|
quantity: it.qty,
|
||||||
|
unit_name: it.unit.trim() || null,
|
||||||
|
unit_price: it.price,
|
||||||
|
})),
|
||||||
|
termsheets: cards.map((c, i) => ({
|
||||||
|
label: "گزینه " + faNum(i + 1),
|
||||||
|
milestones: (c.milestones || []).map((m) => ({
|
||||||
|
due_date: m.when_iso,
|
||||||
|
amount: m.amount,
|
||||||
|
})),
|
||||||
|
})),
|
||||||
|
project_names: [...new Set(Object.values(assignment).flat())],
|
||||||
|
notes: notes.trim() || null,
|
||||||
|
expiry_date: expiryDate || null,
|
||||||
|
};
|
||||||
|
|
||||||
|
await api.drafts.update(draftId, payloadToSave);
|
||||||
|
const updated = await api.drafts.get(draftId);
|
||||||
|
if (updated?.updated_at) {
|
||||||
|
lastRemoteUpdatedAtRef.current = updated.updated_at;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("Auto-save to draft failed:", err);
|
||||||
|
} finally {
|
||||||
|
setTimeout(() => {
|
||||||
|
isSelfUpdatingRef.current = false;
|
||||||
|
}, 600);
|
||||||
|
}
|
||||||
|
}, 800);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [supplierName, isOfficial, items, cards, assignment, notes, expiryDate, params]);
|
||||||
|
|
||||||
// ---------- اعتبارسنجی قبل از ترک هر مرحله ----------
|
// ---------- اعتبارسنجی قبل از ترک هر مرحله ----------
|
||||||
const completedSteps = useMemo(() => {
|
const completedSteps = useMemo(() => {
|
||||||
const s = new Set<number>();
|
const s = new Set<number>();
|
||||||
@@ -393,6 +623,15 @@ export function WizardPage() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const created = await createQuote.mutateAsync(quote);
|
const created = await createQuote.mutateAsync(quote);
|
||||||
|
const draftId = params.get("draft") || params.get("draft_id");
|
||||||
|
if (draftId) {
|
||||||
|
isCommittedOrCancelledRef.current = true;
|
||||||
|
try {
|
||||||
|
await api.drafts.commit(draftId, created.id);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("Could not commit draft:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (proforma) {
|
if (proforma) {
|
||||||
try {
|
try {
|
||||||
await api.files.upload(created.id, "proforma", proforma);
|
await api.files.upload(created.id, "proforma", proforma);
|
||||||
@@ -402,7 +641,7 @@ export function WizardPage() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
toast.success("پیشنهاد ذخیره شد.");
|
toast.success("پیشنهاد ذخیره شد و در ربات بله تأیید گردید.");
|
||||||
track("quote_submitted", { quote_id: created.id, items: lineItems.length });
|
track("quote_submitted", { quote_id: created.id, items: lineItems.length });
|
||||||
navigate("/");
|
navigate("/");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -411,6 +650,19 @@ export function WizardPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCancel = async () => {
|
||||||
|
const draftId = params.get("draft") || params.get("draft_id");
|
||||||
|
if (draftId) {
|
||||||
|
isCommittedOrCancelledRef.current = true;
|
||||||
|
try {
|
||||||
|
await api.drafts.cancel(draftId);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("Could not cancel draft:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
navigate("/");
|
||||||
|
};
|
||||||
|
|
||||||
const save = () => {
|
const save = () => {
|
||||||
const incomplete: { label: string; step: number }[] = [];
|
const incomplete: { label: string; step: number }[] = [];
|
||||||
if (!completedSteps.has(1)) incomplete.push({ label: "تأمینکننده", step: 1 });
|
if (!completedSteps.has(1)) incomplete.push({ label: "تأمینکننده", step: 1 });
|
||||||
@@ -434,6 +686,17 @@ export function WizardPage() {
|
|||||||
backLabel="بازگشت به دفتر"
|
backLabel="بازگشت به دفتر"
|
||||||
/>
|
/>
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
|
{loadedDraftId && (
|
||||||
|
<div className="mb-4 flex items-center justify-between rounded-xl border border-blue-200 bg-blue-50/80 p-3.5 text-sm text-blue-900 dark:border-blue-900/50 dark:bg-blue-950/40 dark:text-blue-200 shadow-xs">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="inline-flex h-6 px-2.5 items-center justify-center rounded-full bg-blue-600 text-xs font-bold text-white">
|
||||||
|
ربات بله
|
||||||
|
</span>
|
||||||
|
<span>اطلاعات پیشنویس استعلام با هوش مصنوعی بارگذاری شد. لطفاً اقلام و شرایط را بازبینی فرمایید.</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-blue-600 dark:text-blue-400 font-mono font-medium">#{loadedDraftId.slice(0, 8)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Stepper current={currentStep} highestVisited={highestVisited} completedSteps={completedSteps} onGoto={goToStep} />
|
<Stepper current={currentStep} highestVisited={highestVisited} completedSteps={completedSteps} onGoto={goToStep} />
|
||||||
|
|
||||||
{currentStep === 1 && (
|
{currentStep === 1 && (
|
||||||
@@ -524,12 +787,13 @@ export function WizardPage() {
|
|||||||
|
|
||||||
{/* دکمههای اقدام (انصراف + بعدی / ذخیره) */}
|
{/* دکمههای اقدام (انصراف + بعدی / ذخیره) */}
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Link
|
<button
|
||||||
to="/"
|
type="button"
|
||||||
className="inline-flex items-center justify-center px-4 py-2 rounded-xl text-sm font-medium text-slate-500 hover:text-slate-700 hover:bg-slate-100 dark:text-zinc-400 dark:hover:text-zinc-200 dark:hover:bg-zinc-800 transition-colors no-underline"
|
onClick={() => void handleCancel()}
|
||||||
|
className="inline-flex items-center justify-center px-4 py-2 rounded-xl text-sm font-medium text-slate-500 hover:text-slate-700 hover:bg-slate-100 dark:text-zinc-400 dark:hover:text-zinc-200 dark:hover:bg-zinc-800 transition-colors cursor-pointer border-none bg-transparent"
|
||||||
>
|
>
|
||||||
انصراف
|
انصراف
|
||||||
</Link>
|
</button>
|
||||||
|
|
||||||
{/* دکمه مرحله بعدی (فلش رو به چپ = پیشروی در فارسی) */}
|
{/* دکمه مرحله بعدی (فلش رو به چپ = پیشروی در فارسی) */}
|
||||||
{currentStep < TOTAL_STEPS && (
|
{currentStep < TOTAL_STEPS && (
|
||||||
|
|||||||
Reference in New Issue
Block a user