import { WRITE_REQUEST_HEADER_NAME, WRITE_REQUEST_HEADER_VALUE, } from "../../shared/constants/request-security"; import type { BuildingDto, CircuitListDto, CreateBuildingInput, CreateFloorInput, CreateProjectDeviceInput, CreateRoomInput, CreateGlobalDeviceInput, DistributionBoardDto, DistributionBoardCommandResultDto, DistributionBoardCopyCommandResultDto, DistributionBoardDeleteCommandResultDto, DistributionBoardInfoDto, DocumentLogoMetadataDto, FloorDto, OfficeProfileDto, GlobalDeviceDto, ProjectCommandDto, ProjectCommandResultDto, ProjectDeviceCommandResultDto, ProjectDeviceDto, ProjectBuildingCommandResultDto, ProjectFloorCommandResultDto, ProjectHistoryStateDto, ProjectRevisionPageDto, ProjectSnapshotMetadataDto, ProjectSnapshotRestoreResultDto, ProjectSettingsCommandResultDto, ProjectRoomCommandResultDto, ProjectDeviceSyncCommandResultDto, ProjectDeviceSyncPreviewDto, ExternalCircuitListObjectsDto, ProjectDto, RoomDto, CircuitTreeResponseDto, ProjectCircuitTreesResponseDto, ExternalModelConfigurationSnapshotDto, ExternalInitialImportPlanDto, ExternalInitialImportResultDto, ExternalInitialImportStatusDto, ApplyExternalInitialImportInput, CreateEstimateBalanceInput, EstimateBalanceCommandResultDto, EstimateBalanceDto, EstimateBalanceUpdateResultDto, EstimateComparisonDto, } from "../types"; import type { ExternalModelConfiguration } from "../../external-model/configuration/external-model-configuration"; 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"; /** * Error of a failed API request. The message stays the server's German * error text; status and optional error code allow callers to react to * specific failures such as a revision conflict (409). */ export class ApiRequestError extends Error { readonly status: number; readonly code: string | null; constructor(message: string, status: number, code: string | null = null) { super(message); this.name = "ApiRequestError"; this.status = status; this.code = code; } } export function isRevisionConflictError(error: unknown) { return ( error instanceof ApiRequestError && (error.status === 409 || error.code === "PROJECT_REVISION_CONFLICT") ); } async function request(url: string, init?: RequestInit): Promise { const response = await fetch(url, { ...init, headers: { "Content-Type": "application/json", [WRITE_REQUEST_HEADER_NAME]: WRITE_REQUEST_HEADER_VALUE, ...init?.headers, }, cache: "no-store", }); if (!response.ok) { let details = await response.text(); let code: string | null = null; try { const parsed = JSON.parse(details) as { error?: unknown; code?: unknown }; if (typeof parsed.error === "string") { details = parsed.error; } if (typeof parsed.code === "string") { code = parsed.code; } } catch { // Keep non-JSON error responses unchanged. } throw new ApiRequestError( details || `Anfrage fehlgeschlagen (Status ${response.status})`, response.status, code ); } if (response.status === 204) { return undefined as T; } return response.json() as Promise; } export function listProjects() { return request("/api/projects"); } export function getProject(projectId: string) { return request(`/api/projects/${projectId}`); } export function getExternalModelConfiguration(projectId: string) { return request<{ configuration: ExternalModelConfigurationSnapshotDto | null }>( `/api/projects/${projectId}/external-model/configuration` ); } export function updateExternalModelConfiguration( projectId: string, expectedRevision: number, configuration: ExternalModelConfiguration ) { return request<{ configuration: ExternalModelConfigurationSnapshotDto; history: ProjectHistoryStateDto; }>(`/api/projects/${projectId}/external-model/configuration`, { method: "PUT", body: JSON.stringify({ expectedRevision, configuration }), }); } export async function detectExternalModelWorkbook( projectId: string, file: File, configuration: ExternalModelConfiguration ) { const bytes = new Uint8Array(await file.arrayBuffer()); let binary = ""; for (const byte of bytes) binary += String.fromCharCode(byte); return request<{ sheetName: string; columnCount: number; objectCount: number }>( `/api/projects/${projectId}/external-model/configuration/detect`, { method: "POST", body: JSON.stringify({ fileName: file.name, contentBase64: btoa(binary), configuration }) } ); } export function getExternalInitialImportStatus(projectId: string) { return request(`/api/projects/${projectId}/external-model/import-status`); } export async function planExternalInitialImport( projectId: string, file: File, configuration: ExternalModelConfiguration, expectedRevision: number ) { return request(`/api/projects/${projectId}/external-model/initial-import/plan`, { method: "POST", body: JSON.stringify({ fileName: file.name, contentBase64: await fileToBase64(file), configuration, expectedRevision }), }); } export async function applyExternalInitialImport( projectId: string, file: File, input: Omit ) { return request(`/api/projects/${projectId}/external-model/initial-import/apply`, { method: "POST", body: JSON.stringify({ ...input, fileName: file.name, contentBase64: await fileToBase64(file) }), }); } async function fileToBase64(file: File) { const bytes = new Uint8Array(await file.arrayBuffer()); let binary = ""; for (const byte of bytes) binary += String.fromCharCode(byte); return btoa(binary); } export function getProjectHistory(projectId: string) { return request( `/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( `/api/projects/${projectId}/history/revisions${suffix}` ); } export function listProjectSnapshots(projectId: string) { return request( `/api/projects/${projectId}/snapshots` ); } export function createNamedProjectSnapshot( projectId: string, expectedRevision: number, name: string, description?: string ) { return request( `/api/projects/${projectId}/snapshots`, { method: "POST", body: JSON.stringify({ expectedRevision, name, ...(description ? { description } : {}), }), } ); } export function restoreProjectSnapshot( projectId: string, snapshotId: string, expectedRevision: number ) { return request( `/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( `/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( `/api/projects/${projectId}/history/${direction}`, { method: "POST", body: JSON.stringify({ expectedRevision }), } ); } export function createProject(name: string) { return request("/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"]; } & Partial> ) { return request( `/api/projects/${projectId}`, { method: "PUT", body: JSON.stringify({ expectedRevision, ...input }), } ); } export function exportProjectTransfer(projectId: string) { return request(`/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(`/api/projects/${projectId}/distribution-boards`); } export function createDistributionBoard( projectId: string, input: { name: string; floorId: string | null; supplyType: NonNullable; }, expectedRevision: number ) { return request( `/api/projects/${projectId}/distribution-boards`, { method: "POST", body: JSON.stringify({ ...input, expectedRevision }), } ); } export type UpdateDistributionBoardInput = { floorId: string | null; supplyType: NonNullable; simultaneityFactor: number; } & Partial & Partial< Pick< DistributionBoardDto, "feedingDistributionBoardId" | "feedingCircuitId" > >; export function updateDistributionBoard( projectId: string, distributionBoardId: string, input: UpdateDistributionBoardInput, expectedRevision: number ) { return request( `/api/projects/${projectId}/distribution-boards/${distributionBoardId}`, { method: "PUT", body: JSON.stringify({ ...input, expectedRevision }), } ); } export function createFeederCircuitAndUpdateDistributionBoard( projectId: string, distributionBoardId: string, input: UpdateDistributionBoardInput & { feedingDistributionBoardId: string; feedingCircuitId: string; }, circuit: CircuitSnapshot, expectedRevision: number ) { return executeProjectCommand( projectId, expectedRevision, { schemaVersion: 1, type: "distribution-board.create-feeder-circuit", payload: { circuit, boardUpdate: { schemaVersion: 2, type: "distribution-board.update", payload: { distributionBoardId, changes: Object.entries(input).map(([field, value]) => ({ field, value, })), }, }, }, }, "Einspeiseabgang anlegen und Verteilung verknüpfen" ); } export const documentLogoUrl = "/api/document-branding/logo"; export function getDocumentBranding() { return request<{ logo: DocumentLogoMetadataDto | null; office: OfficeProfileDto; }>("/api/document-branding"); } export function updateOfficeProfile(office: OfficeProfileDto) { return request<{ office: OfficeProfileDto }>("/api/document-branding/office", { method: "PUT", body: JSON.stringify(office), }); } export async function uploadDocumentLogo(file: File) { const bytes = new Uint8Array(await file.arrayBuffer()); let binary = ""; for (let index = 0; index < bytes.length; index += 0x8000) { binary += String.fromCharCode(...bytes.subarray(index, index + 0x8000)); } return request<{ logo: DocumentLogoMetadataDto }>(documentLogoUrl, { method: "PUT", body: JSON.stringify({ fileName: file.name, mimeType: file.type, contentBase64: btoa(binary), }), }); } export function deleteDocumentLogo() { return request(documentLogoUrl, { method: "DELETE" }); } export function getExternalCircuitListObjects( projectId: string, circuitListId: string ) { return request( `/api/projects/${projectId}/circuit-lists/${circuitListId}/external-objects` ); } export function copyDistributionBoard( projectId: string, distributionBoardId: string, name: string, expectedRevision: number ) { return request( `/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( `/api/projects/${projectId}/distribution-boards/${distributionBoardId}`, { method: "DELETE", body: JSON.stringify({ expectedRevision }), } ); } export function listCircuitLists(projectId: string) { return request(`/api/projects/${projectId}/circuit-lists`); } export function getCircuitTree(projectId: string, circuitListId: string) { return request(`/api/projects/${projectId}/circuit-lists/${circuitListId}/tree`); } export function getProjectCircuitTrees(projectId: string) { return request( `/api/projects/${projectId}/circuit-trees` ); } 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 listBuildings(projectId: string) { return request(`/api/projects/${projectId}/buildings`); } export function createBuilding( projectId: string, input: CreateBuildingInput, expectedRevision: number ) { return request( `/api/projects/${projectId}/buildings`, { method: "POST", body: JSON.stringify({ name: input.name, expectedRevision }), } ); } export function updateBuilding( projectId: string, buildingId: string, input: CreateBuildingInput, expectedRevision: number ) { return request( `/api/projects/${projectId}/buildings/${buildingId}`, { method: "PUT", body: JSON.stringify({ name: input.name, expectedRevision }), } ); } export function deleteBuilding( projectId: string, buildingId: string, expectedRevision: number ) { return request( `/api/projects/${projectId}/buildings/${buildingId}`, { method: "DELETE", body: JSON.stringify({ expectedRevision }), } ); } // buildingId is only sent when the caller passes it (string or null); // an omitted buildingId keeps the current assignment on update. function toFloorRequestBody(input: CreateFloorInput, expectedRevision: number) { return input.buildingId === undefined ? { name: input.name, expectedRevision } : { name: input.name, buildingId: input.buildingId, expectedRevision }; } export function listFloors(projectId: string) { return request(`/api/projects/${projectId}/floors`); } export function createFloor( projectId: string, input: CreateFloorInput, expectedRevision: number ) { return request( `/api/projects/${projectId}/floors`, { method: "POST", body: JSON.stringify(toFloorRequestBody(input, expectedRevision)), } ); } export function updateFloor( projectId: string, floorId: string, input: CreateFloorInput, expectedRevision: number ) { return request(`/api/projects/${projectId}/floors/${floorId}`, { method: "PUT", body: JSON.stringify(toFloorRequestBody(input, expectedRevision)), }); } export function deleteFloor(projectId: string, floorId: string, expectedRevision: number) { return request(`/api/projects/${projectId}/floors/${floorId}`, { method: "DELETE", body: JSON.stringify({ expectedRevision }), }); } export function listRooms(projectId: string) { return request(`/api/projects/${projectId}/rooms`); } export function createRoom( projectId: string, input: CreateRoomInput, expectedRevision: number ) { return request( `/api/projects/${projectId}/rooms`, { method: "POST", body: JSON.stringify({ ...input, expectedRevision }), } ); } export function updateRoom( projectId: string, roomId: string, input: CreateRoomInput, expectedRevision: number ) { return request(`/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(`/api/projects/${projectId}/rooms/${roomId}`, { method: "DELETE", body: JSON.stringify({ expectedRevision }), }); } export function listGlobalDevices() { return request("/api/global-devices"); } export function createGlobalDevice(input: CreateGlobalDeviceInput) { return request("/api/global-devices", { method: "POST", body: JSON.stringify(input), }); } export function updateGlobalDevice(globalDeviceId: string, input: CreateGlobalDeviceInput) { return request(`/api/global-devices/${globalDeviceId}`, { method: "PUT", body: JSON.stringify(input), }); } export function deleteGlobalDevice(globalDeviceId: string) { return request(`/api/global-devices/${globalDeviceId}`, { method: "DELETE" }); } export function copyProjectDeviceToGlobal(projectId: string, projectDeviceId: string) { return request(`/api/global-devices/import-project/${projectId}/${projectDeviceId}`, { method: "POST", }); } export function listProjectDevices(projectId: string) { return request(`/api/project-devices/projects/${projectId}`); } export function createProjectDevice( projectId: string, input: CreateProjectDeviceInput, expectedRevision: number ) { return request(`/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( `/api/project-devices/projects/${projectId}/${projectDeviceId}`, { method: "PUT", body: JSON.stringify({ ...input, expectedRevision }), } ); } export function deleteProjectDevice( projectId: string, projectDeviceId: string, expectedRevision: number ) { return request( `/api/project-devices/projects/${projectId}/${projectDeviceId}`, { method: "DELETE", body: JSON.stringify({ expectedRevision }), } ); } export function copyGlobalDeviceToProject( projectId: string, globalDeviceId: string, expectedRevision: number ) { return request( `/api/project-devices/projects/${projectId}/import-global/${globalDeviceId}`, { method: "POST", body: JSON.stringify({ expectedRevision }), } ); } export function getProjectDeviceSyncPreview(projectId: string, projectDeviceId: string) { return request( `/api/project-devices/projects/${projectId}/${projectDeviceId}/links` ); } export function synchronizeProjectDeviceRows( projectId: string, projectDeviceId: string, rowIds: string[], fields: ProjectDeviceSyncField[], expectedRevision: number ) { return request( `/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( `/api/project-devices/projects/${projectId}/${projectDeviceId}/disconnect`, { method: "POST", body: JSON.stringify({ rowIds, expectedRevision }), } ); } // ── Grobe Leistungsbilanz (LPH 2/3) ───────────────────────────────────── function estimateBalancesUrl(projectId: string, balanceId?: string) { const base = `/api/projects/${projectId}/estimate-balances`; return balanceId ? `${base}/${balanceId}` : base; } export function listEstimateBalances(projectId: string) { return request(estimateBalancesUrl(projectId)); } export function getEstimateBalance(projectId: string, balanceId: string) { return request(estimateBalancesUrl(projectId, balanceId)); } export function createEstimateBalance( projectId: string, input: CreateEstimateBalanceInput, expectedRevision: number ) { return request(estimateBalancesUrl(projectId), { method: "POST", body: JSON.stringify({ ...input, expectedRevision }), }); } /** * Saves the complete target document. `keepalive` lets the request outlive * the page, e.g. for the last autosave while the tab is being closed. */ export function updateEstimateBalance( projectId: string, balance: EstimateBalanceDto, expectedRevision: number, options: { keepalive?: boolean } = {} ) { return request( estimateBalancesUrl(projectId, balance.id), { method: "PUT", body: JSON.stringify({ expectedRevision, balance }), ...(options.keepalive ? { keepalive: true } : {}), } ); } export function deleteEstimateBalance( projectId: string, balanceId: string, expectedRevision: number ) { return request(estimateBalancesUrl(projectId, balanceId), { method: "DELETE", body: JSON.stringify({ expectedRevision }), }); } export function getEstimateComparison(projectId: string, balanceId: string) { return request( `${estimateBalancesUrl(projectId, balanceId)}/comparison` ); }