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>
211 lines
8.9 KiB
C#
211 lines
8.9 KiB
C#
using System.Collections.ObjectModel;
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using CommunityToolkit.Mvvm.Input;
|
|
using LehrerApp.Core.Interfaces;
|
|
using LehrerApp.Core.Models;
|
|
using LehrerApp.Desktop.ViewModels.ClassTeacher;
|
|
|
|
namespace LehrerApp.Desktop.ViewModels.Students;
|
|
|
|
/// <summary>
|
|
/// Schülerbezogener "Vorgänge"-Tab im Schülerdetail — schmalere Variante von
|
|
/// <see cref="ClassTeacherCasesViewModel"/> ohne Klassen-Roster/WebUntis-Klassenbuch-Kopplung: zeigt
|
|
/// und verwaltet nur die Vorgänge dieser einen Person (<see cref="IVorgangRepository.GetByStudent"/>).
|
|
/// Universeller Einstiegspunkt für die klassenübergreifenden Dashboard-Warnungen (Fehlzeiten,
|
|
/// Hausaufgaben-Rückstand, Verspätungen) — der Klassenlehrer-"Vorgänge"-Tab bleibt bewusst
|
|
/// homeroom-only (siehe TODO.md). Beide teilen sich nur <see cref="Vorgang"/>/<see cref="VorgangItem"/>/
|
|
/// <see cref="VorgangDialogViewModel"/>, keine ViewModel-Logik.
|
|
/// </summary>
|
|
public partial class StudentVorgaengeViewModel : ObservableObject
|
|
{
|
|
private readonly IVorgangRepository _vorgaenge;
|
|
private readonly IDocumentationRepository _documentation;
|
|
private readonly IStudentRepository _students;
|
|
|
|
private Guid _studentId;
|
|
private string _studentName = "";
|
|
private List<StudentOption> _studentOptions = [];
|
|
|
|
public ObservableCollection<VorgangItem> Cases { get; } = [];
|
|
|
|
[ObservableProperty] private bool _onlyOpen = true;
|
|
|
|
public bool HasCases => Cases.Count > 0;
|
|
|
|
public Func<List<StudentOption>, Vorgang?, Task<Vorgang?>>? OnEditVorgang { get; set; }
|
|
public Func<VorgangItem, Task<bool>>? OnConfirmDeleteVorgang { get; set; }
|
|
public Func<Guid, Documentation?, Task<Documentation?>>? OnEditDocumentation { get; set; }
|
|
/// Rückfrage vor dem automatischen Anlegen eines Vorgangs aus einer Dashboard-Warnung heraus
|
|
/// (siehe <see cref="HandleWarningNavigation"/>) — Parameter ist das gesuchte Tag.
|
|
public Func<string, Task<bool>>? OnConfirmCreateForWarning { get; set; }
|
|
|
|
public StudentVorgaengeViewModel(IVorgangRepository vorgaenge, IDocumentationRepository documentation,
|
|
IStudentRepository students)
|
|
{
|
|
_vorgaenge = vorgaenge;
|
|
_documentation = documentation;
|
|
_students = students;
|
|
}
|
|
|
|
partial void OnOnlyOpenChanged(bool value) => Load(_studentId);
|
|
|
|
[RelayCommand] private void Refresh() => Load(_studentId);
|
|
|
|
/// Bewusst synchron (anders als ClassTeacherCasesViewModel.LoadInternal) — kein WebUntis-Roster
|
|
/// aufzulösen, deshalb kann HandleWarningNavigation direkt im Anschluss laufen, ohne auf einen
|
|
/// asynchronen Ladevorgang warten zu müssen.
|
|
public void Load(Guid studentId)
|
|
{
|
|
_studentId = studentId;
|
|
_studentName = _students.GetById(studentId)?.FullName ?? "";
|
|
_studentOptions = [new StudentOption(studentId, _studentName)];
|
|
|
|
// IsRevealed übersteht den Neuaufbau der Liste nicht automatisch (neue VorgangItem-Instanzen
|
|
// pro Load) - deshalb hier gemerkt und danach wiederhergestellt (gleiches Muster wie
|
|
// ClassTeacherCasesViewModel.LoadInternal).
|
|
var revealedIds = Cases.Where(c => c.IsRevealed).Select(c => c.Model.Id).ToHashSet();
|
|
Cases.Clear();
|
|
|
|
IEnumerable<Vorgang> vorgaenge = _vorgaenge.GetByStudent(studentId);
|
|
if (OnlyOpen) vorgaenge = vorgaenge.Where(v => v.Status == VorgangStatus.Open);
|
|
|
|
// Nur die Dokumentation dieses einen Schülers (nicht GetAll()) — dieses Panel ist die Sicht
|
|
// "von diesem Schüler aus", nicht Vorgangsverwaltung für mehrere Beteiligte.
|
|
var ownDocs = _documentation.GetByStudent(studentId);
|
|
|
|
foreach (var vorgang in vorgaenge.OrderByDescending(v => v.UpdatedAt))
|
|
{
|
|
var names = string.Join(", ", vorgang.StudentIds
|
|
.Select(id => id == studentId ? _studentName : _students.GetById(id)?.FullName)
|
|
.Where(n => !string.IsNullOrEmpty(n)));
|
|
var item = new VorgangItem(vorgang, names, LinkDocumentation, UnlinkDocumentation,
|
|
RemoveClassRegisterEntry, EditDocumentation)
|
|
{ IsRevealed = revealedIds.Contains(vorgang.Id) };
|
|
|
|
var linkedIds = vorgang.DocumentationIds.ToHashSet();
|
|
foreach (var doc in ownDocs.Where(d => linkedIds.Contains(d.Id)))
|
|
item.LinkedDocumentation.Add(new DocumentationItem(doc, _studentName));
|
|
foreach (var doc in ClassTeacherCasesViewModel.FilterAvailableDocumentation(ownDocs, vorgang))
|
|
item.AvailableDocumentation.Add(new DocumentationItem(doc, _studentName));
|
|
foreach (var entry in vorgang.ClassRegisterEntries.OrderByDescending(e => e.Date))
|
|
item.ClassRegisterRows.Add(new VorgangClassRegisterEntryRow(entry));
|
|
|
|
Cases.Add(item);
|
|
}
|
|
OnPropertyChanged(nameof(HasCases));
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task AddVorgang()
|
|
{
|
|
if (OnEditVorgang is null) return;
|
|
var result = await OnEditVorgang(_studentOptions, null);
|
|
if (result is null) return;
|
|
_vorgaenge.Save(result);
|
|
Load(_studentId);
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task EditVorgang(VorgangItem? item)
|
|
{
|
|
if (item is null || OnEditVorgang is null) return;
|
|
var result = await OnEditVorgang(_studentOptions, item.Model);
|
|
if (result is null) return;
|
|
_vorgaenge.Save(result);
|
|
Load(_studentId);
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void ToggleStatus(VorgangItem? item)
|
|
{
|
|
if (item is null) return;
|
|
item.Model.Status = item.Model.Status == VorgangStatus.Open ? VorgangStatus.Closed : VorgangStatus.Open;
|
|
item.Model.ClosedAt = item.Model.Status == VorgangStatus.Closed ? DateTime.UtcNow : null;
|
|
_vorgaenge.Save(item.Model);
|
|
Load(_studentId);
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task DeleteVorgang(VorgangItem? item)
|
|
{
|
|
if (item is null) return;
|
|
if (OnConfirmDeleteVorgang is not null && !await OnConfirmDeleteVorgang(item)) return;
|
|
_vorgaenge.Delete(item.Model.Id);
|
|
Load(_studentId);
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task CreateAndLinkDocumentation(VorgangItem? item)
|
|
{
|
|
if (item is null || OnEditDocumentation is null) return;
|
|
var result = await OnEditDocumentation(_studentId, null);
|
|
if (result is null) return;
|
|
_documentation.Save(result);
|
|
item.Model.DocumentationIds.Add(result.Id);
|
|
_vorgaenge.Save(item.Model);
|
|
Load(_studentId);
|
|
}
|
|
|
|
private async Task EditDocumentation(DocumentationItem doc)
|
|
{
|
|
if (OnEditDocumentation is null) return;
|
|
var result = await OnEditDocumentation(doc.Model.StudentId, doc.Model);
|
|
if (result is null) return;
|
|
_documentation.Save(result);
|
|
Load(_studentId);
|
|
}
|
|
|
|
private Task LinkDocumentation(VorgangItem item, DocumentationItem doc)
|
|
{
|
|
item.Model.DocumentationIds.Add(doc.Model.Id);
|
|
_vorgaenge.Save(item.Model);
|
|
Load(_studentId);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
private Task UnlinkDocumentation(VorgangItem item, DocumentationItem doc)
|
|
{
|
|
item.Model.DocumentationIds.Remove(doc.Model.Id);
|
|
_vorgaenge.Save(item.Model);
|
|
Load(_studentId);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
private Task RemoveClassRegisterEntry(VorgangItem item, VorgangClassRegisterEntryRow row)
|
|
{
|
|
item.Model.ClassRegisterEntries.Remove(row.Model);
|
|
_vorgaenge.Save(item.Model);
|
|
Load(_studentId);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Von <see cref="MainWindowViewModel.NavigateToStudentCase"/> direkt nach dem Laden aufgerufen:
|
|
/// sucht einen offenen Vorgang mit passendem Tag (z.B. "Absentismus" für die Fehlzeiten-Warnung)
|
|
/// und klappt ihn auf. Findet sich keiner (auch ein geschlossener Vorgang mit passendem Tag zählt
|
|
/// hier bewusst nicht als Treffer — er taucht bei OnlyOpen=true gar nicht erst in <see cref="Cases"/>
|
|
/// auf), wird per Rückfrage ein neuer mit vorbelegtem Titel/Tag/Schüler angeboten.
|
|
/// </summary>
|
|
public async Task HandleWarningNavigation(string suggestedTag, string suggestedTitle)
|
|
{
|
|
var match = Cases.FirstOrDefault(c =>
|
|
c.Model.Tags.Contains(suggestedTag, StringComparer.OrdinalIgnoreCase));
|
|
if (match is not null)
|
|
{
|
|
match.IsRevealed = true;
|
|
return;
|
|
}
|
|
|
|
if (OnConfirmCreateForWarning is null || OnEditVorgang is null) return;
|
|
if (!await OnConfirmCreateForWarning(suggestedTag)) return;
|
|
|
|
var draft = new Vorgang { Title = suggestedTitle, StudentIds = [_studentId], Tags = [suggestedTag] };
|
|
var result = await OnEditVorgang(_studentOptions, draft);
|
|
if (result is null) return;
|
|
_vorgaenge.Save(result);
|
|
Load(_studentId);
|
|
var created = Cases.FirstOrDefault(c => c.Model.Id == result.Id);
|
|
if (created is not null) created.IsRevealed = true;
|
|
}
|
|
}
|