feat(frontend): add React UI — procurement office, wizard, suppliers, inventory panel (dash port), role-based login

This commit is contained in:
ali_mohaghegh
2026-08-25 19:08:15 +03:30
parent 170aed66ea
commit 4e105c5cd5
103 changed files with 16352 additions and 0 deletions
+98
View File
@@ -0,0 +1,98 @@
import { useEffect, useState } from "react";
import { groupCommas, parseNum, toWords } from "../lib/numutil";
import { cn } from "../lib/utils";
export interface NumberInputProps {
value: number | null;
onChange: (n: number | null) => void;
placeholder?: string;
className?: string;
showWords?: boolean; // نمایش معادل فارسی زیر input
inputMode?: "numeric" | "decimal" | undefined;
maxDecimals?: number; // حداکثر رقم اعشار (مثلاً ۲ برای قیمت واحد)
}
// ورودی عددی با فرمت‌دهی خودکار (groupCommas) و نمایش کلمات فارسی
export function NumberInput({
value,
onChange,
placeholder,
className,
showWords = false,
inputMode = "numeric",
maxDecimals,
}: NumberInputProps) {
// فرمت با رعایت حداکثر اعشار (گرد کردن)
const formatValue = (n: number): string => {
let v = n;
if (maxDecimals != null) {
const f = Math.pow(10, maxDecimals);
v = Math.round(n * f) / f;
}
return groupCommas(v, false);
};
const [text, setText] = useState<string>(() => (value != null ? formatValue(value) : ""));
// همگام‌سازی وقتی value از بیرون عوض می‌شود (مثلاً محاسبه‌ی خودکار قیمت کل).
// null از بیرون متن کاربر را پاک نمی‌کند — تا تایپ میانی مثل «0.» از بین نرود.
useEffect(() => {
const cur = parseNum(text);
if (value != null && value !== cur) {
setText(formatValue(value));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value]);
const commit = (raw: string) => {
const n = parseNum(raw);
if (n != null) {
const formatted = formatValue(n);
setText(formatted);
onChange(n);
} else {
setText(raw);
onChange(null);
}
};
const n = parseNum(text);
return (
<div className={cn("min-w-0", className)}>
<input
type="text"
className="num-input h-9 w-full rounded-lg border border-line bg-panel px-3 py-1 text-sm text-ink shadow-sm transition-colors placeholder:text-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40"
value={text}
placeholder={placeholder}
inputMode={inputMode}
onChange={(e) => {
const raw = e.target.value;
// فرمت‌دهی خودکار هنگام تایپ — اما جداکننده‌ی اعشارِ وسط تایپ
// («0.» یا «1.») دست‌نخورده می‌ماند تا کاربر بتواند اعشار بزند
const parsed = parseNum(raw);
const trailingSep = /[.,،/٬٫]$/.test(raw);
if (parsed != null && !trailingSep) {
setText(formatValue(parsed));
onChange(parsed);
} else {
setText(raw);
onChange(parsed);
}
}}
onBlur={() => commit(text)}
/>
{showWords && (
<span className={cn("num-pill", n == null && "empty")}>
{n != null ? (
<>
{groupCommas(n, true)} <span className="num-words">({toWords(n)})</span>
</>
) : (
""
)}
</span>
)}
</div>
);
}