Persist circuit cell edits
This commit is contained in:
@@ -220,15 +220,18 @@ After saving, the row becomes linked to the new project device.
|
|||||||
|
|
||||||
## Undo / Redo
|
## Undo / Redo
|
||||||
|
|
||||||
Session-local UI undo/redo exists for structural and destructive operations.
|
The editor's visible undo/redo stack remains session-local while structural and
|
||||||
The server already persists immutable revisions and project-wide undo/redo
|
destructive operations are being migrated. Existing Circuit and
|
||||||
eligibility for the first commands. Public command, undo and redo endpoints
|
CircuitDeviceRow cell edits already execute persistent project commands and
|
||||||
exist for Circuit and CircuitDeviceRow field updates as well as
|
their toolbar undo/redo actions use the project-wide history endpoints. The
|
||||||
Circuit/CircuitDeviceRow insertion and deletion. Complete command coverage and
|
tree response supplies the optimistic `currentRevision`; the obsolete direct
|
||||||
the UI cutover remain future work. CircuitDeviceRow moves between existing
|
PATCH routes for these fields are removed. Reloading the page does not yet
|
||||||
circuits and moves that create one new placeholder target circuit are also
|
reconstruct the visible editor stack. Public command, undo and redo endpoints
|
||||||
persisted. The latter stores the complete empty target snapshot so undo can
|
also exist for Circuit/CircuitDeviceRow insertion and deletion.
|
||||||
restore the rows and remove only the unchanged generated circuit. Complete
|
CircuitDeviceRow moves between existing circuits and moves that create one new
|
||||||
|
placeholder target circuit are persisted. The latter stores the complete empty
|
||||||
|
target snapshot so undo can restore the rows and remove only the unchanged
|
||||||
|
generated circuit. Complete
|
||||||
in-section Circuit reorders are persisted separately and change sort positions
|
in-section Circuit reorders are persisted separately and change sort positions
|
||||||
without changing equipment identifiers. Explicit complete-section renumbering
|
without changing equipment identifiers. Explicit complete-section renumbering
|
||||||
is persisted through a separate collision-safe command and is never triggered
|
is persisted through a separate collision-safe command and is never triggered
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
The circuit-first editor uses tree, circuit, row and project-device endpoints.
|
The circuit-first editor uses tree, circuit, row and project-device endpoints.
|
||||||
All paths below are mounted below `/api`. There is no Consumer application API.
|
All paths below are mounted below `/api`. There is no Consumer application API.
|
||||||
|
|
||||||
Project responses from `GET /projects` and `GET /projects/:projectId` include
|
Project responses from `GET /projects`, `GET /projects/:projectId` and the
|
||||||
`currentRevision`. Versioned project commands use this value for optimistic
|
circuit-tree endpoint include `currentRevision`. Versioned project commands use
|
||||||
concurrency checks.
|
this value for optimistic concurrency checks.
|
||||||
|
|
||||||
### Project Commands and History
|
### Project Commands and History
|
||||||
|
|
||||||
@@ -35,8 +35,9 @@ with stable ids; delete commands include the expected parent identity. Circuit
|
|||||||
snapshots contain zero, one or multiple complete device rows. Move commands
|
snapshots contain zero, one or multiple complete device rows. Move commands
|
||||||
contain each row's expected and target circuit plus its exact expected and
|
contain each row's expected and target circuit plus its exact expected and
|
||||||
target sort order. This makes deletion and moves undoable without generating
|
target sort order. This makes deletion and moves undoable without generating
|
||||||
replacement identities or renumbering circuits. The editor does not consume
|
replacement identities or renumbering circuits. Existing Circuit and
|
||||||
these endpoints yet.
|
CircuitDeviceRow cell edits in the editor consume these endpoints; the
|
||||||
|
remaining structural editor writes are still being migrated.
|
||||||
|
|
||||||
`circuit-device-row.move` targets existing circuits in the same circuit list.
|
`circuit-device-row.move` targets existing circuits in the same circuit list.
|
||||||
`circuit-device-row.move-with-new-circuit` atomically creates exactly one
|
`circuit-device-row.move-with-new-circuit` atomically creates exactly one
|
||||||
@@ -137,6 +138,7 @@ Response sketch:
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"circuitListId": "cl_1",
|
"circuitListId": "cl_1",
|
||||||
|
"currentRevision": 12,
|
||||||
"sections": [
|
"sections": [
|
||||||
{
|
{
|
||||||
"id": "sec_1",
|
"id": "sec_1",
|
||||||
@@ -167,8 +169,6 @@ Response sketch:
|
|||||||
- create circuit in list/section
|
- create circuit in list/section
|
||||||
- `POST /projects/:projectId/circuit-lists/:circuitListId/circuits-with-device-rows`
|
- `POST /projects/:projectId/circuit-lists/:circuitListId/circuits-with-device-rows`
|
||||||
- atomically create one circuit and one or more initial device rows
|
- atomically create one circuit and one or more initial device rows
|
||||||
- `PATCH /circuits/:circuitId`
|
|
||||||
- update circuit-level fields (BMK, protection, cable, reserve flag, etc.)
|
|
||||||
- `DELETE /circuits/:circuitId`
|
- `DELETE /circuits/:circuitId`
|
||||||
- delete circuit (and related rows via DB relations)
|
- delete circuit (and related rows via DB relations)
|
||||||
- `GET /circuit-sections/:sectionId/next-identifier`
|
- `GET /circuit-sections/:sectionId/next-identifier`
|
||||||
@@ -189,11 +189,14 @@ Request sketch (`POST .../circuits`):
|
|||||||
|
|
||||||
- `POST /circuits/:circuitId/device-rows`
|
- `POST /circuits/:circuitId/device-rows`
|
||||||
- create row inside a circuit
|
- create row inside a circuit
|
||||||
- `PATCH /circuit-device-rows/:rowId`
|
|
||||||
- update row values
|
|
||||||
- `DELETE /circuit-device-rows/:rowId`
|
- `DELETE /circuit-device-rows/:rowId`
|
||||||
- delete row
|
- delete row
|
||||||
|
|
||||||
|
Circuit and device-row field updates are available only as versioned
|
||||||
|
`circuit.update` and `circuit-device-row.update` commands through
|
||||||
|
`POST /projects/:projectId/commands`. There are no direct field-update PATCH
|
||||||
|
routes.
|
||||||
|
|
||||||
### Move Device Row
|
### Move Device Row
|
||||||
|
|
||||||
- `PATCH /circuit-device-rows/:rowId/move`
|
- `PATCH /circuit-device-rows/:rowId/move`
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ Inline cells are static text by default. A cell enters edit mode by:
|
|||||||
|
|
||||||
`Enter` confirms changes. `Escape` cancels current edit draft. `Tab` and `Shift+Tab` confirm and move to next/previous editable cell.
|
`Enter` confirms changes. `Escape` cancels current edit draft. `Tab` and `Shift+Tab` confirm and move to next/previous editable cell.
|
||||||
|
|
||||||
Committed cell edits are persisted as partial database updates. Fields that were not part of the edit are not written back from an older read and therefore cannot overwrite a concurrent change to another field.
|
Committed cells on existing circuits and device rows are persisted as typed
|
||||||
|
project commands with an optimistic project revision. Only the edited fields
|
||||||
|
are included, so an older tree response cannot write unrelated fields back.
|
||||||
|
The server records the inverse in the same transaction.
|
||||||
|
|
||||||
## `selectedCell` vs `editingCell`
|
## `selectedCell` vs `editingCell`
|
||||||
|
|
||||||
@@ -119,7 +122,11 @@ Cross-section device moves show confirmation-required feedback before drop. The
|
|||||||
|
|
||||||
## Undo/Redo
|
## Undo/Redo
|
||||||
|
|
||||||
Undo/redo wraps editor operations as command objects with async `redo`/`undo` and reloads tree after each command.
|
Undo/redo wraps editor operations as command objects and reloads the tree after
|
||||||
|
each command. Existing Circuit and device-row cell edits execute persistent
|
||||||
|
project commands; their toolbar undo/redo calls the project-wide server
|
||||||
|
history. Other covered operations still use their existing direct API writes
|
||||||
|
and session-local inverse callbacks during the cutover.
|
||||||
|
|
||||||
Covered operations include:
|
Covered operations include:
|
||||||
|
|
||||||
@@ -132,5 +139,6 @@ Covered operations include:
|
|||||||
|
|
||||||
Current limitations:
|
Current limitations:
|
||||||
|
|
||||||
- session-local only
|
- the visible editor stack is still session-local and is empty after reload
|
||||||
- no persisted history across browser reload
|
- structural and destructive editor actions are not all connected to the
|
||||||
|
persistent command boundary yet
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
# Circuit List Editor Known Limitations
|
# Circuit List Editor Known Limitations
|
||||||
|
|
||||||
- The circuit-list editor still exposes only its session-local undo/redo UI.
|
- Existing Circuit and CircuitDeviceRow cell edits use persistent project
|
||||||
|
commands and project-wide toolbar undo/redo. The editor's visible stack is
|
||||||
|
still session-local, is empty after reload and still contains direct
|
||||||
|
structural/destructive operations during the cutover.
|
||||||
- Server-side project revisions and persistent undo/redo eligibility exist for
|
- Server-side project revisions and persistent undo/redo eligibility exist for
|
||||||
the command types documented in `circuit-list-editor-api.md`; command coverage
|
the command types documented in `circuit-list-editor-api.md`; complete editor
|
||||||
and the editor UI cutover are not complete.
|
command coverage and history reconstruction are not complete.
|
||||||
- Named logical project snapshots and restore are not implemented yet.
|
- Named logical project snapshots and restore are not implemented yet.
|
||||||
- No Revit/CSV/IFCGUID import and export workflow is implemented yet.
|
- No Revit/CSV/IFCGUID import and export workflow is implemented yet.
|
||||||
- Persistence currently targets local SQLite; PostgreSQL is an architectural option, not an implemented runtime.
|
- Persistence currently targets local SQLite; PostgreSQL is an architectural option, not an implemented runtime.
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ liegt über einen Host-Mount außerhalb des Containers.
|
|||||||
- `src/app/projects/[projectId]/circuit-lists/[circuitListId]/tree-edit/page.tsx`
|
- `src/app/projects/[projectId]/circuit-lists/[circuitListId]/tree-edit/page.tsx`
|
||||||
– unterstützte Editorroute
|
– unterstützte Editorroute
|
||||||
- `src/frontend/components/circuit-tree-editor.tsx` – Editorzustand,
|
- `src/frontend/components/circuit-tree-editor.tsx` – Editorzustand,
|
||||||
Befehlsausführung, Drag-and-drop und sitzungslokales Undo/Redo
|
Befehlsausführung, Drag-and-drop und der schrittweise Historien-Cutover
|
||||||
- `src/frontend/components/circuit-grid-*.ts` – reine Grid-Projektion,
|
- `src/frontend/components/circuit-grid-*.ts` – reine Grid-Projektion,
|
||||||
Zellbesitz, Einfügen und Sicherheitsregeln
|
Zellbesitz, Einfügen und Sicherheitsregeln
|
||||||
- `src/frontend/utils/api.ts` – typisierte Frontend-API-Aufrufe
|
- `src/frontend/utils/api.ts` – typisierte Frontend-API-Aufrufe
|
||||||
@@ -62,7 +62,8 @@ liegt über einen Host-Mount außerhalb des Containers.
|
|||||||
6. Das Frontend lädt den Circuit-Tree neu und stellt Auswahl beziehungsweise
|
6. Das Frontend lädt den Circuit-Tree neu und stellt Auswahl beziehungsweise
|
||||||
Viewport soweit möglich wieder her.
|
Viewport soweit möglich wieder her.
|
||||||
|
|
||||||
Die React-Historie ist derzeit sitzungslokal. Das Datenmodell besitzt bereits
|
Der sichtbare React-Historiestapel ist während des schrittweisen Cutovers noch
|
||||||
|
sitzungslokal. Das Datenmodell besitzt
|
||||||
einen projektbezogenen Revisionszähler sowie getrennte Revision-/Change-Set-
|
einen projektbezogenen Revisionszähler sowie getrennte Revision-/Change-Set-
|
||||||
Tabellen. Ein getestetes Repository kann diese Historienmetadaten optimistisch
|
Tabellen. Ein getestetes Repository kann diese Historienmetadaten optimistisch
|
||||||
und atomar fortschreiben. Vorwärts- und Rückwärtskommandos besitzen einen
|
und atomar fortschreiben. Vorwärts- und Rückwärtskommandos besitzen einen
|
||||||
@@ -85,8 +86,13 @@ inversen Kommando gesichert, sodass Undo dieselbe UUID und alle Fachwerte
|
|||||||
wiederherstellt. `circuit.insert` und `circuit.delete` behandeln einen
|
wiederherstellt. `circuit.insert` und `circuit.delete` behandeln einen
|
||||||
Stromkreis mit null, einer oder mehreren Gerätezeilen als vollständigen Block.
|
Stromkreis mit null, einer oder mehreren Gerätezeilen als vollständigen Block.
|
||||||
Undo bewahrt dabei sämtliche Circuit-/Row-UUIDs und ändert keine
|
Undo bewahrt dabei sämtliche Circuit-/Row-UUIDs und ändert keine
|
||||||
Betriebsmittelkennzeichen. Das Grid verwendet diese Endpunkte noch nicht; seine
|
Betriebsmittelkennzeichen. Bestehende Circuit- und Gerätezeilen-Zelländerungen
|
||||||
sichtbare Historie bleibt daher sitzungslokal.
|
verwendet das Grid bereits über die öffentliche Command-Grenze. Der Tree liefert
|
||||||
|
dazu `currentRevision`; Undo/Redo für diese Zellaktionen läuft über die
|
||||||
|
projektweite Serverhistorie. Die direkten PATCH-Endpunkte für diese Felder sind
|
||||||
|
entfernt. Andere Editoraktionen verbleiben vorerst im sichtbaren
|
||||||
|
sitzungslokalen Stack; nach einem Reload wird dieser noch nicht aus der
|
||||||
|
Serverhistorie rekonstruiert.
|
||||||
`circuit-device-row.move` verschiebt oder sortiert eine oder mehrere Zeilen
|
`circuit-device-row.move` verschiebt oder sortiert eine oder mehrere Zeilen
|
||||||
zwischen vorhandenen Stromkreisen derselben Liste. Erwartete und neue
|
zwischen vorhandenen Stromkreisen derselben Liste. Erwartete und neue
|
||||||
Stromkreis-/Sortierpositionen machen Forward und Inverse deterministisch;
|
Stromkreis-/Sortierpositionen machen Forward und Inverse deterministisch;
|
||||||
@@ -159,7 +165,8 @@ bei einem späteren Wechsel trotzdem einen eigenen PostgreSQL-Adapter.
|
|||||||
|
|
||||||
## Noch nicht unterstützt
|
## Noch nicht unterstützt
|
||||||
|
|
||||||
- persistentes Undo/Redo und Projektversionen
|
- vollständiges persistentes Undo/Redo, sichtbare Historie nach Reload und
|
||||||
|
benannte Projektstände
|
||||||
- Mehrbenutzerbetrieb und Konfliktauflösung
|
- Mehrbenutzerbetrieb und Konfliktauflösung
|
||||||
- Revit-/CSV-/IFCGUID-Round-trip
|
- Revit-/CSV-/IFCGUID-Round-trip
|
||||||
- vollständige elektrische Dimensionierung
|
- vollständige elektrische Dimensionierung
|
||||||
|
|||||||
@@ -173,6 +173,11 @@ Completed foundation:
|
|||||||
to SQLite
|
to SQLite
|
||||||
- public command, undo and redo endpoints require an expected revision; undo
|
- public command, undo and redo endpoints require an expected revision; undo
|
||||||
and redo reconstruct the eligible persisted inverse or forward command
|
and redo reconstruct the eligible persisted inverse or forward command
|
||||||
|
- the circuit tree returns the project's current revision; existing Circuit
|
||||||
|
and CircuitDeviceRow cell edits use the public command endpoint, while their
|
||||||
|
toolbar undo/redo actions use project-wide persistent history
|
||||||
|
- obsolete direct Circuit and CircuitDeviceRow field-update PATCH routes are
|
||||||
|
removed so these editor writes cannot bypass revision history
|
||||||
- `circuit-device-row.insert` and `circuit-device-row.delete` preserve stable
|
- `circuit-device-row.insert` and `circuit-device-row.delete` preserve stable
|
||||||
row ids, complete deleted-row snapshots and circuit reserve state through
|
row ids, complete deleted-row snapshots and circuit reserve state through
|
||||||
atomic insert/delete, revision and stack transitions
|
atomic insert/delete, revision and stack transitions
|
||||||
@@ -210,10 +215,10 @@ Remaining constraints before completing persistent history:
|
|||||||
is complete
|
is complete
|
||||||
- route the remaining structural and destructive domain
|
- route the remaining structural and destructive domain
|
||||||
writes through the shared command, revision and stack transaction boundary
|
writes through the shared command, revision and stack transaction boundary
|
||||||
- replace the session-local frontend command stack only after server-side
|
- finish routing structural and destructive circuit-editor operations through
|
||||||
command coverage is complete for structural and destructive circuit-editor
|
persistent commands, then reconstruct the visible frontend history state
|
||||||
operations; the ProjectDevice synchronization UI already uses its persistent
|
after reload; Circuit/CircuitDeviceRow cell edits and ProjectDevice
|
||||||
command and project-wide undo endpoint
|
synchronization already use persistent commands and project-wide undo
|
||||||
- do not model IFCGUID as an overloaded circuit equipment identifier
|
- do not model IFCGUID as an overloaded circuit equipment identifier
|
||||||
- keep database backup/restore checks separate from project history tests
|
- keep database backup/restore checks separate from project history tests
|
||||||
|
|
||||||
|
|||||||
@@ -390,6 +390,9 @@ Implemented foundation:
|
|||||||
- a central application dispatcher reconstructs persisted commands and exposes
|
- a central application dispatcher reconstructs persisted commands and exposes
|
||||||
optimistic command, undo and redo endpoints for Circuit and CircuitDeviceRow
|
optimistic command, undo and redo endpoints for Circuit and CircuitDeviceRow
|
||||||
field updates
|
field updates
|
||||||
|
- the circuit tree exposes `currentRevision`; existing Circuit and
|
||||||
|
CircuitDeviceRow cell edits use those optimistic commands and project-wide
|
||||||
|
undo/redo instead of direct field-update PATCH routes
|
||||||
- typed CircuitDeviceRow insert/delete commands preserve complete row snapshots
|
- typed CircuitDeviceRow insert/delete commands preserve complete row snapshots
|
||||||
and stable ids; row mutation, circuit reserve state, revision and history
|
and stable ids; row mutation, circuit reserve state, revision and history
|
||||||
stack transition commit or roll back together
|
stack transition commit or roll back together
|
||||||
@@ -424,10 +427,11 @@ Implemented foundation:
|
|||||||
- integration coverage verifies sequential revisions, stale-command rejection
|
- integration coverage verifies sequential revisions, stale-command rejection
|
||||||
and rollback after a forced late persistence failure
|
and rollback after a forced late persistence failure
|
||||||
|
|
||||||
ProjectDevice CRUD, synchronization and disconnect on the project page are
|
ProjectDevice CRUD, synchronization and disconnect on the project page and
|
||||||
connected to this history boundary. The circuit-list editor and the remaining
|
existing Circuit/CircuitDeviceRow cell edits in the circuit-list editor are
|
||||||
domain writes are not fully connected; its visible undo/redo therefore remains
|
connected to this history boundary. Remaining structural editor writes are not
|
||||||
session-local until the remaining tasks are implemented.
|
fully connected; the editor's visible undo/redo stack therefore remains
|
||||||
|
session-local and is not reconstructed after reload.
|
||||||
|
|
||||||
Acceptance criteria:
|
Acceptance criteria:
|
||||||
|
|
||||||
|
|||||||
@@ -7,15 +7,12 @@ import { circuits } from "../schema/circuits.js";
|
|||||||
import { distributionBoards } from "../schema/distribution-boards.js";
|
import { distributionBoards } from "../schema/distribution-boards.js";
|
||||||
import {
|
import {
|
||||||
toCircuitDeviceRowCreateValues,
|
toCircuitDeviceRowCreateValues,
|
||||||
toCircuitDeviceRowPatchValues,
|
|
||||||
toCircuitDeviceRowUpdateValues,
|
toCircuitDeviceRowUpdateValues,
|
||||||
type CircuitDeviceRowCreateInput,
|
type CircuitDeviceRowCreateInput,
|
||||||
type CircuitDeviceRowPatchInput,
|
|
||||||
type CircuitDeviceRowUpdateInput,
|
type CircuitDeviceRowUpdateInput,
|
||||||
} from "./circuit-device-row.persistence.js";
|
} from "./circuit-device-row.persistence.js";
|
||||||
export type {
|
export type {
|
||||||
CircuitDeviceRowCreateInput,
|
CircuitDeviceRowCreateInput,
|
||||||
CircuitDeviceRowPatchInput,
|
|
||||||
CircuitDeviceRowUpdateInput,
|
CircuitDeviceRowUpdateInput,
|
||||||
} from "./circuit-device-row.persistence.js";
|
} from "./circuit-device-row.persistence.js";
|
||||||
export { toCircuitDeviceRowCreateValues } from "./circuit-device-row.persistence.js";
|
export { toCircuitDeviceRowCreateValues } from "./circuit-device-row.persistence.js";
|
||||||
@@ -102,14 +99,6 @@ export class CircuitDeviceRowRepository {
|
|||||||
.where(eq(circuitDeviceRows.id, rowId));
|
.where(eq(circuitDeviceRows.id, rowId));
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateFields(rowId: string, input: CircuitDeviceRowPatchInput) {
|
|
||||||
const values = toCircuitDeviceRowPatchValues(input);
|
|
||||||
if (Object.keys(values).length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await db.update(circuitDeviceRows).set(values).where(eq(circuitDeviceRows.id, rowId));
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete(rowId: string) {
|
async delete(rowId: string) {
|
||||||
await db.delete(circuitDeviceRows).where(eq(circuitDeviceRows.id, rowId));
|
await db.delete(circuitDeviceRows).where(eq(circuitDeviceRows.id, rowId));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,18 +4,14 @@ import { db } from "../client.js";
|
|||||||
import { circuits } from "../schema/circuits.js";
|
import { circuits } from "../schema/circuits.js";
|
||||||
import {
|
import {
|
||||||
toCircuitCreateValues,
|
toCircuitCreateValues,
|
||||||
toCircuitPatchValues,
|
|
||||||
type CircuitCreatePersistenceInput,
|
type CircuitCreatePersistenceInput,
|
||||||
type CircuitPatchPersistenceInput,
|
|
||||||
} from "./circuit.persistence.js";
|
} from "./circuit.persistence.js";
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
CircuitCreatePersistenceInput,
|
CircuitCreatePersistenceInput,
|
||||||
CircuitPatchPersistenceInput,
|
|
||||||
} from "./circuit.persistence.js";
|
} from "./circuit.persistence.js";
|
||||||
export {
|
export {
|
||||||
toCircuitCreateValues,
|
toCircuitCreateValues,
|
||||||
toCircuitPatchValues,
|
|
||||||
} from "./circuit.persistence.js";
|
} from "./circuit.persistence.js";
|
||||||
|
|
||||||
export class CircuitRepository {
|
export class CircuitRepository {
|
||||||
@@ -84,14 +80,6 @@ export class CircuitRepository {
|
|||||||
.where(eq(circuits.id, circuitId));
|
.where(eq(circuits.id, circuitId));
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateFields(circuitId: string, input: CircuitPatchPersistenceInput) {
|
|
||||||
const values = toCircuitPatchValues(input);
|
|
||||||
if (Object.keys(values).length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await db.update(circuits).set(values).where(eq(circuits.id, circuitId));
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete(circuitId: string) {
|
async delete(circuitId: string) {
|
||||||
await db.delete(circuits).where(eq(circuits.id, circuitId));
|
await db.delete(circuits).where(eq(circuits.id, circuitId));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export interface CircuitTreeSectionBlock {
|
|||||||
|
|
||||||
export interface CircuitTreeResponse {
|
export interface CircuitTreeResponse {
|
||||||
circuitListId: string;
|
circuitListId: string;
|
||||||
|
currentRevision: number;
|
||||||
sections: CircuitTreeSectionBlock[];
|
sections: CircuitTreeSectionBlock[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,11 +13,8 @@ import type {
|
|||||||
MoveCircuitDeviceRowsBulkInput,
|
MoveCircuitDeviceRowsBulkInput,
|
||||||
ReorderSectionCircuitsInput,
|
ReorderSectionCircuitsInput,
|
||||||
UpdateSectionEquipmentIdentifiersInput,
|
UpdateSectionEquipmentIdentifiersInput,
|
||||||
UpdateCircuitDeviceRowInput,
|
|
||||||
UpdateCircuitInput,
|
|
||||||
} from "../../shared/validation/circuit.schemas.js";
|
} from "../../shared/validation/circuit.schemas.js";
|
||||||
import { CircuitNumberingService } from "./circuit-numbering.service.js";
|
import { CircuitNumberingService } from "./circuit-numbering.service.js";
|
||||||
import { deriveOverriddenFieldsForLocalEdit } from "./project-device-overrides.js";
|
|
||||||
|
|
||||||
export class CircuitWriteService {
|
export class CircuitWriteService {
|
||||||
private readonly circuitRepository: CircuitRepository;
|
private readonly circuitRepository: CircuitRepository;
|
||||||
@@ -191,21 +188,6 @@ export class CircuitWriteService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateCircuit(circuitId: string, input: UpdateCircuitInput) {
|
|
||||||
const current = await this.circuitRepository.findById(circuitId);
|
|
||||||
if (!current) {
|
|
||||||
throw new Error("Invalid circuit id.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const sectionId = input.sectionId ?? current.sectionId;
|
|
||||||
const equipmentIdentifier = input.equipmentIdentifier ?? current.equipmentIdentifier;
|
|
||||||
await this.assertSectionInList(sectionId, current.circuitListId);
|
|
||||||
await this.assertUniqueEquipmentIdentifier(current.circuitListId, equipmentIdentifier, circuitId);
|
|
||||||
|
|
||||||
await this.circuitRepository.updateFields(circuitId, input);
|
|
||||||
return this.circuitRepository.findById(circuitId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteCircuit(circuitId: string) {
|
async deleteCircuit(circuitId: string) {
|
||||||
const current = await this.circuitRepository.findById(circuitId);
|
const current = await this.circuitRepository.findById(circuitId);
|
||||||
if (!current) {
|
if (!current) {
|
||||||
@@ -246,23 +228,6 @@ export class CircuitWriteService {
|
|||||||
return this.deviceRowRepository.findById(rowId);
|
return this.deviceRowRepository.findById(rowId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateDeviceRow(rowId: string, input: UpdateCircuitDeviceRowInput) {
|
|
||||||
const current = await this.deviceRowRepository.findById(rowId);
|
|
||||||
if (!current) {
|
|
||||||
throw new Error("Invalid device row id.");
|
|
||||||
}
|
|
||||||
await this.assertValidLinkedProjectDevice(current.circuitId, input.linkedProjectDeviceId);
|
|
||||||
const overriddenFields = deriveOverriddenFieldsForLocalEdit(
|
|
||||||
current,
|
|
||||||
input
|
|
||||||
);
|
|
||||||
await this.deviceRowRepository.updateFields(rowId, {
|
|
||||||
...input,
|
|
||||||
...(overriddenFields !== undefined ? { overriddenFields } : {}),
|
|
||||||
});
|
|
||||||
return this.deviceRowRepository.findById(rowId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteDeviceRow(rowId: string) {
|
async deleteDeviceRow(rowId: string) {
|
||||||
const current = await this.deviceRowRepository.findById(rowId);
|
const current = await this.deviceRowRepository.findById(rowId);
|
||||||
if (!current) {
|
if (!current) {
|
||||||
|
|||||||
@@ -17,12 +17,11 @@ import {
|
|||||||
} from "../utils/circuit-grid-safety";
|
} from "../utils/circuit-grid-safety";
|
||||||
import {
|
import {
|
||||||
allColumns,
|
allColumns,
|
||||||
|
buildCircuitEditPatch,
|
||||||
|
buildDeviceRowEditPatch,
|
||||||
defaultVisibleColumnKeys,
|
defaultVisibleColumnKeys,
|
||||||
deviceFieldKeys,
|
deviceFieldKeys,
|
||||||
formatValue,
|
formatValue,
|
||||||
getCircuitValue,
|
|
||||||
getDeviceValue,
|
|
||||||
parseNumeric,
|
|
||||||
} from "../utils/circuit-grid-model";
|
} from "../utils/circuit-grid-model";
|
||||||
import type {
|
import type {
|
||||||
CellKey,
|
CellKey,
|
||||||
@@ -49,14 +48,17 @@ import {
|
|||||||
moveCircuitDeviceRowById,
|
moveCircuitDeviceRowById,
|
||||||
reorderSectionCircuits,
|
reorderSectionCircuits,
|
||||||
renumberCircuitSection,
|
renumberCircuitSection,
|
||||||
|
redoProjectCommand,
|
||||||
updateSectionEquipmentIdentifiers,
|
updateSectionEquipmentIdentifiers,
|
||||||
updateCircuitById,
|
updateCircuitById,
|
||||||
updateCircuitDeviceRowById,
|
updateCircuitDeviceRowById,
|
||||||
|
undoProjectCommand,
|
||||||
} from "../utils/api";
|
} from "../utils/api";
|
||||||
import type {
|
import type {
|
||||||
CircuitTreeCircuitDto,
|
CircuitTreeCircuitDto,
|
||||||
CircuitTreeDeviceRowDto,
|
CircuitTreeDeviceRowDto,
|
||||||
CircuitTreeResponseDto,
|
CircuitTreeResponseDto,
|
||||||
|
ProjectCommandResultDto,
|
||||||
ProjectDeviceDto,
|
ProjectDeviceDto,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
|
|
||||||
@@ -88,6 +90,10 @@ interface HistoryCommand {
|
|||||||
label: string;
|
label: string;
|
||||||
redo: () => Promise<SelectionIntent | null | void>;
|
redo: () => Promise<SelectionIntent | null | void>;
|
||||||
undo: () => Promise<SelectionIntent | null | void>;
|
undo: () => Promise<SelectionIntent | null | void>;
|
||||||
|
persistent?: {
|
||||||
|
undoIntent: () => SelectionIntent | null;
|
||||||
|
redoIntent: () => SelectionIntent | null;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CircuitSnapshot {
|
interface CircuitSnapshot {
|
||||||
@@ -163,6 +169,16 @@ function isPrintableKey(event: KeyboardEvent<HTMLElement>) {
|
|||||||
return event.key.length === 1 && !event.metaKey && !event.ctrlKey && !event.altKey;
|
return event.key.length === 1 && !event.metaKey && !event.ctrlKey && !event.altKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createValuesFromEditPatch(
|
||||||
|
patch: Record<string, unknown>
|
||||||
|
): Record<string, unknown> {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(patch).filter(
|
||||||
|
([, value]) => value !== null && value !== ""
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function CircuitTreeEditor(props: { projectId: string; circuitListId: string }) {
|
export function CircuitTreeEditor(props: { projectId: string; circuitListId: string }) {
|
||||||
/*
|
/*
|
||||||
Circuit-tree editor invariant overview:
|
Circuit-tree editor invariant overview:
|
||||||
@@ -201,7 +217,8 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
const [draggingCircuitId, setDraggingCircuitId] = useState<string | null>(null);
|
const [draggingCircuitId, setDraggingCircuitId] = useState<string | null>(null);
|
||||||
const [draggingCircuitIds, setDraggingCircuitIds] = useState<string[]>([]);
|
const [draggingCircuitIds, setDraggingCircuitIds] = useState<string[]>([]);
|
||||||
const [circuitReorderIntent, setCircuitReorderIntent] = useState<CircuitReorderDropIntent | null>(null);
|
const [circuitReorderIntent, setCircuitReorderIntent] = useState<CircuitReorderDropIntent | null>(null);
|
||||||
// Undo/redo history is session-local UI state (not persisted server-side).
|
// The visible stack is session-local during the cutover. Circuit/device field
|
||||||
|
// commands already use the persistent project-wide server history.
|
||||||
const [undoStack, setUndoStack] = useState<HistoryCommand[]>([]);
|
const [undoStack, setUndoStack] = useState<HistoryCommand[]>([]);
|
||||||
const [redoStack, setRedoStack] = useState<HistoryCommand[]>([]);
|
const [redoStack, setRedoStack] = useState<HistoryCommand[]>([]);
|
||||||
const [historyBusy, setHistoryBusy] = useState(false);
|
const [historyBusy, setHistoryBusy] = useState(false);
|
||||||
@@ -223,6 +240,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
const focusTokenRef = useRef(1);
|
const focusTokenRef = useRef(1);
|
||||||
|
const projectRevisionRef = useRef<number | null>(null);
|
||||||
|
|
||||||
const orderedColumns = useMemo(
|
const orderedColumns = useMemo(
|
||||||
() => columnOrder.map((key) => allColumns.find((column) => column.key === key)).filter(Boolean) as ColumnDef[],
|
() => columnOrder.map((key) => allColumns.find((column) => column.key === key)).filter(Boolean) as ColumnDef[],
|
||||||
@@ -241,6 +259,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const tree = await getCircuitTree(projectId, circuitListId);
|
const tree = await getCircuitTree(projectId, circuitListId);
|
||||||
|
projectRevisionRef.current = tree.currentRevision;
|
||||||
setData(tree);
|
setData(tree);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(normalizeUiError(err));
|
setError(normalizeUiError(err));
|
||||||
@@ -767,6 +786,25 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getExpectedProjectRevision() {
|
||||||
|
if (projectRevisionRef.current === null) {
|
||||||
|
throw new Error("Der Projektstand ist noch nicht geladen.");
|
||||||
|
}
|
||||||
|
return projectRevisionRef.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyProjectCommandResult(result: ProjectCommandResultDto) {
|
||||||
|
projectRevisionRef.current = result.history.currentRevision;
|
||||||
|
setData((current) =>
|
||||||
|
current
|
||||||
|
? {
|
||||||
|
...current,
|
||||||
|
currentRevision: result.history.currentRevision,
|
||||||
|
}
|
||||||
|
: current
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Runs a normal command and records it in session-local history.
|
// Runs a normal command and records it in session-local history.
|
||||||
async function runCommand(command: HistoryCommand) {
|
async function runCommand(command: HistoryCommand) {
|
||||||
try {
|
try {
|
||||||
@@ -790,7 +828,29 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
setError(null);
|
setError(null);
|
||||||
setHistoryBusy(true);
|
setHistoryBusy(true);
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
const intent = mode === "undo" ? await command.undo() : await command.redo();
|
let intent: SelectionIntent | null | void;
|
||||||
|
if (command.persistent) {
|
||||||
|
const result =
|
||||||
|
mode === "undo"
|
||||||
|
? await undoProjectCommand(
|
||||||
|
projectId,
|
||||||
|
getExpectedProjectRevision()
|
||||||
|
)
|
||||||
|
: await redoProjectCommand(
|
||||||
|
projectId,
|
||||||
|
getExpectedProjectRevision()
|
||||||
|
);
|
||||||
|
applyProjectCommandResult(result);
|
||||||
|
intent =
|
||||||
|
mode === "undo"
|
||||||
|
? command.persistent.undoIntent()
|
||||||
|
: command.persistent.redoIntent();
|
||||||
|
} else {
|
||||||
|
intent =
|
||||||
|
mode === "undo"
|
||||||
|
? await command.undo()
|
||||||
|
: await command.redo();
|
||||||
|
}
|
||||||
await reloadWithIntent(intent ?? null);
|
await reloadWithIntent(intent ?? null);
|
||||||
if (mode === "undo") {
|
if (mode === "undo") {
|
||||||
setUndoStack((current) => current.slice(0, -1));
|
setUndoStack((current) => current.slice(0, -1));
|
||||||
@@ -1145,46 +1205,26 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
setSelectedCell({ rowKey: targetRowKey, cellKey: best });
|
setSelectedCell({ rowKey: targetRowKey, cellKey: best });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Maps generic grid cell edits to circuit PATCH payload fields.
|
// Maps grid edits to typed persistent project commands.
|
||||||
async function patchCircuit(circuitId: string, key: CellKey, draft: string) {
|
async function patchCircuit(circuitId: string, key: CellKey, draft: string) {
|
||||||
const payload: Record<string, unknown> = {};
|
const result = await updateCircuitById(
|
||||||
if (key === "protectionSummary" || key === "cableSummary") {
|
projectId,
|
||||||
payload.remark = draft.trim() === "" ? undefined : draft;
|
getExpectedProjectRevision(),
|
||||||
} else if (["protectionRatedCurrent", "cableLength", "voltage"].includes(key)) {
|
circuitId,
|
||||||
payload[key] = parseNumeric(key, draft);
|
buildCircuitEditPatch(key, draft)
|
||||||
} else if (key === "isReserve") {
|
);
|
||||||
const normalized = draft.trim().toLowerCase();
|
applyProjectCommandResult(result);
|
||||||
payload.isReserve = ["1", "true", "yes", "ja"].includes(normalized);
|
|
||||||
} else {
|
|
||||||
payload[key] = draft.trim() === "" ? undefined : draft;
|
|
||||||
}
|
|
||||||
await updateCircuitById(circuitId, payload);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Maps generic grid cell edits to device-row PATCH payload fields.
|
// Device-row commands derive local ProjectDevice override metadata server-side.
|
||||||
async function patchDeviceRow(rowId: string, key: CellKey, draft: string) {
|
async function patchDeviceRow(rowId: string, key: CellKey, draft: string) {
|
||||||
const payload: Record<string, unknown> = {};
|
const result = await updateCircuitDeviceRowById(
|
||||||
if (["quantity", "powerPerUnit", "simultaneityFactor", "cosPhi"].includes(key)) {
|
projectId,
|
||||||
payload[key] = parseNumeric(key, draft);
|
getExpectedProjectRevision(),
|
||||||
} else if (key === "roomSummary") {
|
rowId,
|
||||||
const trimmed = draft.trim();
|
buildDeviceRowEditPatch(key, draft)
|
||||||
if (!trimmed) {
|
);
|
||||||
payload.roomNumberSnapshot = undefined;
|
applyProjectCommandResult(result);
|
||||||
payload.roomNameSnapshot = undefined;
|
|
||||||
} else {
|
|
||||||
const firstSpace = trimmed.indexOf(" ");
|
|
||||||
if (firstSpace > 0) {
|
|
||||||
payload.roomNumberSnapshot = trimmed.slice(0, firstSpace).trim();
|
|
||||||
payload.roomNameSnapshot = trimmed.slice(firstSpace + 1).trim();
|
|
||||||
} else {
|
|
||||||
payload.roomNumberSnapshot = trimmed;
|
|
||||||
payload.roomNameSnapshot = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
payload[key] = draft.trim() === "" ? undefined : draft;
|
|
||||||
}
|
|
||||||
await updateCircuitDeviceRowById(rowId, payload);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a real circuit from virtual placeholder row.
|
// Creates a real circuit from virtual placeholder row.
|
||||||
@@ -1204,6 +1244,9 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
const isDeviceField = deviceFieldKeys.has(key);
|
const isDeviceField = deviceFieldKeys.has(key);
|
||||||
|
|
||||||
if (isDeviceField) {
|
if (isDeviceField) {
|
||||||
|
const editValues = createValuesFromEditPatch(
|
||||||
|
buildDeviceRowEditPatch(key, draft)
|
||||||
|
);
|
||||||
const created = await createCircuitWithDeviceRows(projectId, circuitListId, {
|
const created = await createCircuitWithDeviceRows(projectId, circuitListId, {
|
||||||
circuit: {
|
circuit: {
|
||||||
sectionId,
|
sectionId,
|
||||||
@@ -1221,37 +1264,28 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
powerPerUnit: 0,
|
powerPerUnit: 0,
|
||||||
simultaneityFactor: 1,
|
simultaneityFactor: 1,
|
||||||
cosPhi: 1,
|
cosPhi: 1,
|
||||||
|
...editValues,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
const createdCircuit = created.circuit;
|
const createdCircuit = created.circuit;
|
||||||
const createdRow = created.deviceRows[0];
|
|
||||||
await patchDeviceRow(createdRow.id, key, draft);
|
|
||||||
return { rowKey: `circuitCompact:${createdCircuit.id}`, cellKey: key };
|
return { rowKey: `circuitCompact:${createdCircuit.id}`, cellKey: key };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const editValues = createValuesFromEditPatch(
|
||||||
|
buildCircuitEditPatch(key, draft)
|
||||||
|
);
|
||||||
const createdCircuit = (await createCircuit(projectId, circuitListId, {
|
const createdCircuit = (await createCircuit(projectId, circuitListId, {
|
||||||
sectionId,
|
sectionId,
|
||||||
equipmentIdentifier: next.nextIdentifier,
|
equipmentIdentifier: next.nextIdentifier,
|
||||||
displayName: "Neuer Stromkreis",
|
displayName: "Neuer Stromkreis",
|
||||||
sortOrder,
|
sortOrder,
|
||||||
isReserve: true,
|
isReserve: true,
|
||||||
|
...editValues,
|
||||||
})) as CircuitTreeCircuitDto;
|
})) as CircuitTreeCircuitDto;
|
||||||
await patchCircuit(createdCircuit.id, key, draft);
|
|
||||||
return { rowKey: `reserveCircuit:${createdCircuit.id}`, cellKey: key };
|
return { rowKey: `reserveCircuit:${createdCircuit.id}`, cellKey: key };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reads current persisted value into editable draft text for reversible edit commands.
|
|
||||||
function getCurrentDraftForCell(row: VisibleGridRow, cellKey: CellKey, kind: CellKind) {
|
|
||||||
if (kind === "deviceField" && row.device) {
|
|
||||||
return String(getDeviceValue(row.device, cellKey) ?? "");
|
|
||||||
}
|
|
||||||
if (kind === "circuitField" && row.circuit) {
|
|
||||||
return String(getCircuitValue(row.circuit, cellKey) ?? "");
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Commits the active editor input through command history so edits remain undoable.
|
// Commits the active editor input through command history so edits remain undoable.
|
||||||
async function commitEdit(direction: SaveDirection = "stay") {
|
async function commitEdit(direction: SaveDirection = "stay") {
|
||||||
if (!editingCell) {
|
if (!editingCell) {
|
||||||
@@ -1316,7 +1350,6 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (cell.kind === "circuitField" && row.circuit) {
|
if (cell.kind === "circuitField" && row.circuit) {
|
||||||
const previous = getCurrentDraftForCell(row, editingCell.cellKey, cell.kind);
|
|
||||||
const next = editingCell.draft;
|
const next = editingCell.draft;
|
||||||
const command: HistoryCommand = {
|
const command: HistoryCommand = {
|
||||||
label: "Stromkreisfeld bearbeiten",
|
label: "Stromkreisfeld bearbeiten",
|
||||||
@@ -1324,9 +1357,18 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
await patchCircuit(row.circuit!.id, editingCell.cellKey, next);
|
await patchCircuit(row.circuit!.id, editingCell.cellKey, next);
|
||||||
return nextSelectionIntent();
|
return nextSelectionIntent();
|
||||||
},
|
},
|
||||||
undo: async () => {
|
undo: async () =>
|
||||||
await patchCircuit(row.circuit!.id, editingCell.cellKey, previous);
|
buildSelectionIntent({
|
||||||
return buildSelectionIntent({ rowKey: row.rowKey, cellKey: editingCell.cellKey });
|
rowKey: row.rowKey,
|
||||||
|
cellKey: editingCell.cellKey,
|
||||||
|
}),
|
||||||
|
persistent: {
|
||||||
|
undoIntent: () =>
|
||||||
|
buildSelectionIntent({
|
||||||
|
rowKey: row.rowKey,
|
||||||
|
cellKey: editingCell.cellKey,
|
||||||
|
}),
|
||||||
|
redoIntent: nextSelectionIntent,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
setEditingCell(null);
|
setEditingCell(null);
|
||||||
@@ -1335,7 +1377,6 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (cell.kind === "deviceField" && row.device) {
|
if (cell.kind === "deviceField" && row.device) {
|
||||||
const previous = getCurrentDraftForCell(row, editingCell.cellKey, cell.kind);
|
|
||||||
const next = editingCell.draft;
|
const next = editingCell.draft;
|
||||||
const command: HistoryCommand = {
|
const command: HistoryCommand = {
|
||||||
label: "Gerätefeld bearbeiten",
|
label: "Gerätefeld bearbeiten",
|
||||||
@@ -1343,9 +1384,18 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
await patchDeviceRow(row.device!.id, editingCell.cellKey, next);
|
await patchDeviceRow(row.device!.id, editingCell.cellKey, next);
|
||||||
return nextSelectionIntent();
|
return nextSelectionIntent();
|
||||||
},
|
},
|
||||||
undo: async () => {
|
undo: async () =>
|
||||||
await patchDeviceRow(row.device!.id, editingCell.cellKey, previous);
|
buildSelectionIntent({
|
||||||
return buildSelectionIntent({ rowKey: `device:${row.device!.id}`, cellKey: editingCell.cellKey });
|
rowKey: `device:${row.device!.id}`,
|
||||||
|
cellKey: editingCell.cellKey,
|
||||||
|
}),
|
||||||
|
persistent: {
|
||||||
|
undoIntent: () =>
|
||||||
|
buildSelectionIntent({
|
||||||
|
rowKey: `device:${row.device!.id}`,
|
||||||
|
cellKey: editingCell.cellKey,
|
||||||
|
}),
|
||||||
|
redoIntent: nextSelectionIntent,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
setEditingCell(null);
|
setEditingCell(null);
|
||||||
@@ -1359,6 +1409,9 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
const command: HistoryCommand = {
|
const command: HistoryCommand = {
|
||||||
label: "Gerätezeile im Reservestromkreis erstellen",
|
label: "Gerätezeile im Reservestromkreis erstellen",
|
||||||
redo: async () => {
|
redo: async () => {
|
||||||
|
const editValues = createValuesFromEditPatch(
|
||||||
|
buildDeviceRowEditPatch(editingCell.cellKey, next)
|
||||||
|
);
|
||||||
const created = (await createCircuitDeviceRow(row.circuit!.id, {
|
const created = (await createCircuitDeviceRow(row.circuit!.id, {
|
||||||
name: "Reserveverbraucher",
|
name: "Reserveverbraucher",
|
||||||
displayName: "Reserveverbraucher",
|
displayName: "Reserveverbraucher",
|
||||||
@@ -1367,9 +1420,9 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
powerPerUnit: 0,
|
powerPerUnit: 0,
|
||||||
simultaneityFactor: 1,
|
simultaneityFactor: 1,
|
||||||
cosPhi: 1,
|
cosPhi: 1,
|
||||||
|
...editValues,
|
||||||
})) as { id: string };
|
})) as { id: string };
|
||||||
createdRowId = created.id;
|
createdRowId = created.id;
|
||||||
await patchDeviceRow(created.id, editingCell.cellKey, next);
|
|
||||||
targetSelection = { rowKey: `circuitCompact:${row.circuit!.id}`, cellKey: editingCell.cellKey };
|
targetSelection = { rowKey: `circuitCompact:${row.circuit!.id}`, cellKey: editingCell.cellKey };
|
||||||
return nextSelectionIntent();
|
return nextSelectionIntent();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -240,6 +240,7 @@ export interface CircuitTreeMigrationReportDto {
|
|||||||
|
|
||||||
export interface CircuitTreeResponseDto {
|
export interface CircuitTreeResponseDto {
|
||||||
circuitListId: string;
|
circuitListId: string;
|
||||||
|
currentRevision: number;
|
||||||
sections: CircuitTreeSectionDto[];
|
sections: CircuitTreeSectionDto[];
|
||||||
migrationReport?: CircuitTreeMigrationReportDto;
|
migrationReport?: CircuitTreeMigrationReportDto;
|
||||||
}
|
}
|
||||||
@@ -264,8 +265,6 @@ export interface CreateCircuitInputDto {
|
|||||||
remark?: string;
|
remark?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UpdateCircuitInputDto = Partial<CreateCircuitInputDto>;
|
|
||||||
|
|
||||||
export interface CreateCircuitDeviceRowInputDto {
|
export interface CreateCircuitDeviceRowInputDto {
|
||||||
linkedProjectDeviceId?: string;
|
linkedProjectDeviceId?: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -287,5 +286,4 @@ export interface CreateCircuitDeviceRowInputDto {
|
|||||||
sortOrder?: number;
|
sortOrder?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UpdateCircuitDeviceRowInputDto = Partial<CreateCircuitDeviceRowInputDto>;
|
|
||||||
import type { ProjectDeviceSyncField } from "../shared/constants/project-device-sync-fields";
|
import type { ProjectDeviceSyncField } from "../shared/constants/project-device-sync-fields";
|
||||||
|
|||||||
+32
-12
@@ -20,11 +20,17 @@ import type {
|
|||||||
CircuitTreeCircuitDto,
|
CircuitTreeCircuitDto,
|
||||||
CircuitTreeDeviceRowDto,
|
CircuitTreeDeviceRowDto,
|
||||||
CreateCircuitInputDto,
|
CreateCircuitInputDto,
|
||||||
UpdateCircuitInputDto,
|
|
||||||
CreateCircuitDeviceRowInputDto,
|
CreateCircuitDeviceRowInputDto,
|
||||||
UpdateCircuitDeviceRowInputDto,
|
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import type { ProjectDeviceSyncField } from "../../shared/constants/project-device-sync-fields";
|
import type { ProjectDeviceSyncField } from "../../shared/constants/project-device-sync-fields";
|
||||||
|
import {
|
||||||
|
createCircuitUpdateProjectCommand,
|
||||||
|
type CircuitUpdatePatch,
|
||||||
|
} from "../../domain/models/circuit-project-command.model";
|
||||||
|
import {
|
||||||
|
createCircuitDeviceRowUpdateProjectCommand,
|
||||||
|
type CircuitDeviceRowUpdatePatch,
|
||||||
|
} from "../../domain/models/circuit-device-row-project-command.model";
|
||||||
|
|
||||||
async function request<T>(url: string, init?: RequestInit): Promise<T> {
|
async function request<T>(url: string, init?: RequestInit): Promise<T> {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
@@ -180,11 +186,18 @@ export function createCircuitWithDeviceRows(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateCircuitById(circuitId: string, input: UpdateCircuitInputDto) {
|
export function updateCircuitById(
|
||||||
return request(`/api/circuits/${circuitId}`, {
|
projectId: string,
|
||||||
method: "PATCH",
|
expectedRevision: number,
|
||||||
body: JSON.stringify(input),
|
circuitId: string,
|
||||||
});
|
input: CircuitUpdatePatch
|
||||||
|
) {
|
||||||
|
return executeProjectCommand(
|
||||||
|
projectId,
|
||||||
|
expectedRevision,
|
||||||
|
createCircuitUpdateProjectCommand(circuitId, input),
|
||||||
|
"Stromkreisfeld bearbeiten"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteCircuitById(circuitId: string) {
|
export function deleteCircuitById(circuitId: string) {
|
||||||
@@ -206,11 +219,18 @@ export function createCircuitDeviceRow(circuitId: string, input: CreateCircuitDe
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateCircuitDeviceRowById(rowId: string, input: UpdateCircuitDeviceRowInputDto) {
|
export function updateCircuitDeviceRowById(
|
||||||
return request(`/api/circuit-device-rows/${rowId}`, {
|
projectId: string,
|
||||||
method: "PATCH",
|
expectedRevision: number,
|
||||||
body: JSON.stringify(input),
|
rowId: string,
|
||||||
});
|
input: CircuitDeviceRowUpdatePatch
|
||||||
|
) {
|
||||||
|
return executeProjectCommand(
|
||||||
|
projectId,
|
||||||
|
expectedRevision,
|
||||||
|
createCircuitDeviceRowUpdateProjectCommand(rowId, input),
|
||||||
|
"Gerätezeilenfeld bearbeiten"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteCircuitDeviceRowById(rowId: string) {
|
export function deleteCircuitDeviceRowById(rowId: string) {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import type { CircuitTreeCircuitDto, CircuitTreeDeviceRowDto } from "../types.js";
|
import type { CircuitTreeCircuitDto, CircuitTreeDeviceRowDto } from "../types.js";
|
||||||
|
import type { CircuitUpdatePatch } from "../../domain/models/circuit-project-command.model.js";
|
||||||
|
import type { CircuitDeviceRowUpdatePatch } from "../../domain/models/circuit-device-row-project-command.model.js";
|
||||||
|
|
||||||
export type CellKey =
|
export type CellKey =
|
||||||
| "equipmentIdentifier"
|
| "equipmentIdentifier"
|
||||||
@@ -189,6 +191,78 @@ export function parseNumeric(cellKey: CellKey, draft: string): number | undefine
|
|||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildCircuitEditPatch(
|
||||||
|
key: CellKey,
|
||||||
|
draft: string
|
||||||
|
): CircuitUpdatePatch {
|
||||||
|
if (key === "protectionSummary" || key === "cableSummary") {
|
||||||
|
return { remark: draft.trim() || null };
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
key === "protectionRatedCurrent" ||
|
||||||
|
key === "cableLength" ||
|
||||||
|
key === "voltage"
|
||||||
|
) {
|
||||||
|
return { [key]: parseNumeric(key, draft) ?? null };
|
||||||
|
}
|
||||||
|
if (key === "isReserve") {
|
||||||
|
const normalized = draft.trim().toLowerCase();
|
||||||
|
return {
|
||||||
|
isReserve: ["1", "true", "yes", "ja"].includes(normalized),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (key === "equipmentIdentifier") {
|
||||||
|
return { equipmentIdentifier: draft.trim() };
|
||||||
|
}
|
||||||
|
return { [key]: draft.trim() || null } as CircuitUpdatePatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildDeviceRowEditPatch(
|
||||||
|
key: CellKey,
|
||||||
|
draft: string
|
||||||
|
): CircuitDeviceRowUpdatePatch {
|
||||||
|
if (
|
||||||
|
key === "quantity" ||
|
||||||
|
key === "powerPerUnit" ||
|
||||||
|
key === "simultaneityFactor"
|
||||||
|
) {
|
||||||
|
const value = parseNumeric(key, draft);
|
||||||
|
if (value === undefined) {
|
||||||
|
throw new Error("Dieses Zahlenfeld darf nicht leer sein.");
|
||||||
|
}
|
||||||
|
return { [key]: value };
|
||||||
|
}
|
||||||
|
if (key === "cosPhi") {
|
||||||
|
return { cosPhi: parseNumeric(key, draft) ?? null };
|
||||||
|
}
|
||||||
|
if (key === "roomSummary") {
|
||||||
|
const trimmed = draft.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return {
|
||||||
|
roomNumberSnapshot: null,
|
||||||
|
roomNameSnapshot: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const firstSpace = trimmed.indexOf(" ");
|
||||||
|
return firstSpace > 0
|
||||||
|
? {
|
||||||
|
roomNumberSnapshot: trimmed.slice(0, firstSpace).trim(),
|
||||||
|
roomNameSnapshot: trimmed.slice(firstSpace + 1).trim() || null,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
roomNumberSnapshot: trimmed,
|
||||||
|
roomNameSnapshot: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (key === "technicalName") {
|
||||||
|
return { name: draft.trim() };
|
||||||
|
}
|
||||||
|
if (key === "displayName") {
|
||||||
|
return { displayName: draft.trim() };
|
||||||
|
}
|
||||||
|
return { [key]: draft.trim() || null } as CircuitDeviceRowUpdatePatch;
|
||||||
|
}
|
||||||
|
|
||||||
export function getDeviceValue(device: CircuitTreeDeviceRowDto, key: CellKey): GridValue {
|
export function getDeviceValue(device: CircuitTreeDeviceRowDto, key: CellKey): GridValue {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case "technicalName": return device.name;
|
case "technicalName": return device.name;
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
createCircuitDeviceRowSchema,
|
createCircuitDeviceRowSchema,
|
||||||
moveCircuitDeviceRowsBulkSchema,
|
moveCircuitDeviceRowsBulkSchema,
|
||||||
moveCircuitDeviceRowSchema,
|
moveCircuitDeviceRowSchema,
|
||||||
updateCircuitDeviceRowSchema,
|
|
||||||
} from "../../shared/validation/circuit.schemas.js";
|
} from "../../shared/validation/circuit.schemas.js";
|
||||||
|
|
||||||
export async function createCircuitDeviceRow(req: Request, res: Response) {
|
export async function createCircuitDeviceRow(req: Request, res: Response) {
|
||||||
@@ -26,25 +25,6 @@ export async function createCircuitDeviceRow(req: Request, res: Response) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateCircuitDeviceRow(req: Request, res: Response) {
|
|
||||||
const { rowId } = req.params;
|
|
||||||
if (typeof rowId !== "string") {
|
|
||||||
return res.status(400).json({ error: "Invalid rowId" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsed = updateCircuitDeviceRowSchema.safeParse(req.body);
|
|
||||||
if (!parsed.success) {
|
|
||||||
return res.status(400).json({ error: parsed.error.flatten() });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const updated = await circuitWriteService.updateDeviceRow(rowId, parsed.data);
|
|
||||||
return res.json(updated);
|
|
||||||
} catch (error) {
|
|
||||||
return res.status(400).json({ error: error instanceof Error ? error.message : "Failed to update device row." });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteCircuitDeviceRow(req: Request, res: Response) {
|
export async function deleteCircuitDeviceRow(req: Request, res: Response) {
|
||||||
const { rowId } = req.params;
|
const { rowId } = req.params;
|
||||||
if (typeof rowId !== "string") {
|
if (typeof rowId !== "string") {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { CircuitRepository } from "../../db/repositories/circuit.repository.js";
|
|||||||
import { CircuitDeviceRowRepository } from "../../db/repositories/circuit-device-row.repository.js";
|
import { CircuitDeviceRowRepository } from "../../db/repositories/circuit-device-row.repository.js";
|
||||||
import { CircuitListRepository } from "../../db/repositories/circuit-list.repository.js";
|
import { CircuitListRepository } from "../../db/repositories/circuit-list.repository.js";
|
||||||
import { CircuitSectionRepository } from "../../db/repositories/circuit-section.repository.js";
|
import { CircuitSectionRepository } from "../../db/repositories/circuit-section.repository.js";
|
||||||
|
import { ProjectRepository } from "../../db/repositories/project.repository.js";
|
||||||
import {
|
import {
|
||||||
calculateCircuitTotalPower,
|
calculateCircuitTotalPower,
|
||||||
calculateRowTotalPower,
|
calculateRowTotalPower,
|
||||||
@@ -13,6 +14,7 @@ const circuitListRepository = new CircuitListRepository();
|
|||||||
const circuitSectionRepository = new CircuitSectionRepository();
|
const circuitSectionRepository = new CircuitSectionRepository();
|
||||||
const circuitRepository = new CircuitRepository();
|
const circuitRepository = new CircuitRepository();
|
||||||
const circuitDeviceRowRepository = new CircuitDeviceRowRepository();
|
const circuitDeviceRowRepository = new CircuitDeviceRowRepository();
|
||||||
|
const projectRepository = new ProjectRepository();
|
||||||
|
|
||||||
export function isMissingCircuitTreeSchemaError(error: unknown): boolean {
|
export function isMissingCircuitTreeSchemaError(error: unknown): boolean {
|
||||||
if (!(error instanceof Error)) {
|
if (!(error instanceof Error)) {
|
||||||
@@ -35,6 +37,10 @@ export async function getCircuitTree(req: Request, res: Response) {
|
|||||||
if (!list) {
|
if (!list) {
|
||||||
return res.status(404).json({ error: "Circuit list not found" });
|
return res.status(404).json({ error: "Circuit list not found" });
|
||||||
}
|
}
|
||||||
|
const project = await projectRepository.findById(projectId);
|
||||||
|
if (!project) {
|
||||||
|
return res.status(404).json({ error: "Project not found" });
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const sections = await circuitSectionRepository.listByCircuitList(circuitListId);
|
const sections = await circuitSectionRepository.listByCircuitList(circuitListId);
|
||||||
@@ -52,6 +58,7 @@ export async function getCircuitTree(req: Request, res: Response) {
|
|||||||
|
|
||||||
const tree: CircuitTreeResponse = {
|
const tree: CircuitTreeResponse = {
|
||||||
circuitListId,
|
circuitListId,
|
||||||
|
currentRevision: project.currentRevision,
|
||||||
sections: sections.map((section) => ({
|
sections: sections.map((section) => ({
|
||||||
id: section.id,
|
id: section.id,
|
||||||
key: section.key,
|
key: section.key,
|
||||||
@@ -123,6 +130,7 @@ export async function getCircuitTree(req: Request, res: Response) {
|
|||||||
if (isMissingCircuitTreeSchemaError(error)) {
|
if (isMissingCircuitTreeSchemaError(error)) {
|
||||||
return res.json({
|
return res.json({
|
||||||
circuitListId,
|
circuitListId,
|
||||||
|
currentRevision: project.currentRevision,
|
||||||
sections: [],
|
sections: [],
|
||||||
warning:
|
warning:
|
||||||
"Circuit-first tables are not available yet. Run database migrations (including 0008_circuit_first_model).",
|
"Circuit-first tables are not available yet. Run database migrations (including 0008_circuit_first_model).",
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { circuitWriteService } from "../composition/circuit-write-service.js";
|
|||||||
import {
|
import {
|
||||||
createCircuitSchema,
|
createCircuitSchema,
|
||||||
createCircuitWithDeviceRowsSchema,
|
createCircuitWithDeviceRowsSchema,
|
||||||
updateCircuitSchema,
|
|
||||||
} from "../../shared/validation/circuit.schemas.js";
|
} from "../../shared/validation/circuit.schemas.js";
|
||||||
|
|
||||||
export async function createCircuit(req: Request, res: Response) {
|
export async function createCircuit(req: Request, res: Response) {
|
||||||
@@ -53,25 +52,6 @@ export async function createCircuitWithDeviceRows(req: Request, res: Response) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateCircuit(req: Request, res: Response) {
|
|
||||||
const { circuitId } = req.params;
|
|
||||||
if (typeof circuitId !== "string") {
|
|
||||||
return res.status(400).json({ error: "Invalid circuitId" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsed = updateCircuitSchema.safeParse(req.body);
|
|
||||||
if (!parsed.success) {
|
|
||||||
return res.status(400).json({ error: parsed.error.flatten() });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const updated = await circuitWriteService.updateCircuit(circuitId, parsed.data);
|
|
||||||
return res.json(updated);
|
|
||||||
} catch (error) {
|
|
||||||
return res.status(400).json({ error: error instanceof Error ? error.message : "Failed to update circuit." });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteCircuit(req: Request, res: Response) {
|
export async function deleteCircuit(req: Request, res: Response) {
|
||||||
const { circuitId } = req.params;
|
const { circuitId } = req.params;
|
||||||
if (typeof circuitId !== "string") {
|
if (typeof circuitId !== "string") {
|
||||||
|
|||||||
@@ -3,12 +3,10 @@ import {
|
|||||||
deleteCircuitDeviceRow,
|
deleteCircuitDeviceRow,
|
||||||
moveCircuitDeviceRowsBulk,
|
moveCircuitDeviceRowsBulk,
|
||||||
moveCircuitDeviceRow,
|
moveCircuitDeviceRow,
|
||||||
updateCircuitDeviceRow,
|
|
||||||
} from "../controllers/circuit-device-row.controller.js";
|
} from "../controllers/circuit-device-row.controller.js";
|
||||||
|
|
||||||
export const circuitDeviceRowRouter = Router();
|
export const circuitDeviceRowRouter = Router();
|
||||||
|
|
||||||
circuitDeviceRowRouter.patch("/circuit-device-rows/move-bulk", moveCircuitDeviceRowsBulk);
|
circuitDeviceRowRouter.patch("/circuit-device-rows/move-bulk", moveCircuitDeviceRowsBulk);
|
||||||
circuitDeviceRowRouter.patch("/circuit-device-rows/:rowId", updateCircuitDeviceRow);
|
|
||||||
circuitDeviceRowRouter.patch("/circuit-device-rows/:rowId/move", moveCircuitDeviceRow);
|
circuitDeviceRowRouter.patch("/circuit-device-rows/:rowId/move", moveCircuitDeviceRow);
|
||||||
circuitDeviceRowRouter.delete("/circuit-device-rows/:rowId", deleteCircuitDeviceRow);
|
circuitDeviceRowRouter.delete("/circuit-device-rows/:rowId", deleteCircuitDeviceRow);
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
createCircuitWithDeviceRows,
|
createCircuitWithDeviceRows,
|
||||||
deleteCircuit,
|
deleteCircuit,
|
||||||
getNextCircuitIdentifier,
|
getNextCircuitIdentifier,
|
||||||
updateCircuit,
|
|
||||||
} from "../controllers/circuit.controller.js";
|
} from "../controllers/circuit.controller.js";
|
||||||
import { createCircuitDeviceRow } from "../controllers/circuit-device-row.controller.js";
|
import { createCircuitDeviceRow } from "../controllers/circuit-device-row.controller.js";
|
||||||
|
|
||||||
@@ -15,7 +14,6 @@ circuitRouter.post(
|
|||||||
"/projects/:projectId/circuit-lists/:circuitListId/circuits-with-device-rows",
|
"/projects/:projectId/circuit-lists/:circuitListId/circuits-with-device-rows",
|
||||||
createCircuitWithDeviceRows
|
createCircuitWithDeviceRows
|
||||||
);
|
);
|
||||||
circuitRouter.patch("/circuits/:circuitId", updateCircuit);
|
|
||||||
circuitRouter.delete("/circuits/:circuitId", deleteCircuit);
|
circuitRouter.delete("/circuits/:circuitId", deleteCircuit);
|
||||||
circuitRouter.get("/circuit-sections/:sectionId/next-identifier", getNextCircuitIdentifier);
|
circuitRouter.get("/circuit-sections/:sectionId/next-identifier", getNextCircuitIdentifier);
|
||||||
circuitRouter.post("/circuits/:circuitId/device-rows", createCircuitDeviceRow);
|
circuitRouter.post("/circuits/:circuitId/device-rows", createCircuitDeviceRow);
|
||||||
|
|||||||
@@ -20,13 +20,6 @@ export const createCircuitSchema = z.object({
|
|||||||
remark: z.string().optional(),
|
remark: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const updateCircuitSchema = createCircuitSchema.partial().extend({
|
|
||||||
sectionId: z.string().min(1).optional(),
|
|
||||||
equipmentIdentifier: z.string().min(1).optional(),
|
|
||||||
sortOrder: z.number().optional(),
|
|
||||||
isReserve: z.boolean().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const createCircuitDeviceRowSchema = z.object({
|
export const createCircuitDeviceRowSchema = z.object({
|
||||||
linkedProjectDeviceId: z.string().min(1).optional(),
|
linkedProjectDeviceId: z.string().min(1).optional(),
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
@@ -53,8 +46,6 @@ export const createCircuitWithDeviceRowsSchema = z.object({
|
|||||||
deviceRows: z.array(createCircuitDeviceRowSchema).min(1),
|
deviceRows: z.array(createCircuitDeviceRowSchema).min(1),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const updateCircuitDeviceRowSchema = createCircuitDeviceRowSchema.partial();
|
|
||||||
|
|
||||||
export const moveCircuitDeviceRowSchema = z
|
export const moveCircuitDeviceRowSchema = z
|
||||||
.object({
|
.object({
|
||||||
targetCircuitId: z.string().min(1).optional(),
|
targetCircuitId: z.string().min(1).optional(),
|
||||||
@@ -98,10 +89,8 @@ export const updateSectionEquipmentIdentifiersSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export type CreateCircuitInput = z.infer<typeof createCircuitSchema>;
|
export type CreateCircuitInput = z.infer<typeof createCircuitSchema>;
|
||||||
export type UpdateCircuitInput = z.infer<typeof updateCircuitSchema>;
|
|
||||||
export type CreateCircuitDeviceRowInput = z.infer<typeof createCircuitDeviceRowSchema>;
|
export type CreateCircuitDeviceRowInput = z.infer<typeof createCircuitDeviceRowSchema>;
|
||||||
export type CreateCircuitWithDeviceRowsInput = z.infer<typeof createCircuitWithDeviceRowsSchema>;
|
export type CreateCircuitWithDeviceRowsInput = z.infer<typeof createCircuitWithDeviceRowsSchema>;
|
||||||
export type UpdateCircuitDeviceRowInput = z.infer<typeof updateCircuitDeviceRowSchema>;
|
|
||||||
export type MoveCircuitDeviceRowInput = z.infer<typeof moveCircuitDeviceRowSchema>;
|
export type MoveCircuitDeviceRowInput = z.infer<typeof moveCircuitDeviceRowSchema>;
|
||||||
export type MoveCircuitDeviceRowsBulkInput = z.infer<typeof moveCircuitDeviceRowsBulkSchema>;
|
export type MoveCircuitDeviceRowsBulkInput = z.infer<typeof moveCircuitDeviceRowsBulkSchema>;
|
||||||
export type ReorderSectionCircuitsInput = z.infer<typeof reorderSectionCircuitsSchema>;
|
export type ReorderSectionCircuitsInput = z.infer<typeof reorderSectionCircuitsSchema>;
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import assert from "node:assert/strict";
|
|||||||
import { describe, it } from "node:test";
|
import { describe, it } from "node:test";
|
||||||
import {
|
import {
|
||||||
allColumns,
|
allColumns,
|
||||||
|
buildCircuitEditPatch,
|
||||||
|
buildDeviceRowEditPatch,
|
||||||
getBlockSortValue,
|
getBlockSortValue,
|
||||||
getCellKind,
|
getCellKind,
|
||||||
getCircuitValue,
|
getCircuitValue,
|
||||||
@@ -85,4 +87,38 @@ describe("circuit grid model", () => {
|
|||||||
assert.equal(parseNumeric("quantity", ""), undefined);
|
assert.equal(parseNumeric("quantity", ""), undefined);
|
||||||
assert.throws(() => parseNumeric("quantity", "two"), /Ungültiger Zahlenwert/);
|
assert.throws(() => parseNumeric("quantity", "two"), /Ungültiger Zahlenwert/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("builds nullable circuit command patches from grid drafts", () => {
|
||||||
|
assert.deepEqual(buildCircuitEditPatch("voltage", ""), {
|
||||||
|
voltage: null,
|
||||||
|
});
|
||||||
|
assert.deepEqual(
|
||||||
|
buildCircuitEditPatch("controlRequirement", " DALI "),
|
||||||
|
{ controlRequirement: "DALI" }
|
||||||
|
);
|
||||||
|
assert.deepEqual(buildCircuitEditPatch("isReserve", "ja"), {
|
||||||
|
isReserve: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds device command patches with explicit clearing semantics", () => {
|
||||||
|
assert.deepEqual(buildDeviceRowEditPatch("roomSummary", ""), {
|
||||||
|
roomNumberSnapshot: null,
|
||||||
|
roomNameSnapshot: null,
|
||||||
|
});
|
||||||
|
assert.deepEqual(
|
||||||
|
buildDeviceRowEditPatch("roomSummary", "1.01 Büro"),
|
||||||
|
{
|
||||||
|
roomNumberSnapshot: "1.01",
|
||||||
|
roomNameSnapshot: "Büro",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert.deepEqual(buildDeviceRowEditPatch("technicalName", " Leuchte "), {
|
||||||
|
name: "Leuchte",
|
||||||
|
});
|
||||||
|
assert.throws(
|
||||||
|
() => buildDeviceRowEditPatch("quantity", ""),
|
||||||
|
/darf nicht leer sein/
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ function createTestFixture(): TestFixture {
|
|||||||
displayName: "Licht Bestand",
|
displayName: "Licht Bestand",
|
||||||
sortOrder: 10,
|
sortOrder: 10,
|
||||||
protectionRatedCurrent: 10,
|
protectionRatedCurrent: 10,
|
||||||
|
voltage: 230,
|
||||||
|
controlRequirement: "none",
|
||||||
isReserve: 0,
|
isReserve: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -111,6 +113,8 @@ describe("circuit project-command repository", () => {
|
|||||||
const command = createCircuitUpdateProjectCommand("circuit-1", {
|
const command = createCircuitUpdateProjectCommand("circuit-1", {
|
||||||
displayName: null,
|
displayName: null,
|
||||||
protectionRatedCurrent: 16,
|
protectionRatedCurrent: 16,
|
||||||
|
voltage: 400,
|
||||||
|
controlRequirement: "DALI",
|
||||||
isReserve: true,
|
isReserve: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -125,6 +129,8 @@ describe("circuit project-command repository", () => {
|
|||||||
const circuit = getCircuit(fixture.context);
|
const circuit = getCircuit(fixture.context);
|
||||||
assert.equal(circuit.displayName, null);
|
assert.equal(circuit.displayName, null);
|
||||||
assert.equal(circuit.protectionRatedCurrent, 16);
|
assert.equal(circuit.protectionRatedCurrent, 16);
|
||||||
|
assert.equal(circuit.voltage, 400);
|
||||||
|
assert.equal(circuit.controlRequirement, "DALI");
|
||||||
assert.equal(circuit.isReserve, 1);
|
assert.equal(circuit.isReserve, 1);
|
||||||
assert.equal(executed.revision.revisionNumber, 1);
|
assert.equal(executed.revision.revisionNumber, 1);
|
||||||
assert.deepEqual(executed.inverse.payload, {
|
assert.deepEqual(executed.inverse.payload, {
|
||||||
@@ -132,6 +138,8 @@ describe("circuit project-command repository", () => {
|
|||||||
changes: [
|
changes: [
|
||||||
{ field: "displayName", value: "Licht Bestand" },
|
{ field: "displayName", value: "Licht Bestand" },
|
||||||
{ field: "protectionRatedCurrent", value: 10 },
|
{ field: "protectionRatedCurrent", value: 10 },
|
||||||
|
{ field: "voltage", value: 230 },
|
||||||
|
{ field: "controlRequirement", value: "none" },
|
||||||
{ field: "isReserve", value: false },
|
{ field: "isReserve", value: false },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,45 +16,6 @@ describe("circuit write service rules", () => {
|
|||||||
assert.equal(parsed.success, true);
|
assert.equal(parsed.success, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("passes voltage and control requirements through circuit updates", async () => {
|
|
||||||
let updatePayload: Record<string, unknown> = {};
|
|
||||||
const circuit = {
|
|
||||||
id: "c1",
|
|
||||||
circuitListId: "l1",
|
|
||||||
sectionId: "s1",
|
|
||||||
equipmentIdentifier: "-1F1",
|
|
||||||
sortOrder: 10,
|
|
||||||
isReserve: 0,
|
|
||||||
voltage: 230,
|
|
||||||
controlRequirement: "none",
|
|
||||||
};
|
|
||||||
const service = new CircuitWriteService({
|
|
||||||
circuitSectionRepository: {
|
|
||||||
async findById() {
|
|
||||||
return { id: "s1", circuitListId: "l1" } as never;
|
|
||||||
},
|
|
||||||
} as never,
|
|
||||||
circuitRepository: {
|
|
||||||
async findById() {
|
|
||||||
return circuit as never;
|
|
||||||
},
|
|
||||||
async existsByEquipmentIdentifier() {
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
async updateFields(_id: string, payload: Record<string, unknown>) {
|
|
||||||
updatePayload = payload;
|
|
||||||
},
|
|
||||||
} as never,
|
|
||||||
});
|
|
||||||
|
|
||||||
await service.updateCircuit("c1", { voltage: 400, controlRequirement: "DALI" });
|
|
||||||
|
|
||||||
assert.deepEqual(updatePayload, {
|
|
||||||
voltage: 400,
|
|
||||||
controlRequirement: "DALI",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects duplicate equipment identifiers in same circuit list", async () => {
|
it("rejects duplicate equipment identifiers in same circuit list", async () => {
|
||||||
const service = new CircuitWriteService({
|
const service = new CircuitWriteService({
|
||||||
circuitListRepository: {
|
circuitListRepository: {
|
||||||
@@ -741,36 +702,4 @@ describe("circuit write service rules", () => {
|
|||||||
assert.equal(safeCalled, true);
|
assert.equal(safeCalled, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("tracks local edits on linked device rows as overridden fields", async () => {
|
|
||||||
let updatePayload: Record<string, unknown> = {};
|
|
||||||
const current = {
|
|
||||||
id: "r1",
|
|
||||||
circuitId: "c1",
|
|
||||||
linkedProjectDeviceId: "pd1",
|
|
||||||
name: "Luminaire",
|
|
||||||
displayName: "Local name",
|
|
||||||
quantity: 1,
|
|
||||||
powerPerUnit: 0.1,
|
|
||||||
simultaneityFactor: 1,
|
|
||||||
sortOrder: 10,
|
|
||||||
overriddenFields: null,
|
|
||||||
};
|
|
||||||
const service = new CircuitWriteService({
|
|
||||||
deviceRowRepository: {
|
|
||||||
async findById() {
|
|
||||||
return current as never;
|
|
||||||
},
|
|
||||||
async updateFields(_rowId: string, input: Record<string, unknown>) {
|
|
||||||
updatePayload = input;
|
|
||||||
},
|
|
||||||
} as never,
|
|
||||||
});
|
|
||||||
|
|
||||||
await service.updateDeviceRow("r1", { quantity: 2 });
|
|
||||||
|
|
||||||
assert.deepEqual(updatePayload, {
|
|
||||||
quantity: 2,
|
|
||||||
overriddenFields: "[\"quantity\"]",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user