452 lines
28 KiB
Markdown
452 lines
28 KiB
Markdown
# Finalize-on-print, History view, and unified report preview modal
|
|
|
|
## Context
|
|
|
|
Today, the moment a vehicle's second weighing lands, the backend silently
|
|
computes `net` and assigns a `doc_number` (`_maybe_finalize`,
|
|
[api/views.py:134-141](backend/api/views.py#L134-L141)) — but nothing in the
|
|
UI reflects this as an event. The vehicle just sits in the active list
|
|
forever, indistinguishable from an in-progress one except that its action
|
|
column now shows Print/Design instead of Measure.
|
|
|
|
The user's actual workflow (matching their existing Delphi weighing software,
|
|
and the problem it has that they want to avoid repeating): once a truck is
|
|
weighed twice, the operator opens a report preview, prints or downloads the
|
|
notice, and the truck should then leave the active list — because it's done,
|
|
and a busy station's list should only show trucks still being processed. If a
|
|
print physically fails (paper jam, printer offline — a real, frequent
|
|
occurrence per the user, and one the browser's `window.print()` can never
|
|
report back on, confirmed via `afterprint`'s fundamental limitation), the
|
|
operator needs to get back to that vehicle's report without digging through a
|
|
separate/hidden screen. A **History** view listing finalized vehicles, with
|
|
the same print/download controls available there, is exactly that recovery
|
|
path — and it becomes the *only* path back once a vehicle leaves the active
|
|
list, so it must be trivial to reach, not buried.
|
|
|
|
This session's exploration surfaced two things that must be fixed as part of
|
|
this work, not deferred:
|
|
|
|
1. **`driver_name` is dead on arrival.** The live vehicle API is
|
|
`api/views.py` + `api/serializers.py` (registered in
|
|
`api/urls.py`) — NOT `vehicles/views.py`/`vehicles/serializers.py`, which
|
|
are orphaned (not routed) and contain the `driver_name`-required work from
|
|
the previous session. `driver_name` is missing entirely from
|
|
`VehicleSerializer.Meta.fields` in the real, live
|
|
`api/serializers.py:165`. The frontend's create-vehicle form already sends
|
|
`driver_name`, but the live backend silently drops it on the floor.
|
|
2. **The frontend never removes anything from `vehicles`, and SSE has no
|
|
working delete path.** `NomenclatureContext.handleSSEUpdate`
|
|
([NomenclatureContext.jsx:407-432](frontend/src/contexts/NomenclatureContext.jsx#L407-L432))
|
|
only handles `insert`/`update`; a `delete`/`archive` operation is silently
|
|
dropped even though the backend's `operation_map` already produces
|
|
`'delete'` for a `'deleted'` action. Any "vehicle leaves the list in real
|
|
time, on every open tab" behavior needs this added — it is not
|
|
pre-existing plumbing we can just call.
|
|
|
|
## Decisions made this session (do not re-litigate)
|
|
|
|
- **driver_name**: nullable at the DB level (already migrated), **required on
|
|
create** at the serializer level — fix belongs in `api/serializers.py`
|
|
(the live one), same validation approach already proven correct in the
|
|
orphaned `vehicles/serializers.py` (object-level `validate()`, gated on
|
|
`self.instance is None`, because a per-field `validate_driver_name` hook
|
|
was tested and found to skip validation when the key is omitted entirely).
|
|
- **When a vehicle leaves the active list**: on the operator's explicit
|
|
**Print or Download PDF click** inside the new preview modal — not
|
|
automatically the moment `net`/`doc_number` are assigned server-side. The
|
|
user was explicit: "when press it - the truck is given a document number,
|
|
disappears from list, the preview opens with print/download buttons."
|
|
Concretely: the modal opens as soon as Print Notice is clicked (same
|
|
trigger as today), and the archive+doc_number-visible transition happens
|
|
when the operator acts inside that modal (Print or Download PDF), not on
|
|
the initial open. Opening the modal must not, by itself, remove the truck —
|
|
an operator who opens it and then closes it without printing must find the
|
|
truck still there, still able to re-measure if needed.
|
|
- **Preview modal layout** (replaces `ReportPreviewModal`'s current bare
|
|
preview-only body): top bar with a matrix/A4 kind switcher on the left and
|
|
Download PDF on the right; body renders the selected kind's preview; footer
|
|
bar with centered Print and Close buttons. **The switcher is real, not a
|
|
placeholder**: `notice_report` can have two actual saved `Report` rows
|
|
(one `kind='matrix'`, one `kind='a4'`) side by side — confirmed the model
|
|
already allows this (`name` has no uniqueness constraint) — and the
|
|
switcher toggles between whichever of the two the operator has designed.
|
|
If only one kind has been saved, the other option shows a disabled/empty
|
|
state pointing at Design Notice for that kind. Default kind shown on open =
|
|
the kind that was open/used last, or the first one that exists.
|
|
- **Download PDF: REVERSED from an earlier draft of this plan** — no
|
|
server-side rendering. Originally scoped as Playwright (Python) driving
|
|
headless Chromium; abandoned once an existing plan
|
|
([a4-report-designer.md](.claude/plans/a4-report-designer.md), §"Why
|
|
Puppeteer is deferred (not rejected)") surfaced two concrete costs specific
|
|
to this deployment that the earlier draft hadn't weighed:
|
|
1. **Fonts.** A4 notices print with whatever font is installed on the
|
|
*operator's station* — that's the whole point of using
|
|
`window.print()`/`@page` CSS instead of a PDF library (see that same
|
|
plan's "Why browser printing, not a PDF library"). Server-side
|
|
rendering uses the *server's* fonts instead. At this project's actual
|
|
reference deployment (one server, ten stations, per
|
|
[[deployment-windows-installer]]), that's the wrong machine — a
|
|
server-rendered PDF could visibly mismatch what prints at the station,
|
|
forever, unless fonts are duplicated and kept in sync server-side.
|
|
2. **Puppeteer/Playwright earns its cost when a PDF must be produced with
|
|
no human in the loop** (auto-archiving, emailed tickets, batch jobs).
|
|
Download PDF has an operator clicking a button and waiting — exactly
|
|
the case `window.print()` → "Save as PDF" already covers, today, with
|
|
zero new services and zero font-mismatch risk.
|
|
**Decided approach**: Download PDF triggers the SAME `window.print()` flow
|
|
already used for Print (`printA4Report` for a4, the new print-root-append
|
|
flow from Phase C for matrix), just labelled/framed toward the "Save as
|
|
PDF" destination rather than physical printing. No Playwright, no
|
|
Puppeteer, no new backend endpoint, no offline-installer packaging cost.
|
|
This also means Phase D as originally scoped (a new PDF microservice) is
|
|
**not needed** — see the rewritten Architecture section below.
|
|
- **Print button** in the modal (and Download PDF, per the reversal above)
|
|
both use today's `window.print()`/`printA4Report()` flow for A4, and the
|
|
Phase C print-root-append flow for matrix. No further backend work.
|
|
- **History view**: a new page/route, reachable from the main nav (not
|
|
hidden), listing finalized vehicles (those with `doc_number` set) — most
|
|
recent first. Each row opens the same unified preview modal used from the
|
|
main weighing screen, so reprint/redownload is the identical component and
|
|
code path, not a second implementation.
|
|
- **Nothing is ever deleted.** Explicit, standing rule for this feature and
|
|
beyond: a vehicle record is never destroyed. There are three real states —
|
|
**in progress** (active list), **completed** (History, has `doc_number`),
|
|
and **voided** (History, flagged but still fully visible/queryable) — and
|
|
moving between them is always a flag/field change, never a `DELETE`. This
|
|
also means the orphaned `vehicles/views.py`'s `perform_destroy`
|
|
([confirmed at api/views.py's pattern](backend/api/views.py) — check
|
|
whether `VehicleViewSet` exposes a working DELETE at all; if it does, it is
|
|
now out of step with this rule and should be disabled, flagged to the user
|
|
rather than silently left reachable).
|
|
- **Void ("сторно"/reversal)**: an operator-triggered action available **only
|
|
from History, only on an already-completed record** — not on an
|
|
in-progress vehicle, and not a bulk/automatic thing. Marks a completed
|
|
weighing as voided when it turns out to be wrong (wrong truck, bad
|
|
measurement, mistake caught after printing) — the record, its doc_number,
|
|
and its full weighing history stay intact and visible in History,
|
|
permanently flagged, distinguishable from a normal completed record at a
|
|
glance (e.g. a visual badge, per how `.vehicle-status--*` already
|
|
distinguishes states in the hero bar,
|
|
[Main.css:189-203](frontend/src/components/Main.css#L189-L203)).
|
|
- **`vehicle_number` uniqueness must change.** Today it's a plain
|
|
`unique=True` column ([vehicles/models.py:11](backend/vehicles/models.py#L11))
|
|
— global, forever. Combined with "nothing is ever deleted," this breaks the
|
|
very next time a real truck (plate reused constantly) returns to the scale
|
|
after its first weighing completes: creating the new active record would
|
|
fail on a uniqueness violation. Decided fix: **unique only among
|
|
in-progress vehicles** — a truck can't be on the scale twice at once, but
|
|
its plate can appear any number of times across History. Implemented as a
|
|
Django partial `UniqueConstraint`, not a plain column constraint:
|
|
```python
|
|
class Meta:
|
|
constraints = [
|
|
models.UniqueConstraint(
|
|
fields=['vehicle_number'],
|
|
condition=models.Q(doc_number__isnull=True),
|
|
name='unique_active_vehicle_number',
|
|
),
|
|
]
|
|
```
|
|
(Django 6.0, confirmed installed — `condition=` on `UniqueConstraint` has
|
|
been supported since Django 2.2, compiles to a PostgreSQL partial unique
|
|
index, which this project's Postgres backend fully supports.) Requires
|
|
dropping the existing `unique=True` on the field itself and adding this
|
|
constraint instead — a real migration, not a no-op. Confirm during
|
|
implementation whether "in-progress" should be defined as
|
|
`doc_number__isnull=True` alone, or also exclude voided-but-somehow-
|
|
doc_number-less edge cases — should not arise if void is only ever
|
|
triggered on already-completed (doc_number-bearing) records, per the
|
|
Void scope decision above, but worth a sanity check against real data.
|
|
|
|
## Architecture
|
|
|
|
### Backend
|
|
|
|
**1. Fix the live serializer** — `backend/api/serializers.py`:
|
|
- Add `'driver_name'` to `VehicleSerializer.Meta.fields` (currently missing
|
|
at line 165).
|
|
- Add the required-on-create `validate()` override, ported from the pattern
|
|
already proven in the orphaned `vehicles/serializers.py`:
|
|
```python
|
|
def validate(self, attrs):
|
|
if self.instance is None and not attrs.get('driver_name'):
|
|
raise serializers.ValidationError({'driver_name': 'This field is required.'})
|
|
return attrs
|
|
```
|
|
(If `VehicleSerializer` already defines a `validate()`, merge into it
|
|
rather than adding a second one.)
|
|
- Leave the orphaned `vehicles/` app alone — it's unrouted dead code, not
|
|
this task's problem to clean up (flag to the user separately; don't delete
|
|
without being asked, per project convention).
|
|
|
|
**2. No PDF endpoint needed — REVERSED, see the Download PDF decision above.**
|
|
No new backend endpoint, no Playwright, no server-side rendering. Phase D on
|
|
the backend side is a no-op; everything below is frontend-only.
|
|
|
|
**3. Print-intent signal for both the modal's Print AND Download PDF
|
|
buttons**: both now trigger the *existing* client flow
|
|
(`window.print()`/`printA4Report` for a4, the Phase C print-root-append flow
|
|
for matrix — Download PDF is the same call, just aimed at the "Save as PDF"
|
|
destination the operator picks in that same dialog), which the backend can't
|
|
observe directly.
|
|
Add a lightweight endpoint, e.g. `POST /api/vehicles/{id}/mark-notice-printed/`,
|
|
called by the frontend right when the operator clicks Print in the modal (not
|
|
waiting on `afterprint`, since that signal is untrustworthy per this
|
|
session's earlier discussion) — its only job is to broadcast the SSE removal
|
|
event (below). It does not need to change any DB field; `doc_number` already
|
|
being set is the source of truth for "this vehicle belongs in History, not
|
|
the active list." This keeps "print succeeded" and "should leave the active
|
|
list" cleanly decoupled, exactly as discussed: a failed physical print still
|
|
correctly moves the truck to History (where it's one click to reprint), it
|
|
does not silently strand it in a state nobody can find.
|
|
|
|
**4. SSE removal broadcast**: wherever the "moved to history" signal fires
|
|
(the `mark-notice-printed` endpoint, and/or the new PDF endpoint on
|
|
successful generation), call
|
|
`sse_broadcast_update('vehicle', 'archived', serializer.data)` — a new,
|
|
distinct action string (not reusing `'deleted'`, since the vehicle record
|
|
itself isn't deleted, only its presence in the *active* list changes) — see
|
|
frontend section for how this action is consumed. Using
|
|
[sse_broadcast_update](backend/scalesapp/sse.py#L26-L58), same call shape
|
|
already used by `perform_create`/`perform_update`.
|
|
|
|
**5. Filter the active-list endpoint** (optional but recommended for
|
|
correctness/scale): today `VehicleViewSet.queryset` returns all vehicles
|
|
unconditionally. Once History exists, the main screen's vehicle list should
|
|
probably query only non-finalized vehicles server-side too (e.g.
|
|
`?doc_number__isnull=true`), not rely purely on client-side SSE-driven
|
|
removal — SSE gives real-time updates for tabs already open, but a **fresh
|
|
page load** should not have to fetch every historical vehicle ever weighed
|
|
just to filter them out client-side. Add a query param the frontend's main
|
|
list fetch already passes (`NomenclatureContext`'s vehicle endpoint config,
|
|
[NomenclatureContext.jsx:9](frontend/src/contexts/NomenclatureContext.jsx#L9)),
|
|
and a separate History fetch that explicitly requests
|
|
`?doc_number__isnull=false`, ordered newest-first (`-net_date` or similar).
|
|
|
|
**6. `is_void` field + partial-uniqueness migration** — the `Vehicle` model
|
|
itself lives at `backend/vehicles/models.py` regardless of which app's
|
|
views/serializers are live (there is only ONE `Vehicle` model — `vehicles/`
|
|
being "orphaned" refers only to that app's OWN `views.py`/`serializers.py`,
|
|
not the model, which `api/views.py` imports and uses directly). So this
|
|
migration correctly targets `backend/vehicles/models.py`, but the
|
|
`@action`/serializer changes below belong in `api/views.py`/`api/serializers.py`
|
|
(the live ones), matching how `driver_name` and everything else in this plan
|
|
is split: model in `vehicles/`, live API surface in `api/`.
|
|
- Add `is_void = models.BooleanField(default=False)`, plus
|
|
`void_date`/`void_user`/`void_reason` following the same
|
|
date/user/note provenance pattern already used for `tare`/`gross`/manual
|
|
entries elsewhere on this model (e.g. `tare_manual_user`,
|
|
`tare_manual_date`, `tare_manual_note`,
|
|
[vehicles/models.py:19-22](backend/vehicles/models.py#L19-L22)) — reuse
|
|
that established shape rather than inventing a different one for void.
|
|
- Replace `vehicle_number = models.CharField(max_length=15, unique=True)`
|
|
with the plain field (drop `unique=True`) plus the partial
|
|
`UniqueConstraint` from the decision above, in one migration.
|
|
- New `@action` on `VehicleViewSet`, e.g.
|
|
`POST /api/vehicles/{id}/void/` (mirroring `measure`'s permission-gated
|
|
pattern) — sets `is_void`, `void_date`, `void_user`, accepts a required
|
|
`reason` in the request body (store it; an operator voiding a printed
|
|
document should always leave a reason, same spirit as
|
|
`gross_manual_note`/`tare_manual_note` already require context for a
|
|
manual override). Rejects (400) if `doc_number` is not set — void only
|
|
applies to completed records, per the scope decision above. This does NOT
|
|
set `doc_number` to null or touch `net`/`tare`/`gross` — everything about
|
|
the original weighing stays exactly as it was, only `is_void` changes.
|
|
Broadcasts `sse_broadcast_update('vehicle', 'voided', serializer.data)` so
|
|
any open History view updates live too.
|
|
- `VehicleSerializer` needs `is_void`/`void_date`/`void_reason` added to
|
|
`Meta.fields` (read-only except through the dedicated void action).
|
|
|
|
### Frontend
|
|
|
|
**1. `NomenclatureContext.jsx`** — add a `delete`/`archive` branch to
|
|
`handleSSEUpdate`'s switch
|
|
([NomenclatureContext.jsx:407-432](frontend/src/contexts/NomenclatureContext.jsx#L407-L432)),
|
|
handling the new `'archived'` action (mapped through `operation_map` on the
|
|
backend, or handled as a raw fallback string if not added to that map — check
|
|
which is simpler when implementing) by removing the vehicle from the
|
|
in-memory `vehicles` array. This is the one piece of "real-time removal"
|
|
plumbing that doesn't exist anywhere today and is the crux of making
|
|
multi-station "truck vanishes everywhere at once" actually work.
|
|
|
|
**2. Unified `ReportPreviewModal`** — restructure
|
|
[ReportPreviewModal.jsx](frontend/src/components/ReportPreviewModal.jsx) (and
|
|
its `.css`) into three regions instead of the current bare header+body:
|
|
- **Top bar**: left = matrix/A4 toggle, switching between two genuinely
|
|
separate stored `Report` rows sharing the same `name='notice_report'` but
|
|
different `kind`. Confirmed this needs no schema change: `Report.name` has
|
|
**no uniqueness constraint**
|
|
([api/models.py:45-75](backend/api/models.py#L45-L75)), so a matrix row and
|
|
an a4 row can already coexist under the same name — nothing stops it today.
|
|
The gap is entirely on the fetch/lookup side:
|
|
- `fetchNoticeReport()` ([Main.jsx:357-360](frontend/src/components/Main.jsx#L357-L360))
|
|
currently does `.find(r => r.name === 'notice_report')`, returning
|
|
whichever row the API lists first — it has no way to ask for "the matrix
|
|
one" vs "the a4 one." Change it to accept a `kind` argument and filter
|
|
accordingly (or add a backend query param, `?name=notice_report&kind=a4`,
|
|
since `ReportViewSet` already has `filterset_fields = ['name', 'kind']`
|
|
— the backend filter already exists, this is a client-side fix only).
|
|
- The modal fetches BOTH kind-variants on open (two lookups, or one list
|
|
call filtered by name only, then split client-side by `kind`), so the
|
|
switcher can toggle without a re-fetch. If one kind doesn't exist yet
|
|
(operator never designed an A4 version), that switcher option is
|
|
disabled/shows an empty state with a link to Design Notice for that kind.
|
|
- `handleDesignNotice`/`handleDesignAs`
|
|
([Main.jsx:403-449](frontend/src/components/Main.jsx#L403-L449)) already
|
|
let an operator explicitly open either editor regardless of what's saved
|
|
(`handleDesignAs('matrix'|'a4')`) — this already supports creating the
|
|
*other* kind's report under the same name; the Report Editor's own save
|
|
path (`handleSave`,
|
|
[ReportEditor.jsx:276-297](frontend/src/components/ReportEditor/ReportEditor.jsx#L276-L297))
|
|
does a plain `POST` when `reportId` is unset, so saving a second,
|
|
different-kind `notice_report` is already a supported write path with no
|
|
backend changes needed there either.
|
|
Right = Download PDF button — **REVERSED, see the top-level decision
|
|
above**: no backend endpoint, no blob download. Triggers the same
|
|
`window.print()`/`printA4Report` (a4) or print-root-append (matrix) flow
|
|
as the Print button, so the operator reaches PDF via "Save as PDF" in that
|
|
same dialog, using the station's own installed fonts. (Phase C shipped
|
|
this button as a disabled placeholder before this reversal was decided —
|
|
Phase D's remaining frontend work is wiring it to the real print flow
|
|
instead of leaving it disabled, plus the `MatrixPrintDocument` work above.)
|
|
- **Body**: existing `EditorCanvas`/`DataProvider`/`BandProvider` preview for
|
|
matrix; for A4, mount `PrintDocument` (confirmed stateless/reusable,
|
|
[PrintDocument.jsx](frontend/src/components/ReportEditor/a4/print/PrintDocument.jsx))
|
|
scaled into the modal body instead of only appending it to `document.body`
|
|
for `window.print()` — needs a scaled/contained rendering mode, not the
|
|
full-bleed print-root mounting `printReport.jsx` uses today.
|
|
- **Footer bar**: centered Print + Close. Print triggers the kind-appropriate
|
|
existing print flow (`printA4Report` for a4; for matrix, there is currently
|
|
no equivalent "just print this" path — `ReportPreviewModal` today is
|
|
preview-only with no print button at all, so a matrix print path needs to
|
|
be added, likely also via `window.print()` on the rendered preview DOM, or
|
|
reusing the same print-root-append pattern `printReport.jsx` established).
|
|
Then calls the new `mark-notice-printed` endpoint.
|
|
- Replace `Main.jsx`'s current branching logic
|
|
([handlePrintNotice, lines 375-398](frontend/src/components/Main.jsx#L375-L398))
|
|
so BOTH kinds open this one modal (today only matrix does; a4 skips the
|
|
modal entirely and goes straight to `window.print()`) — this is the biggest
|
|
behavioral change to `handlePrintNotice`: it becomes "always open the
|
|
modal," full stop, with the modal itself deciding what Print/Download do
|
|
based on the loaded report's kind.
|
|
|
|
**3. New History page** — new route (e.g. `/history`) added alongside the
|
|
existing routes in
|
|
[App.jsx](frontend/src/App.jsx) (inside the same provider nest, following the
|
|
existing route-gating pattern used for admin-only pages). Fetches finalized
|
|
vehicles (`?doc_number__isnull=false`, newest first) independently of the
|
|
live `vehicles` array (History should NOT depend on `NomenclatureContext`'s
|
|
in-memory list, since that list is specifically the *active* set now).
|
|
List each with vehicle number, doc number, net weight, date, and a
|
|
completed/voided badge (`is_void`); clicking a row opens the same unified
|
|
`ReportPreviewModal`, passing that vehicle's data exactly as `Main.jsx` does
|
|
today via `vehicleReportData`
|
|
([Main.jsx:313-334](frontend/src/components/Main.jsx#L313-L334) — this
|
|
transform logic is reusable, likely worth extracting to a shared helper
|
|
rather than duplicating in the new History component). Print/Download PDF
|
|
stay available on a voided row too — voiding never removes the ability to
|
|
see or reproduce the original document, it only flags that the weighing
|
|
itself shouldn't be treated as valid.
|
|
- **Void button**: on a completed (non-voided) row, a distinct action from
|
|
opening the preview — requires a reason (matches the backend's required
|
|
`reason` field), then calls the new `/void/` endpoint. On success, the row
|
|
updates in place to show the voided badge; it does not disappear or move
|
|
anywhere (History is a flat list of everything that ever completed,
|
|
voided or not). Confirm with a dialog before voiding — this is meant to be
|
|
rare and deliberate, matching the "сторно/reversal" concept's own
|
|
seriousness (it's correcting the record, not casually undoable further).
|
|
- A nav entry for History (in `Header.jsx`, alongside whatever nav already
|
|
exists) — must be easy to find, per the whole point of this feature being
|
|
"don't bury the recovery path."
|
|
|
|
## Open questions to resolve during implementation
|
|
|
|
Phase D, resolved: no Playwright/Puppeteer, no server-side PDF endpoint (see
|
|
the Download PDF decision above). The matrix print path question is already
|
|
resolved too — Phase C built it (print-root-append, following
|
|
`printReport.jsx`'s pattern, confirmed working).
|
|
|
|
**Remaining open item, genuinely new scope surfaced while designing Phase D**:
|
|
|
|
1. **Matrix has no `PrintDocument`-equivalent.** A4's `PrintDocument.jsx`
|
|
([print/PrintDocument.jsx](frontend/src/components/ReportEditor/a4/print/PrintDocument.jsx))
|
|
is a small (107-line), stateless, non-interactive component that reads
|
|
`layout()`'s pure `pages[]` output and renders plain positioned HTML — no
|
|
hooks, no drag/select/delete callbacks. Its own doc comment already
|
|
anticipated exactly this kind of reuse: *"DELIBERATELY DUMB... that is
|
|
what lets a future Puppeteer service consume this exact markup."* Matrix
|
|
has no equivalent: its preview goes through `EditorCanvas`
|
|
([EditorCanvas.jsx](frontend/src/components/ReportEditor/EditorCanvas.jsx),
|
|
682 lines), the SAME interactive editor component used for actual
|
|
editing, gated by a `previewMode` prop — confirmed by reading it that
|
|
`previewMode` is a flag threaded through the interactive tree, not a
|
|
separate rendering path; every element still gets full
|
|
`onSelect`/`onUpdate`/`onDelete`/`onDragStart` callback props even in
|
|
preview mode. Phase C's matrix print-root flow works today only because
|
|
it reuses this same `EditorCanvas`-in-`previewMode` machinery — it works,
|
|
but it's borrowing an editor component for a job a pure renderer should
|
|
do.
|
|
- **Decided**: build a `MatrixPrintDocument`-equivalent as part of this
|
|
phase — a pure, non-interactive component mirroring what
|
|
`PrintDocument.jsx` already does for A4, reading matrix `elements` +
|
|
resolved band/data output and emitting plain markup. This is real,
|
|
bounded new work (not a redesign of anything existing), and it's a
|
|
prerequisite for the matrix print/preview path to reach the same
|
|
quality bar A4 already has — it does NOT unlock server-side PDF
|
|
generation (that's still explicitly out of scope, per the reversal
|
|
above); it's purely about correctness/simplicity of the existing
|
|
client-side matrix print flow, replacing "borrow the editor" with "use
|
|
a real preview renderer," same shape as A4.
|
|
|
|
## Verification
|
|
|
|
1. Create a vehicle without `driver_name` via the API directly (curl/Postman)
|
|
— confirm it's now rejected with a 400 and the field-specific error,
|
|
where today it would silently succeed with `driver_name` dropped.
|
|
2. Weigh a vehicle twice (tare + gross) in the browser — confirm it still
|
|
shows in the active list (finalization alone must not remove it).
|
|
3. Click Print Notice — confirm the unified modal opens (for BOTH a matrix-
|
|
and an a4-kind `notice_report`, test each), with top bar (kind
|
|
switcher + Download PDF), body preview, footer (Print + Close centered).
|
|
4. Click Close without printing — confirm the vehicle is still in the active
|
|
list (opening the modal alone must not archive).
|
|
5. Click Print — confirm `window.print()`/`printA4Report` still works exactly
|
|
as before, AND the vehicle disappears from the active list within ~1s on
|
|
the SAME tab and on a SECOND open tab (real-time SSE removal across
|
|
stations).
|
|
6. Click Download PDF instead — confirm it opens the same `window.print()`
|
|
dialog as Print (with "Save as PDF" available as a destination, per the
|
|
reversed decision — no direct blob download, no bypass of the OS dialog),
|
|
and the vehicle is similarly removed from the active list once triggered.
|
|
7. Open History — confirm the just-archived vehicle appears, newest first,
|
|
with correct doc_number/net/date.
|
|
8. Click that History row — confirm the same modal opens and Print/Download
|
|
both work again (the reprint path this whole feature exists to guarantee).
|
|
9. Confirm a vehicle still mid-weighing (no `gross` yet) never appears in
|
|
History.
|
|
10. **Reuse a plate**: complete a weighing for `B7898TP`, confirm it archives
|
|
to History, then create a brand-new active vehicle also named `B7898TP`
|
|
— confirm this succeeds (no uniqueness error), proving the partial
|
|
constraint is scoped correctly. Then try creating a SECOND simultaneous
|
|
active `B7898TP` while the first is still in progress — confirm this is
|
|
rejected (proving uniqueness is still enforced among active records).
|
|
11. From History, void a completed record without providing a reason —
|
|
confirm it's rejected. Void it with a reason — confirm the row updates
|
|
in place with a voided badge, doc_number/net/tare/gross are unchanged,
|
|
and Print/Download still work on it afterward.
|
|
12. Attempt to void an active (non-completed) vehicle directly via the API —
|
|
confirm it's rejected (void is completed-records-only, per scope).
|
|
13. Confirm nothing in this feature ever issues a DELETE against a vehicle.
|
|
**Done, not just checked**: `VehicleViewSet.destroy()` (the LIVE one in
|
|
`api/views.py` — found reachable via `IsAuthenticated` alone, worse than
|
|
the orphaned `vehicles/views.py` this item originally worried about) now
|
|
unconditionally returns `405` pointing at `/void/` instead. Verify with a
|
|
direct `DELETE /api/vehicles/{id}/` call that it's rejected and the
|
|
vehicle still exists afterward.
|
|
14. **Matrix print quality**: with the new `MatrixPrintDocument`, print (and
|
|
Download-as-PDF-via-print) a matrix `notice_report` — confirm the output
|
|
matches what the old `EditorCanvas`-in-`previewMode` flow produced
|
|
(no regression), and confirm the print/preview path no longer carries
|
|
editor-only affordances (no residual selection/drag artifacts, since a
|
|
non-interactive component can't have any).
|