Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ A PR template with a short checklist will guide you:

## Master data and PII

Dropdown contents (roles, call signs, streets, personnel) are treated as
Dropdown contents (roles, call signs, brigades, personnel) are treated as
personally identifying data and must **never** be committed to this repository.
`seed-source/` and `*.masterdata.json` are gitignored for exactly this reason;
the only tracked master data is the anonymised
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ git tag v0.3.0 && git push origin v0.3.0

## Master data

Dropdown contents (roles, radio call signs, streets, personnel, ...) are treated
Dropdown contents (roles, radio call signs, brigades, personnel, ...) are treated
as PII and are **never compiled into the application**. A fresh install starts
with **empty** master data; you populate it in the in-app **Stammdaten** editor
by importing a JSON file, and can write your own data back out again.
Expand Down
9 changes: 0 additions & 9 deletions docs/master-data.example.json
Original file line number Diff line number Diff line change
@@ -1,24 +1,16 @@
{
"roles": ["EL", "EAL", "ZF", "GF"],
"status": ["aufgenommen", "übermittelt", "abgearbeitet"],
"unitStatus": ["Alarmiert", "Auf Anfahrt", "Bereitstellungsraum", "Im Einsatz"],
"equipment": ["Mobilteil 1", "Digitalkamera"],
"districts": ["Musterstadt", "Musterdorf"],
"radioCallSigns": ["Florian Musterstadt 1", "Florian Musterstadt 11/1"],
"brigades": ["FF Musterstadt", "FF Musterdorf"],
"truppTypes": ["Angriffstrupp", "Wassertrupp", "Schlauchtrupp", "CSA-Trupp", "LPA-Trupp"],
"einsatzarten": ["B", "THL", "R", "ABC", "Sonstiges"],
"checklistTemplateAufbau": [
{ "text": "Aufstellort ELW frei?", "mandatory": true },
{ "text": "Kennleuchte ein, Blaulicht aus?", "mandatory": false }
],
"checklistTemplateAbbau": [
{ "text": "Fahrzeug abgerüstet?", "mandatory": true }
],
"streets": [
{ "name": "Bahnhofstraße", "district": "Musterstadt" },
{ "name": "Hauptstraße", "district": "Musterdorf" }
],
"personnel": [
{ "lastName": "Mustermann", "firstName": "Max", "role": "ZF", "callSign": "Florian Musterstadt 1", "phone": "0171 0000000" },
{ "lastName": "Musterfrau", "firstName": "Erika", "role": "GF", "callSign": null, "phone": null }
Expand All @@ -29,7 +21,6 @@
"agtMaxDurationMinutes": 30,
"csaMaxDurationMinutes": 20,
"lpaMaxDurationMinutes": 60,
"pressureControlIntervalMinutes": 5,
"returnPressureBar": 50
}
}
6 changes: 0 additions & 6 deletions src/LageBuch.App.Shared/Views/MasterDataEditorView.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,6 @@
Minimum="1" Maximum="180" Increment="1" FormatString="0" />
<TextBlock Grid.Column="2" Text="Min." Classes="secondary" Margin="8,0,0,0" VerticalAlignment="Center" />
</Grid>
<Grid ColumnDefinitions="*,140,Auto" VerticalAlignment="Center">
<TextBlock Grid.Column="0" Text="Atemschutzkontrolle – Intervall" VerticalAlignment="Center" />
<NumericUpDown Grid.Column="1" Value="{Binding PressureControlIntervalMinutes}"
Minimum="1" Maximum="60" Increment="1" FormatString="0" />
<TextBlock Grid.Column="2" Text="Min." Classes="secondary" Margin="8,0,0,0" VerticalAlignment="Center" />
</Grid>
<Grid ColumnDefinitions="*,140,Auto" VerticalAlignment="Center">
<TextBlock Grid.Column="0" Text="Rückzugsdruck" VerticalAlignment="Center" />
<NumericUpDown Grid.Column="1" Value="{Binding ReturnPressureBar}"
Expand Down
60 changes: 25 additions & 35 deletions src/LageBuch.AppLogic/ViewModels/MasterDataEditorViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ namespace LageBuch.AppLogic.ViewModels;

/// <summary>
/// The Stammdaten editor. Loads every editable category from the provider into its own section,
/// tracks a single dirty flag across them, and writes the whole set back on Save. Streets are read
/// but not editable, so they are carried through untouched. Import (offered only while the data is
/// empty) fills the editor from a JSON file for review; Export writes the current set back out.
/// tracks a single dirty flag across them, and writes the whole set back on Save. Import (offered
/// only while the data is empty) fills the editor from a JSON file for review; Export writes the
/// current set back out.
/// </summary>
public sealed partial class MasterDataEditorViewModel : ObservableObject
{
Expand All @@ -24,18 +24,9 @@ public sealed partial class MasterDataEditorViewModel : ObservableObject
// Typed handles kept so BuildSet reads each section without fragile positional casts.
private EditableListSection _roles = null!;

// Typed handles kept so BuildSet reads each section without fragile positional casts.
private EditableListSection _status = null!;

// Typed handles kept so BuildSet reads each section without fragile positional casts.
private EditableListSection _unitStatus = null!;

// Typed handles kept so BuildSet reads each section without fragile positional casts.
private EditableListSection _equipment = null!;

// Typed handles kept so BuildSet reads each section without fragile positional casts.
private EditableListSection _districts = null!;

// Typed handles kept so BuildSet reads each section without fragile positional casts.
private EditableListSection _brigades = null!;

Expand All @@ -45,9 +36,6 @@ public sealed partial class MasterDataEditorViewModel : ObservableObject
// Typed handles kept so BuildSet reads each section without fragile positional casts.
private EditableListSection _truppTypes = null!;

// Typed handles kept so BuildSet reads each section without fragile positional casts.
private EditableListSection _einsatzarten = null!;

private ChecklistTemplateSection _checklistAufbau = null!;
private ChecklistTemplateSection _checklistAbbau = null!;
private LinksSection _links = null!;
Expand Down Expand Up @@ -110,21 +98,29 @@ private void PopulateSections(MasterDataSet set)
var previousIndex = SelectedSection is null ? 0 : Sections.IndexOf(SelectedSection);

Sections.Clear();

// Einstellungen is a meta section (numeric defaults), not a data category, so it stays
// pinned first; everything else sorts alphabetically below it.
Sections.Add(_settings = new SettingsSection("Einstellungen", set.Settings, MarkDirty));
Sections.Add(_roles = new EditableListSection("Rollen", set.Roles, MarkDirty));
Sections.Add(_status = new EditableListSection("Status", set.Status, MarkDirty));
Sections.Add(_unitStatus = new EditableListSection("Einheiten-Status", set.UnitStatus, MarkDirty));
Sections.Add(_equipment = new EditableListSection("Ausrüstung", set.Equipment, MarkDirty));
Sections.Add(_districts = new EditableListSection("Bezirke", set.Districts, MarkDirty));
Sections.Add(_brigades = new EditableListSection("Wachen", set.Brigades, MarkDirty));
Sections.Add(_callSigns = new EditableListSection("Funkrufnamen", set.RadioCallSigns, MarkDirty));
Sections.Add(_truppTypes = new EditableListSection("Trupp-Typen", set.TruppTypes, MarkDirty));
Sections.Add(_einsatzarten = new EditableListSection("Einsatzarten", set.Einsatzarten, MarkDirty));
Sections.Add(_links = new LinksSection("Links", set.Links, MarkDirty));
Sections.Add(_checklistAufbau = new ChecklistTemplateSection("Checkliste Aufbau", set.ChecklistTemplateAufbau, MarkDirty));
Sections.Add(_checklistAbbau = new ChecklistTemplateSection("Checkliste Abbau", set.ChecklistTemplateAbbau, MarkDirty));
Sections.Add(_personnel = new PersonnelSection("Personal", set.Personnel, MarkDirty));
Sections.Add(_vehicles = new VehiclesSection("Fahrzeuge", set.Vehicles, set.Brigades, set.RadioCallSigns, OnVehiclesChanged));

EditorSection[] categories =
{
_roles = new EditableListSection("Rollen", set.Roles, MarkDirty),
_unitStatus = new EditableListSection("Einheiten-Status", set.UnitStatus, MarkDirty),
_brigades = new EditableListSection("Wachen", set.Brigades, MarkDirty),
_callSigns = new EditableListSection("Funkrufnamen", set.RadioCallSigns, MarkDirty),
_truppTypes = new EditableListSection("Trupp-Typen", set.TruppTypes, MarkDirty),
_links = new LinksSection("Links", set.Links, MarkDirty),
_checklistAufbau = new ChecklistTemplateSection("Checkliste Aufbau", set.ChecklistTemplateAufbau, MarkDirty),
_checklistAbbau = new ChecklistTemplateSection("Checkliste Abbau", set.ChecklistTemplateAbbau, MarkDirty),
_personnel = new PersonnelSection("Personal", set.Personnel, MarkDirty),
_vehicles = new VehiclesSection("Fahrzeuge", set.Vehicles, set.Brigades, set.RadioCallSigns, OnVehiclesChanged),
};

foreach (var section in categories.OrderBy(s => s.Title, StringComparer.OrdinalIgnoreCase))
{
Sections.Add(section);
}

SelectedSection = Sections[Math.Clamp(previousIndex < 0 ? 0 : previousIndex, 0, Sections.Count - 1)];
}
Expand Down Expand Up @@ -152,22 +148,16 @@ private void RefreshVehicleConflicts()
private MasterDataSet BuildSet() => _original with
{
Roles = _roles.ToValues(),
Status = _status.ToValues(),
UnitStatus = _unitStatus.ToValues(),
Equipment = _equipment.ToValues(),
Districts = _districts.ToValues(),
Brigades = _brigades.ToValues(),
RadioCallSigns = _callSigns.ToValues(),
TruppTypes = _truppTypes.ToValues(),
Einsatzarten = _einsatzarten.ToValues(),
Links = _links.ToValues(),
ChecklistTemplateAufbau = _checklistAufbau.ToValues(),
ChecklistTemplateAbbau = _checklistAbbau.ToValues(),
Personnel = _personnel.ToPeople(),
Vehicles = _vehicles.ToValues(),
Settings = _settings.ToSettings(),

// Streets are not editable here; _original carries them through unchanged.
};

[RelayCommand(CanExecute = nameof(CanSave))]
Expand Down
13 changes: 3 additions & 10 deletions src/LageBuch.AppLogic/ViewModels/SettingsSection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ namespace LageBuch.AppLogic.ViewModels;

/// <summary>
/// Editor for the operational defaults (<see cref="IncidentSettings"/>): the ILS reminder interval,
/// the AGT/CSA Einsatzzeiten, the Druckkontrolle interval and the Rückzugsdruck. Unlike the list
/// sections these are scalar numbers, so the section exposes one bindable property each and reports
/// any change through the shared dirty callback.
/// the AGT/CSA Einsatzzeiten and the Rückzugsdruck. Unlike the list sections these are scalar
/// numbers, so the section exposes one bindable property each and reports any change through the
/// shared dirty callback.
/// </summary>
public sealed partial class SettingsSection : EditorSection
{
Expand All @@ -23,7 +23,6 @@ public SettingsSection(string title, IncidentSettings settings, Action onChanged
_agtMaxDurationMinutes = settings.AgtMaxDurationMinutes;
_csaMaxDurationMinutes = settings.CsaMaxDurationMinutes;
_lpaMaxDurationMinutes = settings.LpaMaxDurationMinutes;
_pressureControlIntervalMinutes = settings.PressureControlIntervalMinutes;
_returnPressureBar = settings.ReturnPressureBar;
}

Expand All @@ -42,9 +41,6 @@ public SettingsSection(string title, IncidentSettings settings, Action onChanged
[ObservableProperty]
private int _lpaMaxDurationMinutes;

[ObservableProperty]
private int _pressureControlIntervalMinutes;

[ObservableProperty]
private int _returnPressureBar;

Expand All @@ -58,8 +54,6 @@ public SettingsSection(string title, IncidentSettings settings, Action onChanged

partial void OnLpaMaxDurationMinutesChanged(int value) => _onChanged();

partial void OnPressureControlIntervalMinutesChanged(int value) => _onChanged();

partial void OnReturnPressureBarChanged(int value) => _onChanged();

public IncidentSettings ToSettings() => new(
Expand All @@ -68,6 +62,5 @@ public SettingsSection(string title, IncidentSettings settings, Action onChanged
AgtMaxDurationMinutes,
CsaMaxDurationMinutes,
LpaMaxDurationMinutes,
PressureControlIntervalMinutes,
ReturnPressureBar);
}
48 changes: 3 additions & 45 deletions src/LageBuch.Persistence/MasterData/MasterDataSet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,15 @@ namespace LageBuch.Persistence.MasterData;

public sealed record MasterDataSet(
IReadOnlyList<string> Roles,
IReadOnlyList<string> Status,
IReadOnlyList<string> Equipment,
IReadOnlyList<string> Districts,
IReadOnlyList<string> RadioCallSigns,
IReadOnlyList<string> Brigades,

// UnitStatus is the status of a single unit (Alarmiert, Auf Anfahrt, ...) and is deliberately
// separate from Status above, which is the incident-level vocabulary (aufgenommen, ...).
IReadOnlyList<string> UnitStatus,
IReadOnlyList<Street> Streets,
IReadOnlyList<Link> Links,
IReadOnlyList<ChecklistTemplateItem> ChecklistTemplateAufbau,
IReadOnlyList<ChecklistTemplateItem> ChecklistTemplateAbbau,
IReadOnlyList<string> TruppTypes,
IReadOnlyList<Person> Personnel,

// Einsatzart values (ABek Bayern) — the leading token of the complete Einsatznummer.
IReadOnlyList<string> Einsatzarten,

// Vehicles per Wache with their seat count (#76).
IReadOnlyList<Vehicle> Vehicles,

Expand All @@ -43,16 +33,11 @@ public sealed record MasterDataSet(
Array.Empty<string>(),
Array.Empty<string>(),
Array.Empty<string>(),
Array.Empty<string>(),
Array.Empty<string>(),
Array.Empty<string>(),
Array.Empty<Street>(),
Array.Empty<Link>(),
Array.Empty<ChecklistTemplateItem>(),
Array.Empty<ChecklistTemplateItem>(),
Array.Empty<string>(),
Array.Empty<Person>(),
Array.Empty<string>(),
Array.Empty<Vehicle>(),
IncidentSettings.Defaults);

Expand All @@ -63,16 +48,14 @@ public sealed record MasterDataSet(
/// mark the set non-empty would suppress the Import bootstrap on an otherwise fresh install.
/// </summary>
public bool IsEmpty =>
Roles.Count == 0 && Status.Count == 0 && Equipment.Count == 0 && Districts.Count == 0
Roles.Count == 0
&& RadioCallSigns.Count == 0 && Brigades.Count == 0 && UnitStatus.Count == 0
&& Streets.Count == 0 && Links.Count == 0 && ChecklistTemplateAufbau.Count == 0 && ChecklistTemplateAbbau.Count == 0
&& Links.Count == 0 && ChecklistTemplateAufbau.Count == 0 && ChecklistTemplateAbbau.Count == 0
&& TruppTypes.Count == 0
&& Personnel.Count == 0 && Einsatzarten.Count == 0
&& Personnel.Count == 0
&& Vehicles.Count == 0;
}

public sealed record Street(string Name, string District);

/// <summary>A named link — Stammdaten entry so useful external resources can be opened from an Einsatz.</summary>
[SuppressMessage("Design", "CA1054", Justification = "Link URLs are free-form display data in persisted master data; System.Uri would make non-parseable values (relay or relative links) fail to load.")]
[SuppressMessage("Design", "CA1056", Justification = "Link URLs are free-form display data in persisted master data; System.Uri would make non-parseable values (relay or relative links) fail to load.")]
Expand Down Expand Up @@ -107,9 +90,6 @@ public sealed record IncidentSettings(
// Atemschutz Einsatzzeit for an LPA-Trupp (long-duration apparatus) — longer than an AGT.
int LpaMaxDurationMinutes,

// Interval between Druckkontrollen (Atemschutzkontrolle).
int PressureControlIntervalMinutes,

// Rückzugsdruck: pressure at or below which a Trupp must turn back.
int ReturnPressureBar)
{
Expand All @@ -123,7 +103,6 @@ public sealed record IncidentSettings(
AgtMaxDurationMinutes: AtemschutzTrupp.DefaultMaxDurationMinutes,
CsaMaxDurationMinutes: AtemschutzTrupp.DefaultChemicalMaxDurationMinutes,
LpaMaxDurationMinutes: AtemschutzTrupp.DefaultLpaMaxDurationMinutes,
PressureControlIntervalMinutes: AtemschutzTrupp.DefaultPressureControlIntervalMinutes,
ReturnPressureBar: AtemschutzTrupp.DefaultReturnPressureBar);
}

Expand Down Expand Up @@ -272,15 +251,6 @@ static IReadOnlyList<string> Arr(JsonElement e, string prop) =>
? a.EnumerateArray().Select(x => x.GetString()!).ToList()
: Array.Empty<string>();

IReadOnlyList<Street> streets =
root.TryGetProperty("streets", out var st) && st.ValueKind == JsonValueKind.Array
? st.EnumerateArray()
.Select(s => new Street(
s.GetProperty("name").GetString()!,
s.TryGetProperty("district", out var d) ? d.GetString() ?? string.Empty : string.Empty))
.ToList()
: Array.Empty<Street>();

IReadOnlyList<Link> links =
root.TryGetProperty("links", out var lk) && lk.ValueKind == JsonValueKind.Array
? lk.EnumerateArray()
Expand All @@ -302,19 +272,14 @@ static IReadOnlyList<string> Arr(JsonElement e, string prop) =>

return new MasterDataSet(
Arr(root, "roles"),
Arr(root, "status"),
Arr(root, "equipment"),
Arr(root, "districts"),
Arr(root, "radioCallSigns"),
Arr(root, "brigades"),
Arr(root, "unitStatus"),
streets,
links,
checklistAufbau,
checklistAbbau,
Arr(root, "truppTypes"),
ParsePersonnel(root),
Arr(root, "einsatzarten"),
vehicles,
ParseSettings(root));
}
Expand Down Expand Up @@ -372,7 +337,6 @@ static int Int(JsonElement e, string prop, int fallback) =>
Int(s, "agtMaxDurationMinutes", d.AgtMaxDurationMinutes),
Int(s, "csaMaxDurationMinutes", d.CsaMaxDurationMinutes),
Int(s, "lpaMaxDurationMinutes", d.LpaMaxDurationMinutes),
Int(s, "pressureControlIntervalMinutes", d.PressureControlIntervalMinutes),
Int(s, "returnPressureBar", d.ReturnPressureBar));
}

Expand Down Expand Up @@ -406,17 +370,12 @@ public static string Serialize(MasterDataSet set)
var model = new
{
roles = set.Roles,
status = set.Status,
unitStatus = set.UnitStatus,
equipment = set.Equipment,
districts = set.Districts,
radioCallSigns = set.RadioCallSigns,
brigades = set.Brigades,
truppTypes = set.TruppTypes,
einsatzarten = set.Einsatzarten,
checklistTemplateAufbau = set.ChecklistTemplateAufbau.Select(i => new { text = i.Text, mandatory = i.IsMandatory }),
checklistTemplateAbbau = set.ChecklistTemplateAbbau.Select(i => new { text = i.Text, mandatory = i.IsMandatory }),
streets = set.Streets.Select(s => new { name = s.Name, district = s.District }),
links = set.Links.Select(l => new { name = l.Name, url = l.Url }),
vehicles = set.Vehicles.Select(v => new { wache = v.Wache, callSign = v.CallSign, seats = v.Seats }),
personnel = set.Personnel.Select(p => new
Expand All @@ -434,7 +393,6 @@ public static string Serialize(MasterDataSet set)
agtMaxDurationMinutes = set.Settings.AgtMaxDurationMinutes,
csaMaxDurationMinutes = set.Settings.CsaMaxDurationMinutes,
lpaMaxDurationMinutes = set.Settings.LpaMaxDurationMinutes,
pressureControlIntervalMinutes = set.Settings.PressureControlIntervalMinutes,
returnPressureBar = set.Settings.ReturnPressureBar,
},
};
Expand Down
Loading
Loading