feat(frontend): add React UI — procurement office, wizard, suppliers, inventory panel (dash port), role-based login
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user