leistungsbilanz-ts/src/frontend/utils/api.ts
Julian Appel b45dc5002d Fix code-review findings across domain, persistence, server and frontend
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>
2026-08-06 21:31:16 +02:00

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 }),
}
);
}