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
+135 -39
View File
@@ -1,7 +1,7 @@
import { useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import { toast } from "sonner";
import { ClipboardList, Loader2, Wrench, X } from "lucide-react";
import { ClipboardList, Loader2, Lock, ShieldCheck, Wrench, X } from "lucide-react";
import { api, apiV26 } from "../../lib/api";
import { useAuthStore } from "../../store/auth";
import type { Project } from "../../lib/types";
@@ -11,7 +11,11 @@ import type { Project } from "../../lib/types";
// فرم اصلی: Username / Password / Organization (اختیاری) →
// POST /api/v1/auth/login (بک‌اند با grant_type=password از
// Casdoor توکن می‌گیرد و نقش را از جدول users خودمان برمی‌گرداند)
// حالت توسعه: دکمه‌ی ثابت بالا-چپ + پنل تست (نقش/پروژه).
// حالت توسعه (فقط ادمین — سوپر یوزر) — چرخه‌ی ۳ مرحله‌ای:
// مرحله ۱: درگاه احراز هویت ادمین (نام کاربری/رمز — فقط ادمین رد می‌شود)
// مرحله ۲: هاب انتخاب نقش (ادمین / مدیر تدارکات / مدیر پروژه + پروژه)
// مرحله ۳: ورود با نقش انتخاب‌شده — ادمین → پنل مدیریت ادمین،
// مدیر تدارکات → دفتر، مدیر پروژه → پنل انبار پروژه‌ی خودش
// با VITE_ENABLE_DEV_LOGIN=false غیرفعال می‌شود.
// ============================================================
@@ -35,8 +39,12 @@ export function LoginPage() {
const [organization, setOrganization] = useState("");
const [loading, setLoading] = useState(false);
// ---- پنل حالت توسعه ----
// ---- پنل حالت توسعه (۳ مرحله) ----
const [devOpen, setDevOpen] = useState(false);
const [devStep, setDevStep] = useState<"auth" | "hub">("auth");
const [devAdminUser, setDevAdminUser] = useState("");
const [devAdminPass, setDevAdminPass] = useState("");
const [devAuthLoading, setDevAuthLoading] = useState(false);
const [projects, setProjects] = useState<Project[]>([]);
const [devRole, setDevRole] = useState<"pm" | "procurement_manager" | "admin">("procurement_manager");
const [devProjectId, setDevProjectId] = useState("");
@@ -81,6 +89,33 @@ export function LoginPage() {
}
};
// ---- مرحله ۱ حالت توسعه: درگاه ادمین ----
const devAuth = async (e: React.FormEvent) => {
e.preventDefault();
if (!devAdminUser.trim() || !devAdminPass) {
toast.error("نام کاربری و رمز عبور ادمین را وارد کنید");
return;
}
setDevAuthLoading(true);
try {
const { user: adminUser } = await apiV26.auth.login({
username: devAdminUser.trim(),
password: devAdminPass,
...(organization.trim() ? { organization: organization.trim() } : {}),
});
if (adminUser.role !== "admin") {
toast.error("حالت توسعه فقط برای ادمین (سوپر یوزر) در دسترس است");
return;
}
setDevStep("hub");
} catch (err) {
toast.error(err instanceof Error ? err.message : "خطا در ورود ادمین");
} finally {
setDevAuthLoading(false);
}
};
// ---- مرحله ۳ حالت توسعه: ورود با نقش انتخاب‌شده ----
const devLogin = () => {
if (devRole === "pm" && !devProjectId) {
toast.error("برای نقش مدیر پروژه، پروژه را انتخاب کنید");
@@ -88,7 +123,17 @@ export function LoginPage() {
}
setDevUser(devRole, devRole === "pm" ? devProjectId : null, devRole === "pm" ? devProject?.name || null : null);
toast.success(`ورود آزمایشی انجام شد — نقش: ${DEV_ROLE_LABELS[devRole]}`);
navigate(devRole === "pm" ? "/inventory" : "/");
// ادمین → پنل مدیریت ادمین؛ مدیر تدارکات → دفتر؛ مدیر پروژه → پنل انبار
if (devRole === "admin") navigate("/admin");
else if (devRole === "pm") navigate("/inventory");
else navigate("/");
};
const closeDev = () => {
setDevOpen(false);
setDevStep("auth");
setDevAdminUser("");
setDevAdminPass("");
};
return (
@@ -113,55 +158,106 @@ export function LoginPage() {
{devOpen && (
<div className="absolute left-0 top-full mt-2 w-80 rounded-xl border border-dashed border-amber/40 bg-panel p-4 shadow-lg">
<div className="mb-3 flex items-center justify-between">
<h3 className="text-[13px] font-bold text-amber">پنل توسعه (ورود آزمایشی)</h3>
<h3 className="text-[13px] font-bold text-amber">
{devStep === "auth" ? "مرحله ۱ — درگاه ادمین (سوپر یوزر)" : "مرحله ۲ — انتخاب نقش"}
</h3>
<button
type="button"
onClick={() => setDevOpen(false)}
onClick={closeDev}
className="cursor-pointer rounded-md p-1 text-muted hover:bg-slate-bg hover:text-ink"
aria-label="بستن پنل توسعه"
>
<X className="h-4 w-4" />
</button>
</div>
<div className="grid gap-3">
<div className="field">
<label className="mb-1 block text-[12px] font-semibold text-muted">نقش</label>
<select
className="h-9 w-full rounded-lg border border-line bg-panel px-3 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40"
value={devRole}
onChange={(e) => setDevRole(e.target.value as "pm" | "procurement_manager" | "admin")}
>
<option value="procurement_manager">مدیر تدارکات</option>
<option value="admin">ادمین</option>
<option value="pm">مدیر پروژه</option>
</select>
</div>
{devRole === "pm" && (
{devStep === "auth" ? (
<form onSubmit={devAuth} className="grid gap-3">
<p className="text-[11.5px] leading-5 text-muted">
ورود به حالت توسعه فقط با حساب <b>ادمین</b> (سوپر یوزر) ممکن است — سپس نقش دلخواه را
برای تست انتخاب کنید.
</p>
<div className="field">
<label className="mb-1 block text-[12px] font-semibold text-muted">پروژه</label>
<label className="mb-1 block text-[12px] font-semibold text-muted">نام کاربری ادمین</label>
<input
type="text"
dir="ltr"
className="h-9 w-full rounded-lg border border-line bg-panel px-3 text-left text-sm shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40"
placeholder="admin"
value={devAdminUser}
onChange={(e) => setDevAdminUser(e.target.value)}
/>
</div>
<div className="field">
<label className="mb-1 block text-[12px] font-semibold text-muted">رمز عبور ادمین</label>
<input
type="password"
dir="ltr"
className="h-9 w-full rounded-lg border border-line bg-panel px-3 text-left text-sm shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40"
placeholder="••••••"
value={devAdminPass}
onChange={(e) => setDevAdminPass(e.target.value)}
/>
</div>
<button
type="submit"
disabled={devAuthLoading}
className="inline-flex h-9 cursor-pointer items-center justify-center gap-2 rounded-lg bg-amber px-3 text-[13px] font-bold text-white transition-colors hover:bg-amber/90 disabled:opacity-50"
>
{devAuthLoading && <Loader2 className="h-4 w-4 animate-spin" />}
<Lock className="h-3.5 w-3.5" />
تأیید هویت ادمین
</button>
</form>
) : (
<div className="grid gap-3">
<div className="field">
<label className="mb-1 block text-[12px] font-semibold text-muted">نقش</label>
<select
className="h-9 w-full rounded-lg border border-line bg-panel px-3 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40"
value={devProjectId}
onChange={(e) => setDevProjectId(e.target.value)}
value={devRole}
onChange={(e) => setDevRole(e.target.value as "pm" | "procurement_manager" | "admin")}
>
<option value="">— انتخاب پروژه —</option>
{projects.map((p) => (
<option key={p.id} value={p.id}>
{p.name}
</option>
))}
<option value="admin">ادمین — پنل مدیریت ادمین</option>
<option value="procurement_manager">مدیر تدارکات</option>
<option value="pm">مدیر پروژه</option>
</select>
</div>
)}
<button
type="button"
onClick={devLogin}
disabled={devRole === "pm" && !devProjectId}
className="inline-flex h-9 cursor-pointer items-center justify-center rounded-lg bg-amber px-3 text-[13px] font-bold text-white transition-colors hover:bg-amber/90 disabled:cursor-not-allowed disabled:opacity-50"
>
ورود آزمایشی
</button>
</div>
{devRole === "pm" && (
<div className="field">
<label className="mb-1 block text-[12px] font-semibold text-muted">پروژه</label>
<select
className="h-9 w-full rounded-lg border border-line bg-panel px-3 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40"
value={devProjectId}
onChange={(e) => setDevProjectId(e.target.value)}
>
<option value="">— انتخاب پروژه —</option>
{projects.map((p) => (
<option key={p.id} value={p.id}>
{p.name}
</option>
))}
</select>
</div>
)}
<button
type="button"
onClick={devLogin}
disabled={devRole === "pm" && !devProjectId}
className="inline-flex h-9 cursor-pointer items-center justify-center gap-2 rounded-lg bg-amber px-3 text-[13px] font-bold text-white transition-colors hover:bg-amber/90 disabled:cursor-not-allowed disabled:opacity-50"
>
<ShieldCheck className="h-3.5 w-3.5" />
ورود به حالت توسعه
</button>
<button
type="button"
onClick={() => setDevStep("auth")}
className="cursor-pointer text-center text-[11.5px] text-muted hover:text-ink"
>
→ بازگشت به درگاه ادمین
</button>
</div>
)}
</div>
)}
</div>