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.
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.
- «مشخصه فنی» 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.
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.
- متریال 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).
- «ضرایب تبدیل» 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.
- 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).
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.
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.
- 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.
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).
- 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.
- 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).
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).
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.
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