Add implementation plans and remove stray Word temp file

Plans record the decisions behind each feature and, for the protocol and
licensing work, what remains unvalidated against real hardware.

~WRL0003.tmp was a Word autosave artifact committed by accident.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-18 18:58:38 +03:00
parent 0a1b7c31e2
commit fe9fb71860
9 changed files with 1292 additions and 0 deletions
@@ -0,0 +1,115 @@
# Plan: Drag-to-reorder nomenclatures, reflected in vehicle "Additional Data"
## User Request
> in nomenclatures overlay, make the rows in the list draggable, so they can be rearranged. then in vehicle, show the fields in this order
Clarified with screenshots: the target is the **left-hand list in the Nomenclatures overlay**
(Агент, Товар, Товародател, № Контейнер, Корабособственик). The client wants to be able to move
e.g. "Агент" to the bottom of the list, and the vehicle screen's "Additional Data" dropdowns
should then appear in that same order.
## Plan
### Context
The backend already supports this fully: `Nomenclature.sort_order` exists
(`backend/nomenclatures/models.py:29`), `Meta.ordering = ["sort_order", "code"]`
(`models.py:32`), the viewset orders by it (`backend/api/views.py:260`), the serializer
exposes it as writable (`backend/api/serializers.py:264`), and `Main.jsx:31` already sorts
`vehicleNomenclatures` by it. **No backend change and no migration are needed.**
The reason the order looks fixed today is that every existing row has `sort_order = 0` — the
model default — so the sort is a no-op and rows fall back to `code` order. Assigning real
values on the first drag fixes that permanently.
Decisions taken with the user:
- Persist by PATCHing each moved row (frontend-only; no new endpoint).
- Save immediately on drop — no Save-button staging.
- The whole row is draggable (no separate grip handle).
### Scope
Two files change. Both are frontend.
#### 1. `frontend/src/components/NomenclatureManager/NomenclatureManager.jsx`
Add native HTML5 drag-and-drop to the left list (lines 189–217). No library — `package.json`
has no DnD dependency and adding one for a five-row list is not warranted.
**State:** two new pieces of local state — `dragIndex` (index of the row being dragged,
`null` when idle) and `dragOverIndex` (index the row would drop into, for the drop-line
indicator). Plus reuse of the existing `error` state for failure messages.
**Row markup** (`.nm-list-item`, line 191): add
- `draggable={canEdit}` — reordering is an edit, so gate it on the same `canEdit`
(`currentUser?.can_manage_entities`) that already gates the edit/add/delete buttons.
- `onDragStart` → record `dragIndex`; call `e.dataTransfer.setData('text/plain', String(idx))`
(Firefox refuses to start a drag without data set) and `e.dataTransfer.effectAllowed = 'move'`.
- `onDragOver` → `e.preventDefault()` (required or no drop event fires) and set `dragOverIndex`.
- `onDragEnd` → clear both indices (fires even on a cancelled drag, so it is the reliable reset).
- `onDrop` → `e.preventDefault()`, then run the reorder below.
- `className` gains conditional `nm-list-item--dragging` / `nm-list-item--drag-over` modifiers.
Change the `.map(nom => ...)` at line 190 to `.map((nom, idx) => ...)` so handlers have the
index. The React `key={nom.id}` at line 192 is already stable and correct — leave it.
**Reorder handler** `handleDrop(targetIdx)`:
1. Bail if `dragIndex === null || dragIndex === targetIdx`.
2. Build the reordered array with a splice-out/splice-in on a copy of `nomenclatures`.
3. Write `sort_order: i` onto every element of the new array, and `setNomenclatures(...)`
immediately — optimistic update, so the list does not visibly snap back while the requests
are in flight.
4. Diff against the previous array and PATCH only rows whose `sort_order` actually changed:
`api.patch('/api/nomenclatures/${id}/', { sort_order: i })`, all in one `Promise.all`.
PATCH (not PUT) matters — `NomenclatureSerializer.update` deletes and recreates the whole
`fields` collection (`backend/api/serializers.py:284-287`), so a PUT without a `fields` key
would wipe the field definitions. A partial update leaves `fields` untouched.
5. On rejection: `setError('Failed to save order')` and `await loadNomenclatures()` to resync
from the server.
Note the first drag will PATCH nearly every row, because they all currently sit at
`sort_order = 0` and the diff sees every position as changed. That is correct and self-healing —
subsequent drags only touch the rows between the source and target.
**One caveat to handle:** each PATCH triggers `sse_broadcast_update('nomenclature', 'update', ...)`
(`backend/api/views.py:268`), and `NomenclatureDataContext.jsx:82-85` responds to any
`nomenclature` event with a full `loadAll()` refetch. For a 5-row reorder that is ~5 events
collapsing into a few refetches of a small payload — acceptable, and it is what makes the
vehicle screen update live without a page reload. No debounce needed at this scale; revisit
only if the list ever grows large.
#### 2. `frontend/src/components/NomenclatureManager/NomenclatureManager.css`
- `.nm-list-item` (line 117) — the existing rule already has `cursor: pointer`; leave it. Add
`user-select: none` so dragging does not paint a text selection across the row.
- `.nm-list-item--dragging` — `opacity: 0.4`, to show which row is in flight.
- `.nm-list-item--drag-over` — a drop indicator in the accent already used by
`.nm-list-item--active` (line 132). Because `.nm-list-item` has a `border-bottom` (line 122),
use an inset `box-shadow: inset 0 2px 0 #667eea` rather than a border so row height does not
shift by 2px during the drag.
### Explicitly not in scope
- `NomenclatureField` rows (the "Fields" table on the right panel) — they have their own
`sort_order` that the save payload currently drops, but the user asked about the left list
only. Flagged to the user as a separate known gap, not fixed here.
- `NomenclatureEntry` ordering — entries have no `sort_order` column at all; that would need a
migration. Not requested.
- `Main.jsx` — **no change required.** Line 31 already sorts by `sort_order`. Left as is.
### Verification
1. Start backend (`cd backend && uvicorn scalesapp.asgi:application --reload`) and frontend
(`cd frontend && npm start`); log in as a user with `can_manage_entities`.
2. Open the Nomenclatures overlay. Drag **Агент** from the top to the bottom of the left list.
Verify: the row follows the cursor with a drop indicator, and lands last on release.
3. Check the Network tab: several `PATCH /api/nomenclatures/{id}/` requests, each with a
`{sort_order: N}` body only, all returning 200.
4. Without reloading, look at the vehicle screen's "Additional Data" section — **АГЕНТ should
now be the last dropdown**, moved there live by the SSE-driven `loadAll()`.
5. Hard-reload the page (F5) and confirm both the overlay list and Additional Data still show
Агент last — this proves it persisted rather than only living in local state.
6. Re-open the overlay, click a nomenclature to edit it, and confirm its **Fields table still
lists its fields** — this is the PUT-vs-PATCH regression guard from step 4 of the handler.
7. Log in as a user without `can_manage_entities` and confirm rows are not draggable.