- 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
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)
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.
Conflicts resolved keeping the hardened/current side:
- docker-compose.yml: kept new-one's db-backup service, hardened env
(ENVIRONMENT/DEV_LOGIN/CORS/issuer var) and build args; prod_v2's
8083/8085 dev port remaps dropped as they conflict with the hardened
Casdoor issuer expectation.
- AcceptModal.tsx: kept the per-item target_project model (43ff048) over
prod_v2's older projUnion candidate-project logic, kept the
authenticated file upload/download blocks, and adopted prod_v2's
sticky footer.
Wizard:
- failed saves now route to the exact step+field: a new data-field attribute on every required section (supplier_id, line_items, termsheets, project, quote_date, expiry_date, notes, proforma) plus resolveErrorTarget() that maps ApiError.field / message keywords (supplier, line item/material/unit/quantity, termsheet/schedule/milestone, project, dates, notes, file) to the offending step — the «رفع خطا» toast action scrolls/focuses the right control instead of guessing
- material records are no longer created on the fly while typing (rememberMaterial no longer fires addMaterial per blur); materials/specs/units are written once during the final save, avoiding half-created catalog rows
Accept flow:
- quote_service no longer rewrites LineItemProjects on accept — those candidate-project rows stay 100% untouched and only item_decisions.project_id is written, matching the per-item target-project model and the UI payload (item_projects: [])
Dates/time:
- JalaliDatePicker: hour/minute are now editable text fields (Persian digits, clamping, 3-digit retype handling) with a controlled useEffect that only rewrites the inputs when the numeric value actually changes — no more wiping in-progress typing; date-only detection tightened to a full YYYY-MM-DD match so datetime strings are not misread
Also: /api validation logging (RequestValidationError) now includes the path and error list for faster diagnosis.
Verified: api + frontend image builds (python, tsc, vite) clean on top of origin/production (incl. the team's mobile layout merges).
Quotes that already have warehouse receipts can no longer be edited or rolled back:
- backend: quote_has_receipts() guard — replace_quote_children and every backward status transition now return 409 with «این پیشنهاد بهدلیل ثبت رسید در انبار، غیرقابل ویرایش یا بازگشت است.»
- reopening to «offered» additionally clears item_decisions.project_id (the purchased-project link belongs to acceptance, not to a reopened offer)
- frontend EditModal: hasReceipts locks the whole form — disabled inputs and date pickers (JalaliDatePicker/NumberInput/MilestoneRow/MultiSelectCombobox gained disabled support), add/remove controls for items, termsheets and milestones hidden, status change blocked, amber warning banner; quote deletion now goes through a confirmation dialog (useDeleteQuote)
- date-only semantics: JalaliDatePicker keeps/emits YYYY-MM-DD when withTime=false (typing, «امروز», day selection) and toDateOnly no longer mangles pure date strings
Per-item target project (candidate projects stay intact):
- expand_service exposes the decision target project (decision.project + line_item.target_project)
- the accept payload derives item_decisions.project_id from that target project and sends item_projects: [] so line_item_projects keeps the original candidates untouched
- AcceptModal/EditModal select the target project per line item
Material titles:
- quoteUtils.formatMaterialTitle() composes one normalized, deduplicated «name + brand + specs» title, replacing materialLabel/materialSpecs/specsLabel usage across dashboard cards, quote detail and the modals
Verified: python compile + api/frontend image builds (tsc + vite) clean.
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.
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.
- 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).