diff --git a/compose.yaml b/compose.yaml index 071f0bb..6a5ec7c 100644 --- a/compose.yaml +++ b/compose.yaml @@ -20,7 +20,7 @@ services: max-size: "20m" max-file: "10" ports: - - "3221:3000" + - "3000:3000" volumes: - ./src:/app/src - ./scripts:/app/scripts @@ -64,7 +64,7 @@ services: api: condition: service_healthy ports: - - "3220:3001" + - "3001:3001" volumes: - ./src:/app/src - ./next.config.mjs:/app/next.config.mjs:ro @@ -80,9 +80,3 @@ services: timeout: 3s retries: 12 start_period: 20s - -networks: - default: - ipam: - config: - - subnet: 172.16.61.0/24 diff --git a/docs/cable-sizing-module.md b/docs/cable-sizing-module.md deleted file mode 100644 index 80b7cc8..0000000 --- a/docs/cable-sizing-module.md +++ /dev/null @@ -1,243 +0,0 @@ -# Cable Sizing Module (Proposal) - -**Status: proposal, not yet reviewed or merged by the project owner.** This -document and the accompanying `src/cable-sizing/`, `src/db/schema/cable-sizing-calculations.ts`, -`src/server/{controllers,routes}/cable-sizing.*` and `src/frontend/components/cable-sizing-*` -files were written by a third party (see git history/authorship) against the -project's own stated direction in `docs/spec/06-future-sizing-and-calculations.md` -("The app should later support rule-based protection and cable sizing") and -`AGENTS.md` ("... and later electrical sizing logic"). Nothing here has been -pushed to the project's own repository; it exists as a local branch for -review, testing and discussion. - -## Why a separate module instead of extending core domain code - -`AGENTS.md` is explicit that critical multi-write commands, the revision/undo -system and the Circuit-First domain model are the supported architecture, and -that changes should be small and reviewable. A cable-sizing calculation is -**not** a project mutation - it doesn't need `expectedRevision`, doesn't -belong in the undo/redo stack, and shouldn't grow the `circuit.update` -command's `switch` statement or the DTOs it doesn't already have. - -So this module is built exactly like the existing `src/external-model/` -adapter (the Revit/CSV import foundation): a one-way dependency boundary. - -``` -src/cable-sizing/domain/ <- pure functions and types, zero imports from - db/, server/ or frontend/ -src/db/schema/ <- one new, fully additive table -src/db/repositories/ <- one plain repository (no revision semantics) -src/server/{controllers,routes}/cable-sizing.* <- one new, isolated route - group, mounted with a single app.use() line -src/frontend/components/cable-sizing-* <- one new modal + one small - API client, following the existing - FormModal / CircuitProtectionModal pattern -``` - -Nothing outside these files imports from them except the two required -one-line hooks described below. If this module is rejected or needs to move -out again, removing it is a matter of deleting these files, the two -one-line hooks, and running a `DROP TABLE` migration - it never touches -`circuits`, `project_revisions` or any command/history table. - -## The two required hooks into existing code - -1. `src/server/index.ts`: one import + one `app.use("/api/cable-sizing", cableSizingRouter)` - line, next to the existing `app.use("/api/projects", ...)` etc. -2. `src/frontend/components/circuit-tree-editor.tsx`: one new `useState` for - the open modal, one new trigger condition (mirroring the existing - `isProtectionTrigger` / `protectionEditorCircuit` pattern almost exactly), - and one new conditionally-rendered `` alongside the - existing ``. See the diff for the exact lines. - -No changes to `circuit-project-command.model.ts`, `project-command.service.ts`, -any Zod command schema, any migration for `circuits`/`project_revisions`, or -any existing test. - -## Coupling to the grid UI (the one fragile point) - -Everything described above - the calculation, the audit table, applying a -result via `circuit.update` - depends only on stable, explicitly documented -domain fields (`cableType`/`cableCrossSection`/`cableLength`/ -`circuitTotalPower`/`voltage`/`protectionDevice.ratedCurrentA`), the same -ones `AGENTS.md` already treats as protected ("Protection and cable data -belong to the circuit"). None of that breaks if the grid UI changes. - -The one place that *is* coupled to a UI implementation detail: the click -trigger in `circuit-tree-editor.tsx` matches grid **column keys** -(`cableSummary` and `cableCrossSection`, defined in -`circuit-grid-model.ts`) to decide where to show the calculator icon. -Column visibility and order in this app are a per-browser user preference, -not fixed - so this is checked against both known cable-related columns to -reduce (not eliminate) the chance a user's personal column layout hides the -trigger entirely. If a future column rename removes both keys, the failure -mode is purely cosmetic: the calculator icon/click stops appearing on that -cell, nothing crashes, no data is affected, and the rest of the app -(including manual cable-field editing) is completely unaffected - the -module's actual logic and persistence never depend on this column key. - -## Feature overview - -- **Calculate a recommendation**: laying method, insulation, conductor - material, ambient temperature, grouping, cos phi and max voltage drop - - same VDE 0298-4 reference-method model as the sibling Kabelliste tool - (see Verification status below). -- **Breaker-aware sizing**: if the circuit already has a protection device - (`circuit.protectionDevice`), the cross-section is selected against - `designCurrentA = max(operatingCurrentA, protectionDevice.ratedCurrentA)`, - not the load current alone - the standard `In <= Iz` rule (a breaker only - trips at its own rated current, so a cable sized for the actual load - alone could overheat under sustained current below that threshold). If no - cross-section can cover an oversized breaker, that is reported as the - limiting factor by name, not a generic capacity error. -- **Maximum length for the voltage-drop limit**: each cross-section option - also reports the longest single run that still meets the requested - `maxVoltageDropPercent` at the given load - the inverse of the - voltage-drop check, shown for the recommended cross-section. -- **Practical minimum for socket circuits**: `single_phase`-category - circuits are raised to at least 2.5 mm² if the calculation alone would - recommend less - see the dedicated note under Verification status. Never - lowers a calculation that already needs more. -- **Manual entry, always available**: the modal has a plain text Kabeltyp/ - Querschnitt field. Running a calculation pre-fills it as a suggestion, - but the field is the single source of truth for what gets applied, and - stays freely editable - satisfying - `docs/spec/06-future-sizing-and-calculations.md`'s explicit requirement - that "users must remain able to manually override suggestions" without - requiring a calculation to have been run first. A non-blocking warning - appears if the typed value is not a standard cross-section, is smaller - than the last calculation, or is below the practical minimum above - - informational only, never forced, matching the same spec doc's "shown as - a warning or status indicator, not as an automatic forced change." - -## How a suggestion or manual entry is applied - -The module never writes to `circuits` directly. The modal's "Übernehmen" -button calls the **existing, unmodified** frontend helper with whatever is -currently in the Kabeltyp/Querschnitt fields, calculated or hand-typed: - -```ts -updateCircuitById(projectId, expectedRevision, circuitId, { - cableCrossSection: "4 mm²", - cableType: "NYM-J 3x2.5", - cableLength: 30, -}); -``` - -which is the same `circuit.update` command the grid already uses for manual -cell edits. This means: - -- optimistic concurrency (`expectedRevision`) is respected automatically -- the change appears in the project's revision history and is undoable/ - redoable exactly like a manual edit -- nothing is ever written automatically - calculating only fills the modal's - own fields, applying is always an explicit, separate click - -## API contract - -`POST /api/cable-sizing/calculate` - -```jsonc -{ - "phase": 1, // 1 | 3 - "mode": "power", // "power" | "current" - "powerKw": 4.2, // circuit.circuitTotalPower, when mode="power" - "cosPhi": 1, - "voltage": 230, // circuit.voltage (already project-derived, read-only) - "lengthM": 23.5, // circuit.cableLength - "layingMethod": "C", // DIN VDE 0298-4 reference method, A1|A2|B1|B2|C|D1|D2|E|F|G - "conductorMaterial": "copper", // "copper" | "aluminum" - "insulation": "pvc", // "pvc" | "xlpe" - "ambientTemperatureC": 30, - "groupingCircuits": 1, - "maxVoltageDropPercent": 3, - "harmonicNeutralLoad": "none", // "none" | "15to33Percent" | "over33Percent", three-phase only - "existingProtectionRatedCurrentA": 16, // optional, from circuit.protectionDevice - "circuitCategory": "single_phase", // optional, from the circuit's section.category - - // enables the practical-minimum convention below - "context": { "projectId": "...", "circuitId": "...", "equipmentIdentifier": "-1F1.1" } -} -``` - -Response: `{ calculationId, result: CableSizingResult, alerts: CableSizingAlert[] }` -(see `src/cable-sizing/domain/cable-sizing-calculation.ts` for the exact -shape). `GET /api/cable-sizing/laying-methods` and `.../insulation-materials` -expose the pick-list metadata (including which combinations are verified) so -a future non-modal UI could build its own form without hard-coding the enum. - -## Verification status - important - -The reference current-carrying-capacity tables are ported from a sibling -project's Kabelliste module, which was itself only verified against ~8 -public sources for **six** of the ten DIN VDE 0298-4 reference laying -methods (A1, B2, C, E, D1, D2) and **PVC insulation only**. The other four -methods (A2, B1, F, G) and XLPE/VPE insulation could not be verified without -contradiction across sources during that earlier work, so -`calculateCableSizing` deliberately returns `dataVerified: false` and no -numeric recommendation for those combinations, rather than a guessed value. -The UI surfaces this as a plain critical alert. Anyone with access to the -actual norm text can extend `LAYING_METHOD_VERIFIED` and -`CURRENT_CAPACITY_A` in `src/cable-sizing/domain/cable-sizing-calculation.ts` -once the missing tables are confirmed. - -The protection-coordination check (`existingProtectionRatedCurrentA` vs. the -recommended cross-section's corrected capacity) is a **simplified** `In <= -Iz` check only - it is not a full IEC 60364-4-43 overload (`I2 <= 1.45 x -Iz`) or short-circuit withstand check. - -`PRACTICAL_MINIMUM_CROSS_SECTION_MM2` (2.5 mm² for `single_phase`) is -**not** a verified norm value at all - it is a named planning convention, -sourced directly from this project's own -`docs/spec/06-future-sizing-and-calculations.md` ("Standard Single-Phase -Circuits ... usually use ... cable cross-section: 2.5 mm²"), applied as a -floor on top of the calculated recommendation. It intentionally only -covers the one category and one convention that document already states; -it is not a general substitute for norm-compliant calculation. - -## Relationship to the project's own future-sizing spec - -`docs/spec/06-future-sizing-and-calculations.md` separately describes simple -category-based defaults (e.g. lighting circuits -> 10 A / 1.5 mm²) as -"common planning defaults, not a replacement for full norm-compliant -calculation". This module is the latter: an on-demand, norm-referenced -calculation for one circuit at a time, not a bulk default-filling tool. The -two are complementary and could later be wired together (e.g. the category -defaults pre-fill this module's laying-method/insulation fields), but that -integration is out of scope here. - -The spec's `isPublicBuilding` halogen-free rule and the `Control Requirement` -(DALI/KNX/core-count) field are **not** implemented by this module - both -concern cable *type* selection, not cross-section sizing, and the latter -isn't in the current schema yet. They're natural follow-ups once this module -is reviewed. - -## Maintenance plan - -The `src/cable-sizing/domain/` folder has zero imports from the rest of the -app by design (see the dependency-direction note above), so it can be -extracted into an independently versioned/published package later (e.g. a -private npm package or a git subtree) without touching anything outside the -five files/folders listed above - "separately maintained but part of the -app for now", per the intent of this proposal. - -## Local testing - -Deployed and manually exercised end-to-end on an internal test host at -`http://192.168.0.133:3220` (own docker deployment, own subnet/ports to -avoid clashing with ~50 other containers on that host - see the two -commits marked "Local-only" in this branch, which are not part of this -proposal and should not be carried over if it is ever proposed upstream). -Exercised against seeded real project/circuit data created through the -actual command API (not direct DB writes), including a deliberately long -(85 m) circuit to trigger the voltage-drop-critical path. - -`npm test`, `npm run build:api`, `npm run build:web` and -`npm run typecheck:scripts` all pass with this module included at every -commit in this branch. - -This branch (`feature/cable-sizing-module`) is pushed to a mirror of this -repository under this homelab's own Forgejo instance, not to -`git.jappel.io` - no write access to the upstream repository was available -or used. See the branch's commit history for the incremental history of -this module, including fixes made during manual testing (trigger column, -icon rendering, breaker-aware sizing). diff --git a/next.config.mjs b/next.config.mjs index 486b23f..e0610b9 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -5,8 +5,7 @@ const nextConfig = { allowedDevOrigins: [ "192.168.3.13", "docker01.int.jappel.io", - "lb.jappel.io", - "192.168.0.133" + "lb.jappel.io" ], typescript: { diff --git a/src/app/globals.css b/src/app/globals.css index 88e1c35..22da5df 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1209,48 +1209,6 @@ a.kpi:hover { box-shadow: inset 0 0 0 1px var(--color-signal); } -.tree-grid .cell-cable-sizing-trigger { - color: var(--color-primary); - cursor: pointer; - font-weight: 600; - transition: - background-color 0.12s ease, - box-shadow 0.12s ease; -} - -:root[data-bs-theme="dark"] .tree-grid .cell-cable-sizing-trigger { - color: var(--color-accent-pale); -} - -.tree-grid .cell-cable-sizing-trigger:hover { - background: rgba(63, 130, 166, 0.15); - box-shadow: inset 0 0 0 1px var(--color-primary); -} - -.tree-grid .cell-cable-sizing-trigger { - position: relative; - padding-left: 1.4em; -} - -.tree-grid .cell-cable-sizing-trigger::before { - content: ""; - position: absolute; - left: 0.2em; - top: 50%; - transform: translateY(-50%); - width: 0.9em; - height: 0.9em; - opacity: 0.85; - background-repeat: no-repeat; - background-position: center; - background-size: contain; - background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%233f82a6' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='4' y='2' width='16' height='20' rx='2'/%3E%3Cline x1='8' y1='6' x2='16' y2='6'/%3E%3Cline x1='8' y1='10' x2='8' y2='10.01'/%3E%3Cline x1='12' y1='10' x2='12' y2='10.01'/%3E%3Cline x1='16' y1='10' x2='16' y2='10.01'/%3E%3Cline x1='8' y1='14' x2='8' y2='14.01'/%3E%3Cline x1='12' y1='14' x2='12' y2='14.01'/%3E%3Cline x1='16' y1='14' x2='16' y2='14.01'/%3E%3Cline x1='8' y1='18' x2='8' y2='18.01'/%3E%3Cline x1='12' y1='18' x2='12' y2='18.01'/%3E%3Cline x1='16' y1='18' x2='16' y2='18.01'/%3E%3C/svg%3E"); -} - -:root[data-bs-theme="dark"] .tree-grid .cell-cable-sizing-trigger::before { - background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23a9d2e6' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='4' y='2' width='16' height='20' rx='2'/%3E%3Cline x1='8' y1='6' x2='16' y2='6'/%3E%3Cline x1='8' y1='10' x2='8' y2='10.01'/%3E%3Cline x1='12' y1='10' x2='12' y2='10.01'/%3E%3Cline x1='16' y1='10' x2='16' y2='10.01'/%3E%3Cline x1='8' y1='14' x2='8' y2='14.01'/%3E%3Cline x1='12' y1='14' x2='12' y2='14.01'/%3E%3Cline x1='16' y1='14' x2='16' y2='14.01'/%3E%3Cline x1='8' y1='18' x2='8' y2='18.01'/%3E%3Cline x1='12' y1='18' x2='12' y2='18.01'/%3E%3Cline x1='16' y1='18' x2='16' y2='18.01'/%3E%3C/svg%3E"); -} - .tree-grid .device-drag-handle { cursor: grab; } diff --git a/src/cable-sizing/domain/cable-sizing-calculation.ts b/src/cable-sizing/domain/cable-sizing-calculation.ts deleted file mode 100644 index 3f228f4..0000000 --- a/src/cable-sizing/domain/cable-sizing-calculation.ts +++ /dev/null @@ -1,547 +0,0 @@ -// Pure cable sizing domain logic. No imports from db/, server/ or frontend/ - -// mirrors the dependency direction of src/external-model/ (see -// docs/cable-sizing-module.md). Everything here is a stateless function on -// plain data; persistence and the versioned circuit.update command live -// outside this module. -// -// The numeric reference tables below (cross-section current-carrying -// capacity, temperature/grouping correction factors) are ported from a -// sibling project's already-verified Kabelliste module -// (elt-planung-suite/src/tools/kabel/index.ts), not re-derived. See -// docs/cable-sizing-module.md for the verification history and the explicit -// "not yet verified" methods, which intentionally return no numeric result -// instead of a guessed one (dataVerified: false). - -// -- Laying methods (DIN VDE 0298-4 reference installation methods) -------- - -export const LAYING_METHODS = [ - "A1", - "A2", - "B1", - "B2", - "C", - "D1", - "D2", - "E", - "F", - "G", -] as const; -export type LayingMethod = (typeof LAYING_METHODS)[number]; - -export const LAYING_METHOD_GROUP: Record = { - A1: "air", - A2: "air", - B1: "air", - B2: "air", - C: "air", - E: "air", - F: "air", - D1: "ground", - D2: "ground", - G: "ground", -}; - -// A1, B2, C, E, D1, D2: verified against elt-planung-suite's ported original -// tool. A2, B1, F, G: no verified capacity table available yet (see -// docs/cable-sizing-module.md) - calculateCableSizing returns -// dataVerified: false for these instead of a guessed number. -export const LAYING_METHOD_VERIFIED: Record = { - A1: true, - B2: true, - C: true, - E: true, - D1: true, - D2: true, - A2: false, - B1: false, - F: false, - G: false, -}; - -export const LAYING_METHOD_LABELS: Record = { - A1: "A1 - Rohr in wärmegedämmter Wand", - A2: "A2 - Mehradriges Kabel im Rohr in wärmegedämmter Wand (Werte nicht hinterlegt)", - B1: "B1 - Einzeladern im Rohr auf/unter Putz (Werte nicht hinterlegt)", - B2: "B2 - Rohr auf/unter Putz", - C: "C - Kabel direkt auf Wand verlegt", - E: "E - Kabelpritsche / frei in Luft", - F: "F - Kabel frei in Luft, einzeln mit Abstand (Werte nicht hinterlegt)", - D1: "D1 - Kabel direkt im Erdreich", - D2: "D2 - Kabel im Rohr im Erdreich", - G: "G - Erdverlegung, Sonderfall (Werte nicht hinterlegt)", -}; - -// -- Insulation / conductor material ---------------------------------------- - -export const INSULATION_MATERIALS = ["pvc", "xlpe"] as const; -export type InsulationMaterial = (typeof INSULATION_MATERIALS)[number]; - -export const INSULATION_MATERIAL_LABELS: Record = { - pvc: "PVC (Grenztemperatur 70 °C)", - xlpe: "VPE/XLPE (Grenztemperatur 90 °C) - Strombelastbarkeitswerte nicht hinterlegt", -}; - -export const CONDUCTOR_MATERIALS = ["copper", "aluminum"] as const; -export type ConductorMaterial = (typeof CONDUCTOR_MATERIALS)[number]; - -// Only PVC combined with one of the six verified laying methods returns a -// numeric result. XLPE has no verified capacity table for any method yet. -export function isCableSizingDataVerified( - layingMethod: LayingMethod, - insulation: InsulationMaterial -): boolean { - return insulation === "pvc" && LAYING_METHOD_VERIFIED[layingMethod]; -} - -// -- Harmonic neutral loading (IEC 60364-5-52, three-phase only) ----------- - -export const HARMONIC_NEUTRAL_LOAD_OPTIONS = [ - "none", - "15to33Percent", - "over33Percent", -] as const; -export type HarmonicNeutralLoad = (typeof HARMONIC_NEUTRAL_LOAD_OPTIONS)[number]; - -export const HARMONIC_NEUTRAL_LOAD_LABELS: Record = { - none: "Keine nennenswerte 3. Harmonische (< 15 %) - Standardfall", - "15to33Percent": - "3. Harmonische 15-33 % - Reduktionsfaktor 0,86 (IEC 60364-5-52)", - over33Percent: - "3. Harmonische > 33 % - Neutralleiter wie Außenleiter dimensionieren", -}; - -export const HARMONIC_REDUCTION_FACTOR = 0.86; - -// -- Reference tables (ported, verified subset only) ------------------------ - -export const CROSS_SECTIONS_MM2 = [ - 1.5, 2.5, 4, 6, 10, 16, 25, 35, 50, 70, 95, 120, 150, 185, 240, -] as const; - -type CoreCount = 2 | 3; - -export const CURRENT_CAPACITY_A: Partial< - Record> -> = { - A1: { - 2: [15.5, 21, 28, 36, 50, 68, 89, 110, 134, 171, 207, 239, null, null, null], - 3: [13.5, 18, 24, 31, 42, 56, 73, 89, 108, 136, 164, 188, null, null, null], - }, - B2: { - 2: [16.5, 23, 30, 38, 52, 69, 90, 111, 134, 171, 207, 239, null, null, null], - 3: [15, 20, 27, 34, 46, 62, 80, 99, 118, 149, 179, 206, null, null, null], - }, - C: { - 2: [19.5, 27, 36, 46, 63, 85, 112, 138, 168, 213, 258, 299, 344, 392, 461], - 3: [17.5, 24, 32, 41, 57, 76, 96, 119, 144, 184, 223, 259, 299, 341, 403], - }, - E: { - 2: [22, 30, 40, 51, 70, 94, 119, 148, 180, 232, 282, 328, 379, 434, 514], - 3: [18.5, 25, 34, 43, 60, 80, 101, 126, 153, 196, 238, 276, 319, 364, 430], - }, - D1: { - 2: [26, 34, 44, 56, 74, 96, 123, 147, 174, 216, 256, 290, 328, 367, 424], - 3: [22, 29, 38, 47, 63, 81, 104, 125, 148, 183, 216, 246, 278, 312, 361], - }, - D2: { - 2: [22, 29, 38, 47, 63, 81, 104, 125, 148, 183, 216, 246, 278, 312, 360], - 3: [18.5, 24, 31, 39, 52, 67, 86, 103, 122, 151, 179, 203, 230, 258, 297], - }, -}; - -export const TEMPERATURE_FACTOR_AIR: Record = { - 10: 1.29, - 15: 1.22, - 20: 1.15, - 25: 1.08, - 30: 1.0, - 35: 0.91, - 40: 0.82, - 45: 0.71, - 50: 0.58, -}; - -export const TEMPERATURE_FACTOR_GROUND: Record = { - 10: 1.1, - 15: 1.05, - 20: 1.0, - 25: 0.95, - 30: 0.89, - 35: 0.84, - 40: 0.77, -}; - -export const GROUPING_FACTOR_AIR: Record = { - 1: 1.0, - 2: 0.8, - 3: 0.7, - 4: 0.65, - 5: 0.6, - 6: 0.57, - 8: 0.54, - 10: 0.5, -}; - -export const GROUPING_FACTOR_GROUND: Record = { - 1: 1.0, - 2: 0.75, - 3: 0.65, - 4: 0.6, - 5: 0.55, - 6: 0.52, - 8: 0.49, - 10: 0.46, -}; - -export const ALUMINUM_CAPACITY_FACTOR = 0.78; -export const KAPPA: Record = { copper: 48, aluminum: 30 }; - -export const GROUPING_OPTIONS = [1, 2, 3, 4, 5, 6, 8, 10] as const; - -export const GROUPING_LABELS: Record = { - 1: "1 (keine Häufung)", - 2: "2", - 3: "3", - 4: "4", - 5: "5", - 6: "6", - 8: "7-9", - 10: "≥10", -}; - -// -- Practical minimum cross-sections by circuit category ------------------ -// Not a thermal/voltage-drop calculation result - a widely used planning -// convention for margin against future load growth, mechanical robustness -// and fault-current withstand, beyond what a pure Ib-based calculation -// would give. Matches this project's own docs/spec/06-future-sizing-and- -// calculations.md ("Standard Single-Phase Circuits ... usually use ... -// cable cross-section: 2.5 mm²"). Only applied for the "single_phase" -// circuit category (general sockets and similar loads); lighting and -// three-phase circuits are not covered by this specific convention. -export const PRACTICAL_MINIMUM_CROSS_SECTION_MM2: Partial> = { - single_phase: 2.5, -}; - -export type CircuitCategory = "lighting" | "single_phase" | "three_phase"; - -// -- Input / result ----------------------------------------------------- - -export interface CableSizingInput { - phase: 1 | 3; - mode: "power" | "current"; - /** kW, only used when mode === "power" */ - powerKw?: number; - /** A, only used when mode === "current" */ - currentA?: number; - cosPhi: number; - /** Line voltage in V. The circuit-list app derives this from project - * settings (singlePhaseVoltageV / threePhaseVoltageV); pass it through - * rather than hard-coding 230/400 here. */ - voltage: number; - /** Single-run cable length in meters (circuits.cableLength). */ - lengthM: number; - layingMethod: LayingMethod; - conductorMaterial: ConductorMaterial; - insulation: InsulationMaterial; - ambientTemperatureC: number; - groupingCircuits: number; - maxVoltageDropPercent: number; - harmonicNeutralLoad: HarmonicNeutralLoad; - /** Optional: existing protection device rated current (LS/circuit - * breaker), for the simplified coordination check below. */ - existingProtectionRatedCurrentA?: number; - /** Optional: enables the practical-minimum-cross-section convention for - * "single_phase" circuits, see PRACTICAL_MINIMUM_CROSS_SECTION_MM2. */ - circuitCategory?: CircuitCategory; -} - -export interface CrossSectionRow { - crossSectionMm2: number; - ratedCurrentA: number | null; - correctedCurrentA: number | null; - currentSufficient: boolean | null; - voltageDropPercent: number | null; - voltageDropSufficient: boolean | null; - /** Longest single-run length (m) at which this cross-section still meets - * the requested max voltage drop, at the given load/cosPhi/phase - the - * inverse of the voltage-drop formula used for voltageDropPercent above. - * null when the operating current is zero (division by zero). */ - maxLengthForVoltageDropM: number | null; - recommended: boolean; -} - -export interface ProtectionCoordinationResult { - ratedCurrentA: number; - /** Simplified In <= Iz check only - not full overload (I2 <= 1.45 x Iz) - * or short-circuit coordination. */ - coordinated: boolean; -} - -export interface CableSizingResult { - /** false: no verified capacity table for this layingMethod/insulation - * combination - every field below is null/empty, never a guess. */ - dataVerified: boolean; - operatingCurrentA: number; - /** max(operatingCurrentA, existingProtectionRatedCurrentA). The cable - * capacity (Iz) must cover the protective device's rated current (In), - * not just the actual load current - a breaker only trips at In, so a - * cable sized for Ib alone could overheat under sustained load below the - * trip threshold. This is the value cross-section selection actually - * uses; operatingCurrentA is kept for display/voltage-drop only. */ - designCurrentA: number; - crossSectionByCapacityMm2: number | null; - crossSectionByVoltageDropMm2: number | null; - recommendedCrossSectionMm2: number | null; - voltageDropAtRecommendedPercent: number | null; - combinedDerationFactor: number; - harmonicReductionApplied: boolean; - /** true if the recommendation was raised to satisfy - * PRACTICAL_MINIMUM_CROSS_SECTION_MM2 (a convention, not a thermal/ - * voltage-drop requirement of this specific circuit). */ - practicalMinimumApplied: boolean; - rows: CrossSectionRow[]; - protectionCoordination: ProtectionCoordinationResult | null; -} - -function operatingCurrentA(input: CableSizingInput): number { - if (input.mode === "current") { - return input.currentA ?? 0; - } - const powerW = (input.powerKw ?? 0) * 1000; - return input.phase === 1 - ? powerW / (input.voltage * input.cosPhi) - : powerW / (Math.sqrt(3) * input.voltage * input.cosPhi); -} - -export function calculateCableSizing(input: CableSizingInput): CableSizingResult { - const ib = operatingCurrentA(input); - - const designCurrentA = - input.existingProtectionRatedCurrentA != null - ? Math.max(ib, input.existingProtectionRatedCurrentA) - : ib; - - if (!isCableSizingDataVerified(input.layingMethod, input.insulation)) { - return { - dataVerified: false, - operatingCurrentA: ib, - designCurrentA, - crossSectionByCapacityMm2: null, - crossSectionByVoltageDropMm2: null, - recommendedCrossSectionMm2: null, - voltageDropAtRecommendedPercent: null, - combinedDerationFactor: 1, - harmonicReductionApplied: false, - practicalMinimumApplied: false, - rows: [], - protectionCoordination: null, - }; - } - - const coreCount: CoreCount = input.phase === 1 ? 2 : 3; - const aluminumFactor = - input.conductorMaterial === "aluminum" ? ALUMINUM_CAPACITY_FACTOR : 1; - const kappa = KAPPA[input.conductorMaterial]; - const group = LAYING_METHOD_GROUP[input.layingMethod]; - const temperatureTable = - group === "air" ? TEMPERATURE_FACTOR_AIR : TEMPERATURE_FACTOR_GROUND; - const groupingTable = group === "air" ? GROUPING_FACTOR_AIR : GROUPING_FACTOR_GROUND; - const temperatureFactor = temperatureTable[input.ambientTemperatureC] ?? 1; - const groupingFactor = groupingTable[input.groupingCircuits] ?? 1; - - const harmonicReductionApplied = - input.phase === 3 && input.harmonicNeutralLoad === "15to33Percent"; - const harmonicFactor = harmonicReductionApplied ? HARMONIC_REDUCTION_FACTOR : 1; - - const combinedDerationFactor = temperatureFactor * groupingFactor * harmonicFactor; - const voltageDropCoefficient = input.phase === 1 ? 2 : Math.sqrt(3); - - const ratedCurrents = CURRENT_CAPACITY_A[input.layingMethod]![coreCount]; - const correctedCurrents = ratedCurrents.map((value) => - value == null ? null : value * aluminumFactor * combinedDerationFactor - ); - const voltageDrops = CROSS_SECTIONS_MM2.map( - (crossSection) => - ((voltageDropCoefficient * input.lengthM * ib * input.cosPhi) / - (kappa * crossSection) / - input.voltage) * - 100 - ); - - const indexByCapacity = correctedCurrents.findIndex( - (value) => value != null && value >= designCurrentA - ); - const indexByVoltageDrop = voltageDrops.findIndex( - (value) => value <= input.maxVoltageDropPercent - ); - let indexRecommended = -1; - if (indexByCapacity >= 0 && indexByVoltageDrop >= 0) { - indexRecommended = Math.max(indexByCapacity, indexByVoltageDrop); - } - - // Practical-minimum convention (see PRACTICAL_MINIMUM_CROSS_SECTION_MM2): - // only ever raises the recommendation, never lowers what the thermal/ - // voltage-drop calculation already required. - const practicalMinimumMm2 = input.circuitCategory - ? PRACTICAL_MINIMUM_CROSS_SECTION_MM2[input.circuitCategory] - : undefined; - let practicalMinimumApplied = false; - if ( - indexRecommended >= 0 && - practicalMinimumMm2 != null && - CROSS_SECTIONS_MM2[indexRecommended] < practicalMinimumMm2 - ) { - const minimumIndex = CROSS_SECTIONS_MM2.indexOf( - practicalMinimumMm2 as (typeof CROSS_SECTIONS_MM2)[number] - ); - if (minimumIndex >= 0) { - indexRecommended = minimumIndex; - practicalMinimumApplied = true; - } - } - - const voltageDropCurrentBasis = ib * input.cosPhi; - const rows: CrossSectionRow[] = CROSS_SECTIONS_MM2.map((crossSection, i) => { - const rated = ratedCurrents[i]; - const corrected = correctedCurrents[i]; - const currentSufficient = corrected == null ? null : corrected >= designCurrentA; - const voltageDropSufficient = voltageDrops[i] <= input.maxVoltageDropPercent; - const maxLengthForVoltageDropM = - rated == null || voltageDropCurrentBasis <= 0 - ? null - : (input.maxVoltageDropPercent * kappa * crossSection * input.voltage) / - (100 * voltageDropCoefficient * voltageDropCurrentBasis); - return { - crossSectionMm2: crossSection, - ratedCurrentA: rated, - correctedCurrentA: corrected, - currentSufficient, - voltageDropPercent: rated == null ? null : voltageDrops[i], - voltageDropSufficient: rated == null ? null : voltageDropSufficient, - maxLengthForVoltageDropM, - recommended: i === indexRecommended, - }; - }); - - let protectionCoordination: ProtectionCoordinationResult | null = null; - if (input.existingProtectionRatedCurrentA != null && indexRecommended >= 0) { - const recommendedRow = rows[indexRecommended]; - protectionCoordination = { - ratedCurrentA: input.existingProtectionRatedCurrentA, - coordinated: - recommendedRow.correctedCurrentA != null && - input.existingProtectionRatedCurrentA <= recommendedRow.correctedCurrentA, - }; - } - - return { - dataVerified: true, - operatingCurrentA: ib, - designCurrentA, - crossSectionByCapacityMm2: - indexByCapacity >= 0 ? CROSS_SECTIONS_MM2[indexByCapacity] : null, - crossSectionByVoltageDropMm2: - indexByVoltageDrop >= 0 ? CROSS_SECTIONS_MM2[indexByVoltageDrop] : null, - recommendedCrossSectionMm2: - indexRecommended >= 0 ? CROSS_SECTIONS_MM2[indexRecommended] : null, - voltageDropAtRecommendedPercent: - indexRecommended >= 0 ? voltageDrops[indexRecommended] : null, - combinedDerationFactor, - harmonicReductionApplied, - practicalMinimumApplied, - rows, - protectionCoordination, - }; -} - -export interface CableSizingAlert { - kind: "critical" | "warn" | "info" | "ok"; - text: string; -} - -export function buildCableSizingAlerts( - input: CableSizingInput, - result: CableSizingResult -): CableSizingAlert[] { - const alerts: CableSizingAlert[] = []; - - if (!result.dataVerified) { - alerts.push({ - kind: "critical", - text: `Für Verlegeart ${input.layingMethod} mit Isolierstoff ${input.insulation.toUpperCase()} liegen keine geprüften Strombelastbarkeits-Tabellenwerte vor - keine Dimensionierung möglich.`, - }); - return alerts; - } - - const byCapacity = result.crossSectionByCapacityMm2; - const byDrop = result.crossSectionByVoltageDropMm2; - const recommended = result.recommendedCrossSectionMm2; - - if (byCapacity == null) { - const dueToBreaker = - input.existingProtectionRatedCurrentA != null && - input.existingProtectionRatedCurrentA > result.operatingCurrentA; - alerts.push({ - kind: "critical", - text: dueToBreaker - ? `Vorhandene Sicherung (${input.existingProtectionRatedCurrentA} A) übersteigt die Belastbarkeit aller Standardquerschnitte bei Verlegeart ${input.layingMethod} - nicht der Betriebsstrom (${result.operatingCurrentA.toFixed(1)} A). Größere Verlegeart oder kleinere Sicherung prüfen.` - : `Betriebsstrom ${result.operatingCurrentA.toFixed(1)} A übersteigt die Belastbarkeit aller Standardquerschnitte bei Verlegeart ${input.layingMethod}.`, - }); - } else if (byDrop != null && byDrop > byCapacity) { - alerts.push({ - kind: "warn", - text: `Spannungsfall ist maßgeblich, nicht die Belastbarkeit: ${byCapacity} mm² würde thermisch reichen, ${byDrop} mm² ist wegen ΔU ≤ ${input.maxVoltageDropPercent}% nötig.`, - }); - } - if (result.harmonicReductionApplied) { - alerts.push({ - kind: "info", - text: "Reduktionsfaktor 0,86 wegen Oberschwingungsanteil 15-33 % im Neutralleiter angewendet (IEC 60364-5-52).", - }); - } - if (input.phase === 3 && input.harmonicNeutralLoad === "over33Percent") { - alerts.push({ - kind: "warn", - text: "3. Harmonische > 33 %: Neutralleiter muss wie ein Außenleiter dimensioniert werden - hier nicht automatisch berücksichtigt.", - }); - } - if (result.practicalMinimumApplied) { - alerts.push({ - kind: "info", - text: `Auf ${result.recommendedCrossSectionMm2} mm² angehoben (Praxis-Mindestquerschnitt für 1-phasige Stromkreise, keine reine Berechnungsanforderung).`, - }); - } - if (result.combinedDerationFactor < 1) { - alerts.push({ - kind: "info", - text: `Korrekturfaktor angewendet: ${result.combinedDerationFactor.toFixed(2)} (Temperatur × Häufung${result.harmonicReductionApplied ? " × Oberschwingungen" : ""}).`, - }); - } - if (result.protectionCoordination) { - alerts.push( - result.protectionCoordination.coordinated - ? { - kind: "info", - text: `Vorhandene Sicherung (${result.protectionCoordination.ratedCurrentA} A) ist bei der Dimensionierung berücksichtigt (In ≤ Iz, vereinfachte Prüfung, ersetzt keine vollständige Überlast-/Kurzschlussprüfung).`, - } - : { - kind: "warn", - text: `Vorhandene Sicherung (${result.protectionCoordination.ratedCurrentA} A) übersteigt die Belastbarkeit des empfohlenen Querschnitts - Koordination prüfen.`, - } - ); - } - if (alerts.length === 0 && recommended != null) { - alerts.push({ - kind: "ok", - text: `${recommended} mm² (${input.conductorMaterial === "aluminum" ? "Alu" : "Cu"}) deckt bei ${input.lengthM} m Länge sowohl Belastbarkeit (${result.designCurrentA.toFixed(1)} A${input.existingProtectionRatedCurrentA != null ? ", inkl. vorhandener Sicherung" : ""}) als auch Spannungsfall (≤ ${input.maxVoltageDropPercent}%) ab.`, - }); - } - const recommendedRow = result.rows.find((row) => row.recommended); - if (recommendedRow?.maxLengthForVoltageDropM != null) { - alerts.push({ - kind: "info", - text: `Maximale Länge bei ${recommendedRow.crossSectionMm2} mm² und ΔU ≤ ${input.maxVoltageDropPercent}%: ${recommendedRow.maxLengthForVoltageDropM.toFixed(0)} m.`, - }); - } - return alerts; -} diff --git a/src/cable-sizing/domain/cable-sizing-contracts.ts b/src/cable-sizing/domain/cable-sizing-contracts.ts deleted file mode 100644 index 5648d8d..0000000 --- a/src/cable-sizing/domain/cable-sizing-contracts.ts +++ /dev/null @@ -1,49 +0,0 @@ -// Request/response validation for the cable-sizing HTTP API. Pure - depends -// only on zod and the sibling calculation module, never on db/server. -import { z } from "zod"; -import { - CONDUCTOR_MATERIALS, - HARMONIC_NEUTRAL_LOAD_OPTIONS, - INSULATION_MATERIALS, - LAYING_METHODS, - type CableSizingInput, -} from "./cable-sizing-calculation.js"; - -export const cableSizingRequestSchema = z - .object({ - phase: z.union([z.literal(1), z.literal(3)]), - mode: z.enum(["power", "current"]), - powerKw: z.number().min(0).optional(), - currentA: z.number().min(0).optional(), - cosPhi: z.number().min(0.6).max(1), - voltage: z.number().positive(), - lengthM: z.number().min(0), - layingMethod: z.enum(LAYING_METHODS), - conductorMaterial: z.enum(CONDUCTOR_MATERIALS), - insulation: z.enum(INSULATION_MATERIALS), - ambientTemperatureC: z.number(), - groupingCircuits: z.number().int().min(1), - maxVoltageDropPercent: z.number().min(1).max(5), - harmonicNeutralLoad: z.enum(HARMONIC_NEUTRAL_LOAD_OPTIONS), - existingProtectionRatedCurrentA: z.number().positive().optional(), - circuitCategory: z.enum(["lighting", "single_phase", "three_phase"]).optional(), - // Optional context, persisted with the audit-log entry only - never - // used for the calculation itself. - context: z - .object({ - projectId: z.string().min(1).optional(), - circuitId: z.string().min(1).optional(), - equipmentIdentifier: z.string().min(1).optional(), - }) - .optional(), - }) - .refine((v) => (v.mode === "power" ? v.powerKw != null : v.currentA != null), { - message: "powerKw is required for mode 'power', currentA for mode 'current'", - }); - -export type CableSizingRequest = z.infer; - -// Structural check that the request schema stays a superset of the domain -// input shape (minus context, which is API-only metadata). -type _AssertAssignable = T; -type _Check = _AssertAssignable>; diff --git a/src/db/migrations/0007_watery_kingpin.sql b/src/db/migrations/0007_watery_kingpin.sql deleted file mode 100644 index a4f22bf..0000000 --- a/src/db/migrations/0007_watery_kingpin.sql +++ /dev/null @@ -1,14 +0,0 @@ -CREATE TABLE `cable_sizing_calculations` ( - `id` text PRIMARY KEY NOT NULL, - `project_id` text, - `circuit_id` text, - `equipment_identifier` text, - `input` text NOT NULL, - `result` text NOT NULL, - `applied_to_circuit` integer DEFAULT 0 NOT NULL, - `created_at` integer DEFAULT (unixepoch()) NOT NULL, - FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE set null, - FOREIGN KEY (`circuit_id`) REFERENCES `circuits`(`id`) ON UPDATE no action ON DELETE set null -); ---> statement-breakpoint -CREATE INDEX `cable_sizing_calculations_circuit_id_idx` ON `cable_sizing_calculations` (`circuit_id`); \ No newline at end of file diff --git a/src/db/migrations/meta/0007_snapshot.json b/src/db/migrations/meta/0007_snapshot.json deleted file mode 100644 index 8a67832..0000000 --- a/src/db/migrations/meta/0007_snapshot.json +++ /dev/null @@ -1,2522 +0,0 @@ -{ - "version": "6", - "dialect": "sqlite", - "id": "98546058-8ad8-4aa1-99ad-f4e915bea89c", - "prevId": "b25e1a40-c400-4848-bc24-a04bda7a8209", - "tables": { - "cable_sizing_calculations": { - "name": "cable_sizing_calculations", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "circuit_id": { - "name": "circuit_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "equipment_identifier": { - "name": "equipment_identifier", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "input": { - "name": "input", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "result": { - "name": "result", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "applied_to_circuit": { - "name": "applied_to_circuit", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch())" - } - }, - "indexes": { - "cable_sizing_calculations_circuit_id_idx": { - "name": "cable_sizing_calculations_circuit_id_idx", - "columns": [ - "circuit_id" - ], - "isUnique": false - } - }, - "foreignKeys": { - "cable_sizing_calculations_project_id_projects_id_fk": { - "name": "cable_sizing_calculations_project_id_projects_id_fk", - "tableFrom": "cable_sizing_calculations", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "cable_sizing_calculations_circuit_id_circuits_id_fk": { - "name": "cable_sizing_calculations_circuit_id_circuits_id_fk", - "tableFrom": "cable_sizing_calculations", - "tableTo": "circuits", - "columnsFrom": [ - "circuit_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuit_device_rows": { - "name": "circuit_device_rows", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_id": { - "name": "circuit_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "linked_project_device_id": { - "name": "linked_project_device_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "connection_kind": { - "name": "connection_kind", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cost_group": { - "name": "cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "level": { - "name": "level", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_id": { - "name": "room_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_number_snapshot": { - "name": "room_number_snapshot", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_name_snapshot": { - "name": "room_name_snapshot", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "manual_quantity": { - "name": "manual_quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "power_per_unit": { - "name": "power_per_unit", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "simultaneity_factor": { - "name": "simultaneity_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "cos_phi": { - "name": "cos_phi", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "overridden_fields": { - "name": "overridden_fields", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "circuit_device_rows_circuit_id_idx": { - "name": "circuit_device_rows_circuit_id_idx", - "columns": [ - "circuit_id" - ], - "isUnique": false - } - }, - "foreignKeys": { - "circuit_device_rows_circuit_id_circuits_id_fk": { - "name": "circuit_device_rows_circuit_id_circuits_id_fk", - "tableFrom": "circuit_device_rows", - "tableTo": "circuits", - "columnsFrom": [ - "circuit_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "circuit_device_rows_linked_project_device_id_project_devices_id_fk": { - "name": "circuit_device_rows_linked_project_device_id_project_devices_id_fk", - "tableFrom": "circuit_device_rows", - "tableTo": "project_devices", - "columnsFrom": [ - "linked_project_device_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "circuit_device_rows_room_id_rooms_id_fk": { - "name": "circuit_device_rows_room_id_rooms_id_fk", - "tableFrom": "circuit_device_rows", - "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuit_list_equipment_identifiers": { - "name": "circuit_list_equipment_identifiers", - "columns": { - "owner_type": { - "name": "owner_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "owner_id": { - "name": "owner_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "equipment_identifier": { - "name": "equipment_identifier", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "circuit_list_equipment_identifiers_owner_unique": { - "name": "circuit_list_equipment_identifiers_owner_unique", - "columns": [ - "owner_type", - "owner_id" - ], - "isUnique": true - }, - "circuit_list_equipment_identifiers_list_identifier_unique": { - "name": "circuit_list_equipment_identifiers_list_identifier_unique", - "columns": [ - "circuit_list_id", - "equipment_identifier" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuit_list_equipment_identifiers_circuit_list_id_circuit_lists_id_fk": { - "name": "circuit_list_equipment_identifiers_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "circuit_list_equipment_identifiers", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuit_lists": { - "name": "circuit_lists", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "distribution_board_id": { - "name": "distribution_board_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "circuit_lists_distribution_board_id_unique": { - "name": "circuit_lists_distribution_board_id_unique", - "columns": [ - "distribution_board_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuit_lists_project_id_projects_id_fk": { - "name": "circuit_lists_project_id_projects_id_fk", - "tableFrom": "circuit_lists", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "circuit_lists_distribution_board_id_distribution_boards_id_fk": { - "name": "circuit_lists_distribution_board_id_distribution_boards_id_fk", - "tableFrom": "circuit_lists", - "tableTo": "distribution_boards", - "columnsFrom": [ - "distribution_board_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuit_protection_devices": { - "name": "circuit_protection_devices", - "columns": { - "circuit_id": { - "name": "circuit_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "rated_current_a": { - "name": "rated_current_a", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "fuse_utilization_category": { - "name": "fuse_utilization_category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "trip_characteristic": { - "name": "trip_characteristic", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "rcd_type": { - "name": "rcd_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "rated_residual_current_ma": { - "name": "rated_residual_current_ma", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "circuit_protection_devices_circuit_id_circuits_id_fk": { - "name": "circuit_protection_devices_circuit_id_circuits_id_fk", - "tableFrom": "circuit_protection_devices", - "tableTo": "circuits", - "columnsFrom": [ - "circuit_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuit_sections": { - "name": "circuit_sections", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "key": { - "name": "key", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "prefix": { - "name": "prefix", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "group_number": { - "name": "group_number", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "circuit_sections_list_key_unique": { - "name": "circuit_sections_list_key_unique", - "columns": [ - "circuit_list_id", - "key" - ], - "isUnique": true - }, - "circuit_sections_list_prefix_unique": { - "name": "circuit_sections_list_prefix_unique", - "columns": [ - "circuit_list_id", - "prefix" - ], - "isUnique": true - }, - "circuit_sections_list_category_group_unique": { - "name": "circuit_sections_list_category_group_unique", - "columns": [ - "circuit_list_id", - "category", - "group_number" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuit_sections_circuit_list_id_circuit_lists_id_fk": { - "name": "circuit_sections_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "circuit_sections", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuits": { - "name": "circuits", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "section_id": { - "name": "section_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "equipment_identifier": { - "name": "equipment_identifier", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "cable_type": { - "name": "cable_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_cross_section": { - "name": "cable_cross_section", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_length": { - "name": "cable_length", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "rcd_assignment": { - "name": "rcd_assignment", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "terminal_designation": { - "name": "terminal_designation", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "voltage": { - "name": "voltage", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "control_requirement": { - "name": "control_requirement", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "is_reserve": { - "name": "is_reserve", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "circuits_section_id_idx": { - "name": "circuits_section_id_idx", - "columns": [ - "section_id" - ], - "isUnique": false - }, - "circuits_list_equipment_identifier_unique": { - "name": "circuits_list_equipment_identifier_unique", - "columns": [ - "circuit_list_id", - "equipment_identifier" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuits_circuit_list_id_circuit_lists_id_fk": { - "name": "circuits_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "circuits", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "circuits_section_id_circuit_sections_id_fk": { - "name": "circuits_section_id_circuit_sections_id_fk", - "tableFrom": "circuits", - "tableTo": "circuit_sections", - "columnsFrom": [ - "section_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "distribution_board_component_protection_devices": { - "name": "distribution_board_component_protection_devices", - "columns": { - "component_id": { - "name": "component_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "rated_current_a": { - "name": "rated_current_a", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "fuse_utilization_category": { - "name": "fuse_utilization_category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "trip_characteristic": { - "name": "trip_characteristic", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "rcd_type": { - "name": "rcd_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "rated_residual_current_ma": { - "name": "rated_residual_current_ma", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "distribution_board_component_protection_devices_component_id_distribution_board_components_id_fk": { - "name": "distribution_board_component_protection_devices_component_id_distribution_board_components_id_fk", - "tableFrom": "distribution_board_component_protection_devices", - "tableTo": "distribution_board_components", - "columnsFrom": [ - "component_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "distribution_board_components": { - "name": "distribution_board_components", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "section_id": { - "name": "section_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "equipment_identifier": { - "name": "equipment_identifier", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "role": { - "name": "role", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "placement": { - "name": "placement", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": { - "distribution_board_components_list_identifier_unique": { - "name": "distribution_board_components_list_identifier_unique", - "columns": [ - "circuit_list_id", - "equipment_identifier" - ], - "isUnique": true - }, - "distribution_board_components_section_role_unique": { - "name": "distribution_board_components_section_role_unique", - "columns": [ - "section_id", - "role" - ], - "isUnique": true - } - }, - "foreignKeys": { - "distribution_board_components_circuit_list_id_circuit_lists_id_fk": { - "name": "distribution_board_components_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "distribution_board_components", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "distribution_board_components_section_id_circuit_sections_id_fk": { - "name": "distribution_board_components_section_id_circuit_sections_id_fk", - "tableFrom": "distribution_board_components", - "tableTo": "circuit_sections", - "columnsFrom": [ - "section_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "distribution_boards": { - "name": "distribution_boards", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "floor_id": { - "name": "floor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "supply_type": { - "name": "supply_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "simultaneity_factor": { - "name": "simultaneity_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 1 - } - }, - "indexes": {}, - "foreignKeys": { - "distribution_boards_project_id_projects_id_fk": { - "name": "distribution_boards_project_id_projects_id_fk", - "tableFrom": "distribution_boards", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "distribution_boards_floor_id_floors_id_fk": { - "name": "distribution_boards_floor_id_floors_id_fk", - "tableFrom": "distribution_boards", - "tableTo": "floors", - "columnsFrom": [ - "floor_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "external_csv_configurations": { - "name": "external_csv_configurations", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "configuration_version": { - "name": "configuration_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "configuration": { - "name": "configuration", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "external_csv_configurations_project_id_unique": { - "name": "external_csv_configurations_project_id_unique", - "columns": [ - "project_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "external_csv_configurations_project_id_projects_id_fk": { - "name": "external_csv_configurations_project_id_projects_id_fk", - "tableFrom": "external_csv_configurations", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "external_import_batches": { - "name": "external_import_batches", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "source_id": { - "name": "source_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "import_kind": { - "name": "import_kind", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "imported_at_iso": { - "name": "imported_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "file_name": { - "name": "file_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sha256": { - "name": "sha256", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "applied_project_revision": { - "name": "applied_project_revision", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "configuration_version": { - "name": "configuration_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "configuration_snapshot": { - "name": "configuration_snapshot", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "original_bytes": { - "name": "original_bytes", - "type": "blob", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "document": { - "name": "document", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "external_import_batches_project_revision_idx": { - "name": "external_import_batches_project_revision_idx", - "columns": [ - "project_id", - "applied_project_revision" - ], - "isUnique": false - }, - "external_import_batches_source_imported_idx": { - "name": "external_import_batches_source_imported_idx", - "columns": [ - "source_id", - "imported_at_iso" - ], - "isUnique": false - } - }, - "foreignKeys": { - "external_import_batches_project_id_projects_id_fk": { - "name": "external_import_batches_project_id_projects_id_fk", - "tableFrom": "external_import_batches", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "external_import_batches_source_id_external_model_sources_id_fk": { - "name": "external_import_batches_source_id_external_model_sources_id_fk", - "tableFrom": "external_import_batches", - "tableTo": "external_model_sources", - "columnsFrom": [ - "source_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "external_model_objects": { - "name": "external_model_objects", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "source_id": { - "name": "source_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "ifc_guid": { - "name": "ifc_guid", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "last_seen_import_batch_id": { - "name": "last_seen_import_batch_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "last_accepted_import_batch_id": { - "name": "last_accepted_import_batch_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "accepted_source_values": { - "name": "accepted_source_values", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "planning_values": { - "name": "planning_values", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "overridden_fields": { - "name": "overridden_fields", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "external_room_mapping_id": { - "name": "external_room_mapping_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "distribution_board_id": { - "name": "distribution_board_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "linked_project_device_id": { - "name": "linked_project_device_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "circuit_device_row_id": { - "name": "circuit_device_row_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "presence_status": { - "name": "presence_status", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'present'" - } - }, - "indexes": { - "external_model_objects_source_ifc_guid_unique": { - "name": "external_model_objects_source_ifc_guid_unique", - "columns": [ - "source_id", - "ifc_guid" - ], - "isUnique": true - }, - "external_model_objects_project_presence_idx": { - "name": "external_model_objects_project_presence_idx", - "columns": [ - "project_id", - "presence_status" - ], - "isUnique": false - }, - "external_model_objects_distribution_board_idx": { - "name": "external_model_objects_distribution_board_idx", - "columns": [ - "distribution_board_id" - ], - "isUnique": false - }, - "external_model_objects_circuit_device_row_idx": { - "name": "external_model_objects_circuit_device_row_idx", - "columns": [ - "circuit_device_row_id" - ], - "isUnique": false - } - }, - "foreignKeys": { - "external_model_objects_project_id_projects_id_fk": { - "name": "external_model_objects_project_id_projects_id_fk", - "tableFrom": "external_model_objects", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "external_model_objects_source_id_external_model_sources_id_fk": { - "name": "external_model_objects_source_id_external_model_sources_id_fk", - "tableFrom": "external_model_objects", - "tableTo": "external_model_sources", - "columnsFrom": [ - "source_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "external_model_objects_last_seen_import_batch_id_external_import_batches_id_fk": { - "name": "external_model_objects_last_seen_import_batch_id_external_import_batches_id_fk", - "tableFrom": "external_model_objects", - "tableTo": "external_import_batches", - "columnsFrom": [ - "last_seen_import_batch_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "restrict", - "onUpdate": "no action" - }, - "external_model_objects_last_accepted_import_batch_id_external_import_batches_id_fk": { - "name": "external_model_objects_last_accepted_import_batch_id_external_import_batches_id_fk", - "tableFrom": "external_model_objects", - "tableTo": "external_import_batches", - "columnsFrom": [ - "last_accepted_import_batch_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "restrict", - "onUpdate": "no action" - }, - "external_model_objects_external_room_mapping_id_external_room_mappings_id_fk": { - "name": "external_model_objects_external_room_mapping_id_external_room_mappings_id_fk", - "tableFrom": "external_model_objects", - "tableTo": "external_room_mappings", - "columnsFrom": [ - "external_room_mapping_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "external_model_objects_distribution_board_id_distribution_boards_id_fk": { - "name": "external_model_objects_distribution_board_id_distribution_boards_id_fk", - "tableFrom": "external_model_objects", - "tableTo": "distribution_boards", - "columnsFrom": [ - "distribution_board_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "external_model_objects_linked_project_device_id_project_devices_id_fk": { - "name": "external_model_objects_linked_project_device_id_project_devices_id_fk", - "tableFrom": "external_model_objects", - "tableTo": "project_devices", - "columnsFrom": [ - "linked_project_device_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "external_model_objects_circuit_device_row_id_circuit_device_rows_id_fk": { - "name": "external_model_objects_circuit_device_row_id_circuit_device_rows_id_fk", - "tableFrom": "external_model_objects", - "tableTo": "circuit_device_rows", - "columnsFrom": [ - "circuit_device_row_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "external_model_sources": { - "name": "external_model_sources", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "source_type": { - "name": "source_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'revit_csv'" - } - }, - "indexes": { - "external_model_sources_project_type_unique": { - "name": "external_model_sources_project_type_unique", - "columns": [ - "project_id", - "source_type" - ], - "isUnique": true - } - }, - "foreignKeys": { - "external_model_sources_project_id_projects_id_fk": { - "name": "external_model_sources_project_id_projects_id_fk", - "tableFrom": "external_model_sources", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "external_room_mappings": { - "name": "external_room_mappings", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "source_id": { - "name": "source_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "normalized_source_room_key": { - "name": "normalized_source_room_key", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "source_floor_name": { - "name": "source_floor_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "source_room_number": { - "name": "source_room_number", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "source_room_name": { - "name": "source_room_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "room_id": { - "name": "room_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "default_distribution_board_id": { - "name": "default_distribution_board_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "external_room_mappings_source_key_unique": { - "name": "external_room_mappings_source_key_unique", - "columns": [ - "source_id", - "normalized_source_room_key" - ], - "isUnique": true - }, - "external_room_mappings_project_room_idx": { - "name": "external_room_mappings_project_room_idx", - "columns": [ - "project_id", - "room_id" - ], - "isUnique": false - } - }, - "foreignKeys": { - "external_room_mappings_project_id_projects_id_fk": { - "name": "external_room_mappings_project_id_projects_id_fk", - "tableFrom": "external_room_mappings", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "external_room_mappings_source_id_external_model_sources_id_fk": { - "name": "external_room_mappings_source_id_external_model_sources_id_fk", - "tableFrom": "external_room_mappings", - "tableTo": "external_model_sources", - "columnsFrom": [ - "source_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "external_room_mappings_room_id_rooms_id_fk": { - "name": "external_room_mappings_room_id_rooms_id_fk", - "tableFrom": "external_room_mappings", - "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "external_room_mappings_default_distribution_board_id_distribution_boards_id_fk": { - "name": "external_room_mappings_default_distribution_board_id_distribution_boards_id_fk", - "tableFrom": "external_room_mappings", - "tableTo": "distribution_boards", - "columnsFrom": [ - "default_distribution_board_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "floors": { - "name": "floors", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": {}, - "foreignKeys": { - "floors_project_id_projects_id_fk": { - "name": "floors_project_id_projects_id_fk", - "tableFrom": "floors", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "global_devices": { - "name": "global_devices", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "installed_power_per_unit_kw": { - "name": "installed_power_per_unit_kw", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "demand_factor": { - "name": "demand_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "phase_count": { - "name": "phase_count", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "power_factor": { - "name": "power_factor", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "note": { - "name": "note", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_change_sets": { - "name": "project_change_sets", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_revision_id": { - "name": "project_revision_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "command_type": { - "name": "command_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "payload_schema_version": { - "name": "payload_schema_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "forward_payload_json": { - "name": "forward_payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "inverse_payload_json": { - "name": "inverse_payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "project_change_sets_revision_unique": { - "name": "project_change_sets_revision_unique", - "columns": [ - "project_revision_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_change_sets_project_revision_id_project_revisions_id_fk": { - "name": "project_change_sets_project_revision_id_project_revisions_id_fk", - "tableFrom": "project_change_sets", - "tableTo": "project_revisions", - "columnsFrom": [ - "project_revision_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_devices": { - "name": "project_devices", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'single_phase'" - }, - "connection_kind": { - "name": "connection_kind", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cost_group": { - "name": "cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "power_per_unit": { - "name": "power_per_unit", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "simultaneity_factor": { - "name": "simultaneity_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 1 - }, - "cos_phi": { - "name": "cos_phi", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "project_devices_project_id_projects_id_fk": { - "name": "project_devices_project_id_projects_id_fk", - "tableFrom": "project_devices", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_history_stack_entries": { - "name": "project_history_stack_entries", - "columns": { - "change_set_id": { - "name": "change_set_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "stack": { - "name": "stack", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "position": { - "name": "position", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "project_history_stack_entries_position_unique": { - "name": "project_history_stack_entries_position_unique", - "columns": [ - "project_id", - "stack", - "position" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_history_stack_entries_change_set_id_project_change_sets_id_fk": { - "name": "project_history_stack_entries_change_set_id_project_change_sets_id_fk", - "tableFrom": "project_history_stack_entries", - "tableTo": "project_change_sets", - "columnsFrom": [ - "change_set_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "project_history_stack_entries_project_id_projects_id_fk": { - "name": "project_history_stack_entries_project_id_projects_id_fk", - "tableFrom": "project_history_stack_entries", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": { - "project_history_stack_entries_stack_check": { - "name": "project_history_stack_entries_stack_check", - "value": "\"project_history_stack_entries\".\"stack\" in ('undo', 'redo')" - } - } - }, - "project_revisions": { - "name": "project_revisions", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "revision_number": { - "name": "revision_number", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "actor_id": { - "name": "actor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "source": { - "name": "source", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "project_revisions_project_number_unique": { - "name": "project_revisions_project_number_unique", - "columns": [ - "project_id", - "revision_number" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_revisions_project_id_projects_id_fk": { - "name": "project_revisions_project_id_projects_id_fk", - "tableFrom": "project_revisions", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_snapshots": { - "name": "project_snapshots", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "source_revision": { - "name": "source_revision", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "schema_version": { - "name": "schema_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "kind": { - "name": "kind", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'named'" - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "payload_json": { - "name": "payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "payload_sha256": { - "name": "payload_sha256", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_by_actor_id": { - "name": "created_by_actor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "project_snapshots_project_created_idx": { - "name": "project_snapshots_project_created_idx", - "columns": [ - "project_id", - "created_at_iso" - ], - "isUnique": false - }, - "project_snapshots_project_kind_revision_idx": { - "name": "project_snapshots_project_kind_revision_idx", - "columns": [ - "project_id", - "kind", - "source_revision" - ], - "isUnique": false - }, - "project_snapshots_project_name_unique": { - "name": "project_snapshots_project_name_unique", - "columns": [ - "project_id", - "name" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_snapshots_project_id_projects_id_fk": { - "name": "project_snapshots_project_id_projects_id_fk", - "tableFrom": "project_snapshots", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "projects": { - "name": "projects", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "internal_project_number": { - "name": "internal_project_number", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "external_project_number": { - "name": "external_project_number", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "building_owner": { - "name": "building_owner", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "is_public_building": { - "name": "is_public_building", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": false - }, - "single_phase_voltage_v": { - "name": "single_phase_voltage_v", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 230 - }, - "three_phase_voltage_v": { - "name": "three_phase_voltage_v", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 400 - }, - "enabled_distribution_board_supply_types": { - "name": "enabled_distribution_board_supply_types", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'[\"AV\",\"SV\",\"EV\",\"USV\",\"MSR\",\"SiBe\"]'" - }, - "current_revision": { - "name": "current_revision", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "rooms": { - "name": "rooms", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "floor_id": { - "name": "floor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_number": { - "name": "room_number", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "room_name": { - "name": "room_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "rooms_project_id_projects_id_fk": { - "name": "rooms_project_id_projects_id_fk", - "tableFrom": "rooms", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "rooms_floor_id_floors_id_fk": { - "name": "rooms_floor_id_floors_id_fk", - "tableFrom": "rooms", - "tableTo": "floors", - "columnsFrom": [ - "floor_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - } - }, - "views": {}, - "enums": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} - } -} \ No newline at end of file diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index 0b056a8..eedfc7a 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -50,13 +50,6 @@ "when": 1786043080323, "tag": "0006_damp_skrulls", "breakpoints": true - }, - { - "idx": 7, - "version": "6", - "when": 1786114831610, - "tag": "0007_watery_kingpin", - "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/repositories/cable-sizing-calculation.repository.ts b/src/db/repositories/cable-sizing-calculation.repository.ts deleted file mode 100644 index 803965b..0000000 --- a/src/db/repositories/cable-sizing-calculation.repository.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { desc, eq } from "drizzle-orm"; -import type { AppDatabase } from "../database-context.js"; -import { - cableSizingCalculations, - type CableSizingCalculation, - type NewCableSizingCalculation, -} from "../schema/cable-sizing-calculations.js"; - -// Plain repository, no revision/command semantics - mirrors -// application-repositories.ts's flat repository pattern, not the -// project-command-transaction boundary. A calculation is an audit record, -// not a project mutation. -export class CableSizingCalculationRepository { - constructor(private readonly db: AppDatabase) {} - - create(input: NewCableSizingCalculation): CableSizingCalculation { - return this.db.insert(cableSizingCalculations).values(input).returning().get(); - } - - listByCircuit(circuitId: string): CableSizingCalculation[] { - return this.db - .select() - .from(cableSizingCalculations) - .where(eq(cableSizingCalculations.circuitId, circuitId)) - .orderBy(desc(cableSizingCalculations.createdAt)) - .all(); - } - - markApplied(id: string): CableSizingCalculation | undefined { - return this.db - .update(cableSizingCalculations) - .set({ appliedToCircuit: 1 }) - .where(eq(cableSizingCalculations.id, id)) - .returning() - .get(); - } -} diff --git a/src/db/schema/cable-sizing-calculations.ts b/src/db/schema/cable-sizing-calculations.ts deleted file mode 100644 index 511fab3..0000000 --- a/src/db/schema/cable-sizing-calculations.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { sql } from "drizzle-orm"; -import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; -import { circuits } from "./circuits.js"; -import { projects } from "./projects.js"; - -// Audit log for the cable-sizing module (see docs/cable-sizing-module.md). -// Deliberately separate from the revision/command system: a calculation is -// not itself a project mutation, only applying its result via the existing -// circuit.update command is. circuitId/projectId are nullable and -// onDelete "set null" so a deleted circuit or project never blocks or -// cascades into losing the audit trail. -export const cableSizingCalculations = sqliteTable( - "cable_sizing_calculations", - { - id: text("id").primaryKey(), - projectId: text("project_id").references(() => projects.id, { onDelete: "set null" }), - circuitId: text("circuit_id").references(() => circuits.id, { onDelete: "set null" }), - equipmentIdentifier: text("equipment_identifier"), - input: text("input", { mode: "json" }).notNull(), - result: text("result", { mode: "json" }).notNull(), - appliedToCircuit: integer("applied_to_circuit").notNull().default(0), - createdAt: integer("created_at", { mode: "timestamp" }) - .notNull() - .default(sql`(unixepoch())`), - }, - (table) => [index("cable_sizing_calculations_circuit_id_idx").on(table.circuitId)] -); - -export type CableSizingCalculation = typeof cableSizingCalculations.$inferSelect; -export type NewCableSizingCalculation = typeof cableSizingCalculations.$inferInsert; diff --git a/src/frontend/components/cable-sizing-api.ts b/src/frontend/components/cable-sizing-api.ts deleted file mode 100644 index ee52aef..0000000 --- a/src/frontend/components/cable-sizing-api.ts +++ /dev/null @@ -1,71 +0,0 @@ -// Thin, self-contained fetch client for the cable-sizing module. Deliberately -// not merged into ../utils/api.ts to keep this module's frontend footprint a -// single new file rather than growing the existing shared API file - see -// docs/cable-sizing-module.md. -import type { - CableSizingAlert, - CableSizingInput, - CableSizingResult, - InsulationMaterial, - LayingMethod, -} from "../../cable-sizing/domain/cable-sizing-calculation"; - -async function request(url: string, init?: RequestInit): Promise { - const response = await fetch(url, { - ...init, - headers: { "Content-Type": "application/json", ...init?.headers }, - cache: "no-store", - }); - if (!response.ok) { - let details = await response.text(); - try { - const parsed = JSON.parse(details) as { error?: unknown }; - if (typeof parsed.error === "string") details = parsed.error; - } catch { - // keep raw text - } - throw new Error(details || `Anfrage fehlgeschlagen (Status ${response.status})`); - } - return response.json() as Promise; -} - -export interface CalculateCableSizingResponse { - calculationId: string; - result: CableSizingResult; - alerts: CableSizingAlert[]; -} - -export function calculateCableSizing( - input: CableSizingInput, - context?: { projectId?: string; circuitId?: string; equipmentIdentifier?: string } -): Promise { - return request("/api/cable-sizing/calculate", { - method: "POST", - body: JSON.stringify({ ...input, context }), - }); -} - -export function markCableSizingCalculationApplied(calculationId: string): Promise { - return request(`/api/cable-sizing/calculations/${calculationId}/applied`, { - method: "POST", - }); -} - -export interface LayingMethodOption { - method: LayingMethod; - label: string; - dataVerified: boolean; -} - -export function listLayingMethods(): Promise { - return request("/api/cable-sizing/laying-methods"); -} - -export interface InsulationMaterialOption { - insulation: InsulationMaterial; - label: string; -} - -export function listInsulationMaterials(): Promise { - return request("/api/cable-sizing/insulation-materials"); -} diff --git a/src/frontend/components/cable-sizing-modal.tsx b/src/frontend/components/cable-sizing-modal.tsx deleted file mode 100644 index 36b204c..0000000 --- a/src/frontend/components/cable-sizing-modal.tsx +++ /dev/null @@ -1,523 +0,0 @@ -"use client"; - -import { type FormEvent, useMemo, useState } from "react"; -import { - CONDUCTOR_MATERIALS, - CROSS_SECTIONS_MM2, - GROUPING_LABELS, - GROUPING_OPTIONS, - HARMONIC_NEUTRAL_LOAD_LABELS, - HARMONIC_NEUTRAL_LOAD_OPTIONS, - INSULATION_MATERIALS, - INSULATION_MATERIAL_LABELS, - LAYING_METHODS, - LAYING_METHOD_GROUP, - LAYING_METHOD_LABELS, - PRACTICAL_MINIMUM_CROSS_SECTION_MM2, - TEMPERATURE_FACTOR_AIR, - TEMPERATURE_FACTOR_GROUND, - type CableSizingAlert, - type CableSizingInput, - type CableSizingResult, - type CircuitCategory, - type ConductorMaterial, - type HarmonicNeutralLoad, - type InsulationMaterial, - type LayingMethod, -} from "../../cable-sizing/domain/cable-sizing-calculation"; -import { calculateCableSizing } from "./cable-sizing-api"; -import type { CircuitTreeCircuitDto } from "../types"; -import { FormModal } from "./form-modal"; - -interface CableSizingModalProps { - circuit: CircuitTreeCircuitDto; - circuitCategory?: CircuitCategory; - isSaving: boolean; - projectId: string; - onClose: () => void; - onApply: (patch: { - cableCrossSection: string; - cableType?: string; - cableLength?: number; - }) => Promise; -} - -function temperatureOptions(method: LayingMethod) { - const table = - LAYING_METHOD_GROUP[method] === "air" ? TEMPERATURE_FACTOR_AIR : TEMPERATURE_FACTOR_GROUND; - return Object.keys(table).map(Number).sort((a, b) => a - b); -} - -// Extracts the first decimal number from a free-text cross-section entry -// like "2.5 mm²" or "2,5". Returns null if nothing parseable is found - -// used only for the manual-entry sanity check below, never persisted. -function parseCrossSectionMm2(text: string): number | null { - const match = text.match(/([0-9]+(?:[.,][0-9]+)?)/); - if (!match) return null; - const value = Number(match[1].replace(",", ".")); - return Number.isFinite(value) ? value : null; -} - -export function CableSizingModal({ - circuit, - circuitCategory, - isSaving, - projectId, - onClose, - onApply, -}: CableSizingModalProps) { - const [layingMethod, setLayingMethod] = useState("C"); - const [insulation, setInsulation] = useState("pvc"); - const [conductorMaterial, setConductorMaterial] = useState("copper"); - const [ambientTemperatureC, setAmbientTemperatureC] = useState(30); - const [groupingCircuits, setGroupingCircuits] = useState(1); - const [cosPhi, setCosPhi] = useState(1); - const [maxVoltageDropPercent, setMaxVoltageDropPercent] = useState(3); - const [harmonicNeutralLoad, setHarmonicNeutralLoad] = useState("none"); - const [lengthM, setLengthM] = useState(circuit.cableLength ?? 0); - const [phase, setPhase] = useState<1 | 3>(circuit.voltage === 400 ? 3 : 1); - - // Manual override: applying this never requires a calculation. Prefilled - // from the circuit's current values so opening the modal on an already - // specified cable doesn't lose that data. - const [manualCableType, setManualCableType] = useState(circuit.cableType ?? ""); - const [manualCrossSection, setManualCrossSection] = useState(circuit.cableCrossSection ?? ""); - - const [calculating, setCalculating] = useState(false); - const [calculationId, setCalculationId] = useState(null); - const [result, setResult] = useState(null); - const [alerts, setAlerts] = useState([]); - const [error, setError] = useState(null); - - const voltage = circuit.voltage ?? (phase === 1 ? 230 : 400); - - const input: CableSizingInput = useMemo( - () => ({ - phase, - mode: "power", - powerKw: circuit.circuitTotalPower, - cosPhi, - voltage, - lengthM, - layingMethod, - conductorMaterial, - insulation, - ambientTemperatureC, - groupingCircuits, - maxVoltageDropPercent, - harmonicNeutralLoad, - existingProtectionRatedCurrentA: circuit.protectionDevice?.ratedCurrentA, - circuitCategory, - }), - [ - phase, - circuit.circuitTotalPower, - cosPhi, - voltage, - lengthM, - layingMethod, - conductorMaterial, - insulation, - ambientTemperatureC, - groupingCircuits, - maxVoltageDropPercent, - harmonicNeutralLoad, - circuit.protectionDevice?.ratedCurrentA, - circuitCategory, - ] - ); - - const manualCrossSectionWarning = useMemo(() => { - const trimmed = manualCrossSection.trim(); - if (!trimmed) return null; - const parsed = parseCrossSectionMm2(trimmed); - if (parsed == null) return null; - if (!(CROSS_SECTIONS_MM2 as readonly number[]).includes(parsed)) { - return `${parsed} mm² ist kein Standard-Querschnitt (${CROSS_SECTIONS_MM2.join(", ")}).`; - } - if ( - result?.dataVerified && - result.recommendedCrossSectionMm2 != null && - parsed < result.recommendedCrossSectionMm2 - ) { - return `${parsed} mm² ist kleiner als die zuletzt berechnete Empfehlung (${result.recommendedCrossSectionMm2} mm²) - passt nicht zu Last${circuit.protectionDevice ? "/Sicherung" : ""} und Verlegeart.`; - } - const practicalMinimum = circuitCategory - ? PRACTICAL_MINIMUM_CROSS_SECTION_MM2[circuitCategory] - : undefined; - if (practicalMinimum != null && parsed < practicalMinimum) { - return `${parsed} mm² liegt unter dem Praxis-Mindestquerschnitt für 1-phasige Stromkreise (${practicalMinimum} mm²).`; - } - return null; - }, [manualCrossSection, result, circuit.protectionDevice, circuitCategory]); - - function handleLayingMethodChange(method: LayingMethod) { - setLayingMethod(method); - const options = temperatureOptions(method); - const reference = LAYING_METHOD_GROUP[method] === "air" ? 30 : 20; - setAmbientTemperatureC(options.includes(reference) ? reference : options[0]); - setResult(null); - setCalculationId(null); - } - - async function handleCalculate() { - setCalculating(true); - setError(null); - try { - const response = await calculateCableSizing(input, { - projectId, - circuitId: circuit.id, - equipmentIdentifier: circuit.equipmentIdentifier, - }); - setResult(response.result); - setAlerts(response.alerts); - setCalculationId(response.calculationId); - // Calculating fills the manual field as a suggestion, but that field - // stays the single source of truth for what gets applied - the user - // can still edit it by hand before submitting. - if (response.result.dataVerified && response.result.recommendedCrossSectionMm2) { - setManualCrossSection(`${response.result.recommendedCrossSectionMm2} mm²`); - } - } catch (err) { - setError(err instanceof Error ? err.message : "Berechnung fehlgeschlagen"); - } finally { - setCalculating(false); - } - } - - async function handleSubmit(event: FormEvent) { - event.preventDefault(); - if (!manualCrossSection.trim()) return; - await onApply({ - cableCrossSection: manualCrossSection.trim(), - cableType: manualCableType.trim() || undefined, - cableLength: lengthM, - }); - } - - return ( - -
-
- - setLengthM(Number(event.target.value))} - type="number" - value={lengthM} - /> -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
- {phase === 3 && ( -
- - -
- )} -
- - setCosPhi(Number(event.target.value))} - step={0.05} - type="range" - value={cosPhi} - /> - {cosPhi.toFixed(2)} -
-
- - setMaxVoltageDropPercent(Number(event.target.value))} - step={0.5} - type="range" - value={maxVoltageDropPercent} - /> - {maxVoltageDropPercent.toFixed(1)} % -
-
- - - {error &&

{error}

} - - {result && !result.dataVerified && ( -
- Für Verlegeart {layingMethod} mit Isolierstoff {insulation.toUpperCase()} liegen keine - geprüften Strombelastbarkeits-Tabellenwerte vor - keine Dimensionierung möglich. Bitte - eine andere Verlegeart/Isolierstoff-Kombination wählen. -
- )} - - {result && result.dataVerified && ( -
-
-
-
Betriebsstrom Ib
-
{result.operatingCurrentA.toFixed(1)} A
-
-
-
- Bemessungsstrom (für Querschnittswahl){circuit.protectionDevice ? " *" : ""} -
-
- {result.designCurrentA.toFixed(1)} A -
-
-
-
Empfohlener Querschnitt
-
- - {result.recommendedCrossSectionMm2 - ? `${result.recommendedCrossSectionMm2} mm²` - : "-"} - -
-
-
-
ΔU bei Empfehlung
-
- {result.voltageDropAtRecommendedPercent != null - ? `${result.voltageDropAtRecommendedPercent.toFixed(2)} %` - : "-"} -
-
-
-
Korrekturfaktor
-
{result.combinedDerationFactor.toFixed(2)}
-
-
-
Max. Länge (ΔU-Grenze)
-
- {result.rows.find((row) => row.recommended)?.maxLengthForVoltageDropM != null - ? `${result.rows - .find((row) => row.recommended)! - .maxLengthForVoltageDropM!.toFixed(0)} m` - : "-"} -
-
-
- {alerts.map((alert, index) => ( -
- {alert.text} -
- ))} - {circuit.protectionDevice && ( -

- * Bemessungsstrom = Maximum aus Betriebsstrom und vorhandener Sicherung - ({circuit.protectionDevice.ratedCurrentA} A) - die Sicherung löst erst bei ihrem - eigenen Bemessungsstrom aus, das Kabel muss also dafür ausgelegt sein, nicht nur - für die tatsächliche Last. -

- )} -
- )} - -
-

- Querschnitt manuell eingeben oder eine Berechnung oben übernehmen - beides schreibt in - dasselbe Feld, du kannst es vor dem Übernehmen frei anpassen. -

-
-
- - setManualCableType(event.target.value)} - placeholder="z.B. NYM-J 3x1.5" - value={manualCableType} - /> -
-
- - setManualCrossSection(event.target.value)} - placeholder="z.B. 4 mm²" - value={manualCrossSection} - /> -
- {manualCrossSectionWarning && ( -
-
{manualCrossSectionWarning}
-
- )} -
-
- ); -} diff --git a/src/frontend/components/circuit-tree-editor.tsx b/src/frontend/components/circuit-tree-editor.tsx index d113b3b..03aa38d 100644 --- a/src/frontend/components/circuit-tree-editor.tsx +++ b/src/frontend/components/circuit-tree-editor.tsx @@ -137,7 +137,6 @@ import type { import { DistributionBoardComponentModal } from "./distribution-board-component-modal"; import { CircuitGroupModal } from "./circuit-group-modal"; import { CircuitProtectionModal } from "./circuit-protection-modal"; -import { CableSizingModal } from "./cable-sizing-modal"; import { circuitGroupCategoryLabels, type CircuitGroupCategory, @@ -279,8 +278,6 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str useState(null); const [protectionEditorCircuit, setProtectionEditorCircuit] = useState(null); - const [cableSizingEditorCircuit, setCableSizingEditorCircuit] = - useState(null); const [projectDevices, setProjectDevices] = useState([]); const [isProjectDeviceDrawerOpen, setIsProjectDeviceDrawerOpen] = useState(false); @@ -1401,31 +1398,6 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str }); } - async function handleApplyCableSizing(patch: { - cableCrossSection: string; - cableType?: string; - cableLength?: number; - }) { - const circuit = cableSizingEditorCircuit; - if (!circuit) { - return; - } - await runCommand({ - label: "Kabel dimensionieren", - redo: async () => { - const result = await updateCircuitById( - projectId, - getExpectedProjectRevision(), - circuit.id, - patch - ); - applyProjectCommandResult(result); - setCableSizingEditorCircuit(null); - return null; - }, - }); - } - async function handleRedo() { if (historyBusy || isSaving || !historyState || historyState.redoDepth === 0) { return; @@ -3167,20 +3139,6 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str onSave={handleSaveCircuitProtection} /> ) : null} - {cableSizingEditorCircuit ? ( - section.id === cableSizingEditorCircuit.sectionId - )?.category - } - isSaving={isSaving} - projectId={projectId} - onClose={() => setCableSizingEditorCircuit(null)} - onApply={handleApplyCableSizing} - /> - ) : null}