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
+92
View File
@@ -0,0 +1,92 @@
import { useState } from "react";
import { normalize } from "../lib/normalize";
import { faNum } from "../lib/formatters";
import { useClickOutside } from "./useClickOutside";
import { ConfirmDeleteDialog } from "./ConfirmDeleteDialog";
export interface SpecDrawerProps {
specs: string[];
onChange: (specs: string[]) => void;
}
// کشوی چیپ مشخصه‌های فنی: دکمه‌ی «نوع (N)» → کشو با چیپ‌ها + input برای افزودن
export function SpecDrawer({ specs, onChange }: SpecDrawerProps) {
const [open, setOpen] = useState(false);
const [draft, setDraft] = useState("");
const [pendingRemove, setPendingRemove] = useState<string | null>(null);
const ref = useClickOutside<HTMLDivElement>(() => setOpen(false));
const addChip = (val?: string) => {
const v = String(val ?? draft).trim();
if (!v) return;
if (specs.some((s) => normalize(s) === normalize(v))) return;
onChange([...specs, v]);
setDraft("");
};
const removeChip = (v: string) => {
onChange(specs.filter((s) => s !== v));
};
return (
<div ref={ref} className="spec-cell">
<button
type="button"
className="spec-trigger"
title="مشخصه‌های فنی"
onClick={(e) => {
e.stopPropagation();
setOpen(true);
}}
>
<span className={`spec-trigger-label${specs.length ? "" : " muted"}`}>
{specs.length ? `مشخصه (${faNum(specs.length)})` : "مشخصه"}
</span>
<span className="text-muted">▾</span>
</button>
{open && (
<div className="spec-drawer open">
<div className="spec-chips">
{specs.map((s) => (
<span key={s} className="spec-chip">
<span className="spec-chip-t">{s}</span>
<button type="button" className="spec-chip-x" title="حذف" onClick={() => setPendingRemove(s)}>
×
</button>
</span>
))}
</div>
<div className="spec-add-row">
<input
type="text"
className="h-8 w-full rounded-lg border border-line bg-panel px-2 text-[12px] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40"
placeholder="مشخصه را تایپ کنید..."
autoComplete="off"
value={draft}
onChange={(e) => setDraft(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Escape") setOpen(false);
if (e.key === "Enter") {
e.preventDefault();
addChip();
}
}}
/>
<button type="button" className="spec-add-btn" title="افزودن مشخصه" onClick={() => addChip()}>
+
</button>
</div>
</div>
)}
<ConfirmDeleteDialog
open={pendingRemove !== null}
itemName={pendingRemove || ""}
onConfirm={() => {
if (pendingRemove !== null) removeChip(pendingRemove);
setPendingRemove(null);
}}
onCancel={() => setPendingRemove(null)}
/>
</div>
);
}