import type { Request, Response } from "express"; 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(); 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 { 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, floorById: Map ): 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(); }