CI / build-and-test (push) Canceled after 0s
Dashboard warnings (attendance, and now homework backlog and lateness) apply across all classes, but the existing Vorgänge case-file tab only works for the teacher's own class since it resolves the WebUntis classbook roster. Instead of loosening that coupling, students get their own narrower Vorgänge tab, and all three cross-class warnings now jump straight there, offering to open a matching case or create a new one. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
223 lines
9.3 KiB
C#
223 lines
9.3 KiB
C#
using System.Collections.ObjectModel;
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using CommunityToolkit.Mvvm.Input;
|
|
using LehrerApp.Core.Models;
|
|
using LehrerApp.Desktop.ViewModels.Students;
|
|
|
|
namespace LehrerApp.Desktop.ViewModels.ClassTeacher;
|
|
|
|
// ── Anzeige-Helfer ────────────────────────────────────────────────────────
|
|
|
|
public static class VorgangTagDisplay
|
|
{
|
|
// Vom Nutzer selbst genannte Schlagwörter — eigene Labels bleiben trotzdem frei möglich
|
|
// (AutoCompleteBox, gleiches Muster wie DocumentationTagDisplay.Suggestions).
|
|
public static string[] Suggestions { get; } =
|
|
["Absentismus", "Hausaufgaben", "Verspätungen", "Konflikte", "Mitarbeit", "Elternkontakt", "Eskalation"];
|
|
}
|
|
|
|
public static class VorgangStatusDisplay
|
|
{
|
|
public static string Label(VorgangStatus status) => status == VorgangStatus.Closed ? "Geschlossen" : "Offen";
|
|
}
|
|
|
|
/// Ergebnis des "→ Vorgang"-Auswahldialogs beim Anheften einer Klassenbuchzeile: entweder ein
|
|
/// bestehender, offener Vorgang oder der Titel für einen neu anzulegenden.
|
|
public sealed record PinToVorgangChoice(Guid? ExistingVorgangId, string? NewVorgangTitle);
|
|
|
|
/// Eingefrorene WebUntis-Klassenbuchzeile mit deutscher Anzeige-Aufbereitung — das Core-Modell
|
|
/// <see cref="VorgangClassRegisterEntry"/> bleibt bewusst framework-frei ohne Formatierungslogik.
|
|
public sealed record VorgangClassRegisterEntryRow(VorgangClassRegisterEntry Model)
|
|
{
|
|
public string DateLabel => Model.Date.ToString("dd.MM.yyyy");
|
|
public string StudentName => Model.StudentName;
|
|
public string SummaryLabel => string.Join(" · ", new[] { Model.Subject, Model.CategoryName, Model.Text }
|
|
.Where(v => !string.IsNullOrWhiteSpace(v)));
|
|
}
|
|
|
|
// ── Mehrfachauswahl Schüler*innen (Anlegen/Bearbeiten-Dialog) ────────────────
|
|
|
|
public partial class VorgangStudentOption(Guid studentId, string name) : ObservableObject
|
|
{
|
|
public Guid StudentId { get; } = studentId;
|
|
public string Name { get; } = name;
|
|
[ObservableProperty] private bool _isSelected;
|
|
}
|
|
|
|
// ── Listen-Eintrag mit aufklappbarer Detailansicht ───────────────────────────
|
|
|
|
/// <summary>
|
|
/// Ein Vorgang in der Liste des "Vorgänge"-Tabs. Verknüpfte Dokumentation/Klassenbucheinträge
|
|
/// werden von <see cref="ClassTeacherCasesViewModel"/> beim Laden befüllt; Link/Unlink-Aktionen
|
|
/// laufen über die hier übergebenen Callbacks zurück ins ViewModel (statt eigenem Repository-
|
|
/// Zugriff hier), damit dieser Wrapper ein reiner Anzeige-Baustein bleibt — analog
|
|
/// <see cref="DocumentationItem"/>.
|
|
/// </summary>
|
|
public partial class VorgangItem : ObservableObject
|
|
{
|
|
private readonly Func<VorgangItem, DocumentationItem, Task> _onLink;
|
|
private readonly Func<VorgangItem, DocumentationItem, Task> _onUnlink;
|
|
private readonly Func<VorgangItem, VorgangClassRegisterEntryRow, Task> _onRemoveClassRegisterEntry;
|
|
private readonly Func<DocumentationItem, Task> _onEditDocumentation;
|
|
|
|
public Vorgang Model { get; }
|
|
public string StudentNames { get; }
|
|
public List<TagChip> TagChips { get; }
|
|
public string StatusLabel => VorgangStatusDisplay.Label(Model.Status);
|
|
public bool IsOpen => Model.Status == VorgangStatus.Open;
|
|
public string CreatedLabel => $"Angelegt {Model.CreatedAt:dd.MM.yyyy}";
|
|
|
|
public ObservableCollection<DocumentationItem> LinkedDocumentation { get; } = [];
|
|
public ObservableCollection<DocumentationItem> AvailableDocumentation { get; } = [];
|
|
public ObservableCollection<VorgangClassRegisterEntryRow> ClassRegisterRows { get; } = [];
|
|
|
|
public bool HasLinkedDocumentation => LinkedDocumentation.Count > 0;
|
|
public bool HasAvailableDocumentation => AvailableDocumentation.Count > 0;
|
|
public bool HasClassRegisterRows => ClassRegisterRows.Count > 0;
|
|
|
|
[ObservableProperty] private bool _isRevealed;
|
|
|
|
public VorgangItem(Vorgang model, string studentNames,
|
|
Func<VorgangItem, DocumentationItem, Task> onLink,
|
|
Func<VorgangItem, DocumentationItem, Task> onUnlink,
|
|
Func<VorgangItem, VorgangClassRegisterEntryRow, Task> onRemoveClassRegisterEntry,
|
|
Func<DocumentationItem, Task> onEditDocumentation)
|
|
{
|
|
Model = model;
|
|
StudentNames = studentNames;
|
|
TagChips = model.Tags.Select(t => new TagChip(t)).ToList();
|
|
_onLink = onLink;
|
|
_onUnlink = onUnlink;
|
|
_onRemoveClassRegisterEntry = onRemoveClassRegisterEntry;
|
|
_onEditDocumentation = onEditDocumentation;
|
|
}
|
|
|
|
[RelayCommand] private void Reveal() => IsRevealed = !IsRevealed;
|
|
|
|
/// Öffnet den vollen Dokumentations-Dialog zum Nachlesen/Bearbeiten, ohne den Vorgänge-Tab zu
|
|
/// verlassen (Nutzer-Feedback: bisher musste man dafür in den Schülerdatensatz wechseln).
|
|
[RelayCommand]
|
|
private async Task EditDocumentation(DocumentationItem? doc)
|
|
{
|
|
if (doc is not null) await _onEditDocumentation(doc);
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task Link(DocumentationItem? doc)
|
|
{
|
|
if (doc is not null) await _onLink(this, doc);
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task Unlink(DocumentationItem? doc)
|
|
{
|
|
if (doc is not null) await _onUnlink(this, doc);
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task RemoveClassRegisterEntry(VorgangClassRegisterEntryRow? row)
|
|
{
|
|
if (row is not null) await _onRemoveClassRegisterEntry(this, row);
|
|
}
|
|
}
|
|
|
|
// ── Dialog: Vorgang anlegen/bearbeiten ────────────────────────────────────
|
|
|
|
public partial class VorgangDialogViewModel : ObservableObject
|
|
{
|
|
private readonly Vorgang? _editing;
|
|
|
|
public ObservableCollection<VorgangStudentOption> StudentOptions { get; }
|
|
|
|
[ObservableProperty] private string _title = "";
|
|
[ObservableProperty] private string _description = "";
|
|
[ObservableProperty] private string _newTag = "";
|
|
public ObservableCollection<string> Tags { get; } = [];
|
|
public string[] TagSuggestions => VorgangTagDisplay.Suggestions;
|
|
|
|
[ObservableProperty] private string _titleError = "";
|
|
[ObservableProperty] private string _studentsError = "";
|
|
|
|
public string DialogTitle => _editing is null ? "Vorgang anlegen" : "Vorgang bearbeiten";
|
|
public Vorgang? Result { get; private set; }
|
|
|
|
public VorgangDialogViewModel(List<StudentOption> rosterStudents, Vorgang? editing)
|
|
{
|
|
_editing = editing;
|
|
// Bei genau einem Kandidaten (z.B. der schülerbezogene Vorgänge-Tab im Schülerdetail) und
|
|
// einem neuen Vorgang ist ohnehin nur eine Auswahl möglich — Nutzer-Feedback: der sonst
|
|
// nötige Extra-Klick auf die einzige Checkbox ist unnötige Reibung.
|
|
StudentOptions = new ObservableCollection<VorgangStudentOption>(rosterStudents.Select(s =>
|
|
new VorgangStudentOption(s.Id, s.Name)
|
|
{
|
|
IsSelected = editing?.StudentIds.Contains(s.Id) == true
|
|
|| (editing is null && rosterStudents.Count == 1),
|
|
}));
|
|
if (editing is null) return;
|
|
|
|
Title = editing.Title;
|
|
Description = editing.Description;
|
|
foreach (var tag in editing.Tags) Tags.Add(tag);
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void AddTag()
|
|
{
|
|
if (string.IsNullOrWhiteSpace(NewTag)) return;
|
|
var tag = NewTag.Trim();
|
|
if (!Tags.Contains(tag)) Tags.Add(tag);
|
|
NewTag = "";
|
|
}
|
|
|
|
[RelayCommand] private void RemoveTag(string? tag) { if (tag is not null) Tags.Remove(tag); }
|
|
|
|
[RelayCommand]
|
|
private void Save()
|
|
{
|
|
TitleError = ""; StudentsError = "";
|
|
var valid = true;
|
|
|
|
if (string.IsNullOrWhiteSpace(Title)) { TitleError = "Titel erforderlich."; valid = false; }
|
|
var selectedIds = StudentOptions.Where(s => s.IsSelected).Select(s => s.StudentId).ToList();
|
|
if (selectedIds.Count == 0) { StudentsError = "Mindestens eine/n Schüler*in auswählen."; valid = false; }
|
|
|
|
if (!valid) return;
|
|
|
|
Result = _editing ?? new Vorgang();
|
|
Result.Title = Title.Trim();
|
|
Result.Description = (Description ?? "").Trim();
|
|
Result.StudentIds = selectedIds;
|
|
Result.Tags = Tags.ToList();
|
|
}
|
|
}
|
|
|
|
// ── Dialog: Klassenbuchzeile an (bestehenden oder neuen) Vorgang anheften ────
|
|
|
|
public partial class PinToVorgangDialogViewModel : ObservableObject
|
|
{
|
|
public string RowSummary { get; }
|
|
public List<VorgangItem> MatchingOpenCases { get; }
|
|
public bool HasMatchingOpenCases => MatchingOpenCases.Count > 0;
|
|
|
|
[ObservableProperty] private VorgangItem? _selectedCase;
|
|
[ObservableProperty] private string _newTitle = "";
|
|
[ObservableProperty] private string _error = "";
|
|
|
|
public PinToVorgangChoice? Result { get; private set; }
|
|
|
|
public PinToVorgangDialogViewModel(string rowSummary, List<VorgangItem> matchingOpenCases)
|
|
{
|
|
RowSummary = rowSummary;
|
|
MatchingOpenCases = matchingOpenCases;
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void Confirm()
|
|
{
|
|
Error = "";
|
|
if (SelectedCase is not null) { Result = new PinToVorgangChoice(SelectedCase.Model.Id, null); return; }
|
|
if (!string.IsNullOrWhiteSpace(NewTitle)) { Result = new PinToVorgangChoice(null, NewTitle.Trim()); return; }
|
|
Error = "Bestehenden Vorgang wählen oder Titel für einen neuen Vorgang eingeben.";
|
|
}
|
|
}
|