Projects with several buildings could not be structured: floors and rooms hung directly off the project. Floors can now belong to a building (migration 0009: table buildings, nullable floors.building_id). Rooms and distribution boards keep their floor; their building follows from it. Existing floors stay without a building. Buildings use the location-structure command store with project-building.insert/.update/.delete, inverses and persistent Undo/Redo; deleting a building with assigned floors is rejected. Floor commands carry buildingId since command version 2. Version 1 floor commands from existing history predate buildings and are upgraded with buildingId = null before they are executed and persisted, so forward and inverse keep one version. Project snapshot schema 7 stores buildings and floor.buildingId; versions 1-6 are upgraded without buildings. Restore and portable transfer include buildings, and the transfer remaps building ids. Buildings, floors and rooms move from the project overview to their own page /projects/:id/struktur with three columns (Gebäude -> Etagen -> Räume) that filter each other, and a sidebar entry. Floor selects in the board and room dialogs are grouped by building. Unchanged dialogs close without a command. Details in docs/building-structure.md; see issue #6. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
111 lines
3.4 KiB
TypeScript
111 lines
3.4 KiB
TypeScript
"use client";
|
|
|
|
import React, { type FormEvent, useState } from "react";
|
|
import type { BuildingDto, CreateRoomInput, RoomDto } from "../types";
|
|
import { FloorSelectOptions, type FloorOption } from "./floor-select-options";
|
|
import { FormModal } from "./form-modal";
|
|
|
|
interface RoomModalProps {
|
|
/** When given, the floor select groups floors by building. */
|
|
buildings?: ReadonlyArray<Pick<BuildingDto, "id" | "name">>;
|
|
floors: ReadonlyArray<FloorOption>;
|
|
initialRoom?: RoomDto;
|
|
initialValues?: Partial<CreateRoomInput>;
|
|
isSaving: boolean;
|
|
onClose: () => void;
|
|
onDelete?: () => void;
|
|
onSave: (input: CreateRoomInput) => Promise<void>;
|
|
}
|
|
|
|
export function RoomModal({
|
|
buildings,
|
|
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>
|
|
<FloorSelectOptions buildings={buildings} floors={floors} />
|
|
</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>
|
|
);
|
|
}
|