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
+14 -8
View File
@@ -61,19 +61,24 @@ export function QuoteCard({ store, q, expanded, onToggleExpand, onAccept, onEdit
}
// ستون رسید — v2.6: تأیید رسید فقط در «پنل مدیریت انبار» توسط مدیر پروژه انجام می‌شود؛
// اینجا فقط وضعیت نمایش داده می‌شود.
// اینجا فقط وضعیت نمایش داده می‌شود. با رسید جزیی: «رسیده X از Y» + «در راه Z».
const activeItems = (q.line_items || []).filter((li) => !li.rejected);
const receivedSum = activeItems.reduce((s, li) => s + (li.received_qty || 0), 0);
const orderedSum = activeItems.reduce((s, li) => s + (li.quantity || 0), 0);
const partiallyReceived = q.status === "accepted" && receivedSum > 0 && receivedSum < orderedSum;
let arrivedCell: React.ReactNode;
if (q.status === "accepted") {
if (q.arrived_date) {
arrivedCell = <span className="tabular-nums">{fmtDateTime(q.arrived_date)}</span>;
} else {
const items = (q.line_items || []).filter((li) => !li.rejected);
const arrivedCount = items.filter((li) => li.arrived_date).length;
arrivedCell = arrivedCount ? (
<span className="tabular-nums">{fmtNum(arrivedCount)}/{fmtNum(items.length)}</span>
) : (
<span className="text-muted">در انتظار PM</span>
} else if (receivedSum > 0) {
arrivedCell = (
<span className="tabular-nums">
رسیده {fmtNum(receivedSum)} از {fmtNum(orderedSum)} — در راه {fmtNum(Math.max(0, orderedSum - receivedSum))}
</span>
);
} else {
arrivedCell = <span className="text-muted">در انتظار PM</span>;
}
} else if (q.arrived_date) {
arrivedCell = <span className="tabular-nums">{fmtDateTime(q.arrived_date)}</span>;
@@ -105,6 +110,7 @@ export function QuoteCard({ store, q, expanded, onToggleExpand, onAccept, onEdit
</div>
<div className="ms-auto flex shrink-0 items-center gap-1.5">
<Badge variant={q.status as "offered" | "accepted" | "arrived"}>{STATUS_LABEL[q.status]}</Badge>
{partiallyReceived && <Badge variant="offered">رسید جزیی</Badge>}
<button className="chip" onClick={onEdit} title="ویرایش پیشنهاد">
<Pencil className="h-3 w-3" />
ویرایش
@@ -98,6 +98,12 @@ export function QuoteDetail({ store, q }: QuoteDetailProps) {
<span>× {groupCommas(li.unit_price, true)}</span>
<span className="fb-total">= {total != null ? groupCommas(total, true) + " تومان" : "—"}</span>
</div>
{li.received_qty != null && li.received_qty > 0 && li.quantity != null && (
<div className="fb-receipt">
<Package className="me-1 inline h-3 w-3" />
رسیده: {fmtNum(li.received_qty)} از {fmtNum(li.quantity)} — در راه {fmtNum(Math.max(0, li.quantity - li.received_qty))}
</div>
)}
{conv && (
<div className="fb-conv">
<Scale className="me-1 inline h-3 w-3" /> ≈ {groupCommas(conv.qty, true)} {conv.unit}