In LPH 2/3 there are no circuits yet, but the office needs a power balance by DIN 276 cost group, as in the Excel template "Leistungsbilanz - KG400": each trade enters consumers by piece or by area (W/m²) with GLF and cos phi; the balance sums AV and SV per cost group, adds a reserve and derives the maximum current. The module is separate from the circuit structure: own tables (migration 0010), own folder src/estimate-balance and an import-free shared calculation used by API and UI. Writes go through project commands (estimate-balance.insert/.update/.delete with complete documents), so revision checks and Undo/Redo apply; it is not part of project snapshots or transfer yet. A comparison endpoint sums the device rows of all circuit lists by cost group (longest prefix) against the estimate. The UI adds a sidebar entry, a list of Stände (template KG 400, empty or copy) and a spreadsheet-like editor with autosave, AMEV picker for the GLF, totals, reserve, maximum current, comparison table and print layout. The tests reproduce the template example exactly (143.24 kW, 148.48 kVA, 157.56 kW / 163.33 kVA with 10 % reserve, 235.75 A). See docs/estimate-balance.md and issue #7. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1237 lines
31 KiB
TypeScript
1237 lines
31 KiB
TypeScript
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,
|
|
ExternalCsvConfigurationSnapshotDto,
|
|
ExternalCsvPreviewDto,
|
|
ExternalInitialImportPlanDto,
|
|
ApplyExternalInitialImportInput,
|
|
ExternalInitialImportResultDto,
|
|
CreateEstimateBalanceInput,
|
|
EstimateBalanceCommandResultDto,
|
|
EstimateBalanceDto,
|
|
EstimateBalanceUpdateResultDto,
|
|
EstimateComparisonDto,
|
|
} 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";
|
|
|
|
/**
|
|
* 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<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();
|
|
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<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"];
|
|
} & Partial<Pick<ProjectDto, "siteAddress" | "workStage" | "projectLead" | "projectEditor">>
|
|
) {
|
|
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;
|
|
} & Partial<DistributionBoardInfoDto>,
|
|
expectedRevision: number
|
|
) {
|
|
return request<DistributionBoardCommandResultDto>(
|
|
`/api/projects/${projectId}/distribution-boards/${distributionBoardId}`,
|
|
{
|
|
method: "PUT",
|
|
body: JSON.stringify({ ...input, expectedRevision }),
|
|
}
|
|
);
|
|
}
|
|
|
|
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<void>(documentLogoUrl, { method: "DELETE" });
|
|
}
|
|
|
|
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 listBuildings(projectId: string) {
|
|
return request<BuildingDto[]>(`/api/projects/${projectId}/buildings`);
|
|
}
|
|
|
|
export function createBuilding(
|
|
projectId: string,
|
|
input: CreateBuildingInput,
|
|
expectedRevision: number
|
|
) {
|
|
return request<ProjectBuildingCommandResultDto>(
|
|
`/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<ProjectBuildingCommandResultDto>(
|
|
`/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<ProjectCommandResultDto>(
|
|
`/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<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(toFloorRequestBody(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(toFloorRequestBody(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 }),
|
|
}
|
|
);
|
|
}
|
|
|
|
// ── 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<EstimateBalanceDto[]>(estimateBalancesUrl(projectId));
|
|
}
|
|
|
|
export function getEstimateBalance(projectId: string, balanceId: string) {
|
|
return request<EstimateBalanceDto>(estimateBalancesUrl(projectId, balanceId));
|
|
}
|
|
|
|
export function createEstimateBalance(
|
|
projectId: string,
|
|
input: CreateEstimateBalanceInput,
|
|
expectedRevision: number
|
|
) {
|
|
return request<EstimateBalanceCommandResultDto>(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<EstimateBalanceUpdateResultDto>(
|
|
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<ProjectCommandResultDto>(estimateBalancesUrl(projectId, balanceId), {
|
|
method: "DELETE",
|
|
body: JSON.stringify({ expectedRevision }),
|
|
});
|
|
}
|
|
|
|
export function getEstimateComparison(projectId: string, balanceId: string) {
|
|
return request<EstimateComparisonDto>(
|
|
`${estimateBalancesUrl(projectId, balanceId)}/comparison`
|
|
);
|
|
}
|