Commit Graph
63 Commits
Author SHA1 Message Date
MMD 50f8d8e64e feat(ui): modernize button design system and update Dashboard and Inventory pages
Frontend-only cherry-pick of prod_v2 3bdb05f; the dev compose
api port change (8000 -> 8001) is intentionally excluded.
2026-09-13 00:13:57 +03:30
whqreza 3b032d0460 idk anymore what going on...! 2026-09-10 11:52:48 +00:00
whqreza f263cfb9e3 update docker for coolify 2026-09-10 10:45:18 +00:00
whqreza 4609940856 fix(infra): make the Coolify compose file Linux-safe
docker compose config absolutized bind mounts to the Windows dev path
(C:\Users\Reza\Desktop\...), which cannot exist on the Linux Coolify
server and broke parsing. Also normalized the expanded structures back
to compact, Coolify-friendly syntax:

- bind volume sources are now repo-relative (./sql/..., ./infra/...)
- long-syntax volumes (type/source/target/volume objects) -> short
  'src:target[:ro]'
- long-syntax ports (mode:ingress/target/published/protocol objects) ->
  short 'host:container' strings
- dropped the compose-generated top-level 'name:' key

Verified: 'docker compose -f docker-compose.coolify.yml config' resolves
hosts, TLS, and healthchecks with no warnings; minio-init's escaped
shell variables ($${MINIO_*}) are preserved.
2026-09-08 19:51:55 +03:30
whqreza 3c3880aec9 fix(infra): point PROCUREMENT_CASDOOR_ISSUER at the public auth host
The base file bakes http://localhost:8080 as the JWT issuer; casdoor's
production origin is https://${AUTH_HOST}, so token iss checks would
reject every login. The merged Coolify file now derives the issuer from
${AUTH_HOST} like the casdoor origin.
2026-09-08 18:10:37 +03:30
whqreza 545b5dd473 chore(infra): add merged compose file for Coolify deployment
Coolify applies a single self-contained compose file, but the repo splits
production config across docker-compose.yml (base) and
docker-compose.prod.yml (overlay). This is the result of

  docker compose -f docker-compose.yml -f docker-compose.prod.yml config

with build contexts made relative and the production host variables kept
as ${APP_HOST}/${API_HOST}/${AUTH_HOST}/${S3_HOST}/${STORAGE_HOST}/
${TRAEFIK_ACME_EMAIL} placeholders for Coolify's environment settings
(minio-init's $${MINIO_*} shell escapes are preserved).

Verified: 'docker compose -f docker-compose.coolify.yml config' renders
Host(...) routers, the Let's Encrypt resolver email, and the casdoor
origin (https://${AUTH_HOST}) from sample values.
2026-09-08 18:10:10 +03:30
whqreza 7528cc3bbe chore: ignore the machine-local compose override
docker-compose.override.yml publishes the frontend on an extra host
port for LAN/phone access; it is machine-specific, so compose keeps
auto-loading it locally while other machines' stacks stay unchanged.
2026-09-08 16:04:05 +03:30
whqreza b45a6cd033 fix(inventory): keep Organization page actions within mobile bounds
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.
2026-09-08 16:04:02 +03:30
whqreza f0e424316e fix(web): remove the mobile bottom navigation bar
The fixed bottom tab bar (MobileTabBar) was rendered app-wide on
screens <768px, duplicating navigation that already exists in the
header/hamburger menus.

- unmount <MobileTabBar /> in App.tsx and drop its import
- delete components/layout/MobileTabBar.tsx (no remaining references)
- drop the max-md:pb-24 clearance in PageLayout (Header.tsx) that only
  existed to keep page content above the removed fixed bar

Desktop and header-menu navigation are unaffected; mobile pages keep a
full-height viewport without dead bottom padding.
2026-09-08 16:03:56 +03:30
whqreza c10c10a425 data: refresh casdoor dump (org users: razaei, mousavi, mohaghegh, tester) 2026-09-08 15:07:58 +03:30
whqreza f8deb0c6b9 fix(quotes): show latest receipt time on partial arrivals
For accepted quotes with PM-confirmed (partial) receipts the dashboard row
only showed «رسیده X از Y — در راه Z» with no timestamp (quote header
arrived_date is set only when everything arrives). The cell now also shows
«آخرین رسید: <date time>» computed from the newest item receipt; arrived
quotes keep the arrival date with a last-receipt fallback.
v1.5
2026-09-08 14:51:36 +03:30
whqreza c64718ef70 fix(quotes): show real material names instead of category in accept modal & detail
Line-item labels in the accept modal («پروژهی هر قلم» and the keep/reject
list) and the quote-detail flow boxes printed category + brand (e.g. the
topic name). Both now use materialLabel() → the material's actual stored
name, consistent with the rest of the dashboard.
2026-09-08 14:51:36 +03:30
whqreza 529eb67d01 feat(warehouse): wizard-style spec chips + unique unit options
- «مشخصه فنی» 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.
2026-09-08 14:42:24 +03:30
whqreza eb1043cc3a feat(quotes-accept): single-project select per line item
Replaces the MultiSelectCombobox project picker in the accept modal with a
single-select dropdown styled like the payment-terms select. Each kept item
defaults to its previous project (or the quote's first), carries exactly one
project into item_projects AND into the decision payload on accept —
matching the backend's single purchased-project model.
2026-09-08 14:42:24 +03:30
whqreza dbe53a727c fix(wizard): global material suggestions + unit create-option fixes
- متریال field suggests unique BASE material names from the whole catalog
  (brand/spec prefixes stripped; «راجین سیم مسی» → «سیم») — brand/specs
  have their own columns.
- Save path reuses an existing material by full name only (no category
  equality constraint, which never matched topic-named categories) so
  selecting an existing material never duplicates it.
- Unit field: suggestion list no longer includes the row's own typed text,
  which previously suppressed the create option; typing an unknown unit now
  shows «+ [value] به عنوان واحد جدید» (registered immediately via
  POST /units and added to suggestions).
2026-09-08 14:42:16 +03:30
whqreza 58ff8dc496 feat(units): global-material conversion workspace (no category landing)
- «ضرایب تبدیل» opens directly on the global materials list (no topic/category
  page); sidebar accordion: unique base materials (brand/spec prefixes
  stripped) with expandable spec variants; search toolbar filters live.
- Detail binds to the selected material: reference unit (editable combobox
  over global units, changes canonical_unit_id), conversion factors from
  material_units incl. optional brand scope (restored برند field), add/edit/
  delete with live preview (canonical-first wording), unassigned-units
  warning section, global standard conversions kept.
- Self-referencing children excluded (only base+spec variants listed);
  groups without variants selectable via their representative material.
- New lib/units.ts uniqueUnits(): unit dropdowns show each name once,
  preferring the canonical coded row (kg/piece) over duplicate custom rows
  with identical Persian names.
2026-09-08 14:42:16 +03:30
whqreza 8e2afcec59 fix(api): no hardcoded unit defaults; material_units/brand/catalog APIs; accept sends decision project
- Remove all || «عدد» / || "unit" fallbacks: materials/line items without
  an explicit unit now fail with a clear Persian error instead of silently
  creating an «عدد» unit row (POST /units happens only for typed names).
- unitIdByNameOrCreate/createUnit guard empty names; code = normalized
  typed name only.
- Add api.materialUnits (list/create/update/remove with brand_material_id),
  api.brandMaterials (list by material + link), materials.updateCanonicalUnit,
  apiV26.catalog.* unit/brand/material creation helpers (shared usage).
- Accept payload: each chosen item_decision now carries the selected single
  project_id so backend can bind decision.project_id (PM on-the-way
  segregation).
2026-09-08 14:42:06 +03:30
whqreza e8154ae6a6 fix(quotes): derive decision.project_id from accept item_projects
Multi-project quotes: the manager picks one project per line item in the
accept modal (item_projects), but accept_quote only auto-filled
decision.project_id from existing line_item_projects (≤1). For items
without a single existing line-project the decision stayed NULL, so
/ pm/on-the-way (filtered by ItemDecision.project_id) never showed those
items to any project manager — they accumulated only in the global admin
list. Now derivation order is: explicit decision.project_id →
payload.item_projects (single) → existing line_item_projects (single) →
NULL (office-only, never misattributed). Verified E2E: two items of one
quote accepted to two projects appear in exactly their own PM's
on-the-way.
2026-09-08 14:41:57 +03:30
whqreza 7f31fb5105 fix(materials): remove nested db.begin — material PATCH was 500
The router loads the material row (starting a transaction) before calling
material_service.update_material, which then opened its own
async with db.begin() → SQLAlchemy 'A transaction is already begun on
this Session' → INTERNAL_ERROR on every PATCH (incl. changing the
reference unit from ضرایب تبدیل). Mutations now run inside the request
transaction committed by the get_db dependency.
2026-09-08 14:41:57 +03:30
whqreza 468ce64d29 feat(inventory): «موجودی اولیه» rebrand + create-on-the-fly global catalog entries
- 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.
2026-09-06 19:03:42 +03:30
whqreza fd1831b3fc fix(catalog): show real material names, not categories, across quote views
New shared materialLabel() renders the global materials.name with brand/specs
appended only when not already part of the name (deduped; «؟» fallback kept).
Replaces the category-first composer in five places: supplier card badges
(CategoryPage), dashboard item lines (QuoteCard), ledger items summary,
project rows and purchase history. Also fixes the stale Units subtitle
«ضریب تبدیل همان دسته» → «ضریب تبدیل متریال» (category model kept).
2026-09-06 19:03:35 +03:30
whqreza 1f123a0a05 fix(web): blank-screen hardening — stale chunk recovery, error boundary, no-cache index.html
- main.tsx: on vite:preloadError (stale index.html referencing removed
  hashed chunks after a redeploy) reload the page immediately.
- New ErrorBoundary around the whole router: render errors show a Persian
  fallback with «تلاش مجدد» instead of wiping the DOM.
- nginx: index.html served with Cache-Control no-cache,no-store,
  must-revalidate (verified live); /assets/* stays public, immutable.
2026-09-06 19:03:27 +03:30
whqreza 4dd60d7cd9 fix(quotes): lock status on arrived quotes + trim modal helper copy
- Edit modal: for arrived (رسیده) quotes the status select is disabled with
  «به دلیل ثبت ورود اقلام به انبار، امکان تغییر وضعیت پیشنهاد وجود ندارد.»
  so no rollback to accepted/offered is possible; metadata stays editable.
- Remove superfluous helper subtexts: parenthetical after «فاکتور رسمی»
  (wizard step 1) and the MinIO upload explanations under the
  invoice/proforma sections (Edit + Accept modals).
2026-09-06 19:03:19 +03:30
whqreza 3bdb0027f3 fix(quotes): quote date (تاریخ پیشنهاد) defaults to current time, not midnight
Wizard Step 5 initializes the quote date with the client's current local
time instead of 00:00, and the Jalali datepicker's hour/minute controls
default to the current clock whenever no explicit time is configured
(clicking «امروز» already used the current time). Date-only consumers
are unaffected (times are stripped at the API layer).
2026-09-06 19:03:10 +03:30
whqreza c5bbbbbaaa fix(wizard): allow global materials without a canonical category
Quote creation was blocked for any new material whose typed name is not
one of the 8 canonical category names: api.materials.create threw
«دسته ... در سامانه تعریف نشده است» and the failed create cascaded a
bogus temp material_id into the quote payload (invalid-payload error).

v2.5+ model: materials are global (name UNIQUE) and the category
attaches via the supplier (first-link-wins). The backend already
accepts category_id=null (MaterialCreate schema + material_service).
This removes the frontend-only hard block: unknown category names now
create the material with category_id null, and the category is derived
automatically from the first linked supplier on quote save. The 8
canonical categories are still never auto-created (no rogue-category
regression).

Verified end-to-end through the real API: create «سیم» (category null)
→ supplier → quote via material_id → 201/201/201 with
material.category_id derived from the supplier; lint + build green.
2026-09-06 15:39:09 +03:30
whqreza 93230d367c docs: remove stale root NewERD.txt (superseded by docs/NewERD.txt v2.6) v1.4 2026-09-03 19:26:45 +03:30
whqreza 9e2089e853 feat(mobile): mobile-responsive UI — bottom tab bar, bottom-sheet dialogs, PWA
All changes are gated behind max-md:/md:hidden so the >=768px desktop layout is untouched (verified with lint + build + live checks at 375px).

- globals.css: one mobile block — 16px inputs (no iOS auto-zoom), units sidebar stacks above content, wizard item rows become 2-col (text fields full-width, delete button full-width), dd-panels / flow diagram / conv-form stack, larger touch targets
- ui/dialog.tsx: dialogs become full-width bottom sheets on mobile (all modals inherit it); desktop keeps the centered dialog
- Header: sticky on mobile only + PageLayout bottom clearance
- New MobileTabBar: role-aware bottom tabs (office admin/manager, inventory PM, inventory admin); hidden on /wizard
- Wizard step 4: TouchSensor (150ms delay) so touch drag works without stealing scroll
- JalaliDatePicker: dropdown capped to viewport; bigger day cells and time inputs on mobile
- PWA: manifest.webmanifest + 192/512 icons (scripts/gen-pwa-icons.ps1) + theme-color/manifest links in index.html
- New docs/mobile-responsive.md documenting the full change set
2026-09-03 19:26:40 +03:30
ali_mohaghegh 7f58593f04 fix(daily-control): «ماه بعد» navigation + hover feedback
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.
2026-09-03 17:10:58 +03:30
ali_mohaghegh 633e9c2561 docs: add the current integrated ER export (NewERD.txt at repo root)
User-provided export of the final integrated procurement + inventory ER
(DBDatalyzer/dbdata-diagram format). Kept at the root as placed; the older
ERD lives in docs/NewERD.txt.
2026-09-03 15:01:16 +03:30
ali_mohaghegh 0cd4f8f361 feat(ui): AppSwitcherMenu parity + hub-only warehouse headers + calendar badge removal
- 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
2026-09-03 15:01:07 +03:30
ali_mohaghegh e4ae34ddbc feat(sync): live cross-module cache invalidation (no manual refresh)
- 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
2026-09-03 15:00:58 +03:30
ali_mohaghegh f9ec68815e fix(suppliers): enforce Category → Supplier hierarchy, remove rogue-category bug
Domain model (v2.6): suppliers.category_id NOT NULL (1:N from the 8 fixed
categories), materials are global and link to suppliers via
supplier_materials. This fixes three regressions of the old inverted tree:

- remove the intermediate 'یک متریال را انتخاب کنید' screen: a category
  card now routes straight to that category's supplier list
  (/suppliers/category/:category); TopicPage is deleted and legacy
  /topics/:topic[/:category] URLs redirect to the new hierarchy
- kill the accidental-category bug: api.materials.create silently POSTed
  /material-categories for any unknown 'category' (a material name!), which
  is how the rogue «پریز» category (+ junk material) appeared. Unknown
  categories now fail loudly instead of creating catalog rows (DB cleaned:
  exactly the 8 canonical categories remain)
- fix inverted wording: supplier list is «تأمین‌کننده‌های این دسته» with
  category-first breadcrumbs; the legacy material multi-select on the
  add-supplier form is gone (a supplier gets the page's category)
- supplier details screen logic untouched — only its breadcrumb link
  targets were updated to the new routes
2026-09-03 15:00:50 +03:30
ali_mohaghegh b4ed18af71 fix(nginx): resolve api upstream at request time via Docker DNS
After the api container restarts it gets a new IP; nginx cached the old one
at startup and kept proxying /api/* to the stale address — which by then
belonged to another service (casdoor), so login returned Casdoor's error
envelope and the frontend crashed with "Cannot read properties of null
(reading 'user')". proxy_pass now uses a variable + resolver 127.0.0.11 so
the upstream re-resolves per request. Verified: login works, and still works
after restarting the api container.
2026-09-02 18:42:02 +03:30
ali_mohaghegh a55c679bb7 fix(seed): enforce only the 8 canonical topic categories
The topic grids (PM system + wizard) showed extra categories like «سیم»,
«کابل», «ثبثب», «dsfg». Those rows were leftover junk in material_categories
from earlier testing — previously invisible because the frontend hardcoded
the 8-topic list; the API-driven topic grid surfaced them. The rows are
deleted (they had no references) and the seeder now deletes any category
outside the 8 canonical slugs, so a re-seed always yields exactly the same
8 categories as before. Dump refreshed from the clean state.
2026-09-02 18:28:54 +03:30
ali_mohaghegh acb9958059 docs+data: refresh demo dumps (clean seeded state) + restore/seeder docs
- backups/procurement.dump regenerated from the clean seeded state (was the
  junk test data); README documents both the app seeder and the restore path
- docs/casdoor-auth.md: later-batch changelog (fulfillment split, demo
  seeder, API-driven topics, hub-and-spoke, count modal, FOUND ceiling,
  semantic colors)
2026-09-02 18:15:57 +03:30
ali_mohaghegh ac76af2ffe feat(frontend): fulfillment split, API-driven topics, hub-and-spoke nav, stock-count modal, FOUND ceiling, semantic colors
- 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
2026-09-02 18:15:32 +03:30
ali_mohaghegh ca84d8664e feat(backend): demo seeder, receipt allocations in expand, FOUND ceiling guard
- tools/seed.py: wipes app tables and seeds a clean realistic demo (3
  projects x 2 warehouses, 4 suppliers, 12 materials across all 8 topic
  categories, 4 quotes covering offered / in-transit / partial / completed
  with receipts + matching stock); user roles bound to Casdoor identities
  by username (seed_casdoor.py first)
- expand_service: the line-item receipt aggregate now carries the warehouse
  allocations (target warehouse per received part) for the ledger's
  «رسید شده» split
- inventory_service: FOUND («پیدا شده») is guarded so recovery cannot exceed
  the missing balance for the same (warehouse, material, brand, specs) —
  422 with the remaining balance; STOCK_COUNT semantics unchanged (absolute
  counted qty, auto-diff)
2026-09-02 18:15:11 +03:30
ali_mohaghegh d56cd13cb9 chore(backups): ship exact DB-state dumps + restore scripts
The databases live in Docker volumes (not in git), so teammates on a fresh
boot saw an empty system. backups/procurement.dump + casdoor.dump are exact
pg_dump snapshots of the current demo state (users/roles, projects,
warehouses, quotes with receipts, inventory rows; Casdoor org/app/users);
restore.ps1 / restore.sh replace both DBs with the committed state
(--clean --if-exists, verified by a round-trip restore into throwaway DBs).
README documents how to restore and how to take a fresh dump.
2026-09-02 11:21:09 +03:30
ali_mohaghegh 31290ffbda docs: README + casdoor-auth guide for the v1.3 changes
- README: Traefik edge (app at http://localhost), single postgres (casdoor
  DB inside), prod overlay usage, partial receipts, admin panel, dev-mode
  3-step flow, layout for infra/ and 02-casdoor-db.sql
- docs/casdoor-auth.md: new section «Stack changes in this release» (DB
  merge, Traefik, RBAC, partial receipt, calendar, upload, project filter),
  updated addresses and redirect-URI notes
2026-09-01 17:28:45 +03:30
ali_mohaghegh 8825f333a7 infra: single postgres instance (casdoor DB merged) + Traefik v3 edge
- remove the separate casdoor-postgres container; casdoor uses the main
  postgres (db casdoor, created by sql/02-casdoor-db.sql on fresh volumes);
  data migrated via pg_dump/pg_restore (backups kept outside the repo)
- Traefik v3.6 as the public edge (ports 80/443): docker provider pinned to
  the dedicated traefik_network (fixed-name network), host rules for
  app/api/auth/s3/storage (env-overridable, localhost defaults), frontend
  nginx demoted to internal SPA server; docker-compose.prod.yml adds real
  domains + Let's Encrypt + HTTP→HTTPS redirect (redirect middleware in
  infra/traefik-dynamic.yml)
- v3.6 required (Traefik <=3.3 fails against Docker Engine 29.x); socket
  mounted rw (Docker Desktop rejects :ro)
- start.ps1: app URL now http://localhost, casdoor + db notes
2026-09-01 17:28:33 +03:30
ali_mohaghegh c5997ac243 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)
2026-09-01 17:28:19 +03:30
ali_mohaghegh 83f4fb0747 fix(backend): arrival-confirmation 500, receipt state, RBAC, Casdoor user sync
- pm.py/inventory.py: remove db.begin() on already-begun sessions — the
  scope dependencies read from the session first, so opening a new
  transaction raised InvalidRequestError -> 500 on every receipt/transaction
  (JWT path); get_db already commits/rolls back atomically
- pm.py: receipt response returns the real derived state (was hardcoded
  'arrived'); dev-user creation is race-free (insert on conflict)
- expand_service: aggregate 1:N receipts per line item (received_qty sum +
  count) so the ledger can show partial-receipt totals
- users.py: role changes are admin-only (RBAC); new POST /users/sync mirrors
  Casdoor org users into the app users table (identity only, roles untouched)
- config/compose: CASDOOR_ADMIN_USER/PASSWORD for the sync endpoint
- seed_casdoor: redirect URI updated for the Traefik edge
2026-09-01 17:28:06 +03:30
ali_mohaghegh c59f5ad1a3 docs: Casdoor auth guide + README login/known-behavior update
- docs/casdoor-auth.md: addresses and credentials (Casdoor console
  admin/123, seeded users), in-page login flow, status-code table, the
  User/Organization/Role/Permission/Application model, the 9 failure
  scenarios with debug steps, Test 1-6 results, 23-item checklist, and how
  to manage Casdoor (console, seeding, first-admin bootstrap)
- README: replace the 'no authentication' and 'Casdoor not seeded' notes
  with the current login flow, developer-mode toggle and a link to the
  auth guide
2026-08-29 14:44:27 +03:30
ali_mohaghegh 9caa4d804e feat(frontend): in-page Casdoor login form + env-gated Developer Mode panel
- 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
2026-08-29 14:44:13 +03:30
ali_mohaghegh fde2168606 feat(casdoor): seeder for current Casdoor version + wire api env and dev-login build arg
- seed_casdoor.py: JSON type=login + cookie session (the old form-encoded
  login-normal API is gone), application with grantTypes incl. password,
  passwordType=plain for the seeded users, idempotent re-runs; adds a
  dedicated tester user
- docker-compose: Casdoor env for the api service (endpoint casdoor:8000
  in-network, issuer http://localhost:8080 as advertised by discovery) and
  the DEV_LOGIN_ENABLED build arg for the frontend
2026-08-29 14:44:01 +03:30
ali_mohaghegh 3cd76d335f fix(auth): Casdoor JWT verification and 401 semantics; add /auth/login password-grant endpoint
- verify kid from the JWT header (pyjwt decode() returns claims, not the
  header — the old lookup was always None and rejected every token)
- convert JWK entries to crypto keys explicitly (pyjwt 2.13 dropped dict
  keys) and fetch the JWKS from /.well-known/jwks
- 401 for missing/invalid/expired/wrong-issuer/wrong-audience tokens
  (was 403/400); 403 stays for valid-but-roleless users; 503 when Casdoor
  is unreachable; new CASDOOR_ISSUER setting separates the in-network
  endpoint from the browser-facing issuer
- dev-header fallback in inventory/pm scopes now requires an explicit
  X-User-Role header (no auth info at all -> 401)
- new POST /api/v1/auth/login: password grant against Casdoor, local JWT
  verification, auto-provisioning, returns token + profile with the role
  resolved from the app users table
2026-08-29 14:43:49 +03:30
ali_mohaghegh 06a9a654a5 docs: update README layout and login/access notes 2026-08-27 15:04:25 +03:30
ali_mohaghegh 6a91d32cd5 fix(dashboard): show price unit (toman) inside the amount-in-letters bracket 2026-08-27 15:04:25 +03:30
ali_mohaghegh 068c51909c fix(db): fill item_receipts.confirmed_by in office paths; resilient per-statement v2.6 upgrade (root cause of 500 on receipt) 2026-08-27 15:04:25 +03:30
ali_mohaghegh eb2a50eee1 feat(inventory): role-aware header navigation - PM top nav without hamburger, admin top nav plus compact account menu 2026-08-27 15:04:25 +03:30