Persist circuit structure edits
This commit is contained in:
@@ -220,14 +220,15 @@ After saving, the row becomes linked to the new project device.
|
|||||||
|
|
||||||
## Undo / Redo
|
## Undo / Redo
|
||||||
|
|
||||||
The editor's visible undo/redo stack remains session-local while structural and
|
The editor's visible undo/redo stack remains session-local while move, reorder
|
||||||
destructive operations are being migrated. Existing Circuit and
|
and renumber operations are being migrated. Existing Circuit and
|
||||||
CircuitDeviceRow cell edits already execute persistent project commands and
|
CircuitDeviceRow cell edits plus standalone insertion/deletion already execute
|
||||||
their toolbar undo/redo actions use the project-wide history endpoints. The
|
persistent project commands; their toolbar undo/redo actions use the
|
||||||
tree response supplies the optimistic `currentRevision`; the obsolete direct
|
project-wide history endpoints. Insertions use client-generated stable UUIDs,
|
||||||
PATCH routes for these fields are removed. Reloading the page does not yet
|
and deletion undo restores the same ids from complete server snapshots. The
|
||||||
reconstruct the visible editor stack. Public command, undo and redo endpoints
|
tree response supplies the optimistic `currentRevision`; obsolete direct field
|
||||||
also exist for Circuit/CircuitDeviceRow insertion and deletion.
|
PATCH, structure POST and CircuitDeviceRow DELETE routes are removed.
|
||||||
|
Reloading the page does not yet reconstruct the visible editor stack.
|
||||||
CircuitDeviceRow moves between existing circuits and moves that create one new
|
CircuitDeviceRow moves between existing circuits and moves that create one new
|
||||||
placeholder target circuit are persisted. The latter stores the complete empty
|
placeholder target circuit are persisted. The latter stores the complete empty
|
||||||
target snapshot so undo can restore the rows and remove only the unchanged
|
target snapshot so undo can restore the rows and remove only the unchanged
|
||||||
|
|||||||
@@ -36,8 +36,9 @@ 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. Existing Circuit and
|
replacement identities or renumbering circuits. Existing Circuit and
|
||||||
CircuitDeviceRow cell edits in the editor consume these endpoints; the
|
CircuitDeviceRow cell edits plus standalone insert/delete actions in the editor
|
||||||
remaining structural editor writes are still being migrated.
|
consume these endpoints. Move, reorder and renumber UI paths 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
|
||||||
@@ -163,39 +164,17 @@ Response sketch:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Circuit CRUD
|
### Circuit Structure
|
||||||
|
|
||||||
- `POST /projects/:projectId/circuit-lists/:circuitListId/circuits`
|
|
||||||
- create circuit in list/section
|
|
||||||
- `POST /projects/:projectId/circuit-lists/:circuitListId/circuits-with-device-rows`
|
|
||||||
- atomically create one circuit and one or more initial device rows
|
|
||||||
- `DELETE /circuits/:circuitId`
|
|
||||||
- delete circuit (and related rows via DB relations)
|
|
||||||
- `GET /circuit-sections/:sectionId/next-identifier`
|
- `GET /circuit-sections/:sectionId/next-identifier`
|
||||||
- preview next identifier for section (`prefix + maxSuffix + 1`)
|
- preview next identifier for section (`prefix + maxSuffix + 1`)
|
||||||
|
|
||||||
Request sketch (`POST .../circuits`):
|
Circuit and device-row field updates, standalone insertions and standalone
|
||||||
|
deletions are available only as the corresponding versioned commands through
|
||||||
```json
|
`POST /projects/:projectId/commands`. There are no direct field-update PATCH,
|
||||||
{
|
structure POST or CircuitDeviceRow DELETE routes. The direct
|
||||||
"sectionId": "sec_1",
|
`DELETE /circuits/:circuitId` route remains temporarily for the inverse of the
|
||||||
"equipmentIdentifier": "-2F14",
|
not-yet-migrated placeholder move UI and must not be used by new features.
|
||||||
"displayName": "Sockets East",
|
|
||||||
"sortOrder": 140
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Device Row CRUD
|
|
||||||
|
|
||||||
- `POST /circuits/:circuitId/device-rows`
|
|
||||||
- create row inside a circuit
|
|
||||||
- `DELETE /circuit-device-rows/:rowId`
|
|
||||||
- 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
|
||||||
|
|
||||||
|
|||||||
@@ -123,10 +123,12 @@ Cross-section device moves show confirmation-required feedback before drop. The
|
|||||||
## Undo/Redo
|
## Undo/Redo
|
||||||
|
|
||||||
Undo/redo wraps editor operations as command objects and reloads the tree after
|
Undo/redo wraps editor operations as command objects and reloads the tree after
|
||||||
each command. Existing Circuit and device-row cell edits execute persistent
|
each command. Existing Circuit and device-row cell edits plus standalone
|
||||||
project commands; their toolbar undo/redo calls the project-wide server
|
insert/delete actions execute persistent project commands; their toolbar
|
||||||
history. Other covered operations still use their existing direct API writes
|
undo/redo calls the project-wide server history. New entities receive stable
|
||||||
and session-local inverse callbacks during the cutover.
|
UUIDs before insertion, and deletion undo restores those same ids. Move,
|
||||||
|
reorder and renumber operations still use their existing direct API writes and
|
||||||
|
session-local inverse callbacks during the cutover.
|
||||||
|
|
||||||
Covered operations include:
|
Covered operations include:
|
||||||
|
|
||||||
@@ -140,5 +142,5 @@ Covered operations include:
|
|||||||
Current limitations:
|
Current limitations:
|
||||||
|
|
||||||
- the visible editor stack is still session-local and is empty after reload
|
- the visible editor stack is still session-local and is empty after reload
|
||||||
- structural and destructive editor actions are not all connected to the
|
- move, reorder and renumber editor actions are not connected to the persistent
|
||||||
persistent command boundary yet
|
command boundary yet
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
# Circuit List Editor Known Limitations
|
# Circuit List Editor Known Limitations
|
||||||
|
|
||||||
- Existing Circuit and CircuitDeviceRow cell edits use persistent project
|
- Existing Circuit and CircuitDeviceRow cell edits plus standalone
|
||||||
commands and project-wide toolbar undo/redo. The editor's visible stack is
|
insert/delete actions use persistent project commands and project-wide
|
||||||
still session-local, is empty after reload and still contains direct
|
toolbar undo/redo. The editor's visible stack is still session-local, is
|
||||||
structural/destructive operations during the cutover.
|
empty after reload and still contains direct move/reorder/renumber 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`; complete editor
|
the command types documented in `circuit-list-editor-api.md`; complete editor
|
||||||
command coverage and history reconstruction are not complete.
|
command coverage and history reconstruction are not complete.
|
||||||
|
|||||||
@@ -87,12 +87,14 @@ 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. Bestehende Circuit- und Gerätezeilen-Zelländerungen
|
Betriebsmittelkennzeichen. Bestehende Circuit- und Gerätezeilen-Zelländerungen
|
||||||
verwendet das Grid bereits über die öffentliche Command-Grenze. Der Tree liefert
|
sowie eigenständiges Einfügen und Löschen verwendet das Grid bereits über die
|
||||||
dazu `currentRevision`; Undo/Redo für diese Zellaktionen läuft über die
|
öffentliche Command-Grenze. Neue Circuits und Gerätezeilen erhalten ihre stabile
|
||||||
projektweite Serverhistorie. Die direkten PATCH-Endpunkte für diese Felder sind
|
UUID vor dem Command; Löschen und Undo bewahren diese Identität. Der Tree liefert
|
||||||
entfernt. Andere Editoraktionen verbleiben vorerst im sichtbaren
|
dazu `currentRevision`; Undo/Redo für diese Aktionen läuft über die projektweite
|
||||||
sitzungslokalen Stack; nach einem Reload wird dieser noch nicht aus der
|
Serverhistorie. Direkte Feld-PATCH-, Struktur-POST- und Gerätezeilen-DELETE-
|
||||||
Serverhistorie rekonstruiert.
|
Endpunkte sind entfernt. Move-, Reorder- und Renumber-UI-Pfade 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;
|
||||||
|
|||||||
@@ -184,6 +184,9 @@ Completed foundation:
|
|||||||
- `circuit.insert` and `circuit.delete` persist a complete circuit block with
|
- `circuit.insert` and `circuit.delete` persist a complete circuit block with
|
||||||
zero, one or multiple device rows; undo restores all ids and fields without
|
zero, one or multiple device rows; undo restores all ids and fields without
|
||||||
renumbering
|
renumbering
|
||||||
|
- standalone Circuit/CircuitDeviceRow insert/delete UI paths use these commands
|
||||||
|
with client-generated stable UUIDs; obsolete direct structure POST and
|
||||||
|
CircuitDeviceRow DELETE routes are removed
|
||||||
- `circuit-device-row.move` stores exact expected and target circuit/sort
|
- `circuit-device-row.move` stores exact expected and target circuit/sort
|
||||||
positions for one or multiple rows, recalculates all affected reserve states
|
positions for one or multiple rows, recalculates all affected reserve states
|
||||||
and reconstructs a deterministic inverse across multiple source circuits
|
and reconstructs a deterministic inverse across multiple source circuits
|
||||||
@@ -208,17 +211,14 @@ Completed foundation:
|
|||||||
|
|
||||||
Remaining constraints before completing persistent history:
|
Remaining constraints before completing persistent history:
|
||||||
|
|
||||||
- extend the concrete project-scoped command union and executors beyond the
|
- route the remaining move, reorder and renumber editor writes through their
|
||||||
Circuit field/insert/delete and CircuitDeviceRow
|
existing typed persistent commands and remove the final direct mutation
|
||||||
field/insert/delete/move, section-reorder/renumber and ProjectDevice
|
routes
|
||||||
field/insert/delete/row-sync slices; the generic versioned command envelope
|
- reconstruct the visible frontend history state after reload; Circuit and
|
||||||
is complete
|
CircuitDeviceRow field/insert/delete actions plus ProjectDevice operations
|
||||||
- route the remaining structural and destructive domain
|
already use persistent commands and project-wide undo
|
||||||
writes through the shared command, revision and stack transaction boundary
|
- extend project-scoped commands only when further project mutations are
|
||||||
- finish routing structural and destructive circuit-editor operations through
|
deliberately added to history; the generic versioned envelope is complete
|
||||||
persistent commands, then reconstruct the visible frontend history state
|
|
||||||
after reload; Circuit/CircuitDeviceRow cell edits and ProjectDevice
|
|
||||||
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
|
||||||
|
|
||||||
|
|||||||
@@ -398,6 +398,9 @@ Implemented foundation:
|
|||||||
stack transition commit or roll back together
|
stack transition commit or roll back together
|
||||||
- typed Circuit insert/delete commands treat the circuit and all device rows as
|
- typed Circuit insert/delete commands treat the circuit and all device rows as
|
||||||
one block, preserve every stable id and never renumber neighbouring circuits
|
one block, preserve every stable id and never renumber neighbouring circuits
|
||||||
|
- standalone Circuit/CircuitDeviceRow insert/delete editor paths use those
|
||||||
|
commands with stable client-generated UUIDs and project-wide undo/redo; the
|
||||||
|
obsolete direct structure POST and CircuitDeviceRow DELETE routes are removed
|
||||||
- typed CircuitDeviceRow move commands persist exact old/new circuit and sort
|
- typed CircuitDeviceRow move commands persist exact old/new circuit and sort
|
||||||
positions for single or multi-row moves between existing circuits and derive
|
positions for single or multi-row moves between existing circuits and derive
|
||||||
affected reserve states in the same transaction
|
affected reserve states in the same transaction
|
||||||
@@ -428,9 +431,9 @@ Implemented foundation:
|
|||||||
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 and
|
ProjectDevice CRUD, synchronization and disconnect on the project page and
|
||||||
existing Circuit/CircuitDeviceRow cell edits in the circuit-list editor are
|
Circuit/CircuitDeviceRow field/insert/delete actions in the circuit-list editor
|
||||||
connected to this history boundary. Remaining structural editor writes are not
|
are connected to this history boundary. Move, reorder and renumber UI paths are
|
||||||
fully connected; the editor's visible undo/redo stack therefore remains
|
not yet connected; the editor's visible undo/redo stack therefore remains
|
||||||
session-local and is not reconstructed after reload.
|
session-local and is not reconstructed after reload.
|
||||||
|
|
||||||
Acceptance criteria:
|
Acceptance criteria:
|
||||||
|
|||||||
+2
-2
@@ -14,8 +14,8 @@
|
|||||||
"build:api": "tsc -p tsconfig.json",
|
"build:api": "tsc -p tsconfig.json",
|
||||||
"build:web": "next build",
|
"build:web": "next build",
|
||||||
"start": "node dist/server/index.js",
|
"start": "node dist/server/index.js",
|
||||||
"test": "tsx --test tests/project-device-schema.test.ts tests/project-device-schema-migration.test.ts tests/project-device-placement.service.test.ts tests/project-device-sync.service.test.ts tests/legacy-consumer-migration-planner.test.ts tests/legacy-consumer-migration.repository.test.ts tests/circuit-numbering.service.test.ts tests/circuit-write.rules.test.ts tests/circuit-power-calculation.test.ts tests/circuit-tree.controller.test.ts tests/circuit-grid-insertion.test.ts tests/circuit-grid-safety.test.ts tests/circuit-grid-model.test.ts tests/circuit-grid-projection.test.ts tests/distribution-board.repository.test.ts tests/circuit-device-row-transaction.repository.test.ts tests/project-device-project-command.repository.test.ts tests/project-device-structure-project-command.repository.test.ts tests/project-device-row-sync-project-command.repository.test.ts tests/circuit-section-transaction.repository.test.ts tests/circuit-section-reorder-project-command.repository.test.ts tests/circuit-section-renumber-project-command.repository.test.ts tests/project-command.model.test.ts tests/project-revision.repository.test.ts tests/circuit-project-command.repository.test.ts tests/circuit-structure-project-command.repository.test.ts tests/circuit-device-row-project-command.repository.test.ts tests/circuit-device-row-structure-project-command.repository.test.ts tests/circuit-device-row-move-project-command.repository.test.ts tests/project-history.repository.test.ts tests/project-command.service.test.ts tests/database-backup.test.ts",
|
"test": "tsx --test tests/project-device-schema.test.ts tests/project-device-schema-migration.test.ts tests/project-device-placement.service.test.ts tests/project-device-sync.service.test.ts tests/legacy-consumer-migration-planner.test.ts tests/legacy-consumer-migration.repository.test.ts tests/circuit-numbering.service.test.ts tests/circuit-write.rules.test.ts tests/circuit-power-calculation.test.ts tests/circuit-tree.controller.test.ts tests/circuit-grid-insertion.test.ts tests/circuit-grid-safety.test.ts tests/circuit-grid-model.test.ts tests/circuit-structure-command.test.ts tests/circuit-grid-projection.test.ts tests/distribution-board.repository.test.ts tests/circuit-device-row-transaction.repository.test.ts tests/project-device-project-command.repository.test.ts tests/project-device-structure-project-command.repository.test.ts tests/project-device-row-sync-project-command.repository.test.ts tests/circuit-section-transaction.repository.test.ts tests/circuit-section-reorder-project-command.repository.test.ts tests/circuit-section-renumber-project-command.repository.test.ts tests/project-command.model.test.ts tests/project-revision.repository.test.ts tests/circuit-project-command.repository.test.ts tests/circuit-structure-project-command.repository.test.ts tests/circuit-device-row-project-command.repository.test.ts tests/circuit-device-row-structure-project-command.repository.test.ts tests/circuit-device-row-move-project-command.repository.test.ts tests/project-history.repository.test.ts tests/project-command.service.test.ts tests/database-backup.test.ts",
|
||||||
"test:watch": "tsx --watch --test tests/project-device-schema.test.ts tests/project-device-schema-migration.test.ts tests/project-device-placement.service.test.ts tests/project-device-sync.service.test.ts tests/legacy-consumer-migration-planner.test.ts tests/legacy-consumer-migration.repository.test.ts tests/circuit-numbering.service.test.ts tests/circuit-write.rules.test.ts tests/circuit-power-calculation.test.ts tests/circuit-tree.controller.test.ts tests/circuit-grid-insertion.test.ts tests/circuit-grid-safety.test.ts tests/circuit-grid-model.test.ts tests/circuit-grid-projection.test.ts tests/distribution-board.repository.test.ts tests/circuit-device-row-transaction.repository.test.ts tests/project-device-project-command.repository.test.ts tests/project-device-structure-project-command.repository.test.ts tests/project-device-row-sync-project-command.repository.test.ts tests/circuit-section-transaction.repository.test.ts tests/circuit-section-reorder-project-command.repository.test.ts tests/circuit-section-renumber-project-command.repository.test.ts tests/project-command.model.test.ts tests/project-revision.repository.test.ts tests/circuit-project-command.repository.test.ts tests/circuit-structure-project-command.repository.test.ts tests/circuit-device-row-project-command.repository.test.ts tests/circuit-device-row-structure-project-command.repository.test.ts tests/circuit-device-row-move-project-command.repository.test.ts tests/project-history.repository.test.ts tests/project-command.service.test.ts tests/database-backup.test.ts",
|
"test:watch": "tsx --watch --test tests/project-device-schema.test.ts tests/project-device-schema-migration.test.ts tests/project-device-placement.service.test.ts tests/project-device-sync.service.test.ts tests/legacy-consumer-migration-planner.test.ts tests/legacy-consumer-migration.repository.test.ts tests/circuit-numbering.service.test.ts tests/circuit-write.rules.test.ts tests/circuit-power-calculation.test.ts tests/circuit-tree.controller.test.ts tests/circuit-grid-insertion.test.ts tests/circuit-grid-safety.test.ts tests/circuit-grid-model.test.ts tests/circuit-structure-command.test.ts tests/circuit-grid-projection.test.ts tests/distribution-board.repository.test.ts tests/circuit-device-row-transaction.repository.test.ts tests/project-device-project-command.repository.test.ts tests/project-device-structure-project-command.repository.test.ts tests/project-device-row-sync-project-command.repository.test.ts tests/circuit-section-transaction.repository.test.ts tests/circuit-section-reorder-project-command.repository.test.ts tests/circuit-section-renumber-project-command.repository.test.ts tests/project-command.model.test.ts tests/project-revision.repository.test.ts tests/circuit-project-command.repository.test.ts tests/circuit-structure-project-command.repository.test.ts tests/circuit-device-row-project-command.repository.test.ts tests/circuit-device-row-structure-project-command.repository.test.ts tests/circuit-device-row-move-project-command.repository.test.ts tests/project-history.repository.test.ts tests/project-command.service.test.ts tests/database-backup.test.ts",
|
||||||
"db:generate": "drizzle-kit generate",
|
"db:generate": "drizzle-kit generate",
|
||||||
"db:migrate": "drizzle-kit migrate",
|
"db:migrate": "drizzle-kit migrate",
|
||||||
"db:backup": "tsx scripts/db-backup.ts",
|
"db:backup": "tsx scripts/db-backup.ts",
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
import { CircuitDeviceRowRepository } from "../../db/repositories/circuit-device-row.repository.js";
|
import { CircuitDeviceRowRepository } from "../../db/repositories/circuit-device-row.repository.js";
|
||||||
import type { CircuitDeviceRowTransactionStore } from "../ports/circuit-device-row-transaction.store.js";
|
import type { CircuitDeviceRowTransactionStore } from "../ports/circuit-device-row-transaction.store.js";
|
||||||
import type { CircuitSectionTransactionStore } from "../ports/circuit-section-transaction.store.js";
|
import type { CircuitSectionTransactionStore } from "../ports/circuit-section-transaction.store.js";
|
||||||
import { CircuitListRepository } from "../../db/repositories/circuit-list.repository.js";
|
|
||||||
import { CircuitRepository } from "../../db/repositories/circuit.repository.js";
|
import { CircuitRepository } from "../../db/repositories/circuit.repository.js";
|
||||||
import { CircuitSectionRepository } from "../../db/repositories/circuit-section.repository.js";
|
import { CircuitSectionRepository } from "../../db/repositories/circuit-section.repository.js";
|
||||||
import { ProjectDeviceRepository } from "../../db/repositories/project-device.repository.js";
|
|
||||||
import type {
|
import type {
|
||||||
CreateCircuitDeviceRowInput,
|
|
||||||
CreateCircuitInput,
|
|
||||||
CreateCircuitWithDeviceRowsInput,
|
|
||||||
MoveCircuitDeviceRowInput,
|
MoveCircuitDeviceRowInput,
|
||||||
MoveCircuitDeviceRowsBulkInput,
|
MoveCircuitDeviceRowsBulkInput,
|
||||||
ReorderSectionCircuitsInput,
|
ReorderSectionCircuitsInput,
|
||||||
@@ -19,30 +14,24 @@ import { CircuitNumberingService } from "./circuit-numbering.service.js";
|
|||||||
export class CircuitWriteService {
|
export class CircuitWriteService {
|
||||||
private readonly circuitRepository: CircuitRepository;
|
private readonly circuitRepository: CircuitRepository;
|
||||||
private readonly circuitSectionRepository: CircuitSectionRepository;
|
private readonly circuitSectionRepository: CircuitSectionRepository;
|
||||||
private readonly circuitListRepository: CircuitListRepository;
|
|
||||||
private readonly deviceRowRepository: CircuitDeviceRowRepository;
|
private readonly deviceRowRepository: CircuitDeviceRowRepository;
|
||||||
private readonly deviceRowTransactionStore?: CircuitDeviceRowTransactionStore;
|
private readonly deviceRowTransactionStore?: CircuitDeviceRowTransactionStore;
|
||||||
private readonly circuitSectionTransactionStore?: CircuitSectionTransactionStore;
|
private readonly circuitSectionTransactionStore?: CircuitSectionTransactionStore;
|
||||||
private readonly projectDeviceRepository: ProjectDeviceRepository;
|
|
||||||
private readonly numberingService: CircuitNumberingService;
|
private readonly numberingService: CircuitNumberingService;
|
||||||
|
|
||||||
constructor(deps?: {
|
constructor(deps?: {
|
||||||
circuitRepository?: CircuitRepository;
|
circuitRepository?: CircuitRepository;
|
||||||
circuitSectionRepository?: CircuitSectionRepository;
|
circuitSectionRepository?: CircuitSectionRepository;
|
||||||
circuitListRepository?: CircuitListRepository;
|
|
||||||
deviceRowRepository?: CircuitDeviceRowRepository;
|
deviceRowRepository?: CircuitDeviceRowRepository;
|
||||||
deviceRowTransactionStore?: CircuitDeviceRowTransactionStore;
|
deviceRowTransactionStore?: CircuitDeviceRowTransactionStore;
|
||||||
circuitSectionTransactionStore?: CircuitSectionTransactionStore;
|
circuitSectionTransactionStore?: CircuitSectionTransactionStore;
|
||||||
projectDeviceRepository?: ProjectDeviceRepository;
|
|
||||||
numberingService?: CircuitNumberingService;
|
numberingService?: CircuitNumberingService;
|
||||||
}) {
|
}) {
|
||||||
this.circuitRepository = deps?.circuitRepository ?? new CircuitRepository();
|
this.circuitRepository = deps?.circuitRepository ?? new CircuitRepository();
|
||||||
this.circuitSectionRepository = deps?.circuitSectionRepository ?? new CircuitSectionRepository();
|
this.circuitSectionRepository = deps?.circuitSectionRepository ?? new CircuitSectionRepository();
|
||||||
this.circuitListRepository = deps?.circuitListRepository ?? new CircuitListRepository();
|
|
||||||
this.deviceRowRepository = deps?.deviceRowRepository ?? new CircuitDeviceRowRepository();
|
this.deviceRowRepository = deps?.deviceRowRepository ?? new CircuitDeviceRowRepository();
|
||||||
this.deviceRowTransactionStore = deps?.deviceRowTransactionStore;
|
this.deviceRowTransactionStore = deps?.deviceRowTransactionStore;
|
||||||
this.circuitSectionTransactionStore = deps?.circuitSectionTransactionStore;
|
this.circuitSectionTransactionStore = deps?.circuitSectionTransactionStore;
|
||||||
this.projectDeviceRepository = deps?.projectDeviceRepository ?? new ProjectDeviceRepository();
|
|
||||||
this.numberingService = deps?.numberingService ?? new CircuitNumberingService();
|
this.numberingService = deps?.numberingService ?? new CircuitNumberingService();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,36 +47,6 @@ export class CircuitWriteService {
|
|||||||
return section;
|
return section;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enforces BMK uniqueness inside one circuit list.
|
|
||||||
private async assertUniqueEquipmentIdentifier(
|
|
||||||
circuitListId: string,
|
|
||||||
equipmentIdentifier: string,
|
|
||||||
excludeCircuitId?: string
|
|
||||||
) {
|
|
||||||
const exists = await this.circuitRepository.existsByEquipmentIdentifier(
|
|
||||||
circuitListId,
|
|
||||||
equipmentIdentifier,
|
|
||||||
excludeCircuitId
|
|
||||||
);
|
|
||||||
if (exists) {
|
|
||||||
throw new Error("Duplicate equipmentIdentifier in circuit list.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validates linked project-device id against owning project of the circuit list.
|
|
||||||
private async assertValidLinkedProjectDeviceForProject(
|
|
||||||
projectId: string,
|
|
||||||
linkedProjectDeviceId?: string
|
|
||||||
) {
|
|
||||||
if (!linkedProjectDeviceId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const device = await this.projectDeviceRepository.findById(projectId, linkedProjectDeviceId);
|
|
||||||
if (!device) {
|
|
||||||
throw new Error("Invalid linked project device id.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private getDeviceRowTransactionStore() {
|
private getDeviceRowTransactionStore() {
|
||||||
if (!this.deviceRowTransactionStore) {
|
if (!this.deviceRowTransactionStore) {
|
||||||
throw new Error("Circuit device-row transactions are not configured.");
|
throw new Error("Circuit device-row transactions are not configured.");
|
||||||
@@ -102,92 +61,6 @@ export class CircuitWriteService {
|
|||||||
return this.circuitSectionTransactionStore;
|
return this.circuitSectionTransactionStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async assertValidLinkedProjectDevice(circuitId: string, linkedProjectDeviceId?: string) {
|
|
||||||
if (!linkedProjectDeviceId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const circuit = await this.circuitRepository.findById(circuitId);
|
|
||||||
if (!circuit) {
|
|
||||||
throw new Error("Invalid circuit id.");
|
|
||||||
}
|
|
||||||
const list = await this.circuitListRepository.findByIdByListIdOnly(circuit.circuitListId);
|
|
||||||
if (!list) {
|
|
||||||
throw new Error("Circuit list not found.");
|
|
||||||
}
|
|
||||||
await this.assertValidLinkedProjectDeviceForProject(list.projectId, linkedProjectDeviceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async createCircuit(projectId: string, circuitListId: string, input: CreateCircuitInput) {
|
|
||||||
const list = await this.circuitListRepository.findById(projectId, circuitListId);
|
|
||||||
if (!list) {
|
|
||||||
throw new Error("Circuit list not found in project.");
|
|
||||||
}
|
|
||||||
await this.assertSectionInList(input.sectionId, circuitListId);
|
|
||||||
await this.assertUniqueEquipmentIdentifier(circuitListId, input.equipmentIdentifier);
|
|
||||||
const id = await this.circuitRepository.create({
|
|
||||||
circuitListId,
|
|
||||||
sectionId: input.sectionId,
|
|
||||||
equipmentIdentifier: input.equipmentIdentifier,
|
|
||||||
displayName: input.displayName,
|
|
||||||
sortOrder: input.sortOrder,
|
|
||||||
protectionType: input.protectionType,
|
|
||||||
protectionRatedCurrent: input.protectionRatedCurrent,
|
|
||||||
protectionCharacteristic: input.protectionCharacteristic,
|
|
||||||
cableType: input.cableType,
|
|
||||||
cableCrossSection: input.cableCrossSection,
|
|
||||||
cableLength: input.cableLength,
|
|
||||||
rcdAssignment: input.rcdAssignment,
|
|
||||||
terminalDesignation: input.terminalDesignation,
|
|
||||||
voltage: input.voltage,
|
|
||||||
controlRequirement: input.controlRequirement,
|
|
||||||
status: input.status,
|
|
||||||
isReserve: input.isReserve ?? true,
|
|
||||||
remark: input.remark,
|
|
||||||
});
|
|
||||||
return this.circuitRepository.findById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
async createCircuitWithDeviceRows(
|
|
||||||
projectId: string,
|
|
||||||
circuitListId: string,
|
|
||||||
input: CreateCircuitWithDeviceRowsInput
|
|
||||||
) {
|
|
||||||
const list = await this.circuitListRepository.findById(projectId, circuitListId);
|
|
||||||
if (!list) {
|
|
||||||
throw new Error("Circuit list not found in project.");
|
|
||||||
}
|
|
||||||
await this.assertSectionInList(input.circuit.sectionId, circuitListId);
|
|
||||||
await this.assertUniqueEquipmentIdentifier(
|
|
||||||
circuitListId,
|
|
||||||
input.circuit.equipmentIdentifier
|
|
||||||
);
|
|
||||||
for (const row of input.deviceRows) {
|
|
||||||
await this.assertValidLinkedProjectDeviceForProject(
|
|
||||||
list.projectId,
|
|
||||||
row.linkedProjectDeviceId
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const created = this.getDeviceRowTransactionStore().createCircuitWithDeviceRows({
|
|
||||||
circuit: {
|
|
||||||
circuitListId,
|
|
||||||
...input.circuit,
|
|
||||||
},
|
|
||||||
deviceRows: input.deviceRows,
|
|
||||||
});
|
|
||||||
const circuit = await this.circuitRepository.findById(created.circuitId);
|
|
||||||
const deviceRows = await Promise.all(
|
|
||||||
created.rowIds.map((rowId) => this.deviceRowRepository.findById(rowId))
|
|
||||||
);
|
|
||||||
if (!circuit || deviceRows.some((row) => !row)) {
|
|
||||||
throw new Error("Der angelegte Stromkreis konnte nicht geladen werden.");
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
circuit,
|
|
||||||
deviceRows,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
||||||
@@ -196,50 +69,6 @@ export class CircuitWriteService {
|
|||||||
await this.circuitRepository.delete(circuitId);
|
await this.circuitRepository.delete(circuitId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createDeviceRow(circuitId: string, input: CreateCircuitDeviceRowInput) {
|
|
||||||
const circuit = await this.circuitRepository.findById(circuitId);
|
|
||||||
if (!circuit) {
|
|
||||||
throw new Error("Invalid circuit id.");
|
|
||||||
}
|
|
||||||
await this.assertValidLinkedProjectDevice(circuitId, input.linkedProjectDeviceId);
|
|
||||||
|
|
||||||
const rowId = this.getDeviceRowTransactionStore().createInCircuit({
|
|
||||||
circuitId,
|
|
||||||
linkedProjectDeviceId: input.linkedProjectDeviceId,
|
|
||||||
sortOrder: input.sortOrder,
|
|
||||||
name: input.name,
|
|
||||||
displayName: input.displayName,
|
|
||||||
phaseType: input.phaseType,
|
|
||||||
connectionKind: input.connectionKind,
|
|
||||||
costGroup: input.costGroup,
|
|
||||||
category: input.category,
|
|
||||||
level: input.level,
|
|
||||||
roomId: input.roomId,
|
|
||||||
roomNumberSnapshot: input.roomNumberSnapshot,
|
|
||||||
roomNameSnapshot: input.roomNameSnapshot,
|
|
||||||
quantity: input.quantity,
|
|
||||||
powerPerUnit: input.powerPerUnit,
|
|
||||||
simultaneityFactor: input.simultaneityFactor,
|
|
||||||
cosPhi: input.cosPhi,
|
|
||||||
remark: input.remark,
|
|
||||||
overriddenFields: input.overriddenFields,
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.deviceRowRepository.findById(rowId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteDeviceRow(rowId: string) {
|
|
||||||
const current = await this.deviceRowRepository.findById(rowId);
|
|
||||||
if (!current) {
|
|
||||||
throw new Error("Invalid device row id.");
|
|
||||||
}
|
|
||||||
const circuit = await this.circuitRepository.findById(current.circuitId);
|
|
||||||
if (!circuit) {
|
|
||||||
throw new Error("Invalid circuit id.");
|
|
||||||
}
|
|
||||||
this.getDeviceRowTransactionStore().deleteFromCircuit(rowId, circuit.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
async moveDeviceRow(rowId: string, input: MoveCircuitDeviceRowInput) {
|
async moveDeviceRow(rowId: string, input: MoveCircuitDeviceRowInput) {
|
||||||
const row = await this.deviceRowRepository.findById(rowId);
|
const row = await this.deviceRowRepository.findById(rowId);
|
||||||
if (!row) {
|
if (!row) {
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ import {
|
|||||||
deviceFieldKeys,
|
deviceFieldKeys,
|
||||||
formatValue,
|
formatValue,
|
||||||
} from "../utils/circuit-grid-model";
|
} from "../utils/circuit-grid-model";
|
||||||
|
import {
|
||||||
|
buildCircuitDeviceRowInsertSnapshot,
|
||||||
|
buildCircuitInsertSnapshot,
|
||||||
|
} from "../utils/circuit-structure-command";
|
||||||
import type {
|
import type {
|
||||||
CellKey,
|
CellKey,
|
||||||
CellKind,
|
CellKind,
|
||||||
@@ -36,13 +40,13 @@ import {
|
|||||||
} from "../utils/circuit-grid-projection";
|
} from "../utils/circuit-grid-projection";
|
||||||
import type { VisibleGridRow } from "../utils/circuit-grid-projection";
|
import type { VisibleGridRow } from "../utils/circuit-grid-projection";
|
||||||
import {
|
import {
|
||||||
createCircuit,
|
|
||||||
createCircuitWithDeviceRows,
|
|
||||||
createCircuitDeviceRow,
|
|
||||||
deleteCircuitById,
|
deleteCircuitById,
|
||||||
deleteCircuitDeviceRowById,
|
deleteCircuitCommand,
|
||||||
|
deleteCircuitDeviceRowCommand,
|
||||||
getCircuitTree,
|
getCircuitTree,
|
||||||
getNextCircuitIdentifier,
|
getNextCircuitIdentifier,
|
||||||
|
insertCircuitCommand,
|
||||||
|
insertCircuitDeviceRowCommand,
|
||||||
listProjectDevices,
|
listProjectDevices,
|
||||||
moveCircuitDeviceRowsBulk,
|
moveCircuitDeviceRowsBulk,
|
||||||
moveCircuitDeviceRowById,
|
moveCircuitDeviceRowById,
|
||||||
@@ -56,11 +60,18 @@ import {
|
|||||||
} from "../utils/api";
|
} from "../utils/api";
|
||||||
import type {
|
import type {
|
||||||
CircuitTreeCircuitDto,
|
CircuitTreeCircuitDto,
|
||||||
CircuitTreeDeviceRowDto,
|
|
||||||
CircuitTreeResponseDto,
|
CircuitTreeResponseDto,
|
||||||
|
CreateCircuitDeviceRowInputDto,
|
||||||
|
CreateCircuitInputDto,
|
||||||
ProjectCommandResultDto,
|
ProjectCommandResultDto,
|
||||||
ProjectDeviceDto,
|
ProjectDeviceDto,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
|
import type {
|
||||||
|
CircuitDeviceRowSnapshot,
|
||||||
|
} from "../../domain/models/circuit-device-row-structure-project-command.model";
|
||||||
|
import type {
|
||||||
|
CircuitSnapshot,
|
||||||
|
} from "../../domain/models/circuit-structure-project-command.model";
|
||||||
|
|
||||||
type SaveDirection = "stay" | "next" | "prev";
|
type SaveDirection = "stay" | "next" | "prev";
|
||||||
type StartEditMode = "selectExisting" | "replaceWithTypedChar";
|
type StartEditMode = "selectExisting" | "replaceWithTypedChar";
|
||||||
@@ -96,28 +107,6 @@ interface HistoryCommand {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CircuitSnapshot {
|
|
||||||
id: string;
|
|
||||||
sectionId: string;
|
|
||||||
equipmentIdentifier: string;
|
|
||||||
displayName?: string;
|
|
||||||
sortOrder: number;
|
|
||||||
protectionType?: string;
|
|
||||||
protectionRatedCurrent?: number;
|
|
||||||
protectionCharacteristic?: string;
|
|
||||||
cableType?: string;
|
|
||||||
cableCrossSection?: string;
|
|
||||||
cableLength?: number;
|
|
||||||
rcdAssignment?: string;
|
|
||||||
terminalDesignation?: string;
|
|
||||||
voltage?: number;
|
|
||||||
controlRequirement?: string;
|
|
||||||
status?: string;
|
|
||||||
isReserve: boolean;
|
|
||||||
remark?: string;
|
|
||||||
deviceRows: CircuitTreeDeviceRowDto[];
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProjectDeviceDropIntent =
|
type ProjectDeviceDropIntent =
|
||||||
| { kind: "new-circuit"; sectionId: string; valid: boolean }
|
| { kind: "new-circuit"; sectionId: string; valid: boolean }
|
||||||
| { kind: "add-to-circuit"; circuitId: string; sectionId: string; valid: boolean };
|
| { kind: "add-to-circuit"; circuitId: string; sectionId: string; valid: boolean };
|
||||||
@@ -805,6 +794,90 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createDeviceRowSnapshot(
|
||||||
|
circuitId: string,
|
||||||
|
values: CreateCircuitDeviceRowInputDto,
|
||||||
|
defaultSortOrder: number
|
||||||
|
) {
|
||||||
|
return buildCircuitDeviceRowInsertSnapshot({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
circuitId,
|
||||||
|
values,
|
||||||
|
defaultSortOrder,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCircuitSnapshot(
|
||||||
|
values: CreateCircuitInputDto,
|
||||||
|
deviceRowValues: CreateCircuitDeviceRowInputDto[] = []
|
||||||
|
) {
|
||||||
|
const circuitId = crypto.randomUUID();
|
||||||
|
const deviceRows = deviceRowValues.map((row, index) =>
|
||||||
|
createDeviceRowSnapshot(circuitId, row, (index + 1) * 10)
|
||||||
|
);
|
||||||
|
return buildCircuitInsertSnapshot({
|
||||||
|
id: circuitId,
|
||||||
|
circuitListId,
|
||||||
|
values,
|
||||||
|
deviceRows,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function persistCircuitInsert(
|
||||||
|
circuit: CircuitSnapshot,
|
||||||
|
description: string
|
||||||
|
) {
|
||||||
|
const result = await insertCircuitCommand(
|
||||||
|
projectId,
|
||||||
|
getExpectedProjectRevision(),
|
||||||
|
circuit,
|
||||||
|
description
|
||||||
|
);
|
||||||
|
applyProjectCommandResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function persistCircuitDelete(
|
||||||
|
circuitId: string,
|
||||||
|
description: string
|
||||||
|
) {
|
||||||
|
const result = await deleteCircuitCommand(
|
||||||
|
projectId,
|
||||||
|
getExpectedProjectRevision(),
|
||||||
|
circuitId,
|
||||||
|
circuitListId,
|
||||||
|
description
|
||||||
|
);
|
||||||
|
applyProjectCommandResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function persistDeviceRowInsert(
|
||||||
|
row: CircuitDeviceRowSnapshot,
|
||||||
|
description: string
|
||||||
|
) {
|
||||||
|
const result = await insertCircuitDeviceRowCommand(
|
||||||
|
projectId,
|
||||||
|
getExpectedProjectRevision(),
|
||||||
|
row,
|
||||||
|
description
|
||||||
|
);
|
||||||
|
applyProjectCommandResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function persistDeviceRowDelete(
|
||||||
|
rowId: string,
|
||||||
|
circuitId: string,
|
||||||
|
description: string
|
||||||
|
) {
|
||||||
|
const result = await deleteCircuitDeviceRowCommand(
|
||||||
|
projectId,
|
||||||
|
getExpectedProjectRevision(),
|
||||||
|
rowId,
|
||||||
|
circuitId,
|
||||||
|
description
|
||||||
|
);
|
||||||
|
applyProjectCommandResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
// 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 {
|
||||||
@@ -1247,15 +1320,15 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
const editValues = createValuesFromEditPatch(
|
const editValues = createValuesFromEditPatch(
|
||||||
buildDeviceRowEditPatch(key, draft)
|
buildDeviceRowEditPatch(key, draft)
|
||||||
);
|
);
|
||||||
const created = await createCircuitWithDeviceRows(projectId, circuitListId, {
|
const circuit = createCircuitSnapshot(
|
||||||
circuit: {
|
{
|
||||||
sectionId,
|
sectionId,
|
||||||
equipmentIdentifier: next.nextIdentifier,
|
equipmentIdentifier: next.nextIdentifier,
|
||||||
displayName: "Neuer Stromkreis",
|
displayName: "Neuer Stromkreis",
|
||||||
sortOrder,
|
sortOrder,
|
||||||
isReserve: false,
|
isReserve: false,
|
||||||
},
|
},
|
||||||
deviceRows: [
|
[
|
||||||
{
|
{
|
||||||
name: "Manuelles Gerät",
|
name: "Manuelles Gerät",
|
||||||
displayName: "Manuelles Gerät",
|
displayName: "Manuelles Gerät",
|
||||||
@@ -1266,24 +1339,25 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
cosPhi: 1,
|
cosPhi: 1,
|
||||||
...editValues,
|
...editValues,
|
||||||
},
|
},
|
||||||
],
|
]
|
||||||
});
|
);
|
||||||
const createdCircuit = created.circuit;
|
await persistCircuitInsert(circuit, "Aus freier Zeile erstellen");
|
||||||
return { rowKey: `circuitCompact:${createdCircuit.id}`, cellKey: key };
|
return { rowKey: `circuitCompact:${circuit.id}`, cellKey: key };
|
||||||
}
|
}
|
||||||
|
|
||||||
const editValues = createValuesFromEditPatch(
|
const editValues = createValuesFromEditPatch(
|
||||||
buildCircuitEditPatch(key, draft)
|
buildCircuitEditPatch(key, draft)
|
||||||
);
|
);
|
||||||
const createdCircuit = (await createCircuit(projectId, circuitListId, {
|
const circuit = createCircuitSnapshot({
|
||||||
sectionId,
|
sectionId,
|
||||||
equipmentIdentifier: next.nextIdentifier,
|
equipmentIdentifier: next.nextIdentifier,
|
||||||
displayName: "Neuer Stromkreis",
|
displayName: "Neuer Stromkreis",
|
||||||
sortOrder,
|
sortOrder,
|
||||||
isReserve: true,
|
isReserve: true,
|
||||||
...editValues,
|
...editValues,
|
||||||
})) as CircuitTreeCircuitDto;
|
});
|
||||||
return { rowKey: `reserveCircuit:${createdCircuit.id}`, cellKey: key };
|
await persistCircuitInsert(circuit, "Aus freier Zeile erstellen");
|
||||||
|
return { rowKey: `reserveCircuit:${circuit.id}`, cellKey: key };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Commits the active editor input through command history so edits remain undoable.
|
// Commits the active editor input through command history so edits remain undoable.
|
||||||
@@ -1337,11 +1411,32 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
return nextSelectionIntent();
|
return nextSelectionIntent();
|
||||||
},
|
},
|
||||||
undo: async () => {
|
undo: async () => {
|
||||||
if (!createdCircuitId) {
|
return {
|
||||||
return null;
|
rowKey: `placeholder:${row.sectionId}`,
|
||||||
|
cellKey: editingCell.cellKey,
|
||||||
|
rowType: "placeholder",
|
||||||
|
sectionId: row.sectionId,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
persistent: {
|
||||||
|
undoIntent: () => ({
|
||||||
|
rowKey: `placeholder:${row.sectionId}`,
|
||||||
|
cellKey: editingCell.cellKey,
|
||||||
|
rowType: "placeholder",
|
||||||
|
sectionId: row.sectionId,
|
||||||
|
}),
|
||||||
|
redoIntent: () =>
|
||||||
|
createdCircuitId
|
||||||
|
? {
|
||||||
|
rowKey: targetSelection.rowKey,
|
||||||
|
cellKey: editingCell.cellKey,
|
||||||
|
rowType: targetSelection.rowKey.startsWith("circuitCompact:")
|
||||||
|
? "circuitCompact"
|
||||||
|
: "reserveCircuit",
|
||||||
|
sectionId: row.sectionId,
|
||||||
|
circuitId: createdCircuitId,
|
||||||
}
|
}
|
||||||
await deleteCircuitById(createdCircuitId);
|
: null,
|
||||||
return buildSelectionIntent({ rowKey: `placeholder:${row.sectionId}`, cellKey: editingCell.cellKey });
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
setEditingCell(null);
|
setEditingCell(null);
|
||||||
@@ -1412,7 +1507,9 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
const editValues = createValuesFromEditPatch(
|
const editValues = createValuesFromEditPatch(
|
||||||
buildDeviceRowEditPatch(editingCell.cellKey, next)
|
buildDeviceRowEditPatch(editingCell.cellKey, next)
|
||||||
);
|
);
|
||||||
const created = (await createCircuitDeviceRow(row.circuit!.id, {
|
const snapshot = createDeviceRowSnapshot(
|
||||||
|
row.circuit!.id,
|
||||||
|
{
|
||||||
name: "Reserveverbraucher",
|
name: "Reserveverbraucher",
|
||||||
displayName: "Reserveverbraucher",
|
displayName: "Reserveverbraucher",
|
||||||
phaseType: "single_phase",
|
phaseType: "single_phase",
|
||||||
@@ -1421,17 +1518,43 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
simultaneityFactor: 1,
|
simultaneityFactor: 1,
|
||||||
cosPhi: 1,
|
cosPhi: 1,
|
||||||
...editValues,
|
...editValues,
|
||||||
})) as { id: string };
|
},
|
||||||
createdRowId = created.id;
|
10
|
||||||
|
);
|
||||||
|
await persistDeviceRowInsert(
|
||||||
|
snapshot,
|
||||||
|
"Gerätezeile im Reservestromkreis erstellen"
|
||||||
|
);
|
||||||
|
createdRowId = snapshot.id;
|
||||||
targetSelection = { rowKey: `circuitCompact:${row.circuit!.id}`, cellKey: editingCell.cellKey };
|
targetSelection = { rowKey: `circuitCompact:${row.circuit!.id}`, cellKey: editingCell.cellKey };
|
||||||
return nextSelectionIntent();
|
return nextSelectionIntent();
|
||||||
},
|
},
|
||||||
undo: async () => {
|
undo: async () => ({
|
||||||
if (!createdRowId) {
|
rowKey: `reserveCircuit:${row.circuit!.id}`,
|
||||||
return null;
|
cellKey: editingCell.cellKey,
|
||||||
|
rowType: "reserveCircuit",
|
||||||
|
sectionId: row.sectionId,
|
||||||
|
circuitId: row.circuit!.id,
|
||||||
|
}),
|
||||||
|
persistent: {
|
||||||
|
undoIntent: () => ({
|
||||||
|
rowKey: `reserveCircuit:${row.circuit!.id}`,
|
||||||
|
cellKey: editingCell.cellKey,
|
||||||
|
rowType: "reserveCircuit",
|
||||||
|
sectionId: row.sectionId,
|
||||||
|
circuitId: row.circuit!.id,
|
||||||
|
}),
|
||||||
|
redoIntent: () =>
|
||||||
|
createdRowId
|
||||||
|
? {
|
||||||
|
rowKey: `circuitCompact:${row.circuit!.id}`,
|
||||||
|
cellKey: editingCell.cellKey,
|
||||||
|
rowType: "circuitCompact",
|
||||||
|
sectionId: row.sectionId,
|
||||||
|
circuitId: row.circuit!.id,
|
||||||
|
deviceId: createdRowId,
|
||||||
}
|
}
|
||||||
await deleteCircuitDeviceRowById(createdRowId);
|
: null,
|
||||||
return buildSelectionIntent({ rowKey: `reserveCircuit:${row.circuit!.id}`, cellKey: editingCell.cellKey });
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
setEditingCell(null);
|
setEditingCell(null);
|
||||||
@@ -1460,33 +1583,47 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
redo: async () => {
|
redo: async () => {
|
||||||
const next = await getNextCircuitIdentifier(sectionId);
|
const next = await getNextCircuitIdentifier(sectionId);
|
||||||
const sortOrder = getInsertionSortOrder(section.circuits, afterCircuitId);
|
const sortOrder = getInsertionSortOrder(section.circuits, afterCircuitId);
|
||||||
const created = (await createCircuit(projectId, circuitListId, {
|
const circuit = createCircuitSnapshot({
|
||||||
sectionId,
|
sectionId,
|
||||||
equipmentIdentifier: next.nextIdentifier,
|
equipmentIdentifier: next.nextIdentifier,
|
||||||
displayName: "Reserve",
|
displayName: "Reserve",
|
||||||
sortOrder,
|
sortOrder,
|
||||||
isReserve: true,
|
isReserve: true,
|
||||||
})) as CircuitTreeCircuitDto;
|
});
|
||||||
createdCircuitId = created.id;
|
await persistCircuitInsert(circuit, "Stromkreis hinzufügen");
|
||||||
|
createdCircuitId = circuit.id;
|
||||||
setActiveSectionId(sectionId);
|
setActiveSectionId(sectionId);
|
||||||
return {
|
return {
|
||||||
rowKey: `reserveCircuit:${created.id}`,
|
rowKey: `reserveCircuit:${circuit.id}`,
|
||||||
cellKey: "equipmentIdentifier",
|
cellKey: "equipmentIdentifier",
|
||||||
rowType: "reserveCircuit",
|
rowType: "reserveCircuit",
|
||||||
sectionId,
|
sectionId,
|
||||||
circuitId: created.id,
|
circuitId: circuit.id,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
undo: async () => {
|
undo: async () => ({
|
||||||
if (createdCircuitId) {
|
|
||||||
await deleteCircuitById(createdCircuitId);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
rowKey: `placeholder:${sectionId}`,
|
rowKey: `placeholder:${sectionId}`,
|
||||||
cellKey: "equipmentIdentifier",
|
cellKey: "equipmentIdentifier",
|
||||||
rowType: "placeholder",
|
rowType: "placeholder",
|
||||||
sectionId,
|
sectionId,
|
||||||
};
|
}),
|
||||||
|
persistent: {
|
||||||
|
undoIntent: () => ({
|
||||||
|
rowKey: `placeholder:${sectionId}`,
|
||||||
|
cellKey: "equipmentIdentifier",
|
||||||
|
rowType: "placeholder",
|
||||||
|
sectionId,
|
||||||
|
}),
|
||||||
|
redoIntent: () =>
|
||||||
|
createdCircuitId
|
||||||
|
? {
|
||||||
|
rowKey: `reserveCircuit:${createdCircuitId}`,
|
||||||
|
cellKey: "equipmentIdentifier",
|
||||||
|
rowType: "reserveCircuit",
|
||||||
|
sectionId,
|
||||||
|
circuitId: createdCircuitId,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1499,34 +1636,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
const originalDeviceCount = circuit.deviceRows.length;
|
const originalDeviceCount = circuit.deviceRows.length;
|
||||||
const section = data?.sections.find((entry) => entry.id === sectionId);
|
const section = data?.sections.find((entry) => entry.id === sectionId);
|
||||||
let createdRowId: string | null = null;
|
let createdRowId: string | null = null;
|
||||||
await runCommand({
|
const getUndoIntent = (): SelectionIntent => {
|
||||||
label: "Manuelles Gerät hinzufügen",
|
|
||||||
redo: async () => {
|
|
||||||
const created = (await createCircuitDeviceRow(circuit.id, {
|
|
||||||
name: "Manuelles Gerät",
|
|
||||||
displayName: "Manuelles Gerät",
|
|
||||||
phaseType: section?.key === "three_phase" ? "three_phase" : "single_phase",
|
|
||||||
quantity: 1,
|
|
||||||
powerPerUnit: 0,
|
|
||||||
simultaneityFactor: 1,
|
|
||||||
cosPhi: 1,
|
|
||||||
sortOrder: getInsertionSortOrder(circuit.deviceRows, afterDeviceRowId),
|
|
||||||
})) as { id: string };
|
|
||||||
createdRowId = created.id;
|
|
||||||
setActiveSectionId(sectionId);
|
|
||||||
return {
|
|
||||||
rowKey: originalDeviceCount === 0 ? `circuitCompact:${circuit.id}` : `device:${created.id}`,
|
|
||||||
cellKey: "displayName",
|
|
||||||
rowType: originalDeviceCount === 0 ? "circuitCompact" : "deviceRow",
|
|
||||||
sectionId,
|
|
||||||
circuitId: circuit.id,
|
|
||||||
deviceId: created.id,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
undo: async () => {
|
|
||||||
if (createdRowId) {
|
|
||||||
await deleteCircuitDeviceRowById(createdRowId);
|
|
||||||
}
|
|
||||||
const rowKey =
|
const rowKey =
|
||||||
originalDeviceCount === 0
|
originalDeviceCount === 0
|
||||||
? `reserveCircuit:${circuit.id}`
|
? `reserveCircuit:${circuit.id}`
|
||||||
@@ -1550,6 +1660,55 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
circuitId: circuit.id,
|
circuitId: circuit.id,
|
||||||
deviceId: afterDeviceRowId,
|
deviceId: afterDeviceRowId,
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
await runCommand({
|
||||||
|
label: "Manuelles Gerät hinzufügen",
|
||||||
|
redo: async () => {
|
||||||
|
const rowSnapshot = createDeviceRowSnapshot(circuit.id, {
|
||||||
|
name: "Manuelles Gerät",
|
||||||
|
displayName: "Manuelles Gerät",
|
||||||
|
phaseType: section?.key === "three_phase" ? "three_phase" : "single_phase",
|
||||||
|
quantity: 1,
|
||||||
|
powerPerUnit: 0,
|
||||||
|
simultaneityFactor: 1,
|
||||||
|
cosPhi: 1,
|
||||||
|
sortOrder: getInsertionSortOrder(circuit.deviceRows, afterDeviceRowId),
|
||||||
|
}, 10);
|
||||||
|
await persistDeviceRowInsert(
|
||||||
|
rowSnapshot,
|
||||||
|
"Manuelles Gerät hinzufügen"
|
||||||
|
);
|
||||||
|
createdRowId = rowSnapshot.id;
|
||||||
|
setActiveSectionId(sectionId);
|
||||||
|
return {
|
||||||
|
rowKey: originalDeviceCount === 0 ? `circuitCompact:${circuit.id}` : `device:${rowSnapshot.id}`,
|
||||||
|
cellKey: "displayName",
|
||||||
|
rowType: originalDeviceCount === 0 ? "circuitCompact" : "deviceRow",
|
||||||
|
sectionId,
|
||||||
|
circuitId: circuit.id,
|
||||||
|
deviceId: rowSnapshot.id,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
undo: async () => getUndoIntent(),
|
||||||
|
persistent: {
|
||||||
|
undoIntent: getUndoIntent,
|
||||||
|
redoIntent: () =>
|
||||||
|
createdRowId
|
||||||
|
? {
|
||||||
|
rowKey:
|
||||||
|
originalDeviceCount === 0
|
||||||
|
? `circuitCompact:${circuit.id}`
|
||||||
|
: `device:${createdRowId}`,
|
||||||
|
cellKey: "displayName",
|
||||||
|
rowType:
|
||||||
|
originalDeviceCount === 0
|
||||||
|
? "circuitCompact"
|
||||||
|
: "deviceRow",
|
||||||
|
sectionId,
|
||||||
|
circuitId: circuit.id,
|
||||||
|
deviceId: createdRowId,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1646,16 +1805,30 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
deviceId: created.rowId,
|
deviceId: created.rowId,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
undo: async () => {
|
undo: async () => ({
|
||||||
if (createdCircuitId) {
|
|
||||||
await deleteCircuitById(createdCircuitId);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
rowKey: `placeholder:${targetSectionId}`,
|
rowKey: `placeholder:${targetSectionId}`,
|
||||||
cellKey: "displayName",
|
cellKey: "displayName",
|
||||||
rowType: "placeholder",
|
rowType: "placeholder",
|
||||||
sectionId: targetSectionId,
|
sectionId: targetSectionId,
|
||||||
};
|
}),
|
||||||
|
persistent: {
|
||||||
|
undoIntent: () => ({
|
||||||
|
rowKey: `placeholder:${targetSectionId}`,
|
||||||
|
cellKey: "displayName",
|
||||||
|
rowType: "placeholder",
|
||||||
|
sectionId: targetSectionId,
|
||||||
|
}),
|
||||||
|
redoIntent: () =>
|
||||||
|
createdCircuitId && createdRowId
|
||||||
|
? {
|
||||||
|
rowKey: `device:${createdRowId}`,
|
||||||
|
cellKey: "displayName",
|
||||||
|
rowType: "deviceRow",
|
||||||
|
sectionId: targetSectionId,
|
||||||
|
circuitId: createdCircuitId,
|
||||||
|
deviceId: createdRowId,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1698,17 +1871,32 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
deviceId: created.rowId,
|
deviceId: created.rowId,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
undo: async () => {
|
undo: async () => ({
|
||||||
if (createdRowId) {
|
|
||||||
await deleteCircuitDeviceRowById(createdRowId);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
rowKey: `circuitSummary:${circuitId}`,
|
rowKey: `circuitSummary:${circuitId}`,
|
||||||
cellKey: "displayName",
|
cellKey: "displayName",
|
||||||
rowType: "circuitSummary",
|
rowType: "circuitSummary",
|
||||||
sectionId: findCircuitSectionId(circuitId) ?? "",
|
sectionId,
|
||||||
circuitId,
|
circuitId,
|
||||||
};
|
}),
|
||||||
|
persistent: {
|
||||||
|
undoIntent: () => ({
|
||||||
|
rowKey: `circuitSummary:${circuitId}`,
|
||||||
|
cellKey: "displayName",
|
||||||
|
rowType: "circuitSummary",
|
||||||
|
sectionId,
|
||||||
|
circuitId,
|
||||||
|
}),
|
||||||
|
redoIntent: () =>
|
||||||
|
createdRowId
|
||||||
|
? {
|
||||||
|
rowKey: `device:${createdRowId}`,
|
||||||
|
cellKey: "displayName",
|
||||||
|
rowType: "deviceRow",
|
||||||
|
sectionId,
|
||||||
|
circuitId,
|
||||||
|
deviceId: createdRowId,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1721,15 +1909,15 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
const next = await getNextCircuitIdentifier(sectionId);
|
const next = await getNextCircuitIdentifier(sectionId);
|
||||||
const sortOrder =
|
const sortOrder =
|
||||||
section.circuits.length > 0 ? Math.max(...section.circuits.map((circuit) => circuit.sortOrder)) + 10 : 10;
|
section.circuits.length > 0 ? Math.max(...section.circuits.map((circuit) => circuit.sortOrder)) + 10 : 10;
|
||||||
const created = await createCircuitWithDeviceRows(projectId, circuitListId, {
|
const circuit = createCircuitSnapshot(
|
||||||
circuit: {
|
{
|
||||||
sectionId,
|
sectionId,
|
||||||
equipmentIdentifier: next.nextIdentifier,
|
equipmentIdentifier: next.nextIdentifier,
|
||||||
displayName: device.displayName || device.name,
|
displayName: device.displayName || device.name,
|
||||||
sortOrder,
|
sortOrder,
|
||||||
isReserve: false,
|
isReserve: false,
|
||||||
},
|
},
|
||||||
deviceRows: [
|
[
|
||||||
{
|
{
|
||||||
linkedProjectDeviceId: device.id,
|
linkedProjectDeviceId: device.id,
|
||||||
name: device.name,
|
name: device.name,
|
||||||
@@ -1744,18 +1932,27 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
category: device.category ?? undefined,
|
category: device.category ?? undefined,
|
||||||
remark: device.remark ?? undefined,
|
remark: device.remark ?? undefined,
|
||||||
},
|
},
|
||||||
],
|
]
|
||||||
});
|
);
|
||||||
const createdCircuit = created.circuit;
|
await persistCircuitInsert(
|
||||||
const createdRow = created.deviceRows[0];
|
circuit,
|
||||||
|
"Projektgerät als neuen Stromkreis hinzufügen"
|
||||||
|
);
|
||||||
|
const createdRow = circuit.deviceRows[0];
|
||||||
|
|
||||||
setActiveSectionId(sectionId);
|
setActiveSectionId(sectionId);
|
||||||
setTargetCircuitId(createdCircuit.id);
|
setTargetCircuitId(circuit.id);
|
||||||
return { circuitId: createdCircuit.id, rowId: createdRow.id };
|
return { circuitId: circuit.id, rowId: createdRow.id };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function insertProjectDeviceToCircuit(device: ProjectDeviceDto, circuitId: string) {
|
async function insertProjectDeviceToCircuit(device: ProjectDeviceDto, circuitId: string) {
|
||||||
const createdRow = (await createCircuitDeviceRow(circuitId, {
|
const circuit = allCircuits.find((entry) => entry.id === circuitId);
|
||||||
|
if (!circuit) {
|
||||||
|
throw new Error("Der Zielstromkreis ist ungültig.");
|
||||||
|
}
|
||||||
|
const row = createDeviceRowSnapshot(
|
||||||
|
circuitId,
|
||||||
|
{
|
||||||
linkedProjectDeviceId: device.id,
|
linkedProjectDeviceId: device.id,
|
||||||
name: device.name,
|
name: device.name,
|
||||||
displayName: device.displayName,
|
displayName: device.displayName,
|
||||||
@@ -1768,101 +1965,14 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
cosPhi: device.cosPhi ?? undefined,
|
cosPhi: device.cosPhi ?? undefined,
|
||||||
category: device.category ?? undefined,
|
category: device.category ?? undefined,
|
||||||
remark: device.remark ?? undefined,
|
remark: device.remark ?? undefined,
|
||||||
})) as { id: string };
|
|
||||||
return { rowId: createdRow.id };
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCircuitSnapshot(circuitId: string): CircuitSnapshot | null {
|
|
||||||
for (const section of data?.sections ?? []) {
|
|
||||||
const found = section.circuits.find((entry) => entry.id === circuitId);
|
|
||||||
if (!found) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
id: found.id,
|
|
||||||
sectionId: found.sectionId,
|
|
||||||
equipmentIdentifier: found.equipmentIdentifier,
|
|
||||||
displayName: found.displayName ?? undefined,
|
|
||||||
sortOrder: found.sortOrder,
|
|
||||||
protectionType: found.protectionType ?? undefined,
|
|
||||||
protectionRatedCurrent: found.protectionRatedCurrent ?? undefined,
|
|
||||||
protectionCharacteristic: found.protectionCharacteristic ?? undefined,
|
|
||||||
cableType: found.cableType ?? undefined,
|
|
||||||
cableCrossSection: found.cableCrossSection ?? undefined,
|
|
||||||
cableLength: found.cableLength ?? undefined,
|
|
||||||
rcdAssignment: found.rcdAssignment ?? undefined,
|
|
||||||
terminalDesignation: found.terminalDesignation ?? undefined,
|
|
||||||
voltage: found.voltage ?? undefined,
|
|
||||||
controlRequirement: found.controlRequirement ?? undefined,
|
|
||||||
status: found.status ?? undefined,
|
|
||||||
isReserve: found.isReserve,
|
|
||||||
remark: found.remark ?? undefined,
|
|
||||||
deviceRows: found.deviceRows.map((row) => ({ ...row })),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function recreateCircuit(snapshot: CircuitSnapshot) {
|
|
||||||
const circuitInput = {
|
|
||||||
sectionId: snapshot.sectionId,
|
|
||||||
equipmentIdentifier: snapshot.equipmentIdentifier,
|
|
||||||
displayName: snapshot.displayName,
|
|
||||||
sortOrder: snapshot.sortOrder,
|
|
||||||
protectionType: snapshot.protectionType,
|
|
||||||
protectionRatedCurrent: snapshot.protectionRatedCurrent,
|
|
||||||
protectionCharacteristic: snapshot.protectionCharacteristic,
|
|
||||||
cableType: snapshot.cableType,
|
|
||||||
cableCrossSection: snapshot.cableCrossSection,
|
|
||||||
cableLength: snapshot.cableLength,
|
|
||||||
rcdAssignment: snapshot.rcdAssignment,
|
|
||||||
terminalDesignation: snapshot.terminalDesignation,
|
|
||||||
voltage: snapshot.voltage,
|
|
||||||
controlRequirement: snapshot.controlRequirement,
|
|
||||||
status: snapshot.status,
|
|
||||||
isReserve: snapshot.isReserve,
|
|
||||||
remark: snapshot.remark,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (snapshot.deviceRows.length === 0) {
|
|
||||||
const createdCircuit = (await createCircuit(
|
|
||||||
projectId,
|
|
||||||
circuitListId,
|
|
||||||
circuitInput
|
|
||||||
)) as CircuitTreeCircuitDto;
|
|
||||||
return { circuitId: createdCircuit.id, rowIds: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
const created = await createCircuitWithDeviceRows(projectId, circuitListId, {
|
|
||||||
circuit: {
|
|
||||||
...circuitInput,
|
|
||||||
isReserve: false,
|
|
||||||
},
|
},
|
||||||
deviceRows: snapshot.deviceRows.map((row) => ({
|
getInsertionSortOrder(circuit.deviceRows)
|
||||||
linkedProjectDeviceId: row.linkedProjectDeviceId,
|
);
|
||||||
name: row.name,
|
await persistDeviceRowInsert(
|
||||||
displayName: row.displayName,
|
row,
|
||||||
phaseType: row.phaseType,
|
"Projektgerät zum Stromkreis hinzufügen"
|
||||||
connectionKind: row.connectionKind,
|
);
|
||||||
costGroup: row.costGroup,
|
return { rowId: row.id };
|
||||||
category: row.category,
|
|
||||||
level: row.level,
|
|
||||||
roomId: row.roomId,
|
|
||||||
roomNumberSnapshot: row.roomNumberSnapshot,
|
|
||||||
roomNameSnapshot: row.roomNameSnapshot,
|
|
||||||
quantity: row.quantity,
|
|
||||||
powerPerUnit: row.powerPerUnit,
|
|
||||||
simultaneityFactor: row.simultaneityFactor,
|
|
||||||
cosPhi: row.cosPhi,
|
|
||||||
remark: row.remark,
|
|
||||||
overriddenFields: row.overriddenFields,
|
|
||||||
sortOrder: row.sortOrder,
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
circuitId: created.circuit.id,
|
|
||||||
rowIds: created.deviceRows.map((row) => row.id),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseDraggedProjectDeviceId(event: DragEvent<HTMLElement>) {
|
function parseDraggedProjectDeviceId(event: DragEvent<HTMLElement>) {
|
||||||
@@ -2053,11 +2163,13 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
// it can create new rows/circuits instead of moving existing ones.
|
// it can create new rows/circuits instead of moving existing ones.
|
||||||
if (intent.kind === "new-circuit") {
|
if (intent.kind === "new-circuit") {
|
||||||
let createdCircuitId: string | null = null;
|
let createdCircuitId: string | null = null;
|
||||||
|
let createdRowId: string | null = null;
|
||||||
await runCommand({
|
await runCommand({
|
||||||
label: "Projektgerät in neuen Stromkreis ziehen",
|
label: "Projektgerät in neuen Stromkreis ziehen",
|
||||||
redo: async () => {
|
redo: async () => {
|
||||||
const created = await insertProjectDeviceAsNewCircuit(device, intent.sectionId);
|
const created = await insertProjectDeviceAsNewCircuit(device, intent.sectionId);
|
||||||
createdCircuitId = created.circuitId;
|
createdCircuitId = created.circuitId;
|
||||||
|
createdRowId = created.rowId;
|
||||||
return {
|
return {
|
||||||
rowKey: `device:${created.rowId}`,
|
rowKey: `device:${created.rowId}`,
|
||||||
cellKey: "displayName",
|
cellKey: "displayName",
|
||||||
@@ -2067,11 +2179,20 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
deviceId: created.rowId,
|
deviceId: created.rowId,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
undo: async () => {
|
undo: async () => null,
|
||||||
if (createdCircuitId) {
|
persistent: {
|
||||||
await deleteCircuitById(createdCircuitId);
|
undoIntent: () => null,
|
||||||
|
redoIntent: () =>
|
||||||
|
createdCircuitId && createdRowId
|
||||||
|
? {
|
||||||
|
rowKey: `device:${createdRowId}`,
|
||||||
|
cellKey: "displayName",
|
||||||
|
rowType: "deviceRow",
|
||||||
|
sectionId: intent.sectionId,
|
||||||
|
circuitId: createdCircuitId,
|
||||||
|
deviceId: createdRowId,
|
||||||
}
|
}
|
||||||
return null;
|
: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -2091,11 +2212,20 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
deviceId: created.rowId,
|
deviceId: created.rowId,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
undo: async () => {
|
undo: async () => null,
|
||||||
if (createdRowId) {
|
persistent: {
|
||||||
await deleteCircuitDeviceRowById(createdRowId);
|
undoIntent: () => null,
|
||||||
|
redoIntent: () =>
|
||||||
|
createdRowId
|
||||||
|
? {
|
||||||
|
rowKey: `device:${createdRowId}`,
|
||||||
|
cellKey: "displayName",
|
||||||
|
rowType: "deviceRow",
|
||||||
|
sectionId: intent.sectionId,
|
||||||
|
circuitId: intent.circuitId,
|
||||||
|
deviceId: createdRowId,
|
||||||
}
|
}
|
||||||
return null;
|
: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2259,10 +2389,12 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete undo recreates the row from captured snapshot because delete is destructive.
|
// Persistent delete commands preserve the complete row and its stable id.
|
||||||
async function handleDeleteDevice(rowId: string) {
|
async function handleDeleteDevice(rowId: string) {
|
||||||
const sourceCircuitId = findDeviceRowCircuitId(rowId);
|
const sourceCircuitId = findDeviceRowCircuitId(rowId);
|
||||||
const sourceCircuit = sourceCircuitId ? getCircuitSnapshot(sourceCircuitId) : null;
|
const sourceCircuit = sourceCircuitId
|
||||||
|
? allCircuits.find((circuit) => circuit.id === sourceCircuitId)
|
||||||
|
: null;
|
||||||
const rowSnapshot = sourceCircuit?.deviceRows.find((row) => row.id === rowId);
|
const rowSnapshot = sourceCircuit?.deviceRows.find((row) => row.id === rowId);
|
||||||
if (!sourceCircuitId || !sourceCircuit || !rowSnapshot) {
|
if (!sourceCircuitId || !sourceCircuit || !rowSnapshot) {
|
||||||
setError("Die Gerätezeile ist ungültig.");
|
setError("Die Gerätezeile ist ungültig.");
|
||||||
@@ -2287,68 +2419,50 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
} else if (!confirm(`Gerätezeile „${rowSnapshot.displayName}“ löschen?`)) {
|
} else if (!confirm(`Gerätezeile „${rowSnapshot.displayName}“ löschen?`)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let recreatedRowId: string | null = null;
|
|
||||||
await runCommand({
|
await runCommand({
|
||||||
label: "Gerätezeile löschen",
|
label: "Gerätezeile löschen",
|
||||||
redo: async () => {
|
redo: async () => {
|
||||||
await deleteCircuitDeviceRowById(recreatedRowId ?? rowId);
|
await persistDeviceRowDelete(
|
||||||
|
rowId,
|
||||||
|
sourceCircuitId,
|
||||||
|
"Gerätezeile löschen"
|
||||||
|
);
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
undo: async () => {
|
undo: async () => null,
|
||||||
// Recreate instead of "undelete": backend ids are immutable once deleted.
|
persistent: {
|
||||||
const created = (await createCircuitDeviceRow(sourceCircuitId, {
|
undoIntent: () => null,
|
||||||
linkedProjectDeviceId: rowSnapshot.linkedProjectDeviceId,
|
redoIntent: () => null,
|
||||||
name: rowSnapshot.name,
|
|
||||||
displayName: rowSnapshot.displayName,
|
|
||||||
phaseType: rowSnapshot.phaseType,
|
|
||||||
connectionKind: rowSnapshot.connectionKind,
|
|
||||||
costGroup: rowSnapshot.costGroup,
|
|
||||||
category: rowSnapshot.category,
|
|
||||||
level: rowSnapshot.level,
|
|
||||||
roomId: rowSnapshot.roomId,
|
|
||||||
roomNumberSnapshot: rowSnapshot.roomNumberSnapshot,
|
|
||||||
roomNameSnapshot: rowSnapshot.roomNameSnapshot,
|
|
||||||
quantity: rowSnapshot.quantity,
|
|
||||||
powerPerUnit: rowSnapshot.powerPerUnit,
|
|
||||||
simultaneityFactor: rowSnapshot.simultaneityFactor,
|
|
||||||
cosPhi: rowSnapshot.cosPhi,
|
|
||||||
remark: rowSnapshot.remark,
|
|
||||||
overriddenFields: rowSnapshot.overriddenFields,
|
|
||||||
sortOrder: rowSnapshot.sortOrder,
|
|
||||||
})) as { id: string };
|
|
||||||
recreatedRowId = created.id;
|
|
||||||
return null;
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deletes a circuit and all child rows; undo recreates from captured snapshot.
|
// Persistent circuit delete preserves the complete circuit block and all stable ids.
|
||||||
async function handleDeleteCircuit(circuitId: string, askForConfirmation = true) {
|
async function handleDeleteCircuit(circuitId: string, askForConfirmation = true) {
|
||||||
const snapshot = getCircuitSnapshot(circuitId);
|
const circuit = allCircuits.find((entry) => entry.id === circuitId);
|
||||||
if (!snapshot) {
|
if (!circuit) {
|
||||||
setError("Der Stromkreis ist ungültig.");
|
setError("Der Stromkreis ist ungültig.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
askForConfirmation &&
|
askForConfirmation &&
|
||||||
!confirm(
|
!confirm(
|
||||||
`Stromkreis ${snapshot.equipmentIdentifier} und ${snapshot.deviceRows.length} zugeordnete Gerätezeile(n) löschen?\n\n` +
|
`Stromkreis ${circuit.equipmentIdentifier} und ${circuit.deviceRows.length} zugeordnete Gerätezeile(n) löschen?\n\n` +
|
||||||
"Bestehende Betriebsmittelkennzeichen werden nicht neu nummeriert."
|
"Bestehende Betriebsmittelkennzeichen werden nicht neu nummeriert."
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let recreatedCircuitId: string | null = null;
|
|
||||||
await runCommand({
|
await runCommand({
|
||||||
label: "Stromkreis löschen",
|
label: "Stromkreis löschen",
|
||||||
redo: async () => {
|
redo: async () => {
|
||||||
await deleteCircuitById(recreatedCircuitId ?? circuitId);
|
await persistCircuitDelete(circuitId, "Stromkreis löschen");
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
undo: async () => {
|
undo: async () => null,
|
||||||
const recreated = await recreateCircuit(snapshot);
|
persistent: {
|
||||||
recreatedCircuitId = recreated.circuitId;
|
undoIntent: () => null,
|
||||||
return null;
|
redoIntent: () => null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+78
-42
@@ -17,10 +17,6 @@ import type {
|
|||||||
ProjectDto,
|
ProjectDto,
|
||||||
RoomDto,
|
RoomDto,
|
||||||
CircuitTreeResponseDto,
|
CircuitTreeResponseDto,
|
||||||
CircuitTreeCircuitDto,
|
|
||||||
CircuitTreeDeviceRowDto,
|
|
||||||
CreateCircuitInputDto,
|
|
||||||
CreateCircuitDeviceRowInputDto,
|
|
||||||
} 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 {
|
import {
|
||||||
@@ -31,6 +27,12 @@ import {
|
|||||||
createCircuitDeviceRowUpdateProjectCommand,
|
createCircuitDeviceRowUpdateProjectCommand,
|
||||||
type CircuitDeviceRowUpdatePatch,
|
type CircuitDeviceRowUpdatePatch,
|
||||||
} from "../../domain/models/circuit-device-row-project-command.model";
|
} from "../../domain/models/circuit-device-row-project-command.model";
|
||||||
|
import type {
|
||||||
|
CircuitDeviceRowSnapshot,
|
||||||
|
} from "../../domain/models/circuit-device-row-structure-project-command.model";
|
||||||
|
import type {
|
||||||
|
CircuitSnapshot,
|
||||||
|
} from "../../domain/models/circuit-structure-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, {
|
||||||
@@ -159,33 +161,6 @@ export function getCircuitTree(projectId: string, circuitListId: string) {
|
|||||||
return request<CircuitTreeResponseDto>(`/api/projects/${projectId}/circuit-lists/${circuitListId}/tree`);
|
return request<CircuitTreeResponseDto>(`/api/projects/${projectId}/circuit-lists/${circuitListId}/tree`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createCircuit(projectId: string, circuitListId: string, input: CreateCircuitInputDto) {
|
|
||||||
return request(`/api/projects/${projectId}/circuit-lists/${circuitListId}/circuits`, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(input),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createCircuitWithDeviceRows(
|
|
||||||
projectId: string,
|
|
||||||
circuitListId: string,
|
|
||||||
input: {
|
|
||||||
circuit: CreateCircuitInputDto;
|
|
||||||
deviceRows: CreateCircuitDeviceRowInputDto[];
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
return request<{
|
|
||||||
circuit: CircuitTreeCircuitDto;
|
|
||||||
deviceRows: CircuitTreeDeviceRowDto[];
|
|
||||||
}>(
|
|
||||||
`/api/projects/${projectId}/circuit-lists/${circuitListId}/circuits-with-device-rows`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(input),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateCircuitById(
|
export function updateCircuitById(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
expectedRevision: number,
|
expectedRevision: number,
|
||||||
@@ -200,6 +175,43 @@ export function updateCircuitById(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function insertCircuitCommand(
|
||||||
|
projectId: string,
|
||||||
|
expectedRevision: number,
|
||||||
|
circuit: CircuitSnapshot,
|
||||||
|
description: string
|
||||||
|
) {
|
||||||
|
return executeProjectCommand(
|
||||||
|
projectId,
|
||||||
|
expectedRevision,
|
||||||
|
{
|
||||||
|
schemaVersion: 1,
|
||||||
|
type: "circuit.insert",
|
||||||
|
payload: { circuit },
|
||||||
|
},
|
||||||
|
description
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteCircuitCommand(
|
||||||
|
projectId: string,
|
||||||
|
expectedRevision: number,
|
||||||
|
circuitId: string,
|
||||||
|
expectedCircuitListId: string,
|
||||||
|
description: string
|
||||||
|
) {
|
||||||
|
return executeProjectCommand(
|
||||||
|
projectId,
|
||||||
|
expectedRevision,
|
||||||
|
{
|
||||||
|
schemaVersion: 1,
|
||||||
|
type: "circuit.delete",
|
||||||
|
payload: { circuitId, expectedCircuitListId },
|
||||||
|
},
|
||||||
|
description
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function deleteCircuitById(circuitId: string) {
|
export function deleteCircuitById(circuitId: string) {
|
||||||
return request<void>(`/api/circuits/${circuitId}`, {
|
return request<void>(`/api/circuits/${circuitId}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
@@ -212,13 +224,6 @@ export function getNextCircuitIdentifier(sectionId: string) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createCircuitDeviceRow(circuitId: string, input: CreateCircuitDeviceRowInputDto) {
|
|
||||||
return request(`/api/circuits/${circuitId}/device-rows`, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(input),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateCircuitDeviceRowById(
|
export function updateCircuitDeviceRowById(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
expectedRevision: number,
|
expectedRevision: number,
|
||||||
@@ -233,10 +238,41 @@ export function updateCircuitDeviceRowById(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteCircuitDeviceRowById(rowId: string) {
|
export function insertCircuitDeviceRowCommand(
|
||||||
return request<void>(`/api/circuit-device-rows/${rowId}`, {
|
projectId: string,
|
||||||
method: "DELETE",
|
expectedRevision: number,
|
||||||
});
|
row: CircuitDeviceRowSnapshot,
|
||||||
|
description: string
|
||||||
|
) {
|
||||||
|
return executeProjectCommand(
|
||||||
|
projectId,
|
||||||
|
expectedRevision,
|
||||||
|
{
|
||||||
|
schemaVersion: 1,
|
||||||
|
type: "circuit-device-row.insert",
|
||||||
|
payload: { row },
|
||||||
|
},
|
||||||
|
description
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteCircuitDeviceRowCommand(
|
||||||
|
projectId: string,
|
||||||
|
expectedRevision: number,
|
||||||
|
rowId: string,
|
||||||
|
expectedCircuitId: string,
|
||||||
|
description: string
|
||||||
|
) {
|
||||||
|
return executeProjectCommand(
|
||||||
|
projectId,
|
||||||
|
expectedRevision,
|
||||||
|
{
|
||||||
|
schemaVersion: 1,
|
||||||
|
type: "circuit-device-row.delete",
|
||||||
|
payload: { rowId, expectedCircuitId },
|
||||||
|
},
|
||||||
|
description
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function moveCircuitDeviceRowById(
|
export function moveCircuitDeviceRowById(
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import type {
|
||||||
|
CircuitDeviceRowSnapshot,
|
||||||
|
} from "../../domain/models/circuit-device-row-structure-project-command.model.js";
|
||||||
|
import type {
|
||||||
|
CircuitSnapshot,
|
||||||
|
} from "../../domain/models/circuit-structure-project-command.model.js";
|
||||||
|
import type {
|
||||||
|
CreateCircuitDeviceRowInputDto,
|
||||||
|
CreateCircuitInputDto,
|
||||||
|
} from "../types.js";
|
||||||
|
|
||||||
|
export function buildCircuitDeviceRowInsertSnapshot(input: {
|
||||||
|
id: string;
|
||||||
|
circuitId: string;
|
||||||
|
values: CreateCircuitDeviceRowInputDto;
|
||||||
|
defaultSortOrder: number;
|
||||||
|
}): CircuitDeviceRowSnapshot {
|
||||||
|
const { id, circuitId, values, defaultSortOrder } = input;
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
circuitId,
|
||||||
|
linkedProjectDeviceId: values.linkedProjectDeviceId ?? null,
|
||||||
|
legacyConsumerId: null,
|
||||||
|
sortOrder: values.sortOrder ?? defaultSortOrder,
|
||||||
|
name: values.name,
|
||||||
|
displayName: values.displayName,
|
||||||
|
phaseType: values.phaseType ?? null,
|
||||||
|
connectionKind: values.connectionKind ?? null,
|
||||||
|
costGroup: values.costGroup ?? null,
|
||||||
|
category: values.category ?? null,
|
||||||
|
level: values.level ?? null,
|
||||||
|
roomId: values.roomId ?? null,
|
||||||
|
roomNumberSnapshot: values.roomNumberSnapshot ?? null,
|
||||||
|
roomNameSnapshot: values.roomNameSnapshot ?? null,
|
||||||
|
quantity: values.quantity,
|
||||||
|
powerPerUnit: values.powerPerUnit,
|
||||||
|
simultaneityFactor: values.simultaneityFactor,
|
||||||
|
cosPhi: values.cosPhi ?? null,
|
||||||
|
remark: values.remark ?? null,
|
||||||
|
overriddenFields: values.overriddenFields ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildCircuitInsertSnapshot(input: {
|
||||||
|
id: string;
|
||||||
|
circuitListId: string;
|
||||||
|
values: CreateCircuitInputDto;
|
||||||
|
deviceRows: CircuitDeviceRowSnapshot[];
|
||||||
|
}): CircuitSnapshot {
|
||||||
|
const { id, circuitListId, values, deviceRows } = input;
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
circuitListId,
|
||||||
|
sectionId: values.sectionId,
|
||||||
|
equipmentIdentifier: values.equipmentIdentifier,
|
||||||
|
displayName: values.displayName ?? null,
|
||||||
|
sortOrder: values.sortOrder,
|
||||||
|
protectionType: values.protectionType ?? null,
|
||||||
|
protectionRatedCurrent: values.protectionRatedCurrent ?? null,
|
||||||
|
protectionCharacteristic: values.protectionCharacteristic ?? null,
|
||||||
|
cableType: values.cableType ?? null,
|
||||||
|
cableCrossSection: values.cableCrossSection ?? null,
|
||||||
|
cableLength: values.cableLength ?? null,
|
||||||
|
rcdAssignment: values.rcdAssignment ?? null,
|
||||||
|
terminalDesignation: values.terminalDesignation ?? null,
|
||||||
|
voltage: values.voltage ?? null,
|
||||||
|
controlRequirement: values.controlRequirement ?? null,
|
||||||
|
status: values.status ?? null,
|
||||||
|
isReserve: deviceRows.length === 0,
|
||||||
|
remark: values.remark ?? null,
|
||||||
|
deviceRows,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,44 +1,10 @@
|
|||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import { circuitWriteService } from "../composition/circuit-write-service.js";
|
import { circuitWriteService } from "../composition/circuit-write-service.js";
|
||||||
import {
|
import {
|
||||||
createCircuitDeviceRowSchema,
|
|
||||||
moveCircuitDeviceRowsBulkSchema,
|
moveCircuitDeviceRowsBulkSchema,
|
||||||
moveCircuitDeviceRowSchema,
|
moveCircuitDeviceRowSchema,
|
||||||
} from "../../shared/validation/circuit.schemas.js";
|
} from "../../shared/validation/circuit.schemas.js";
|
||||||
|
|
||||||
export async function createCircuitDeviceRow(req: Request, res: Response) {
|
|
||||||
const { circuitId } = req.params;
|
|
||||||
if (typeof circuitId !== "string") {
|
|
||||||
return res.status(400).json({ error: "Invalid circuitId" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsed = createCircuitDeviceRowSchema.safeParse(req.body);
|
|
||||||
if (!parsed.success) {
|
|
||||||
return res.status(400).json({ error: parsed.error.flatten() });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const created = await circuitWriteService.createDeviceRow(circuitId, parsed.data);
|
|
||||||
return res.status(201).json(created);
|
|
||||||
} catch (error) {
|
|
||||||
return res.status(400).json({ error: error instanceof Error ? error.message : "Failed to create device row." });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteCircuitDeviceRow(req: Request, res: Response) {
|
|
||||||
const { rowId } = req.params;
|
|
||||||
if (typeof rowId !== "string") {
|
|
||||||
return res.status(400).json({ error: "Invalid rowId" });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await circuitWriteService.deleteDeviceRow(rowId);
|
|
||||||
return res.status(204).send();
|
|
||||||
} catch (error) {
|
|
||||||
return res.status(400).json({ error: error instanceof Error ? error.message : "Failed to delete device row." });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function moveCircuitDeviceRow(req: Request, res: Response) {
|
export async function moveCircuitDeviceRow(req: Request, res: Response) {
|
||||||
const { rowId } = req.params;
|
const { rowId } = req.params;
|
||||||
if (typeof rowId !== "string") {
|
if (typeof rowId !== "string") {
|
||||||
|
|||||||
@@ -1,56 +1,5 @@
|
|||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import { circuitWriteService } from "../composition/circuit-write-service.js";
|
import { circuitWriteService } from "../composition/circuit-write-service.js";
|
||||||
import {
|
|
||||||
createCircuitSchema,
|
|
||||||
createCircuitWithDeviceRowsSchema,
|
|
||||||
} from "../../shared/validation/circuit.schemas.js";
|
|
||||||
|
|
||||||
export async function createCircuit(req: Request, res: Response) {
|
|
||||||
const { projectId, circuitListId } = req.params;
|
|
||||||
if (typeof projectId !== "string" || typeof circuitListId !== "string") {
|
|
||||||
return res.status(400).json({ error: "Invalid parameters" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsed = createCircuitSchema.safeParse(req.body);
|
|
||||||
if (!parsed.success) {
|
|
||||||
return res.status(400).json({ error: parsed.error.flatten() });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const created = await circuitWriteService.createCircuit(projectId, circuitListId, parsed.data);
|
|
||||||
return res.status(201).json(created);
|
|
||||||
} catch (error) {
|
|
||||||
return res.status(400).json({ error: error instanceof Error ? error.message : "Failed to create circuit." });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createCircuitWithDeviceRows(req: Request, res: Response) {
|
|
||||||
const { projectId, circuitListId } = req.params;
|
|
||||||
if (typeof projectId !== "string" || typeof circuitListId !== "string") {
|
|
||||||
return res.status(400).json({ error: "Ungültige Parameter." });
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsed = createCircuitWithDeviceRowsSchema.safeParse(req.body);
|
|
||||||
if (!parsed.success) {
|
|
||||||
return res.status(400).json({ error: parsed.error.flatten() });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const created = await circuitWriteService.createCircuitWithDeviceRows(
|
|
||||||
projectId,
|
|
||||||
circuitListId,
|
|
||||||
parsed.data
|
|
||||||
);
|
|
||||||
return res.status(201).json(created);
|
|
||||||
} catch (error) {
|
|
||||||
return res.status(400).json({
|
|
||||||
error:
|
|
||||||
error instanceof Error
|
|
||||||
? error.message
|
|
||||||
: "Stromkreis und Gerätezeilen konnten nicht erstellt werden.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteCircuit(req: Request, res: Response) {
|
export async function deleteCircuit(req: Request, res: Response) {
|
||||||
const { circuitId } = req.params;
|
const { circuitId } = req.params;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import {
|
import {
|
||||||
deleteCircuitDeviceRow,
|
|
||||||
moveCircuitDeviceRowsBulk,
|
moveCircuitDeviceRowsBulk,
|
||||||
moveCircuitDeviceRow,
|
moveCircuitDeviceRow,
|
||||||
} from "../controllers/circuit-device-row.controller.js";
|
} from "../controllers/circuit-device-row.controller.js";
|
||||||
@@ -9,4 +8,3 @@ 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/move", moveCircuitDeviceRow);
|
circuitDeviceRowRouter.patch("/circuit-device-rows/:rowId/move", moveCircuitDeviceRow);
|
||||||
circuitDeviceRowRouter.delete("/circuit-device-rows/:rowId", deleteCircuitDeviceRow);
|
|
||||||
|
|||||||
@@ -1,20 +1,11 @@
|
|||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import {
|
import {
|
||||||
createCircuit,
|
|
||||||
createCircuitWithDeviceRows,
|
|
||||||
deleteCircuit,
|
deleteCircuit,
|
||||||
getNextCircuitIdentifier,
|
getNextCircuitIdentifier,
|
||||||
} from "../controllers/circuit.controller.js";
|
} from "../controllers/circuit.controller.js";
|
||||||
import { createCircuitDeviceRow } from "../controllers/circuit-device-row.controller.js";
|
|
||||||
|
|
||||||
export const circuitRouter = Router();
|
export const circuitRouter = Router();
|
||||||
|
|
||||||
circuitRouter.post("/projects/:projectId/circuit-lists/:circuitListId/circuits", createCircuit);
|
|
||||||
circuitRouter.post(
|
|
||||||
"/projects/:projectId/circuit-lists/:circuitListId/circuits-with-device-rows",
|
|
||||||
createCircuitWithDeviceRows
|
|
||||||
);
|
|
||||||
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);
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,51 +1,5 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const createCircuitSchema = z.object({
|
|
||||||
sectionId: z.string().min(1),
|
|
||||||
equipmentIdentifier: z.string().min(1),
|
|
||||||
displayName: z.string().optional(),
|
|
||||||
sortOrder: z.number(),
|
|
||||||
protectionType: z.string().optional(),
|
|
||||||
protectionRatedCurrent: z.number().min(0).optional(),
|
|
||||||
protectionCharacteristic: z.string().optional(),
|
|
||||||
cableType: z.string().optional(),
|
|
||||||
cableCrossSection: z.string().optional(),
|
|
||||||
cableLength: z.number().min(0).optional(),
|
|
||||||
rcdAssignment: z.string().optional(),
|
|
||||||
terminalDesignation: z.string().optional(),
|
|
||||||
voltage: z.number().positive().optional(),
|
|
||||||
controlRequirement: z.string().optional(),
|
|
||||||
status: z.string().optional(),
|
|
||||||
isReserve: z.boolean().optional(),
|
|
||||||
remark: z.string().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const createCircuitDeviceRowSchema = z.object({
|
|
||||||
linkedProjectDeviceId: z.string().min(1).optional(),
|
|
||||||
name: z.string().min(1),
|
|
||||||
displayName: z.string().min(1),
|
|
||||||
phaseType: z.string().optional(),
|
|
||||||
connectionKind: z.string().optional(),
|
|
||||||
costGroup: z.string().optional(),
|
|
||||||
category: z.string().optional(),
|
|
||||||
level: z.string().optional(),
|
|
||||||
roomId: z.string().min(1).optional(),
|
|
||||||
roomNumberSnapshot: z.string().optional(),
|
|
||||||
roomNameSnapshot: z.string().optional(),
|
|
||||||
quantity: z.number().min(0),
|
|
||||||
powerPerUnit: z.number().min(0),
|
|
||||||
simultaneityFactor: z.number().min(0),
|
|
||||||
cosPhi: z.number().positive().optional(),
|
|
||||||
remark: z.string().optional(),
|
|
||||||
overriddenFields: z.string().optional(),
|
|
||||||
sortOrder: z.number().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const createCircuitWithDeviceRowsSchema = z.object({
|
|
||||||
circuit: createCircuitSchema,
|
|
||||||
deviceRows: z.array(createCircuitDeviceRowSchema).min(1),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const moveCircuitDeviceRowSchema = z
|
export const moveCircuitDeviceRowSchema = z
|
||||||
.object({
|
.object({
|
||||||
targetCircuitId: z.string().min(1).optional(),
|
targetCircuitId: z.string().min(1).optional(),
|
||||||
@@ -88,9 +42,6 @@ export const updateSectionEquipmentIdentifiersSchema = z.object({
|
|||||||
.min(1),
|
.min(1),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type CreateCircuitInput = z.infer<typeof createCircuitSchema>;
|
|
||||||
export type CreateCircuitDeviceRowInput = z.infer<typeof createCircuitDeviceRowSchema>;
|
|
||||||
export type CreateCircuitWithDeviceRowsInput = z.infer<typeof createCircuitWithDeviceRowsSchema>;
|
|
||||||
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>;
|
||||||
|
|||||||
@@ -0,0 +1,207 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { describe, it } from "node:test";
|
||||||
|
import {
|
||||||
|
buildCircuitDeviceRowInsertSnapshot,
|
||||||
|
buildCircuitInsertSnapshot,
|
||||||
|
} from "../src/frontend/utils/circuit-structure-command.js";
|
||||||
|
import {
|
||||||
|
deleteCircuitCommand,
|
||||||
|
deleteCircuitDeviceRowCommand,
|
||||||
|
insertCircuitCommand,
|
||||||
|
insertCircuitDeviceRowCommand,
|
||||||
|
} from "../src/frontend/utils/api.js";
|
||||||
|
|
||||||
|
describe("circuit structure command adapters", () => {
|
||||||
|
it("builds a complete row snapshot without legacy identities", () => {
|
||||||
|
const row = buildCircuitDeviceRowInsertSnapshot({
|
||||||
|
id: "row-1",
|
||||||
|
circuitId: "circuit-1",
|
||||||
|
defaultSortOrder: 20,
|
||||||
|
values: {
|
||||||
|
linkedProjectDeviceId: "device-1",
|
||||||
|
name: "Steckdose",
|
||||||
|
displayName: "Steckdose Büro",
|
||||||
|
quantity: 2,
|
||||||
|
powerPerUnit: 0.4,
|
||||||
|
simultaneityFactor: 0.8,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(row, {
|
||||||
|
id: "row-1",
|
||||||
|
circuitId: "circuit-1",
|
||||||
|
linkedProjectDeviceId: "device-1",
|
||||||
|
legacyConsumerId: null,
|
||||||
|
sortOrder: 20,
|
||||||
|
name: "Steckdose",
|
||||||
|
displayName: "Steckdose Büro",
|
||||||
|
phaseType: null,
|
||||||
|
connectionKind: null,
|
||||||
|
costGroup: null,
|
||||||
|
category: null,
|
||||||
|
level: null,
|
||||||
|
roomId: null,
|
||||||
|
roomNumberSnapshot: null,
|
||||||
|
roomNameSnapshot: null,
|
||||||
|
quantity: 2,
|
||||||
|
powerPerUnit: 0.4,
|
||||||
|
simultaneityFactor: 0.8,
|
||||||
|
cosPhi: null,
|
||||||
|
remark: null,
|
||||||
|
overriddenFields: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("derives reserve state from the complete device-row snapshot", () => {
|
||||||
|
const reserve = buildCircuitInsertSnapshot({
|
||||||
|
id: "circuit-1",
|
||||||
|
circuitListId: "list-1",
|
||||||
|
values: {
|
||||||
|
sectionId: "section-1",
|
||||||
|
equipmentIdentifier: "-2F1",
|
||||||
|
sortOrder: 10,
|
||||||
|
isReserve: false,
|
||||||
|
voltage: 230,
|
||||||
|
controlRequirement: "DALI",
|
||||||
|
},
|
||||||
|
deviceRows: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(reserve.isReserve, true);
|
||||||
|
assert.equal(reserve.displayName, null);
|
||||||
|
assert.equal(reserve.voltage, 230);
|
||||||
|
assert.equal(reserve.controlRequirement, "DALI");
|
||||||
|
|
||||||
|
const row = buildCircuitDeviceRowInsertSnapshot({
|
||||||
|
id: "row-1",
|
||||||
|
circuitId: reserve.id,
|
||||||
|
defaultSortOrder: 10,
|
||||||
|
values: {
|
||||||
|
name: "Gerät",
|
||||||
|
displayName: "Gerät",
|
||||||
|
quantity: 1,
|
||||||
|
powerPerUnit: 0,
|
||||||
|
simultaneityFactor: 1,
|
||||||
|
cosPhi: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const populated = buildCircuitInsertSnapshot({
|
||||||
|
id: reserve.id,
|
||||||
|
circuitListId: reserve.circuitListId,
|
||||||
|
values: {
|
||||||
|
sectionId: reserve.sectionId,
|
||||||
|
equipmentIdentifier: reserve.equipmentIdentifier,
|
||||||
|
sortOrder: reserve.sortOrder,
|
||||||
|
isReserve: true,
|
||||||
|
},
|
||||||
|
deviceRows: [row],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(populated.isReserve, false);
|
||||||
|
assert.deepEqual(populated.deviceRows, [row]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sends structure envelopes through the project command route", async () => {
|
||||||
|
const requests: Array<{ url: string; body: Record<string, unknown> }> = [];
|
||||||
|
const originalFetch = globalThis.fetch;
|
||||||
|
globalThis.fetch = async (input, init) => {
|
||||||
|
requests.push({
|
||||||
|
url: String(input),
|
||||||
|
body: JSON.parse(String(init?.body)) as Record<string, unknown>,
|
||||||
|
});
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
revision: {
|
||||||
|
revisionId: "revision-1",
|
||||||
|
changeSetId: "change-1",
|
||||||
|
projectId: "project-1",
|
||||||
|
revisionNumber: requests.length,
|
||||||
|
createdAtIso: "2026-07-25T00:00:00.000Z",
|
||||||
|
},
|
||||||
|
history: {
|
||||||
|
projectId: "project-1",
|
||||||
|
currentRevision: requests.length,
|
||||||
|
undoDepth: requests.length,
|
||||||
|
redoDepth: 0,
|
||||||
|
undoChangeSetId: "change-1",
|
||||||
|
redoChangeSetId: null,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{ status: 200, headers: { "Content-Type": "application/json" } }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const row = buildCircuitDeviceRowInsertSnapshot({
|
||||||
|
id: "row-1",
|
||||||
|
circuitId: "circuit-1",
|
||||||
|
defaultSortOrder: 10,
|
||||||
|
values: {
|
||||||
|
name: "Gerät",
|
||||||
|
displayName: "Gerät",
|
||||||
|
quantity: 1,
|
||||||
|
powerPerUnit: 0,
|
||||||
|
simultaneityFactor: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const circuit = buildCircuitInsertSnapshot({
|
||||||
|
id: "circuit-1",
|
||||||
|
circuitListId: "list-1",
|
||||||
|
values: {
|
||||||
|
sectionId: "section-1",
|
||||||
|
equipmentIdentifier: "-2F1",
|
||||||
|
sortOrder: 10,
|
||||||
|
},
|
||||||
|
deviceRows: [row],
|
||||||
|
});
|
||||||
|
|
||||||
|
await insertCircuitCommand("project-1", 0, circuit, "insert circuit");
|
||||||
|
await deleteCircuitCommand(
|
||||||
|
"project-1",
|
||||||
|
1,
|
||||||
|
circuit.id,
|
||||||
|
circuit.circuitListId,
|
||||||
|
"delete circuit"
|
||||||
|
);
|
||||||
|
await insertCircuitDeviceRowCommand(
|
||||||
|
"project-1",
|
||||||
|
2,
|
||||||
|
row,
|
||||||
|
"insert row"
|
||||||
|
);
|
||||||
|
await deleteCircuitDeviceRowCommand(
|
||||||
|
"project-1",
|
||||||
|
3,
|
||||||
|
row.id,
|
||||||
|
row.circuitId,
|
||||||
|
"delete row"
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
globalThis.fetch = originalFetch;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
requests.map((request) => request.url),
|
||||||
|
Array(4).fill("/api/projects/project-1/commands")
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
requests.map((request) => {
|
||||||
|
const command = request.body.command as {
|
||||||
|
schemaVersion: number;
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
return [
|
||||||
|
request.body.expectedRevision,
|
||||||
|
command.schemaVersion,
|
||||||
|
command.type,
|
||||||
|
];
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
[0, 1, "circuit.insert"],
|
||||||
|
[1, 1, "circuit.delete"],
|
||||||
|
[2, 1, "circuit-device-row.insert"],
|
||||||
|
[3, 1, "circuit-device-row.delete"],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,265 +1,8 @@
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { describe, it } from "node:test";
|
import { describe, it } from "node:test";
|
||||||
import { CircuitWriteService } from "../src/domain/services/circuit-write.service.js";
|
import { CircuitWriteService } from "../src/domain/services/circuit-write.service.js";
|
||||||
import { createCircuitSchema } from "../src/shared/validation/circuit.schemas.js";
|
|
||||||
|
|
||||||
describe("circuit write service rules", () => {
|
describe("circuit write service rules", () => {
|
||||||
it("accepts future sizing inputs without calculating sizing suggestions", () => {
|
|
||||||
const parsed = createCircuitSchema.safeParse({
|
|
||||||
sectionId: "s1",
|
|
||||||
equipmentIdentifier: "-1F1",
|
|
||||||
sortOrder: 10,
|
|
||||||
voltage: 230,
|
|
||||||
controlRequirement: "DALI",
|
|
||||||
});
|
|
||||||
|
|
||||||
assert.equal(parsed.success, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects duplicate equipment identifiers in same circuit list", async () => {
|
|
||||||
const service = new CircuitWriteService({
|
|
||||||
circuitListRepository: {
|
|
||||||
async findById() {
|
|
||||||
return { id: "list1", projectId: "p1" } as never;
|
|
||||||
},
|
|
||||||
} as never,
|
|
||||||
circuitSectionRepository: {
|
|
||||||
async findById() {
|
|
||||||
return { id: "sec1", circuitListId: "list1" } as never;
|
|
||||||
},
|
|
||||||
} as never,
|
|
||||||
circuitRepository: {
|
|
||||||
async existsByEquipmentIdentifier() {
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
} as never,
|
|
||||||
});
|
|
||||||
|
|
||||||
await assert.rejects(
|
|
||||||
() =>
|
|
||||||
service.createCircuit("p1", "list1", {
|
|
||||||
sectionId: "sec1",
|
|
||||||
equipmentIdentifier: "-2F1",
|
|
||||||
sortOrder: 10,
|
|
||||||
}),
|
|
||||||
/Duplicate equipmentIdentifier/
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects section and list mismatch", async () => {
|
|
||||||
const service = new CircuitWriteService({
|
|
||||||
circuitListRepository: {
|
|
||||||
async findById() {
|
|
||||||
return { id: "list1", projectId: "p1" } as never;
|
|
||||||
},
|
|
||||||
} as never,
|
|
||||||
circuitSectionRepository: {
|
|
||||||
async findById() {
|
|
||||||
return { id: "sec1", circuitListId: "other" } as never;
|
|
||||||
},
|
|
||||||
} as never,
|
|
||||||
circuitRepository: {
|
|
||||||
async existsByEquipmentIdentifier() {
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
} as never,
|
|
||||||
});
|
|
||||||
|
|
||||||
await assert.rejects(
|
|
||||||
() =>
|
|
||||||
service.createCircuit("p1", "list1", {
|
|
||||||
sectionId: "sec1",
|
|
||||||
equipmentIdentifier: "-2F1",
|
|
||||||
sortOrder: 10,
|
|
||||||
}),
|
|
||||||
/Section does not belong to circuit list/
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("deleting last device row keeps circuit and sets reserve", async () => {
|
|
||||||
let transactionalDelete: { rowId: string; circuitId: string } | undefined;
|
|
||||||
const service = new CircuitWriteService({
|
|
||||||
deviceRowRepository: {
|
|
||||||
async findById() {
|
|
||||||
return { id: "r1", circuitId: "c1" } as never;
|
|
||||||
},
|
|
||||||
} as never,
|
|
||||||
deviceRowTransactionStore: {
|
|
||||||
deleteFromCircuit(rowId: string, circuitId: string) {
|
|
||||||
transactionalDelete = { rowId, circuitId };
|
|
||||||
},
|
|
||||||
} as never,
|
|
||||||
circuitRepository: {
|
|
||||||
async findById() {
|
|
||||||
return {
|
|
||||||
id: "c1",
|
|
||||||
sectionId: "s1",
|
|
||||||
circuitListId: "l1",
|
|
||||||
equipmentIdentifier: "-2F1",
|
|
||||||
sortOrder: 10,
|
|
||||||
isReserve: 0,
|
|
||||||
} as never;
|
|
||||||
},
|
|
||||||
} as never,
|
|
||||||
});
|
|
||||||
|
|
||||||
await service.deleteDeviceRow("r1");
|
|
||||||
assert.deepEqual(transactionalDelete, { rowId: "r1", circuitId: "c1" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("creating device row in reserve circuit clears reserve status", async () => {
|
|
||||||
let transactionalCreate:
|
|
||||||
| {
|
|
||||||
circuitId: string;
|
|
||||||
sortOrder?: number;
|
|
||||||
name: string;
|
|
||||||
displayName: string;
|
|
||||||
}
|
|
||||||
| undefined;
|
|
||||||
const service = new CircuitWriteService({
|
|
||||||
circuitRepository: {
|
|
||||||
async findById() {
|
|
||||||
return {
|
|
||||||
id: "c1",
|
|
||||||
sectionId: "s1",
|
|
||||||
circuitListId: "l1",
|
|
||||||
equipmentIdentifier: "-2F1",
|
|
||||||
sortOrder: 10,
|
|
||||||
isReserve: 1,
|
|
||||||
} as never;
|
|
||||||
},
|
|
||||||
} as never,
|
|
||||||
deviceRowRepository: {
|
|
||||||
async findById() {
|
|
||||||
return { id: "row1" } as never;
|
|
||||||
},
|
|
||||||
} as never,
|
|
||||||
deviceRowTransactionStore: {
|
|
||||||
createInCircuit(input: typeof transactionalCreate) {
|
|
||||||
transactionalCreate = input;
|
|
||||||
return "row1";
|
|
||||||
},
|
|
||||||
} as never,
|
|
||||||
circuitListRepository: {} as never,
|
|
||||||
circuitSectionRepository: {} as never,
|
|
||||||
projectDeviceRepository: {
|
|
||||||
async findById() {
|
|
||||||
return { id: "pd1" } as never;
|
|
||||||
},
|
|
||||||
} as never,
|
|
||||||
});
|
|
||||||
|
|
||||||
await service.createDeviceRow("c1", {
|
|
||||||
name: "Load",
|
|
||||||
displayName: "Load",
|
|
||||||
quantity: 1,
|
|
||||||
powerPerUnit: 1,
|
|
||||||
simultaneityFactor: 1,
|
|
||||||
});
|
|
||||||
assert.deepEqual(transactionalCreate, {
|
|
||||||
circuitId: "c1",
|
|
||||||
linkedProjectDeviceId: undefined,
|
|
||||||
sortOrder: undefined,
|
|
||||||
name: "Load",
|
|
||||||
displayName: "Load",
|
|
||||||
phaseType: undefined,
|
|
||||||
connectionKind: undefined,
|
|
||||||
costGroup: undefined,
|
|
||||||
category: undefined,
|
|
||||||
level: undefined,
|
|
||||||
roomId: undefined,
|
|
||||||
roomNumberSnapshot: undefined,
|
|
||||||
roomNameSnapshot: undefined,
|
|
||||||
quantity: 1,
|
|
||||||
powerPerUnit: 1,
|
|
||||||
simultaneityFactor: 1,
|
|
||||||
cosPhi: undefined,
|
|
||||||
remark: undefined,
|
|
||||||
overriddenFields: undefined,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("creates a circuit and all first device rows through one repository command", async () => {
|
|
||||||
let transactionalCreate: {
|
|
||||||
circuit: { circuitListId: string; sectionId: string; equipmentIdentifier: string };
|
|
||||||
deviceRows: Array<{ linkedProjectDeviceId?: string; name: string }>;
|
|
||||||
} | undefined;
|
|
||||||
const service = new CircuitWriteService({
|
|
||||||
circuitListRepository: {
|
|
||||||
async findById() {
|
|
||||||
return { id: "l1", projectId: "p1" } as never;
|
|
||||||
},
|
|
||||||
} as never,
|
|
||||||
circuitSectionRepository: {
|
|
||||||
async findById() {
|
|
||||||
return { id: "s1", circuitListId: "l1" } as never;
|
|
||||||
},
|
|
||||||
} as never,
|
|
||||||
circuitRepository: {
|
|
||||||
async existsByEquipmentIdentifier() {
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
async findById() {
|
|
||||||
return { id: "c-new", circuitListId: "l1", sectionId: "s1" } as never;
|
|
||||||
},
|
|
||||||
} as never,
|
|
||||||
projectDeviceRepository: {
|
|
||||||
async findById(projectId: string, deviceId: string) {
|
|
||||||
return projectId === "p1" && deviceId === "pd1" ? ({ id: "pd1" } as never) : null;
|
|
||||||
},
|
|
||||||
} as never,
|
|
||||||
deviceRowRepository: {
|
|
||||||
async findById(rowId: string) {
|
|
||||||
return { id: rowId, circuitId: "c-new" } as never;
|
|
||||||
},
|
|
||||||
} as never,
|
|
||||||
deviceRowTransactionStore: {
|
|
||||||
createCircuitWithDeviceRows(input: typeof transactionalCreate) {
|
|
||||||
transactionalCreate = input;
|
|
||||||
return { circuitId: "c-new", rowIds: ["r1", "r2"] };
|
|
||||||
},
|
|
||||||
} as never,
|
|
||||||
});
|
|
||||||
|
|
||||||
const created = await service.createCircuitWithDeviceRows("p1", "l1", {
|
|
||||||
circuit: {
|
|
||||||
sectionId: "s1",
|
|
||||||
equipmentIdentifier: "-2F1",
|
|
||||||
displayName: "Steckdosen",
|
|
||||||
sortOrder: 10,
|
|
||||||
},
|
|
||||||
deviceRows: [
|
|
||||||
{
|
|
||||||
linkedProjectDeviceId: "pd1",
|
|
||||||
name: "Socket",
|
|
||||||
displayName: "Steckdose",
|
|
||||||
quantity: 1,
|
|
||||||
powerPerUnit: 0.2,
|
|
||||||
simultaneityFactor: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Manual",
|
|
||||||
displayName: "Manuell",
|
|
||||||
quantity: 2,
|
|
||||||
powerPerUnit: 0.1,
|
|
||||||
simultaneityFactor: 0.8,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
assert.equal(transactionalCreate?.circuit.circuitListId, "l1");
|
|
||||||
assert.equal(transactionalCreate?.circuit.sectionId, "s1");
|
|
||||||
assert.equal(transactionalCreate?.circuit.equipmentIdentifier, "-2F1");
|
|
||||||
assert.deepEqual(
|
|
||||||
transactionalCreate?.deviceRows.map((row) => row.name),
|
|
||||||
["Socket", "Manual"]
|
|
||||||
);
|
|
||||||
assert.deepEqual(
|
|
||||||
created.deviceRows.map((row) => row?.id),
|
|
||||||
["r1", "r2"]
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renumber uses safe bulk identifier update for swapped identifiers", async () => {
|
it("renumber uses safe bulk identifier update for swapped identifiers", async () => {
|
||||||
let safeUpdatePayload: Array<{ id: string; equipmentIdentifier: string }> = [];
|
let safeUpdatePayload: Array<{ id: string; equipmentIdentifier: string }> = [];
|
||||||
const service = new CircuitWriteService({
|
const service = new CircuitWriteService({
|
||||||
|
|||||||
Reference in New Issue
Block a user