Commit Graph
113 Commits
Author SHA1 Message Date
fatemeh sane 7bb432e715 feat(dashboard): supporting schema, seed, catalog links, funnel/PM labels and API contract (merge resolution) 2026-10-08 11:18:57 +03:30
fatemeh sane d84637769a fix(dashboard): re-apply filter clear/toggle, 3-level drill-down, Persian formatting + wire /dashboard route 2026-10-08 11:18:47 +03:30
fatemeh sane b0776da061 chore: restore MVP dashboard components from backup 2026-10-08 11:16:15 +03:30
MMD dcc4d693f5 fix(frontend): declare pathname alongside location in RequireAuth (pm merge semantic conflict) 2026-10-06 15:47:32 +03:30
MMD 64bd292335 merge: pm (unified access matrix, limited procurement flow, platform rebrand)
# Conflicts:
#	frontend/src/App.tsx
#	frontend/src/pages/Auth/LoginPage.tsx
#	frontend/src/pages/Wizard/WizardPage.tsx
2026-10-06 15:45:23 +03:30
MMD 98c4b28815 merge: prod-v10 (prod-v8 + prod-v9 consolidation, list-based ChangeInventoryPage + return ledger)
# Conflicts:
#	frontend/src/pages/Inventory/ChangeInventoryPage.tsx
2026-10-06 15:41:28 +03:30
MMD 965054093e fix(frontend): repair merge-damaged syntax left by the pm/RBAC revert cycle
Stray braces in App.tsx / ChangeInventoryPage.tsx, an orphaned admin gate in ArrivalsPage.tsx, and a literal revert commit subject pasted into LoginPage.tsx — all made the committed tree unbuildable.
2026-10-06 15:39:43 +03:30
whqreza 3931baadd5 feat(inventory): list-based ChangeInventoryPage + mobile-first UX + return ledger controls
- All ChangeInventoryPage sections (withdrawal/damage/waste/loss/transfer/
  stock-count/project-transfer) become list-based like the withdrawal
  section: pick a warehouse, see its stock rows, material name is only an
  optional filter; manual material/brand/unit entry removed everywhere
  except PURCHASE.
- RETURN redesigned: day-range presets (today/yesterday/7d/30d + Jalali
  custom range) with server-side filters, immutable-ledger returnable
  remainder per withdrawal (REF:[tx-id] note tag + FIFO attribution for
  legacy returns, round4 float-safe), destination warehouse selector.
- PROJECT_TRANSFER: row-based list with one destination project per batch;
  sequential submits keep failed rows inline with errors and drop
  succeeded rows; history dashboard untouched.
- PURCHASE: unit locked to catalog (search-only Combobox), wizard-style
  '+ register new material' option with yellow warning, optional Key-Value
  specs editor (replaces mandatory text chips), continuous-entry UX
  (warehouse/date kept, auto-focus, session log, duplicate warning).
- Mobile-first: responsive table/cards core (StockTable), numeric keyboards
  (inputMode+pattern), sticky bottom submit bar with dynamic counter above
  MobileTabBar incl. safe-area inset, horizontally scrollable touch chips.
- Backend fix (prereq of RETURN): GET /inventory/transactions date filters
  crashed with 500 under asyncpg (string vs timestamptz); parse to aware
  UTC datetimes (DB runs Etc/UTC).
- docs/inventory-ledger-gaps.md: ledger gap report (server-side return cap,
  standalone TRANSFER_IN, transfer row fallback, audit coverage, legacy
  REF backfill, UTC day semantics, pagination gaps).
2026-10-03 21:56:46 +03:30
whqreza 1bf55ba8a6 merge: integrate prod-v8 (inter-project transfers + multi-project allocation)
Conflict resolved in ChangeInventoryPage.tsx (canSubmit): kept both the
WITHDRAWAL branch (prod-v9: delivered-to validation) and the
PROJECT_TRANSFER branch (prod-v8: inter-project transfer validation).
All other files auto-merged; both feature sets fully preserved.
2026-10-03 19:43:24 +03:30
Milad df3ff4ea14 feat(erp-pm): integrate unified access matrix, streamline limited procurement flow, and brand as integrated project management, procurement and warehouse platform 2026-10-02 03:47:33 +03:30
MMD ea6a377023 Merge feature/show-material-suppliers into the all-branches integration
Conflict in supplier_materials.py resolved by keeping HEAD's auth
(_procurement_writer) and list route while adopting the branch's UUID
parameter annotations.
2026-10-01 15:44:12 +03:30
MMD 5400588751 Merge pm (panel-based access, supervisor role) onto the restored RBAC base
Reverting 253c922 first made this merge small — only two conflicts:
- MobileTabBar: supervisor null-check combined with v7's /review hiding
- ArrivalsPage: pm's PM+supervisor scoping combined with v8's 3-way fetch
  (transfers) and its transfer-receive helpers, plus pm's canAccess gate
2026-10-01 15:36:57 +03:30
MMD 0ea0322967 Revert 253c922: restore the PM/RBAC work to the integration
Reverting the production revert so the pm branch can merge cleanly on
top of the restored RBAC base. Conflicts against newer integration work
resolved by combining:
- App.tsx: v7's bot-draft wizard bypass + login returnUrl kept alongside
  the supervisor gating (no-project block screen, panel scoping)
- LoginPage: returnUrl navigation kept, supervisor routing added
- migration.sql: bot_drafts table kept, unified-projects ALTERs restored
2026-10-01 15:24:56 +03:30
MMD a84b33dde3 Merge prod-v8 (multi-project allocation, inter-project transfers) into the all-branches integration
Conflicts (all keep-both):
- init_db.py: v7 added ensure_bot_drafts_table, v8 added
  ensure_project_transfers_table at the same anchor — both functions and
  both startup calls kept
- main.py: same, both ensure calls kept
- ChangeInventoryPage.canSubmit: v9's WITHDRAWAL branch and v8's
  PROJECT_TRANSFER branch — both validation branches kept
2026-10-01 15:05:09 +03:30
MMD fbd34277c0 Merge remote-tracking branch 'origin/prod-v7' into integration-all 2026-10-01 14:43:02 +03:30
fatemeh sane 8c5f4ab770 feat: show material suppliers 2026-10-01 11:16:46 +03:30
Behrad af092fe4c8 fix(dashboard): refine accept modal item allocation layout and live persian number words 2026-09-30 11:38:50 +03:30
whqreza 07643dbdf5 feat(bot,wizard): smart message filtering, encrypted standalone wizard, and file persistence 2026-09-30 00:49:04 +03:30
Milad e9fef5a104 feat(pm-panel): pm panel level gating — RequirePmPanel/HomeRoute/menu honor panels.pm (full/limited); pm-only users land directly in the PM panel 2026-09-29 13:45:37 +03:30
Milad 4a3c226518 feat(org): panel-based access system replacing roles (procurement/inventory)
- new tables user_panels (panel: procurement/inventory/pm + level full/limited)
  and user_project_members (M:N membership, separate from pm responsibility)
- UserContext carries panels + member_project_ids; /users/me and the users
  list include them
- admin endpoints: PUT /users/{id}/panels, PUT /users/{id}/panel-projects
- inventory scope: panel inventory 'full' -> manager scope (all projects, NO
  stock change); 'limited' -> supervisor scope (own memberships, stock change
  allowed); legacy roles kept as migration fallback; admin untouched
- procurement writes: _procurement_writer is now panel-aware (admin, legacy
  procurement_manager, or procurement 'full') across all 18 write routers;
  quote CREATE additionally allows 'limited' users restricted to their member
  projects (validated server-side)
- frontend: panel-aware HomeRoute (inventory -> warehouse panel, procurement
  limited -> minimal RequesterHome), wizard project options restricted for
  limited users, change-inventory button/route gated to inventory 'limited',
  organization panel gains the panels & access-levels management section
2026-09-29 13:37:59 +03:30
whqreza a2e7c27e85 feat(bot): support concurrent drafts and reply-based edits with error resilience 2026-09-29 00:48:08 +03:30
whqreza 57e0e51c16 merge: incorporate origin/production updates into prod-v7 2026-09-28 23:40:24 +03:30
whqreza d85ede7d11 feat(inventory): display all warehouse items on selection and track withdrawal recipient
- Show all available inventory items immediately when selecting a warehouse in withdrawal form
- Add vertical scrolling container with max height to prevent layout overflow for large item lists
- Add Material column to the inventory rows table for clear item identification
- Add required 'delivered_to' recipient field in withdrawal form with frontend validation
- Track delivered_to in database, models, schemas, and API
- Display recipient badge on withdrawal cards in daily control page
- Group daily control withdrawals by inventory ID and recipient
2026-09-28 22:54:02 +03:30
Milad c7d860f431 feat(inventory): limit supervisor access to own project inventory and remove rectangular change inventory icon 2026-09-27 18:42:05 +03:30
ZCode 253c922280 revert: restore production to the state before 730d0bd (PM/RBAC work moved to 'pm')
Their three commits (730d0bd, 5a9b543, 387edfb) reached production by push and
auto-deployed. 387edfb expects a users.username column that the live database does
not have, so POST /api/v1/auth/login was returning HTTP 500. Their schema changes
never applied either (projects still has only id, name, description, start_date,
pm_user_id, created_at), so nothing depends on the reverted code.

This commit sets the tree back to exactly a02b3ea — verified with git diff against
it — while keeping history linear: the work itself is preserved on 'pm' at 387edfb.
2026-09-26 17:41:07 +00:00
Milad 387edfb1ab feat(org,rbac): dedicate organization panel, enforce role permissions, subheader info, and fix supervisor project sync 2026-09-26 18:51:37 +03:30
Milad 5a9b543040 feat(rbac): enforce procurement write restrictions and support supervisor role in SSO 2026-09-26 12:32:01 +03:30
Milad 730d0bd496 feat: PM panel under ERP shell + project-supervisor role
- /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)
2026-09-26 01:39:48 +03:30
whqreza b2aba2fcfe fix(inventory): enable smart warehouse stock binding and optional brand/specs for inter-project transfer 2026-09-23 23:11:18 +03:30
whqreza e560018273 fix(inventory): envelope transfer responses and add defensive array guards in UI 2026-09-23 22:46:07 +03:30
whqreza 856513b6d7 feat(frontend): integrate inter-project material transfer into ChangeInventoryPage and ArrivalsPage
- Add PROJECT_TRANSFER tab under reductions in ChangeInventoryPage
- Render complete material spec form with destination project selector
- Show matching stock preview in source warehouse
- Add transfer history table with in-transit edit modal and cancellation dialog
- Add incoming transfer cards in ArrivalsPage with 100% full delivery allocation
2026-09-23 22:31:10 +03:30
whqreza da883a28c0 feat(dashboard): add multi-project allocation UI in AcceptModal and merge on reopen in EditModal
- Add allocations field to LineItem and propagate in api.quotes.update
- Implement allocation cards in AcceptModal Section 3 with per-row project select and sum validation
- Disable previously selected projects within the same item to prevent duplicates
- Merge split items client-side in EditModal when transitioning status to offered
2026-09-23 22:02:49 +03:30
MMD a02b3ea7cc fix(edit-modal): restore the proforma upload section
The proforma FileList existed in prod_v5 (a181a87) but was dropped when the
prod_v2 EditModal redesign (de177f3) was merged — that branch predates the
feature and its version of the file was taken wholesale to keep the new
tabbed layout, silently discarding the proforma block.

Restores the state, the load effect, the saved patch field and the upload
UI (on the تاریخ tab, next to the invoice card), using the current
authenticated download/preview helpers rather than the removed urlOf.
2026-09-23 22:01:14 +03:30
Behrad 49dd159fb1 feat: improve line items layout, number inputs and supplier click-to-call 2026-09-22 23:06:17 +03:30
whqreza 4cfb9ec6f6 feat(wizard): add real-time bi-directional sync, debounced auto-save, and bot draft prefill 2026-09-21 20:22:12 +03:30
Behrad 972b1b49cb fix(dashboard): restore expand toggle on mobile quote cards and add explicit expand action bar 2026-09-21 19:57:15 +03:30
Behrad b176f0f5ff fix(wizard): optimize stepper and line items layout for mobile 2026-09-21 19:39:12 +03:30
Behrad 9386d4846f feat: add lead time badge, refine mobile supplier click, and integrate termsheet cards in edit modal 2026-09-21 19:26:52 +03:30
ZCode 34290b9371 fix(analytics): declare the script build arg and send the Track API's real envelope
Two blockers found while wiring OpenPanel for real:

- frontend/Dockerfile declared ARG for VITE_OPENPANEL_URL/CLIENT_ID/SENTRY_DSN but not
  VITE_OPENPANEL_SCRIPT_URL, so passing it as a build arg was silently ignored and the
  loader compiled out (import.meta.env value undefined).
- backend/app/services/analytics.py posted a flat {type, event, properties} body; the
  Track API expects {type, payload:{event, properties, profileId}} (verified against the
  SDK source in packages/sdks/sdk). The API answered 400 and the module swallows every
  error by design, so all server-side events were being dropped without a trace.

Verified after deploy: the bundle carries the client id + op1.js URL + the queueing shim,
op1.js serves 200, and a track event posted with the real envelope lands in OpenPanel.
2026-09-21 09:15:02 +00:00
ZCode cff35e3660 fix(analytics): actually load the OpenPanel browser script
analytics.ts called window.op?.(…) but nothing ever defined that global — no script tag
in index.html and @openpanel/web was never imported — so every call was a silent no-op
and product analytics would have stayed empty no matter how the build args were set.

Now, when VITE_OPENPANEL_SCRIPT_URL is configured, the loader installs OpenPanel's
documented queueing shim (window.op becomes a queue) and appends op1.js async; the
existing op("init"/"identify"/"track") calls are queued and drained when it loads, so
ordering cannot break them. Without the URL the behaviour is unchanged (no-op), which
keeps local dev and air-gapped builds silent.

Build args: VITE_OPENPANEL_URL (API base, e.g. https://stats.<host>/api),
VITE_OPENPANEL_CLIENT_ID, VITE_OPENPANEL_SCRIPT_URL (e.g. https://stats.<host>/op1.js).
The client *secret* is deliberately not a frontend arg — it belongs to the API only.
2026-09-21 09:13:27 +00:00
ZCode b469238153 refactor(catalog): reduce the category taxonomy to a supplier label
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.
2026-09-20 19:44:23 +00:00
ZCode 2fd72fecf7 feat(dashboard): drop the category filter
The category filter is not used: remove it end to end — the toolbar's category
combobox (desktop + mobile drawer), its option list, the active-filter count and
the filterQuotes() clause it drove, plus the now-unused `category` field in the
Filters type. Supplier and status filters are unchanged.

The backend's own quote filtering by category is untouched (it filters on the
quote's supplier category when ?category_id= is passed), so the API keeps its
contract; only the dashboard UI stops offering the filter.
2026-09-20 19:25:19 +00:00
ZCode ae62a0391b fix(catalog): resolve material category through the linked supplier (v2.7 follow-up)
v2.7 dropped materials.category_id and the API started returning category: null for
every material, but the frontend still read material.category as the material's label.
Effect: the edit page's material field rendered blank for every row, the dashboard
category filter listed nothing (and matched nothing), the wizard's category suggestions
were empty, and the conversion lookups keyed by category (refConverted →
categoryUnits/unitConversions) silently returned null, dropping the per-reference-unit
price. Saving an edited quote matched materials on an always-true category term, so two
materials sharing brand+specs were interchangeable.

Auditing the dropped column also turned up four places still dereferencing it, one
request away from a 500 and hidden only because the inventory is empty:
GET /inventory (serializer), GET /inventory?category_id=…, GET /inventory/materials/options
and DELETE /material-categories/{id}.

Backend:
- expand_service: category_by_material() resolves a material's display category from its
  FIRST linked supplier (oldest supplier_materials row — the v2.5 trigger's rule), used by
  the material and line-item payloads; category_id is populated again when not expanded.
- inventory: list filter, row serializer and material-options filter match through
  supplier_materials ⋈ suppliers, mirroring GET /materials?category_id=…
- material_categories: the delete guard blocks on suppliers (the RESTRICT FK) and says so.

Frontend:
- Material.category → Material.supplier_category (supplier-derived, documented); the
  rename pushed every read site through tsc.
- Labels use the material's own name; the edit modal field is «متریال» and prefills by
  name; formatMaterialTitle no longer falls back to a category.
- Identity matching (edit modal, wizard, material memory) is name + brand + specs via a
  shared matchMaterial(), so same-brand same-spec materials are not conflated; legacy
  composed names are still found through materialBaseName().
- Filters read the supplier's category (dashboard options + quote filter, consistent with
  the backend's quote filtering); supplier categories now come from the supplier FK
  instead of the removed JSONB column. Conversion keys use supplier_category.
- Removed dead code carrying the old logic: two uncalled topicCategories copies and
  materialOptions' unused "cat" branch.

Verified: both images build clean (tsc -b && vite build); the API was exercised in-process
against a restore of the current production dump with an inventory row added — material
and line-item payloads carry the supplier category, the inventory endpoints that used to
raise return 200, and the category delete guard returns 409 instead of 500.
2026-09-20 18:51:29 +00:00
MMD 7057ac4f96 fix(suppliers): read the supplier's own category instead of inferring it
Supplier topic/categories were derived from quote line items' material
category and topic. The catalog v2.7 refactor decoupled materials from
categories, so that derivation now yields nothing and every supplier lost
its topic/category — the suppliers dashboard rendered without names,
because it groups topic → category → supplier.

The backend already returns the supplier's own category
(category_id FK + nested category{id,slug,name}); adaptSupplier simply
never read it. Now it is the primary source, with quote-derived values and
the local registry still taking precedence when present.

Verified against the live stack: all 7 suppliers return their category
from /suppliers, and the suppliers page renders names again.
2026-09-20 21:01:16 +03:30
MMD 36414863d4 Merge remote-tracking branch 'origin/prod_v2' into integration 2026-09-20 20:35:19 +03:30
MMD baa32a2652 feat(files): preview attachments via short-lived signed URLs
Downloads require an Authorization header, which <iframe>/<img> cannot
send. The previous blob: workaround broke under download managers (IDM
hooks blob URLs, fails to resolve them, and leaves a 0-byte file) and in
Chrome's mobile emulation, which has no inline PDF plugin.

Backend:
- preview_token.py: HMAC-signed token bound to one (quote, kind, file)
  with a 5-minute TTL, keyed off an existing secret
- GET /quotes/{id}/preview-url/{kind}/{attachment} mints the URL
- the two file GET routes accept a Bearer session OR the signed ?t= token
  (they leave the global auth dep, so the other six file routes now carry
  it explicitly — nothing is open by omission)

Frontend:
- api.files.previewUrl() replaces the blob helper
- shared FilePreviewModal + useFilePreview; FileList and the dashboard
  quote card both use it
- PDFs render with the browser's native viewer on desktop and pdf.js on
  canvas where no inline viewer exists (mobile); pdf.js is fed the bytes
  in memory so no PDF URL is exposed for a download manager to intercept
- removed the per-attachment download buttons on the dashboard card and
  the modal header

Ops:
- nginx: explicit MIME types (.mjs as text/javascript) — a module worker
  served as octet-stream is refused and crashed the page
- frontend service bypasses the nginx entrypoint, whose apk manifest call
  hangs when the package mirror is unreachable (502s)
2026-09-20 20:35:16 +03:30
Behrad 14a4a29431 feat(procurement): sync status pipeline cards, add supplier lead time badges, and enhance badge contrast 2026-09-20 15:10:10 +03:30
Behrad d2b9af5278 feat(dashboard): filter quotes by clicking status cards in StatsBar 2026-09-20 14:40:27 +03:30
MMD e637d80b76 Merge latest prod_v2 (FileList preview modal, EditModal tabs) into integration
prod_v2's inline preview modal and EditModal redesign conflicted with the
hardening branch's authenticated file API (urlOf/downloadUrl were removed
because <a href>/<img> cannot send the Authorization header).

- FileList: kept prod_v2's design (icons, preview modal, per-row actions)
  and adapted it — previews now fetch the bytes with a Bearer token and
  use a local objectURL (api.files.blobUrl), with revoke on close/unmount;
  the row download uses the authenticated onDownload.
- EditModal: took prod_v2's tabbed redesign wholesale (it already carried
  our globalSpecs, clean material name and target_project work) and
  converted its FileList to onDownload + loadPreview.
- AcceptModal: converted the remaining urlOf sites and removed duplicated
  file/date blocks the earlier merge had left outside the scroll body.
- api.ts: restored the global handle401 (definition + all four fetch
  sites) alongside the new blobUrl helper.
2026-09-20 14:23:19 +03:30
Behrad 3c7ae80b02 feat(ui): add inline preview modal for invoice files in FileList 2026-09-19 20:09:27 +03:30