feat(frontend): add React UI — procurement office, wizard, suppliers, inventory panel (dash port), role-based login
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
import { useCallback, useLayoutEffect, useState } from "react";
|
||||
|
||||
export interface FlowRefs {
|
||||
items: Map<string, HTMLDivElement>;
|
||||
termsheets: Map<string, HTMLDivElement>;
|
||||
}
|
||||
|
||||
export interface FlowDiagramProps {
|
||||
flowEl: HTMLDivElement | null;
|
||||
refs: FlowRefs;
|
||||
redrawKey?: string; // وقتی دادهی quote عوض شد (پذیرش/رسید/ویرایش) خطوط دوباره رسم شوند
|
||||
}
|
||||
|
||||
interface PathSpec {
|
||||
d: string;
|
||||
accepted: boolean;
|
||||
}
|
||||
|
||||
// خطوط اتصال SVG بین جعبههای اقلام و ترمشیتها — موقعیت جعبهها با
|
||||
// getBoundingClientRect خوانده میشود و با ResizeObserver در جریان میماند.
|
||||
// خاکستری = باز، آبی = جفت پذیرفتهشدهی دقیق.
|
||||
export function FlowDiagram({ flowEl, refs, redrawKey = "" }: FlowDiagramProps) {
|
||||
const [paths, setPaths] = useState<PathSpec[]>([]);
|
||||
|
||||
const draw = useCallback(() => {
|
||||
if (!flowEl) return;
|
||||
const crect = flowEl.getBoundingClientRect();
|
||||
if (!crect.width) return;
|
||||
|
||||
const out: PathSpec[] = [];
|
||||
refs.items.forEach((item) => {
|
||||
const ir = item.getBoundingClientRect();
|
||||
const x1 = ir.left - crect.left; // لبهی چپ جعبهی قلم (رو به فاصله)
|
||||
const y1 = ir.top + ir.height / 2 - crect.top;
|
||||
const isRejected = item.classList.contains("rejected");
|
||||
const acceptedTsId = item.dataset.acceptedTs || "";
|
||||
const tsIds = (item.dataset.tsIds || "").split(",").filter(Boolean);
|
||||
tsIds.forEach((tsId) => {
|
||||
const t = refs.termsheets.get(tsId);
|
||||
if (!t) return;
|
||||
const tr = t.getBoundingClientRect();
|
||||
const x2 = tr.right - crect.left; // لبهی راست جعبهی termsheet
|
||||
const y2 = tr.top + tr.height / 2 - crect.top;
|
||||
const cx = (x1 + x2) / 2;
|
||||
// آبی فقط برای جفت دقیقی که قلم پذیرفته — بقیه خاکستری
|
||||
const accepted = !isRejected && acceptedTsId === tsId;
|
||||
out.push({
|
||||
d: `M ${x1} ${y1} C ${cx} ${y1}, ${cx} ${y2}, ${x2} ${y2}`,
|
||||
accepted,
|
||||
});
|
||||
});
|
||||
});
|
||||
setPaths(out);
|
||||
}, [flowEl, refs, redrawKey]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
draw();
|
||||
if (!flowEl) return;
|
||||
const ro = new ResizeObserver(draw);
|
||||
ro.observe(flowEl);
|
||||
window.addEventListener("resize", draw);
|
||||
if (typeof document !== "undefined" && document.fonts && document.fonts.ready) {
|
||||
void document.fonts.ready.then(draw);
|
||||
}
|
||||
return () => {
|
||||
ro.disconnect();
|
||||
window.removeEventListener("resize", draw);
|
||||
};
|
||||
}, [draw, flowEl]);
|
||||
|
||||
return (
|
||||
<svg className="flow-lines" aria-hidden="true">
|
||||
{paths.map((p, i) => (
|
||||
<path key={i} className={`flow-line${p.accepted ? " accepted" : ""}`} d={p.d} />
|
||||
))}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user