diff --git a/frontend/src/pages/Dashboard/AcceptModal.tsx b/frontend/src/pages/Dashboard/AcceptModal.tsx index 6d499d1..5072acf 100644 --- a/frontend/src/pages/Dashboard/AcceptModal.tsx +++ b/frontend/src/pages/Dashboard/AcceptModal.tsx @@ -25,11 +25,132 @@ import { FileList } from "../../components/FileList"; import { JalaliDatePicker } from "../../components/JalaliDatePicker"; import { Building2, Plus, Trash2, CheckCircle2, AlertCircle } from "lucide-react"; +// تبدیل عدد به حروف فارسی جهت نمایش زیر باکس مقدار +const ONES = ["", "یک", "دو", "سه", "چهار", "پنج", "شش", "هفت", "هشت", "نه"]; +const TEENS = [ + "ده", + "یازده", + "دوازده", + "سیزده", + "چهارده", + "پانزده", + "شانزده", + "هفده", + "هجده", + "نوزده", +]; +const TENS = [ + "", + "", + "بیست", + "سی", + "چهل", + "پنجاه", + "شصت", + "هفتاد", + "هشتاد", + "نود", +]; +const HUNDREDS = [ + "", + "صد", + "دویست", + "سیصد", + "چهارصد", + "پانصد", + "ششصد", + "هفتصد", + "هشتصد", + "نهصد", +]; +const SCALES = ["", "هزار", "میلیون", "میلیارد", "تریلیون"]; +const DECIMAL_SCALES: Record = { + 1: "دهم", + 2: "صدم", + 3: "هزارم", + 4: "ده‌هزارم", +}; + +function convertThreeDigits(n: number): string { + if (n === 0) return ""; + const parts: string[] = []; + const h = Math.floor(n / 100); + const rem = n % 100; + if (h > 0) parts.push(HUNDREDS[h]); + if (rem >= 10 && rem <= 19) { + parts.push(TEENS[rem - 10]); + } else { + const t = Math.floor(rem / 10); + const u = rem % 10; + if (t > 0) parts.push(TENS[t]); + if (u > 0) parts.push(ONES[u]); + } + return parts.join(" و "); +} + +function toEnglishDigits(str: string): string { + return str + .replace(/[۰-۹]/g, (d) => String(d.charCodeAt(0) - 1776)) + .replace(/[٠-٩]/g, (d) => String(d.charCodeAt(0) - 1632)); +} + +function numberToPersianWords(val: number | string | null | undefined): string { + if (val == null || val === "") return ""; + const numStr = toEnglishDigits(String(val).trim()); + const num = parseFloat(numStr); + if (isNaN(num)) return ""; + if (num === 0) return "صفر"; + + const isNeg = num < 0; + const parts = Math.abs(num).toString().split("."); + const intStr = parts[0]; + const decStr = parts[1]; + + let intNum = parseInt(intStr, 10); + let intResult = ""; + + if (intNum === 0) { + intResult = "صفر"; + } else { + const chunks: number[] = []; + while (intNum > 0) { + chunks.push(intNum % 1000); + intNum = Math.floor(intNum / 1000); + } + const chunkWords: string[] = []; + for (let i = chunks.length - 1; i >= 0; i--) { + const c = chunks[i]; + if (c > 0) { + const w = convertThreeDigits(c); + const s = SCALES[i]; + chunkWords.push(s ? `${w} ${s}` : w); + } + } + intResult = chunkWords.join(" و "); + } + + if (decStr && parseInt(decStr, 10) > 0) { + const cleanDecStr = decStr.slice(0, 4); + const decNum = parseInt(cleanDecStr, 10); + const decWord = numberToPersianWords(decNum); + const scale = DECIMAL_SCALES[cleanDecStr.length]; + if (scale) { + if (intNum === 0) { + return `${isNeg ? "منفی " : ""}${decWord} ${scale}`; + } + return `${isNeg ? "منفی " : ""}${intResult} و ${decWord} ${scale}`; + } + return `${isNeg ? "منفی " : ""}${intResult} ممیز ${decWord}`; + } + + return isNeg ? `منفی ${intResult}` : intResult; +} + interface CardDraft { domId: string; isNew: boolean; milestones: Milestone[]; - label: string | null; // null = پیش‌فرض «گزینه N» + label: string | null; } export interface AcceptModalProps { @@ -222,6 +343,7 @@ export function AcceptModal({ quoteId, onClose }: AcceptModalProps) { next.line_items.forEach((li) => { if (removedItems.has(li.id)) li.rejected = true; }); + const active = activeItems(next); if (!active.length) { toast.error("همه‌ی قلم‌ها رد شده‌اند؛ حداقل یک قلم لازم است."); @@ -266,7 +388,6 @@ export function AcceptModal({ quoteId, onClose }: AcceptModalProps) { if (t.status === "accepted" && !usedIds.has(t.id)) t.status = "offered"; }); - // اعتبارسنجی تخصیص‌های چندپروژه‌ای به ازای هر قلم (الزامات ۳ و ۵) for (const li of active) { const allocs = itemAllocations[li.id] || []; const m = materialById(materials, li.material_id); @@ -290,14 +411,12 @@ export function AcceptModal({ quoteId, onClose }: AcceptModalProps) { } } - // الزام ۳: جلوگیری از انتخاب پروژه تکراری برای یک قلم const pNames = allocs.map((a) => a.project_name.trim()); if (new Set(pNames).size !== pNames.length) { toast.error(`برای قلم «${title}»، پروژهٔ تکراری در ردیف‌های تخصیص انتخاب شده است.`); return; } - // الزام ۵: مقایسه با دقت اعشاری بالا const sumQty = allocs.reduce((sum, a) => sum + (parseFloat(String(a.quantity)) || 0), 0); const diff = Math.abs(sumQty - targetQty); if (diff > 0.0001) { @@ -308,7 +427,6 @@ export function AcceptModal({ quoteId, onClose }: AcceptModalProps) { } } - // انتساب تخصیص‌های چندپروژه‌ای به اقلام next.line_items.forEach((li) => { if (li.rejected) return; const allocs = itemAllocations[li.id]; @@ -343,7 +461,7 @@ export function AcceptModal({ quoteId, onClose }: AcceptModalProps) {
- {/* هدر ثابت و هم‌تراز */} + {/* هدر ثابت */}
@@ -521,9 +639,9 @@ export function AcceptModal({ quoteId, onClose }: AcceptModalProps) {
- {/* ردیف‌های تخصیص */} + {/* ردیف‌های تخصیص — تراز افقی یکپارچه بدون جابجایی دکمه‌ها */}
- {allocs.map((alloc, idx) => { + {allocs.map((alloc) => { const otherUsedProjects = new Set( allocs .filter((a) => a.rowId !== alloc.rowId) @@ -531,65 +649,88 @@ export function AcceptModal({ quoteId, onClose }: AcceptModalProps) { .filter(Boolean) ); + const words = numberToPersianWords(alloc.quantity); + return (
- - {faNum(idx + 1)}. - + {/* انتخاب پروژه و دکمه حذف موبایل */} +
+
+ +
- {/* انتخاب پروژه (الزام ۳: عدم امکان انتخاب پروژه تکراری) */} -
- + +
- {/* مقدار تخصیص و برچسب واحد */} -
- - updateAllocationRow(li.id, alloc.rowId, { - quantity: e.target.value, - }) - } - /> - - {li.unit} - + {/* فیلد مقدار بدون اسپینر + واحد ثابت + متن حروفی */} +
+
+ + updateAllocationRow(li.id, alloc.rowId, { + quantity: e.target.value, + }) + } + /> +
+ {li.unit} +
+
+ + {words ? ( +
+ {words} {li.unit} +
+ ) : null}
- {/* دکمه حذف ردیف */} + {/* دکمه حذف دسکتاپ — تراز شده با لبه بالای دراپ‌داون و اینپوت */}
- {/* فوتر کارت قلم: دکمه افزودن ردیف جدید */} + {/* فوتر کارت قلم */}
- - -
+ {/* فوتر ثابت */} +
+ + + + +