Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
29eddd7
feat(domain): add Building, Dwelling, DwellingStatus, CoMeasurementLa…
DeepDiver1975 Aug 25, 2026
24760cc
test(domain): add CoMeasurement tests for Building, Dwelling, labels
DeepDiver1975 Aug 25, 2026
80c79f9
feat(domain): add Building/Dwelling mutation methods to Incident
DeepDiver1975 Aug 25, 2026
52b084a
feat(persistence): add V15 migration and Save/Load for CO measurement
DeepDiver1975 Aug 25, 2026
e5da684
feat(sync): add 7 CO measurement commands, DTOs, and mapper
DeepDiver1975 Aug 25, 2026
0eec247
feat(session): add CO measurement methods to IIncidentSession
DeepDiver1975 Aug 25, 2026
8673a7d
feat(applogic): add CoMessprotokollViewModel with matrix and flyout e…
DeepDiver1975 Aug 25, 2026
ddf9c96
feat(ui): add CO-MESSUNG tab with matrix view
DeepDiver1975 Aug 26, 2026
6914d2a
feat(documents): add CO-Messprotokoll PDF section with traffic-light …
DeepDiver1975 Aug 26, 2026
cfec1c3
test(sync): add CommandApplier and SnapshotRoundTrip tests for CO mea…
DeepDiver1975 Aug 26, 2026
854ae88
test(acceptance): add CO-Messprotokoll render test
DeepDiver1975 Aug 26, 2026
a6c575b
fix: Co-Messung design
DeepDiver1975 Aug 26, 2026
da544cc
feat(co-messung): editable apartment headers, compact rows, and layou…
DeepDiver1975 Aug 26, 2026
093d28f
style(co-messung): replace solid-fill matrix cells with door-mark cards
DeepDiver1975 Aug 26, 2026
87cddb1
fix(co-messung): stop long custom headers overflowing their column
DeepDiver1975 Aug 26, 2026
148091f
fix(co-messung): stop header inputs popping a full box on focus
DeepDiver1975 Aug 26, 2026
2daed7a
fix(co-messung): stop cells growing wider than their column
DeepDiver1975 Aug 26, 2026
c416668
fix(co-messung): refresh CO-Messprotokoll view model on session changes
DeepDiver1975 Aug 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ seed-source/
# Local SDD scratch / ledger
.superpowers/

# Local Claude Code state
# Local AI agent state
.claude/
.opencode/

# Generated voice/audio drafts (filenames can carry voice-artist names)
*.mp3
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.Globalization;
using Avalonia.Data.Converters;
using Avalonia.Media;

namespace LageBuch.App.Shared.Converters;

public sealed class StatusToForegroundValueConverter : IValueConverter
{
public static readonly StatusToForegroundValueConverter Instance = new();

public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return new SolidColorBrush(Colors.Black);
}

public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) =>
throw new NotSupportedException();
}
252 changes: 252 additions & 0 deletions src/LageBuch.App.Shared/Views/CoMessprotokollView.axaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
<UserControl xmlns="/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:LageBuch.AppLogic.ViewModels;assembly=LageBuch.AppLogic"
xmlns:conv="clr-namespace:LageBuch.App.Shared.Converters"
xmlns:dom="clr-namespace:LageBuch.Domain.CoMeasurement;assembly=LageBuch.Domain"
x:Class="LageBuch.App.Shared.Views.CoMessprotokollView"
x:DataType="vm:CoMessprotokollViewModel">
<DockPanel>
<!-- Header: Building selector + actions -->
<Grid DockPanel.Dock="Top" ColumnDefinitions="*,Auto,Auto" Margin="0,0,0,14">
<ComboBox Grid.Column="0" ItemsSource="{Binding BuildingOptions}"
SelectedItem="{Binding SelectedBuilding}"
PlaceholderText="Haus auswählen..."
IsEnabled="{Binding CanModify}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="dom:Building">
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<Button Grid.Column="1" Content="HAUS HINZUFÜGEN"
Command="{Binding AddBuildingCommand}"
IsEnabled="{Binding !IsReadOnly}"
Margin="8,0,0,0" />
<Button Grid.Column="2" Content="HAUS ENTFERNEN"
Command="{Binding RemoveBuildingCommand}"
IsEnabled="{Binding CanModify}"
Margin="8,0,0,0" />
</Grid>

<!-- Add Building Dialog -->
<Border DockPanel.Dock="Top"
IsVisible="{Binding IsAddBuildingDialogOpen}"
Background="#0F141D"
BorderBrush="{StaticResource HairlineBrush}"
BorderThickness="1"
CornerRadius="{StaticResource RadiusMd}"
Padding="14" Margin="0,0,0,14">
<StackPanel Spacing="8">
<TextBlock Text="NEUES HAUS HINZUFÜGEN" Classes="overline" />
<StackPanel Spacing="2">
<TextBlock Text="BEZEICHNUNG" Classes="overline" FontSize="10" />
<TextBox Text="{Binding NewBuildingName}" PlaceholderText="z. B. Haus A" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="8">
<StackPanel Spacing="2">
<TextBlock Text="GESCHOOSSE" Classes="overline" FontSize="10" />
<NumericUpDown Minimum="1" Maximum="50" Value="{Binding NewBuildingFloors}" FormatString="0" />
</StackPanel>
<StackPanel Spacing="2">
<TextBlock Text="WOHNUNGEN/GESCHOSS" Classes="overline" FontSize="10" />
<NumericUpDown Minimum="1" Maximum="30" Value="{Binding NewBuildingApartments}" FormatString="0" />
</StackPanel>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Classes="signal" Content="HINZUFÜGEN" Command="{Binding ConfirmAddBuildingCommand}" />
<Button Content="ABBRECHEN" Command="{Binding CancelAddBuildingCommand}" />
</StackPanel>
</StackPanel>
</Border>

<!-- Remove Building Confirmation -->
<Border DockPanel.Dock="Top"
IsVisible="{Binding IsRemoveBuildingConfirmOpen}"
Background="#0F141D"
BorderBrush="{StaticResource HairlineBrush}"
BorderThickness="1"
CornerRadius="{StaticResource RadiusMd}"
Padding="14" Margin="0,0,0,14">
<StackPanel Spacing="8">
<TextBlock Text="HAUS ENTFERNEN" Classes="overline" />
<TextBlock Text="Möchten Sie das ausgewählte Haus wirklich entfernen? Alle zugehörigen Wohnungen werden ebenfalls gelöscht." TextWrapping="Wrap" />
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Classes="signal" Content="ENTFERNEN" Command="{Binding ConfirmRemoveBuildingCommand}" />
<Button Content="ABBRECHEN" Command="{Binding CancelRemoveBuildingCommand}" />
</StackPanel>
</StackPanel>
</Border>

<!-- Empty state -->
<TextBlock DockPanel.Dock="Top"
Text="Keine Häuser erfasst. Klicken Sie auf 'HAUS HINZUFÜGEN' um zu beginnen."
Foreground="{StaticResource TextMutedBrush}"
IsVisible="{Binding !HasBuildings}"
Margin="0,20,0,0" HorizontalAlignment="Center" />

<!-- Editor Sidebar (right) -->
<Border DockPanel.Dock="Right"
IsVisible="{Binding IsEditorOpen}"
Background="#0F141D"
BorderBrush="{StaticResource HairlineBrush}"
BorderThickness="1,0,0,0"
Width="320"
Padding="16"
IsEnabled="{Binding !IsReadOnly}">
<StackPanel Spacing="12">
<!-- Header with close button -->
<Grid ColumnDefinitions="*,Auto">
<TextBlock Grid.Column="0" Text="WOHNUNG BEARBEITEN" Classes="overline"
VerticalAlignment="Center" />
<Button Grid.Column="1" Content="&#x2715;"
Command="{Binding CloseEditorCommand}"
Background="Transparent" BorderThickness="0"
FontSize="14" Padding="4" />
</Grid>

<!-- Status buttons -->
<TextBlock Text="STATUS" Classes="overline" FontSize="10" />
<StackPanel Spacing="8">
<Button Background="#FFC000" Content="Noch nicht abgesucht"
HorizontalContentAlignment="Center" HorizontalAlignment="Stretch"
Command="{Binding SetEditorStatusNotSearchedCommand}" />
<Button Background="#92D050" Content="Abgesucht – keine Personen"
HorizontalContentAlignment="Center" HorizontalAlignment="Stretch"
Command="{Binding SetEditorStatusSearchedCommand}" />
<Button Background="#FF0000" Content="Person(en) betroffen"
HorizontalContentAlignment="Center" HorizontalAlignment="Stretch"
Command="{Binding SetEditorStatusAffectedCommand}" />
</StackPanel>

<!-- CO Value -->
<StackPanel Spacing="2">
<TextBlock Text="CO-WERT (ppm)" Classes="overline" FontSize="10" />
<NumericUpDown Minimum="0" Maximum="9999" Value="{Binding SelectedCell.CoValue}"
FormatString="0" PlaceholderText="Kein Messwert" />
</StackPanel>

<!-- Resident Name -->
<StackPanel Spacing="2">
<TextBlock Text="NAME DER BEWOHNER" Classes="overline" FontSize="10" />
<TextBox Text="{Binding SelectedCell.ResidentName}" PlaceholderText="Optional" />
</StackPanel>

<!-- Key Available -->
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="SCHLÜSSEL VORHANDEN" Classes="overline" FontSize="10" />
<CheckBox IsChecked="{Binding SelectedCell.KeyAvailable}" />
</StackPanel>

<!-- Action buttons -->
<StackPanel Orientation="Horizontal" Spacing="8" HorizontalAlignment="Right">
<Button Content="ABBRECHEN" Command="{Binding CloseEditorCommand}" />
<Button Classes="signal" Content="FERTIG" Command="{Binding ConfirmEditorCommand}" />
</StackPanel>
</StackPanel>
</Border>

<!-- Matrix -->
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"
IsVisible="{Binding HasBuildings}">
<StackPanel>
<!-- Column headers (editable) -->
<StackPanel Orientation="Horizontal" Spacing="4" Margin="88,0,0,4">
<ItemsControl ItemsSource="{Binding ApartmentColumns}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Rows="1" ColumnSpacing="4" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:ApartmentColumnViewModel">
<!-- FluentTheme swaps in its own full-box chrome on focus/pointer-over, styling
Border#PART_BorderElement directly and ignoring the plain BorderBrush/
BorderThickness set below; override those exact states locally so the header
stays a quiet underline throughout instead of popping a box while editing. -->
<TextBox Text="{Binding Label}" Width="148" FontSize="11" FontWeight="SemiBold"
HorizontalContentAlignment="Center" Padding="4,2"
Background="Transparent" BorderThickness="0,0,0,1"
BorderBrush="{StaticResource HairlineBrush}"
IsEnabled="{Binding !IsReadOnly}">
<TextBox.Styles>
<Style Selector="TextBox:focus /template/ Border#PART_BorderElement">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{StaticResource BorderStrongBrush}" />
<Setter Property="BorderThickness" Value="0,0,0,1" />
</Style>
<Style Selector="TextBox:pointerover /template/ Border#PART_BorderElement">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{StaticResource BorderStrongBrush}" />
</Style>
</TextBox.Styles>
</TextBox>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
<ItemsControl ItemsSource="{Binding MatrixRows}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Spacing="4" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:FloorRowViewModel">
<Grid ColumnDefinitions="80,Auto,*">
<TextBlock Grid.Column="0" Text="{Binding Label}" FontWeight="SemiBold"
VerticalAlignment="Center" Margin="0,0,8,0" />
<ItemsControl Grid.Column="1" ItemsSource="{Binding Cells}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Rows="1" ColumnSpacing="4" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:DwellingCellViewModel">
<Button Background="Transparent" BorderThickness="0" Padding="0" Height="NaN"
HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch"
Command="{Binding OpenEditorCommand}">
<!-- Door-mark card: dark tile, colored accent stripe + a slash/X/circled-X
glyph mirror the spray-marked search convention crews already read on
scene, instead of a solid traffic-light fill. -->
<Border Width="148" CornerRadius="6"
BorderBrush="{Binding StatusBrush}" BorderThickness="1"
Background="{StaticResource SurfaceRaisedBrush}"
ClipToBounds="True">
<Grid ColumnDefinitions="4,*">
<Border Grid.Column="0" Background="{Binding StatusBrush}" />
<Grid Grid.Column="1" ColumnDefinitions="*,Auto" RowDefinitions="Auto,Auto"
Margin="8,6">
<StackPanel Grid.Row="0" Grid.Column="0" Orientation="Horizontal" Spacing="5">
<TextBlock Text="{Binding StatusGlyph}" FontFamily="{StaticResource MonoFont}"
FontWeight="Bold" FontSize="13"
Foreground="{Binding StatusBrush}" />
<TextBlock Text="{Binding CoDisplay}" FontFamily="{StaticResource MonoFont}"
FontSize="12" Foreground="{StaticResource TextPrimaryBrush}"
TextTrimming="CharacterEllipsis" />
</StackPanel>
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding KeyDisplay}" FontSize="11"
Margin="6,0,0,0" Foreground="{StaticResource TextSecondaryBrush}" />
<TextBlock Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"
Text="{Binding ResidentName}" FontSize="10"
Foreground="{StaticResource TextSecondaryBrush}"
TextTrimming="CharacterEllipsis" MaxWidth="112" />
</Grid>
</Grid>
</Border>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock Grid.Column="2" Text="{Binding Description}"
VerticalAlignment="Center" Margin="8,0,0,0"
Width="200" TextWrapping="Wrap" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ScrollViewer>

</DockPanel>
</UserControl>
11 changes: 11 additions & 0 deletions src/LageBuch.App.Shared/Views/CoMessprotokollView.axaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Avalonia.Controls;

namespace LageBuch.App.Shared.Views;

public partial class CoMessprotokollView : UserControl
{
public CoMessprotokollView()
{
InitializeComponent();
}
}
5 changes: 5 additions & 0 deletions src/LageBuch.App.Shared/Views/IncidentWorkspaceView.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,11 @@
<ContentControl Content="{Binding Scba}" />
</Border>
</TabItem>
<TabItem Header="CO-MESSUNG">
<Border Background="{StaticResource SurfaceBrush}" Padding="24" Margin="0">
<ContentControl Content="{Binding CoMessprotokoll}" />
</Border>
</TabItem>
<TabItem Header="DATEIEN">
<Border Background="{StaticResource SurfaceBrush}" Padding="24" Margin="0">
<ContentControl Content="{Binding Files}" />
Expand Down
27 changes: 27 additions & 0 deletions src/LageBuch.AppLogic/LocalIncidentSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using LageBuch.Documents;
using LageBuch.Domain;
using LageBuch.Domain.Atemschutz;
using LageBuch.Domain.CoMeasurement;
using LageBuch.Domain.Etb;
using LageBuch.Domain.Files;
using LageBuch.Domain.Time;
Expand Down Expand Up @@ -204,6 +205,30 @@ public Task AddFileAsync(string fileName, string contentType, byte[] bytes)

public void RenameFile(Guid fileId, string? displayName) => Mutate(() => Incident.RenameFile(fileId, displayName));

public void AddCoBuilding(string name, int floorCount, int apartmentsPerFloor) =>
Mutate(() => Incident.AddCoBuilding(_clock, RequireOperator(), name, floorCount, apartmentsPerFloor));

public void UpdateCoBuildingStructure(Guid buildingId, int floorCount, int apartmentsPerFloor) =>
Mutate(() => Incident.UpdateCoBuildingStructure(_clock, RequireOperator(), buildingId, floorCount, apartmentsPerFloor));

public void RemoveCoBuilding(Guid buildingId) =>
Mutate(() => Incident.RemoveCoBuilding(_clock, RequireOperator(), buildingId));

public void RecordCoValue(Guid buildingId, int floorOrdinal, int apartmentNumber, int? coValue) =>
Mutate(() => Incident.RecordCoValue(_clock, RequireOperator(), buildingId, floorOrdinal, apartmentNumber, coValue));

public void SetDwellingStatus(Guid buildingId, int floorOrdinal, int apartmentNumber, DwellingStatus status) =>
Mutate(() => Incident.SetDwellingStatus(_clock, RequireOperator(), buildingId, floorOrdinal, apartmentNumber, status));

public void SetDwellingDetails(Guid buildingId, int floorOrdinal, int apartmentNumber, string? residentName, bool? keyAvailable) =>
Mutate(() => Incident.SetDwellingDetails(buildingId, floorOrdinal, apartmentNumber, residentName, keyAvailable));

public void SetFloorDescription(Guid buildingId, int floorOrdinal, string? description) =>
Mutate(() => Incident.SetFloorDescription(buildingId, floorOrdinal, description));

public void SetApartmentLabel(Guid buildingId, int apartmentNumber, string? label) =>
Mutate(() => Incident.SetApartmentLabel(buildingId, apartmentNumber, label));

public void Close()
{
if (IsReadOnly)
Expand All @@ -215,6 +240,8 @@ public void Close()

private void Mutate(Action apply)
{
if (IsReadOnly)
throw new InvalidOperationException("Der Einsatz ist bereits abgeschlossen.");
apply();
Save();
Changed?.Invoke();
Expand Down
Loading
Loading