leistungsbilanz-ts/src/frontend/components/room-modal.tsx
Grovy311 cd89c4a499 Add buildings above floors and a building structure page
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>
2026-09-26 12:52:34 +02:00

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>
);
}