"use client"; import { useState, type FormEvent } from "react"; import type { ExternalModelConfiguration } from "../../external-model/configuration/external-model-configuration"; import type { ExternalInitialImportPlanDto, ProjectHistoryStateDto } from "../types"; import { applyExternalInitialImport, planExternalInitialImport } from "../utils/api"; import { createClientId } from "../utils/client-id"; import { FormModal } from "./form-modal"; const steps = ["Vorschau", "Räume", "Verteiler", "Familienregeln", "Projektgeräte", "Bestätigung"]; export function RevitInitialImportWizard({ projectId, expectedRevision, initialConfiguration, onClose, onImported }: { projectId: string; expectedRevision: number; initialConfiguration: ExternalModelConfiguration; onClose: () => void; onImported: (result: { history: ProjectHistoryStateDto; objectCount: number }) => void; }) { const [step, setStep] = useState(0); const [file, setFile] = useState(null); const [configuration, setConfiguration] = useState(() => structuredClone(initialConfiguration)); const [plan, setPlan] = useState(null); const [roomIds, setRoomIds] = useState>( {} ); const [boardIds, setBoardIds] = useState>( {} ); const [deviceIds, setDeviceIds] = useState>( {} ); const [sourceName, setSourceName] = useState("Revit-Gesamtmodell"); const [noRoomBoardId, setNoRoomBoardId] = useState(""); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); async function loadPlan(nextFile: File, nextConfiguration = configuration) { setBusy(true); setError(null); try { const loaded = await planExternalInitialImport(projectId, nextFile, nextConfiguration, expectedRevision); setPlan(loaded); setRoomIds((current) => Object.fromEntries(loaded.sourceRooms.map((room) => [room.sourceRoomKey, current[room.sourceRoomKey] ?? room.suggestedRoomId ?? ""]))); setBoardIds((current) => Object.fromEntries(loaded.sourceRooms.map((room) => [room.sourceRoomKey, current[room.sourceRoomKey] ?? ""]))); setDeviceIds((current) => Object.fromEntries(loaded.familyGroups.map((family) => [family.familyAndType, current[family.familyAndType] ?? ""]))); } catch (value) { setError(value instanceof Error ? value.message : "XLSX-Vorschau fehlgeschlagen."); setPlan(null); } finally { setBusy(false); } } async function classifyFamily(familyAndType: string, category: "lighting" | "single_phase" | "three_phase", quantityRule: "one" | "socketCount") { if (!file) return; const deviceTypeId = createClientId(); const next = structuredClone(configuration); 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 }); setConfiguration(next); await loadPlan(file, next); } async function assignExistingDeviceType(familyAndType: string, encoded: string) { if (!file || !encoded) return; const separator = encoded.lastIndexOf("|"); const deviceTypeId = encoded.slice(0, separator); const quantityRule = encoded.slice(separator + 1) as "one" | "socketCount"; const next = structuredClone(configuration); next.familyTypeRules.push({ id: createClientId(), exactFamilyAndType: familyAndType, deviceTypeId, quantityRule }); setConfiguration(next); await loadPlan(file, next); } async function toggleQuantityRule(familyAndType: string) { if (!file) return; const next = structuredClone(configuration); const rule = next.familyTypeRules.find((candidate) => candidate.exactFamilyAndType === familyAndType); if (!rule) return; rule.quantityRule = rule.quantityRule === "one" ? "socketCount" : "one"; setConfiguration(next); await loadPlan(file, next); } async function submit(event: FormEvent) { event.preventDefault(); if (step < steps.length - 1) { setStep(step + 1); return; } if (!file || !plan) return; setBusy(true); setError(null); try { const result = await applyExternalInitialImport(projectId, file, { expectedRevision, expectedSha256: plan.sha256, sourceName, configuration, objectsWithoutRoomDefaultDistributionBoardId: noRoomBoardId || null, roomDecisions: plan.sourceRooms.map((room) => ({ sourceRoomKey: room.sourceRoomKey, roomId: roomIds[room.sourceRoomKey] || null, defaultDistributionBoardId: boardIds[room.sourceRoomKey] || null })), familyProjectDeviceDecisions: plan.familyGroups.map((family) => ({ familyAndType: family.familyAndType, projectDeviceId: deviceIds[family.familyAndType] || null })), }); onImported({ history: result.history, objectCount: result.import.objectCount }); } catch (value) { setError(value instanceof Error ? value.message : "Erstimport fehlgeschlagen."); } finally { setBusy(false); } } const blocked = !plan || (step >= 3 && (plan.issueCounts.unknownFamilyAndType > 0 || plan.issueCounts.invalidQuantity > 0)); return void submit(event)} submitDisabled={blocked} submitLabel={step === steps.length - 1 ? "Erstimport übernehmen" : "Weiter"}>
{steps.map((label, index) => {index + 1}. {label})}
{error ?
{error}
: null} {step === 0 ?
{ const selected = event.target.files?.[0] ?? null; setFile(selected); if (selected) void loadPlan(selected); }} />
{plan ? <>
{plan.objectCount}
Objekte
{plan.sourceRooms.length}
Quellräume
{plan.familyGroups.length}
Familien
{plan.suspectObjectCount}
übersprungene verdächtige Zeilen
Unbekannte Familien: {plan.issueCounts.unknownFamilyAndType} · Ungültige Leistungen: {plan.issueCounts.invalidPower} · Ungültige Mengen: {plan.issueCounts.invalidQuantity} · Objekte ohne Raum: {plan.issueCounts.missingRoom}
{plan.warnings.length ?
{plan.warnings.map((warning) =>
{warning}
)}
: null} : null}
: null} {step === 1 && plan ?

Quellräume vorhandenen Projekträumen zuordnen. Neue Räume können vorher in der Gebäudestruktur angelegt werden.

{plan.sourceRooms.map((room) =>
{room.roomNumber || "–"} {room.roomName} {room.objectCount}
)}
: null} {step === 2 && plan ?

Der Standardverteiler gilt beim Import nur für unzugeordnete Objekte.

{plan.sourceRooms.map((room) =>
{room.roomNumber || "–"} · {room.roomName}
)}{plan.issueCounts.missingRoom > 0 ?
Objekte ohne Quellraum {plan.issueCounts.missingRoom}
: null}
: null} {step === 3 && plan ?

Exakte Familie-und-Typ-Werte werden einer Geräteart und Mengenregel zugeordnet. Ein fehlender GZF startet sichtbar mit 1.

{plan.familyGroups.map((family) =>
{family.familyAndType} {family.objectCount}
{family.classified ? `${family.deviceTypeName} · ${family.quantityRule === "socketCount" ? "Anzahl aus XLSX" : "Menge 1"} · GZF 1, falls nicht gepflegt` : "Noch nicht klassifiziert"}
{family.classified ? :
{configuration.deviceTypes.length ? : null}
}
)}
: null} {step === 4 && plan ?

Optional kann jede Familie mit einem vorhandenen Projektgerät verknüpft werden.

{plan.familyGroups.map((family) =>
{family.familyAndType}
)}
: null} {step === 5 && plan ?
setSourceName(event.target.value)} />
{plan.objectCount} Revit-Objekte werden importiert. Es werden keine Gerätezeilen, Stromkreise oder BMKs erzeugt. Die Datei wird beim Übernehmen erneut übertragen und geprüft.
: null} {step > 0 ? : null}
; }