import { useEffect, useRef, useState } from "react"; import { Check, Loader2, Pencil, X } from "lucide-react"; import { cn } from "../../lib/utils"; /* ---------- digit helpers (accept both Persian & Latin input) ---------- */ const FA_DIGITS = "۰۱۲۳۴۵۶۷۸۹"; const AR_DIGITS = "٠١٢٣٤٥٦٧٨٩"; function toLatinDigits(input: string): string { return input .replace(/[۰-۹]/g, (d) => String(FA_DIGITS.indexOf(d))) .replace(/[٠-٩]/g, (d) => String(AR_DIGITS.indexOf(d))); } /** Extract only the integer digits from any incoming value ("120000.00" -> "120000"). */ function normalizeIncoming(raw: string | null | undefined): string { if (raw === null || raw === undefined || raw === "") return ""; const n = parseFloat(toLatinDigits(String(raw))); if (!isFinite(n) || n <= 0) return ""; return String(Math.round(n)); } /** Keep only digits typed by the user (drops separators, letters, Persian digits normalized). */ function onlyDigits(input: string): string { return toLatinDigits(input).replace(/[^\d]/g, ""); } /** Group with thousands separators for the edit input ("120000" -> "120,000"). */ function groupLatin(digits: string): string { if (!digits) return ""; return digits.replace(/\B(?=(\d{3})+(?!\d))/g, ","); } /** Persian-grouped display ("120000" -> "۱۲۰٬۰۰۰"). */ function toFaDisplay(digits: string): string { if (!digits) return ""; return Number(digits).toLocaleString("fa-IR"); } /* ------------------------------- component ------------------------------ */ interface InlinePriceEditorProps { /** Current raw price (decimal string) or null/"" when unset. */ value: string | null | undefined; /** Called with the new integer price string, or null when cleared (only if allowEmpty). */ onSave: (value: string | null) => void; isPending?: boolean; /** Allow saving an empty value as null (e.g. a variant inheriting the base price). */ allowEmpty?: boolean; /** Muted hint shown when there is no value and we're not editing (e.g. inherited price). */ placeholder?: string; suffix?: string; size?: "sm" | "md"; align?: "start" | "center" | "end"; ariaLabel?: string; className?: string; } export function InlinePriceEditor({ value, onSave, isPending = false, allowEmpty = false, placeholder, suffix = "تومان", size = "md", align = "end", ariaLabel = "ویرایش قیمت", className, }: InlinePriceEditorProps) { const [editing, setEditing] = useState(false); const [draft, setDraft] = useState(""); // raw digits while editing // Optimistic value shown during the async save so the number doesn't flicker // back to the old server value before the refetch lands. const [optimistic, setOptimistic] = useState(null); const inputRef = useRef(null); const current = normalizeIncoming(value); // Clear the optimistic value once the mutation settles. useEffect(() => { if (!isPending) setOptimistic(null); }, [isPending]); useEffect(() => { if (editing && inputRef.current) { inputRef.current.focus(); inputRef.current.select(); } }, [editing]); const openEditor = () => { if (isPending) return; setDraft(current); setEditing(true); }; const commit = () => { const next = onlyDigits(draft); // No-op if unchanged. if (next === current) { setEditing(false); return; } if (!next) { if (allowEmpty) { setOptimistic(""); onSave(null); } else { // Empty not allowed (e.g. base price) → discard. setEditing(false); return; } } else { setOptimistic(next); onSave(next); } setEditing(false); }; const cancel = () => { setDraft(current); setEditing(false); }; const sizeText = size === "sm" ? "text-[13px]" : "text-[14px]"; const alignClass = align === "center" ? "justify-center text-center" : align === "start" ? "justify-start text-right" : "justify-end text-right"; /* ------------------------------ edit mode ----------------------------- */ if (editing) { return (
setDraft(onlyDigits(e.target.value))} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); commit(); } else if (e.key === "Escape") { e.preventDefault(); cancel(); } }} placeholder={allowEmpty ? "پیش‌فرض" : "قیمت"} className={cn( "w-24 rounded-lg border border-VITROWN_BLUE bg-WHITE px-2 py-2 text-left tabular-nums outline-none", "focus:ring-2 focus:ring-VITROWN_BLUE/30 transition-shadow", sizeText )} />
); } /* ---------------------------- display mode ---------------------------- */ const shown = isPending && optimistic !== null ? optimistic : current; const hasValue = shown !== ""; return ( ); } export default InlinePriceEditor;