- /panel/pm: the PM app (served on the same origin under /pm) is embedded as
a full-height iframe; supervisors are funneled there from every route,
ERP MobileTabBar hidden on that route
- Panels menu: «مدیریت پروژه (PM)» is an internal route (admin/pm/supervisor
only — procurement managers do not see it); PM link is no longer external
- New system role 'supervisor' (user_role enum): label in users router,
allowed in UserRoleUpdate schema, deny-by-default on all ERP routers;
admin panel can grant/revoke it alongside procurement manager
- Removed the obsolete VITE_PM_URL build plumbing (same-origin now)
Categories are kept only as the label each supplier carries (suppliers.category_id
+ the material_categories list it points at). Everything that treated category as
a shared key across materials, inventory, quotes and unit conversions is removed —
including two features that were never populated (0 rows on every volume).
Removed (backend):
- routers category_attributes, category_ref_units, unit_conversions + registration
- ?category_id= filters on /materials, /inventory and /quotes (the /suppliers one
stays — it filters suppliers by their own label)
- GET /inventory/materials/options: required a category and had no caller
- the material-payload category derivation (expand_service.category_by_material,
the inventory serializer lookup) and the material category on /pm/on-the-way;
the wire keys stay, now always null, so the payload shape is unchanged
- startup DDL: DROP the empty category_attributes / category_reference_units /
unit_conversions tables (idempotent) and their CREATE statements; seed.py
truncate list updated
Removed (frontend):
- the category-keyed conversion path: refUnitForCategory / findConversion /
refConverted, the categoryUnits + unitConversions store slices, api clients,
hooks and the "≈ converted quantity" line on quote detail and project pages
(it needed rows in those tables to render, and there were none)
- material.supplier_category and its read sites (the edit modal grew one
yesterday to restore labels; the label lives on the supplier now)
- the inventory page's category chips, the row filter and the «دسته» column,
plus the lookup's categories fetch and catName helper
Kept: supplier label + its list and picker, /suppliers?category_id=, the global
standard_conversions feature and the units page (material_units conversions).
Verified against a restore of the production dump: startup DDL drops exactly the
three tables, suppliers keep their label (3/3), material_categories still serves
its 8 rows, the removed endpoints 404, and materials/inventory/quotes/units/
standard-conversions all return 200 with quote line items intact.
Task 1 — PM 1:N + access-loss handling:
- projects.pm_user_id had no UNIQUE constraint (verified live + in schema); added a lookup index via init_db
- auth: UserContext.project_ids = every project of a PM (oldest first); project_id stays the first for compatibility
- GET /users/me/projects — assigned projects of the caller (drives the switcher + revocation validation)
- login / /users/me / /users list now return the full projects array per user
- /inventory/* and /pm/* scopes accept an explicit ?project_id=… and verify the PM manages it (403 otherwise); without the param they fall back to the first project; admin/manager use it as an optional filter
- POST /users/{id}/project is multi-aware: assigns one project (replacing that project's previous PM) and KEEPS the user's other assignments; null clears all
- PATCH /projects/{id} accepts pm_user_id (admin / procurement manager only)
- frontend: auth store holds projects + the active project (persisted via the existing localStorage store); RequirePmProjects re-validates on load, falls back when the active project was revoked, and shows a dedicated no-projects screen with logout (no redirect loop, no re-auth)
- Panels menu: «مدیریت انبار» gets a ChevronDown accordion listing the PM's projects with «✓ فعلی»; selecting one switches only the active project and reloads the section data
- inventory/pm API calls now send the active project_id explicitly
Task 2 — Organization page:
- new searchable ProjectMultiSelect combobox (chips with ×, live Persian filtering via normalize, assigned projects hidden) replaces the single select for PM users
- add = PATCH project pm_user_id=<user>; × = pm_user_id=null; table and project cards sync immediately; RBAC unchanged (role column admin-only, combobox editable for admin + procurement manager)
Verified locally: 17 scripted checks — multi-project assignment without unique conflicts, /users/me/projects, explicit project_id authorization (foreign project 403 on /inventory, /pm/on-the-way, /pm/receipts), PM blocked from pm_user_id changes, legacy assign endpoint keeps other assignments, revocation removes the project from the list, zero-project PM gets an empty list and a guarded inventory endpoint. All test data cleaned up afterwards.
- bug1: quote header fulfillment counter («رسید: X از Y قلم» amber / «تکمیل
شد» green / «در راه»); expanded line items visually split into the
received part (رسید شده ✓ + date + target warehouse from the receipt
allocations) and the in-transit remainder — quantities always sum to the
original line (invariant D holds, display-only split)
- bug2: 8-topic grids (suppliers/wizard/units) now come from
/material-categories via the store (new lib/topics.ts; fixed the mismatched
«ابزار و تجهیزات ساخت» label); suppliers topic->category resolution no
longer falls back to an arbitrary category; duplicate month/status lists
reuse the shared dictionaries; TopicGrid gained a loading EmptyState
- bug3: hub-and-spoke — the tab strip renders only on the warehouse
dashboard; sub-views hide it and navigate back via «بازگشت به داشبورد»;
«افزایش موجود» became the «تغییر موجودی» entry button
- bug4: stock count goes through a confirmation modal (current vs counted
vs variance with کسری/مازاد badges) before posting STOCK_COUNT; numbers
parsed defensively
- bug5: found qty is capped client-side (red input + inline warning, submit
disabled) and backend returns 422 when exceeding the missing balance;
fully-recovered losses drop off the pending list
- bug6+7: quantities render as absolute values with semantic color coding
(emerald inflow / rose outflow / blue neutral) across dashboard timeline,
daily control and the change-inventory form
- redesigned /login (Tailwind, RTL): centered username/password/organization
form posting to POST /api/v1/auth/login — session starts without leaving
the page; PM lands on the inventory panel, others on the procurement office
- Developer Mode button fixed top-left opens a role/project test panel
(admin role added) gated by VITE_ENABLE_DEV_LOGIN (DEV_LOGIN_ENABLED build
arg in Docker, default enabled)
- removed the old Casdoor redirect button, the PKCE ?code= callback, and the
public casdoor-config.js — the client secret is no longer shipped to the
browser; backend mediates the password grant instead