forked from jappel/leistungsbilanz-ts
Add per-project socket limit and Revit socket device-type flag
- Project setting maxSocketsPerCircuit (migration 0018, project.update-settings schema 4, snapshot schema 11); older command and snapshot versions keep or upgrade the value to null. - Revit configuration schema 2 marks device types as socket; version 1 stays readable and derives the flag from socketCount family rules. - Settings dialog field and device-type card on the Revit page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
85cbdbf8e0
commit
e48042bde3
35 changed files with 3781 additions and 41 deletions
|
|
@ -98,6 +98,7 @@ const requiredProjectColumns = [
|
||||||
"work_stage",
|
"work_stage",
|
||||||
"project_lead",
|
"project_lead",
|
||||||
"project_editor",
|
"project_editor",
|
||||||
|
"max_sockets_per_circuit",
|
||||||
];
|
];
|
||||||
const projectColumns = new Set(
|
const projectColumns = new Set(
|
||||||
db.prepare("PRAGMA table_info(projects)").all().map((column) => column.name)
|
db.prepare("PRAGMA table_info(projects)").all().map((column) => column.name)
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { useEffect, useState } from "react";
|
||||||
import { IconLink } from "../../../../frontend/components/icon-link";
|
import { IconLink } from "../../../../frontend/components/icon-link";
|
||||||
import { RevitInitialImportWizard } from "../../../../frontend/components/revit-initial-import-wizard";
|
import { RevitInitialImportWizard } from "../../../../frontend/components/revit-initial-import-wizard";
|
||||||
import { RevitFollowUpImportWizard } from "../../../../frontend/components/revit-follow-up-import-wizard";
|
import { RevitFollowUpImportWizard } from "../../../../frontend/components/revit-follow-up-import-wizard";
|
||||||
|
import { RevitDeviceTypeCard } from "../../../../frontend/components/revit-device-type-card";
|
||||||
import { RevitObjectTable } from "../../../../frontend/components/revit-object-table";
|
import { RevitObjectTable } from "../../../../frontend/components/revit-object-table";
|
||||||
import { RevitXlsxSettingsModal } from "../../../../frontend/components/revit-xlsx-settings-modal";
|
import { RevitXlsxSettingsModal } from "../../../../frontend/components/revit-xlsx-settings-modal";
|
||||||
import type { ExternalModelConfiguration } from "../../../../external-model/configuration/external-model-configuration";
|
import type { ExternalModelConfiguration } from "../../../../external-model/configuration/external-model-configuration";
|
||||||
|
|
@ -46,6 +47,7 @@ export default function RevitPage() {
|
||||||
<div className="page-header"><div><div className="kicker">Projekt · Revit</div><h1>{project?.name ?? "Revit-Anbindung"}</h1><p className="text-secondary mb-0">Linear-XLSX austauschen und Revit-Objekte verwalten</p></div><div className="d-flex gap-2"><button className="btn btn-success btn-with-icon" disabled={!project || !snapshot || Boolean(importStatus?.imported)} onClick={() => setImportOpen(true)} type="button"><Upload size={16} />Erstimport</button><button className="btn btn-success btn-with-icon" disabled={!project || !snapshot || !importStatus?.imported} onClick={() => setFollowUpOpen(true)} type="button"><Upload size={16} />Folgeimport</button><button className="btn btn-primary btn-with-icon" disabled={!project} onClick={() => setSettingsOpen(true)} type="button"><Settings size={16} />Einstellungen</button><IconLink href={`/projects/${projectId}`} icon={ArrowLeft} label="Zum Projekt" showLabel /></div></div>
|
<div className="page-header"><div><div className="kicker">Projekt · Revit</div><h1>{project?.name ?? "Revit-Anbindung"}</h1><p className="text-secondary mb-0">Linear-XLSX austauschen und Revit-Objekte verwalten</p></div><div className="d-flex gap-2"><button className="btn btn-success btn-with-icon" disabled={!project || !snapshot || Boolean(importStatus?.imported)} onClick={() => setImportOpen(true)} type="button"><Upload size={16} />Erstimport</button><button className="btn btn-success btn-with-icon" disabled={!project || !snapshot || !importStatus?.imported} onClick={() => setFollowUpOpen(true)} type="button"><Upload size={16} />Folgeimport</button><button className="btn btn-primary btn-with-icon" disabled={!project} onClick={() => setSettingsOpen(true)} type="button"><Settings size={16} />Einstellungen</button><IconLink href={`/projects/${projectId}`} icon={ArrowLeft} label="Zum Projekt" showLabel /></div></div>
|
||||||
{error ? <div className="alert alert-danger">{error}</div> : null}
|
{error ? <div className="alert alert-danger">{error}</div> : null}
|
||||||
<div className="row g-3 mt-1"><div className="col-md-6"><section className="card h-100"><div className="card-body"><FileSpreadsheet className="mb-3" size={28} /><h2 className="h5">XLSX-Konfiguration</h2><p className="text-secondary">{snapshot ? "Die Linear-Spalten und das Tabellenlayout sind konfiguriert." : "Noch keine XLSX-Konfiguration gespeichert."}</p><span className={`badge ${snapshot ? "text-bg-success" : "text-bg-secondary"}`}>{snapshot ? "Konfiguriert" : "Offen"}</span></div></section></div><div className="col-md-6"><section className="card h-100"><div className="card-body"><h2 className="h5">Importstatus</h2>{importStatus?.imported ? <><p className="mb-1"><strong>{importStatus.objectCount} Revit-Objekte</strong>{importStatus.fileName ? ` aus ${importStatus.fileName}` : ""}</p><p className="text-secondary mb-0">Quelle: {importStatus.sourceName} · Fehlend: {importStatus.missingObjectCount}</p></> : <p className="text-secondary mb-0">Noch kein Linear-XLSX-Import vorhanden.</p>}</div></section></div></div>
|
<div className="row g-3 mt-1"><div className="col-md-6"><section className="card h-100"><div className="card-body"><FileSpreadsheet className="mb-3" size={28} /><h2 className="h5">XLSX-Konfiguration</h2><p className="text-secondary">{snapshot ? "Die Linear-Spalten und das Tabellenlayout sind konfiguriert." : "Noch keine XLSX-Konfiguration gespeichert."}</p><span className={`badge ${snapshot ? "text-bg-success" : "text-bg-secondary"}`}>{snapshot ? "Konfiguriert" : "Offen"}</span></div></section></div><div className="col-md-6"><section className="card h-100"><div className="card-body"><h2 className="h5">Importstatus</h2>{importStatus?.imported ? <><p className="mb-1"><strong>{importStatus.objectCount} Revit-Objekte</strong>{importStatus.fileName ? ` aus ${importStatus.fileName}` : ""}</p><p className="text-secondary mb-0">Quelle: {importStatus.sourceName} · Fehlend: {importStatus.missingObjectCount}</p></> : <p className="text-secondary mb-0">Noch kein Linear-XLSX-Import vorhanden.</p>}</div></section></div></div>
|
||||||
|
{snapshot ? <RevitDeviceTypeCard configuration={snapshot.configuration} isSaving={saving} onSave={save} /> : null}
|
||||||
{importStatus?.imported ? <RevitObjectTable key={project?.currentRevision} projectId={projectId} onRevision={(revision) => setProject((current) => current ? { ...current, currentRevision: revision } : current)} /> : null}
|
{importStatus?.imported ? <RevitObjectTable key={project?.currentRevision} projectId={projectId} onRevision={(revision) => setProject((current) => current ? { ...current, currentRevision: revision } : current)} /> : null}
|
||||||
{settingsOpen ? <RevitXlsxSettingsModal configuration={snapshot?.configuration ?? createDefaultRevitXlsxConfiguration()} isNewConfiguration={snapshot === null} isSaving={saving} onClose={() => setSettingsOpen(false)} onInspectSample={async (file, configuration) => { const result = await detectExternalModelWorkbook(projectId, file, configuration); return `Muster erkannt: ${result.sheetName}, ${result.columnCount} Spalten, ${result.objectCount} Objekte.`; }} onSave={save} /> : null}
|
{settingsOpen ? <RevitXlsxSettingsModal configuration={snapshot?.configuration ?? createDefaultRevitXlsxConfiguration()} isNewConfiguration={snapshot === null} isSaving={saving} onClose={() => setSettingsOpen(false)} onInspectSample={async (file, configuration) => { const result = await detectExternalModelWorkbook(projectId, file, configuration); return `Muster erkannt: ${result.sheetName}, ${result.columnCount} Spalten, ${result.objectCount} Objekte.`; }} onSave={save} /> : null}
|
||||||
{importOpen && project && snapshot ? <RevitInitialImportWizard projectId={projectId} expectedRevision={project.currentRevision} initialConfiguration={snapshot.configuration} onClose={() => setImportOpen(false)} onImported={({ history }) => { setProject({ ...project, currentRevision: history.currentRevision }); setImportOpen(false); void Promise.all([getExternalInitialImportStatus(projectId), getExternalModelConfiguration(projectId)]).then(([status, configuration]) => { setImportStatus(status); setSnapshot(configuration.configuration); }); }} /> : null}
|
{importOpen && project && snapshot ? <RevitInitialImportWizard projectId={projectId} expectedRevision={project.currentRevision} initialConfiguration={snapshot.configuration} onClose={() => setImportOpen(false)} onImported={({ history }) => { setProject({ ...project, currentRevision: history.currentRevision }); setImportOpen(false); void Promise.all([getExternalInitialImportStatus(projectId), getExternalModelConfiguration(projectId)]).then(([status, configuration]) => { setImportStatus(status); setSnapshot(configuration.configuration); }); }} /> : null}
|
||||||
|
|
|
||||||
1
src/db/migrations/0018_project_socket_limit.sql
Normal file
1
src/db/migrations/0018_project_socket_limit.sql
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE `projects` ADD `max_sockets_per_circuit` integer;
|
||||||
3217
src/db/migrations/meta/0018_snapshot.json
Normal file
3217
src/db/migrations/meta/0018_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -127,6 +127,13 @@
|
||||||
"when": 1793359753805,
|
"when": 1793359753805,
|
||||||
"tag": "0017_circuit_protection_none",
|
"tag": "0017_circuit_protection_none",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 18,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1793391088159,
|
||||||
|
"tag": "0018_project_socket_limit",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ import {
|
||||||
baselineProjectSettingsUpdateCommandSchemaVersion,
|
baselineProjectSettingsUpdateCommandSchemaVersion,
|
||||||
createProjectSettingsUpdateProjectCommand,
|
createProjectSettingsUpdateProjectCommand,
|
||||||
withoutCoverProjectSettingsUpdateCommandSchemaVersion,
|
withoutCoverProjectSettingsUpdateCommandSchemaVersion,
|
||||||
|
withoutSocketLimitProjectSettingsUpdateCommandSchemaVersion,
|
||||||
type ProjectSettingsValues,
|
type ProjectSettingsValues,
|
||||||
} from "../../domain/models/project-settings-project-command.model.js";
|
} from "../../domain/models/project-settings-project-command.model.js";
|
||||||
import { projectCoverFields } from "../../shared/constants/project-cover.js";
|
import { projectCoverFields } from "../../shared/constants/project-cover.js";
|
||||||
|
|
@ -53,16 +54,23 @@ export class ProjectSettingsProjectCommandRepository
|
||||||
// the current project state and are therefore also absent from the
|
// the current project state and are therefore also absent from the
|
||||||
// inverse of the same version.
|
// inverse of the same version.
|
||||||
const command = input.command;
|
const command = input.command;
|
||||||
|
const currentSocketLimit = {
|
||||||
|
maxSocketsPerCircuit: current.maxSocketsPerCircuit,
|
||||||
|
};
|
||||||
const target: ProjectSettingsValues =
|
const target: ProjectSettingsValues =
|
||||||
command.schemaVersion === baselineProjectSettingsUpdateCommandSchemaVersion
|
command.schemaVersion === baselineProjectSettingsUpdateCommandSchemaVersion
|
||||||
? {
|
? {
|
||||||
...command.payload,
|
...command.payload,
|
||||||
isPublicBuilding: current.isPublicBuilding,
|
isPublicBuilding: current.isPublicBuilding,
|
||||||
...currentCoverValues,
|
...currentCoverValues,
|
||||||
|
...currentSocketLimit,
|
||||||
}
|
}
|
||||||
: command.schemaVersion ===
|
: command.schemaVersion ===
|
||||||
withoutCoverProjectSettingsUpdateCommandSchemaVersion
|
withoutCoverProjectSettingsUpdateCommandSchemaVersion
|
||||||
? { ...command.payload, ...currentCoverValues }
|
? { ...command.payload, ...currentCoverValues, ...currentSocketLimit }
|
||||||
|
: command.schemaVersion ===
|
||||||
|
withoutSocketLimitProjectSettingsUpdateCommandSchemaVersion
|
||||||
|
? { ...command.payload, ...currentSocketLimit }
|
||||||
: command.payload;
|
: command.payload;
|
||||||
if (projectSettingsEqual(current, target)) {
|
if (projectSettingsEqual(current, target)) {
|
||||||
throw new Error("Project settings did not change.");
|
throw new Error("Project settings did not change.");
|
||||||
|
|
@ -80,6 +88,7 @@ export class ProjectSettingsProjectCommandRepository
|
||||||
enabledDistributionBoardSupplyTypes:
|
enabledDistributionBoardSupplyTypes:
|
||||||
current.enabledDistributionBoardSupplyTypes,
|
current.enabledDistributionBoardSupplyTypes,
|
||||||
...currentCoverValues,
|
...currentCoverValues,
|
||||||
|
...currentSocketLimit,
|
||||||
};
|
};
|
||||||
const inverse =
|
const inverse =
|
||||||
command.schemaVersion === baselineProjectSettingsUpdateCommandSchemaVersion
|
command.schemaVersion === baselineProjectSettingsUpdateCommandSchemaVersion
|
||||||
|
|
@ -87,6 +96,9 @@ export class ProjectSettingsProjectCommandRepository
|
||||||
: command.schemaVersion ===
|
: command.schemaVersion ===
|
||||||
withoutCoverProjectSettingsUpdateCommandSchemaVersion
|
withoutCoverProjectSettingsUpdateCommandSchemaVersion
|
||||||
? createWithoutCoverProjectSettingsUpdateCommand(inverseValues)
|
? createWithoutCoverProjectSettingsUpdateCommand(inverseValues)
|
||||||
|
: command.schemaVersion ===
|
||||||
|
withoutSocketLimitProjectSettingsUpdateCommandSchemaVersion
|
||||||
|
? createWithoutSocketLimitProjectSettingsUpdateCommand(inverseValues)
|
||||||
: createProjectSettingsUpdateProjectCommand(inverseValues);
|
: createProjectSettingsUpdateProjectCommand(inverseValues);
|
||||||
assertDisabledSupplyTypesAreUnused(tx, input.projectId, target);
|
assertDisabledSupplyTypesAreUnused(tx, input.projectId, target);
|
||||||
const updated = tx
|
const updated = tx
|
||||||
|
|
@ -103,6 +115,17 @@ export class ProjectSettingsProjectCommandRepository
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createWithoutSocketLimitProjectSettingsUpdateCommand(
|
||||||
|
values: ProjectSettingsValues
|
||||||
|
) {
|
||||||
|
const { maxSocketsPerCircuit: _omitted, ...payload } = values;
|
||||||
|
return {
|
||||||
|
schemaVersion: withoutSocketLimitProjectSettingsUpdateCommandSchemaVersion,
|
||||||
|
type: "project.update-settings" as const,
|
||||||
|
payload,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function createWithoutCoverProjectSettingsUpdateCommand(
|
function createWithoutCoverProjectSettingsUpdateCommand(
|
||||||
values: ProjectSettingsValues
|
values: ProjectSettingsValues
|
||||||
) {
|
) {
|
||||||
|
|
@ -158,6 +181,7 @@ function projectSettingsEqual(
|
||||||
current.singlePhaseVoltageV === target.singlePhaseVoltageV &&
|
current.singlePhaseVoltageV === target.singlePhaseVoltageV &&
|
||||||
current.threePhaseVoltageV === target.threePhaseVoltageV &&
|
current.threePhaseVoltageV === target.threePhaseVoltageV &&
|
||||||
projectCoverFields.every((field) => current[field] === target[field]) &&
|
projectCoverFields.every((field) => current[field] === target[field]) &&
|
||||||
|
current.maxSocketsPerCircuit === target.maxSocketsPerCircuit &&
|
||||||
sameSupplyTypes(
|
sameSupplyTypes(
|
||||||
current.enabledDistributionBoardSupplyTypes,
|
current.enabledDistributionBoardSupplyTypes,
|
||||||
target.enabledDistributionBoardSupplyTypes
|
target.enabledDistributionBoardSupplyTypes
|
||||||
|
|
|
||||||
|
|
@ -325,6 +325,7 @@ function replaceProjectState(
|
||||||
workStage: state.project.workStage,
|
workStage: state.project.workStage,
|
||||||
projectLead: state.project.projectLead,
|
projectLead: state.project.projectLead,
|
||||||
projectEditor: state.project.projectEditor,
|
projectEditor: state.project.projectEditor,
|
||||||
|
maxSocketsPerCircuit: state.project.maxSocketsPerCircuit,
|
||||||
})
|
})
|
||||||
.where(eq(projects.id, state.project.id))
|
.where(eq(projects.id, state.project.id))
|
||||||
.run();
|
.run();
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ export function readProjectStateSnapshot(
|
||||||
workStage: projects.workStage,
|
workStage: projects.workStage,
|
||||||
projectLead: projects.projectLead,
|
projectLead: projects.projectLead,
|
||||||
projectEditor: projects.projectEditor,
|
projectEditor: projects.projectEditor,
|
||||||
|
maxSocketsPerCircuit: projects.maxSocketsPerCircuit,
|
||||||
currentRevision: projects.currentRevision,
|
currentRevision: projects.currentRevision,
|
||||||
})
|
})
|
||||||
.from(projects)
|
.from(projects)
|
||||||
|
|
@ -264,6 +265,7 @@ export function readProjectStateSnapshot(
|
||||||
workStage: project.workStage,
|
workStage: project.workStage,
|
||||||
projectLead: project.projectLead,
|
projectLead: project.projectLead,
|
||||||
projectEditor: project.projectEditor,
|
projectEditor: project.projectEditor,
|
||||||
|
maxSocketsPerCircuit: project.maxSocketsPerCircuit,
|
||||||
},
|
},
|
||||||
distributionBoards: boardRows,
|
distributionBoards: boardRows,
|
||||||
externalModelConfiguration,
|
externalModelConfiguration,
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ export class ProjectRepository {
|
||||||
workStage: null,
|
workStage: null,
|
||||||
projectLead: null,
|
projectLead: null,
|
||||||
projectEditor: null,
|
projectEditor: null,
|
||||||
|
maxSocketsPerCircuit: null,
|
||||||
currentRevision: 0,
|
currentRevision: 0,
|
||||||
};
|
};
|
||||||
await db.insert(projects).values(project);
|
await db.insert(projects).values(project);
|
||||||
|
|
|
||||||
|
|
@ -25,5 +25,7 @@ export const projects = sqliteTable("projects", {
|
||||||
workStage: text("work_stage"),
|
workStage: text("work_stage"),
|
||||||
projectLead: text("project_lead"),
|
projectLead: text("project_lead"),
|
||||||
projectEditor: text("project_editor"),
|
projectEditor: text("project_editor"),
|
||||||
|
// Client-specific highest effective socket count per circuit; null = none.
|
||||||
|
maxSocketsPerCircuit: integer("max_sockets_per_circuit"),
|
||||||
currentRevision: integer("current_revision").notNull().default(0),
|
currentRevision: integer("current_revision").notNull().default(0),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,17 @@ import {
|
||||||
projectCoverFields,
|
projectCoverFields,
|
||||||
type ProjectCoverField,
|
type ProjectCoverField,
|
||||||
} from "../../shared/constants/project-cover.js";
|
} from "../../shared/constants/project-cover.js";
|
||||||
|
import { isValidMaxSocketsPerCircuit } from "../../shared/constants/socket-limit.js";
|
||||||
|
|
||||||
export const projectSettingsUpdateCommandType =
|
export const projectSettingsUpdateCommandType =
|
||||||
"project.update-settings" as const;
|
"project.update-settings" as const;
|
||||||
// Version 3 adds the print cover fields. Versions 2 (without cover fields)
|
// Version 4 adds the per-project socket limit, version 3 the print cover
|
||||||
// and 1 (additionally without the public-building flag) stay executable;
|
// fields. Versions 3 (without socket limit), 2 (additionally without cover
|
||||||
// their missing values keep the current project values.
|
// fields) and 1 (additionally without the public-building flag) stay
|
||||||
export const projectSettingsUpdateCommandSchemaVersion = 3 as const;
|
// executable; their missing values keep the current project values.
|
||||||
|
export const projectSettingsUpdateCommandSchemaVersion = 4 as const;
|
||||||
|
export const withoutSocketLimitProjectSettingsUpdateCommandSchemaVersion =
|
||||||
|
3 as const;
|
||||||
export const withoutCoverProjectSettingsUpdateCommandSchemaVersion = 2 as const;
|
export const withoutCoverProjectSettingsUpdateCommandSchemaVersion = 2 as const;
|
||||||
export const baselineProjectSettingsUpdateCommandSchemaVersion = 1 as const;
|
export const baselineProjectSettingsUpdateCommandSchemaVersion = 1 as const;
|
||||||
|
|
||||||
|
|
@ -29,10 +33,17 @@ export interface ProjectSettingsValues
|
||||||
singlePhaseVoltageV: number;
|
singlePhaseVoltageV: number;
|
||||||
threePhaseVoltageV: number;
|
threePhaseVoltageV: number;
|
||||||
enabledDistributionBoardSupplyTypes: DistributionBoardSupplyType[];
|
enabledDistributionBoardSupplyTypes: DistributionBoardSupplyType[];
|
||||||
|
// Highest effective socket count per circuit; null means no limit.
|
||||||
|
maxSocketsPerCircuit: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WithoutCoverProjectSettingsValues = Omit<
|
export type WithoutSocketLimitProjectSettingsValues = Omit<
|
||||||
ProjectSettingsValues,
|
ProjectSettingsValues,
|
||||||
|
"maxSocketsPerCircuit"
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type WithoutCoverProjectSettingsValues = Omit<
|
||||||
|
WithoutSocketLimitProjectSettingsValues,
|
||||||
ProjectCoverField
|
ProjectCoverField
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
|
@ -47,6 +58,12 @@ export interface CurrentProjectSettingsUpdateProjectCommand
|
||||||
type: typeof projectSettingsUpdateCommandType;
|
type: typeof projectSettingsUpdateCommandType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WithoutSocketLimitProjectSettingsUpdateProjectCommand
|
||||||
|
extends SerializedProjectCommand<WithoutSocketLimitProjectSettingsValues> {
|
||||||
|
schemaVersion: typeof withoutSocketLimitProjectSettingsUpdateCommandSchemaVersion;
|
||||||
|
type: typeof projectSettingsUpdateCommandType;
|
||||||
|
}
|
||||||
|
|
||||||
export interface WithoutCoverProjectSettingsUpdateProjectCommand
|
export interface WithoutCoverProjectSettingsUpdateProjectCommand
|
||||||
extends SerializedProjectCommand<WithoutCoverProjectSettingsValues> {
|
extends SerializedProjectCommand<WithoutCoverProjectSettingsValues> {
|
||||||
schemaVersion: typeof withoutCoverProjectSettingsUpdateCommandSchemaVersion;
|
schemaVersion: typeof withoutCoverProjectSettingsUpdateCommandSchemaVersion;
|
||||||
|
|
@ -61,6 +78,7 @@ export interface BaselineProjectSettingsUpdateProjectCommand
|
||||||
|
|
||||||
export type ProjectSettingsUpdateProjectCommand =
|
export type ProjectSettingsUpdateProjectCommand =
|
||||||
| CurrentProjectSettingsUpdateProjectCommand
|
| CurrentProjectSettingsUpdateProjectCommand
|
||||||
|
| WithoutSocketLimitProjectSettingsUpdateProjectCommand
|
||||||
| WithoutCoverProjectSettingsUpdateProjectCommand
|
| WithoutCoverProjectSettingsUpdateProjectCommand
|
||||||
| BaselineProjectSettingsUpdateProjectCommand;
|
| BaselineProjectSettingsUpdateProjectCommand;
|
||||||
|
|
||||||
|
|
@ -82,6 +100,8 @@ export function assertProjectSettingsUpdateProjectCommand(
|
||||||
if (
|
if (
|
||||||
command.type !== projectSettingsUpdateCommandType ||
|
command.type !== projectSettingsUpdateCommandType ||
|
||||||
(command.schemaVersion !== projectSettingsUpdateCommandSchemaVersion &&
|
(command.schemaVersion !== projectSettingsUpdateCommandSchemaVersion &&
|
||||||
|
command.schemaVersion !==
|
||||||
|
withoutSocketLimitProjectSettingsUpdateCommandSchemaVersion &&
|
||||||
command.schemaVersion !==
|
command.schemaVersion !==
|
||||||
withoutCoverProjectSettingsUpdateCommandSchemaVersion &&
|
withoutCoverProjectSettingsUpdateCommandSchemaVersion &&
|
||||||
command.schemaVersion !==
|
command.schemaVersion !==
|
||||||
|
|
@ -123,16 +143,23 @@ export function assertProjectSettingsUpdateProjectCommand(
|
||||||
: command.schemaVersion ===
|
: command.schemaVersion ===
|
||||||
withoutCoverProjectSettingsUpdateCommandSchemaVersion
|
withoutCoverProjectSettingsUpdateCommandSchemaVersion
|
||||||
? 9
|
? 9
|
||||||
: 9 + projectCoverFields.length;
|
: command.schemaVersion ===
|
||||||
|
withoutSocketLimitProjectSettingsUpdateCommandSchemaVersion
|
||||||
|
? 9 + projectCoverFields.length
|
||||||
|
: 10 + projectCoverFields.length;
|
||||||
if (
|
if (
|
||||||
Object.keys(payload).length !== expectedKeyCount ||
|
Object.keys(payload).length !== expectedKeyCount ||
|
||||||
(command.schemaVersion !==
|
(command.schemaVersion !==
|
||||||
baselineProjectSettingsUpdateCommandSchemaVersion &&
|
baselineProjectSettingsUpdateCommandSchemaVersion &&
|
||||||
typeof payload.isPublicBuilding !== "boolean") ||
|
typeof payload.isPublicBuilding !== "boolean") ||
|
||||||
(command.schemaVersion === projectSettingsUpdateCommandSchemaVersion &&
|
((command.schemaVersion === projectSettingsUpdateCommandSchemaVersion ||
|
||||||
|
command.schemaVersion ===
|
||||||
|
withoutSocketLimitProjectSettingsUpdateCommandSchemaVersion) &&
|
||||||
!projectCoverFields.every((field) =>
|
!projectCoverFields.every((field) =>
|
||||||
isNullableTrimmedString(payload[field], projectCoverFieldMaxLengths[field])
|
isNullableTrimmedString(payload[field], projectCoverFieldMaxLengths[field])
|
||||||
))
|
)) ||
|
||||||
|
(command.schemaVersion === projectSettingsUpdateCommandSchemaVersion &&
|
||||||
|
!isValidMaxSocketsPerCircuit(payload.maxSocketsPerCircuit))
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Project settings update command contains invalid values."
|
"Project settings update command contains invalid values."
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
projectCoverFieldMaxLengths,
|
projectCoverFieldMaxLengths,
|
||||||
type ProjectCoverField,
|
type ProjectCoverField,
|
||||||
} from "../../shared/constants/project-cover.js";
|
} from "../../shared/constants/project-cover.js";
|
||||||
|
import { maxSocketsPerCircuitUpperBound } from "../../shared/constants/socket-limit.js";
|
||||||
import {
|
import {
|
||||||
circuitGroupCategories,
|
circuitGroupCategories,
|
||||||
} from "../../shared/constants/circuit-group.js";
|
} from "../../shared/constants/circuit-group.js";
|
||||||
|
|
@ -43,7 +44,8 @@ import {
|
||||||
type ExternalModelStateSnapshot,
|
type ExternalModelStateSnapshot,
|
||||||
} from "../../external-model/domain/external-model-contracts.js";
|
} from "../../external-model/domain/external-model-contracts.js";
|
||||||
|
|
||||||
export const projectStateSnapshotSchemaVersion = 10 as const;
|
export const projectStateSnapshotSchemaVersion = 11 as const;
|
||||||
|
const withoutSocketLimitProjectStateSnapshotSchemaVersion = 10 as const;
|
||||||
const legacyCsvProjectStateSnapshotSchemaVersion = 9 as const;
|
const legacyCsvProjectStateSnapshotSchemaVersion = 9 as const;
|
||||||
const roomAreaProjectStateSnapshotSchemaVersion = 8 as const;
|
const roomAreaProjectStateSnapshotSchemaVersion = 8 as const;
|
||||||
const buildingProjectStateSnapshotSchemaVersion = 7 as const;
|
const buildingProjectStateSnapshotSchemaVersion = 7 as const;
|
||||||
|
|
@ -87,7 +89,7 @@ const projectCoverValueSchema = (field: ProjectCoverField) =>
|
||||||
z.string().trim().min(1).max(projectCoverFieldMaxLengths[field]).nullable();
|
z.string().trim().min(1).max(projectCoverFieldMaxLengths[field]).nullable();
|
||||||
|
|
||||||
// Schema version 6 adds the print cover fields.
|
// Schema version 6 adds the print cover fields.
|
||||||
const projectSchema = publicBuildingProjectSchema
|
const coverProjectSchema = publicBuildingProjectSchema
|
||||||
.extend({
|
.extend({
|
||||||
siteAddress: projectCoverValueSchema("siteAddress"),
|
siteAddress: projectCoverValueSchema("siteAddress"),
|
||||||
workStage: projectCoverValueSchema("workStage"),
|
workStage: projectCoverValueSchema("workStage"),
|
||||||
|
|
@ -96,6 +98,18 @@ const projectSchema = publicBuildingProjectSchema
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
|
// Schema version 11 adds the per-project socket limit.
|
||||||
|
const projectSchema = coverProjectSchema
|
||||||
|
.extend({
|
||||||
|
maxSocketsPerCircuit: z
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.min(1)
|
||||||
|
.max(maxSocketsPerCircuitUpperBound)
|
||||||
|
.nullable(),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
const emptyProjectCoverValues = {
|
const emptyProjectCoverValues = {
|
||||||
siteAddress: null,
|
siteAddress: null,
|
||||||
workStage: null,
|
workStage: null,
|
||||||
|
|
@ -737,10 +751,21 @@ export const projectStateSnapshotSchema = z
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
|
const withoutSocketLimitProjectStateSnapshotSchema = z
|
||||||
|
.object({
|
||||||
|
schemaVersion: z.literal(withoutSocketLimitProjectStateSnapshotSchemaVersion),
|
||||||
|
project: coverProjectSchema,
|
||||||
|
distributionBoards: z.array(distributionBoardSchema),
|
||||||
|
externalModelConfiguration: externalModelConfigurationSnapshotSchema.nullable(),
|
||||||
|
externalModel: externalModelStateSchema,
|
||||||
|
...projectStateSnapshotContents,
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
const legacyCsvProjectStateSnapshotSchema = z
|
const legacyCsvProjectStateSnapshotSchema = z
|
||||||
.object({
|
.object({
|
||||||
schemaVersion: z.literal(legacyCsvProjectStateSnapshotSchemaVersion),
|
schemaVersion: z.literal(legacyCsvProjectStateSnapshotSchemaVersion),
|
||||||
project: projectSchema,
|
project: coverProjectSchema,
|
||||||
distributionBoards: z.array(distributionBoardSchema),
|
distributionBoards: z.array(distributionBoardSchema),
|
||||||
externalCsvConfiguration: externalCsvConfigurationSnapshotSchema.nullable(),
|
externalCsvConfiguration: externalCsvConfigurationSnapshotSchema.nullable(),
|
||||||
externalModel: legacyExternalModelStateSchema,
|
externalModel: legacyExternalModelStateSchema,
|
||||||
|
|
@ -751,7 +776,7 @@ const legacyCsvProjectStateSnapshotSchema = z
|
||||||
const roomAreaProjectStateSnapshotSchema = z
|
const roomAreaProjectStateSnapshotSchema = z
|
||||||
.object({
|
.object({
|
||||||
schemaVersion: z.literal(roomAreaProjectStateSnapshotSchemaVersion),
|
schemaVersion: z.literal(roomAreaProjectStateSnapshotSchemaVersion),
|
||||||
project: projectSchema,
|
project: coverProjectSchema,
|
||||||
distributionBoards: z.array(distributionBoardWithoutFeedingSchema),
|
distributionBoards: z.array(distributionBoardWithoutFeedingSchema),
|
||||||
externalCsvConfiguration: externalCsvConfigurationSnapshotSchema.nullable(),
|
externalCsvConfiguration: externalCsvConfigurationSnapshotSchema.nullable(),
|
||||||
externalModel: legacyExternalModelStateSchema,
|
externalModel: legacyExternalModelStateSchema,
|
||||||
|
|
@ -762,7 +787,7 @@ const roomAreaProjectStateSnapshotSchema = z
|
||||||
const buildingProjectStateSnapshotSchema = z
|
const buildingProjectStateSnapshotSchema = z
|
||||||
.object({
|
.object({
|
||||||
schemaVersion: z.literal(buildingProjectStateSnapshotSchemaVersion),
|
schemaVersion: z.literal(buildingProjectStateSnapshotSchemaVersion),
|
||||||
project: projectSchema,
|
project: coverProjectSchema,
|
||||||
distributionBoards: z.array(distributionBoardWithoutFeedingSchema),
|
distributionBoards: z.array(distributionBoardWithoutFeedingSchema),
|
||||||
externalCsvConfiguration: externalCsvConfigurationSnapshotSchema.nullable(),
|
externalCsvConfiguration: externalCsvConfigurationSnapshotSchema.nullable(),
|
||||||
externalModel: legacyExternalModelStateSchema,
|
externalModel: legacyExternalModelStateSchema,
|
||||||
|
|
@ -773,7 +798,7 @@ const buildingProjectStateSnapshotSchema = z
|
||||||
const coverProjectStateSnapshotSchema = z
|
const coverProjectStateSnapshotSchema = z
|
||||||
.object({
|
.object({
|
||||||
schemaVersion: z.literal(coverProjectStateSnapshotSchemaVersion),
|
schemaVersion: z.literal(coverProjectStateSnapshotSchemaVersion),
|
||||||
project: projectSchema,
|
project: coverProjectSchema,
|
||||||
distributionBoards: z.array(distributionBoardWithoutFeedingSchema),
|
distributionBoards: z.array(distributionBoardWithoutFeedingSchema),
|
||||||
externalCsvConfiguration: externalCsvConfigurationSnapshotSchema.nullable(),
|
externalCsvConfiguration: externalCsvConfigurationSnapshotSchema.nullable(),
|
||||||
externalModel: legacyExternalModelStateSchema,
|
externalModel: legacyExternalModelStateSchema,
|
||||||
|
|
@ -861,6 +886,28 @@ export function parseProjectStateSnapshot(
|
||||||
}
|
}
|
||||||
|
|
||||||
function upgradePreviousProjectStateSnapshot(value: unknown): unknown {
|
function upgradePreviousProjectStateSnapshot(value: unknown): unknown {
|
||||||
|
const withoutSocketLimit = upgradeToWithoutSocketLimitProjectStateSnapshot(value);
|
||||||
|
if (
|
||||||
|
withoutSocketLimit !== null &&
|
||||||
|
typeof withoutSocketLimit === "object" &&
|
||||||
|
!Array.isArray(withoutSocketLimit) &&
|
||||||
|
(withoutSocketLimit as { schemaVersion?: unknown }).schemaVersion ===
|
||||||
|
withoutSocketLimitProjectStateSnapshotSchemaVersion
|
||||||
|
) {
|
||||||
|
const previous =
|
||||||
|
withoutSocketLimitProjectStateSnapshotSchema.parse(withoutSocketLimit);
|
||||||
|
return {
|
||||||
|
...previous,
|
||||||
|
schemaVersion: projectStateSnapshotSchemaVersion,
|
||||||
|
project: { ...previous.project, maxSocketsPerCircuit: null },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return withoutSocketLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function upgradeToWithoutSocketLimitProjectStateSnapshot(
|
||||||
|
value: unknown
|
||||||
|
): unknown {
|
||||||
const legacyCsv = upgradeToLegacyCsvProjectStateSnapshot(value);
|
const legacyCsv = upgradeToLegacyCsvProjectStateSnapshot(value);
|
||||||
if (
|
if (
|
||||||
legacyCsv !== null &&
|
legacyCsv !== null &&
|
||||||
|
|
@ -885,7 +932,7 @@ function upgradePreviousProjectStateSnapshot(value: unknown): unknown {
|
||||||
} = previous;
|
} = previous;
|
||||||
return {
|
return {
|
||||||
...remaining,
|
...remaining,
|
||||||
schemaVersion: projectStateSnapshotSchemaVersion,
|
schemaVersion: withoutSocketLimitProjectStateSnapshotSchemaVersion,
|
||||||
externalModelConfiguration: null,
|
externalModelConfiguration: null,
|
||||||
externalModel: emptyExternalModelState(),
|
externalModel: emptyExternalModelState(),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,12 @@ import {
|
||||||
type ExternalXlsxTransportConfiguration,
|
type ExternalXlsxTransportConfiguration,
|
||||||
} from "../xlsx/external-xlsx-contracts.js";
|
} from "../xlsx/external-xlsx-contracts.js";
|
||||||
|
|
||||||
export const externalModelConfigurationSchemaVersion = 1 as const;
|
// Version 2 adds `deviceTypes[].isSocket`. Version 1 stays readable wherever
|
||||||
|
// it is stored (configuration row, import-batch copies, history commands,
|
||||||
|
// snapshots); socket classification then derives from the family rules
|
||||||
|
// (`resolveSocketDeviceTypeIds`).
|
||||||
|
export const externalModelConfigurationSchemaVersion = 2 as const;
|
||||||
|
export const legacyExternalModelConfigurationSchemaVersion = 1 as const;
|
||||||
export const externalQuantityRules = ["one", "socketCount"] as const;
|
export const externalQuantityRules = ["one", "socketCount"] as const;
|
||||||
export type ExternalQuantityRule = (typeof externalQuantityRules)[number];
|
export type ExternalQuantityRule = (typeof externalQuantityRules)[number];
|
||||||
|
|
||||||
|
|
@ -21,6 +26,8 @@ export interface ExternalDeviceTypeConfiguration {
|
||||||
name: string;
|
name: string;
|
||||||
category: CircuitGroupCategory;
|
category: CircuitGroupCategory;
|
||||||
defaults: ExternalDeviceTypeDefaults;
|
defaults: ExternalDeviceTypeDefaults;
|
||||||
|
// Required from schema version 2, absent in version 1.
|
||||||
|
isSocket?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExternalFamilyTypeRule {
|
export interface ExternalFamilyTypeRule {
|
||||||
|
|
@ -32,7 +39,9 @@ export interface ExternalFamilyTypeRule {
|
||||||
|
|
||||||
export interface ExternalModelConfiguration
|
export interface ExternalModelConfiguration
|
||||||
extends ExternalXlsxTransportConfiguration {
|
extends ExternalXlsxTransportConfiguration {
|
||||||
schemaVersion: typeof externalModelConfigurationSchemaVersion;
|
schemaVersion:
|
||||||
|
| typeof externalModelConfigurationSchemaVersion
|
||||||
|
| typeof legacyExternalModelConfigurationSchemaVersion;
|
||||||
deviceTypes: ExternalDeviceTypeConfiguration[];
|
deviceTypes: ExternalDeviceTypeConfiguration[];
|
||||||
familyTypeRules: ExternalFamilyTypeRule[];
|
familyTypeRules: ExternalFamilyTypeRule[];
|
||||||
}
|
}
|
||||||
|
|
@ -49,7 +58,11 @@ export function createDefaultExternalModelConfiguration(): ExternalModelConfigur
|
||||||
export function assertExternalModelConfiguration(
|
export function assertExternalModelConfiguration(
|
||||||
value: unknown
|
value: unknown
|
||||||
): asserts value is ExternalModelConfiguration {
|
): asserts value is ExternalModelConfiguration {
|
||||||
if (!isRecord(value) || value.schemaVersion !== externalModelConfigurationSchemaVersion) {
|
if (
|
||||||
|
!isRecord(value) ||
|
||||||
|
(value.schemaVersion !== externalModelConfigurationSchemaVersion &&
|
||||||
|
value.schemaVersion !== legacyExternalModelConfigurationSchemaVersion)
|
||||||
|
) {
|
||||||
throw new Error("Nicht unterstützte Revit-XLSX-Konfiguration.");
|
throw new Error("Nicht unterstützte Revit-XLSX-Konfiguration.");
|
||||||
}
|
}
|
||||||
assertExactKeys(value, [
|
assertExactKeys(value, [
|
||||||
|
|
@ -72,7 +85,11 @@ export function assertExternalModelConfiguration(
|
||||||
const deviceTypeIds = new Set<string>();
|
const deviceTypeIds = new Set<string>();
|
||||||
const deviceTypeNames = new Set<string>();
|
const deviceTypeNames = new Set<string>();
|
||||||
for (const [index, deviceType] of value.deviceTypes.entries()) {
|
for (const [index, deviceType] of value.deviceTypes.entries()) {
|
||||||
assertDeviceType(deviceType, index);
|
assertDeviceType(
|
||||||
|
deviceType,
|
||||||
|
index,
|
||||||
|
value.schemaVersion === externalModelConfigurationSchemaVersion
|
||||||
|
);
|
||||||
assertUnique(deviceTypeIds, deviceType.id, "Geräteart-ID");
|
assertUnique(deviceTypeIds, deviceType.id, "Geräteart-ID");
|
||||||
assertUnique(deviceTypeNames, deviceType.name, "Geräteartname");
|
assertUnique(deviceTypeNames, deviceType.name, "Geräteartname");
|
||||||
}
|
}
|
||||||
|
|
@ -153,9 +170,22 @@ function assertColumns(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function assertDeviceType(value: unknown, index: number): asserts value is ExternalDeviceTypeConfiguration {
|
function assertDeviceType(
|
||||||
|
value: unknown,
|
||||||
|
index: number,
|
||||||
|
hasSocketFlag: boolean
|
||||||
|
): asserts value is ExternalDeviceTypeConfiguration {
|
||||||
if (!isRecord(value)) throw new Error(`Geräteart ${index + 1} ist ungültig.`);
|
if (!isRecord(value)) throw new Error(`Geräteart ${index + 1} ist ungültig.`);
|
||||||
assertExactKeys(value, ["id", "name", "category", "defaults"], `Geräteart ${index + 1}`);
|
assertExactKeys(
|
||||||
|
value,
|
||||||
|
hasSocketFlag
|
||||||
|
? ["id", "name", "category", "defaults", "isSocket"]
|
||||||
|
: ["id", "name", "category", "defaults"],
|
||||||
|
`Geräteart ${index + 1}`
|
||||||
|
);
|
||||||
|
if (hasSocketFlag && typeof value.isSocket !== "boolean") {
|
||||||
|
throw new Error(`Geräteart ${index + 1}.isSocket muss ja oder nein sein.`);
|
||||||
|
}
|
||||||
assertUuid(value.id, `Geräteart ${index + 1}.id`);
|
assertUuid(value.id, `Geräteart ${index + 1}.id`);
|
||||||
assertNonEmptyString(value.name, `Geräteart ${index + 1}.name`);
|
assertNonEmptyString(value.name, `Geräteart ${index + 1}.name`);
|
||||||
if (!(["lighting", "single_phase", "three_phase"] as unknown[]).includes(value.category)) {
|
if (!(["lighting", "single_phase", "three_phase"] as unknown[]).includes(value.category)) {
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import {
|
||||||
projectCoverFields,
|
projectCoverFields,
|
||||||
type ProjectCoverField,
|
type ProjectCoverField,
|
||||||
} from "../../shared/constants/project-cover";
|
} from "../../shared/constants/project-cover";
|
||||||
|
import { maxSocketsPerCircuitUpperBound } from "../../shared/constants/socket-limit";
|
||||||
import { FormModal } from "./form-modal";
|
import { FormModal } from "./form-modal";
|
||||||
|
|
||||||
export interface ProjectSettingsInput
|
export interface ProjectSettingsInput
|
||||||
|
|
@ -26,6 +27,7 @@ export interface ProjectSettingsInput
|
||||||
singlePhaseVoltageV: number;
|
singlePhaseVoltageV: number;
|
||||||
threePhaseVoltageV: number;
|
threePhaseVoltageV: number;
|
||||||
enabledDistributionBoardSupplyTypes: DistributionBoardSupplyType[];
|
enabledDistributionBoardSupplyTypes: DistributionBoardSupplyType[];
|
||||||
|
maxSocketsPerCircuit: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const coverFieldPlaceholders: Record<ProjectCoverField, string> = {
|
const coverFieldPlaceholders: Record<ProjectCoverField, string> = {
|
||||||
|
|
@ -93,6 +95,11 @@ export function ProjectSettingsModal({
|
||||||
projectLead: project.projectLead ?? "",
|
projectLead: project.projectLead ?? "",
|
||||||
projectEditor: project.projectEditor ?? "",
|
projectEditor: project.projectEditor ?? "",
|
||||||
});
|
});
|
||||||
|
const [maxSocketsPerCircuit, setMaxSocketsPerCircuit] = useState(
|
||||||
|
project.maxSocketsPerCircuit === null
|
||||||
|
? ""
|
||||||
|
: String(project.maxSocketsPerCircuit)
|
||||||
|
);
|
||||||
const [transfer, setTransfer] = useState<unknown>(null);
|
const [transfer, setTransfer] = useState<unknown>(null);
|
||||||
const [transferFilename, setTransferFilename] = useState("");
|
const [transferFilename, setTransferFilename] = useState("");
|
||||||
const [importMode, setImportMode] = useState<"replace" | "duplicate">(
|
const [importMode, setImportMode] = useState<"replace" | "duplicate">(
|
||||||
|
|
@ -117,6 +124,7 @@ export function ProjectSettingsModal({
|
||||||
workStage: toNullableString(coverValues.workStage),
|
workStage: toNullableString(coverValues.workStage),
|
||||||
projectLead: toNullableString(coverValues.projectLead),
|
projectLead: toNullableString(coverValues.projectLead),
|
||||||
projectEditor: toNullableString(coverValues.projectEditor),
|
projectEditor: toNullableString(coverValues.projectEditor),
|
||||||
|
maxSocketsPerCircuit: toNullableSocketLimit(maxSocketsPerCircuit),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -124,7 +132,8 @@ export function ProjectSettingsModal({
|
||||||
name.trim().length > 0 &&
|
name.trim().length > 0 &&
|
||||||
Number(singlePhaseVoltageV) > 0 &&
|
Number(singlePhaseVoltageV) > 0 &&
|
||||||
Number(threePhaseVoltageV) > 0 &&
|
Number(threePhaseVoltageV) > 0 &&
|
||||||
enabledDistributionBoardSupplyTypes.length > 0;
|
enabledDistributionBoardSupplyTypes.length > 0 &&
|
||||||
|
isSocketLimitInputValid(maxSocketsPerCircuit);
|
||||||
|
|
||||||
function toggleSupplyType(supplyType: DistributionBoardSupplyType) {
|
function toggleSupplyType(supplyType: DistributionBoardSupplyType) {
|
||||||
if (
|
if (
|
||||||
|
|
@ -335,6 +344,30 @@ export function ProjectSettingsModal({
|
||||||
value={threePhaseVoltageV}
|
value={threePhaseVoltageV}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="col-12 col-md-6">
|
||||||
|
<label className="form-label" htmlFor="max-sockets-per-circuit">
|
||||||
|
Max. Steckdosen je Stromkreis
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className={`form-control ${
|
||||||
|
isSocketLimitInputValid(maxSocketsPerCircuit) ? "" : "is-invalid"
|
||||||
|
}`}
|
||||||
|
id="max-sockets-per-circuit"
|
||||||
|
max={maxSocketsPerCircuitUpperBound}
|
||||||
|
min="1"
|
||||||
|
onChange={(event) =>
|
||||||
|
setMaxSocketsPerCircuit(event.target.value)
|
||||||
|
}
|
||||||
|
placeholder="keine Grenze"
|
||||||
|
step="1"
|
||||||
|
type="number"
|
||||||
|
value={maxSocketsPerCircuit}
|
||||||
|
/>
|
||||||
|
<div className="form-text">
|
||||||
|
Vorgabe des Auftraggebers. Gilt bei der Zuordnung von
|
||||||
|
Revit-Steckdosen; leer bedeutet keine Grenze.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
<hr className="my-2" />
|
<hr className="my-2" />
|
||||||
<h3 className="h6">Deckblatt</h3>
|
<h3 className="h6">Deckblatt</h3>
|
||||||
|
|
@ -487,3 +520,19 @@ export function ProjectSettingsModal({
|
||||||
function toNullableString(value: string) {
|
function toNullableString(value: string) {
|
||||||
return value.trim() || null;
|
return value.trim() || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isSocketLimitInputValid(value: string) {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (trimmed === "") return true;
|
||||||
|
const parsed = Number(trimmed);
|
||||||
|
return (
|
||||||
|
Number.isInteger(parsed) &&
|
||||||
|
parsed >= 1 &&
|
||||||
|
parsed <= maxSocketsPerCircuitUpperBound
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toNullableSocketLimit(value: string) {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
return trimmed === "" ? null : Number(trimmed);
|
||||||
|
}
|
||||||
|
|
|
||||||
86
src/frontend/components/revit-device-type-card.tsx
Normal file
86
src/frontend/components/revit-device-type-card.tsx
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { ExternalModelConfiguration } from "../../external-model/configuration/external-model-configuration";
|
||||||
|
import { circuitGroupCategoryLabels } from "../../shared/constants/circuit-group";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Device types of the Revit configuration with their socket flag. Only
|
||||||
|
* device types marked as socket count towards the project's socket limit
|
||||||
|
* per circuit.
|
||||||
|
*/
|
||||||
|
export function RevitDeviceTypeCard({
|
||||||
|
configuration,
|
||||||
|
isSaving,
|
||||||
|
onSave,
|
||||||
|
}: {
|
||||||
|
configuration: ExternalModelConfiguration;
|
||||||
|
isSaving: boolean;
|
||||||
|
onSave: (configuration: ExternalModelConfiguration) => Promise<void>;
|
||||||
|
}) {
|
||||||
|
const ruleCounts = new Map<string, number>();
|
||||||
|
for (const rule of configuration.familyTypeRules) {
|
||||||
|
ruleCounts.set(rule.deviceTypeId, (ruleCounts.get(rule.deviceTypeId) ?? 0) + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSocket(deviceTypeId: string, isSocket: boolean) {
|
||||||
|
void onSave({
|
||||||
|
...configuration,
|
||||||
|
deviceTypes: configuration.deviceTypes.map((deviceType) =>
|
||||||
|
deviceType.id === deviceTypeId ? { ...deviceType, isSocket } : deviceType
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="card mt-3">
|
||||||
|
<details className="card-body">
|
||||||
|
<summary className="h5 mb-0">
|
||||||
|
Gerätearten ({configuration.deviceTypes.length})
|
||||||
|
</summary>
|
||||||
|
<div className="mt-3">
|
||||||
|
{configuration.deviceTypes.length === 0 ? (
|
||||||
|
<p className="text-secondary mb-0">
|
||||||
|
Gerätearten entstehen bei der Familienzuordnung im Import.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<p className="text-secondary small">
|
||||||
|
Als „Steckdose“ markierte Gerätearten zählen für den
|
||||||
|
Höchstwert je Stromkreis aus den Projekteinstellungen.
|
||||||
|
</p>
|
||||||
|
<table className="table table-sm align-middle mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Geräteart</th>
|
||||||
|
<th>Kategorie</th>
|
||||||
|
<th className="text-end">Familien</th>
|
||||||
|
<th className="text-center">Steckdose</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{configuration.deviceTypes.map((deviceType) => (
|
||||||
|
<tr key={deviceType.id}>
|
||||||
|
<td>{deviceType.name}</td>
|
||||||
|
<td>{circuitGroupCategoryLabels[deviceType.category]}</td>
|
||||||
|
<td className="text-end">{ruleCounts.get(deviceType.id) ?? 0}</td>
|
||||||
|
<td className="text-center">
|
||||||
|
<input
|
||||||
|
aria-label={`${deviceType.name} ist Steckdose`}
|
||||||
|
checked={deviceType.isSocket === true}
|
||||||
|
className="form-check-input"
|
||||||
|
disabled={isSaving}
|
||||||
|
onChange={(event) => toggleSocket(deviceType.id, event.target.checked)}
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -40,7 +40,7 @@ export function RevitFollowUpImportWizard({ projectId, expectedRevision, initial
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
const next = structuredClone(configuration);
|
const next = structuredClone(configuration);
|
||||||
const targetTypeId = deviceTypeId ?? createClientId();
|
const targetTypeId = deviceTypeId ?? createClientId();
|
||||||
if (!deviceTypeId) next.deviceTypes.push({ id: targetTypeId, name: familyAndType, category, defaults: { powerPerUnitW: null, simultaneityFactor: 1, cosPhi: null, costGroup: null } });
|
if (!deviceTypeId) next.deviceTypes.push({ id: targetTypeId, name: familyAndType, category, defaults: { powerPerUnitW: null, simultaneityFactor: 1, cosPhi: null, costGroup: null }, ...(next.schemaVersion === 2 ? { isSocket: quantityRule === "socketCount" } : {}) });
|
||||||
next.familyTypeRules.push({ id: createClientId(), exactFamilyAndType: familyAndType, deviceTypeId: targetTypeId, quantityRule });
|
next.familyTypeRules.push({ id: createClientId(), exactFamilyAndType: familyAndType, deviceTypeId: targetTypeId, quantityRule });
|
||||||
setConfiguration(next); await loadPlan(file, next);
|
setConfiguration(next); await loadPlan(file, next);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ export function RevitInitialImportWizard({ projectId, expectedRevision, initialC
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
const deviceTypeId = createClientId();
|
const deviceTypeId = createClientId();
|
||||||
const next = structuredClone(configuration);
|
const next = structuredClone(configuration);
|
||||||
next.deviceTypes.push({ id: deviceTypeId, name: familyAndType, category, defaults: { powerPerUnitW: null, simultaneityFactor: 1, cosPhi: null, costGroup: null } });
|
next.deviceTypes.push({ id: deviceTypeId, name: familyAndType, category, defaults: { powerPerUnitW: null, simultaneityFactor: 1, cosPhi: null, costGroup: null }, ...(next.schemaVersion === 2 ? { isSocket: quantityRule === "socketCount" } : {}) });
|
||||||
next.familyTypeRules.push({ id: createClientId(), exactFamilyAndType: familyAndType, deviceTypeId, quantityRule });
|
next.familyTypeRules.push({ id: createClientId(), exactFamilyAndType: familyAndType, deviceTypeId, quantityRule });
|
||||||
setConfiguration(next);
|
setConfiguration(next);
|
||||||
await loadPlan(file, next);
|
await loadPlan(file, next);
|
||||||
|
|
|
||||||
|
|
@ -194,6 +194,7 @@ export interface ProjectDto {
|
||||||
workStage: string | null;
|
workStage: string | null;
|
||||||
projectLead: string | null;
|
projectLead: string | null;
|
||||||
projectEditor: string | null;
|
projectEditor: string | null;
|
||||||
|
maxSocketsPerCircuit: number | null;
|
||||||
currentRevision: number;
|
currentRevision: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -446,7 +446,7 @@ export function updateProjectSettings(
|
||||||
singlePhaseVoltageV: number;
|
singlePhaseVoltageV: number;
|
||||||
threePhaseVoltageV: number;
|
threePhaseVoltageV: number;
|
||||||
enabledDistributionBoardSupplyTypes: ProjectDto["enabledDistributionBoardSupplyTypes"];
|
enabledDistributionBoardSupplyTypes: ProjectDto["enabledDistributionBoardSupplyTypes"];
|
||||||
} & Partial<Pick<ProjectDto, "siteAddress" | "workStage" | "projectLead" | "projectEditor">>
|
} & Partial<Pick<ProjectDto, "siteAddress" | "workStage" | "projectLead" | "projectEditor" | "maxSocketsPerCircuit">>
|
||||||
) {
|
) {
|
||||||
return request<ProjectSettingsCommandResultDto>(
|
return request<ProjectSettingsCommandResultDto>(
|
||||||
`/api/projects/${projectId}`,
|
`/api/projects/${projectId}`,
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import type { ExternalModelConfiguration } from "../../external-model/configurat
|
||||||
|
|
||||||
export function createDefaultRevitXlsxConfiguration(): ExternalModelConfiguration {
|
export function createDefaultRevitXlsxConfiguration(): ExternalModelConfiguration {
|
||||||
return {
|
return {
|
||||||
schemaVersion: 1,
|
schemaVersion: 2,
|
||||||
layout: { headerRow: 1, firstDataRow: 3, firstDataColumn: 1 },
|
layout: { headerRow: 1, firstDataRow: 3, firstDataColumn: 1 },
|
||||||
circuitMode: "combined",
|
circuitMode: "combined",
|
||||||
columns: {
|
columns: {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import { createExternalModelConfigurationUpdateProjectCommand } from "../../domain/models/external-model-configuration-project-command.model.js";
|
import { createExternalModelConfigurationUpdateProjectCommand } from "../../domain/models/external-model-configuration-project-command.model.js";
|
||||||
import { assertExternalModelConfiguration } from "../../external-model/configuration/external-model-configuration.js";
|
import { assertExternalModelConfiguration } from "../../external-model/configuration/external-model-configuration.js";
|
||||||
|
import { withSocketDeviceTypeFlags } from "../../shared/revit/socket-device-types.js";
|
||||||
import { parseExternalXlsx } from "../../external-model/xlsx/external-xlsx-transport.js";
|
import { parseExternalXlsx } from "../../external-model/xlsx/external-xlsx-transport.js";
|
||||||
import { externalModelConfigurationRepository } from "../composition/application-repositories.js";
|
import { externalModelConfigurationRepository } from "../composition/application-repositories.js";
|
||||||
import { projectCommandService } from "../composition/project-command-stores.js";
|
import { projectCommandService } from "../composition/project-command-stores.js";
|
||||||
|
|
@ -11,7 +12,16 @@ export function getExternalModelConfiguration(req: Request, res: Response) {
|
||||||
const projectId = String(req.params.projectId ?? "");
|
const projectId = String(req.params.projectId ?? "");
|
||||||
const result = externalModelConfigurationRepository.getByProject(projectId);
|
const result = externalModelConfigurationRepository.getByProject(projectId);
|
||||||
if (!result.projectExists) return res.status(404).json({ error: "Projekt nicht gefunden." });
|
if (!result.projectExists) return res.status(404).json({ error: "Projekt nicht gefunden." });
|
||||||
return res.json({ configuration: result.configuration });
|
// Clients always edit version 2; a stored version 1 is upgraded here so a
|
||||||
|
// later save writes the explicit socket flags.
|
||||||
|
return res.json({
|
||||||
|
configuration: result.configuration === null
|
||||||
|
? null
|
||||||
|
: {
|
||||||
|
...result.configuration,
|
||||||
|
configuration: withSocketDeviceTypeFlags(result.configuration.configuration),
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateExternalModelConfiguration(req: Request, res: Response) {
|
export function updateExternalModelConfiguration(req: Request, res: Response) {
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,10 @@ export async function updateProjectSettings(req: Request, res: Response) {
|
||||||
command: createProjectSettingsUpdateProjectCommand({
|
command: createProjectSettingsUpdateProjectCommand({
|
||||||
...settings,
|
...settings,
|
||||||
...coverValues,
|
...coverValues,
|
||||||
|
maxSocketsPerCircuit:
|
||||||
|
settings.maxSocketsPerCircuit === undefined
|
||||||
|
? current.maxSocketsPerCircuit
|
||||||
|
: settings.maxSocketsPerCircuit,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const project = await projectRepository.findById(projectId);
|
const project = await projectRepository.findById(projectId);
|
||||||
|
|
|
||||||
19
src/shared/constants/socket-limit.ts
Normal file
19
src/shared/constants/socket-limit.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
/**
|
||||||
|
* Client-specific highest number of effective sockets per circuit. It is an
|
||||||
|
* ordinary project setting (`project.update-settings`, logical snapshots and
|
||||||
|
* project transfer); `null` means no limit. Only Revit objects whose device
|
||||||
|
* type is marked as socket count towards it.
|
||||||
|
*/
|
||||||
|
export const maxSocketsPerCircuitUpperBound = 1000;
|
||||||
|
|
||||||
|
export function isValidMaxSocketsPerCircuit(
|
||||||
|
value: unknown
|
||||||
|
): value is number | null {
|
||||||
|
return (
|
||||||
|
value === null ||
|
||||||
|
(typeof value === "number" &&
|
||||||
|
Number.isInteger(value) &&
|
||||||
|
value >= 1 &&
|
||||||
|
value <= maxSocketsPerCircuitUpperBound)
|
||||||
|
);
|
||||||
|
}
|
||||||
61
src/shared/revit/socket-device-types.ts
Normal file
61
src/shared/revit/socket-device-types.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
/**
|
||||||
|
* Socket classification of Revit device types, shared by API and frontend.
|
||||||
|
*
|
||||||
|
* Configuration schema version 2 stores an explicit `isSocket` flag per
|
||||||
|
* device type. Version 1 has no flag; there a device type counts as socket
|
||||||
|
* when one of its family rules takes the quantity from the socket-count
|
||||||
|
* column. This file has no imports because Turbopack resolves frontend
|
||||||
|
* imports without the `.js` suffix the API build needs.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const socketFlagConfigurationSchemaVersion = 2 as const;
|
||||||
|
|
||||||
|
export interface SocketClassificationConfiguration {
|
||||||
|
schemaVersion: number;
|
||||||
|
deviceTypes: ReadonlyArray<{ id: string; isSocket?: boolean }>;
|
||||||
|
familyTypeRules: ReadonlyArray<{ deviceTypeId: string; quantityRule: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deriveSocketDeviceTypeIds(
|
||||||
|
familyTypeRules: SocketClassificationConfiguration["familyTypeRules"]
|
||||||
|
): Set<string> {
|
||||||
|
return new Set(
|
||||||
|
familyTypeRules
|
||||||
|
.filter((rule) => rule.quantityRule === "socketCount")
|
||||||
|
.map((rule) => rule.deviceTypeId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveSocketDeviceTypeIds(
|
||||||
|
configuration: SocketClassificationConfiguration | null
|
||||||
|
): Set<string> {
|
||||||
|
if (configuration === null) return new Set();
|
||||||
|
if (configuration.schemaVersion >= socketFlagConfigurationSchemaVersion) {
|
||||||
|
return new Set(
|
||||||
|
configuration.deviceTypes
|
||||||
|
.filter((deviceType) => deviceType.isSocket === true)
|
||||||
|
.map((deviceType) => deviceType.id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return deriveSocketDeviceTypeIds(configuration.familyTypeRules);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns a version-2 configuration; version 1 derives the socket flags. */
|
||||||
|
export function withSocketDeviceTypeFlags<
|
||||||
|
T extends SocketClassificationConfiguration & {
|
||||||
|
deviceTypes: Array<{ id: string; isSocket?: boolean }>;
|
||||||
|
},
|
||||||
|
>(configuration: T): T {
|
||||||
|
if (configuration.schemaVersion >= socketFlagConfigurationSchemaVersion) {
|
||||||
|
return configuration;
|
||||||
|
}
|
||||||
|
const socketIds = deriveSocketDeviceTypeIds(configuration.familyTypeRules);
|
||||||
|
return {
|
||||||
|
...configuration,
|
||||||
|
schemaVersion: socketFlagConfigurationSchemaVersion,
|
||||||
|
deviceTypes: configuration.deviceTypes.map((deviceType) => ({
|
||||||
|
...deviceType,
|
||||||
|
isSocket: socketIds.has(deviceType.id),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
projectCoverFieldMaxLengths,
|
projectCoverFieldMaxLengths,
|
||||||
type ProjectCoverField,
|
type ProjectCoverField,
|
||||||
} from "../constants/project-cover.js";
|
} from "../constants/project-cover.js";
|
||||||
|
import { maxSocketsPerCircuitUpperBound } from "../constants/socket-limit.js";
|
||||||
import { expectedProjectRevisionSchema } from "./project-command.schemas.js";
|
import { expectedProjectRevisionSchema } from "./project-command.schemas.js";
|
||||||
|
|
||||||
// Empty text is stored as null so "not entered" has one representation.
|
// Empty text is stored as null so "not entered" has one representation.
|
||||||
|
|
@ -53,6 +54,14 @@ export const updateProjectSettingsSchema = z
|
||||||
workStage: optionalCoverValueSchema("workStage"),
|
workStage: optionalCoverValueSchema("workStage"),
|
||||||
projectLead: optionalCoverValueSchema("projectLead"),
|
projectLead: optionalCoverValueSchema("projectLead"),
|
||||||
projectEditor: optionalCoverValueSchema("projectEditor"),
|
projectEditor: optionalCoverValueSchema("projectEditor"),
|
||||||
|
// Socket limit per circuit; null = no limit, omitted = unchanged.
|
||||||
|
maxSocketsPerCircuit: z
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.min(1)
|
||||||
|
.max(maxSocketsPerCircuitUpperBound)
|
||||||
|
.nullable()
|
||||||
|
.optional(),
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ import { createRevitXlsxFixture, externalXlsxTestConfiguration, loadXlsxWorkbook
|
||||||
|
|
||||||
const deviceTypeA = "10000000-0000-4000-8000-000000000001";
|
const deviceTypeA = "10000000-0000-4000-8000-000000000001";
|
||||||
const deviceTypeB = "10000000-0000-4000-8000-000000000002";
|
const deviceTypeB = "10000000-0000-4000-8000-000000000002";
|
||||||
const configuration = { ...createDefaultExternalModelConfiguration(), ...externalXlsxTestConfiguration, deviceTypes: [{ id: deviceTypeA, name: "Steckdose", category: "single_phase" as const, defaults: { powerPerUnitW: null, simultaneityFactor: 1, cosPhi: 0.9, costGroup: null } }, { id: deviceTypeB, name: "Leuchte", category: "lighting" as const, defaults: { powerPerUnitW: 40, simultaneityFactor: 1, cosPhi: null, costGroup: null } }], familyTypeRules: [{ id: "20000000-0000-4000-8000-000000000001", exactFamilyAndType: "Steckdose: Standard", deviceTypeId: deviceTypeA, quantityRule: "socketCount" as const }, { id: "20000000-0000-4000-8000-000000000002", exactFamilyAndType: "Leuchte: Decke", deviceTypeId: deviceTypeB, quantityRule: "one" as const }] };
|
const configuration = { ...createDefaultExternalModelConfiguration(), ...externalXlsxTestConfiguration, deviceTypes: [{ id: deviceTypeA, name: "Steckdose", category: "single_phase" as const, defaults: { powerPerUnitW: null, simultaneityFactor: 1, cosPhi: 0.9, costGroup: null }, isSocket: true }, { id: deviceTypeB, name: "Leuchte", category: "lighting" as const, defaults: { powerPerUnitW: 40, simultaneityFactor: 1, cosPhi: null, costGroup: null }, isSocket: false }], familyTypeRules: [{ id: "20000000-0000-4000-8000-000000000001", exactFamilyAndType: "Steckdose: Standard", deviceTypeId: deviceTypeA, quantityRule: "socketCount" as const }, { id: "20000000-0000-4000-8000-000000000002", exactFamilyAndType: "Leuchte: Decke", deviceTypeId: deviceTypeB, quantityRule: "one" as const }] };
|
||||||
const catalogs = { floors: [], rooms: [], distributionBoards: [], projectDevices: [] };
|
const catalogs = { floors: [], rooms: [], distributionBoards: [], projectDevices: [] };
|
||||||
|
|
||||||
describe("external follow-up import project command", () => {
|
describe("external follow-up import project command", () => {
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,10 @@ import {
|
||||||
assertExternalModelConfiguration,
|
assertExternalModelConfiguration,
|
||||||
createDefaultExternalModelConfiguration,
|
createDefaultExternalModelConfiguration,
|
||||||
} from "../src/external-model/configuration/external-model-configuration.js";
|
} from "../src/external-model/configuration/external-model-configuration.js";
|
||||||
|
import {
|
||||||
|
resolveSocketDeviceTypeIds,
|
||||||
|
withSocketDeviceTypeFlags,
|
||||||
|
} from "../src/shared/revit/socket-device-types.js";
|
||||||
|
|
||||||
const deviceTypeId = "10000000-0000-4000-8000-000000000001";
|
const deviceTypeId = "10000000-0000-4000-8000-000000000001";
|
||||||
const ruleId = "20000000-0000-4000-8000-000000000001";
|
const ruleId = "20000000-0000-4000-8000-000000000001";
|
||||||
|
|
@ -25,6 +29,7 @@ function configuration() {
|
||||||
name: "Steckdose",
|
name: "Steckdose",
|
||||||
category: "single_phase",
|
category: "single_phase",
|
||||||
defaults: { powerPerUnitW: 200, simultaneityFactor: null, cosPhi: 0.95, costGroup: "440" },
|
defaults: { powerPerUnitW: 200, simultaneityFactor: null, cosPhi: 0.95, costGroup: "440" },
|
||||||
|
isSocket: true,
|
||||||
});
|
});
|
||||||
value.familyTypeRules.push({
|
value.familyTypeRules.push({
|
||||||
id: ruleId,
|
id: ruleId,
|
||||||
|
|
@ -61,6 +66,27 @@ describe("external model configuration", () => {
|
||||||
assert.throws(() => assertExternalModelConfiguration(unknownType), /unbekannte Ger.*teart/);
|
assert.throws(() => assertExternalModelConfiguration(unknownType), /unbekannte Ger.*teart/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("requires the socket flag in version 2 and keeps version 1 readable", () => {
|
||||||
|
const missingFlag = configuration();
|
||||||
|
delete missingFlag.deviceTypes[0]!.isSocket;
|
||||||
|
assert.throws(() => assertExternalModelConfiguration(missingFlag), /unbekannte oder fehlende Felder/);
|
||||||
|
|
||||||
|
const legacy = { ...configuration(), schemaVersion: 1 as const };
|
||||||
|
assert.throws(() => assertExternalModelConfiguration(legacy), /unbekannte oder fehlende Felder/);
|
||||||
|
delete legacy.deviceTypes[0]!.isSocket;
|
||||||
|
assert.doesNotThrow(() => assertExternalModelConfiguration(legacy));
|
||||||
|
|
||||||
|
assert.deepEqual([...resolveSocketDeviceTypeIds(legacy)], [deviceTypeId]);
|
||||||
|
const upgraded = withSocketDeviceTypeFlags(legacy);
|
||||||
|
assert.equal(upgraded.schemaVersion, 2);
|
||||||
|
assert.equal(upgraded.deviceTypes[0]!.isSocket, true);
|
||||||
|
assert.doesNotThrow(() => assertExternalModelConfiguration(upgraded));
|
||||||
|
|
||||||
|
const notSocket = configuration();
|
||||||
|
notSocket.deviceTypes[0]!.isSocket = false;
|
||||||
|
assert.equal(resolveSocketDeviceTypeIds(notSocket).size, 0);
|
||||||
|
});
|
||||||
|
|
||||||
it("persists changes without an independent configuration revision", () => {
|
it("persists changes without an independent configuration revision", () => {
|
||||||
const context = createTestDatabase();
|
const context = createTestDatabase();
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ const deviceTypeA = "10000000-0000-4000-8000-000000000001";
|
||||||
const deviceTypeB = "10000000-0000-4000-8000-000000000002";
|
const deviceTypeB = "10000000-0000-4000-8000-000000000002";
|
||||||
const newWorksharingId = "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE-00000003";
|
const newWorksharingId = "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE-00000003";
|
||||||
const catalogs = { floors: [], rooms: [], distributionBoards: [], projectDevices: [] };
|
const catalogs = { floors: [], rooms: [], distributionBoards: [], projectDevices: [] };
|
||||||
const configuration = { ...createDefaultExternalModelConfiguration(), ...externalXlsxTestConfiguration, deviceTypes: [{ id: deviceTypeA, name: "Steckdose", category: "single_phase" as const, defaults: { powerPerUnitW: null, simultaneityFactor: 1, cosPhi: 0.9, costGroup: null } }, { id: deviceTypeB, name: "Leuchte", category: "lighting" as const, defaults: { powerPerUnitW: 40, simultaneityFactor: 1, cosPhi: null, costGroup: null } }], familyTypeRules: [{ id: "20000000-0000-4000-8000-000000000001", exactFamilyAndType: "Steckdose: Standard", deviceTypeId: deviceTypeA, quantityRule: "socketCount" as const }, { id: "20000000-0000-4000-8000-000000000002", exactFamilyAndType: "Leuchte: Decke", deviceTypeId: deviceTypeB, quantityRule: "one" as const }] };
|
const configuration = { ...createDefaultExternalModelConfiguration(), ...externalXlsxTestConfiguration, deviceTypes: [{ id: deviceTypeA, name: "Steckdose", category: "single_phase" as const, defaults: { powerPerUnitW: null, simultaneityFactor: 1, cosPhi: 0.9, costGroup: null }, isSocket: true }, { id: deviceTypeB, name: "Leuchte", category: "lighting" as const, defaults: { powerPerUnitW: 40, simultaneityFactor: 1, cosPhi: null, costGroup: null }, isSocket: false }], familyTypeRules: [{ id: "20000000-0000-4000-8000-000000000001", exactFamilyAndType: "Steckdose: Standard", deviceTypeId: deviceTypeA, quantityRule: "socketCount" as const }, { id: "20000000-0000-4000-8000-000000000002", exactFamilyAndType: "Leuchte: Decke", deviceTypeId: deviceTypeB, quantityRule: "one" as const }] };
|
||||||
|
|
||||||
async function initialState() {
|
async function initialState() {
|
||||||
const bytes = await createRevitXlsxFixture();
|
const bytes = await createRevitXlsxFixture();
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,8 @@ function configured() {
|
||||||
...createDefaultExternalModelConfiguration(),
|
...createDefaultExternalModelConfiguration(),
|
||||||
...externalXlsxTestConfiguration,
|
...externalXlsxTestConfiguration,
|
||||||
deviceTypes: [
|
deviceTypes: [
|
||||||
{ id: deviceTypeA, name: "Steckdose", category: "single_phase" as const, defaults: { powerPerUnitW: null, simultaneityFactor: null, cosPhi: 0.9, costGroup: "440" } },
|
{ id: deviceTypeA, name: "Steckdose", category: "single_phase" as const, defaults: { powerPerUnitW: null, simultaneityFactor: null, cosPhi: 0.9, costGroup: "440" }, isSocket: true },
|
||||||
{ id: deviceTypeB, name: "Leuchte", category: "lighting" as const, defaults: { powerPerUnitW: 40, simultaneityFactor: 0.8, cosPhi: null, costGroup: null } },
|
{ id: deviceTypeB, name: "Leuchte", category: "lighting" as const, defaults: { powerPerUnitW: 40, simultaneityFactor: 0.8, cosPhi: null, costGroup: null }, isSocket: false },
|
||||||
],
|
],
|
||||||
familyTypeRules: [
|
familyTypeRules: [
|
||||||
{ id: "20000000-0000-4000-8000-000000000001", exactFamilyAndType: "Steckdose: Standard", deviceTypeId: deviceTypeA, quantityRule: "socketCount" as const },
|
{ id: "20000000-0000-4000-8000-000000000001", exactFamilyAndType: "Steckdose: Standard", deviceTypeId: deviceTypeA, quantityRule: "socketCount" as const },
|
||||||
|
|
|
||||||
|
|
@ -1081,6 +1081,7 @@ describe("project command service", () => {
|
||||||
workStage: null,
|
workStage: null,
|
||||||
projectLead: null,
|
projectLead: null,
|
||||||
projectEditor: null,
|
projectEditor: null,
|
||||||
|
maxSocketsPerCircuit: null,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
assert.equal(updated.history.currentRevision, 1);
|
assert.equal(updated.history.currentRevision, 1);
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ function getSettings(context: DatabaseContext) {
|
||||||
workStage: projects.workStage,
|
workStage: projects.workStage,
|
||||||
projectLead: projects.projectLead,
|
projectLead: projects.projectLead,
|
||||||
projectEditor: projects.projectEditor,
|
projectEditor: projects.projectEditor,
|
||||||
|
maxSocketsPerCircuit: projects.maxSocketsPerCircuit,
|
||||||
currentRevision: projects.currentRevision,
|
currentRevision: projects.currentRevision,
|
||||||
})
|
})
|
||||||
.from(projects)
|
.from(projects)
|
||||||
|
|
@ -91,6 +92,7 @@ function settings(
|
||||||
workStage: null,
|
workStage: null,
|
||||||
projectLead: null,
|
projectLead: null,
|
||||||
projectEditor: null,
|
projectEditor: null,
|
||||||
|
maxSocketsPerCircuit: null,
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -231,6 +233,7 @@ describe("project settings project-command repository", () => {
|
||||||
workStage: null,
|
workStage: null,
|
||||||
projectLead: null,
|
projectLead: null,
|
||||||
projectEditor: null,
|
projectEditor: null,
|
||||||
|
maxSocketsPerCircuit: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
@ -307,6 +310,7 @@ describe("project settings project-command repository", () => {
|
||||||
workStage: null,
|
workStage: null,
|
||||||
projectLead: null,
|
projectLead: null,
|
||||||
projectEditor: null,
|
projectEditor: null,
|
||||||
|
maxSocketsPerCircuit: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const undoStep = history.getNextCommand("project-1", "undo");
|
const undoStep = history.getNextCommand("project-1", "undo");
|
||||||
|
|
@ -339,6 +343,7 @@ describe("project settings project-command repository", () => {
|
||||||
workStage: null,
|
workStage: null,
|
||||||
projectLead: null,
|
projectLead: null,
|
||||||
projectEditor: null,
|
projectEditor: null,
|
||||||
|
maxSocketsPerCircuit: null,
|
||||||
currentRevision: 2,
|
currentRevision: 2,
|
||||||
});
|
});
|
||||||
assert.deepEqual(getDerivedVoltages(context), {
|
assert.deepEqual(getDerivedVoltages(context), {
|
||||||
|
|
@ -382,6 +387,7 @@ describe("project settings project-command repository", () => {
|
||||||
workStage: null,
|
workStage: null,
|
||||||
projectLead: null,
|
projectLead: null,
|
||||||
projectEditor: null,
|
projectEditor: null,
|
||||||
|
maxSocketsPerCircuit: null,
|
||||||
currentRevision: 3,
|
currentRevision: 3,
|
||||||
});
|
});
|
||||||
assert.deepEqual(getDerivedVoltages(context), {
|
assert.deepEqual(getDerivedVoltages(context), {
|
||||||
|
|
@ -495,7 +501,7 @@ describe("project settings project-command repository", () => {
|
||||||
projectLead: "P. Leitung",
|
projectLead: "P. Leitung",
|
||||||
projectEditor: "B. Arbeiter",
|
projectEditor: "B. Arbeiter",
|
||||||
});
|
});
|
||||||
assert.equal(forward.inverse.schemaVersion, 3);
|
assert.equal(forward.inverse.schemaVersion, 4);
|
||||||
|
|
||||||
const undoStep = new ProjectHistoryRepository(
|
const undoStep = new ProjectHistoryRepository(
|
||||||
context.db
|
context.db
|
||||||
|
|
@ -543,6 +549,7 @@ describe("project settings project-command repository", () => {
|
||||||
workStage: _workStage,
|
workStage: _workStage,
|
||||||
projectLead: _projectLead,
|
projectLead: _projectLead,
|
||||||
projectEditor: _projectEditor,
|
projectEditor: _projectEditor,
|
||||||
|
maxSocketsPerCircuit: _maxSocketsPerCircuit,
|
||||||
...versionTwoPayload
|
...versionTwoPayload
|
||||||
} = settings({ name: "Stand aus Version 2" });
|
} = settings({ name: "Stand aus Version 2" });
|
||||||
const forward = repository.executeUpdate({
|
const forward = repository.executeUpdate({
|
||||||
|
|
@ -570,6 +577,83 @@ describe("project settings project-command repository", () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("persists the socket limit with undo and keeps it for version 3 commands", () => {
|
||||||
|
const context = createTestDatabase();
|
||||||
|
try {
|
||||||
|
const repository = new ProjectSettingsProjectCommandRepository(
|
||||||
|
context.db
|
||||||
|
);
|
||||||
|
const readLimit = () =>
|
||||||
|
context.db
|
||||||
|
.select({ maxSocketsPerCircuit: projects.maxSocketsPerCircuit })
|
||||||
|
.from(projects)
|
||||||
|
.where(eq(projects.id, "project-1"))
|
||||||
|
.get()?.maxSocketsPerCircuit;
|
||||||
|
const forward = repository.executeUpdate({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 0,
|
||||||
|
source: "user",
|
||||||
|
command: createProjectSettingsUpdateProjectCommand(
|
||||||
|
settings({ maxSocketsPerCircuit: 8 })
|
||||||
|
),
|
||||||
|
});
|
||||||
|
assert.equal(readLimit(), 8);
|
||||||
|
assert.equal(forward.inverse.schemaVersion, 4);
|
||||||
|
assert.equal(
|
||||||
|
(forward.inverse.payload as { maxSocketsPerCircuit: unknown })
|
||||||
|
.maxSocketsPerCircuit,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
maxSocketsPerCircuit: _maxSocketsPerCircuit,
|
||||||
|
...versionThreePayload
|
||||||
|
} = settings({ name: "Stand aus Version 3" });
|
||||||
|
const versionThree = repository.executeUpdate({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 1,
|
||||||
|
source: "user",
|
||||||
|
command: {
|
||||||
|
schemaVersion: 3,
|
||||||
|
type: "project.update-settings",
|
||||||
|
payload: versionThreePayload,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
assert.equal(readLimit(), 8);
|
||||||
|
assert.equal(versionThree.inverse.schemaVersion, 3);
|
||||||
|
assert.equal(
|
||||||
|
"maxSocketsPerCircuit" in versionThree.inverse.payload,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
const history = new ProjectHistoryRepository(context.db);
|
||||||
|
for (const expectedRevision of [2, 3]) {
|
||||||
|
const undoStep = history.getNextCommand("project-1", "undo");
|
||||||
|
assert.ok(undoStep);
|
||||||
|
repository.executeUpdate({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision,
|
||||||
|
source: "undo",
|
||||||
|
historyTargetChangeSetId: undoStep.changeSetId,
|
||||||
|
command: undoStep.command as never,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
assert.equal(readLimit(), null);
|
||||||
|
|
||||||
|
for (const invalid of [0, 2.5, 1001]) {
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
createProjectSettingsUpdateProjectCommand(
|
||||||
|
settings({ maxSocketsPerCircuit: invalid })
|
||||||
|
),
|
||||||
|
/invalid values/
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
context.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("persists the project supply-type selection and rejects disabling an in-use type", () => {
|
it("persists the project supply-type selection and rejects disabling an in-use type", () => {
|
||||||
const context = createTestDatabase();
|
const context = createTestDatabase();
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -474,7 +474,7 @@ describe("project snapshot repository", () => {
|
||||||
for (const room of payload.rooms) {
|
for (const room of payload.rooms) {
|
||||||
delete room.areaM2;
|
delete room.areaM2;
|
||||||
}
|
}
|
||||||
for (const field of ["siteAddress", "workStage", "projectLead", "projectEditor"]) {
|
for (const field of ["siteAddress", "workStage", "projectLead", "projectEditor", "maxSocketsPerCircuit"]) {
|
||||||
delete payload.project[field];
|
delete payload.project[field];
|
||||||
}
|
}
|
||||||
for (const board of payload.distributionBoards) {
|
for (const board of payload.distributionBoards) {
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,19 @@ import { createDefaultExternalCsvConfiguration } from "../src/external-model/csv
|
||||||
import { externalCsvTestColumns } from "./fixtures/revit-csv-fixtures.js";
|
import { externalCsvTestColumns } from "./fixtures/revit-csv-fixtures.js";
|
||||||
import { zodIssues } from "./support/zod-assertions.js";
|
import { zodIssues } from "./support/zod-assertions.js";
|
||||||
|
|
||||||
/** Removes the fields introduced by snapshot schema version 9. */
|
/** Removes the field introduced by snapshot schema version 11. */
|
||||||
|
function withoutVersionElevenFields<T extends { project: object }>(
|
||||||
|
snapshot: T
|
||||||
|
): T {
|
||||||
|
delete (snapshot.project as Record<string, unknown>).maxSocketsPerCircuit;
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Removes the fields introduced by snapshot schema versions 9 to 11. */
|
||||||
function withoutVersionNineFields<
|
function withoutVersionNineFields<
|
||||||
T extends { distributionBoards: object[] },
|
T extends { project: object; distributionBoards: object[] },
|
||||||
>(snapshot: T): T {
|
>(snapshot: T): T {
|
||||||
|
withoutVersionElevenFields(snapshot);
|
||||||
const record = snapshot as Record<string, unknown>;
|
const record = snapshot as Record<string, unknown>;
|
||||||
record.externalCsvConfiguration = null;
|
record.externalCsvConfiguration = null;
|
||||||
delete record.externalModelConfiguration;
|
delete record.externalModelConfiguration;
|
||||||
|
|
@ -29,7 +38,7 @@ function withoutVersionNineFields<
|
||||||
|
|
||||||
/** Removes the fields introduced by snapshot schema version 7. */
|
/** Removes the fields introduced by snapshot schema version 7. */
|
||||||
function withoutVersionSevenFields<
|
function withoutVersionSevenFields<
|
||||||
T extends { floors: object[]; distributionBoards: object[] },
|
T extends { project: object; floors: object[]; distributionBoards: object[] },
|
||||||
>(snapshot: T): T {
|
>(snapshot: T): T {
|
||||||
withoutVersionNineFields(snapshot);
|
withoutVersionNineFields(snapshot);
|
||||||
delete (snapshot as Record<string, unknown>).buildings;
|
delete (snapshot as Record<string, unknown>).buildings;
|
||||||
|
|
@ -82,6 +91,7 @@ function minimalSnapshot() {
|
||||||
workStage: "LPH 5" as string | null,
|
workStage: "LPH 5" as string | null,
|
||||||
projectLead: "P. Leitung" as string | null,
|
projectLead: "P. Leitung" as string | null,
|
||||||
projectEditor: null as string | null,
|
projectEditor: null as string | null,
|
||||||
|
maxSocketsPerCircuit: null as number | null,
|
||||||
},
|
},
|
||||||
distributionBoards: [],
|
distributionBoards: [],
|
||||||
externalModelConfiguration: null,
|
externalModelConfiguration: null,
|
||||||
|
|
@ -351,6 +361,24 @@ describe("project state snapshot model", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps the socket limit in version 11 and upgrades version 10 without a limit", () => {
|
||||||
|
const current = structuredClone(minimalSnapshot());
|
||||||
|
current.project.maxSocketsPerCircuit = 8;
|
||||||
|
assert.equal(parseProjectStateSnapshot(current).project.maxSocketsPerCircuit, 8);
|
||||||
|
|
||||||
|
const previous = withoutVersionElevenFields(structuredClone(minimalSnapshot()));
|
||||||
|
previous.schemaVersion = 10 as typeof previous.schemaVersion;
|
||||||
|
const upgraded = parseProjectStateSnapshot(previous);
|
||||||
|
assert.equal(upgraded.schemaVersion, projectStateSnapshotSchemaVersion);
|
||||||
|
assert.equal(upgraded.project.maxSocketsPerCircuit, null);
|
||||||
|
|
||||||
|
const invalid = structuredClone(minimalSnapshot());
|
||||||
|
invalid.project.maxSocketsPerCircuit = 0;
|
||||||
|
assert.throws(() => parseProjectStateSnapshot(invalid));
|
||||||
|
const missing = withoutVersionElevenFields(structuredClone(minimalSnapshot()));
|
||||||
|
assert.throws(() => parseProjectStateSnapshot(missing));
|
||||||
|
});
|
||||||
|
|
||||||
it("upgrades snapshot version 3 with an empty external model", () => {
|
it("upgrades snapshot version 3 with an empty external model", () => {
|
||||||
const previous = withoutVersionSixFields(structuredClone(minimalSnapshot()));
|
const previous = withoutVersionSixFields(structuredClone(minimalSnapshot()));
|
||||||
previous.schemaVersion = 3 as typeof previous.schemaVersion;
|
previous.schemaVersion = 3 as typeof previous.schemaVersion;
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ const circuitId = "circuit-e2e";
|
||||||
const rowId = "row-e2e";
|
const rowId = "row-e2e";
|
||||||
const deviceTypeA = "10000000-0000-4000-8000-000000000001";
|
const deviceTypeA = "10000000-0000-4000-8000-000000000001";
|
||||||
const deviceTypeB = "10000000-0000-4000-8000-000000000002";
|
const deviceTypeB = "10000000-0000-4000-8000-000000000002";
|
||||||
const configuration = { ...createDefaultExternalModelConfiguration(), ...externalXlsxTestConfiguration, deviceTypes: [{ id: deviceTypeA, name: "Steckdose", category: "single_phase" as const, defaults: { powerPerUnitW: 125, simultaneityFactor: 1, cosPhi: 0.9, costGroup: "440" } }, { id: deviceTypeB, name: "Leuchte", category: "lighting" as const, defaults: { powerPerUnitW: 40, simultaneityFactor: 0.8, cosPhi: null, costGroup: null } }], familyTypeRules: [{ id: "20000000-0000-4000-8000-000000000001", exactFamilyAndType: "Steckdose: Standard", deviceTypeId: deviceTypeA, quantityRule: "socketCount" as const }, { id: "20000000-0000-4000-8000-000000000002", exactFamilyAndType: "Leuchte: Decke", deviceTypeId: deviceTypeB, quantityRule: "one" as const }] };
|
const configuration = { ...createDefaultExternalModelConfiguration(), ...externalXlsxTestConfiguration, deviceTypes: [{ id: deviceTypeA, name: "Steckdose", category: "single_phase" as const, defaults: { powerPerUnitW: 125, simultaneityFactor: 1, cosPhi: 0.9, costGroup: "440" }, isSocket: true }, { id: deviceTypeB, name: "Leuchte", category: "lighting" as const, defaults: { powerPerUnitW: 40, simultaneityFactor: 0.8, cosPhi: null, costGroup: null }, isSocket: false }], familyTypeRules: [{ id: "20000000-0000-4000-8000-000000000001", exactFamilyAndType: "Steckdose: Standard", deviceTypeId: deviceTypeA, quantityRule: "socketCount" as const }, { id: "20000000-0000-4000-8000-000000000002", exactFamilyAndType: "Leuchte: Decke", deviceTypeId: deviceTypeB, quantityRule: "one" as const }] };
|
||||||
|
|
||||||
describe("Linear XLSX end-to-end", () => {
|
describe("Linear XLSX end-to-end", () => {
|
||||||
it("survives return, follow-up, snapshot restore, duplicate and restart undo/redo", async () => {
|
it("survives return, follow-up, snapshot restore, duplicate and restart undo/redo", async () => {
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ const deviceTypeId = "10000000-0000-4000-8000-000000000001";
|
||||||
const configuration = {
|
const configuration = {
|
||||||
...createDefaultExternalModelConfiguration(),
|
...createDefaultExternalModelConfiguration(),
|
||||||
...externalXlsxTestConfiguration,
|
...externalXlsxTestConfiguration,
|
||||||
deviceTypes: [{ id: deviceTypeId, name: "Steckdose", category: "single_phase" as const, defaults: { powerPerUnitW: 125, simultaneityFactor: 1, cosPhi: 0.9, costGroup: "440" } }],
|
deviceTypes: [{ id: deviceTypeId, name: "Steckdose", category: "single_phase" as const, defaults: { powerPerUnitW: 125, simultaneityFactor: 1, cosPhi: 0.9, costGroup: "440" }, isSocket: true }],
|
||||||
familyTypeRules: [{ id: "20000000-0000-4000-8000-000000000001", exactFamilyAndType: "Steckdose: Standard", deviceTypeId, quantityRule: "socketCount" as const }],
|
familyTypeRules: [{ id: "20000000-0000-4000-8000-000000000001", exactFamilyAndType: "Steckdose: Standard", deviceTypeId, quantityRule: "socketCount" as const }],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue