Full-codebase review turned up five real correctness/security bugs and
a dozen smaller inconsistencies; all are fixed here with matching test
coverage:
- BMK uniqueness silently allowed German-umlaut duplicates ("Ä1" vs
"ä1") because the DB's normalized index only folds ASCII case. Added
a shared Unicode-aware pre-check used by every circuit/component
insert and rename path (one of which had no pre-check at all).
- CircuitDeviceRow.simultaneityFactor had no upper bound at the row
level (command model and snapshot/restore schema), unlike every
sibling entity, letting a bad value silently corrupt power totals.
- Grid cell editing silently misread German thousands-separator input
("1.500" parsed as 1.5); "." is now rejected outright with a clear
message instead of guessing.
- The editor's shared command runner (runCommand/applyHistory) had no
re-entrancy guard, so a double click/drop could fire the same
command twice and race a BMK collision or revision conflict. Added a
synchronous ref guard plus isSaving on the buttons that lacked it.
- GET .../next-identifier leaked circuit-numbering state for sections
in other projects (no ownership check, 400 instead of 404). Moved
under /projects/:projectId and scoped it.
Also: added the missing circuits.section_id / circuit_device_rows.
circuit_id indexes (migration 0006), gave FormModal a focus trap /
Escape-to-close / focus restore and rebuilt ProjectSettingsModal on
top of it instead of duplicated markup, removed dead code (3 orphaned
domain model files, an unused persistence helper, a wrapper only used
by its own test), pointed the project page at GET /projects/:id
instead of listing+filtering client-side, closed the gap between the
documented 18 MB CSV limit and the ~17.17 MiB actually enforced, added
missing upper bounds on several free-text fields, filled in nine
missing German labels in the revision timeline, replaced a
key-order-fragile JSON.stringify equality check with a real field
comparison, made an implicit sort-order assumption in three
renumbering helpers explicit, cleared the sidebar's target selection
when it no longer resolves after a tree reload, and fixed
updateGlobalDevice to check-then-write instead of write-then-check.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1040 lines
25 KiB
TypeScript
1040 lines
25 KiB
TypeScript
import type {
|
|
CircuitListDto,
|
|
CreateFloorInput,
|
|
CreateProjectDeviceInput,
|
|
CreateRoomInput,
|
|
CreateGlobalDeviceInput,
|
|
DistributionBoardDto,
|
|
DistributionBoardCommandResultDto,
|
|
DistributionBoardCopyCommandResultDto,
|
|
DistributionBoardDeleteCommandResultDto,
|
|
FloorDto,
|
|
GlobalDeviceDto,
|
|
ProjectCommandDto,
|
|
ProjectCommandResultDto,
|
|
ProjectDeviceCommandResultDto,
|
|
ProjectDeviceDto,
|
|
ProjectFloorCommandResultDto,
|
|
ProjectHistoryStateDto,
|
|
ProjectRevisionPageDto,
|
|
ProjectSnapshotMetadataDto,
|
|
ProjectSnapshotRestoreResultDto,
|
|
ProjectSettingsCommandResultDto,
|
|
ProjectRoomCommandResultDto,
|
|
ProjectDeviceSyncCommandResultDto,
|
|
ProjectDeviceSyncPreviewDto,
|
|
ExternalCircuitListObjectsDto,
|
|
ProjectDto,
|
|
RoomDto,
|
|
CircuitTreeResponseDto,
|
|
ExternalCsvConfigurationSnapshotDto,
|
|
ExternalCsvPreviewDto,
|
|
ExternalInitialImportPlanDto,
|
|
ApplyExternalInitialImportInput,
|
|
ExternalInitialImportResultDto,
|
|
} from "../types";
|
|
import type { ExternalCsvConfiguration } from "../../external-model/csv/external-csv-contracts";
|
|
import type { ProjectDeviceSyncField } from "../../shared/constants/project-device-sync-fields";
|
|
import {
|
|
createCircuitUpdateProjectCommand,
|
|
type CircuitUpdatePatch,
|
|
} from "../../domain/models/circuit-project-command.model";
|
|
import {
|
|
createCircuitDeviceRowUpdateProjectCommand,
|
|
type CircuitDeviceRowUpdatePatch,
|
|
} from "../../domain/models/circuit-device-row-project-command.model";
|
|
import type {
|
|
CircuitDeviceRowSnapshot,
|
|
} from "../../domain/models/circuit-device-row-structure-project-command.model";
|
|
import type {
|
|
CircuitSnapshot,
|
|
} from "../../domain/models/circuit-structure-project-command.model";
|
|
import type {
|
|
CircuitDeviceRowMoveAssignment,
|
|
} from "../../domain/models/circuit-device-row-move-project-command.model";
|
|
import type {
|
|
CircuitSectionReorderAssignment,
|
|
} from "../../domain/models/circuit-section-reorder-project-command.model";
|
|
import type {
|
|
CircuitSectionsReorderSection,
|
|
} from "../../domain/models/circuit-sections-reorder-project-command.model";
|
|
import type {
|
|
CircuitSectionRenumberAssignment,
|
|
} from "../../domain/models/circuit-section-renumber-project-command.model";
|
|
import type {
|
|
DistributionBoardComponentSnapshot,
|
|
} from "../../domain/models/distribution-board-component-structure-project-command.model";
|
|
import type {
|
|
CircuitGroupSnapshot,
|
|
CircuitGroupReorderAssignment,
|
|
} from "../../domain/models/circuit-group-structure-project-command.model";
|
|
import type {
|
|
CircuitProtectionSnapshot,
|
|
} from "../../domain/models/circuit-protection-project-command.model";
|
|
import type {
|
|
CircuitGroupRenumberPlan,
|
|
} from "../../domain/services/circuit-group-renumbering";
|
|
import type {
|
|
CircuitGroupMovePlan,
|
|
} from "../../domain/services/circuit-group-move-planning";
|
|
import type {
|
|
CircuitGroupSubtreeSnapshot,
|
|
} from "../../domain/models/circuit-group-subtree-snapshot.model";
|
|
|
|
async function request<T>(url: string, init?: RequestInit): Promise<T> {
|
|
const response = await fetch(url, {
|
|
...init,
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
...init?.headers,
|
|
},
|
|
cache: "no-store",
|
|
});
|
|
|
|
if (!response.ok) {
|
|
let details = await response.text();
|
|
try {
|
|
const parsed = JSON.parse(details) as { error?: unknown };
|
|
if (typeof parsed.error === "string") {
|
|
details = parsed.error;
|
|
}
|
|
} catch {
|
|
// Keep non-JSON error responses unchanged.
|
|
}
|
|
throw new Error(details || `Anfrage fehlgeschlagen (Status ${response.status})`);
|
|
}
|
|
|
|
if (response.status === 204) {
|
|
return undefined as T;
|
|
}
|
|
|
|
return response.json() as Promise<T>;
|
|
}
|
|
|
|
export function listProjects() {
|
|
return request<ProjectDto[]>("/api/projects");
|
|
}
|
|
|
|
export function getProject(projectId: string) {
|
|
return request<ProjectDto>(`/api/projects/${projectId}`);
|
|
}
|
|
|
|
export function getProjectHistory(projectId: string) {
|
|
return request<ProjectHistoryStateDto>(
|
|
`/api/projects/${projectId}/history`
|
|
);
|
|
}
|
|
|
|
export function listProjectRevisions(
|
|
projectId: string,
|
|
input: { limit?: number; beforeRevision?: number } = {}
|
|
) {
|
|
const query = new URLSearchParams();
|
|
if (input.limit !== undefined) {
|
|
query.set("limit", String(input.limit));
|
|
}
|
|
if (input.beforeRevision !== undefined) {
|
|
query.set("beforeRevision", String(input.beforeRevision));
|
|
}
|
|
const suffix = query.size ? `?${query.toString()}` : "";
|
|
return request<ProjectRevisionPageDto>(
|
|
`/api/projects/${projectId}/history/revisions${suffix}`
|
|
);
|
|
}
|
|
|
|
export function listProjectSnapshots(projectId: string) {
|
|
return request<ProjectSnapshotMetadataDto[]>(
|
|
`/api/projects/${projectId}/snapshots`
|
|
);
|
|
}
|
|
|
|
export function createNamedProjectSnapshot(
|
|
projectId: string,
|
|
expectedRevision: number,
|
|
name: string,
|
|
description?: string
|
|
) {
|
|
return request<ProjectSnapshotMetadataDto>(
|
|
`/api/projects/${projectId}/snapshots`,
|
|
{
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
expectedRevision,
|
|
name,
|
|
...(description ? { description } : {}),
|
|
}),
|
|
}
|
|
);
|
|
}
|
|
|
|
export function restoreProjectSnapshot(
|
|
projectId: string,
|
|
snapshotId: string,
|
|
expectedRevision: number
|
|
) {
|
|
return request<ProjectSnapshotRestoreResultDto>(
|
|
`/api/projects/${projectId}/snapshots/${snapshotId}/restore`,
|
|
{
|
|
method: "POST",
|
|
body: JSON.stringify({ expectedRevision }),
|
|
}
|
|
);
|
|
}
|
|
|
|
export function executeProjectCommand(
|
|
projectId: string,
|
|
expectedRevision: number,
|
|
command: ProjectCommandDto,
|
|
description?: string
|
|
) {
|
|
return request<ProjectCommandResultDto>(
|
|
`/api/projects/${projectId}/commands`,
|
|
{
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
expectedRevision,
|
|
command,
|
|
...(description ? { description } : {}),
|
|
}),
|
|
}
|
|
);
|
|
}
|
|
|
|
export function undoProjectCommand(
|
|
projectId: string,
|
|
expectedRevision: number
|
|
) {
|
|
return executeProjectHistoryCommand(
|
|
projectId,
|
|
expectedRevision,
|
|
"undo"
|
|
);
|
|
}
|
|
|
|
export function redoProjectCommand(
|
|
projectId: string,
|
|
expectedRevision: number
|
|
) {
|
|
return executeProjectHistoryCommand(
|
|
projectId,
|
|
expectedRevision,
|
|
"redo"
|
|
);
|
|
}
|
|
|
|
function executeProjectHistoryCommand(
|
|
projectId: string,
|
|
expectedRevision: number,
|
|
direction: "undo" | "redo"
|
|
) {
|
|
return request<ProjectCommandResultDto>(
|
|
`/api/projects/${projectId}/history/${direction}`,
|
|
{
|
|
method: "POST",
|
|
body: JSON.stringify({ expectedRevision }),
|
|
}
|
|
);
|
|
}
|
|
|
|
export function createProject(name: string) {
|
|
return request<ProjectDto>("/api/projects", {
|
|
method: "POST",
|
|
body: JSON.stringify({ name }),
|
|
});
|
|
}
|
|
|
|
export function updateProjectSettings(
|
|
projectId: string,
|
|
expectedRevision: number,
|
|
input: {
|
|
name: string;
|
|
internalProjectNumber: string | null;
|
|
externalProjectNumber: string | null;
|
|
buildingOwner: string | null;
|
|
description: string | null;
|
|
isPublicBuilding: boolean;
|
|
singlePhaseVoltageV: number;
|
|
threePhaseVoltageV: number;
|
|
enabledDistributionBoardSupplyTypes: ProjectDto["enabledDistributionBoardSupplyTypes"];
|
|
}
|
|
) {
|
|
return request<ProjectSettingsCommandResultDto>(
|
|
`/api/projects/${projectId}`,
|
|
{
|
|
method: "PUT",
|
|
body: JSON.stringify({ expectedRevision, ...input }),
|
|
}
|
|
);
|
|
}
|
|
|
|
export function exportProjectTransfer(projectId: string) {
|
|
return request<unknown>(`/api/projects/${projectId}/export`);
|
|
}
|
|
|
|
export function importProjectTransferAsNew(transfer: unknown) {
|
|
return request<{ projectId: string; name: string }>(
|
|
"/api/projects/import",
|
|
{
|
|
method: "POST",
|
|
body: JSON.stringify({ transfer }),
|
|
}
|
|
);
|
|
}
|
|
|
|
export function importProjectTransfer(
|
|
projectId: string,
|
|
mode: "replace" | "duplicate",
|
|
expectedRevision: number,
|
|
transfer: unknown
|
|
) {
|
|
return request<
|
|
| { projectId: string; name: string }
|
|
| ProjectSettingsCommandResultDto
|
|
>(`/api/projects/${projectId}/import`, {
|
|
method: "POST",
|
|
body: JSON.stringify(
|
|
mode === "replace"
|
|
? { mode, expectedRevision, transfer }
|
|
: { mode, transfer }
|
|
),
|
|
});
|
|
}
|
|
|
|
export function listDistributionBoards(projectId: string) {
|
|
return request<DistributionBoardDto[]>(`/api/projects/${projectId}/distribution-boards`);
|
|
}
|
|
|
|
export function createDistributionBoard(
|
|
projectId: string,
|
|
input: {
|
|
name: string;
|
|
floorId: string | null;
|
|
supplyType: NonNullable<DistributionBoardDto["supplyType"]>;
|
|
},
|
|
expectedRevision: number
|
|
) {
|
|
return request<DistributionBoardCommandResultDto>(
|
|
`/api/projects/${projectId}/distribution-boards`,
|
|
{
|
|
method: "POST",
|
|
body: JSON.stringify({ ...input, expectedRevision }),
|
|
}
|
|
);
|
|
}
|
|
|
|
export function updateDistributionBoard(
|
|
projectId: string,
|
|
distributionBoardId: string,
|
|
input: {
|
|
floorId: string | null;
|
|
supplyType: NonNullable<DistributionBoardDto["supplyType"]>;
|
|
simultaneityFactor: number;
|
|
},
|
|
expectedRevision: number
|
|
) {
|
|
return request<DistributionBoardCommandResultDto>(
|
|
`/api/projects/${projectId}/distribution-boards/${distributionBoardId}`,
|
|
{
|
|
method: "PUT",
|
|
body: JSON.stringify({ ...input, expectedRevision }),
|
|
}
|
|
);
|
|
}
|
|
|
|
export function getExternalCsvConfiguration(projectId: string) {
|
|
return request<{ configuration: ExternalCsvConfigurationSnapshotDto | null }>(
|
|
`/api/projects/${projectId}/external-csv/configuration`
|
|
);
|
|
}
|
|
|
|
export function getExternalCircuitListObjects(
|
|
projectId: string,
|
|
circuitListId: string
|
|
) {
|
|
return request<ExternalCircuitListObjectsDto>(
|
|
`/api/projects/${projectId}/circuit-lists/${circuitListId}/external-objects`
|
|
);
|
|
}
|
|
|
|
export function updateExternalCsvConfiguration(
|
|
projectId: string,
|
|
expectedRevision: number,
|
|
configuration: ExternalCsvConfiguration
|
|
) {
|
|
return request<
|
|
ProjectCommandResultDto & { configuration: ExternalCsvConfigurationSnapshotDto }
|
|
>(`/api/projects/${projectId}/external-csv/configuration`, {
|
|
method: "PUT",
|
|
body: JSON.stringify({ expectedRevision, configuration }),
|
|
});
|
|
}
|
|
|
|
export function previewExternalCsv(
|
|
projectId: string,
|
|
fileName: string,
|
|
contentBase64: string
|
|
) {
|
|
return request<ExternalCsvPreviewDto>(
|
|
`/api/projects/${projectId}/external-csv/preview`,
|
|
{
|
|
method: "POST",
|
|
body: JSON.stringify({ fileName, contentBase64 }),
|
|
}
|
|
);
|
|
}
|
|
|
|
export function planExternalInitialImport(
|
|
projectId: string,
|
|
fileName: string,
|
|
contentBase64: string
|
|
) {
|
|
return request<ExternalInitialImportPlanDto>(
|
|
`/api/projects/${projectId}/external-csv/initial-import/plan`,
|
|
{
|
|
method: "POST",
|
|
body: JSON.stringify({ fileName, contentBase64 }),
|
|
}
|
|
);
|
|
}
|
|
|
|
export function applyExternalInitialImport(
|
|
projectId: string,
|
|
input: ApplyExternalInitialImportInput
|
|
) {
|
|
return request<ExternalInitialImportResultDto>(
|
|
`/api/projects/${projectId}/external-csv/initial-import/apply`,
|
|
{ method: "POST", body: JSON.stringify(input) }
|
|
);
|
|
}
|
|
|
|
export function copyDistributionBoard(
|
|
projectId: string,
|
|
distributionBoardId: string,
|
|
name: string,
|
|
expectedRevision: number
|
|
) {
|
|
return request<DistributionBoardCopyCommandResultDto>(
|
|
`/api/projects/${projectId}/distribution-boards/${distributionBoardId}/copy`,
|
|
{
|
|
method: "POST",
|
|
body: JSON.stringify({ name, expectedRevision }),
|
|
}
|
|
);
|
|
}
|
|
|
|
export function deleteDistributionBoard(
|
|
projectId: string,
|
|
distributionBoardId: string,
|
|
expectedRevision: number
|
|
) {
|
|
return request<DistributionBoardDeleteCommandResultDto>(
|
|
`/api/projects/${projectId}/distribution-boards/${distributionBoardId}`,
|
|
{
|
|
method: "DELETE",
|
|
body: JSON.stringify({ expectedRevision }),
|
|
}
|
|
);
|
|
}
|
|
|
|
export function listCircuitLists(projectId: string) {
|
|
return request<CircuitListDto[]>(`/api/projects/${projectId}/circuit-lists`);
|
|
}
|
|
|
|
export function getCircuitTree(projectId: string, circuitListId: string) {
|
|
return request<CircuitTreeResponseDto>(`/api/projects/${projectId}/circuit-lists/${circuitListId}/tree`);
|
|
}
|
|
|
|
export function insertDistributionBoardComponentCommand(
|
|
projectId: string,
|
|
expectedRevision: number,
|
|
snapshot: DistributionBoardComponentSnapshot
|
|
) {
|
|
return executeProjectCommand(
|
|
projectId,
|
|
expectedRevision,
|
|
{
|
|
schemaVersion: 1,
|
|
type: "distribution-board-component.insert",
|
|
payload: { snapshot },
|
|
},
|
|
"Verteilerkomponente hinzufügen"
|
|
);
|
|
}
|
|
|
|
export function updateDistributionBoardComponentCommand(
|
|
projectId: string,
|
|
expectedRevision: number,
|
|
expected: DistributionBoardComponentSnapshot,
|
|
target: DistributionBoardComponentSnapshot
|
|
) {
|
|
return executeProjectCommand(
|
|
projectId,
|
|
expectedRevision,
|
|
{
|
|
schemaVersion: 1,
|
|
type: "distribution-board-component.update",
|
|
payload: { expected, target },
|
|
},
|
|
"Verteilerkomponente bearbeiten"
|
|
);
|
|
}
|
|
|
|
export function deleteDistributionBoardComponentCommand(
|
|
projectId: string,
|
|
expectedRevision: number,
|
|
snapshot: DistributionBoardComponentSnapshot
|
|
) {
|
|
return executeProjectCommand(
|
|
projectId,
|
|
expectedRevision,
|
|
{
|
|
schemaVersion: 1,
|
|
type: "distribution-board-component.delete",
|
|
payload: { snapshot },
|
|
},
|
|
"Verteilerkomponente entfernen"
|
|
);
|
|
}
|
|
|
|
export function insertCircuitGroupCommand(
|
|
projectId: string,
|
|
expectedRevision: number,
|
|
snapshot: CircuitGroupSnapshot
|
|
) {
|
|
return executeProjectCommand(
|
|
projectId,
|
|
expectedRevision,
|
|
{
|
|
schemaVersion: 1,
|
|
type: "circuit-group.insert",
|
|
payload: { snapshot },
|
|
},
|
|
"Stromkreisgruppe hinzufügen"
|
|
);
|
|
}
|
|
|
|
export function updateCircuitGroupCommand(
|
|
projectId: string,
|
|
expectedRevision: number,
|
|
expected: CircuitGroupSnapshot,
|
|
target: CircuitGroupSnapshot
|
|
) {
|
|
return executeProjectCommand(
|
|
projectId,
|
|
expectedRevision,
|
|
{
|
|
schemaVersion: 1,
|
|
type: "circuit-group.update",
|
|
payload: { expected, target },
|
|
},
|
|
"Stromkreisgruppe bearbeiten"
|
|
);
|
|
}
|
|
|
|
export function deleteCircuitGroupCommand(
|
|
projectId: string,
|
|
expectedRevision: number,
|
|
snapshot: CircuitGroupSnapshot
|
|
) {
|
|
return executeProjectCommand(
|
|
projectId,
|
|
expectedRevision,
|
|
{
|
|
schemaVersion: 1,
|
|
type: "circuit-group.delete",
|
|
payload: { snapshot },
|
|
},
|
|
"Leere Stromkreisgruppe entfernen"
|
|
);
|
|
}
|
|
|
|
export function deleteCircuitGroupSubtreeCommand(
|
|
projectId: string,
|
|
expectedRevision: number,
|
|
snapshot: CircuitGroupSubtreeSnapshot
|
|
) {
|
|
return executeProjectCommand(
|
|
projectId,
|
|
expectedRevision,
|
|
{
|
|
schemaVersion: 1,
|
|
type: "circuit-group.delete-subtree",
|
|
payload: { snapshot },
|
|
},
|
|
"Befüllte Stromkreisgruppe entfernen"
|
|
);
|
|
}
|
|
|
|
export function reorderCircuitGroupsCommand(
|
|
projectId: string,
|
|
expectedRevision: number,
|
|
circuitListId: string,
|
|
assignments: CircuitGroupReorderAssignment[]
|
|
) {
|
|
return executeProjectCommand(
|
|
projectId,
|
|
expectedRevision,
|
|
{
|
|
schemaVersion: 1,
|
|
type: "circuit-group.reorder",
|
|
payload: { circuitListId, assignments },
|
|
},
|
|
"Stromkreisgruppen sortieren"
|
|
);
|
|
}
|
|
|
|
export function renumberCircuitGroupsCommand(
|
|
projectId: string,
|
|
expectedRevision: number,
|
|
circuitListId: string,
|
|
plan: CircuitGroupRenumberPlan
|
|
) {
|
|
return executeProjectCommand(
|
|
projectId,
|
|
expectedRevision,
|
|
{
|
|
schemaVersion: 1,
|
|
type: "circuit-group.renumber",
|
|
payload: { circuitListId, groups: plan.groups },
|
|
},
|
|
"Stromkreisgruppen neu nummerieren"
|
|
);
|
|
}
|
|
|
|
export function moveCircuitToGroupCommand(
|
|
projectId: string,
|
|
expectedRevision: number,
|
|
plan: CircuitGroupMovePlan
|
|
) {
|
|
return executeProjectCommand(
|
|
projectId,
|
|
expectedRevision,
|
|
{
|
|
schemaVersion: 1,
|
|
type: "circuit.move-group",
|
|
payload: plan,
|
|
},
|
|
"Stromkreis in andere Gruppe verschieben"
|
|
);
|
|
}
|
|
|
|
export function updateCircuitProtectionCommand(
|
|
projectId: string,
|
|
expectedRevision: number,
|
|
circuitId: string,
|
|
expected: CircuitProtectionSnapshot | null,
|
|
target: CircuitProtectionSnapshot
|
|
) {
|
|
return executeProjectCommand(
|
|
projectId,
|
|
expectedRevision,
|
|
{
|
|
schemaVersion: 1,
|
|
type: "circuit-protection.update",
|
|
payload: { circuitId, expected, target },
|
|
},
|
|
"Stromkreisschutz bearbeiten"
|
|
);
|
|
}
|
|
|
|
export function updateCircuitById(
|
|
projectId: string,
|
|
expectedRevision: number,
|
|
circuitId: string,
|
|
input: CircuitUpdatePatch
|
|
) {
|
|
return executeProjectCommand(
|
|
projectId,
|
|
expectedRevision,
|
|
createCircuitUpdateProjectCommand(circuitId, input),
|
|
"Stromkreisfeld bearbeiten"
|
|
);
|
|
}
|
|
|
|
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 getNextCircuitIdentifier(projectId: string, sectionId: string) {
|
|
return request<{ sectionId: string; nextIdentifier: string }>(
|
|
`/api/projects/${projectId}/circuit-sections/${sectionId}/next-identifier`
|
|
);
|
|
}
|
|
|
|
export function updateCircuitDeviceRowById(
|
|
projectId: string,
|
|
expectedRevision: number,
|
|
rowId: string,
|
|
input: CircuitDeviceRowUpdatePatch
|
|
) {
|
|
return executeProjectCommand(
|
|
projectId,
|
|
expectedRevision,
|
|
createCircuitDeviceRowUpdateProjectCommand(rowId, input),
|
|
"Gerätezeilenfeld bearbeiten"
|
|
);
|
|
}
|
|
|
|
export function insertCircuitDeviceRowCommand(
|
|
projectId: string,
|
|
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 moveCircuitDeviceRowsCommand(
|
|
projectId: string,
|
|
expectedRevision: number,
|
|
moves: CircuitDeviceRowMoveAssignment[],
|
|
description: string
|
|
) {
|
|
return executeProjectCommand(
|
|
projectId,
|
|
expectedRevision,
|
|
{
|
|
schemaVersion: 1,
|
|
type: "circuit-device-row.move",
|
|
payload: { moves },
|
|
},
|
|
description
|
|
);
|
|
}
|
|
|
|
export function moveCircuitDeviceRowsToNewCircuitCommand(
|
|
projectId: string,
|
|
expectedRevision: number,
|
|
targetCircuit: CircuitSnapshot,
|
|
moves: CircuitDeviceRowMoveAssignment[],
|
|
description: string
|
|
) {
|
|
return executeProjectCommand(
|
|
projectId,
|
|
expectedRevision,
|
|
{
|
|
schemaVersion: 1,
|
|
type: "circuit-device-row.move-with-new-circuit",
|
|
payload: {
|
|
targetCircuitAction: "create",
|
|
targetCircuit,
|
|
moves,
|
|
},
|
|
},
|
|
description
|
|
);
|
|
}
|
|
|
|
export function reorderCircuitSectionCommand(
|
|
projectId: string,
|
|
expectedRevision: number,
|
|
sectionId: string,
|
|
assignments: CircuitSectionReorderAssignment[],
|
|
description: string
|
|
) {
|
|
return executeProjectCommand(
|
|
projectId,
|
|
expectedRevision,
|
|
{
|
|
schemaVersion: 1,
|
|
type: "circuit.reorder-section",
|
|
payload: { sectionId, assignments },
|
|
},
|
|
description
|
|
);
|
|
}
|
|
|
|
export function reorderCircuitSectionsCommand(
|
|
projectId: string,
|
|
expectedRevision: number,
|
|
sections: CircuitSectionsReorderSection[],
|
|
description: string
|
|
) {
|
|
return executeProjectCommand(
|
|
projectId,
|
|
expectedRevision,
|
|
{
|
|
schemaVersion: 1,
|
|
type: "circuit.reorder-sections",
|
|
payload: { sections },
|
|
},
|
|
description
|
|
);
|
|
}
|
|
|
|
export function renumberCircuitSectionCommand(
|
|
projectId: string,
|
|
expectedRevision: number,
|
|
sectionId: string,
|
|
assignments: CircuitSectionRenumberAssignment[],
|
|
description: string
|
|
) {
|
|
return executeProjectCommand(
|
|
projectId,
|
|
expectedRevision,
|
|
{
|
|
schemaVersion: 1,
|
|
type: "circuit.renumber-section",
|
|
payload: { sectionId, assignments },
|
|
},
|
|
description
|
|
);
|
|
}
|
|
|
|
export function listFloors(projectId: string) {
|
|
return request<FloorDto[]>(`/api/projects/${projectId}/floors`);
|
|
}
|
|
|
|
export function createFloor(
|
|
projectId: string,
|
|
input: CreateFloorInput,
|
|
expectedRevision: number
|
|
) {
|
|
return request<ProjectFloorCommandResultDto>(
|
|
`/api/projects/${projectId}/floors`,
|
|
{
|
|
method: "POST",
|
|
body: JSON.stringify({ ...input, expectedRevision }),
|
|
}
|
|
);
|
|
}
|
|
|
|
export function updateFloor(
|
|
projectId: string,
|
|
floorId: string,
|
|
input: CreateFloorInput,
|
|
expectedRevision: number
|
|
) {
|
|
return request<ProjectFloorCommandResultDto>(`/api/projects/${projectId}/floors/${floorId}`, {
|
|
method: "PUT",
|
|
body: JSON.stringify({ ...input, expectedRevision }),
|
|
});
|
|
}
|
|
|
|
export function deleteFloor(projectId: string, floorId: string, expectedRevision: number) {
|
|
return request<ProjectCommandResultDto>(`/api/projects/${projectId}/floors/${floorId}`, {
|
|
method: "DELETE",
|
|
body: JSON.stringify({ expectedRevision }),
|
|
});
|
|
}
|
|
|
|
export function listRooms(projectId: string) {
|
|
return request<RoomDto[]>(`/api/projects/${projectId}/rooms`);
|
|
}
|
|
|
|
export function createRoom(
|
|
projectId: string,
|
|
input: CreateRoomInput,
|
|
expectedRevision: number
|
|
) {
|
|
return request<ProjectRoomCommandResultDto>(
|
|
`/api/projects/${projectId}/rooms`,
|
|
{
|
|
method: "POST",
|
|
body: JSON.stringify({ ...input, expectedRevision }),
|
|
}
|
|
);
|
|
}
|
|
|
|
export function updateRoom(
|
|
projectId: string,
|
|
roomId: string,
|
|
input: CreateRoomInput,
|
|
expectedRevision: number
|
|
) {
|
|
return request<ProjectRoomCommandResultDto>(`/api/projects/${projectId}/rooms/${roomId}`, {
|
|
method: "PUT",
|
|
body: JSON.stringify({ ...input, floorId: input.floorId ?? null, expectedRevision }),
|
|
});
|
|
}
|
|
|
|
export function deleteRoom(projectId: string, roomId: string, expectedRevision: number) {
|
|
return request<ProjectCommandResultDto>(`/api/projects/${projectId}/rooms/${roomId}`, {
|
|
method: "DELETE",
|
|
body: JSON.stringify({ expectedRevision }),
|
|
});
|
|
}
|
|
|
|
export function listGlobalDevices() {
|
|
return request<GlobalDeviceDto[]>("/api/global-devices");
|
|
}
|
|
|
|
export function createGlobalDevice(input: CreateGlobalDeviceInput) {
|
|
return request<GlobalDeviceDto>("/api/global-devices", {
|
|
method: "POST",
|
|
body: JSON.stringify(input),
|
|
});
|
|
}
|
|
|
|
export function updateGlobalDevice(globalDeviceId: string, input: CreateGlobalDeviceInput) {
|
|
return request<GlobalDeviceDto>(`/api/global-devices/${globalDeviceId}`, {
|
|
method: "PUT",
|
|
body: JSON.stringify(input),
|
|
});
|
|
}
|
|
|
|
export function deleteGlobalDevice(globalDeviceId: string) {
|
|
return request<void>(`/api/global-devices/${globalDeviceId}`, { method: "DELETE" });
|
|
}
|
|
|
|
export function copyProjectDeviceToGlobal(projectId: string, projectDeviceId: string) {
|
|
return request<GlobalDeviceDto>(`/api/global-devices/import-project/${projectId}/${projectDeviceId}`, {
|
|
method: "POST",
|
|
});
|
|
}
|
|
|
|
export function listProjectDevices(projectId: string) {
|
|
return request<ProjectDeviceDto[]>(`/api/project-devices/projects/${projectId}`);
|
|
}
|
|
|
|
export function createProjectDevice(
|
|
projectId: string,
|
|
input: CreateProjectDeviceInput,
|
|
expectedRevision: number
|
|
) {
|
|
return request<ProjectDeviceCommandResultDto>(`/api/project-devices/projects/${projectId}`, {
|
|
method: "POST",
|
|
body: JSON.stringify({ ...input, expectedRevision }),
|
|
});
|
|
}
|
|
|
|
export function updateProjectDevice(
|
|
projectId: string,
|
|
projectDeviceId: string,
|
|
input: CreateProjectDeviceInput,
|
|
expectedRevision: number
|
|
) {
|
|
return request<ProjectDeviceCommandResultDto>(
|
|
`/api/project-devices/projects/${projectId}/${projectDeviceId}`,
|
|
{
|
|
method: "PUT",
|
|
body: JSON.stringify({ ...input, expectedRevision }),
|
|
}
|
|
);
|
|
}
|
|
|
|
export function deleteProjectDevice(
|
|
projectId: string,
|
|
projectDeviceId: string,
|
|
expectedRevision: number
|
|
) {
|
|
return request<ProjectCommandResultDto>(
|
|
`/api/project-devices/projects/${projectId}/${projectDeviceId}`,
|
|
{
|
|
method: "DELETE",
|
|
body: JSON.stringify({ expectedRevision }),
|
|
}
|
|
);
|
|
}
|
|
|
|
export function copyGlobalDeviceToProject(
|
|
projectId: string,
|
|
globalDeviceId: string,
|
|
expectedRevision: number
|
|
) {
|
|
return request<ProjectDeviceCommandResultDto>(
|
|
`/api/project-devices/projects/${projectId}/import-global/${globalDeviceId}`,
|
|
{
|
|
method: "POST",
|
|
body: JSON.stringify({ expectedRevision }),
|
|
}
|
|
);
|
|
}
|
|
|
|
export function getProjectDeviceSyncPreview(projectId: string, projectDeviceId: string) {
|
|
return request<ProjectDeviceSyncPreviewDto>(
|
|
`/api/project-devices/projects/${projectId}/${projectDeviceId}/links`
|
|
);
|
|
}
|
|
|
|
export function synchronizeProjectDeviceRows(
|
|
projectId: string,
|
|
projectDeviceId: string,
|
|
rowIds: string[],
|
|
fields: ProjectDeviceSyncField[],
|
|
expectedRevision: number
|
|
) {
|
|
return request<ProjectDeviceSyncCommandResultDto>(
|
|
`/api/project-devices/projects/${projectId}/${projectDeviceId}/synchronize`,
|
|
{
|
|
method: "POST",
|
|
body: JSON.stringify({ rowIds, fields, expectedRevision }),
|
|
}
|
|
);
|
|
}
|
|
|
|
export function disconnectProjectDeviceRows(
|
|
projectId: string,
|
|
projectDeviceId: string,
|
|
rowIds: string[],
|
|
expectedRevision: number
|
|
) {
|
|
return request<ProjectDeviceSyncCommandResultDto>(
|
|
`/api/project-devices/projects/${projectId}/${projectDeviceId}/disconnect`,
|
|
{
|
|
method: "POST",
|
|
body: JSON.stringify({ rowIds, expectedRevision }),
|
|
}
|
|
);
|
|
}
|