feat(frontend): unified warehouse UI, calendar fix, partial receipts, RBAC views, admin panel, dev-mode 3-step

- header: icon box + dynamic subheader '[user] | [role] – [project/همه پروژهها]'
  on every warehouse page; module context kept in the hamburger (items 1-2)
- project manager view: office button + inline logout removed; hamburger
  contains only «خروج از حساب»; sub-views get «بازگشت به داشبورد» (Header
  backTo/goBack); «افزایش موجود» primary button on the dashboard (item 8)
- daily control: cells render the day-of-month (was the Jalali year),
  month nav uses real Jalali months, header is pure Jalali
  «امروز ۸ شهریور ۱۴۰۵», today gets a blue accent (item 3)
- arrivals: partial (item/qty) receipt — any valid item is enough to submit,
  ceiling + allocation-sum validation with inline errors, Received/In-Transit
  badges; ledger shows «رسیده X از Y» / «در راه Z» + «رسید جزیی» badge (item 4)
- api client: multipart uploads no longer send Content-Type json (fixes the
  invoice/proforma 400 «دادهی ارسالشده نامعتبر است»); undefined query
  params are dropped (fixes empty admin project filter, item 5)
- organization: user-creation card removed, role column read-only for
  procurement manager, Casdoor sync button (item 6)
- admin panel (/admin): Casdoor user sync + grant/revoke «مدیر تدارکات»;
  Developer Mode is a 3-step admin-gated flow on the login page (item 7)
This commit is contained in:
ali_mohaghegh
2026-09-01 17:28:19 +03:30
parent 83f4fb0747
commit c5997ac243
17 changed files with 648 additions and 228 deletions
+25 -3
View File
@@ -35,6 +35,16 @@ import type {
const BASE_URL = (import.meta.env.VITE_API_BASE_URL as string | undefined)?.trim() || "/api/v1";
const PAGE_SIZE = 100;
// حذف کلیدهای خالی/undefined از پارامترهای کوئری — URLSearchParams مقدار
// undefined را به رشته‌ی "undefined" تبدیل می‌کند و بک‌اند ۴۰۰ برمی‌گرداند.
function cleanParams(params: Record<string, unknown>): Record<string, string> {
const out: Record<string, string> = {};
for (const [k, v] of Object.entries(params)) {
if (v !== undefined && v !== null && v !== "") out[k] = String(v);
}
return out;
}
// v2.6: توکن Casdoor (Bearer) یا هدرهای توسعه (X-User-*) — بک‌اند هر دو را می‌پذیرد.
function authHeaders(init?: RequestInit): Record<string, string> {
const { token, user, isDev } = useAuthStore.getState();
@@ -192,7 +202,11 @@ async function uploadFile(path: string, file: File): Promise<Attachment> {
form.append("file", file);
let res: Response;
try {
res = await fetch(BASE_URL + path, { method: "POST", body: form, headers: authHeaders() });
// Content-Type را حذف می‌کنیم — مرورگر باید multipart boundary را خودش
// بگذارد؛ وگرنه FastAPI فرم را نمی‌خواند و ۴۰۰/۴۲۲ برمی‌گردد.
const headers = authHeaders();
delete headers["Content-Type"];
res = await fetch(BASE_URL + path, { method: "POST", body: form, headers });
} catch {
throw new Error("اتصال به سرور برقرار نشد — آیا بک‌اند در دسترس است؟");
}
@@ -466,6 +480,7 @@ function adaptLineItem(raw: RawLineItem): Quote["line_items"][number] {
accepted_ts_id: raw.accepted_termsheet_id ?? raw.decision?.chosen_item_termsheet_id ?? null,
rejected: raw.decision?.status === "rejected",
arrived_date: raw.receipt?.received_at ?? null,
received_qty: raw.receipt?.received_qty ?? null,
projects: (raw.projects || []).map((p) => ({ id: p.id, name: p.name })),
};
}
@@ -1421,6 +1436,13 @@ export const apiV26 = {
patchData<UserRow>(`/users/${id}`, { role }),
assignProject: async (id: string, projectId: string | null): Promise<UserRow> =>
postData<UserRow>(`/users/${id}/project`, { project_id: projectId }),
// همگام‌سازی هویت‌ها از Casdoor (ادمین/مدیر تدارکات) — نقش‌ها دست‌نخورده می‌مانند
sync: async (): Promise<{ users: UserRow[]; created: number }> => {
const res = await request<{ data: UserRow[]; meta: { total: number; created: number } }>("/users/sync", {
method: "POST",
});
return { users: res.data, created: res.meta?.created ?? 0 };
},
},
warehouses: {
@@ -1444,12 +1466,12 @@ export const apiV26 = {
inventory: {
list: async (params: { project_id?: string; category_id?: string; warehouse_id?: string; material_id?: string; sort?: string } = {}): Promise<InventoryRow[]> => {
const qs = new URLSearchParams({ page_size: "100", ...(params as Record<string, string>) }).toString();
const qs = new URLSearchParams({ page_size: "100", ...cleanParams(params) }).toString();
const data = await getData<Parameters<typeof rawInventory>[0][]>(`/inventory?${qs}`);
return data.map(rawInventory);
},
transactions: async (params: { project_id?: string; warehouse_id?: string; transaction_type?: string; date_from?: string; date_to?: string; sort?: string } = {}): Promise<InventoryTransaction[]> => {
const qs = new URLSearchParams({ page_size: "100", ...(params as Record<string, string>) }).toString();
const qs = new URLSearchParams({ page_size: "100", ...cleanParams(params) }).toString();
return getData<InventoryTransaction[]>(`/inventory/transactions?${qs}`);
},
createTransaction: async (body: {
+1
View File
@@ -69,6 +69,7 @@ export interface LineItem {
accepted_ts_id: string | null; // با کدوم ترم‌شیت پذیرش شده
rejected: boolean; // از معامله حذف شده؟
arrived_date: string | null; // تاریخ رسید
received_qty?: number | null; // مجموع رسیدهای ثبت‌شده (رسید جزیی) — null = هنوز داده‌ای نیست
projects?: { id: string; name: string }[]; // پروژه‌های تخصیص‌یافته به این قلم
}