Merge origin/main into feature/user-management
Renumber the users migration to 0013_users so it follows the upstream 0012_good_gateway (logical board feeds); AGENTS.md deferred-work list keeps both the new feed items and the auth items. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
95bfba2f67
63 changed files with 5546 additions and 443 deletions
217
src/domain/calculations/distribution-board-load-calculation.ts
Normal file
217
src/domain/calculations/distribution-board-load-calculation.ts
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
export interface DistributionBoardLoadBoardInput {
|
||||
id: string;
|
||||
name: string;
|
||||
boardNumber: string | null;
|
||||
simultaneityFactor: number;
|
||||
feedingDistributionBoardId: string | null;
|
||||
feedingCircuitId: string | null;
|
||||
}
|
||||
|
||||
export interface DistributionBoardLoadCircuitInput {
|
||||
id: string;
|
||||
distributionBoardId: string;
|
||||
rows: Array<{
|
||||
activePowerKw: number;
|
||||
cosPhi: number | null;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface DownstreamDistributionBoardLoad {
|
||||
distributionBoardId: string;
|
||||
name: string;
|
||||
boardNumber: string | null;
|
||||
activePowerKw: number;
|
||||
apparentPowerKva: number;
|
||||
cosPhi: number;
|
||||
}
|
||||
|
||||
export interface CalculatedCircuitLoad {
|
||||
circuitId: string;
|
||||
deviceActivePowerKw: number;
|
||||
deviceApparentPowerKva: number;
|
||||
activePowerKw: number;
|
||||
apparentPowerKva: number;
|
||||
cosPhi: number;
|
||||
downstreamBoards: DownstreamDistributionBoardLoad[];
|
||||
}
|
||||
|
||||
export interface CalculatedDistributionBoardLoad {
|
||||
distributionBoardId: string;
|
||||
activePowerBeforeSimultaneityKw: number;
|
||||
apparentPowerBeforeSimultaneityKva: number;
|
||||
demandActivePowerKw: number;
|
||||
demandApparentPowerKva: number;
|
||||
cosPhi: number;
|
||||
}
|
||||
|
||||
export interface DistributionBoardLoadCalculation {
|
||||
boards: Map<string, CalculatedDistributionBoardLoad>;
|
||||
circuits: Map<string, CalculatedCircuitLoad>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates one project graph. A child board enters exactly one feeder
|
||||
* circuit with its demand after the child's own board simultaneity factor.
|
||||
* Board-only relations form a hierarchy but intentionally add no load.
|
||||
*/
|
||||
export function calculateDistributionBoardLoads(input: {
|
||||
boards: DistributionBoardLoadBoardInput[];
|
||||
circuits: DistributionBoardLoadCircuitInput[];
|
||||
}): DistributionBoardLoadCalculation {
|
||||
const boardById = new Map(input.boards.map((board) => [board.id, board]));
|
||||
const circuitById = new Map(input.circuits.map((circuit) => [circuit.id, circuit]));
|
||||
const circuitsByBoardId = groupBy(
|
||||
input.circuits,
|
||||
(circuit) => circuit.distributionBoardId
|
||||
);
|
||||
const childrenByCircuitId = new Map<string, DistributionBoardLoadBoardInput[]>();
|
||||
for (const board of input.boards) {
|
||||
assertBoardInput(board, boardById, circuitById);
|
||||
if (board.feedingCircuitId === null) continue;
|
||||
const children = childrenByCircuitId.get(board.feedingCircuitId) ?? [];
|
||||
children.push(board);
|
||||
childrenByCircuitId.set(board.feedingCircuitId, children);
|
||||
}
|
||||
|
||||
const boards = new Map<string, CalculatedDistributionBoardLoad>();
|
||||
const circuits = new Map<string, CalculatedCircuitLoad>();
|
||||
const activePath = new Set<string>();
|
||||
|
||||
const calculateBoard = (boardId: string): CalculatedDistributionBoardLoad => {
|
||||
const cached = boards.get(boardId);
|
||||
if (cached) return cached;
|
||||
if (activePath.has(boardId)) {
|
||||
throw new Error("Distribution-board feeding graph contains a cycle.");
|
||||
}
|
||||
const board = boardById.get(boardId);
|
||||
if (!board) throw new Error("Distribution board is missing from load graph.");
|
||||
activePath.add(boardId);
|
||||
|
||||
let activePowerBeforeSimultaneityKw = 0;
|
||||
let apparentPowerBeforeSimultaneityKva = 0;
|
||||
for (const circuit of circuitsByBoardId.get(boardId) ?? []) {
|
||||
let deviceActivePowerKw = 0;
|
||||
let deviceApparentPowerKva = 0;
|
||||
for (const row of circuit.rows) {
|
||||
assertNonNegativeFinite(row.activePowerKw, "row active power");
|
||||
const cosPhi = row.cosPhi ?? 1;
|
||||
if (!Number.isFinite(cosPhi) || cosPhi <= 0 || cosPhi > 1) {
|
||||
throw new Error("Row cos phi must be greater than zero and at most one.");
|
||||
}
|
||||
deviceActivePowerKw += row.activePowerKw;
|
||||
deviceApparentPowerKva += row.activePowerKw / cosPhi;
|
||||
}
|
||||
const downstreamBoards = (childrenByCircuitId.get(circuit.id) ?? [])
|
||||
.map((child) => {
|
||||
const childLoad = calculateBoard(child.id);
|
||||
return {
|
||||
distributionBoardId: child.id,
|
||||
name: child.name,
|
||||
boardNumber: child.boardNumber,
|
||||
activePowerKw: childLoad.demandActivePowerKw,
|
||||
apparentPowerKva: childLoad.demandApparentPowerKva,
|
||||
cosPhi: effectiveCosPhi(
|
||||
childLoad.demandActivePowerKw,
|
||||
childLoad.demandApparentPowerKva
|
||||
),
|
||||
};
|
||||
});
|
||||
const activePowerKw =
|
||||
deviceActivePowerKw +
|
||||
downstreamBoards.reduce((sum, child) => sum + child.activePowerKw, 0);
|
||||
const apparentPowerKva =
|
||||
deviceApparentPowerKva +
|
||||
downstreamBoards.reduce((sum, child) => sum + child.apparentPowerKva, 0);
|
||||
circuits.set(circuit.id, {
|
||||
circuitId: circuit.id,
|
||||
deviceActivePowerKw,
|
||||
deviceApparentPowerKva,
|
||||
activePowerKw,
|
||||
apparentPowerKva,
|
||||
cosPhi: effectiveCosPhi(activePowerKw, apparentPowerKva),
|
||||
downstreamBoards,
|
||||
});
|
||||
activePowerBeforeSimultaneityKw += activePowerKw;
|
||||
apparentPowerBeforeSimultaneityKva += apparentPowerKva;
|
||||
}
|
||||
|
||||
activePath.delete(boardId);
|
||||
const result = {
|
||||
distributionBoardId: boardId,
|
||||
activePowerBeforeSimultaneityKw,
|
||||
apparentPowerBeforeSimultaneityKva,
|
||||
demandActivePowerKw:
|
||||
activePowerBeforeSimultaneityKw * board.simultaneityFactor,
|
||||
demandApparentPowerKva:
|
||||
apparentPowerBeforeSimultaneityKva * board.simultaneityFactor,
|
||||
cosPhi: effectiveCosPhi(
|
||||
activePowerBeforeSimultaneityKw,
|
||||
apparentPowerBeforeSimultaneityKva
|
||||
),
|
||||
};
|
||||
boards.set(boardId, result);
|
||||
return result;
|
||||
};
|
||||
|
||||
for (const board of input.boards) calculateBoard(board.id);
|
||||
return { boards, circuits };
|
||||
}
|
||||
|
||||
function assertBoardInput(
|
||||
board: DistributionBoardLoadBoardInput,
|
||||
boardById: Map<string, DistributionBoardLoadBoardInput>,
|
||||
circuitById: Map<string, DistributionBoardLoadCircuitInput>
|
||||
) {
|
||||
if (
|
||||
!Number.isFinite(board.simultaneityFactor) ||
|
||||
board.simultaneityFactor < 0 ||
|
||||
board.simultaneityFactor > 1
|
||||
) {
|
||||
throw new Error("Board simultaneity factor must be between zero and one.");
|
||||
}
|
||||
if (board.feedingDistributionBoardId === board.id) {
|
||||
throw new Error("Distribution board cannot feed itself.");
|
||||
}
|
||||
if (
|
||||
board.feedingDistributionBoardId !== null &&
|
||||
!boardById.has(board.feedingDistributionBoardId)
|
||||
) {
|
||||
throw new Error("Feeding distribution board is missing from load graph.");
|
||||
}
|
||||
if (
|
||||
board.feedingCircuitId !== null &&
|
||||
board.feedingDistributionBoardId === null
|
||||
) {
|
||||
throw new Error("Feeding circuit requires a feeding distribution board.");
|
||||
}
|
||||
if (
|
||||
board.feedingCircuitId !== null &&
|
||||
circuitById.get(board.feedingCircuitId)?.distributionBoardId !==
|
||||
board.feedingDistributionBoardId
|
||||
) {
|
||||
throw new Error("Feeding circuit belongs to another distribution board.");
|
||||
}
|
||||
}
|
||||
|
||||
function effectiveCosPhi(activePowerKw: number, apparentPowerKva: number) {
|
||||
return activePowerKw > 0 && apparentPowerKva > 0
|
||||
? activePowerKw / apparentPowerKva
|
||||
: 1;
|
||||
}
|
||||
|
||||
function assertNonNegativeFinite(value: number, label: string) {
|
||||
if (!Number.isFinite(value) || value < 0) {
|
||||
throw new Error(`${label} must be a non-negative finite number.`);
|
||||
}
|
||||
}
|
||||
|
||||
function groupBy<T>(values: T[], keyOf: (value: T) => string) {
|
||||
const groups = new Map<string, T[]>();
|
||||
for (const value of values) {
|
||||
const key = keyOf(value);
|
||||
const group = groups.get(key) ?? [];
|
||||
group.push(value);
|
||||
groups.set(key, group);
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
|
@ -40,7 +40,10 @@ export interface CircuitTreeCircuit {
|
|||
isReserve: boolean;
|
||||
remark?: string;
|
||||
circuitTotalPower: number;
|
||||
circuitApparentPower: number;
|
||||
circuitCosPhi: number;
|
||||
deviceRows: CircuitTreeDeviceRow[];
|
||||
downstreamBoardLoads: CircuitTreeDownstreamBoardLoad[];
|
||||
protectionDevice?: CircuitTreeProtectionDevice;
|
||||
}
|
||||
|
||||
|
|
@ -59,6 +62,11 @@ export interface CircuitTreeSectionBlock {
|
|||
|
||||
export interface CircuitTreeResponse {
|
||||
circuitListId: string;
|
||||
distributionBoardId: string;
|
||||
distributionBoardName: string;
|
||||
distributionBoardNumber?: string;
|
||||
distributionBoardSupplyType?: DistributionBoardSupplyType;
|
||||
feedingReference?: CircuitTreeFeedingReference;
|
||||
currentRevision: number;
|
||||
singlePhaseVoltageV: number;
|
||||
threePhaseVoltageV: number;
|
||||
|
|
@ -70,7 +78,36 @@ export interface CircuitTreeResponse {
|
|||
footerComponents: CircuitTreeComponent[];
|
||||
}
|
||||
|
||||
export interface CircuitTreeDownstreamBoardLoad {
|
||||
distributionBoardId: string;
|
||||
name: string;
|
||||
boardNumber?: string;
|
||||
supplyType?: DistributionBoardSupplyType;
|
||||
activePowerKw: number;
|
||||
apparentPowerKva: number;
|
||||
cosPhi: number;
|
||||
}
|
||||
|
||||
export interface CircuitTreeFeedingReference {
|
||||
distributionBoardId: string;
|
||||
circuitListId: string;
|
||||
distributionBoardName: string;
|
||||
distributionBoardNumber?: string;
|
||||
circuitId?: string;
|
||||
circuitEquipmentIdentifier?: string;
|
||||
circuitDisplayName?: string;
|
||||
circuitCategory?: CircuitGroupCategory;
|
||||
}
|
||||
|
||||
export interface ProjectCircuitTreesResponse {
|
||||
currentRevision: number;
|
||||
trees: CircuitTreeResponse[];
|
||||
}
|
||||
|
||||
import type { CircuitGroupCategory } from "../../shared/constants/circuit-group.js";
|
||||
import type {
|
||||
DistributionBoardSupplyType,
|
||||
} from "../../shared/constants/distribution-board.js";
|
||||
import type {
|
||||
DistributionBoardComponentPlacement,
|
||||
DistributionBoardComponentRole,
|
||||
|
|
|
|||
|
|
@ -9,16 +9,18 @@ import type { SerializedProjectCommand } from "./project-command.model.js";
|
|||
|
||||
export const distributionBoardUpdateCommandType =
|
||||
"distribution-board.update" as const;
|
||||
// The info-card fields were added additively to schema version 1: every
|
||||
// previously stored command remains valid, and the inverse is still derived
|
||||
// from the persisted row for exactly the changed fields.
|
||||
export const distributionBoardUpdateCommandSchemaVersion = 1 as const;
|
||||
// Version 2 adds logical feeding references. Version-1 commands remain
|
||||
// executable and can only change the fields they originally knew.
|
||||
export const distributionBoardUpdateCommandSchemaVersion = 2 as const;
|
||||
const legacyDistributionBoardUpdateCommandSchemaVersion = 1 as const;
|
||||
|
||||
export interface DistributionBoardUpdateValues
|
||||
extends Record<DistributionBoardInfoField, string | null> {
|
||||
floorId: string | null;
|
||||
supplyType: DistributionBoardSupplyType | null;
|
||||
simultaneityFactor: number;
|
||||
feedingDistributionBoardId: string | null;
|
||||
feedingCircuitId: string | null;
|
||||
}
|
||||
|
||||
export type DistributionBoardUpdateField =
|
||||
|
|
@ -41,16 +43,20 @@ export interface DistributionBoardUpdateCommandPayload {
|
|||
|
||||
export interface DistributionBoardUpdateProjectCommand
|
||||
extends SerializedProjectCommand<DistributionBoardUpdateCommandPayload> {
|
||||
schemaVersion: typeof distributionBoardUpdateCommandSchemaVersion;
|
||||
schemaVersion:
|
||||
| typeof legacyDistributionBoardUpdateCommandSchemaVersion
|
||||
| typeof distributionBoardUpdateCommandSchemaVersion;
|
||||
type: typeof distributionBoardUpdateCommandType;
|
||||
}
|
||||
|
||||
export function createDistributionBoardUpdateProjectCommand(
|
||||
distributionBoardId: string,
|
||||
patch: DistributionBoardUpdatePatch
|
||||
patch: DistributionBoardUpdatePatch,
|
||||
schemaVersion: DistributionBoardUpdateProjectCommand["schemaVersion"] =
|
||||
distributionBoardUpdateCommandSchemaVersion
|
||||
): DistributionBoardUpdateProjectCommand {
|
||||
const command: DistributionBoardUpdateProjectCommand = {
|
||||
schemaVersion: distributionBoardUpdateCommandSchemaVersion,
|
||||
schemaVersion,
|
||||
type: distributionBoardUpdateCommandType,
|
||||
payload: {
|
||||
distributionBoardId,
|
||||
|
|
@ -68,7 +74,8 @@ export function assertDistributionBoardUpdateProjectCommand(
|
|||
command: SerializedProjectCommand<unknown>
|
||||
): asserts command is DistributionBoardUpdateProjectCommand {
|
||||
if (
|
||||
command.schemaVersion !== distributionBoardUpdateCommandSchemaVersion ||
|
||||
(command.schemaVersion !== legacyDistributionBoardUpdateCommandSchemaVersion &&
|
||||
command.schemaVersion !== distributionBoardUpdateCommandSchemaVersion) ||
|
||||
command.type !== distributionBoardUpdateCommandType ||
|
||||
!isPlainObject(command.payload)
|
||||
) {
|
||||
|
|
@ -93,7 +100,12 @@ export function assertDistributionBoardUpdateProjectCommand(
|
|||
(change.field !== "floorId" &&
|
||||
change.field !== "supplyType" &&
|
||||
change.field !== "simultaneityFactor" &&
|
||||
change.field !== "feedingDistributionBoardId" &&
|
||||
change.field !== "feedingCircuitId" &&
|
||||
!isInfoField(change.field)) ||
|
||||
(command.schemaVersion === legacyDistributionBoardUpdateCommandSchemaVersion &&
|
||||
(change.field === "feedingDistributionBoardId" ||
|
||||
change.field === "feedingCircuitId")) ||
|
||||
seen.has(change.field)
|
||||
) {
|
||||
throw new Error(
|
||||
|
|
@ -105,7 +117,9 @@ export function assertDistributionBoardUpdateProjectCommand(
|
|||
} else if (change.field === "floorId") {
|
||||
if (
|
||||
change.value !== null &&
|
||||
(typeof change.value !== "string" || !change.value.trim())
|
||||
(typeof change.value !== "string" ||
|
||||
!change.value.trim() ||
|
||||
change.value !== change.value.trim())
|
||||
) {
|
||||
throw new Error("floorId must be a non-empty string or null.");
|
||||
}
|
||||
|
|
@ -118,6 +132,16 @@ export function assertDistributionBoardUpdateProjectCommand(
|
|||
) {
|
||||
throw new Error("supplyType is invalid.");
|
||||
}
|
||||
} else if (
|
||||
change.field === "feedingDistributionBoardId" ||
|
||||
change.field === "feedingCircuitId"
|
||||
) {
|
||||
if (
|
||||
change.value !== null &&
|
||||
(typeof change.value !== "string" || !change.value.trim())
|
||||
) {
|
||||
throw new Error(`${change.field} must be a non-empty string or null.`);
|
||||
}
|
||||
} else if (
|
||||
typeof change.value !== "number" ||
|
||||
!Number.isFinite(change.value) ||
|
||||
|
|
|
|||
|
|
@ -74,6 +74,8 @@ export interface DistributionBoardSubtreeSnapshot {
|
|||
floorId: string | null;
|
||||
supplyType: DistributionBoardSupplyType | null;
|
||||
simultaneityFactor: number;
|
||||
feedingDistributionBoardId: string | null;
|
||||
feedingCircuitId: string | null;
|
||||
} & Record<DistributionBoardInfoField, string | null>;
|
||||
circuitList: {
|
||||
id: string;
|
||||
|
|
@ -184,6 +186,9 @@ export function normalizeDistributionBoardSubtreeSnapshot(
|
|||
for (const field of distributionBoardInfoFields) {
|
||||
board[field] = board[field] ?? null;
|
||||
}
|
||||
board.feedingDistributionBoardId =
|
||||
board.feedingDistributionBoardId ?? null;
|
||||
board.feedingCircuitId = board.feedingCircuitId ?? null;
|
||||
return { ...snapshot, distributionBoard: board };
|
||||
}
|
||||
|
||||
|
|
@ -227,6 +232,18 @@ export function assertDistributionBoardSubtreeSnapshot(
|
|||
assertNullableString(board[field], `distributionBoard.${field}`);
|
||||
}
|
||||
}
|
||||
if (board.feedingDistributionBoardId !== undefined) {
|
||||
assertNullableString(
|
||||
board.feedingDistributionBoardId,
|
||||
"distributionBoard.feedingDistributionBoardId"
|
||||
);
|
||||
}
|
||||
if (board.feedingCircuitId !== undefined) {
|
||||
assertNullableString(
|
||||
board.feedingCircuitId,
|
||||
"distributionBoard.feedingCircuitId"
|
||||
);
|
||||
}
|
||||
for (const field of [
|
||||
"id",
|
||||
"projectId",
|
||||
|
|
|
|||
|
|
@ -35,7 +35,8 @@ import {
|
|||
type ExternalModelStateSnapshot,
|
||||
} from "../../external-model/domain/external-model-contracts.js";
|
||||
|
||||
export const projectStateSnapshotSchemaVersion = 8 as const;
|
||||
export const projectStateSnapshotSchemaVersion = 9 as const;
|
||||
const roomAreaProjectStateSnapshotSchemaVersion = 8 as const;
|
||||
const buildingProjectStateSnapshotSchemaVersion = 7 as const;
|
||||
const coverProjectStateSnapshotSchemaVersion = 6 as const;
|
||||
const boardInfoProjectStateSnapshotSchemaVersion = 5 as const;
|
||||
|
|
@ -112,7 +113,7 @@ const distributionBoardInfoValueSchema = (field: DistributionBoardInfoField) =>
|
|||
.max(distributionBoardInfoFieldMaxLengths[field])
|
||||
.nullable();
|
||||
|
||||
const distributionBoardSchema = legacyDistributionBoardSchema
|
||||
const distributionBoardWithoutFeedingSchema = legacyDistributionBoardSchema
|
||||
.extend({
|
||||
boardNumber: distributionBoardInfoValueSchema("boardNumber"),
|
||||
location: distributionBoardInfoValueSchema("location"),
|
||||
|
|
@ -121,6 +122,13 @@ const distributionBoardSchema = legacyDistributionBoardSchema
|
|||
})
|
||||
.strict();
|
||||
|
||||
const distributionBoardSchema = distributionBoardWithoutFeedingSchema
|
||||
.extend({
|
||||
feedingDistributionBoardId: idSchema.nullable(),
|
||||
feedingCircuitId: idSchema.nullable(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
function withEmptyDistributionBoardInfo(
|
||||
boards: Array<z.infer<typeof legacyDistributionBoardSchema>>
|
||||
) {
|
||||
|
|
@ -133,6 +141,16 @@ function withEmptyDistributionBoardInfo(
|
|||
}));
|
||||
}
|
||||
|
||||
function withEmptyDistributionBoardFeeding(
|
||||
boards: Array<z.infer<typeof distributionBoardWithoutFeedingSchema>>
|
||||
) {
|
||||
return boards.map((board) => ({
|
||||
...board,
|
||||
feedingDistributionBoardId: null,
|
||||
feedingCircuitId: null,
|
||||
}));
|
||||
}
|
||||
|
||||
const circuitListSchema = z
|
||||
.object({
|
||||
id: idSchema,
|
||||
|
|
@ -553,11 +571,22 @@ export const projectStateSnapshotSchema = z
|
|||
})
|
||||
.strict();
|
||||
|
||||
const roomAreaProjectStateSnapshotSchema = z
|
||||
.object({
|
||||
schemaVersion: z.literal(roomAreaProjectStateSnapshotSchemaVersion),
|
||||
project: projectSchema,
|
||||
distributionBoards: z.array(distributionBoardWithoutFeedingSchema),
|
||||
externalCsvConfiguration: externalCsvConfigurationSnapshotSchema.nullable(),
|
||||
externalModel: externalModelStateSchema,
|
||||
...projectStateSnapshotContents,
|
||||
})
|
||||
.strict();
|
||||
|
||||
const buildingProjectStateSnapshotSchema = z
|
||||
.object({
|
||||
schemaVersion: z.literal(buildingProjectStateSnapshotSchemaVersion),
|
||||
project: projectSchema,
|
||||
distributionBoards: z.array(distributionBoardSchema),
|
||||
distributionBoards: z.array(distributionBoardWithoutFeedingSchema),
|
||||
externalCsvConfiguration: externalCsvConfigurationSnapshotSchema.nullable(),
|
||||
externalModel: externalModelStateSchema,
|
||||
...buildingProjectStateSnapshotContents,
|
||||
|
|
@ -568,7 +597,7 @@ const coverProjectStateSnapshotSchema = z
|
|||
.object({
|
||||
schemaVersion: z.literal(coverProjectStateSnapshotSchemaVersion),
|
||||
project: projectSchema,
|
||||
distributionBoards: z.array(distributionBoardSchema),
|
||||
distributionBoards: z.array(distributionBoardWithoutFeedingSchema),
|
||||
externalCsvConfiguration: externalCsvConfigurationSnapshotSchema.nullable(),
|
||||
externalModel: externalModelStateSchema,
|
||||
...previousProjectStateSnapshotContents,
|
||||
|
|
@ -655,6 +684,27 @@ export function parseProjectStateSnapshot(
|
|||
}
|
||||
|
||||
function upgradePreviousProjectStateSnapshot(value: unknown): unknown {
|
||||
const roomArea = upgradeToRoomAreaProjectStateSnapshot(value);
|
||||
if (
|
||||
roomArea !== null &&
|
||||
typeof roomArea === "object" &&
|
||||
!Array.isArray(roomArea) &&
|
||||
(roomArea as { schemaVersion?: unknown }).schemaVersion ===
|
||||
roomAreaProjectStateSnapshotSchemaVersion
|
||||
) {
|
||||
const previous = roomAreaProjectStateSnapshotSchema.parse(roomArea);
|
||||
return {
|
||||
...previous,
|
||||
schemaVersion: projectStateSnapshotSchemaVersion,
|
||||
distributionBoards: withEmptyDistributionBoardFeeding(
|
||||
previous.distributionBoards
|
||||
),
|
||||
};
|
||||
}
|
||||
return roomArea;
|
||||
}
|
||||
|
||||
function upgradeToRoomAreaProjectStateSnapshot(value: unknown): unknown {
|
||||
const cover = upgradeToCoverProjectStateSnapshot(value);
|
||||
if (
|
||||
cover !== null &&
|
||||
|
|
@ -666,7 +716,7 @@ function upgradePreviousProjectStateSnapshot(value: unknown): unknown {
|
|||
const previous = buildingProjectStateSnapshotSchema.parse(cover);
|
||||
return {
|
||||
...previous,
|
||||
schemaVersion: projectStateSnapshotSchemaVersion,
|
||||
schemaVersion: roomAreaProjectStateSnapshotSchemaVersion,
|
||||
rooms: previous.rooms.map((room) => ({ ...room, areaM2: null })),
|
||||
};
|
||||
}
|
||||
|
|
@ -683,7 +733,7 @@ function upgradePreviousProjectStateSnapshot(value: unknown): unknown {
|
|||
// building. Build version 7 first so the room-area upgrade above remains a
|
||||
// single exact transition.
|
||||
const previous = coverProjectStateSnapshotSchema.parse(cover);
|
||||
return upgradePreviousProjectStateSnapshot({
|
||||
return upgradeToRoomAreaProjectStateSnapshot({
|
||||
...previous,
|
||||
schemaVersion: buildingProjectStateSnapshotSchemaVersion,
|
||||
buildings: [],
|
||||
|
|
@ -873,6 +923,15 @@ function assertProjectStateSnapshotRelations(
|
|||
const sectionById = new Map(
|
||||
snapshot.circuitSections.map((entry) => [entry.id, entry])
|
||||
);
|
||||
const boardById = new Map(
|
||||
snapshot.distributionBoards.map((entry) => [entry.id, entry])
|
||||
);
|
||||
const circuitListById = new Map(
|
||||
snapshot.circuitLists.map((entry) => [entry.id, entry])
|
||||
);
|
||||
const circuitById = new Map(
|
||||
snapshot.circuits.map((entry) => [entry.id, entry])
|
||||
);
|
||||
const componentById = new Map(
|
||||
snapshot.distributionBoardComponents.map((entry) => [
|
||||
entry.id,
|
||||
|
|
@ -901,6 +960,47 @@ function assertProjectStateSnapshotRelations(
|
|||
"Snapshot distribution board uses a disabled supply type."
|
||||
);
|
||||
}
|
||||
if (board.feedingDistributionBoardId === board.id) {
|
||||
throw new Error("Snapshot distribution board cannot feed itself.");
|
||||
}
|
||||
if (board.feedingDistributionBoardId !== null) {
|
||||
assertReference(
|
||||
boardIds,
|
||||
board.feedingDistributionBoardId,
|
||||
"feeding distribution board"
|
||||
);
|
||||
}
|
||||
if (board.feedingCircuitId !== null) {
|
||||
if (board.feedingDistributionBoardId === null) {
|
||||
throw new Error(
|
||||
"Snapshot feeding circuit requires a feeding distribution board."
|
||||
);
|
||||
}
|
||||
assertReference(circuitIds, board.feedingCircuitId, "feeding circuit");
|
||||
const feedingCircuit = circuitById.get(board.feedingCircuitId);
|
||||
const feedingList = feedingCircuit
|
||||
? circuitListById.get(feedingCircuit.circuitListId)
|
||||
: undefined;
|
||||
if (
|
||||
!feedingList ||
|
||||
feedingList.distributionBoardId !== board.feedingDistributionBoardId
|
||||
) {
|
||||
throw new Error(
|
||||
"Snapshot feeding circuit belongs to another distribution board."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const board of snapshot.distributionBoards) {
|
||||
const visited = new Set<string>([board.id]);
|
||||
let currentId = board.feedingDistributionBoardId;
|
||||
while (currentId !== null) {
|
||||
if (visited.has(currentId)) {
|
||||
throw new Error("Snapshot distribution-board feeding graph contains a cycle.");
|
||||
}
|
||||
visited.add(currentId);
|
||||
currentId = boardById.get(currentId)?.feedingDistributionBoardId ?? null;
|
||||
}
|
||||
}
|
||||
for (const list of snapshot.circuitLists) {
|
||||
assertProjectOwnership(list.projectId, projectId, "circuit list");
|
||||
|
|
|
|||
|
|
@ -223,6 +223,14 @@ export function remapProjectState(
|
|||
...board,
|
||||
id: requiredId(boardIds, board.id),
|
||||
projectId: targetProjectId,
|
||||
feedingDistributionBoardId:
|
||||
board.feedingDistributionBoardId === null
|
||||
? null
|
||||
: requiredId(boardIds, board.feedingDistributionBoardId),
|
||||
feedingCircuitId:
|
||||
board.feedingCircuitId === null
|
||||
? null
|
||||
: requiredId(circuitIds, board.feedingCircuitId),
|
||||
floorId:
|
||||
board.floorId === null
|
||||
? null
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue