Add Revit catalog creation flow
This commit is contained in:
@@ -295,7 +295,10 @@ external source and returns source-room groups, exact room-number suggestions,
|
|||||||
exact family/type groups, projected object values, issue counts and the current
|
exact family/type groups, projected object values, issue counts and the current
|
||||||
floor/room/board/ProjectDevice catalogs. It creates no draft, revision or
|
floor/room/board/ProjectDevice catalogs. It creates no draft, revision or
|
||||||
domain row. The wide modal renders these groups and catalogs as explicit
|
domain row. The wide modal renders these groups and catalogs as explicit
|
||||||
decisions and identifies exact, ambiguous and new room matches.
|
decisions and identifies exact, ambiguous and new room matches. Missing rooms
|
||||||
|
and ProjectDevices are created through the reused project forms with mapped
|
||||||
|
Revit values as visible defaults. Each catalog creation remains its own project
|
||||||
|
command; the wizard adds and selects the result without discarding its plan.
|
||||||
The dedicated
|
The dedicated
|
||||||
`POST /api/projects/:projectId/external-csv/initial-import/apply` endpoint
|
`POST /api/projects/:projectId/external-csv/initial-import/apply` endpoint
|
||||||
retransmits and replans the file against the expected hash, configuration
|
retransmits and replans the file against the expected hash, configuration
|
||||||
|
|||||||
@@ -566,6 +566,12 @@ Standardverteilungen und ProjectDevices explizit zuordnen und blockiert die
|
|||||||
Übernahme bei unklassifizierten Familien. Erst nach einem Bestätigungsdialog
|
Übernahme bei unklassifizierten Familien. Erst nach einem Bestätigungsdialog
|
||||||
wird der vollständige Import als eine persistente, rückgängig machbare
|
wird der vollständige Import als eine persistente, rückgängig machbare
|
||||||
Projektrevision angewendet. Dabei entstehen keine Stromkreise oder Gerätezeilen.
|
Projektrevision angewendet. Dabei entstehen keine Stromkreise oder Gerätezeilen.
|
||||||
|
Fehlende Räume und ProjectDevices können direkt aus der jeweiligen Gruppe über
|
||||||
|
die regulären Projektformulare angelegt werden. Revit-Werte befüllen diese
|
||||||
|
Formulare vor, werden aber vor dem Speichern sichtbar vom Nutzer bestätigt.
|
||||||
|
Die Kataloganlage ist jeweils ein eigener Projekt-Command; der Wizard übernimmt
|
||||||
|
den neuen Datensatz anschließend in seinen lokalen Auswahlkatalog und markiert
|
||||||
|
ihn für die betroffene Gruppe.
|
||||||
|
|
||||||
## Noch nicht unterstützt
|
## Noch nicht unterstützt
|
||||||
|
|
||||||
|
|||||||
@@ -301,7 +301,10 @@ einen Commit aufgenommen.
|
|||||||
ProjectDevice-Links werden bestätigt, Stromkreis- und Row-Links bleiben
|
ProjectDevice-Links werden bestätigt, Stromkreis- und Row-Links bleiben
|
||||||
ausgeschlossen. Das breite Projektmodal zeigt Raumtreffer und Warnungen,
|
ausgeschlossen. Das breite Projektmodal zeigt Raumtreffer und Warnungen,
|
||||||
bietet die vorhandenen Raum-, Verteiler- und ProjectDevice-Kataloge an und
|
bietet die vorhandenen Raum-, Verteiler- und ProjectDevice-Kataloge an und
|
||||||
verlangt vor der atomaren Übernahme eine ausdrückliche Bestätigung.
|
verlangt vor der atomaren Übernahme eine ausdrückliche Bestätigung. Fehlende
|
||||||
|
Räume und ProjectDevices lassen sich über die regulären, vorausgefüllten
|
||||||
|
Projektformulare anlegen und werden danach unmittelbar ausgewählt. Diese
|
||||||
|
Kataloganlagen bleiben eigenständige, rückgängig machbare Projektrevisionen.
|
||||||
|
|
||||||
Vor Beginn werden die oben vorgeschlagenen Namen, die erneute Dateiübertragung
|
Vor Beginn werden die oben vorgeschlagenen Namen, die erneute Dateiübertragung
|
||||||
statt serverseitiger Entwürfe, die Konfigurationsversionierung und die
|
statt serverseitiger Entwürfe, die Konfigurationsversionierung und die
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import {
|
|||||||
} from "../../../frontend/utils/api";
|
} from "../../../frontend/utils/api";
|
||||||
import type {
|
import type {
|
||||||
CircuitListDto,
|
CircuitListDto,
|
||||||
|
CreateRoomInput,
|
||||||
CreateProjectDeviceInput,
|
CreateProjectDeviceInput,
|
||||||
DistributionBoardDto,
|
DistributionBoardDto,
|
||||||
FloorDto,
|
FloorDto,
|
||||||
@@ -60,6 +61,7 @@ import {
|
|||||||
import { FormModal } from "../../../frontend/components/form-modal";
|
import { FormModal } from "../../../frontend/components/form-modal";
|
||||||
import { ProjectDeviceModal } from "../../../frontend/components/project-device-modal";
|
import { ProjectDeviceModal } from "../../../frontend/components/project-device-modal";
|
||||||
import { RevitCsvModal } from "../../../frontend/components/revit-csv-modal";
|
import { RevitCsvModal } from "../../../frontend/components/revit-csv-modal";
|
||||||
|
import { RoomModal } from "../../../frontend/components/room-modal";
|
||||||
|
|
||||||
const projectDeviceSyncFieldLabels: Record<ProjectDeviceSyncField, string> = {
|
const projectDeviceSyncFieldLabels: Record<ProjectDeviceSyncField, string> = {
|
||||||
name: "Technischer Name",
|
name: "Technischer Name",
|
||||||
@@ -101,9 +103,6 @@ export default function ProjectDetailPage() {
|
|||||||
const [editingBoardCopyName, setEditingBoardCopyName] = useState("");
|
const [editingBoardCopyName, setEditingBoardCopyName] = useState("");
|
||||||
const [floorName, setFloorName] = useState("");
|
const [floorName, setFloorName] = useState("");
|
||||||
const [editingFloor, setEditingFloor] = useState<FloorDto | null>(null);
|
const [editingFloor, setEditingFloor] = useState<FloorDto | null>(null);
|
||||||
const [roomNumber, setRoomNumber] = useState("");
|
|
||||||
const [roomName, setRoomName] = useState("");
|
|
||||||
const [roomFloorId, setRoomFloorId] = useState("");
|
|
||||||
const [editingRoom, setEditingRoom] = useState<RoomDto | null>(null);
|
const [editingRoom, setEditingRoom] = useState<RoomDto | null>(null);
|
||||||
const [isProjectSettingsOpen, setIsProjectSettingsOpen] = useState(false);
|
const [isProjectSettingsOpen, setIsProjectSettingsOpen] = useState(false);
|
||||||
const [isRevitCsvOpen, setIsRevitCsvOpen] = useState(false);
|
const [isRevitCsvOpen, setIsRevitCsvOpen] = useState(false);
|
||||||
@@ -285,24 +284,11 @@ export default function ProjectDetailPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSaveRoom(event: FormEvent<HTMLFormElement>) {
|
async function handleSaveRoom(input: CreateRoomInput) {
|
||||||
event.preventDefault();
|
if (!projectId || !project) return;
|
||||||
if (
|
|
||||||
!projectId ||
|
|
||||||
!project ||
|
|
||||||
!roomNumber.trim() ||
|
|
||||||
!roomName.trim()
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const input = {
|
|
||||||
floorId: roomFloorId || undefined,
|
|
||||||
roomNumber: roomNumber.trim(),
|
|
||||||
roomName: roomName.trim(),
|
|
||||||
};
|
|
||||||
const result = editingRoom
|
const result = editingRoom
|
||||||
? await updateRoom(
|
? await updateRoom(
|
||||||
projectId,
|
projectId,
|
||||||
@@ -319,9 +305,6 @@ export default function ProjectDetailPage() {
|
|||||||
: [...current, result.room]
|
: [...current, result.room]
|
||||||
);
|
);
|
||||||
applyProjectRevision(result.history.currentRevision);
|
applyProjectRevision(result.history.currentRevision);
|
||||||
setRoomNumber("");
|
|
||||||
setRoomName("");
|
|
||||||
setRoomFloorId("");
|
|
||||||
setEditingRoom(null);
|
setEditingRoom(null);
|
||||||
setStructureModal(null);
|
setStructureModal(null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -500,6 +483,22 @@ export default function ProjectDetailPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleCreateRevitRoom(input: CreateRoomInput) {
|
||||||
|
if (!projectId || !project) throw new Error("Projekt ist nicht geladen.");
|
||||||
|
const result = await createRoom(projectId, input, project.currentRevision);
|
||||||
|
setRooms((current) => [...current, result.room]);
|
||||||
|
applyProjectRevision(result.history.currentRevision);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCreateRevitProjectDevice(input: CreateProjectDeviceInput) {
|
||||||
|
if (!projectId || !project) throw new Error("Projekt ist nicht geladen.");
|
||||||
|
const result = await createProjectDevice(projectId, input, project.currentRevision);
|
||||||
|
setProjectDevices((current) => [...current, result.projectDevice]);
|
||||||
|
applyProjectRevision(result.history.currentRevision);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
function openCreateFloor() {
|
function openCreateFloor() {
|
||||||
setEditingFloor(null);
|
setEditingFloor(null);
|
||||||
setFloorName("");
|
setFloorName("");
|
||||||
@@ -514,17 +513,11 @@ export default function ProjectDetailPage() {
|
|||||||
|
|
||||||
function openCreateRoom() {
|
function openCreateRoom() {
|
||||||
setEditingRoom(null);
|
setEditingRoom(null);
|
||||||
setRoomNumber("");
|
|
||||||
setRoomName("");
|
|
||||||
setRoomFloorId("");
|
|
||||||
setStructureModal("room");
|
setStructureModal("room");
|
||||||
}
|
}
|
||||||
|
|
||||||
function openEditRoom(room: RoomDto) {
|
function openEditRoom(room: RoomDto) {
|
||||||
setEditingRoom(room);
|
setEditingRoom(room);
|
||||||
setRoomNumber(room.roomNumber);
|
|
||||||
setRoomName(room.roomName);
|
|
||||||
setRoomFloorId(room.floorId ?? "");
|
|
||||||
setStructureModal("room");
|
setStructureModal("room");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -557,9 +550,6 @@ export default function ProjectDetailPage() {
|
|||||||
setRooms((current) => current.filter((room) => room.id !== editingRoom.id));
|
setRooms((current) => current.filter((room) => room.id !== editingRoom.id));
|
||||||
applyProjectRevision(result.history.currentRevision);
|
applyProjectRevision(result.history.currentRevision);
|
||||||
setEditingRoom(null);
|
setEditingRoom(null);
|
||||||
setRoomNumber("");
|
|
||||||
setRoomName("");
|
|
||||||
setRoomFloorId("");
|
|
||||||
setStructureModal(null);
|
setStructureModal(null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : "Raum konnte nicht gelöscht werden.");
|
setError(err instanceof Error ? err.message : "Raum konnte nicht gelöscht werden.");
|
||||||
@@ -1585,73 +1575,17 @@ export default function ProjectDetailPage() {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{structureModal === "room" ? (
|
{structureModal === "room" ? (
|
||||||
<FormModal
|
<RoomModal
|
||||||
|
floors={floors}
|
||||||
|
initialRoom={editingRoom ?? undefined}
|
||||||
isSaving={isSaving}
|
isSaving={isSaving}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setStructureModal(null);
|
setStructureModal(null);
|
||||||
setEditingRoom(null);
|
setEditingRoom(null);
|
||||||
}}
|
}}
|
||||||
onSubmit={handleSaveRoom}
|
onDelete={editingRoom ? () => void handleDeleteRoom() : undefined}
|
||||||
submitDisabled={!roomNumber.trim() || !roomName.trim()}
|
onSave={handleSaveRoom}
|
||||||
submitLabel={editingRoom ? "Änderungen speichern" : "Raum anlegen"}
|
/>
|
||||||
title={editingRoom ? "Raum bearbeiten" : "Raum hinzufügen"}
|
|
||||||
>
|
|
||||||
<div className="row g-3">
|
|
||||||
<div className="col-12 col-md-4">
|
|
||||||
<label className="form-label" htmlFor="room-number">
|
|
||||||
Raumnummer
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
autoFocus
|
|
||||||
className="form-control"
|
|
||||||
id="room-number"
|
|
||||||
onChange={(event) => setRoomNumber(event.target.value)}
|
|
||||||
required
|
|
||||||
value={roomNumber}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="col-12 col-md-8">
|
|
||||||
<label className="form-label" htmlFor="room-name">
|
|
||||||
Raumname
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
className="form-control"
|
|
||||||
id="room-name"
|
|
||||||
onChange={(event) => setRoomName(event.target.value)}
|
|
||||||
required
|
|
||||||
value={roomName}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="col-12">
|
|
||||||
<label className="form-label" htmlFor="room-floor">
|
|
||||||
Etage
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
className="form-select"
|
|
||||||
id="room-floor"
|
|
||||||
onChange={(event) => setRoomFloorId(event.target.value)}
|
|
||||||
value={roomFloorId}
|
|
||||||
>
|
|
||||||
<option value="">Ohne Etage</option>
|
|
||||||
{floors.map((floor) => (
|
|
||||||
<option key={floor.id} value={floor.id}>
|
|
||||||
{floor.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{editingRoom ? (
|
|
||||||
<div className="border-top mt-4 pt-3">
|
|
||||||
<p className="text-secondary small mb-2">
|
|
||||||
In Stromkreisen verwendete Räume können nicht gelöscht werden.
|
|
||||||
</p>
|
|
||||||
<button className="btn btn-outline-danger" disabled={isSaving} onClick={() => void handleDeleteRoom()} type="button">
|
|
||||||
Raum löschen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</FormModal>
|
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{isProjectDeviceModalOpen ? (
|
{isProjectDeviceModalOpen ? (
|
||||||
@@ -1692,6 +1626,8 @@ export default function ProjectDetailPage() {
|
|||||||
<RevitCsvModal
|
<RevitCsvModal
|
||||||
currentRevision={project.currentRevision}
|
currentRevision={project.currentRevision}
|
||||||
onClose={() => setIsRevitCsvOpen(false)}
|
onClose={() => setIsRevitCsvOpen(false)}
|
||||||
|
onCreateProjectDevice={handleCreateRevitProjectDevice}
|
||||||
|
onCreateRoom={handleCreateRevitRoom}
|
||||||
onRevisionChange={applyProjectRevision}
|
onRevisionChange={applyProjectRevision}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
interface ProjectDeviceModalProps {
|
interface ProjectDeviceModalProps {
|
||||||
globalDevices: GlobalDeviceDto[];
|
globalDevices: GlobalDeviceDto[];
|
||||||
initialDevice?: ProjectDeviceDto;
|
initialDevice?: ProjectDeviceDto;
|
||||||
|
initialValues?: Partial<CreateProjectDeviceInput>;
|
||||||
isSaving: boolean;
|
isSaving: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onImportGlobal: (globalDeviceId: string) => Promise<void>;
|
onImportGlobal: (globalDeviceId: string) => Promise<void>;
|
||||||
@@ -25,12 +26,13 @@ interface ProjectDeviceModalProps {
|
|||||||
export function ProjectDeviceModal({
|
export function ProjectDeviceModal({
|
||||||
globalDevices,
|
globalDevices,
|
||||||
initialDevice,
|
initialDevice,
|
||||||
|
initialValues,
|
||||||
isSaving,
|
isSaving,
|
||||||
onClose,
|
onClose,
|
||||||
onImportGlobal,
|
onImportGlobal,
|
||||||
onSave,
|
onSave,
|
||||||
}: ProjectDeviceModalProps) {
|
}: ProjectDeviceModalProps) {
|
||||||
const [values, setValues] = useState(() => toFormValues(initialDevice));
|
const [values, setValues] = useState(() => toFormValues(initialDevice, initialValues));
|
||||||
const [selectedGlobalDeviceId, setSelectedGlobalDeviceId] = useState("");
|
const [selectedGlobalDeviceId, setSelectedGlobalDeviceId] = useState("");
|
||||||
|
|
||||||
function update(key: keyof typeof values, value: string) {
|
function update(key: keyof typeof values, value: string) {
|
||||||
@@ -254,18 +256,21 @@ function NumberField({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toFormValues(device?: ProjectDeviceDto) {
|
function toFormValues(
|
||||||
|
device?: ProjectDeviceDto,
|
||||||
|
initialValues?: Partial<CreateProjectDeviceInput>
|
||||||
|
) {
|
||||||
return {
|
return {
|
||||||
name: device?.name ?? "",
|
name: device?.name ?? initialValues?.name ?? "",
|
||||||
displayName: device?.displayName ?? "",
|
displayName: device?.displayName ?? initialValues?.displayName ?? "",
|
||||||
connectionKind: device?.connectionKind ?? "",
|
connectionKind: device?.connectionKind ?? initialValues?.connectionKind ?? "",
|
||||||
costGroup: device?.costGroup ?? "",
|
costGroup: device?.costGroup ?? initialValues?.costGroup ?? "",
|
||||||
category: device?.category ?? "single_phase",
|
category: device?.category ?? initialValues?.category ?? "single_phase",
|
||||||
quantity: String(device?.quantity ?? 1),
|
quantity: String(device?.quantity ?? initialValues?.quantity ?? 1),
|
||||||
powerPerUnit: String(device?.powerPerUnit ?? 0.1),
|
powerPerUnit: String(device?.powerPerUnit ?? initialValues?.powerPerUnit ?? 0.1),
|
||||||
simultaneityFactor: String(device?.simultaneityFactor ?? 1),
|
simultaneityFactor: String(device?.simultaneityFactor ?? initialValues?.simultaneityFactor ?? 1),
|
||||||
cosPhi: String(device?.cosPhi ?? 1),
|
cosPhi: String(device?.cosPhi ?? initialValues?.cosPhi ?? 1),
|
||||||
remark: device?.remark ?? "",
|
remark: device?.remark ?? initialValues?.remark ?? "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,14 @@ import {
|
|||||||
type ExternalCsvConfiguration,
|
type ExternalCsvConfiguration,
|
||||||
type ExternalCsvFamilyTypeRule,
|
type ExternalCsvFamilyTypeRule,
|
||||||
} from "../../external-model/csv/external-csv-contracts";
|
} from "../../external-model/csv/external-csv-contracts";
|
||||||
import type { ExternalCsvPreviewDto, ExternalInitialImportPlanDto } from "../types";
|
import type {
|
||||||
|
CreateProjectDeviceInput,
|
||||||
|
CreateRoomInput,
|
||||||
|
ExternalCsvPreviewDto,
|
||||||
|
ExternalInitialImportPlanDto,
|
||||||
|
ProjectDeviceCommandResultDto,
|
||||||
|
ProjectRoomCommandResultDto,
|
||||||
|
} from "../types";
|
||||||
import {
|
import {
|
||||||
applyExternalInitialImport,
|
applyExternalInitialImport,
|
||||||
getExternalCsvConfiguration,
|
getExternalCsvConfiguration,
|
||||||
@@ -15,10 +22,16 @@ import {
|
|||||||
updateExternalCsvConfiguration,
|
updateExternalCsvConfiguration,
|
||||||
} from "../utils/api";
|
} from "../utils/api";
|
||||||
import { FormModal } from "./form-modal";
|
import { FormModal } from "./form-modal";
|
||||||
|
import { ProjectDeviceModal } from "./project-device-modal";
|
||||||
|
import { RoomModal } from "./room-modal";
|
||||||
|
|
||||||
interface RevitCsvModalProps {
|
interface RevitCsvModalProps {
|
||||||
currentRevision: number;
|
currentRevision: number;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
onCreateProjectDevice: (
|
||||||
|
input: CreateProjectDeviceInput
|
||||||
|
) => Promise<ProjectDeviceCommandResultDto>;
|
||||||
|
onCreateRoom: (input: CreateRoomInput) => Promise<ProjectRoomCommandResultDto>;
|
||||||
onRevisionChange: (currentRevision: number) => void;
|
onRevisionChange: (currentRevision: number) => void;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
}
|
}
|
||||||
@@ -55,6 +68,8 @@ const columnFields = [
|
|||||||
export function RevitCsvModal({
|
export function RevitCsvModal({
|
||||||
currentRevision,
|
currentRevision,
|
||||||
onClose,
|
onClose,
|
||||||
|
onCreateProjectDevice,
|
||||||
|
onCreateRoom,
|
||||||
onRevisionChange,
|
onRevisionChange,
|
||||||
projectId,
|
projectId,
|
||||||
}: RevitCsvModalProps) {
|
}: RevitCsvModalProps) {
|
||||||
@@ -74,6 +89,9 @@ export function RevitCsvModal({
|
|||||||
const [isPreviewing, setIsPreviewing] = useState(false);
|
const [isPreviewing, setIsPreviewing] = useState(false);
|
||||||
const [isPlanning, setIsPlanning] = useState(false);
|
const [isPlanning, setIsPlanning] = useState(false);
|
||||||
const [isApplying, setIsApplying] = useState(false);
|
const [isApplying, setIsApplying] = useState(false);
|
||||||
|
const [isCreatingCatalogEntry, setIsCreatingCatalogEntry] = useState(false);
|
||||||
|
const [newRoomSourceKey, setNewRoomSourceKey] = useState<string | null>(null);
|
||||||
|
const [newProjectDeviceFamily, setNewProjectDeviceFamily] = useState<string | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -209,6 +227,93 @@ export function RevitCsvModal({
|
|||||||
setImportPlan(null);
|
setImportPlan(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleCreateRoom(input: CreateRoomInput) {
|
||||||
|
if (!newRoomSourceKey) return;
|
||||||
|
setIsCreatingCatalogEntry(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const result = await onCreateRoom(input);
|
||||||
|
setImportPlan((current) => current ? {
|
||||||
|
...current,
|
||||||
|
existing: {
|
||||||
|
...current.existing,
|
||||||
|
rooms: [...current.existing.rooms, result.room],
|
||||||
|
},
|
||||||
|
} : current);
|
||||||
|
setRoomDecisions((current) => ({
|
||||||
|
...current,
|
||||||
|
[newRoomSourceKey]: {
|
||||||
|
roomId: result.room.id,
|
||||||
|
defaultDistributionBoardId:
|
||||||
|
current[newRoomSourceKey]?.defaultDistributionBoardId ?? null,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
setNewRoomSourceKey(null);
|
||||||
|
} catch (reason) {
|
||||||
|
setError(reason instanceof Error ? reason.message : "Raum konnte nicht angelegt werden.");
|
||||||
|
setNewRoomSourceKey(null);
|
||||||
|
} finally {
|
||||||
|
setIsCreatingCatalogEntry(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCreateProjectDevice(input: CreateProjectDeviceInput) {
|
||||||
|
if (!newProjectDeviceFamily) return;
|
||||||
|
setIsCreatingCatalogEntry(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const result = await onCreateProjectDevice(input);
|
||||||
|
setImportPlan((current) => current ? {
|
||||||
|
...current,
|
||||||
|
existing: {
|
||||||
|
...current.existing,
|
||||||
|
projectDevices: [...current.existing.projectDevices, result.projectDevice],
|
||||||
|
},
|
||||||
|
} : current);
|
||||||
|
setFamilyLinks((current) => ({
|
||||||
|
...current,
|
||||||
|
[newProjectDeviceFamily]: result.projectDevice.id,
|
||||||
|
}));
|
||||||
|
setNewProjectDeviceFamily(null);
|
||||||
|
} catch (reason) {
|
||||||
|
setError(reason instanceof Error ? reason.message : "ProjectDevice konnte nicht angelegt werden.");
|
||||||
|
setNewProjectDeviceFamily(null);
|
||||||
|
} finally {
|
||||||
|
setIsCreatingCatalogEntry(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newRoomSourceKey && importPlan) {
|
||||||
|
const sourceRoom = importPlan.sourceRooms.find((room) => room.sourceRoomKey === newRoomSourceKey);
|
||||||
|
if (sourceRoom) {
|
||||||
|
return (
|
||||||
|
<RoomModal
|
||||||
|
floors={importPlan.existing.floors.map((floor) => ({ ...floor, projectId }))}
|
||||||
|
initialValues={{ roomNumber: sourceRoom.roomNumber, roomName: sourceRoom.roomName }}
|
||||||
|
isSaving={isCreatingCatalogEntry}
|
||||||
|
onClose={() => setNewRoomSourceKey(null)}
|
||||||
|
onSave={handleCreateRoom}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newProjectDeviceFamily && importPlan) {
|
||||||
|
const group = importPlan.familyGroups.find((entry) => entry.familyAndType === newProjectDeviceFamily);
|
||||||
|
if (group) {
|
||||||
|
return (
|
||||||
|
<ProjectDeviceModal
|
||||||
|
globalDevices={[]}
|
||||||
|
initialValues={createRevitProjectDevicePrefill(importPlan, group.familyAndType)}
|
||||||
|
isSaving={isCreatingCatalogEntry}
|
||||||
|
onClose={() => setNewProjectDeviceFamily(null)}
|
||||||
|
onImportGlobal={async () => undefined}
|
||||||
|
onSave={handleCreateProjectDevice}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormModal
|
<FormModal
|
||||||
description="Projektweites Mapping speichern und Revit-CSV ohne Datenübernahme prüfen."
|
description="Projektweites Mapping speichern und Revit-CSV ohne Datenübernahme prüfen."
|
||||||
@@ -266,6 +371,8 @@ export function RevitCsvModal({
|
|||||||
familyLinks={familyLinks}
|
familyLinks={familyLinks}
|
||||||
isApplying={isApplying}
|
isApplying={isApplying}
|
||||||
onApply={handleApplyImport}
|
onApply={handleApplyImport}
|
||||||
|
onCreateProjectDevice={setNewProjectDeviceFamily}
|
||||||
|
onCreateRoom={setNewRoomSourceKey}
|
||||||
onFamilyLinkChange={(familyAndType, projectDeviceId) => setFamilyLinks((current) => ({ ...current, [familyAndType]: projectDeviceId }))}
|
onFamilyLinkChange={(familyAndType, projectDeviceId) => setFamilyLinks((current) => ({ ...current, [familyAndType]: projectDeviceId }))}
|
||||||
onRoomDecisionChange={(sourceRoomKey, decision) => setRoomDecisions((current) => ({ ...current, [sourceRoomKey]: decision }))}
|
onRoomDecisionChange={(sourceRoomKey, decision) => setRoomDecisions((current) => ({ ...current, [sourceRoomKey]: decision }))}
|
||||||
plan={importPlan}
|
plan={importPlan}
|
||||||
@@ -308,6 +415,8 @@ export function RevitCsvImportPlan({
|
|||||||
familyLinks,
|
familyLinks,
|
||||||
isApplying,
|
isApplying,
|
||||||
onApply,
|
onApply,
|
||||||
|
onCreateProjectDevice,
|
||||||
|
onCreateRoom,
|
||||||
onFamilyLinkChange,
|
onFamilyLinkChange,
|
||||||
onRoomDecisionChange,
|
onRoomDecisionChange,
|
||||||
plan,
|
plan,
|
||||||
@@ -318,6 +427,8 @@ export function RevitCsvImportPlan({
|
|||||||
familyLinks: Record<string, string | null>;
|
familyLinks: Record<string, string | null>;
|
||||||
isApplying: boolean;
|
isApplying: boolean;
|
||||||
onApply: () => void;
|
onApply: () => void;
|
||||||
|
onCreateProjectDevice: (familyAndType: string) => void;
|
||||||
|
onCreateRoom: (sourceRoomKey: string) => void;
|
||||||
onFamilyLinkChange: (familyAndType: string, projectDeviceId: string | null) => void;
|
onFamilyLinkChange: (familyAndType: string, projectDeviceId: string | null) => void;
|
||||||
onRoomDecisionChange: (sourceRoomKey: string, decision: RoomDecision) => void;
|
onRoomDecisionChange: (sourceRoomKey: string, decision: RoomDecision) => void;
|
||||||
plan: ExternalInitialImportPlanDto;
|
plan: ExternalInitialImportPlanDto;
|
||||||
@@ -389,21 +500,31 @@ export function RevitCsvImportPlan({
|
|||||||
<td>{sourceRoom.objectCount}</td>
|
<td>{sourceRoom.objectCount}</td>
|
||||||
<td><RoomMatchBadge status={sourceRoom.matchStatus} /></td>
|
<td><RoomMatchBadge status={sourceRoom.matchStatus} /></td>
|
||||||
<td>
|
<td>
|
||||||
<select
|
<div className="input-group input-group-sm">
|
||||||
aria-label={`Interner Raum für ${sourceRoom.roomNumber || sourceRoom.roomName}`}
|
<select
|
||||||
className="form-select form-select-sm"
|
aria-label={`Interner Raum für ${sourceRoom.roomNumber || sourceRoom.roomName}`}
|
||||||
onChange={(event) => onRoomDecisionChange(sourceRoom.sourceRoomKey, {
|
className="form-select"
|
||||||
...decision,
|
onChange={(event) => onRoomDecisionChange(sourceRoom.sourceRoomKey, {
|
||||||
roomId: event.target.value || null,
|
...decision,
|
||||||
})}
|
roomId: event.target.value || null,
|
||||||
value={decision.roomId ?? ""}
|
})}
|
||||||
>
|
value={decision.roomId ?? ""}
|
||||||
<option value="">Nicht verknüpfen</option>
|
>
|
||||||
{plan.existing.rooms.map((room) => {
|
<option value="">Nicht verknüpfen</option>
|
||||||
const floor = room.floorId ? floorsById.get(room.floorId) : null;
|
{plan.existing.rooms.map((room) => {
|
||||||
return <option key={room.id} value={room.id}>{room.roomNumber} · {room.roomName}{floor ? ` (${floor.name})` : ""}</option>;
|
const floor = room.floorId ? floorsById.get(room.floorId) : null;
|
||||||
})}
|
return <option key={room.id} value={room.id}>{room.roomNumber} · {room.roomName}{floor ? ` (${floor.name})` : ""}</option>;
|
||||||
</select>
|
})}
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
aria-label={`Raum ${sourceRoom.roomNumber || sourceRoom.roomName} neu anlegen`}
|
||||||
|
className="btn btn-outline-secondary"
|
||||||
|
onClick={() => onCreateRoom(sourceRoom.sourceRoomKey)}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Neu
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<select
|
<select
|
||||||
@@ -441,17 +562,28 @@ export function RevitCsvImportPlan({
|
|||||||
: <span className="badge text-bg-danger">Regel fehlt</span>}
|
: <span className="badge text-bg-danger">Regel fehlt</span>}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<select
|
<div className="input-group input-group-sm">
|
||||||
aria-label={`ProjectDevice für ${group.familyAndType}`}
|
<select
|
||||||
className="form-select form-select-sm"
|
aria-label={`ProjectDevice für ${group.familyAndType}`}
|
||||||
onChange={(event) => onFamilyLinkChange(group.familyAndType, event.target.value || null)}
|
className="form-select"
|
||||||
value={familyLinks[group.familyAndType] ?? ""}
|
onChange={(event) => onFamilyLinkChange(group.familyAndType, event.target.value || null)}
|
||||||
>
|
value={familyLinks[group.familyAndType] ?? ""}
|
||||||
<option value="">Nicht verknüpfen</option>
|
>
|
||||||
{plan.existing.projectDevices.map((device) => (
|
<option value="">Nicht verknüpfen</option>
|
||||||
<option key={device.id} value={device.id}>{device.displayName || device.name} · {categoryLabel(device.category)}</option>
|
{plan.existing.projectDevices.map((device) => (
|
||||||
))}
|
<option key={device.id} value={device.id}>{device.displayName || device.name} · {categoryLabel(device.category)}</option>
|
||||||
</select>
|
))}
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
aria-label={`ProjectDevice für ${group.familyAndType} neu anlegen`}
|
||||||
|
className="btn btn-outline-secondary"
|
||||||
|
disabled={!group.classified}
|
||||||
|
onClick={() => onCreateProjectDevice(group.familyAndType)}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Neu
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
@@ -486,6 +618,37 @@ function categoryLabel(category: string | null) {
|
|||||||
return "Ohne Kategorie";
|
return "Ohne Kategorie";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createRevitProjectDevicePrefill(
|
||||||
|
plan: ExternalInitialImportPlanDto,
|
||||||
|
familyAndType: string
|
||||||
|
): Partial<CreateProjectDeviceInput> {
|
||||||
|
const group = plan.familyGroups.find((entry) => entry.familyAndType === familyAndType);
|
||||||
|
const firstObject = plan.objects.find(
|
||||||
|
(object) => object.sourceValues.familyAndType === familyAndType
|
||||||
|
);
|
||||||
|
const category = group?.category;
|
||||||
|
return {
|
||||||
|
name: group?.internalDeviceType || familyAndType || "Revit-Gerät",
|
||||||
|
displayName: firstObject?.suggestedPlanningValues.displayName || familyAndType,
|
||||||
|
connectionKind: group?.connectionKind ?? undefined,
|
||||||
|
category:
|
||||||
|
category === "lighting" || category === "three_phase"
|
||||||
|
? category
|
||||||
|
: "single_phase",
|
||||||
|
quantity: firstObject?.suggestedPlanningValues.effectiveQuantity ?? 1,
|
||||||
|
powerPerUnit:
|
||||||
|
firstObject?.suggestedPlanningValues.powerPerUnitW === null ||
|
||||||
|
firstObject?.suggestedPlanningValues.powerPerUnitW === undefined
|
||||||
|
? 0.1
|
||||||
|
: firstObject.suggestedPlanningValues.powerPerUnitW / 1000,
|
||||||
|
simultaneityFactor:
|
||||||
|
firstObject?.suggestedPlanningValues.simultaneityFactor ?? 1,
|
||||||
|
cosPhi: firstObject?.suggestedPlanningValues.cosPhi ?? undefined,
|
||||||
|
costGroup: firstObject?.suggestedPlanningValues.costGroup ?? undefined,
|
||||||
|
remark: firstObject?.suggestedPlanningValues.remark ?? undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function Stat({ label, value }: { label: string; value: number }) { return <div className="col-6 col-lg-3"><div className="border rounded p-2"><div className="small text-secondary">{label}</div><strong>{value}</strong></div></div>; }
|
function Stat({ label, value }: { label: string; value: number }) { return <div className="col-6 col-lg-3"><div className="border rounded p-2"><div className="small text-secondary">{label}</div><strong>{value}</strong></div></div>; }
|
||||||
|
|
||||||
function fileToBase64(file: File) {
|
function fileToBase64(file: File) {
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { type FormEvent, useState } from "react";
|
||||||
|
import type { CreateRoomInput, FloorDto, RoomDto } from "../types";
|
||||||
|
import { FormModal } from "./form-modal";
|
||||||
|
|
||||||
|
interface RoomModalProps {
|
||||||
|
floors: FloorDto[];
|
||||||
|
initialRoom?: RoomDto;
|
||||||
|
initialValues?: Partial<CreateRoomInput>;
|
||||||
|
isSaving: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onDelete?: () => void;
|
||||||
|
onSave: (input: CreateRoomInput) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RoomModal({
|
||||||
|
floors,
|
||||||
|
initialRoom,
|
||||||
|
initialValues,
|
||||||
|
isSaving,
|
||||||
|
onClose,
|
||||||
|
onDelete,
|
||||||
|
onSave,
|
||||||
|
}: RoomModalProps) {
|
||||||
|
const [roomNumber, setRoomNumber] = useState(
|
||||||
|
initialRoom?.roomNumber ?? initialValues?.roomNumber ?? ""
|
||||||
|
);
|
||||||
|
const [roomName, setRoomName] = useState(
|
||||||
|
initialRoom?.roomName ?? initialValues?.roomName ?? ""
|
||||||
|
);
|
||||||
|
const [floorId, setFloorId] = useState(
|
||||||
|
initialRoom?.floorId ?? initialValues?.floorId ?? ""
|
||||||
|
);
|
||||||
|
|
||||||
|
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!roomNumber.trim() || !roomName.trim()) return;
|
||||||
|
await onSave({
|
||||||
|
floorId: floorId || undefined,
|
||||||
|
roomNumber: roomNumber.trim(),
|
||||||
|
roomName: roomName.trim(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormModal
|
||||||
|
isSaving={isSaving}
|
||||||
|
onClose={onClose}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
submitDisabled={!roomNumber.trim() || !roomName.trim()}
|
||||||
|
submitLabel={initialRoom ? "Änderungen speichern" : "Raum anlegen"}
|
||||||
|
title={initialRoom ? "Raum bearbeiten" : "Raum hinzufügen"}
|
||||||
|
>
|
||||||
|
<div className="row g-3">
|
||||||
|
<div className="col-12 col-md-4">
|
||||||
|
<label className="form-label" htmlFor="room-number">Raumnummer</label>
|
||||||
|
<input
|
||||||
|
autoFocus
|
||||||
|
className="form-control"
|
||||||
|
id="room-number"
|
||||||
|
onChange={(event) => setRoomNumber(event.target.value)}
|
||||||
|
required
|
||||||
|
value={roomNumber}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-12 col-md-8">
|
||||||
|
<label className="form-label" htmlFor="room-name">Raumname</label>
|
||||||
|
<input
|
||||||
|
className="form-control"
|
||||||
|
id="room-name"
|
||||||
|
onChange={(event) => setRoomName(event.target.value)}
|
||||||
|
required
|
||||||
|
value={roomName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-12">
|
||||||
|
<label className="form-label" htmlFor="room-floor">Etage</label>
|
||||||
|
<select
|
||||||
|
className="form-select"
|
||||||
|
id="room-floor"
|
||||||
|
onChange={(event) => setFloorId(event.target.value)}
|
||||||
|
value={floorId}
|
||||||
|
>
|
||||||
|
<option value="">Ohne Etage</option>
|
||||||
|
{floors.map((floor) => <option key={floor.id} value={floor.id}>{floor.name}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{initialRoom && onDelete ? (
|
||||||
|
<div className="border-top mt-4 pt-3">
|
||||||
|
<p className="text-secondary small mb-2">
|
||||||
|
In Stromkreisen verwendete Räume können nicht gelöscht werden.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
className="btn btn-outline-danger"
|
||||||
|
disabled={isSaving}
|
||||||
|
onClick={onDelete}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Raum löschen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</FormModal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,9 +3,12 @@ import { describe, it } from "node:test";
|
|||||||
import { createElement } from "react";
|
import { createElement } from "react";
|
||||||
import { renderToStaticMarkup } from "react-dom/server";
|
import { renderToStaticMarkup } from "react-dom/server";
|
||||||
import {
|
import {
|
||||||
|
createRevitProjectDevicePrefill,
|
||||||
RevitCsvImportPlan,
|
RevitCsvImportPlan,
|
||||||
RevitCsvModal,
|
RevitCsvModal,
|
||||||
} from "../src/frontend/components/revit-csv-modal.js";
|
} from "../src/frontend/components/revit-csv-modal.js";
|
||||||
|
import { ProjectDeviceModal } from "../src/frontend/components/project-device-modal.js";
|
||||||
|
import { RoomModal } from "../src/frontend/components/room-modal.js";
|
||||||
import {
|
import {
|
||||||
applyExternalInitialImport,
|
applyExternalInitialImport,
|
||||||
getExternalCsvConfiguration,
|
getExternalCsvConfiguration,
|
||||||
@@ -98,6 +101,8 @@ describe("Revit CSV modal presentation", () => {
|
|||||||
createElement(RevitCsvModal, {
|
createElement(RevitCsvModal, {
|
||||||
currentRevision: 0,
|
currentRevision: 0,
|
||||||
onClose: () => undefined,
|
onClose: () => undefined,
|
||||||
|
onCreateProjectDevice: async () => ({} as never),
|
||||||
|
onCreateRoom: async () => ({} as never),
|
||||||
onRevisionChange: () => undefined,
|
onRevisionChange: () => undefined,
|
||||||
projectId: "project-1",
|
projectId: "project-1",
|
||||||
})
|
})
|
||||||
@@ -148,6 +153,8 @@ describe("Revit CSV modal presentation", () => {
|
|||||||
familyLinks: { "Steckdose : Standard": "device-1" },
|
familyLinks: { "Steckdose : Standard": "device-1" },
|
||||||
isApplying: false,
|
isApplying: false,
|
||||||
onApply: () => undefined,
|
onApply: () => undefined,
|
||||||
|
onCreateProjectDevice: () => undefined,
|
||||||
|
onCreateRoom: () => undefined,
|
||||||
onFamilyLinkChange: () => undefined,
|
onFamilyLinkChange: () => undefined,
|
||||||
onRoomDecisionChange: () => undefined,
|
onRoomDecisionChange: () => undefined,
|
||||||
plan,
|
plan,
|
||||||
@@ -160,6 +167,78 @@ describe("Revit CSV modal presentation", () => {
|
|||||||
assert.match(markup, /101 · Büro/);
|
assert.match(markup, /101 · Büro/);
|
||||||
assert.match(markup, /Steckdose : Standard/);
|
assert.match(markup, /Steckdose : Standard/);
|
||||||
assert.match(markup, /Erstimport übernehmen/);
|
assert.match(markup, /Erstimport übernehmen/);
|
||||||
|
assert.match(markup, /Raum 101 neu anlegen/);
|
||||||
|
assert.match(markup, /ProjectDevice für Steckdose : Standard neu anlegen/);
|
||||||
assert.doesNotMatch(markup, /IFC-GUID/);
|
assert.doesNotMatch(markup, /IFC-GUID/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("prefills the existing room and ProjectDevice forms from Revit decisions", () => {
|
||||||
|
const roomMarkup = renderToStaticMarkup(createElement(RoomModal, {
|
||||||
|
floors: [{ id: "floor-1", projectId: "project-1", name: "EG", sortOrder: 0 }],
|
||||||
|
initialValues: { roomNumber: "101", roomName: "Büro" },
|
||||||
|
isSaving: false,
|
||||||
|
onClose: () => undefined,
|
||||||
|
onSave: async () => undefined,
|
||||||
|
}));
|
||||||
|
const deviceMarkup = renderToStaticMarkup(createElement(ProjectDeviceModal, {
|
||||||
|
globalDevices: [],
|
||||||
|
initialValues: {
|
||||||
|
name: "socket",
|
||||||
|
displayName: "Steckdose",
|
||||||
|
category: "single_phase",
|
||||||
|
quantity: 2,
|
||||||
|
powerPerUnit: 0.1,
|
||||||
|
simultaneityFactor: 1,
|
||||||
|
},
|
||||||
|
isSaving: false,
|
||||||
|
onClose: () => undefined,
|
||||||
|
onImportGlobal: async () => undefined,
|
||||||
|
onSave: async () => undefined,
|
||||||
|
}));
|
||||||
|
|
||||||
|
assert.match(roomMarkup, /value="101"/);
|
||||||
|
assert.match(roomMarkup, /value="Büro"/);
|
||||||
|
assert.match(deviceMarkup, /value="socket"/);
|
||||||
|
assert.match(deviceMarkup, /value="Steckdose"/);
|
||||||
|
assert.match(deviceMarkup, /value="2"/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("converts mapped Revit watts into ProjectDevice kilowatts", () => {
|
||||||
|
const plan = {
|
||||||
|
familyGroups: [{
|
||||||
|
familyAndType: "Steckdose : Standard",
|
||||||
|
internalDeviceType: "socket",
|
||||||
|
category: "single_phase",
|
||||||
|
connectionKind: "230 V",
|
||||||
|
}],
|
||||||
|
objects: [{
|
||||||
|
sourceValues: { familyAndType: "Steckdose : Standard" },
|
||||||
|
suggestedPlanningValues: {
|
||||||
|
displayName: "Arbeitsplatzsteckdose",
|
||||||
|
effectiveQuantity: 4,
|
||||||
|
powerPerUnitW: 250,
|
||||||
|
simultaneityFactor: 0.8,
|
||||||
|
cosPhi: 0.95,
|
||||||
|
costGroup: "440",
|
||||||
|
remark: "Aus Revit",
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
} as ExternalInitialImportPlanDto;
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
createRevitProjectDevicePrefill(plan, "Steckdose : Standard"),
|
||||||
|
{
|
||||||
|
name: "socket",
|
||||||
|
displayName: "Arbeitsplatzsteckdose",
|
||||||
|
connectionKind: "230 V",
|
||||||
|
category: "single_phase",
|
||||||
|
quantity: 4,
|
||||||
|
powerPerUnit: 0.25,
|
||||||
|
simultaneityFactor: 0.8,
|
||||||
|
cosPhi: 0.95,
|
||||||
|
costGroup: "440",
|
||||||
|
remark: "Aus Revit",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user