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
+63
View File
@@ -0,0 +1,63 @@
// ============================================================
// Auth store — Casdoor OIDC + PKCE session (v2.6)
// Dev mode: no token — identity sent via X-User-Role/X-Project-Id
// headers (backend dev fallback) so the dash works before Casdoor
// is fully seeded.
// ============================================================
import { create } from "zustand";
import { persist } from "zustand/middleware";
export interface AuthUser {
id: string;
casdoor_id: string;
first_name: string;
last_name: string;
phone: string | null;
role: "pm" | "procurement_manager" | "admin" | null;
role_label: string | null;
project_id: string | null;
project_name: string | null;
display_name: string;
}
interface AuthStore {
token: string | null;
user: AuthUser | null;
isDev: boolean;
setToken: (token: string | null) => void;
setUser: (user: AuthUser | null) => void;
setDevUser: (role: "pm" | "admin" | "procurement_manager", projectId: string | null, projectName: string | null) => void;
logout: () => void;
}
export const useAuthStore = create<AuthStore>()(
persist(
(set) => ({
token: null,
user: null,
isDev: false,
setToken: (token) => set({ token, isDev: false }),
setUser: (user) => set({ user, isDev: false }),
setDevUser: (role, projectId, projectName) =>
set({
token: null,
isDev: true,
user: {
id: "dev-user",
casdoor_id: "dev",
first_name: "کاربر",
last_name: "آزمایشی",
phone: null,
role,
role_label: role === "pm" ? "مدیر پروژه (آزمایشی)" : role === "procurement_manager" ? "مدیر تدارکات (آزمایشی)" : "ادمین (آزمایشی)",
project_id: projectId,
project_name: projectName,
display_name: projectId ? `کاربر آزمایشی — ${projectName || ""}` : "کاربر آزمایشی",
},
}),
logout: () => set({ token: null, user: null, isDev: false }),
}),
{ name: "procurement-auth-v1" },
),
);
+133
View File
@@ -0,0 +1,133 @@
// ============================================================
// Zustand store — داده‌ها + state UI
// داده‌ها از React Query می‌آیند (api client) و اینجا mirror می‌شوند؛
// mutationها اول خوش‌بینانه اینجا آپدیت می‌کنند، بعد API را صدا
// می‌زنند و آخر cache را invalidate می‌کنند تا با حقیقت همگرا شود.
// ============================================================
import { create } from "zustand";
import type {
CategoryUnit,
Material,
Project,
Quote,
StandardConversion,
Store,
Supplier,
UnitConversion,
} from "../lib/types";
interface AppStore {
// داده‌ها (همگام با API)
suppliers: Supplier[];
materials: Material[];
projects: Project[];
quotes: Quote[];
categoryUnits: CategoryUnit[];
unitConversions: UnitConversion[];
standardConversions: StandardConversion[];
// UI state
expandedQuoteIds: Set<string>;
// wizard
wizardStep: number;
wizardHighestVisited: number;
// actions — data
setData: (store: Store) => void;
addQuote: (q: Quote) => void;
updateQuote: (id: string, patch: Partial<Quote>) => void;
deleteQuote: (id: string) => void;
addSupplier: (s: Supplier) => void;
updateSupplier: (id: string, patch: Partial<Supplier>) => void;
deleteSupplier: (id: string) => void;
addMaterial: (m: Material) => void;
addProject: (p: Project) => void;
setCategoryUnit: (cu: CategoryUnit) => void;
setConversion: (c: UnitConversion) => void;
deleteConversion: (id: string) => void;
setStandardConversion: (sc: StandardConversion) => void;
deleteStandardConversion: (id: string) => void;
// actions — UI
toggleExpand: (id: string) => void;
setWizardStep: (step: number) => void;
setWizardHighestVisited: (n: number) => void;
}
export const useAppStore = create<AppStore>()((set) => ({
suppliers: [],
materials: [],
projects: [],
quotes: [],
categoryUnits: [],
unitConversions: [],
standardConversions: [],
expandedQuoteIds: new Set<string>(),
wizardStep: 1,
wizardHighestVisited: 1,
setData: (store) =>
set({
suppliers: store.suppliers,
materials: store.materials,
projects: store.projects,
quotes: store.quotes,
categoryUnits: store.categoryUnits,
unitConversions: store.unitConversions,
standardConversions: store.standardConversions,
}),
addQuote: (q) => set((s) => ({ quotes: [...s.quotes, q] })),
updateQuote: (id, patch) =>
set((s) => ({
quotes: s.quotes.map((q) => (q.id === id ? { ...q, ...patch } : q)),
})),
deleteQuote: (id) => set((s) => ({ quotes: s.quotes.filter((q) => q.id !== id) })),
addSupplier: (sup) => set((s) => ({ suppliers: [...s.suppliers, sup] })),
updateSupplier: (id, patch) =>
set((s) => ({
suppliers: s.suppliers.map((x) => (x.id === id ? { ...x, ...patch } : x)),
})),
deleteSupplier: (id) => set((s) => ({ suppliers: s.suppliers.filter((x) => x.id !== id) })),
addMaterial: (m) => set((s) => ({ materials: [...s.materials, m] })),
addProject: (p) => set((s) => ({ projects: [...s.projects, p] })),
setCategoryUnit: (cu) =>
set((s) => {
const idx = s.categoryUnits.findIndex((x) => x.id === cu.id);
const categoryUnits = [...s.categoryUnits];
if (idx >= 0) categoryUnits[idx] = cu;
else categoryUnits.push(cu);
return { categoryUnits };
}),
setConversion: (c) =>
set((s) => {
const idx = s.unitConversions.findIndex((x) => x.id === c.id);
const unitConversions = [...s.unitConversions];
if (idx >= 0) unitConversions[idx] = c;
else unitConversions.push(c);
return { unitConversions };
}),
deleteConversion: (id) =>
set((s) => ({ unitConversions: s.unitConversions.filter((x) => x.id !== id) })),
setStandardConversion: (sc) =>
set((s) => {
const idx = s.standardConversions.findIndex((x) => x.id === sc.id);
const standardConversions = [...s.standardConversions];
if (idx >= 0) standardConversions[idx] = sc;
else standardConversions.push(sc);
return { standardConversions };
}),
deleteStandardConversion: (id) =>
set((s) => ({ standardConversions: s.standardConversions.filter((x) => x.id !== id) })),
toggleExpand: (id) =>
set((s) => {
const next = new Set(s.expandedQuoteIds);
if (next.has(id)) next.delete(id);
else next.add(id);
return { expandedQuoteIds: next };
}),
setWizardStep: (step) => set({ wizardStep: step }),
setWizardHighestVisited: (n) => set({ wizardHighestVisited: n }),
}));