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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user