Add optional room areas

This commit is contained in:
Julian Appel 2026-09-27 20:00:03 +02:00
parent a6500e2b26
commit dce31ee143
26 changed files with 3669 additions and 78 deletions

View file

@ -48,13 +48,17 @@ import {
filterRooms,
groupFloorsByBuilding,
hasFloorsWithoutBuilding,
hasRoomsWithoutFloor,
normalizeBuildingSelection,
normalizeFloorSelection,
showsRoomsWithoutFloorEntry,
summarizeRoomAreas,
type RoomAreaSummary,
type StructureSelection,
} from "../../../../frontend/utils/building-structure";
import { FormModal } from "../../../../frontend/components/form-modal";
import { RoomModal } from "../../../../frontend/components/room-modal";
import { formatRoomArea } from "../../../../frontend/utils/room-area";
const BUILDING_HAS_FLOORS_MESSAGE =
"Das Gebäude kann nicht gelöscht werden, solange Etagen zugeordnet sind.";
@ -68,6 +72,23 @@ function formatCount(count: number, singular: string, plural: string) {
return `${count} ${count === 1 ? singular : plural}`;
}
function formatAreaSummary(summary: RoomAreaSummary) {
return `${formatRoomArea(summary.areaM2)} m² · ${formatCount(
summary.missingAreaCount,
"Raum ohne Fläche",
"Räume ohne Fläche"
)}`;
}
function formatStructureMeta(
count: number,
singular: string,
plural: string,
summary: RoomAreaSummary
) {
return `${formatCount(count, singular, plural)} · ${formatAreaSummary(summary)}`;
}
function compareRooms(left: RoomDto, right: RoomDto) {
return (
left.roomNumber.localeCompare(right.roomNumber, "de", { numeric: true }) ||
@ -307,7 +328,25 @@ export default function BuildingStructurePage() {
() => countRoomsByFloor(rooms, floors),
[floors, rooms]
);
const showUnassignedBuilding = hasFloorsWithoutBuilding(floors, buildings);
const areaSummaries = useMemo(
() => summarizeRoomAreas(rooms, floors, buildings),
[buildings, floors, rooms]
);
const emptyAreaSummary: RoomAreaSummary = {
areaM2: 0,
roomCount: 0,
missingAreaCount: 0,
};
const selectedBuildingAreaSummary =
effectiveBuildingSelection === ALL_SELECTION
? areaSummaries.total
: areaSummaries.byBuilding.get(
effectiveBuildingSelection === UNASSIGNED_SELECTION
? null
: effectiveBuildingSelection
) ?? emptyAreaSummary;
const showUnassignedBuilding =
hasFloorsWithoutBuilding(floors, buildings) || hasRoomsWithoutFloor(rooms, floors);
const showUnassignedFloor = showsRoomsWithoutFloorEntry(
rooms,
floors,
@ -435,7 +474,8 @@ export default function BuildingStructurePage() {
editingRoom &&
editingRoom.roomNumber === input.roomNumber.trim() &&
editingRoom.roomName === input.roomName.trim() &&
editingRoom.floorId === (input.floorId || null)
editingRoom.floorId === (input.floorId || null) &&
editingRoom.areaM2 === input.areaM2
) {
setDialog(null);
return;
@ -536,7 +576,12 @@ export default function BuildingStructurePage() {
<StructureEntry
isSelected={effectiveBuildingSelection === ALL_SELECTION}
label="Alle"
meta={formatCount(floors.length, "Etage", "Etagen")}
meta={formatStructureMeta(
floors.length,
"Etage",
"Etagen",
areaSummaries.total
)}
onSelect={() => selectBuilding(ALL_SELECTION)}
/>
{buildings.map((building) => (
@ -553,10 +598,11 @@ export default function BuildingStructurePage() {
isSelected={effectiveBuildingSelection === building.id}
key={building.id}
label={building.name}
meta={formatCount(
meta={formatStructureMeta(
floorCountByBuilding.get(building.id) ?? 0,
"Etage",
"Etagen"
"Etagen",
areaSummaries.byBuilding.get(building.id) ?? emptyAreaSummary
)}
onSelect={() => selectBuilding(building.id)}
/>
@ -565,10 +611,11 @@ export default function BuildingStructurePage() {
<StructureEntry
isSelected={effectiveBuildingSelection === UNASSIGNED_SELECTION}
label={<em>{UNASSIGNED_BUILDING_LABEL}</em>}
meta={formatCount(
meta={formatStructureMeta(
floorCountByBuilding.get(null) ?? 0,
"Etage",
"Etagen"
"Etagen",
areaSummaries.byBuilding.get(null) ?? emptyAreaSummary
)}
onSelect={() => selectBuilding(UNASSIGNED_SELECTION)}
/>
@ -591,7 +638,12 @@ export default function BuildingStructurePage() {
<StructureEntry
isSelected={effectiveFloorSelection === ALL_SELECTION}
label="Alle"
meta={formatCount(roomsInBuildingSelection, "Raum", "Räume")}
meta={formatStructureMeta(
roomsInBuildingSelection,
"Raum",
"Räume",
selectedBuildingAreaSummary
)}
onSelect={() => setFloorSelection(ALL_SELECTION)}
/>
{visibleFloorGroups.map((group) => (
@ -613,10 +665,11 @@ export default function BuildingStructurePage() {
isSelected={effectiveFloorSelection === floor.id}
key={floor.id}
label={floor.name}
meta={formatCount(
meta={formatStructureMeta(
roomCountByFloor.get(floor.id) ?? 0,
"Raum",
"Räume"
"Räume",
areaSummaries.byFloor.get(floor.id) ?? emptyAreaSummary
)}
onSelect={() => setFloorSelection(floor.id)}
/>
@ -627,7 +680,12 @@ export default function BuildingStructurePage() {
<StructureEntry
isSelected={effectiveFloorSelection === UNASSIGNED_SELECTION}
label={<em>{UNASSIGNED_FLOOR_LABEL}</em>}
meta={formatCount(roomCountByFloor.get(null) ?? 0, "Raum", "Räume")}
meta={formatStructureMeta(
roomCountByFloor.get(null) ?? 0,
"Raum",
"Räume",
areaSummaries.byFloor.get(null) ?? emptyAreaSummary
)}
onSelect={() => setFloorSelection(UNASSIGNED_SELECTION)}
/>
) : null}
@ -671,6 +729,11 @@ export default function BuildingStructurePage() {
{room.roomName}
</>
}
meta={
room.areaM2 === null
? "Fläche fehlt"
: `${formatRoomArea(room.areaM2)} m²`
}
/>
))}
{!visibleRooms.length && !isLoading ? (