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