|
|
|
@@ -1,394 +0,0 @@
|
|
|
|
|
import type { Request, Response } from "express";
|
|
|
|
|
import { db } from "../../db/client.js";
|
|
|
|
|
import { CircuitListRepository } from "../../db/repositories/circuit-list.repository.js";
|
|
|
|
|
import { ConsumerRepository } from "../../db/repositories/consumer.repository.js";
|
|
|
|
|
import { DistributionBoardRepository } from "../../db/repositories/distribution-board.repository.js";
|
|
|
|
|
import { FloorRepository } from "../../db/repositories/floor.repository.js";
|
|
|
|
|
import { ProjectDeviceRepository } from "../../db/repositories/project-device.repository.js";
|
|
|
|
|
import { ProjectRepository } from "../../db/repositories/project.repository.js";
|
|
|
|
|
import { RoomRepository } from "../../db/repositories/room.repository.js";
|
|
|
|
|
import type { Consumer } from "../../domain/models/consumer.model.js";
|
|
|
|
|
import { applyLinkedProjectDeviceValues } from "../../domain/services/consumer-linking.service.js";
|
|
|
|
|
import { PowerBalanceService } from "../../domain/services/power-balance.service.js";
|
|
|
|
|
import {
|
|
|
|
|
type CreateConsumerInput,
|
|
|
|
|
createConsumerSchema,
|
|
|
|
|
updateConsumerSchema,
|
|
|
|
|
} from "../../shared/validation/consumer.schemas.js";
|
|
|
|
|
|
|
|
|
|
const circuitListRepository = new CircuitListRepository();
|
|
|
|
|
const consumerRepository = new ConsumerRepository();
|
|
|
|
|
const distributionBoardRepository = new DistributionBoardRepository(db);
|
|
|
|
|
const floorRepository = new FloorRepository();
|
|
|
|
|
const projectDeviceRepository = new ProjectDeviceRepository();
|
|
|
|
|
const projectRepository = new ProjectRepository();
|
|
|
|
|
const roomRepository = new RoomRepository();
|
|
|
|
|
const powerBalanceService = new PowerBalanceService();
|
|
|
|
|
|
|
|
|
|
type ConsumerRow = {
|
|
|
|
|
id: string;
|
|
|
|
|
projectId: string;
|
|
|
|
|
distributionBoardId: string | null;
|
|
|
|
|
circuitListId: string | null;
|
|
|
|
|
projectDeviceId: string | null;
|
|
|
|
|
isLinkedToDevice: number;
|
|
|
|
|
roomId: string | null;
|
|
|
|
|
circuitNumber: string | null;
|
|
|
|
|
description: string | null;
|
|
|
|
|
name: string;
|
|
|
|
|
category: string | null;
|
|
|
|
|
deviceType: string | null;
|
|
|
|
|
phaseType: string | null;
|
|
|
|
|
tradeOrCostGroup: string | null;
|
|
|
|
|
group: string | null;
|
|
|
|
|
protectionType: string | null;
|
|
|
|
|
protectionRatedCurrent: number | null;
|
|
|
|
|
protectionCharacteristic: string | null;
|
|
|
|
|
cableType: string | null;
|
|
|
|
|
cableCrossSection: string | null;
|
|
|
|
|
comment: string | null;
|
|
|
|
|
quantity: number;
|
|
|
|
|
installedPowerPerUnitKw: number;
|
|
|
|
|
demandFactor: number;
|
|
|
|
|
voltageV: number | null;
|
|
|
|
|
phaseCount: number | null;
|
|
|
|
|
powerFactor: number | null;
|
|
|
|
|
note: string | null;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
async function validateDistributionBoardOwnership(
|
|
|
|
|
projectId: string,
|
|
|
|
|
distributionBoardId: string | undefined
|
|
|
|
|
) {
|
|
|
|
|
if (!distributionBoardId) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return distributionBoardRepository.existsInProject(projectId, distributionBoardId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function validateRoomOwnership(projectId: string, roomId: string | undefined) {
|
|
|
|
|
if (!roomId) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return roomRepository.existsInProject(projectId, roomId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function validateProjectDeviceOwnership(projectId: string, projectDeviceId: string | undefined) {
|
|
|
|
|
if (!projectDeviceId) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
const row = await projectDeviceRepository.findById(projectId, projectDeviceId);
|
|
|
|
|
return Boolean(row);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function applyDeviceLinkIfNeeded(input: CreateConsumerInput): Promise<CreateConsumerInput> {
|
|
|
|
|
if (!input.projectDeviceId || !input.isLinkedToDevice) {
|
|
|
|
|
return input;
|
|
|
|
|
}
|
|
|
|
|
const device = await projectDeviceRepository.findById(input.projectId, input.projectDeviceId);
|
|
|
|
|
return applyLinkedProjectDeviceValues(input, device);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function resolveCircuitScope(input: {
|
|
|
|
|
projectId: string;
|
|
|
|
|
distributionBoardId?: string;
|
|
|
|
|
circuitListId?: string;
|
|
|
|
|
}) {
|
|
|
|
|
let distributionBoardId = input.distributionBoardId;
|
|
|
|
|
let circuitListId = input.circuitListId;
|
|
|
|
|
|
|
|
|
|
if (distributionBoardId) {
|
|
|
|
|
const linkedList = await circuitListRepository.findByDistributionBoardId(
|
|
|
|
|
input.projectId,
|
|
|
|
|
distributionBoardId
|
|
|
|
|
);
|
|
|
|
|
if (!linkedList) {
|
|
|
|
|
return { ok: false as const, error: "No circuit list found for the provided distribution board." };
|
|
|
|
|
}
|
|
|
|
|
if (circuitListId && circuitListId !== linkedList.id) {
|
|
|
|
|
return {
|
|
|
|
|
ok: false as const,
|
|
|
|
|
error: "Circuit list does not match the provided distribution board.",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
circuitListId = linkedList.id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (circuitListId) {
|
|
|
|
|
const list = await circuitListRepository.findById(input.projectId, circuitListId);
|
|
|
|
|
if (!list) {
|
|
|
|
|
return { ok: false as const, error: "Circuit list does not belong to the provided project." };
|
|
|
|
|
}
|
|
|
|
|
if (distributionBoardId && distributionBoardId !== list.distributionBoardId) {
|
|
|
|
|
return {
|
|
|
|
|
ok: false as const,
|
|
|
|
|
error: "Circuit list does not match the provided distribution board.",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
distributionBoardId = list.distributionBoardId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
ok: true as const,
|
|
|
|
|
distributionBoardId,
|
|
|
|
|
circuitListId,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildConsumerFromRow(
|
|
|
|
|
row: ConsumerRow,
|
|
|
|
|
roomById: Map<string, { floorId: string | null; roomName: string; roomNumber: string }>,
|
|
|
|
|
floorById: Map<string, { name: string }>
|
|
|
|
|
): Consumer {
|
|
|
|
|
const room = row.roomId ? roomById.get(row.roomId) : undefined;
|
|
|
|
|
const floor = room?.floorId ? floorById.get(room.floorId) : undefined;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
id: row.id,
|
|
|
|
|
projectId: row.projectId,
|
|
|
|
|
distributionBoardId: row.distributionBoardId ?? undefined,
|
|
|
|
|
circuitListId: row.circuitListId ?? undefined,
|
|
|
|
|
projectDeviceId: row.projectDeviceId ?? undefined,
|
|
|
|
|
isLinkedToDevice: Boolean(row.isLinkedToDevice),
|
|
|
|
|
roomId: row.roomId ?? undefined,
|
|
|
|
|
roomNumber: room?.roomNumber,
|
|
|
|
|
roomName: room?.roomName,
|
|
|
|
|
floorId: room?.floorId ?? undefined,
|
|
|
|
|
floorName: floor?.name,
|
|
|
|
|
circuitNumber: row.circuitNumber ?? undefined,
|
|
|
|
|
description: row.description ?? undefined,
|
|
|
|
|
name: row.name,
|
|
|
|
|
category: row.category ?? undefined,
|
|
|
|
|
deviceType: row.deviceType ?? undefined,
|
|
|
|
|
phaseType: row.phaseType ?? undefined,
|
|
|
|
|
tradeOrCostGroup: row.tradeOrCostGroup ?? undefined,
|
|
|
|
|
group: row.group ?? undefined,
|
|
|
|
|
protectionType: row.protectionType ?? undefined,
|
|
|
|
|
protectionRatedCurrent: row.protectionRatedCurrent ?? undefined,
|
|
|
|
|
protectionCharacteristic: row.protectionCharacteristic ?? undefined,
|
|
|
|
|
cableType: row.cableType ?? undefined,
|
|
|
|
|
cableCrossSection: row.cableCrossSection ?? undefined,
|
|
|
|
|
comment: row.comment ?? undefined,
|
|
|
|
|
quantity: row.quantity,
|
|
|
|
|
installedPowerPerUnitKw: row.installedPowerPerUnitKw,
|
|
|
|
|
demandFactor: row.demandFactor,
|
|
|
|
|
voltageV: row.voltageV ?? undefined,
|
|
|
|
|
phaseCount: row.phaseCount === 1 || row.phaseCount === 3 ? row.phaseCount : undefined,
|
|
|
|
|
powerFactor: row.powerFactor ?? undefined,
|
|
|
|
|
note: row.note ?? undefined,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function listConsumersByProject(req: Request, res: Response) {
|
|
|
|
|
const { projectId } = req.params;
|
|
|
|
|
if (typeof projectId !== "string") {
|
|
|
|
|
return res.status(400).json({ error: "Invalid projectId" });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const [rows, project, floors, rooms] = await Promise.all([
|
|
|
|
|
consumerRepository.listByProject(projectId),
|
|
|
|
|
projectRepository.findById(projectId),
|
|
|
|
|
floorRepository.listByProject(projectId),
|
|
|
|
|
roomRepository.listByProject(projectId),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const roomById = new Map(
|
|
|
|
|
rooms.map((room) => [room.id, { floorId: room.floorId, roomName: room.roomName, roomNumber: room.roomNumber }])
|
|
|
|
|
);
|
|
|
|
|
const floorById = new Map(floors.map((floor) => [floor.id, { name: floor.name }]));
|
|
|
|
|
|
|
|
|
|
const projectVoltageDefaults = project
|
|
|
|
|
? {
|
|
|
|
|
singlePhaseVoltageV: project.singlePhaseVoltageV,
|
|
|
|
|
threePhaseVoltageV: project.threePhaseVoltageV,
|
|
|
|
|
}
|
|
|
|
|
: undefined;
|
|
|
|
|
|
|
|
|
|
const enriched = rows.map((row) =>
|
|
|
|
|
powerBalanceService.enrichConsumer(
|
|
|
|
|
buildConsumerFromRow(row as ConsumerRow, roomById, floorById),
|
|
|
|
|
projectVoltageDefaults
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return res.json(enriched);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function createConsumer(req: Request, res: Response) {
|
|
|
|
|
const parsed = createConsumerSchema.safeParse(req.body);
|
|
|
|
|
if (!parsed.success) {
|
|
|
|
|
return res.status(400).json({ error: parsed.error.flatten() });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const normalizedData: CreateConsumerInput = {
|
|
|
|
|
...parsed.data,
|
|
|
|
|
name: parsed.data.name?.trim() || "Unbenannter Eintrag",
|
|
|
|
|
quantity: parsed.data.quantity ?? 0,
|
|
|
|
|
installedPowerPerUnitKw: parsed.data.installedPowerPerUnitKw ?? 0,
|
|
|
|
|
demandFactor: parsed.data.demandFactor ?? 1,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const [hasValidDistributionBoard, hasValidRoom, hasValidProjectDevice] = await Promise.all([
|
|
|
|
|
validateDistributionBoardOwnership(normalizedData.projectId, normalizedData.distributionBoardId),
|
|
|
|
|
validateRoomOwnership(normalizedData.projectId, normalizedData.roomId),
|
|
|
|
|
validateProjectDeviceOwnership(normalizedData.projectId, normalizedData.projectDeviceId),
|
|
|
|
|
]);
|
|
|
|
|
if (!hasValidDistributionBoard) {
|
|
|
|
|
return res
|
|
|
|
|
.status(400)
|
|
|
|
|
.json({ error: "Distribution board does not belong to the provided project." });
|
|
|
|
|
}
|
|
|
|
|
if (!hasValidRoom) {
|
|
|
|
|
return res.status(400).json({ error: "Room does not belong to the provided project." });
|
|
|
|
|
}
|
|
|
|
|
if (!hasValidProjectDevice) {
|
|
|
|
|
return res.status(400).json({ error: "Project device does not belong to the provided project." });
|
|
|
|
|
}
|
|
|
|
|
if (normalizedData.isLinkedToDevice && !normalizedData.projectDeviceId) {
|
|
|
|
|
return res.status(400).json({ error: "Linked entries require a projectDeviceId." });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const resolvedScope = await resolveCircuitScope({
|
|
|
|
|
projectId: normalizedData.projectId,
|
|
|
|
|
distributionBoardId: normalizedData.distributionBoardId,
|
|
|
|
|
circuitListId: normalizedData.circuitListId,
|
|
|
|
|
});
|
|
|
|
|
if (!resolvedScope.ok) {
|
|
|
|
|
return res.status(400).json({ error: resolvedScope.error });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const payload = await applyDeviceLinkIfNeeded({
|
|
|
|
|
...normalizedData,
|
|
|
|
|
distributionBoardId: resolvedScope.distributionBoardId,
|
|
|
|
|
circuitListId: resolvedScope.circuitListId,
|
|
|
|
|
description: normalizedData.description ?? normalizedData.name,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const created = await consumerRepository.create(payload);
|
|
|
|
|
const [project, floors, rooms] = await Promise.all([
|
|
|
|
|
projectRepository.findById(normalizedData.projectId),
|
|
|
|
|
floorRepository.listByProject(normalizedData.projectId),
|
|
|
|
|
roomRepository.listByProject(normalizedData.projectId),
|
|
|
|
|
]);
|
|
|
|
|
const roomById = new Map(
|
|
|
|
|
rooms.map((room) => [room.id, { floorId: room.floorId, roomName: room.roomName, roomNumber: room.roomNumber }])
|
|
|
|
|
);
|
|
|
|
|
const floorById = new Map(floors.map((floor) => [floor.id, { name: floor.name }]));
|
|
|
|
|
|
|
|
|
|
const enriched = powerBalanceService.enrichConsumer(
|
|
|
|
|
{
|
|
|
|
|
...(created as Consumer),
|
|
|
|
|
description: created.description ?? created.name,
|
|
|
|
|
roomNumber: created.roomId ? roomById.get(created.roomId)?.roomNumber : undefined,
|
|
|
|
|
roomName: created.roomId ? roomById.get(created.roomId)?.roomName : undefined,
|
|
|
|
|
floorId: created.roomId ? roomById.get(created.roomId)?.floorId ?? undefined : undefined,
|
|
|
|
|
floorName:
|
|
|
|
|
created.roomId && roomById.get(created.roomId)?.floorId
|
|
|
|
|
? floorById.get(roomById.get(created.roomId)!.floorId as string)?.name
|
|
|
|
|
: undefined,
|
|
|
|
|
},
|
|
|
|
|
project
|
|
|
|
|
? {
|
|
|
|
|
singlePhaseVoltageV: project.singlePhaseVoltageV,
|
|
|
|
|
threePhaseVoltageV: project.threePhaseVoltageV,
|
|
|
|
|
}
|
|
|
|
|
: undefined
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return res.status(201).json(enriched);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function updateConsumer(req: Request, res: Response) {
|
|
|
|
|
const { consumerId } = req.params;
|
|
|
|
|
if (typeof consumerId !== "string") {
|
|
|
|
|
return res.status(400).json({ error: "Invalid consumerId" });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const parsed = updateConsumerSchema.safeParse(req.body);
|
|
|
|
|
if (!parsed.success) {
|
|
|
|
|
return res.status(400).json({ error: parsed.error.flatten() });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const normalizedData: CreateConsumerInput = {
|
|
|
|
|
...parsed.data,
|
|
|
|
|
name: parsed.data.name?.trim() || "Unbenannter Eintrag",
|
|
|
|
|
quantity: parsed.data.quantity ?? 0,
|
|
|
|
|
installedPowerPerUnitKw: parsed.data.installedPowerPerUnitKw ?? 0,
|
|
|
|
|
demandFactor: parsed.data.demandFactor ?? 1,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const [hasValidDistributionBoard, hasValidRoom, hasValidProjectDevice] = await Promise.all([
|
|
|
|
|
validateDistributionBoardOwnership(normalizedData.projectId, normalizedData.distributionBoardId),
|
|
|
|
|
validateRoomOwnership(normalizedData.projectId, normalizedData.roomId),
|
|
|
|
|
validateProjectDeviceOwnership(normalizedData.projectId, normalizedData.projectDeviceId),
|
|
|
|
|
]);
|
|
|
|
|
if (!hasValidDistributionBoard) {
|
|
|
|
|
return res
|
|
|
|
|
.status(400)
|
|
|
|
|
.json({ error: "Distribution board does not belong to the provided project." });
|
|
|
|
|
}
|
|
|
|
|
if (!hasValidRoom) {
|
|
|
|
|
return res.status(400).json({ error: "Room does not belong to the provided project." });
|
|
|
|
|
}
|
|
|
|
|
if (!hasValidProjectDevice) {
|
|
|
|
|
return res.status(400).json({ error: "Project device does not belong to the provided project." });
|
|
|
|
|
}
|
|
|
|
|
if (normalizedData.isLinkedToDevice && !normalizedData.projectDeviceId) {
|
|
|
|
|
return res.status(400).json({ error: "Linked entries require a projectDeviceId." });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const resolvedScope = await resolveCircuitScope({
|
|
|
|
|
projectId: normalizedData.projectId,
|
|
|
|
|
distributionBoardId: normalizedData.distributionBoardId,
|
|
|
|
|
circuitListId: normalizedData.circuitListId,
|
|
|
|
|
});
|
|
|
|
|
if (!resolvedScope.ok) {
|
|
|
|
|
return res.status(400).json({ error: resolvedScope.error });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const payload = await applyDeviceLinkIfNeeded({
|
|
|
|
|
...normalizedData,
|
|
|
|
|
distributionBoardId: resolvedScope.distributionBoardId,
|
|
|
|
|
circuitListId: resolvedScope.circuitListId,
|
|
|
|
|
description: normalizedData.description ?? normalizedData.name,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await consumerRepository.update(consumerId, payload);
|
|
|
|
|
|
|
|
|
|
const row = await consumerRepository.findById(consumerId);
|
|
|
|
|
if (!row) {
|
|
|
|
|
return res.status(404).json({ error: "Consumer not found" });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const [project, floors, rooms] = await Promise.all([
|
|
|
|
|
projectRepository.findById(row.projectId),
|
|
|
|
|
floorRepository.listByProject(row.projectId),
|
|
|
|
|
roomRepository.listByProject(row.projectId),
|
|
|
|
|
]);
|
|
|
|
|
const roomById = new Map(
|
|
|
|
|
rooms.map((room) => [room.id, { floorId: room.floorId, roomName: room.roomName, roomNumber: room.roomNumber }])
|
|
|
|
|
);
|
|
|
|
|
const floorById = new Map(floors.map((floor) => [floor.id, { name: floor.name }]));
|
|
|
|
|
|
|
|
|
|
const enriched = powerBalanceService.enrichConsumer(
|
|
|
|
|
buildConsumerFromRow(row as ConsumerRow, roomById, floorById),
|
|
|
|
|
project
|
|
|
|
|
? {
|
|
|
|
|
singlePhaseVoltageV: project.singlePhaseVoltageV,
|
|
|
|
|
threePhaseVoltageV: project.threePhaseVoltageV,
|
|
|
|
|
}
|
|
|
|
|
: undefined
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return res.json(enriched);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function deleteConsumer(req: Request, res: Response) {
|
|
|
|
|
const { consumerId } = req.params;
|
|
|
|
|
if (typeof consumerId !== "string") {
|
|
|
|
|
return res.status(400).json({ error: "Invalid consumerId" });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await consumerRepository.delete(consumerId);
|
|
|
|
|
return res.status(204).send();
|
|
|
|
|
}
|