Files
LehrerApp/LehrerApp.Desktop/ViewModels/Students/DocumentationViewModels.cs
T
adminandClaude Opus 5.5 9a84bbaa38
CI / build-and-test (push) Canceled after 0s
Gemeinsame Dokumentation für mehrere Beteiligte eines Vorgangs
Ein Dokumentationseintrag kann jetzt weitere Schüler*innen betreffen
(Documentation.AdditionalStudentIds, StudentId bleibt Hauptperson). Der
Eintrag existiert nur einmal, erscheint aber im Dokumentations-Tab jeder
beteiligten Person. Der Dokumentations-Dialog bietet in beiden
Vorgänge-Tabs "Betrifft außerdem" mit den Beteiligten des Vorgangs an.
Klassenlehrer-Listen, WebUntis-Abgleich, Löschfristen und
Referenzzählung berücksichtigen die weiteren Beteiligten.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 08:43:24 +02:00

550 lines
24 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Models;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
namespace LehrerApp.Desktop.ViewModels.Students;
/// Für die Schüler-Auswahl im Dokumentationsdialog, wenn er ohne festen Schüler geöffnet wird
/// (Gruppen-Tab, siehe GroupDocumentationTabViewModel).
public record StudentOption(Guid Id, string Name);
// ── Dokumentation: deutsche Anzeige für Typ/Status (5.1) ─────────────────────
public static class DocumentationTypeDisplay
{
public static string[] Options { get; } =
["Gespräch", "Vorkommnis", "Förderplan", "Fehlzeit", "Elternanruf", "Elternbrief", "Planung / Erinnerung"];
public static string Label(DocumentationType t) => t switch
{
DocumentationType.Conversation => "Gespräch",
DocumentationType.Incident => "Vorkommnis",
DocumentationType.SupportPlan => "Förderplan",
DocumentationType.Absence => "Fehlzeit",
DocumentationType.ParentCall => "Elternanruf",
DocumentationType.ParentLetter => "Elternbrief",
DocumentationType.Planning => "Planung / Erinnerung",
_ => "",
};
public static DocumentationType FromLabel(string label) => label switch
{
"Vorkommnis" => DocumentationType.Incident,
"Förderplan" => DocumentationType.SupportPlan,
"Fehlzeit" => DocumentationType.Absence,
"Elternanruf" => DocumentationType.ParentCall,
"Elternbrief" => DocumentationType.ParentLetter,
"Planung / Erinnerung" => DocumentationType.Planning,
_ => DocumentationType.Conversation,
};
}
// ── Dokumentation: Labels zur Nachverfolgung ─────────────────────────────────
public static class DocumentationTagDisplay
{
// Vorschläge für die AutoCompleteBox im Dialog — eigene Labels sind trotzdem frei möglich.
public static string[] Suggestions { get; } =
[
"Kritisch", "Nacharbeiten", "Mit JGL abklären", "Erkundigung einholen",
"Elterngespräch nötig", "Mit Schulleitung abklären", "Klassenkonferenz",
"Frist beachten", "Beobachten", "Erledigt",
];
public static string ColorHex(string tag) => tag switch
{
"Kritisch" or "Dringend" => "#E53935", // rot – Priorität
"Erledigt" => "#43A047", // grün – abgeschlossen
"Beobachten" or "Frist beachten" => "#1E88E5", // blau – im Blick behalten
"Nacharbeiten" or "Mit JGL abklären" or "Erkundigung einholen"
or "Elterngespräch nötig" or "Mit Schulleitung abklären" or "Klassenkonferenz"
=> "#FB8C00", // orange – Handlungsbedarf
_ => "#757575", // grau – freies Label
};
}
public static class SupportStatusDisplay
{
public static string[] Options { get; } = ["Aktiv", "Abgeschlossen", "Pausiert"];
public static string Label(SupportStatus s) => s switch
{
SupportStatus.Completed => "Abgeschlossen",
SupportStatus.Paused => "Pausiert",
_ => "Aktiv",
};
public static SupportStatus FromLabel(string label) => label switch
{
"Abgeschlossen" => SupportStatus.Completed,
"Pausiert" => SupportStatus.Paused,
_ => SupportStatus.Active,
};
}
// ── Dialog: Dokumentationseintrag anlegen/bearbeiten (5.1.1–5.1.3, 5.3.1) ────
public partial class DocumentationDialogViewModel : ObservableObject
{
private readonly IAttachmentStorage _attachmentStorage;
private readonly Documentation? _editing;
private readonly Guid _studentId;
private readonly Guid? _contextGroupId;
private readonly List<string> _newlyUploadedStorageIds = [];
// Nur gesetzt, wenn der Dialog aus einem Gruppen-Kontext (5.1, Dokumentation-Tab der
// Lerngruppe) ohne festen Schüler geöffnet wird — vom Schüler-Tab aus (fester _studentId)
// bleibt die Liste leer und die Auswahl unsichtbar.
public List<StudentOption> StudentOptions { get; }
public bool CanPickStudent => StudentOptions.Count > 0;
[ObservableProperty] private StudentOption? _selectedStudent;
[ObservableProperty] private string _studentError = "";
// Weitere Beteiligte (Documentation.AdditionalStudentIds) — nur angeboten, wenn der Aufrufer
// Kandidaten mitgibt (z.B. die übrigen Schüler*innen eines Vorgangs). Die Hauptperson steht
// mit in der Liste, wird aber ausgeblendet (CoStudentChoice.IsPrimary).
public List<CoStudentChoice> CoStudents { get; }
public bool HasCoStudents => CoStudents.Any(c => !c.IsPrimary);
[ObservableProperty] private string _typeName = DocumentationTypeDisplay.Options[0];
[ObservableProperty] private string _dateText = DateOnly.FromDateTime(DateTime.Today).ToString("dd.MM.yyyy");
[ObservableProperty] private string _title = "";
[ObservableProperty] private string _content = "";
[ObservableProperty] private bool _isConfidential;
[ObservableProperty] private bool _excludeFromWebUntisSync;
[ObservableProperty] private string _newParticipant = "";
public ObservableCollection<string> Participants { get; } = [];
[ObservableProperty] private int _lessonCount = 1;
[ObservableProperty] private bool _absenceExcused;
[ObservableProperty] private string _absenceReason = "";
[ObservableProperty] private string _newMeasure = "";
public ObservableCollection<string> Measures { get; } = [];
[ObservableProperty] private string _reviewDateText = "";
[ObservableProperty] private string _supportStatusName = SupportStatusDisplay.Options[0];
// Elternanruf (nur Planung — Abhaken/Eindrücke laufen über ParentCallSessionDialog)
[ObservableProperty] private string _newCallPoint = "";
public ObservableCollection<string> CallPoints { get; } = [];
// Elternbrief
[ObservableProperty] private string _letterDraftContent = "";
[ObservableProperty] private string _letterSentDateText = "";
[ObservableProperty] private bool _letterResponseReceived;
[ObservableProperty] private string _letterResponseDateText = "";
[ObservableProperty] private string _letterResponseNote = "";
[ObservableProperty] private string _letterSentDateError = "";
[ObservableProperty] private string _letterResponseDateError = "";
// Anhänge (für alle Typen)
public ObservableCollection<AttachmentItem> Attachments { get; } = [];
[ObservableProperty] private string _attachmentError = "";
// Labels zur Nachverfolgung (für alle Typen)
[ObservableProperty] private string _newTag = "";
public ObservableCollection<string> Tags { get; } = [];
public string[] TagSuggestions => DocumentationTagDisplay.Suggestions;
[ObservableProperty] private string _titleError = "";
[ObservableProperty] private string _dateTextError = "";
[ObservableProperty] private string _reviewDateTextError = "";
public bool IsConversation => TypeName == "Gespräch";
public bool IsAbsence => TypeName == "Fehlzeit";
public bool IsSupportPlan => TypeName == "Förderplan";
public bool IsParentCall => TypeName == "Elternanruf";
public bool IsParentLetter => TypeName == "Elternbrief";
public string[] TypeOptions => DocumentationTypeDisplay.Options;
public string[] SupportStatusOptions => SupportStatusDisplay.Options;
private readonly bool _isNewDraft;
public string DialogTitle => _editing is null || _isNewDraft ? "Dokumentation hinzufügen" : "Dokumentation bearbeiten";
public long MaxAttachmentSizeBytes => IAttachmentStorage.MaxSizeBytes;
public Documentation? Result { get; private set; }
/// <param name="coStudentOptions">Kandidaten für "Betrifft außerdem" — ohne Angabe bleiben
/// bestehende weitere Beteiligte eines bearbeiteten Eintrags unverändert erhalten.</param>
public DocumentationDialogViewModel(Guid studentId, Documentation? editing, IAttachmentStorage attachmentStorage,
List<StudentOption>? studentOptions = null, Guid? contextGroupId = null, bool isNewDraft = false,
List<StudentOption>? coStudentOptions = null)
{
_studentId = studentId;
_editing = editing;
// editing ist dann ein vorbefüllter, noch ungespeicherter Entwurf (z.B. aus einer
// Vorgangs-Vorlage) — wird wie ein Bestandseintrag vorbelegt, aber als "hinzufügen" betitelt.
_isNewDraft = isNewDraft;
_attachmentStorage = attachmentStorage;
_contextGroupId = contextGroupId;
StudentOptions = studentOptions ?? [];
CoStudents = (coStudentOptions ?? [])
.Where(o => o.Id != Guid.Empty)
.DistinctBy(o => o.Id)
.Select(o => new CoStudentChoice(o.Id, o.Name)
{
IsSelected = editing?.AdditionalStudentIds.Contains(o.Id) == true,
})
.ToList();
if (CanPickStudent)
SelectedStudent = StudentOptions.FirstOrDefault(s => s.Id == studentId);
UpdatePrimaryCoStudent();
if (editing is null) return;
TypeName = DocumentationTypeDisplay.Label(editing.Type);
DateText = editing.Date.ToString("dd.MM.yyyy");
Title = editing.Title;
Content = editing.Content;
IsConfidential = editing.IsConfidential;
ExcludeFromWebUntisSync = editing.ExcludeFromWebUntisSync;
foreach (var p in editing.Participants) Participants.Add(p);
if (editing.AbsenceData is { } a)
{
LessonCount = a.LessonCount;
AbsenceExcused = a.Excused;
AbsenceReason = a.Reason ?? "";
}
if (editing.SupportData is { } s)
{
foreach (var m in s.Measures) Measures.Add(m);
ReviewDateText = s.ReviewDate?.ToString("dd.MM.yyyy") ?? "";
SupportStatusName = SupportStatusDisplay.Label(s.Status);
}
if (editing.ParentCallData is { } pc)
foreach (var point in pc.Points) CallPoints.Add(point.Text);
if (editing.ParentLetterData is { } pl)
{
LetterDraftContent = pl.DraftContent;
LetterSentDateText = pl.SentDate?.ToString("dd.MM.yyyy") ?? "";
LetterResponseReceived = pl.ResponseReceived;
LetterResponseDateText = pl.ResponseDate?.ToString("dd.MM.yyyy") ?? "";
LetterResponseNote = pl.ResponseNote;
}
foreach (var att in editing.Attachments)
Attachments.Add(new AttachmentItem(att.StorageId, att.FileName, att.SizeBytes, att.UploadedAt));
foreach (var tag in editing.Tags) Tags.Add(tag);
}
// Beim Bearbeiten bleibt die Hauptperson die des bestehenden Eintrags (Save() übernimmt die
// Auswahl dann nicht), sonst folgt sie der Bezug-Auswahl.
private Guid PrimaryStudentId =>
_editing is not null && !_isNewDraft ? _editing.StudentId
: CanPickStudent ? SelectedStudent?.Id ?? Guid.Empty
: _editing?.StudentId ?? _studentId;
partial void OnSelectedStudentChanged(StudentOption? value) => UpdatePrimaryCoStudent();
private void UpdatePrimaryCoStudent()
{
var primary = PrimaryStudentId;
foreach (var c in CoStudents) c.IsPrimary = c.Id == primary;
OnPropertyChanged(nameof(HasCoStudents));
}
partial void OnTypeNameChanged(string value)
{
OnPropertyChanged(nameof(IsConversation));
OnPropertyChanged(nameof(IsAbsence));
OnPropertyChanged(nameof(IsSupportPlan));
OnPropertyChanged(nameof(IsParentCall));
OnPropertyChanged(nameof(IsParentLetter));
}
// ── Anhänge ────────────────────────────────────────────────────────────
public void AddAttachment(string fileName, Stream content)
{
AttachmentError = "";
if (content.Length > IAttachmentStorage.MaxSizeBytes)
{
AttachmentError = $"Datei zu groß (max. {IAttachmentStorage.MaxSizeBytes / 1024 / 1024} MB).";
return;
}
var storageId = _attachmentStorage.Upload(fileName, content);
_newlyUploadedStorageIds.Add(storageId);
Attachments.Add(new AttachmentItem(storageId, fileName, content.Length, DateTime.UtcNow));
}
public Stream? OpenAttachment(AttachmentItem item) => _attachmentStorage.OpenRead(item.StorageId);
[RelayCommand]
private void RemoveAttachment(AttachmentItem? item)
{
if (item is null) return;
_attachmentStorage.Delete(item.StorageId);
_newlyUploadedStorageIds.Remove(item.StorageId);
Attachments.Remove(item);
}
/// Vom Code-Behind beim Abbrechen aufgerufen: neu hochgeladene, nie gespeicherte Anhänge
/// wieder entfernen, damit keine verwaisten Blobs in der Datenbank zurückbleiben.
public void DiscardUnsavedAttachments()
{
foreach (var id in _newlyUploadedStorageIds) _attachmentStorage.Delete(id);
}
[RelayCommand]
private void AddParticipant()
{
if (string.IsNullOrWhiteSpace(NewParticipant)) return;
Participants.Add(NewParticipant.Trim());
NewParticipant = "";
}
[RelayCommand]
private void RemoveParticipant(string? p) { if (p is not null) Participants.Remove(p); }
[RelayCommand]
private void AddMeasure()
{
if (string.IsNullOrWhiteSpace(NewMeasure)) return;
Measures.Add(NewMeasure.Trim());
NewMeasure = "";
}
[RelayCommand]
private void RemoveMeasure(string? m) { if (m is not null) Measures.Remove(m); }
[RelayCommand]
private void AddCallPoint()
{
if (string.IsNullOrWhiteSpace(NewCallPoint)) return;
CallPoints.Add(NewCallPoint.Trim());
NewCallPoint = "";
}
[RelayCommand]
private void RemoveCallPoint(string? p) { if (p is not null) CallPoints.Remove(p); }
[RelayCommand]
private void AddTag()
{
if (string.IsNullOrWhiteSpace(NewTag)) return;
var tag = NewTag.Trim();
if (Tags.Contains(tag)) return;
Tags.Add(tag);
NewTag = "";
}
[RelayCommand]
private void RemoveTag(string? t) { if (t is not null) Tags.Remove(t); }
[RelayCommand]
private void Save()
{
TitleError = ""; DateTextError = ""; ReviewDateTextError = ""; StudentError = "";
LetterSentDateError = ""; LetterResponseDateError = "";
var valid = true;
if (CanPickStudent && SelectedStudent is null) { StudentError = "Bezug auswählen."; valid = false; }
if (!CanPickStudent && _studentId == Guid.Empty && _contextGroupId is null)
{
StudentError = "Die Schülerliste ist noch nicht geladen. Dialog schließen und erneut öffnen.";
valid = false;
}
if (string.IsNullOrWhiteSpace(Title)) { TitleError = "Titel erforderlich."; valid = false; }
if (!DateOnly.TryParseExact(DateText, "dd.MM.yyyy", null, DateTimeStyles.None, out var date))
{ DateTextError = "Format TT.MM.JJJJ."; valid = false; }
DateOnly? reviewDate = null;
if (IsSupportPlan && !string.IsNullOrWhiteSpace(ReviewDateText))
{
if (!DateOnly.TryParseExact(ReviewDateText, "dd.MM.yyyy", null, DateTimeStyles.None, out var r))
{ ReviewDateTextError = "Format TT.MM.JJJJ."; valid = false; }
else reviewDate = r;
}
DateOnly? sentDate = null;
if (IsParentLetter && !string.IsNullOrWhiteSpace(LetterSentDateText))
{
if (!DateOnly.TryParseExact(LetterSentDateText, "dd.MM.yyyy", null, DateTimeStyles.None, out var s))
{ LetterSentDateError = "Format TT.MM.JJJJ."; valid = false; }
else sentDate = s;
}
DateOnly? responseDate = null;
if (IsParentLetter && !string.IsNullOrWhiteSpace(LetterResponseDateText))
{
if (!DateOnly.TryParseExact(LetterResponseDateText, "dd.MM.yyyy", null, DateTimeStyles.None, out var r))
{ LetterResponseDateError = "Format TT.MM.JJJJ."; valid = false; }
else responseDate = r;
}
if (!valid) return;
var type = DocumentationTypeDisplay.FromLabel(TypeName);
var effectiveStudentId = CanPickStudent ? SelectedStudent!.Id : _studentId;
Result = _editing ?? new Documentation { StudentId = effectiveStudentId, GroupId = _contextGroupId };
// Nicht angebotene weitere Beteiligte bleiben erhalten (z.B. beim Bearbeiten aus dem Schülerdatensatz,
// wo keine Kandidaten bekannt sind), angebotene folgen der Auswahl.
var offered = CoStudents.Select(c => c.Id).ToHashSet();
Result.AdditionalStudentIds = Result.AdditionalStudentIds.Where(id => !offered.Contains(id))
.Concat(CoStudents.Where(c => c.IsSelected).Select(c => c.Id))
.Where(id => id != Result.StudentId && id != Guid.Empty)
.Distinct()
.ToList();
Result.Type = type;
Result.Date = date;
Result.Title = Title.Trim();
Result.Content = (Content ?? "").Trim();
// Das bewusste Speichern im vollständigen Dialog schließt einen im Sitzplan erzeugten
// Schnellentwurf ab. Stunden- und Lesson-Bezug bleiben am bestehenden Objekt erhalten.
Result.IsDraft = false;
Result.IsConfidential = IsConfidential;
Result.ExcludeFromWebUntisSync = ExcludeFromWebUntisSync;
Result.Participants = type == DocumentationType.Conversation ? Participants.ToList() : [];
Result.AbsenceData = type == DocumentationType.Absence
? new AbsenceData
{
LessonCount = LessonCount,
Excused = AbsenceExcused,
Reason = string.IsNullOrWhiteSpace(AbsenceReason) ? null : AbsenceReason.Trim(),
}
: null;
Result.SupportData = type == DocumentationType.SupportPlan
? new SupportData
{
Measures = Measures.ToList(),
ReviewDate = reviewDate,
Status = SupportStatusDisplay.FromLabel(SupportStatusName),
}
: null;
if (type == DocumentationType.ParentCall)
{
var existingPoints = _editing?.ParentCallData?.Points ?? [];
Result.ParentCallData = new ParentCallData
{
// Punkte mit unverändertem Text behalten ihren Abhak-Status; neue/umbenannte
// Punkte starten offen — das Abhaken selbst läuft über ParentCallSessionDialog.
Points = CallPoints.Select(text =>
existingPoints.FirstOrDefault(p => p.Text == text) ?? new ParentCallPoint { Text = text }).ToList(),
Impressions = _editing?.ParentCallData?.Impressions ?? "",
IsConducted = _editing?.ParentCallData?.IsConducted ?? false,
ConductedDate = _editing?.ParentCallData?.ConductedDate,
};
}
else Result.ParentCallData = null;
Result.ParentLetterData = type == DocumentationType.ParentLetter
? new ParentLetterData
{
DraftContent = (LetterDraftContent ?? "").Trim(),
SentDate = sentDate,
ResponseReceived = LetterResponseReceived,
ResponseDate = responseDate,
ResponseNote = (LetterResponseNote ?? "").Trim(),
}
: null;
Result.Attachments = Attachments.Select(a => new DocumentAttachment
{
StorageId = a.StorageId, FileName = a.FileName, SizeBytes = a.SizeBytes, UploadedAt = a.UploadedAt,
}).ToList();
Result.Tags = Tags.ToList();
// Nach erfolgreichem Speichern sollen diese Anhänge NICHT mehr beim Abbrechen-Cleanup
// gelöscht werden — der Aufrufer speichert den Eintrag direkt im Anschluss an Save().
_newlyUploadedStorageIds.Clear();
}
}
public partial class CoStudentChoice(Guid id, string name) : ObservableObject
{
public Guid Id { get; } = id;
public string Name { get; } = name;
[ObservableProperty] private bool _isSelected;
[ObservableProperty] private bool _isPrimary;
}
public class AttachmentItem(string storageId, string fileName, long sizeBytes, DateTime uploadedAt)
{
public const string HazardAssessmentSuffix = ".gbu.json";
public string StorageId { get; } = storageId;
public string FileName { get; } = fileName;
public long SizeBytes { get; } = sizeBytes;
public DateTime UploadedAt { get; } = uploadedAt;
public string SizeDisplay => $"{SizeBytes / 1024.0:0} KB";
/// Strukturierter Gefährdungsbeurteilungs-Anhang (siehe HazardAssessment) statt einer
/// beliebigen hochgeladenen Datei — steuert im Anhang-Editor, ob "Öffnen"+"PDF" statt des
/// generischen "Speichern" angeboten wird.
public bool IsHazardAssessment => FileName.EndsWith(HazardAssessmentSuffix, StringComparison.OrdinalIgnoreCase);
}
public class TagChip(string text)
{
public string Text { get; } = text;
public string ColorHex { get; } = DocumentationTagDisplay.ColorHex(text);
}
// ── Listen-Eintrag mit Vertraulichkeits-Freigabe (5.1.3/5.4.1) ───────────────
public partial class DocumentationItem : ObservableObject
{
public Documentation Model { get; }
public string DateDisplay { get; }
public string TypeLabel { get; }
public bool IsConfidential { get; }
public bool IsParentCall { get; }
public bool HasAttachments { get; }
public bool IsDraft { get; }
public string StatusLabel { get; }
public List<TagChip> TagChips { get; }
/// Nur im Gruppen-Tab (5.1, GroupDocumentationTabViewModel) gefüllt — die Schüler-Detailansicht
/// zeigt ohnehin nur Einträge eines einzelnen Schülers und braucht den Namen nicht.
public string StudentName { get; }
/// True, wenn der Eintrag keiner Gruppe zugeordnet ist oder der aktuell angezeigten Gruppe
/// entspricht — false für Einträge aus einem anderen Unterricht desselben Schülers (werden im
/// Gruppen-Tab optisch abgesetzt statt komplett ausgeblendet, siehe Nutzer-Feedback).
public bool IsOwnGroup { get; }
/// Name der Gruppe, aus der ein nicht-eigener Eintrag stammt (nur gesetzt, wenn !IsOwnGroup).
public string OtherGroupLabel { get; }
/// Dimmt Einträge aus anderen Lerngruppen im Gruppen-Tab, ohne sie zu verstecken.
public double ContentOpacity => IsOwnGroup ? 1.0 : 0.55;
/// Nur im Schülerdatensatz gefüllt: die übrigen Beteiligten eines gemeinsamen Eintrags
/// (Documentation.AdditionalStudentIds bzw. Hauptperson), damit er dort als geteilt erkennbar ist.
public string SharedWith { get; init; } = "";
public bool IsShared => SharedWith != "";
[ObservableProperty] private bool _isRevealed;
public DocumentationItem(Documentation d, string studentName = "", bool isOwnGroup = true, string otherGroupLabel = "")
{
Model = d;
DateDisplay = d.Date.ToString("dd.MM.yyyy");
TypeLabel = DocumentationTypeDisplay.Label(d.Type);
IsConfidential = d.IsConfidential;
IsRevealed = !d.IsConfidential;
IsParentCall = d.Type == DocumentationType.ParentCall;
HasAttachments = d.Attachments.Count > 0;
IsDraft = d.IsDraft;
StatusLabel = BuildStatusLabel(d);
TagChips = d.Tags.Select(t => new TagChip(t)).ToList();
StudentName = studentName;
IsOwnGroup = isOwnGroup;
OtherGroupLabel = otherGroupLabel;
}
private static string BuildStatusLabel(Documentation d) => d.Type switch
{
_ when d.IsDraft => "Nacharbeiten",
DocumentationType.ParentCall when d.ParentCallData is { IsConducted: true } pc =>
$"Durchgeführt am {pc.ConductedDate:dd.MM.yyyy}",
DocumentationType.ParentCall => "Noch nicht durchgeführt",
DocumentationType.ParentLetter when d.ParentLetterData is { } pl =>
pl.ResponseReceived ? "Rückmeldung erhalten"
: pl.SentDate.HasValue ? $"Versendet am {pl.SentDate:dd.MM.yyyy}"
: "Noch nicht versendet",
_ => "",
};
[RelayCommand] private void Reveal() => IsRevealed = true;
}