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