"use client"; import { useState, type FormEvent } from "react"; import type { ExternalModelConfiguration } from "../../external-model/configuration/external-model-configuration"; import type { ExternalFollowUpImportPlanDto, ProjectHistoryStateDto } from "../types"; import { applyExternalFollowUpImport, planExternalFollowUpImport } from "../utils/api"; import { createClientId } from "../utils/client-id"; import { FormModal } from "./form-modal"; const steps = ["Vorschau", "Quelländerungen", "Neue Räume", "Familienregeln", "Projektgeräte", "Bestätigung"]; const fieldLabels: Record = { ifcGuid: "IfcGUID", roomNumber: "Raumnummer", roomName: "Raumname", familyAndType: "Familie und Typ", selectionMarker: "Auswahlkenner", socketCount: "Anzahl", sourceRemark: "Bemerkung" }; export function RevitFollowUpImportWizard({ 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 [accepted, setAccepted] = useState>({}); const [roomIds, setRoomIds] = useState>({}); const [boardIds, setBoardIds] = useState>({}); const [deviceIds, setDeviceIds] = useState>({}); 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 planExternalFollowUpImport(projectId, nextFile, nextConfiguration, expectedRevision); setPlan(loaded); setAccepted((current) => Object.fromEntries(loaded.changes.map((change) => [change.objectId, current[change.objectId] ?? true]))); setRoomIds((current) => Object.fromEntries(loaded.newSourceRoomKeys.map((key) => { const room = loaded.sourceRooms.find((candidate) => candidate.sourceRoomKey === key)!; return [key, current[key] ?? room.suggestedRoomId ?? ""]; }))); setBoardIds((current) => Object.fromEntries(loaded.newSourceRoomKeys.map((key) => [key, current[key] ?? ""]))); setDeviceIds((current) => Object.fromEntries(loaded.newFamilyAndTypes.map((family) => [family, current[family] ?? ""]))); } catch (value) { setError(value instanceof Error ? value.message : "Folgeimport-Vorschau fehlgeschlagen."); setPlan(null); } finally { setBusy(false); } } async function addRule(familyAndType: string, category: "lighting" | "single_phase" | "three_phase", quantityRule: "one" | "socketCount", deviceTypeId?: string) { if (!file) return; const next = structuredClone(configuration); const targetTypeId = deviceTypeId ?? createClientId(); if (!deviceTypeId) next.deviceTypes.push({ id: targetTypeId, name: familyAndType, category, defaults: { powerPerUnitW: null, simultaneityFactor: 1, cosPhi: null, costGroup: null } }); next.familyTypeRules.push({ id: createClientId(), exactFamilyAndType: familyAndType, deviceTypeId: targetTypeId, 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 applyExternalFollowUpImport(projectId, file, { expectedRevision, expectedSha256: plan.sha256, configuration, objectsWithoutRoomDefaultDistributionBoardId: noRoomBoardId || null, sourceChangeDecisions: plan.changes.map((change) => ({ objectId: change.objectId, accept: accepted[change.objectId] ?? false })), roomDecisions: plan.newSourceRoomKeys.map((sourceRoomKey) => ({ sourceRoomKey, roomId: roomIds[sourceRoomKey] || null, defaultDistributionBoardId: boardIds[sourceRoomKey] || null })), familyProjectDeviceDecisions: plan.newFamilyAndTypes.map((familyAndType) => ({ familyAndType, projectDeviceId: deviceIds[familyAndType] || null })), }); onImported({ history: result.history, objectCount: result.import.objectCount }); } catch (value) { setError(value instanceof Error ? value.message : "Folgeimport fehlgeschlagen."); } finally { setBusy(false); } } const relevantUnknownFamilies = plan?.familyGroups.filter((family) => !family.classified && (plan.newFamilyAndTypes.includes(family.familyAndType) || plan.changes.some((change) => accepted[change.objectId] && change.familyAndType === family.familyAndType))) ?? []; const relevantInvalidQuantity = plan?.objects.some((object) => object.issues.includes("invalid-quantity") && (plan.newObjectWorksharingIds.includes(object.worksharingId) || plan.changes.some((change) => change.worksharingId === object.worksharingId && accepted[change.objectId]))) ?? false; const invalidQuantityFamilies = plan ? [...new Set(plan.objects.filter((object) => object.issues.includes("invalid-quantity") && (plan.newObjectWorksharingIds.includes(object.worksharingId) || plan.changes.some((change) => change.worksharingId === object.worksharingId && accepted[change.objectId]))).map((object) => object.sourceValues.familyAndType))] : []; const blocked = !plan || relevantUnknownFamilies.length > 0 || relevantInvalidQuantity; return void submit(event)} submitDisabled={blocked} submitLabel={step === steps.length - 1 ? "Folgeimport ü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.returnDeviationCount} Objekte enthalten Rückgabefelder, die in Revit noch nicht aktualisiert sind.
{plan.warnings.length ?
{plan.warnings.map((warning) =>
{warning}
)}
: null} : null}
: null} {step === 1 && plan ?

Nur fachliche Quelländerungen benötigen eine Entscheidung. Abweichende Rückgabefelder werden lediglich als „in Revit noch nicht aktualisiert“ gemeldet.

{plan.changes.length ? plan.changes.map((change) => ) :
Keine fachlichen Quelländerungen.
}{plan.missingObjects.length ?
{plan.missingObjects.length} fehlende Objekte werden markiert. Links und Mengen bleiben bestehen.
: null}
: null} {step === 2 && plan ?

Nur neue, tatsächlich übernommene Quellräume benötigen eine Zuordnung.

{plan.newSourceRoomKeys.map((key) => { const room = plan.sourceRooms.find((candidate) => candidate.sourceRoomKey === key)!; return
{room.roomNumber || "–"} {room.roomName}
; })}
: null} {step === 3 && plan ?

Unbekannte Familien müssen nur für neue Objekte oder akzeptierte Familienänderungen klassifiziert werden.

{relevantUnknownFamilies.map((family) =>
{family.familyAndType}
{configuration.deviceTypes.flatMap((type) => [, ])}
)}{!relevantUnknownFamilies.length ?
Alle benötigten Familien sind klassifiziert.
: null}{relevantInvalidQuantity ?
Mindestens eine übernommene Mengenangabe ist ungültig.{invalidQuantityFamilies.map((family) => )}
: null}
: null} {step === 4 && plan ?

Neue Revit-Objekte können familienweise mit einem vorhandenen Projektgerät verknüpft werden.

{plan.newFamilyAndTypes.map((family) =>
{family}
)}
: null} {step === 5 && plan ?
{plan.objectCount} Objekte im vollständigen Import.
Neu: {plan.newObjectCount}, fachlich geändert: {plan.changedObjectCount}, fehlend: {plan.missingObjectCount}. Fehlende Objekte werden nicht gelöst oder gelöscht. Apply liest die Datei erneut; Undo und Redo verwenden ausschließlich die gespeicherten Zustände.
: null} {step > 0 ? : null}
; } function Count({ label, value }: { label: string; value: number }) { return
{value}
{label}
; }