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
@@ -0,0 +1,88 @@
import { Link } from "react-router-dom";
import { MapPin, Phone, User, Plus } from "lucide-react";
import type { Material, Supplier } from "../../lib/types";
import { normalize } from "../../lib/normalize";
import { topicByName } from "../../lib/constants";
// دسته‌های یک تاپیک (اتحاد materials + categories تأمین‌کننده‌ها)
export function topicCategories(topic: string, materials: Material[], suppliers: Supplier[]): string[] {
const out = new Set<string>();
materials
.filter((m) => topic && normalize(m.topic) === normalize(topic) && m.category)
.forEach((m) => out.add(m.category));
suppliers
.filter((s) => topic && normalize(s.topic) === normalize(topic))
.forEach((s) => (s.categories || []).forEach((c) => c && out.add(c)));
return [...out].sort((a, b) => a.localeCompare(b, "fa"));
}
export function suppliersOfCategory(topic: string, cat: string, suppliers: Supplier[]): Supplier[] {
return suppliers.filter(
(s) => normalize(s.topic) === normalize(topic) && (s.categories || []).some((c) => normalize(c) === normalize(cat)),
);
}
export function topicFromParam(param: string | null): string | null {
if (!param) return null;
const t = topicByName(param);
return t ? t.name : null;
}
// کارت تأمین‌کننده
export function SupplierCard({
s,
topic,
deletable,
onDelete,
materials = [],
}: {
s: Supplier;
topic: string;
deletable?: boolean;
onDelete?: (id: string) => void;
// متریال‌های خریداری‌شده از این تأمین‌کننده (به‌جای دسته‌بندی)
materials?: string[];
}) {
return (
<div className="sup-card">
<div className="sc-head">
<Link to={`/suppliers/${s.id}`} className="sc-name-link">
{s.name}
</Link>
{deletable && onDelete && (
<button type="button" className="sup-del" title="حذف تأمین‌کننده" onClick={() => onDelete(s.id)}>
×
</button>
)}
</div>
<div className="sc-meta">
<span>
<MapPin className="me-1 inline h-3 w-3" /> {s.location || "—"}
</span>
<span>
<Phone className="me-1 inline h-3 w-3" /> {s.phone || "—"}
</span>
<span>
<User className="me-1 inline h-3 w-3" /> {s.contact || "—"}
</span>
</div>
{materials.length > 0 && (
<div className="mt-2 border-t border-line pt-1.5">
<div className="mb-1 flex flex-wrap gap-1">
{materials.map((m, i) => (
<span key={i} className="spec-chip">
{m}
</span>
))}
</div>
</div>
)}
<div className="sc-actions">
<Link to={`/wizard?topic=${encodeURIComponent(topic)}&supplier=${encodeURIComponent(s.id)}`} className="btn-line-file no-underline">
<Plus className="h-3 w-3" />
پیشنهاد جدید
</Link>
</div>
</div>
);
}