fix(wizard): optimize stepper and line items layout for mobile

This commit is contained in:
Behrad
2026-09-21 19:39:12 +03:30
parent 9386d4846f
commit b176f0f5ff
3 changed files with 239 additions and 187 deletions
+109 -59
View File
@@ -1,4 +1,5 @@
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { Trash2, Plus } from "lucide-react";
import type { Material } from "../../lib/types"; import type { Material } from "../../lib/types";
import { normalize } from "../../lib/normalize"; import { normalize } from "../../lib/normalize";
import { useMaterials } from "../../hooks"; import { useMaterials } from "../../hooks";
@@ -7,6 +8,7 @@ import { Combobox } from "../../components/Combobox";
import { NumberInput } from "../../components/NumberInput"; import { NumberInput } from "../../components/NumberInput";
import { MultiSelectCombobox } from "../../components/MultiSelectCombobox"; import { MultiSelectCombobox } from "../../components/MultiSelectCombobox";
import { api, apiV26 } from "../../lib/api"; import { api, apiV26 } from "../../lib/api";
import { faNum } from "../../lib/formatters";
export interface ItemDraft { export interface ItemDraft {
tempId: string; tempId: string;
@@ -51,7 +53,6 @@ export function materialOptions(
apiBrands: string[] = [], apiBrands: string[] = [],
apiSpecs: string[] = [], apiSpecs: string[] = [],
): string[] { ): string[] {
// متریال‌ها سراسری هستند — مستقل از تاپیک
let list = materials; let list = materials;
const cat = item.cat.trim(); const cat = item.cat.trim();
@@ -67,21 +68,17 @@ export function materialOptions(
}; };
if (field === "brand") { if (field === "brand") {
// برند سراسری است — مستقل از تاپیک و متریال
const allBrands = new Set(materials.map((m) => m.brand).filter(Boolean)); const allBrands = new Set(materials.map((m) => m.brand).filter(Boolean));
apiBrands.forEach((b) => b && allBrands.add(b)); apiBrands.forEach((b) => b && allBrands.add(b));
fromRows((it) => it.brand).forEach((b) => allBrands.add(b)); fromRows((it) => it.brand).forEach((b) => allBrands.add(b));
return [...allBrands].sort((a, b) => a.localeCompare(b, "fa")); return [...allBrands].sort((a, b) => a.localeCompare(b, "fa"));
} }
// فیلدِ اولِ هر ردیف «نام متریال» است (نه دسته) — مشخصه‌های متریالِ
// هم‌نام پیشنهاد می‌شوند.
if (cat) if (cat)
list = list.filter( list = list.filter(
(m) => normalize(materialBaseName(m.name, m.brand, m.specs)) === normalize(cat), (m) => normalize(materialBaseName(m.name, m.brand, m.specs)) === normalize(cat),
); );
// مشخصه‌ها سراسری هستند: رجیستری attribute-definitions + مشخصه‌های همه‌ی
// متریال‌ها + مشخصه‌های تایپ‌شده در سایر ردیف‌ها. به تاپیک/دسته/برندِ قلمِ
// فعلی گره نمی‌خورند تا هنگام افزودن پیشنهاد جدید همیشه در دسترس باشند.
const base = new Set<string>(); const base = new Set<string>();
apiSpecs.forEach((s) => s && base.add(s)); apiSpecs.forEach((s) => s && base.add(s));
materials.forEach((m) => (m.specs || []).forEach((s) => s && base.add(s))); materials.forEach((m) => (m.specs || []).forEach((s) => s && base.add(s)));
@@ -90,7 +87,7 @@ export function materialOptions(
return [...base].sort((a, b) => a.localeCompare(b, "fa")); return [...base].sort((a, b) => a.localeCompare(b, "fa"));
} }
// مرحله ۲ — جدول پویای اقلام سفارش // مرحله ۲ — مدیریت اقلام سفارش
export function Step2LineItems({ items, onItemsChange, selectedTopic, onRemember }: Step2Props) { export function Step2LineItems({ items, onItemsChange, selectedTopic, onRemember }: Step2Props) {
const materials = useMaterials(); const materials = useMaterials();
@@ -167,46 +164,83 @@ export function Step2LineItems({ items, onItemsChange, selectedTopic, onRemember
if (!it.specs.length) return; if (!it.specs.length) return;
rec = { topic: selectedTopic, category: it.cat.trim(), brand: it.brand.trim(), specs: it.specs }; rec = { topic: selectedTopic, category: it.cat.trim(), brand: it.brand.trim(), specs: it.specs };
} }
// rec.category در واقع نامِ متریال است (فیلدِ اولِ ردیف) — تطبیق بر اساس
// همان هویتِ متریال انجام می‌شود، نه دسته.
const dup = !!matchMaterial(materials, rec.category, rec.brand, rec.specs); const dup = !!matchMaterial(materials, rec.category, rec.brand, rec.specs);
if (dup) return; if (dup) return;
onRemember(rec); onRemember(rec);
}; };
return ( return (
<div> <div className="space-y-4">
<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-blue-600 text-[12px] font-bold text-white">۲</span> <div className="space-y-1">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-blue-600 text-xs font-bold text-white shadow-xs">
۲
</span>
<h3 className="text-sm sm:text-base font-bold text-gray-900">
اقلام سفارش اقلام سفارش
</h3> </h3>
<div className="mb-3 text-[12px] text-gray-500" dangerouslySetInnerHTML={{ __html: 'هر سطر یک <b>قلم</b> است (متریال + برند + مشخصه + واحد + تعداد + قیمت). متریال، برند و مشخصه را می‌توان از گزینه‌ها انتخاب کرد یا تایپ کرد تا به‌عنوان گزینه‌ی جدید اضافه شود. شرایط پرداخت هر قلم را در مرحله‌ی بعد مشخص می‌کنید.' }} /> </div>
<span className="rounded-full bg-blue-50 px-2.5 py-0.5 text-[11px] font-semibold text-blue-700 border border-blue-100">
<div className="item-row wizard-item-row item-row-head mb-1 pe-7"> {faNum(items.length)} قلم
<span>متریال</span> </span>
<span>برند</span> </div>
<span>مشخصه</span> <p className="text-[11.5px] sm:text-xs text-gray-500 leading-relaxed">
<span>تعداد</span> مشخصات، تعداد و قیمت هر قلم را ثبت کنید. گزینه‌های جدید با تایپ ذخیره می‌شوند.
<span>واحد</span> </p>
<span>قیمت واحد</span>
<span>قیمت کل</span>
</div> </div>
<div data-field="line_items" className="flex flex-col gap-2"> {/* لیست کارت‌های اقلام */}
{items.map((it) => ( <div data-field="line_items" className="flex flex-col gap-3">
<div key={it.tempId} className="item-row wizard-item-row items-center"> {items.map((it, idx) => (
<div
key={it.tempId}
className="rounded-xl border border-gray-200 bg-white p-3.5 sm:p-4 shadow-xs space-y-3 transition-all"
>
{/* هدر قلم با شماره و نام و دکمه حذف */}
<div className="flex items-center justify-between border-b border-gray-100 pb-2">
<div className="flex items-center gap-1.5 min-w-0">
<span className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-blue-50 text-[11px] font-bold text-blue-600">
{faNum(idx + 1)}
</span>
<span className="text-xs font-bold text-gray-900 truncate">
{it.cat || "قلم جدید"} {it.brand ? `• ${it.brand}` : ""}
</span>
</div>
{items.length > 1 && (
<button
type="button"
className="inline-flex items-center gap-1 rounded-md px-2 py-0.5 text-xs text-red-600 hover:bg-red-50 transition-colors"
title="حذف قلم"
onClick={() => remove(it.tempId)}
>
<Trash2 className="h-3.5 w-3.5" />
<span className="text-[11px]">حذف</span>
</button>
)}
</div>
{/* ورودی‌های متریال و برند */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2.5">
<div className="flex flex-col gap-1">
<label className="text-[11px] font-semibold text-gray-600">متریال</label>
<Combobox <Combobox
value={it.cat} value={it.cat}
onChange={(x) => patch(it.tempId, { cat: x.label })} onChange={(x) => patch(it.tempId, { cat: x.label })}
items={allMaterialNames} items={allMaterialNames}
allowAdd allowAdd
addSuffix="به عنوان متریال جدید" addSuffix="به عنوان متریال جدید"
placeholder="متریال" placeholder="نام متریال"
onInputChange={(v) => patch(it.tempId, { cat: v })} onInputChange={(v) => patch(it.tempId, { cat: v })}
onBlur={() => rememberField("i-cat", it)} onBlur={() => rememberField("i-cat", it)}
inputClassName="!text-[12px] !h-9" inputClassName="!text-xs !h-9"
className="min-w-0" className="min-w-0"
/> />
</div>
<div className="flex flex-col gap-1">
<label className="text-[11px] font-semibold text-gray-600">برند</label>
<Combobox <Combobox
value={it.brand} value={it.brand}
onChange={(x) => patch(it.tempId, { brand: x.label })} onChange={(x) => patch(it.tempId, { brand: x.label })}
@@ -216,9 +250,16 @@ export function Step2LineItems({ items, onItemsChange, selectedTopic, onRemember
placeholder="برند" placeholder="برند"
onInputChange={(v) => patch(it.tempId, { brand: v })} onInputChange={(v) => patch(it.tempId, { brand: v })}
onBlur={() => rememberField("i-brand", it)} onBlur={() => rememberField("i-brand", it)}
inputClassName="!text-[12px] !h-9" inputClassName="!text-xs !h-9"
className="min-w-0" className="min-w-0"
/> />
</div>
</div>
{/* ورودی‌های مشخصه و واحد */}
<div className="grid grid-cols-2 gap-2.5">
<div className="flex flex-col gap-1">
<label className="text-[11px] font-semibold text-gray-600">مشخصه</label>
<MultiSelectCombobox <MultiSelectCombobox
values={it.specs} values={it.specs}
onChange={(specs) => patch(it.tempId, { specs })} onChange={(specs) => patch(it.tempId, { specs })}
@@ -226,24 +267,17 @@ export function Step2LineItems({ items, onItemsChange, selectedTopic, onRemember
placeholder="مشخصه" placeholder="مشخصه"
className="min-w-0" className="min-w-0"
/> />
<NumberInput </div>
value={it.qty}
onChange={(qty) => { <div className="flex flex-col gap-1">
patch(it.tempId, { <label className="text-[11px] font-semibold text-gray-600">واحد</label>
qty,
total: qty != null && it.price != null ? Math.round(qty * it.price * 100) / 100 : it.total,
});
}}
placeholder="تعداد"
showWords
/>
<Combobox <Combobox
value={it.unit} value={it.unit}
onChange={(x) => patch(it.tempId, { unit: x.label })} onChange={(x) => patch(it.tempId, { unit: x.label })}
items={allUnits.filter((u) => normalize(u) !== normalize(it.unit))} items={allUnits.filter((u) => normalize(u) !== normalize(it.unit))}
allowAdd allowAdd
addSuffix="به عنوان واحد جدید" addSuffix="به عنوان واحد جدید"
placeholder="واحد" placeholder="واحد سنجش"
onInputChange={(v) => patch(it.tempId, { unit: v })} onInputChange={(v) => patch(it.tempId, { unit: v })}
onAdd={(v) => { onAdd={(v) => {
const name = v.trim(); const name = v.trim();
@@ -257,9 +291,31 @@ export function Step2LineItems({ items, onItemsChange, selectedTopic, onRemember
}) })
.catch(() => {}); .catch(() => {});
}} }}
inputClassName="!text-[12px] !h-9" inputClassName="!text-xs !h-9"
className="min-w-0" className="min-w-0"
/> />
</div>
</div>
{/* جعبه مالی و مقادیر (تعداد، قیمت واحد، قیمت کل) */}
<div className="grid grid-cols-3 gap-2 rounded-lg bg-gray-50 p-2.5 border border-gray-200">
<div className="flex flex-col gap-1">
<label className="text-[10.5px] font-semibold text-gray-600">تعداد</label>
<NumberInput
value={it.qty}
onChange={(qty) => {
patch(it.tempId, {
qty,
total: qty != null && it.price != null ? Math.round(qty * it.price * 100) / 100 : it.total,
});
}}
placeholder="تعداد"
showWords
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-[10.5px] font-semibold text-gray-600">قیمت واحد</label>
<NumberInput <NumberInput
value={it.price} value={it.price}
onChange={(price) => { onChange={(price) => {
@@ -268,10 +324,14 @@ export function Step2LineItems({ items, onItemsChange, selectedTopic, onRemember
total: it.qty != null && price != null ? Math.round(it.qty * price * 100) / 100 : it.total, total: it.qty != null && price != null ? Math.round(it.qty * price * 100) / 100 : it.total,
}); });
}} }}
placeholder="قیمت واحد" placeholder="واحد"
showWords showWords
maxDecimals={2} maxDecimals={2}
/> />
</div>
<div className="flex flex-col gap-1">
<label className="text-[10.5px] font-semibold text-gray-600">قیمت کل</label>
<NumberInput <NumberInput
value={it.total} value={it.total}
onChange={(total) => { onChange={(total) => {
@@ -281,32 +341,22 @@ export function Step2LineItems({ items, onItemsChange, selectedTopic, onRemember
total != null && it.qty != null && it.qty > 0 ? Math.round((total / it.qty) * 100) / 100 : it.price, total != null && it.qty != null && it.qty > 0 ? Math.round((total / it.qty) * 100) / 100 : it.price,
}); });
}} }}
placeholder="قیمت کل" placeholder="جمع"
showWords showWords
/> />
</div>
{/* دکمه حذف مدرن با آیکون سطل زباله */} </div>
<button
type="button"
className="flex h-8 w-8 items-center justify-center rounded-lg text-gray-400 hover:bg-red-50 hover:text-red-500 transition-colors"
title="حذف قلم"
onClick={() => remove(it.tempId)}
>
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div> </div>
))} ))}
</div> </div>
{/* دکمه افزودن قلم با استایل برجسته و خط‌چین */} {/* دکمه افزودن قلم جدید */}
<button <button
type="button" type="button"
className="mt-3 flex w-full items-center justify-center gap-2 rounded-xl border-2 border-dashed border-blue-300 bg-blue-50/40 py-2.5 px-4 text-[13px] font-semibold text-blue-600 hover:border-blue-500 hover:bg-blue-100/70 transition-all duration-150" className="flex w-full items-center justify-center gap-1.5 rounded-xl border border-dashed border-gray-300 bg-gray-50/50 py-2.5 text-xs font-semibold text-gray-700 hover:border-blue-500 hover:text-blue-600 hover:bg-blue-50/50 active:scale-[0.99] transition-all"
onClick={add} onClick={add}
> >
<span className="text-base font-bold leading-none">+</span> <Plus className="h-4 w-4" />
<span>افزودن قلم جدید</span> <span>افزودن قلم جدید</span>
</button> </button>
</div> </div>
+32 -14
View File
@@ -9,40 +9,58 @@ export interface StepperProps {
onGoto: (step: number) => void; onGoto: (step: number) => void;
} }
const STEPS = ["تأمین‌کننده", "اقلام", "شرایط پرداخت", "پروژه", "بازبینی و ثبت"]; const STEPS = [
{ id: 1, label: "تأمین‌کننده" },
{ id: 2, label: "اقلام" },
{ id: 3, label: "شرایط پرداخت" },
{ id: 4, label: "پروژه" },
{ id: 5, label: "بازبینی و ثبت" },
];
// نوار ۵ مرحله‌ای ویزارد — کلیک روی مراحل بازدیدشده مجاز است // نوار ۵ مرحله‌ای ویزارد — واکنش‌گرا و ضدشکستگی در موبایل
// مراحل تکمیل‌شده سبز، مرحله‌ی فعلی آبی
export function Stepper({ current, completedSteps, onGoto }: StepperProps) { export function Stepper({ current, completedSteps, onGoto }: StepperProps) {
return ( return (
<div className="mb-4 flex flex-wrap gap-2 rounded-xl border border-line bg-panel p-2 shadow-sm"> <div className="mb-4 flex items-center justify-between gap-1 sm:gap-2 rounded-xl border border-line bg-panel p-1.5 sm:p-2 shadow-xs select-none">
{STEPS.map((label, i) => { {STEPS.map(({ id: step, label }) => {
const step = i + 1;
const isCurrent = step === current; const isCurrent = step === current;
const isComplete = completedSteps.has(step); const isComplete = completedSteps.has(step);
return ( return (
<button <button
key={step} key={step}
type="button" type="button"
className={cn( className={cn(
"flex flex-1 cursor-pointer items-center justify-center gap-2 rounded-lg px-3 py-2 text-[12.5px] transition-colors", "flex items-center justify-center gap-1.5 rounded-lg py-1.5 px-2 sm:px-3 text-xs transition-all duration-150 cursor-pointer",
// در موبایل مرحله جاری فضا می‌گیرد و بقیه جمع‌وجور می‌مانند تا سطر نشکند
isCurrent isCurrent
? "bg-[#2563eb] font-bold text-white" ? "flex-1 sm:flex-1 bg-blue-600 font-bold text-white shadow-xs"
: isComplete : "flex-initial sm:flex-1 text-muted hover:text-foreground hover:bg-slate-50 dark:hover:bg-zinc-800/60",
? "bg-green-bg font-medium text-green" isComplete && !isCurrent && "font-medium text-emerald-600 dark:text-emerald-400",
: "hover:bg-slate-bg",
)} )}
onClick={() => onGoto(step)} onClick={() => onGoto(step)}
> >
<span <span
className={cn( className={cn(
"flex h-5 w-5 items-center justify-center rounded-full text-[11px] font-bold", "flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-[11px] font-bold transition-colors",
isCurrent ? "bg-white/25" : isComplete ? "bg-green-bg text-green" : "bg-slate-bg text-muted", isCurrent
? "bg-white/20 text-white"
: isComplete
? "bg-emerald-100 text-emerald-700 dark:bg-emerald-950/80 dark:text-emerald-300"
: "bg-muted/15 text-muted",
)} )}
> >
{isComplete ? <Check className="h-3 w-3" /> : faNum(step)} {isComplete ? <Check className="h-3 w-3 stroke-[2.5]" /> : faNum(step)}
</span> </span>
{/* در موبایل فقط نام مرحله جاری باز است؛ در صفحه بزرگتر همه نام‌ها نمایش دارند */}
<span
className={cn(
"whitespace-nowrap truncate text-[11.5px] sm:text-xs",
!isCurrent && "hidden md:inline",
)}
>
{label} {label}
</span>
</button> </button>
); );
})} })}
+22 -38
View File
@@ -127,11 +127,9 @@ export function WizardPage() {
goToStep(currentStep + 1); goToStep(currentStep + 1);
}; };
const rememberMaterial = (_rec: MaterialRec) => { const rememberMaterial = (_rec: MaterialRec) => {};
// رکوردهای متریال به همراه واحد و مشخصه‌ها در زمان ذخیره نهایی ساخته می‌شوند
};
// ---------- پروژه‌ها: از مجموعه + نام‌های قدیمی quoteها ---------- // ---------- پروژه‌ها ----------
const projectNames = useMemo(() => { const projectNames = useMemo(() => {
const names = new Set<string>(projects.map((p) => p.name)); const names = new Set<string>(projects.map((p) => p.name));
quotes.forEach((q) => { quotes.forEach((q) => {
@@ -159,7 +157,7 @@ export function WizardPage() {
}); });
}; };
// ---------- خلاصه‌ی مرحله ۵ ---------- // ---------- خلاصه مرحله ۵ ----------
const review = useMemo(() => { const review = useMemo(() => {
let grandTotal = 0; let grandTotal = 0;
items.forEach((it) => { items.forEach((it) => {
@@ -202,23 +200,14 @@ export function WizardPage() {
const combined = `${field} ${msg}`.toLowerCase(); const combined = `${field} ${msg}`.toLowerCase();
// ۱. تأمین‌کننده
if (/supplier|تأمین|تامین/i.test(combined)) return { step: 1, field: "supplier_id" }; if (/supplier|تأمین|تامین/i.test(combined)) return { step: 1, field: "supplier_id" };
// ۲. اقلام / متریال / واحد / تعداد / قیمت
if (/line_item|material|canonical_unit|unit_id|quantity|متریال|واحد|قلم|تعداد/i.test(combined)) { if (/line_item|material|canonical_unit|unit_id|quantity|متریال|واحد|قلم|تعداد/i.test(combined)) {
return { step: 2, field: field || "line_items" }; return { step: 2, field: field || "line_items" };
} }
// ۳. شرایط پرداخت / termsheet / مراحل پرداخت / سررسید
if (/termsheet|item_termsheet|schedule_step|due_date|milestone|ترم|پرداخت|پوشش|سررسید/i.test(combined)) { if (/termsheet|item_termsheet|schedule_step|due_date|milestone|ترم|پرداخت|پوشش|سررسید/i.test(combined)) {
return { step: 3, field: field || "termsheets" }; return { step: 3, field: field || "termsheets" };
} }
// ۴. پروژه
if (/project|پروژه/i.test(combined)) return { step: 4, field: "project" }; if (/project|پروژه/i.test(combined)) return { step: 4, field: "project" };
// ۵. تاریخ‌ها، یادداشت، پیش‌فاکتور
if (/expiry_date|تاریخ انقضا/i.test(combined)) return { step: 5, field: "expiry_date" }; 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 (/quote_date|تاریخ پیشنهاد/i.test(combined)) return { step: 5, field: "quote_date" };
if (/proforma|پیش‌فاکتور/i.test(combined)) return { step: 5, field: "proforma" }; if (/proforma|پیش‌فاکتور/i.test(combined)) return { step: 5, field: "proforma" };
@@ -291,10 +280,6 @@ export function WizardPage() {
if (!cat && !brand && qty == null && price == null) continue; if (!cat && !brand && qty == null && price == null) continue;
const wantName = (cat || brand || specs.join(" ") || "").trim() || "کالای جدید"; const wantName = (cat || brand || specs.join(" ") || "").trim() || "کالای جدید";
// هویت متریال = نام + برند + مشخصه. پیش از v2.7 دسته هم در تطبیق بود؛
// دسته دیگر روی متریال نیست و تکیه بر نام جلوی اشتباه‌گرفتن دو متریال با
// برند و مشخصه‌ی یکسان را می‌گیرد (matchMaterial نامِ پایه را هم می‌بیند
// تا رکوردهای قدیمی با نامِ «برند + دسته + مشخصه» پیدا شوند).
let material = materials.find((m) => looksLikeUuid(m.id) && normalize(m.name) === normalize(wantName)); let material = materials.find((m) => looksLikeUuid(m.id) && normalize(m.name) === normalize(wantName));
if (!material) { if (!material) {
const matched = matchMaterial( const matched = matchMaterial(
@@ -426,7 +411,7 @@ export function WizardPage() {
}; };
return ( return (
<div className="min-h-screen"> <div className="min-h-screen pb-10">
<Header <Header
title="افزودن پیشنهاد جدید" title="افزودن پیشنهاد جدید"
sub="اطلاعات را از تماس با تأمین‌کننده وارد کنید" sub="اطلاعات را از تماس با تأمین‌کننده وارد کنید"
@@ -434,10 +419,12 @@ export function WizardPage() {
backLabel="بازگشت به دفتر" backLabel="بازگشت به دفتر"
/> />
<PageLayout> <PageLayout>
{/* نوار مراحل استپر */}
<Stepper current={currentStep} highestVisited={highestVisited} completedSteps={completedSteps} onGoto={goToStep} /> <Stepper current={currentStep} highestVisited={highestVisited} completedSteps={completedSteps} onGoto={goToStep} />
{/* محتوای مراحل با پدینگ ریسپانسیو و بهینه برای موبایل */}
{currentStep === 1 && ( {currentStep === 1 && (
<div data-step="1" className="rounded-xl border border-line bg-panel p-5 shadow-sm"> <div data-step="1" className="rounded-xl border border-line bg-panel p-3.5 sm:p-5 shadow-xs">
<Step1TopicSupplier <Step1TopicSupplier
selectedTopic={selectedTopic} selectedTopic={selectedTopic}
onTopicSelect={(t) => { onTopicSelect={(t) => {
@@ -461,12 +448,12 @@ export function WizardPage() {
</div> </div>
)} )}
{currentStep === 2 && ( {currentStep === 2 && (
<div data-step="2" className="rounded-xl border border-line bg-panel p-5 shadow-sm"> <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} /> <Step2LineItems items={items} onItemsChange={setItems} selectedTopic={selectedTopic} onRemember={rememberMaterial} />
</div> </div>
)} )}
{currentStep === 3 && ( {currentStep === 3 && (
<div data-step="3" className="rounded-xl border border-line bg-panel p-5 shadow-sm"> <div data-step="3" className="rounded-xl border border-line bg-panel p-3.5 sm:p-5 shadow-xs">
<Step3Termsheets <Step3Termsheets
cards={cards} cards={cards}
onCardsChange={setCards} onCardsChange={setCards}
@@ -477,7 +464,7 @@ export function WizardPage() {
</div> </div>
)} )}
{currentStep === 4 && ( {currentStep === 4 && (
<div data-step="4" className="rounded-xl border border-line bg-panel p-5 shadow-sm"> <div data-step="4" className="rounded-xl border border-line bg-panel p-3.5 sm:p-5 shadow-xs">
<Step4Projects <Step4Projects
items={items} items={items}
projects={projectNames} projects={projectNames}
@@ -491,7 +478,7 @@ export function WizardPage() {
</div> </div>
)} )}
{currentStep === 5 && ( {currentStep === 5 && (
<div data-step="5" className="rounded-xl border border-line bg-panel p-5 shadow-sm"> <div data-step="5" className="rounded-xl border border-line bg-panel p-3.5 sm:p-5 shadow-xs">
<Step5Review <Step5Review
quoteDate={quoteDate} quoteDate={quoteDate}
onQuoteDate={setQuoteDate} onQuoteDate={setQuoteDate}
@@ -506,15 +493,14 @@ export function WizardPage() {
</div> </div>
)} )}
{/* پاورقی ویزارد — با آیکون‌های استاندارد و جهت‌گیری دقیق RTL */} {/* پاورقی کنترل مراحل */}
<div className="mt-5 flex items-center justify-between rounded-xl border border-line bg-panel p-3.5 shadow-sm"> <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> <div>
{currentStep > 1 && ( {currentStep > 1 && (
<button <button
type="button" type="button"
onClick={() => goToStep(currentStep - 1)} onClick={() => goToStep(currentStep - 1)}
className="inline-flex items-center gap-1.5 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 dark:hover:bg-zinc-700 text-sm font-medium transition-colors shadow-xs cursor-pointer" 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" /> <ChevronRight className="h-4 w-4" />
<span>قبلی</span> <span>قبلی</span>
@@ -522,21 +508,19 @@ export function WizardPage() {
)} )}
</div> </div>
{/* دکمه‌های اقدام (انصراف + بعدی / ذخیره) */} <div className="flex items-center gap-2 sm:gap-3">
<div className="flex items-center gap-3">
<Link <Link
to="/" to="/"
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" 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> </Link>
{/* دکمه مرحله بعدی (فلش رو به چپ = پیشروی در فارسی) */}
{currentStep < TOTAL_STEPS && ( {currentStep < TOTAL_STEPS && (
<button <button
type="button" type="button"
onClick={next} onClick={next}
className="inline-flex items-center gap-1.5 px-6 py-2 rounded-xl bg-blue-600 hover:bg-blue-700 active:bg-blue-800 text-white text-sm font-semibold shadow-sm hover:shadow transition-all duration-150 cursor-pointer" 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> <span>بعدی</span>
<ChevronLeft className="h-4 w-4" /> <ChevronLeft className="h-4 w-4" />
@@ -547,7 +531,7 @@ export function WizardPage() {
<button <button
type="button" type="button"
onClick={() => void save()} onClick={() => void save()}
className="inline-flex items-center gap-1.5 px-6 py-2 rounded-xl bg-emerald-600 hover:bg-emerald-700 active:bg-emerald-800 text-white text-sm font-semibold shadow-sm hover:shadow transition-all duration-150 cursor-pointer" 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> <span>ذخیره پیشنهاد</span>
<Check className="h-4 w-4" /> <Check className="h-4 w-4" />
@@ -556,21 +540,21 @@ export function WizardPage() {
</div> </div>
</div> </div>
{/* دیالوگ ذخیره با فیلدهای خالی */} {/* دیالوگ فیلدهای خالی */}
<Dialog open={confirmOpen} onOpenChange={setConfirmOpen}> <Dialog open={confirmOpen} onOpenChange={setConfirmOpen}>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>فیلدهای خالی</DialogTitle> <DialogTitle>فیلدهای خالی</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="text-[13.5px] leading-7"> <div className="text-[13.5px] leading-7 text-gray-700">
هنوز تعدادی از فیلد ها خالی هستند. آیا مطمئنی که می خواهید تغییرات را ذخیره کنید؟ هنوز تعدادی از فیلدها خالی هستند. آیا مطمئنید که می‌خواهید ادامه دهید؟
</div> </div>
<div className="flex flex-wrap gap-x-3 gap-y-1 text-[13px]"> <div className="flex flex-wrap gap-x-3 gap-y-1 text-[13px]">
{emptyFields.map((f) => ( {emptyFields.map((f) => (
<button <button
key={f.step} key={f.step}
type="button" type="button"
className="cursor-pointer text-accent underline underline-offset-2 hover:text-accent-dark" className="cursor-pointer text-blue-600 underline underline-offset-2 hover:text-blue-800"
onClick={() => { onClick={() => {
setConfirmOpen(false); setConfirmOpen(false);
goToStep(f.step); goToStep(f.step);