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.
On /inventory/org two container rows could not shrink or wrap and
overflowed past the left edge on phones (RTL), forcing horizontal
scrolling:
- the add-warehouse form was a non-wrapping flex row of fixed-width
inputs (w-44 + w-52) plus the button (~500px total); it now stacks
vertically below sm (full-width fields and button) and returns to the
inline layout at sm+
- each warehouse row's action cluster (dashboard / move-project select /
delete) is now flex-wrap with justify-end, and the name block is
flex-1 so a long select option can no longer push the buttons out of
the card on narrow viewports
Desktop layout is unchanged; containerized rows stay within the page.
- «مشخصه فنی» in موجودی اولیه is now the same MultiSelectCombobox chip
control as the wizard's «مشخصه» (name-only chips, stored as spec keys
with empty values so stock variants stay separate).
- Unit combobox options deduped via uniqueUnits (canonical base preferred).
- Description/validation copy updated accordingly.
- PURCHASE rebranded to «موجودی اولیه» in the تغییر موجودی screen
(tab, description, empty-state copy; submit button reads
«ثبت موجودی اولیه» automatically).
- Material/brand/unit inputs of that form are now searchable Comboboxes
with «…به عنوان جدید» affordance; submitting an unknown name creates
the entry on the fly in the global catalogs (new apiV26.catalog.* →
POST /materials with null category, /brands, /units) so initial stock
no longer requires a procurement prerequisite.
- The hub timeline chip for PURCHASE stays «خرید» because real deliveries
from کالاهای در راه also post as PURCHASE.
The month-shift anchor discarded the day returned by jalaliToGregorian:
new Date(gy, gm-1, 1) instead of new Date(gy, gm-1, gd). Jalali months
start mid-Gregorian-month (مهر ۱ = 2026-09-23), so the anchor landed on
the 1st of the Gregorian month — still inside the SAME Jalali month
(شهریور ۱۰) — and «ماه بعد» appeared dead while «ماه قبل» only seemed
to work because its wrong target showed a different label. This was also
the source of the reported ~22-day date mismatches.
- shiftMonth now keeps the converted day (gy, gm, gd)
- nav buttons fire on pointerup/click/keyboard with a dedupe guard so the
action survives browsers that swallow the click event
- buttons got hover feedback (cursor-pointer, hover:bg-slate-100,
transition-colors)
Verified against the library math: from شهریور ۱۴۰۵, next → مهر ۱۴۰۵,
prev → مرداد ۱۴۰۵; confirmed in the deployed bundle.
- shared AppSwitcherMenu ('پنلها' title, active filled item with ✓ فعلی,
«مدیریت انبار»/«مدیریت تدارکات» switcher, divider + خروج از حساب) used by
both the procurement dashboard and the warehouse hub (PM variant keeps
logout-only); replaced the old raw «بازگشت به دفتر» link
- warehouse header mirrors the procurement pattern: icon + hamburger only
on the hub dashboard; sub-views (تغییر موجودی، کنترل روزانه، انبارها،
کالاهای در راه، سازماندهی) render a minimal header with just the title
and «بازگشت به داشبورد»
- removed the self-referencing «داشبورد انبار» tab from the hub strip
- removed the red per-day transaction-count badges from the daily-control
calendar (backend untouched — purely visual)
- removed the test subtitle «دادهها در پایگاه داده ذخیره میشوند» from the
procurement dashboard header
- procurement store refreshes right after a warehouse receipt (arrivals) and
after role/project changes (organization/admin), so the ledger reflects
warehouse actions immediately
- refetchOnWindowFocus enabled on the shared query client
- BroadcastChannel (lib/crossTab.ts): mutations broadcast 'quotes' /
'inventory'; all warehouse pages + the store hook resync open tabs
automatically (receipt in one tab updates the ledger in another)
- warehouse mutations (stock changes) notify other tabs; same-page refresh
behavior unchanged
- 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