forked from jappel/leistungsbilanz-ts
Add Revit catalog creation flow
This commit is contained in:
parent
ab2449419b
commit
bc2bc7ece5
8 changed files with 435 additions and 133 deletions
|
|
@ -6,7 +6,14 @@ import {
|
|||
type ExternalCsvConfiguration,
|
||||
type ExternalCsvFamilyTypeRule,
|
||||
} from "../../external-model/csv/external-csv-contracts";
|
||||
import type { ExternalCsvPreviewDto, ExternalInitialImportPlanDto } from "../types";
|
||||
import type {
|
||||
CreateProjectDeviceInput,
|
||||
CreateRoomInput,
|
||||
ExternalCsvPreviewDto,
|
||||
ExternalInitialImportPlanDto,
|
||||
ProjectDeviceCommandResultDto,
|
||||
ProjectRoomCommandResultDto,
|
||||
} from "../types";
|
||||
import {
|
||||
applyExternalInitialImport,
|
||||
getExternalCsvConfiguration,
|
||||
|
|
@ -15,10 +22,16 @@ import {
|
|||
updateExternalCsvConfiguration,
|
||||
} from "../utils/api";
|
||||
import { FormModal } from "./form-modal";
|
||||
import { ProjectDeviceModal } from "./project-device-modal";
|
||||
import { RoomModal } from "./room-modal";
|
||||
|
||||
interface RevitCsvModalProps {
|
||||
currentRevision: number;
|
||||
onClose: () => void;
|
||||
onCreateProjectDevice: (
|
||||
input: CreateProjectDeviceInput
|
||||
) => Promise<ProjectDeviceCommandResultDto>;
|
||||
onCreateRoom: (input: CreateRoomInput) => Promise<ProjectRoomCommandResultDto>;
|
||||
onRevisionChange: (currentRevision: number) => void;
|
||||
projectId: string;
|
||||
}
|
||||
|
|
@ -55,6 +68,8 @@ const columnFields = [
|
|||
export function RevitCsvModal({
|
||||
currentRevision,
|
||||
onClose,
|
||||
onCreateProjectDevice,
|
||||
onCreateRoom,
|
||||
onRevisionChange,
|
||||
projectId,
|
||||
}: RevitCsvModalProps) {
|
||||
|
|
@ -74,6 +89,9 @@ export function RevitCsvModal({
|
|||
const [isPreviewing, setIsPreviewing] = useState(false);
|
||||
const [isPlanning, setIsPlanning] = 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);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -209,6 +227,93 @@ export function RevitCsvModal({
|
|||
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 (
|
||||
<FormModal
|
||||
description="Projektweites Mapping speichern und Revit-CSV ohne Datenübernahme prüfen."
|
||||
|
|
@ -266,6 +371,8 @@ export function RevitCsvModal({
|
|||
familyLinks={familyLinks}
|
||||
isApplying={isApplying}
|
||||
onApply={handleApplyImport}
|
||||
onCreateProjectDevice={setNewProjectDeviceFamily}
|
||||
onCreateRoom={setNewRoomSourceKey}
|
||||
onFamilyLinkChange={(familyAndType, projectDeviceId) => setFamilyLinks((current) => ({ ...current, [familyAndType]: projectDeviceId }))}
|
||||
onRoomDecisionChange={(sourceRoomKey, decision) => setRoomDecisions((current) => ({ ...current, [sourceRoomKey]: decision }))}
|
||||
plan={importPlan}
|
||||
|
|
@ -308,6 +415,8 @@ export function RevitCsvImportPlan({
|
|||
familyLinks,
|
||||
isApplying,
|
||||
onApply,
|
||||
onCreateProjectDevice,
|
||||
onCreateRoom,
|
||||
onFamilyLinkChange,
|
||||
onRoomDecisionChange,
|
||||
plan,
|
||||
|
|
@ -318,6 +427,8 @@ export function RevitCsvImportPlan({
|
|||
familyLinks: Record<string, string | null>;
|
||||
isApplying: boolean;
|
||||
onApply: () => void;
|
||||
onCreateProjectDevice: (familyAndType: string) => void;
|
||||
onCreateRoom: (sourceRoomKey: string) => void;
|
||||
onFamilyLinkChange: (familyAndType: string, projectDeviceId: string | null) => void;
|
||||
onRoomDecisionChange: (sourceRoomKey: string, decision: RoomDecision) => void;
|
||||
plan: ExternalInitialImportPlanDto;
|
||||
|
|
@ -389,21 +500,31 @@ export function RevitCsvImportPlan({
|
|||
<td>{sourceRoom.objectCount}</td>
|
||||
<td><RoomMatchBadge status={sourceRoom.matchStatus} /></td>
|
||||
<td>
|
||||
<select
|
||||
aria-label={`Interner Raum für ${sourceRoom.roomNumber || sourceRoom.roomName}`}
|
||||
className="form-select form-select-sm"
|
||||
onChange={(event) => onRoomDecisionChange(sourceRoom.sourceRoomKey, {
|
||||
...decision,
|
||||
roomId: event.target.value || null,
|
||||
})}
|
||||
value={decision.roomId ?? ""}
|
||||
>
|
||||
<option value="">Nicht verknüpfen</option>
|
||||
{plan.existing.rooms.map((room) => {
|
||||
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>
|
||||
<div className="input-group input-group-sm">
|
||||
<select
|
||||
aria-label={`Interner Raum für ${sourceRoom.roomNumber || sourceRoom.roomName}`}
|
||||
className="form-select"
|
||||
onChange={(event) => onRoomDecisionChange(sourceRoom.sourceRoomKey, {
|
||||
...decision,
|
||||
roomId: event.target.value || null,
|
||||
})}
|
||||
value={decision.roomId ?? ""}
|
||||
>
|
||||
<option value="">Nicht verknüpfen</option>
|
||||
{plan.existing.rooms.map((room) => {
|
||||
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>
|
||||
<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>
|
||||
<select
|
||||
|
|
@ -441,17 +562,28 @@ export function RevitCsvImportPlan({
|
|||
: <span className="badge text-bg-danger">Regel fehlt</span>}
|
||||
</td>
|
||||
<td>
|
||||
<select
|
||||
aria-label={`ProjectDevice für ${group.familyAndType}`}
|
||||
className="form-select form-select-sm"
|
||||
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 key={device.id} value={device.id}>{device.displayName || device.name} · {categoryLabel(device.category)}</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="input-group input-group-sm">
|
||||
<select
|
||||
aria-label={`ProjectDevice für ${group.familyAndType}`}
|
||||
className="form-select"
|
||||
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 key={device.id} value={device.id}>{device.displayName || device.name} · {categoryLabel(device.category)}</option>
|
||||
))}
|
||||
</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>
|
||||
</tr>
|
||||
))}
|
||||
|
|
@ -486,6 +618,37 @@ function categoryLabel(category: string | null) {
|
|||
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 fileToBase64(file: File) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue