Initial
This commit is contained in:
136
LehrerApp.Desktop/ViewModels/DashboardViewModel.cs
Normal file
136
LehrerApp.Desktop/ViewModels/DashboardViewModel.cs
Normal file
@@ -0,0 +1,136 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using LehrerApp.Core.Interfaces;
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Core.Services;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace LehrerApp.Desktop.ViewModels;
|
||||
|
||||
public partial class DashboardViewModel : ObservableObject
|
||||
{
|
||||
private readonly IGroupRepository _groups;
|
||||
private readonly ILessonRepository _lessons;
|
||||
private readonly IWorkTaskRepository _tasks;
|
||||
private readonly SchoolYearService _schoolYearService;
|
||||
|
||||
[ObservableProperty] private string _greeting = "";
|
||||
[ObservableProperty] private string _currentDate = "";
|
||||
[ObservableProperty] private string _currentSchoolYear = "";
|
||||
|
||||
public ObservableCollection<LessonItem> TodaysLessons { get; } = [];
|
||||
public ObservableCollection<TaskItem> OpenTasks { get; } = [];
|
||||
public ObservableCollection<GroupChip> CurrentGroups { get; } = [];
|
||||
|
||||
public DashboardViewModel(
|
||||
IGroupRepository groups,
|
||||
ILessonRepository lessons,
|
||||
IWorkTaskRepository tasks,
|
||||
SchoolYearService schoolYearService)
|
||||
{
|
||||
_groups = groups;
|
||||
_lessons = lessons;
|
||||
_tasks = tasks;
|
||||
_schoolYearService = schoolYearService;
|
||||
|
||||
Load();
|
||||
}
|
||||
|
||||
private void Load()
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
var today = DateOnly.FromDateTime(now);
|
||||
|
||||
CurrentDate = now.ToString("dddd, d. MMMM yyyy",
|
||||
new System.Globalization.CultureInfo("de-DE"));
|
||||
CurrentSchoolYear = _schoolYearService.CurrentSchoolYear();
|
||||
Greeting = now.Hour < 12 ? "Guten Morgen" :
|
||||
now.Hour < 18 ? "Guten Tag" : "Guten Abend";
|
||||
|
||||
// Heutige Stunden
|
||||
TodaysLessons.Clear();
|
||||
var schoolYear = _schoolYearService.CurrentSchoolYear();
|
||||
var allGroups = _groups.GetBySchoolYear(schoolYear)
|
||||
.ToDictionary(g => g.Id);
|
||||
|
||||
var todayLessons = allGroups.Keys
|
||||
.SelectMany(gid => _lessons.GetByGroupAndDate(gid, today))
|
||||
.OrderBy(l => l.LessonNumber)
|
||||
.ToList();
|
||||
|
||||
foreach (var lesson in todayLessons)
|
||||
{
|
||||
if (!allGroups.TryGetValue(lesson.GroupId, out var group)) continue;
|
||||
TodaysLessons.Add(new LessonItem
|
||||
{
|
||||
GroupName = group.Name,
|
||||
Topic = lesson.Topic,
|
||||
Status = lesson.Status,
|
||||
});
|
||||
}
|
||||
|
||||
// Offene Aufgaben (mit Fälligkeit)
|
||||
OpenTasks.Clear();
|
||||
var openTasks = _tasks.GetByStatus(WorkTaskStatus.Open)
|
||||
.Concat(_tasks.GetByStatus(WorkTaskStatus.InProgress))
|
||||
.OrderBy(t => t.DueDate ?? DateOnly.MaxValue)
|
||||
.Take(5);
|
||||
|
||||
foreach (var task in openTasks)
|
||||
{
|
||||
var isOverdue = task.DueDate.HasValue && task.DueDate < today;
|
||||
var isDueSoon = task.DueDate.HasValue &&
|
||||
task.DueDate >= today &&
|
||||
task.DueDate <= today.AddDays(3);
|
||||
|
||||
OpenTasks.Add(new TaskItem
|
||||
{
|
||||
Title = task.Title,
|
||||
DueDate = task.DueDate?.ToString("dd.MM.") ?? "",
|
||||
IsOverdue = isOverdue,
|
||||
IsDueSoon = isDueSoon,
|
||||
Status = task.Status,
|
||||
});
|
||||
}
|
||||
|
||||
// Aktuelle Lerngruppen als Chips
|
||||
CurrentGroups.Clear();
|
||||
foreach (var group in _groups.GetBySchoolYear(schoolYear)
|
||||
.OrderBy(g => g.Name))
|
||||
{
|
||||
CurrentGroups.Add(new GroupChip
|
||||
{
|
||||
GroupId = group.Id,
|
||||
Name = group.Name,
|
||||
Subject = group.Subject ?? "",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Refresh() => Load();
|
||||
}
|
||||
|
||||
// Anzeigemodelle für Dashboard-Widgets
|
||||
public class LessonItem
|
||||
{
|
||||
public string GroupName { get; set; } = "";
|
||||
public string Topic { get; set; } = "";
|
||||
public LessonStatus Status { get; set; }
|
||||
}
|
||||
|
||||
public class TaskItem
|
||||
{
|
||||
public string Title { get; set; } = "";
|
||||
public string DueDate { get; set; } = "";
|
||||
public bool IsOverdue { get; set; }
|
||||
public bool IsDueSoon { get; set; }
|
||||
public WorkTaskStatus Status { get; set; }
|
||||
}
|
||||
|
||||
public class GroupChip
|
||||
{
|
||||
public Guid GroupId { get; set; }
|
||||
public string Name { get; set; } = "";
|
||||
public string Subject { get; set; } = "";
|
||||
}
|
||||
158
LehrerApp.Desktop/ViewModels/DevicePairingViewModel.cs
Normal file
158
LehrerApp.Desktop/ViewModels/DevicePairingViewModel.cs
Normal file
@@ -0,0 +1,158 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using LehrerApp.Data;
|
||||
using LehrerApp.Sync;
|
||||
using LehrerApp.Sync.Models;
|
||||
|
||||
namespace LehrerApp.Desktop.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// Steuert den Dialog zum Einrichten eines neuen Geräts.
|
||||
///
|
||||
/// Zwei Modi:
|
||||
/// Sender → "Neues Gerät hinzufügen" – erzeugt Snapshot und zeigt Code
|
||||
/// Empfänger → "Mit bestehendem Account verbinden" – Code eingeben, Restore
|
||||
/// </summary>
|
||||
public partial class DevicePairingViewModel : ObservableObject
|
||||
{
|
||||
private readonly SnapshotService _snapshotService;
|
||||
|
||||
// ── Gemeinsamer State ─────────────────────────────────────────────────────
|
||||
|
||||
[ObservableProperty] private PairingMode _mode = PairingMode.SelectMode;
|
||||
[ObservableProperty] private bool _isBusy;
|
||||
[ObservableProperty] private string _statusMessage = "";
|
||||
[ObservableProperty] private int _progressPercent;
|
||||
[ObservableProperty] private bool _isError;
|
||||
[ObservableProperty] private bool _isComplete;
|
||||
|
||||
// ── Sender-State ─────────────────────────────────────────────────────────
|
||||
|
||||
[ObservableProperty] private string _generatedCode = "";
|
||||
[ObservableProperty] private DateTime _codeExpiresAt;
|
||||
[ObservableProperty] private string _codeExpiresText = "";
|
||||
|
||||
// ── Empfänger-State ───────────────────────────────────────────────────────
|
||||
|
||||
[ObservableProperty] private string _inputCode = "";
|
||||
|
||||
public DevicePairingViewModel(SnapshotService snapshotService)
|
||||
{
|
||||
_snapshotService = snapshotService;
|
||||
_snapshotService.ProgressChanged += OnProgress;
|
||||
}
|
||||
|
||||
// ── Navigation zwischen Modi ──────────────────────────────────────────────
|
||||
|
||||
[RelayCommand]
|
||||
private void SelectSender() => Mode = PairingMode.Sender;
|
||||
|
||||
[RelayCommand]
|
||||
private void SelectReceiver() => Mode = PairingMode.Receiver;
|
||||
|
||||
[RelayCommand]
|
||||
private void Back()
|
||||
{
|
||||
Mode = PairingMode.SelectMode;
|
||||
Reset();
|
||||
}
|
||||
|
||||
// ── Sender: Snapshot erstellen ────────────────────────────────────────────
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanCreateSnapshot))]
|
||||
private async Task CreateSnapshotAsync()
|
||||
{
|
||||
IsBusy = true;
|
||||
IsError = false;
|
||||
IsComplete = false;
|
||||
|
||||
try
|
||||
{
|
||||
var result = await _snapshotService.CreateAndUploadAsync();
|
||||
|
||||
GeneratedCode = result.Code;
|
||||
CodeExpiresAt = result.ExpiresAt;
|
||||
CodeExpiresText = $"Gültig bis {result.ExpiresAt:HH:mm} Uhr " +
|
||||
$"({result.ExpiresAt:dd.MM.yyyy})";
|
||||
Mode = PairingMode.SenderShowCode;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
IsError = true;
|
||||
StatusMessage = $"Fehler: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanCreateSnapshot() => !IsBusy;
|
||||
|
||||
// ── Empfänger: Snapshot wiederherstellen ──────────────────────────────────
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanRestore))]
|
||||
private async Task RestoreAsync(string targetDbPath)
|
||||
{
|
||||
IsBusy = true;
|
||||
IsError = false;
|
||||
IsComplete = false;
|
||||
|
||||
try
|
||||
{
|
||||
// Schlüssel wird aus dem Code extrahiert und lokal gespeichert
|
||||
// → App-Neustart lädt ihn automatisch
|
||||
await _snapshotService.RestoreFromCodeAsync(
|
||||
InputCode.Trim(), targetDbPath);
|
||||
|
||||
IsComplete = true;
|
||||
StatusMessage = "Erfolgreich! Bitte die App neu starten.";
|
||||
Mode = PairingMode.ReceiverDone;
|
||||
}
|
||||
catch (SnapshotNotFoundException ex)
|
||||
{
|
||||
IsError = true;
|
||||
StatusMessage = ex.Message;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
IsError = true;
|
||||
StatusMessage = $"Fehler beim Wiederherstellen: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanRestore() =>
|
||||
!IsBusy && InputCode.Trim().Length >= 5;
|
||||
|
||||
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
|
||||
|
||||
private void OnProgress(SnapshotProgress progress)
|
||||
{
|
||||
StatusMessage = progress.Message;
|
||||
ProgressPercent = progress.PercentComplete;
|
||||
}
|
||||
|
||||
private void Reset()
|
||||
{
|
||||
StatusMessage = "";
|
||||
ProgressPercent = 0;
|
||||
IsError = false;
|
||||
IsComplete = false;
|
||||
GeneratedCode = "";
|
||||
InputCode = "";
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
public enum PairingMode
|
||||
{
|
||||
SelectMode, // Startbildschirm: Sender oder Empfänger wählen
|
||||
Sender, // Sender bereit, Snapshot erstellen
|
||||
SenderShowCode, // Code wird angezeigt
|
||||
Receiver, // Code-Eingabe
|
||||
ReceiverDone, // Erfolgreich wiederhergestellt
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using LehrerApp.Core.Interfaces;
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Core.Services;
|
||||
|
||||
namespace LehrerApp.Desktop.ViewModels.Groups;
|
||||
|
||||
public partial class AddGroupDialogViewModel : ObservableObject
|
||||
{
|
||||
private readonly IGroupRepository _groups;
|
||||
private readonly SchoolYearService _schoolYearService;
|
||||
|
||||
[ObservableProperty] private string _name = "";
|
||||
[ObservableProperty] private string _subject = "";
|
||||
[ObservableProperty] private GroupType _type = GroupType.Course;
|
||||
[ObservableProperty] private int _gradeLevel = 10;
|
||||
[ObservableProperty] private GradingSystem _gradingSystem = GradingSystem.Grades1To6;
|
||||
[ObservableProperty] private int? _hoursPerWeek;
|
||||
[ObservableProperty] private string _selectedSchoolYear = "";
|
||||
[ObservableProperty] private string _validationMessage = "";
|
||||
|
||||
public List<string> SchoolYears { get; }
|
||||
public List<GroupTypeItem> GroupTypes { get; } =
|
||||
[
|
||||
new(GroupType.Course, "Fachkurs"),
|
||||
new(GroupType.Class, "Klassenverband"),
|
||||
];
|
||||
public List<GradingSystemItem> GradingSystems { get; } =
|
||||
[
|
||||
new(GradingSystem.Grades1To6, "Noten 1–6 (Sek I)"),
|
||||
new(GradingSystem.Points0To15, "Punkte 0–15 (Oberstufe)"),
|
||||
];
|
||||
|
||||
// Wird vom Dialog aufgerufen wenn OK geklickt wurde
|
||||
public LearningGroup? Result { get; private set; }
|
||||
|
||||
public AddGroupDialogViewModel(
|
||||
IGroupRepository groups,
|
||||
SchoolYearService schoolYearService)
|
||||
{
|
||||
_groups = groups;
|
||||
_schoolYearService = schoolYearService;
|
||||
SchoolYears = schoolYearService.RecentSchoolYears(3);
|
||||
SelectedSchoolYear = schoolYearService.CurrentSchoolYear();
|
||||
|
||||
// Automatisch Notensystem vorschlagen wenn Klassenstufe sich ändert
|
||||
PropertyChanged += (_, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(GradeLevel))
|
||||
GradingSystem = GradeLevel >= 11
|
||||
? GradingSystem.Points0To15
|
||||
: GradingSystem.Grades1To6;
|
||||
};
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private bool Save()
|
||||
{
|
||||
// Validierung
|
||||
if (string.IsNullOrWhiteSpace(Name))
|
||||
{
|
||||
ValidationMessage = "Bitte einen Namen eingeben.";
|
||||
return false;
|
||||
}
|
||||
if (GradeLevel is < 1 or > 13)
|
||||
{
|
||||
ValidationMessage = "Klassenstufe muss zwischen 1 und 13 liegen.";
|
||||
return false;
|
||||
}
|
||||
|
||||
Result = new LearningGroup
|
||||
{
|
||||
Name = Name.Trim(),
|
||||
Subject = string.IsNullOrWhiteSpace(Subject) ? null : Subject.Trim(),
|
||||
Type = Type,
|
||||
GradeLevel = GradeLevel,
|
||||
GradingSystem = GradingSystem,
|
||||
SchoolYear = SelectedSchoolYear,
|
||||
HoursPerWeek = HoursPerWeek,
|
||||
};
|
||||
|
||||
_groups.Save(Result);
|
||||
ValidationMessage = "";
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public record GroupTypeItem(GroupType Value, string Label);
|
||||
public record GradingSystemItem(GradingSystem Value, string Label);
|
||||
208
LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs
Normal file
208
LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs
Normal file
@@ -0,0 +1,208 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using LehrerApp.Core.Interfaces;
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Core.Services;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace LehrerApp.Desktop.ViewModels.Groups;
|
||||
|
||||
// ── Gruppenliste ──────────────────────────────────────────────────────────────
|
||||
|
||||
public partial class GroupListViewModel : ObservableObject
|
||||
{
|
||||
private readonly IGroupRepository _groups;
|
||||
private readonly SchoolYearService _schoolYearService;
|
||||
|
||||
[ObservableProperty] private string _selectedSchoolYear = "";
|
||||
[ObservableProperty] private string _searchText = "";
|
||||
[ObservableProperty] private GroupListItem? _selectedGroup;
|
||||
|
||||
public ObservableCollection<string> SchoolYears { get; } = [];
|
||||
public ObservableCollection<GroupListItem> Groups { get; } = [];
|
||||
|
||||
public GroupListViewModel(
|
||||
IGroupRepository groups,
|
||||
SchoolYearService schoolYearService)
|
||||
{
|
||||
_groups = groups;
|
||||
_schoolYearService = schoolYearService;
|
||||
|
||||
foreach (var y in schoolYearService.RecentSchoolYears())
|
||||
SchoolYears.Add(y);
|
||||
|
||||
SelectedSchoolYear = schoolYearService.CurrentSchoolYear();
|
||||
LoadGroups();
|
||||
}
|
||||
|
||||
partial void OnSelectedSchoolYearChanged(string value) => LoadGroups();
|
||||
partial void OnSearchTextChanged(string value) => LoadGroups();
|
||||
|
||||
private void LoadGroups()
|
||||
{
|
||||
Groups.Clear();
|
||||
var all = _groups.GetBySchoolYear(SelectedSchoolYear);
|
||||
|
||||
var filtered = string.IsNullOrWhiteSpace(SearchText)
|
||||
? all
|
||||
: all.Where(g =>
|
||||
g.Name.Contains(SearchText, StringComparison.OrdinalIgnoreCase) ||
|
||||
(g.Subject?.Contains(SearchText, StringComparison.OrdinalIgnoreCase) ?? false));
|
||||
|
||||
foreach (var g in filtered)
|
||||
Groups.Add(new GroupListItem(g));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void AddGroup()
|
||||
{
|
||||
// TODO: Dialog öffnen
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Refresh() => LoadGroups();
|
||||
}
|
||||
|
||||
public class GroupListItem
|
||||
{
|
||||
public Guid Id { get; }
|
||||
public string Name { get; }
|
||||
public string Subject { get; }
|
||||
public string SchoolYear { get; }
|
||||
public int GradeLevel { get; }
|
||||
public string TypeLabel { get; }
|
||||
public string GradingLabel { get; }
|
||||
|
||||
public GroupListItem(LearningGroup g)
|
||||
{
|
||||
Id = g.Id;
|
||||
Name = g.Name;
|
||||
Subject = g.Subject ?? "";
|
||||
SchoolYear = g.SchoolYear;
|
||||
GradeLevel = g.GradeLevel;
|
||||
TypeLabel = g.Type == GroupType.Class ? "Klasse" : "Kurs";
|
||||
GradingLabel = g.GradingSystem == GradingSystem.Grades1To6
|
||||
? "Noten 1–6"
|
||||
: "Punkte 0–15";
|
||||
}
|
||||
}
|
||||
|
||||
// ── Gruppendetail (Tab-Navigation) ────────────────────────────────────────────
|
||||
|
||||
public partial class GroupDetailViewModel : ObservableObject
|
||||
{
|
||||
private readonly IGroupRepository _groups;
|
||||
private readonly IStudentRepository _students;
|
||||
private readonly IEnrollmentRepository _enrollments;
|
||||
private readonly IExamRepository _exams;
|
||||
private readonly SchoolYearService _schoolYearService;
|
||||
|
||||
[ObservableProperty] private LearningGroup? _group;
|
||||
[ObservableProperty] private string _groupTitle = "";
|
||||
[ObservableProperty] private int _studentCount;
|
||||
[ObservableProperty] private GroupTab _activeTab = GroupTab.Overview;
|
||||
|
||||
// Sub-ViewModels für Tabs – lazy geladen
|
||||
public ObservableCollection<StudentSummary> Students { get; } = [];
|
||||
public ObservableCollection<ExamSummary> Exams { get; } = [];
|
||||
|
||||
public GroupDetailViewModel(
|
||||
IGroupRepository groups,
|
||||
IStudentRepository students,
|
||||
IEnrollmentRepository enrollments,
|
||||
IExamRepository exams,
|
||||
SchoolYearService schoolYearService)
|
||||
{
|
||||
_groups = groups;
|
||||
_students = students;
|
||||
_enrollments = enrollments;
|
||||
_exams = exams;
|
||||
_schoolYearService = schoolYearService;
|
||||
}
|
||||
|
||||
public void LoadGroup(Guid groupId)
|
||||
{
|
||||
Group = _groups.GetById(groupId);
|
||||
if (Group is null) return;
|
||||
|
||||
GroupTitle = $"{Group.Name} · {Group.SchoolYear}";
|
||||
LoadStudents();
|
||||
LoadExams();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void SwitchTab(GroupTab tab)
|
||||
{
|
||||
ActiveTab = tab;
|
||||
}
|
||||
|
||||
private void LoadStudents()
|
||||
{
|
||||
if (Group is null) return;
|
||||
Students.Clear();
|
||||
|
||||
var enrolled = _students.GetByGroup(Group.Id, Group.SchoolYear);
|
||||
StudentCount = enrolled.Count;
|
||||
|
||||
foreach (var s in enrolled)
|
||||
Students.Add(new StudentSummary(s));
|
||||
}
|
||||
|
||||
private void LoadExams()
|
||||
{
|
||||
if (Group is null) return;
|
||||
Exams.Clear();
|
||||
|
||||
foreach (var e in _exams.GetByGroup(Group.Id))
|
||||
Exams.Add(new ExamSummary(e));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void AddStudent()
|
||||
{
|
||||
// TODO: Schüler-Auswahl-Dialog
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void AddExam()
|
||||
{
|
||||
// TODO: Klausur-Erstellen-Dialog
|
||||
}
|
||||
}
|
||||
|
||||
public enum GroupTab { Overview, Students, Exams, Grades, Planner, Documentation }
|
||||
|
||||
public class StudentSummary
|
||||
{
|
||||
public Guid Id { get; }
|
||||
public string FullName { get; }
|
||||
|
||||
public StudentSummary(Core.Models.Student s)
|
||||
{
|
||||
Id = s.Id;
|
||||
FullName = s.FullName;
|
||||
}
|
||||
}
|
||||
|
||||
public class ExamSummary
|
||||
{
|
||||
public Guid Id { get; }
|
||||
public string Title { get; }
|
||||
public string Date { get; }
|
||||
public string Status { get; }
|
||||
|
||||
public ExamSummary(Core.Models.Exam e)
|
||||
{
|
||||
Id = e.Id;
|
||||
Title = e.Title;
|
||||
Date = e.Date.ToString("dd.MM.yyyy");
|
||||
Status = e.Status switch
|
||||
{
|
||||
ExamStatus.Planned => "Geplant",
|
||||
ExamStatus.Conducted => "Durchgeführt",
|
||||
ExamStatus.Graded => "Korrigiert",
|
||||
ExamStatus.Returned => "Zurückgegeben",
|
||||
_ => "",
|
||||
};
|
||||
}
|
||||
}
|
||||
87
LehrerApp.Desktop/ViewModels/MainWindowViewModel.cs
Normal file
87
LehrerApp.Desktop/ViewModels/MainWindowViewModel.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using LehrerApp.Core.Services;
|
||||
using LehrerApp.Desktop.ViewModels.Groups;
|
||||
using LehrerApp.Desktop.ViewModels.Students;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace LehrerApp.Desktop.ViewModels;
|
||||
|
||||
public partial class MainWindowViewModel : ObservableObject
|
||||
{
|
||||
private readonly IServiceProvider _services;
|
||||
|
||||
[ObservableProperty]
|
||||
private ObservableObject? _currentPage;
|
||||
|
||||
[ObservableProperty]
|
||||
private NavItem _activeNavItem = NavItem.Dashboard;
|
||||
|
||||
// ── Fehlende Property die im MainWindow.axaml referenziert wird ───────────
|
||||
[ObservableProperty]
|
||||
private string _currentSchoolYear = "";
|
||||
|
||||
public MainWindowViewModel(
|
||||
IServiceProvider services,
|
||||
DashboardViewModel dashboard,
|
||||
SchoolYearService schoolYearService)
|
||||
{
|
||||
_services = services;
|
||||
CurrentPage = dashboard;
|
||||
CurrentSchoolYear = schoolYearService.CurrentSchoolYear();
|
||||
}
|
||||
|
||||
// ── Navigation ────────────────────────────────────────────────────────────
|
||||
|
||||
[RelayCommand]
|
||||
private void NavigateTo(NavItem item)
|
||||
{
|
||||
ActiveNavItem = item;
|
||||
CurrentPage = item switch
|
||||
{
|
||||
NavItem.Dashboard => _services.GetRequiredService<DashboardViewModel>(),
|
||||
NavItem.Groups => _services.GetRequiredService<GroupListViewModel>(),
|
||||
NavItem.Students => _services.GetRequiredService<StudentListViewModel>(),
|
||||
NavItem.Exams => CreatePlaceholder("Klausuren"),
|
||||
NavItem.Planner => CreatePlaceholder("Unterrichtsplanung"),
|
||||
NavItem.Workload => CreatePlaceholder("Arbeitszeit"),
|
||||
NavItem.Settings => CreatePlaceholder("Einstellungen"),
|
||||
_ => CurrentPage,
|
||||
};
|
||||
}
|
||||
|
||||
public void NavigateToGroup(Guid groupId)
|
||||
{
|
||||
ActiveNavItem = NavItem.Groups;
|
||||
var vm = _services.GetRequiredService<GroupDetailViewModel>();
|
||||
vm.LoadGroup(groupId);
|
||||
CurrentPage = vm;
|
||||
}
|
||||
|
||||
public void NavigateToStudent(Guid studentId)
|
||||
{
|
||||
ActiveNavItem = NavItem.Students;
|
||||
var vm = _services.GetRequiredService<StudentDetailViewModel>();
|
||||
vm.LoadStudent(studentId);
|
||||
CurrentPage = vm;
|
||||
}
|
||||
|
||||
private static PlaceholderViewModel CreatePlaceholder(string title) =>
|
||||
new() { Title = title };
|
||||
}
|
||||
|
||||
public enum NavItem
|
||||
{
|
||||
Dashboard,
|
||||
Groups,
|
||||
Students,
|
||||
Exams,
|
||||
Planner,
|
||||
Workload,
|
||||
Settings,
|
||||
}
|
||||
|
||||
public partial class PlaceholderViewModel : ObservableObject
|
||||
{
|
||||
[ObservableProperty] private string _title = "";
|
||||
}
|
||||
223
LehrerApp.Desktop/ViewModels/Students/StudentViewModels.cs
Normal file
223
LehrerApp.Desktop/ViewModels/Students/StudentViewModels.cs
Normal file
@@ -0,0 +1,223 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using LehrerApp.Core.Interfaces;
|
||||
using LehrerApp.Core.Models;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace LehrerApp.Desktop.ViewModels.Students;
|
||||
|
||||
// ── Schülerliste ──────────────────────────────────────────────────────────────
|
||||
|
||||
public partial class StudentListViewModel : ObservableObject
|
||||
{
|
||||
private readonly IStudentRepository _students;
|
||||
|
||||
[ObservableProperty] private string _searchText = "";
|
||||
[ObservableProperty] private bool _showInactive;
|
||||
[ObservableProperty] private StudentListItem? _selectedStudent;
|
||||
|
||||
public ObservableCollection<StudentListItem> Students { get; } = [];
|
||||
|
||||
public StudentListViewModel(IStudentRepository students)
|
||||
{
|
||||
_students = students;
|
||||
LoadStudents();
|
||||
}
|
||||
|
||||
partial void OnSearchTextChanged(string value) => LoadStudents();
|
||||
partial void OnShowInactiveChanged(bool value) => LoadStudents();
|
||||
|
||||
private void LoadStudents()
|
||||
{
|
||||
Students.Clear();
|
||||
var all = _students.GetAll(includeInactive: ShowInactive);
|
||||
|
||||
var filtered = string.IsNullOrWhiteSpace(SearchText)
|
||||
? all
|
||||
: all.Where(s =>
|
||||
s.LastName.Contains(SearchText, StringComparison.OrdinalIgnoreCase) ||
|
||||
s.FirstName.Contains(SearchText, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
foreach (var s in filtered)
|
||||
Students.Add(new StudentListItem(s));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void AddStudent()
|
||||
{
|
||||
// TODO: Neuer-Schüler-Dialog
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Refresh() => LoadStudents();
|
||||
}
|
||||
|
||||
public class StudentListItem
|
||||
{
|
||||
public Guid Id { get; }
|
||||
public string FullName { get; }
|
||||
public string DateOfBirth { get; }
|
||||
public bool IsActive { get; }
|
||||
|
||||
public StudentListItem(Student s)
|
||||
{
|
||||
Id = s.Id;
|
||||
FullName = s.FullName;
|
||||
DateOfBirth = s.DateOfBirth?.ToString("dd.MM.yyyy") ?? "";
|
||||
IsActive = s.IsActive;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Schülerdetail ─────────────────────────────────────────────────────────────
|
||||
|
||||
public partial class StudentDetailViewModel : ObservableObject
|
||||
{
|
||||
private readonly IStudentRepository _students;
|
||||
private readonly IEnrollmentRepository _enrollments;
|
||||
private readonly IGroupRepository _groups;
|
||||
private readonly IGradeRepository _grades;
|
||||
private readonly IDocumentationRepository _docs;
|
||||
|
||||
[ObservableProperty] private Student? _student;
|
||||
[ObservableProperty] private string _studentTitle = "";
|
||||
[ObservableProperty] private StudentTab _activeTab = StudentTab.Overview;
|
||||
[ObservableProperty] private bool _isEditing;
|
||||
|
||||
// Bearbeitbare Felder
|
||||
[ObservableProperty] private string _editFirstName = "";
|
||||
[ObservableProperty] private string _editLastName = "";
|
||||
[ObservableProperty] private string _editNotes = "";
|
||||
|
||||
public ObservableCollection<EnrollmentEntry> Enrollments { get; } = [];
|
||||
public ObservableCollection<GradeEntry> Grades { get; } = [];
|
||||
public ObservableCollection<DocEntry> Documentation { get; } = [];
|
||||
|
||||
public StudentDetailViewModel(
|
||||
IStudentRepository students,
|
||||
IEnrollmentRepository enrollments,
|
||||
IGroupRepository groups,
|
||||
IGradeRepository grades,
|
||||
IDocumentationRepository docs)
|
||||
{
|
||||
_students = students;
|
||||
_enrollments = enrollments;
|
||||
_groups = groups;
|
||||
_grades = grades;
|
||||
_docs = docs;
|
||||
}
|
||||
|
||||
public void LoadStudent(Guid studentId)
|
||||
{
|
||||
Student = _students.GetById(studentId);
|
||||
if (Student is null) return;
|
||||
|
||||
StudentTitle = Student.FullName;
|
||||
EditFirstName = Student.FirstName;
|
||||
EditLastName = Student.LastName;
|
||||
EditNotes = Student.Notes ?? "";
|
||||
|
||||
LoadEnrollments();
|
||||
LoadDocs();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void SwitchTab(StudentTab tab) => ActiveTab = tab;
|
||||
|
||||
[RelayCommand]
|
||||
private void StartEdit() => IsEditing = true;
|
||||
|
||||
[RelayCommand]
|
||||
private void CancelEdit()
|
||||
{
|
||||
if (Student is null) return;
|
||||
EditFirstName = Student.FirstName;
|
||||
EditLastName = Student.LastName;
|
||||
EditNotes = Student.Notes ?? "";
|
||||
IsEditing = false;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void SaveEdit()
|
||||
{
|
||||
if (Student is null) return;
|
||||
Student.FirstName = EditFirstName;
|
||||
Student.LastName = EditLastName;
|
||||
Student.Notes = string.IsNullOrWhiteSpace(EditNotes) ? null : EditNotes;
|
||||
_students.Save(Student);
|
||||
StudentTitle = Student.FullName;
|
||||
IsEditing = false;
|
||||
}
|
||||
|
||||
private void LoadEnrollments()
|
||||
{
|
||||
if (Student is null) return;
|
||||
Enrollments.Clear();
|
||||
|
||||
var enrollments = _enrollments.GetByStudent(Student.Id);
|
||||
var groupIds = enrollments.Select(e => e.GroupId).Distinct();
|
||||
var groupMap = groupIds
|
||||
.Select(id => _groups.GetById(id))
|
||||
.Where(g => g is not null)
|
||||
.ToDictionary(g => g!.Id);
|
||||
|
||||
foreach (var e in enrollments.OrderByDescending(e => e.SchoolYear))
|
||||
{
|
||||
if (!groupMap.TryGetValue(e.GroupId, out var group)) continue;
|
||||
Enrollments.Add(new EnrollmentEntry
|
||||
{
|
||||
SchoolYear = e.SchoolYear,
|
||||
GroupName = group.Name,
|
||||
Subject = group.Subject ?? "",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadDocs()
|
||||
{
|
||||
if (Student is null) return;
|
||||
Documentation.Clear();
|
||||
|
||||
foreach (var doc in _docs.GetByStudent(Student.Id))
|
||||
{
|
||||
Documentation.Add(new DocEntry
|
||||
{
|
||||
Date = doc.Date.ToString("dd.MM.yyyy"),
|
||||
Title = doc.Title,
|
||||
TypeLabel = doc.Type switch
|
||||
{
|
||||
DocumentationType.Conversation => "Gespräch",
|
||||
DocumentationType.Incident => "Vorkommnis",
|
||||
DocumentationType.SupportPlan => "Förderplan",
|
||||
DocumentationType.Absence => "Fehlzeit",
|
||||
_ => "",
|
||||
},
|
||||
IsConfidential = doc.IsConfidential,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum StudentTab { Overview, Grades, Documentation }
|
||||
|
||||
public class EnrollmentEntry
|
||||
{
|
||||
public string SchoolYear { get; set; } = "";
|
||||
public string GroupName { get; set; } = "";
|
||||
public string Subject { get; set; } = "";
|
||||
}
|
||||
|
||||
public class GradeEntry
|
||||
{
|
||||
public string Date { get; set; } = "";
|
||||
public string Value { get; set; } = "";
|
||||
public string Category { get; set; } = "";
|
||||
public string GroupName { get; set; } = "";
|
||||
}
|
||||
|
||||
public class DocEntry
|
||||
{
|
||||
public string Date { get; set; } = "";
|
||||
public string Title { get; set; } = "";
|
||||
public string TypeLabel { get; set; } = "";
|
||||
public bool IsConfidential { get; set; }
|
||||
}
|
||||
63
LehrerApp.Desktop/ViewModels/SyncStatusViewModel.cs
Normal file
63
LehrerApp.Desktop/ViewModels/SyncStatusViewModel.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using LehrerApp.Sync;
|
||||
using LehrerApp.Sync.Models;
|
||||
|
||||
namespace LehrerApp.Desktop.ViewModels;
|
||||
|
||||
public partial class SyncStatusViewModel : ObservableObject
|
||||
{
|
||||
private readonly SyncEngine? _engine;
|
||||
|
||||
[ObservableProperty] private string _statusText = "Kein Server konfiguriert";
|
||||
[ObservableProperty] private string _lastSyncText = "";
|
||||
[ObservableProperty] private int _pendingCount;
|
||||
[ObservableProperty] private int _conflictCount;
|
||||
[ObservableProperty] private bool _isSyncing;
|
||||
[ObservableProperty] private bool _hasConflicts;
|
||||
[ObservableProperty] private bool _isServerConfigured;
|
||||
|
||||
public SyncStatusViewModel(SyncEngine? engine)
|
||||
{
|
||||
_engine = engine;
|
||||
IsServerConfigured = engine is not null;
|
||||
|
||||
if (_engine is not null)
|
||||
{
|
||||
_engine.StatusChanged += OnStatusChanged;
|
||||
OnStatusChanged(_engine.Status);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnStatusChanged(SyncStatus status)
|
||||
{
|
||||
IsSyncing = status.State == SyncState.Syncing;
|
||||
HasConflicts = status.ConflictCount > 0;
|
||||
PendingCount = status.PendingEvents;
|
||||
ConflictCount = status.ConflictCount;
|
||||
|
||||
StatusText = status.State switch
|
||||
{
|
||||
SyncState.Idle => PendingCount > 0
|
||||
? $"{PendingCount} ausstehend"
|
||||
: "Synchronisiert",
|
||||
SyncState.Syncing => "Synchronisiere...",
|
||||
SyncState.Offline => "Offline",
|
||||
SyncState.Error => $"Fehler: {status.ErrorMessage}",
|
||||
_ => "",
|
||||
};
|
||||
|
||||
LastSyncText = status.LastSyncAt.HasValue
|
||||
? $"Zuletzt: {status.LastSyncAt:HH:mm}"
|
||||
: "Noch nie synchronisiert";
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanSyncNow))]
|
||||
private async Task SyncNow()
|
||||
{
|
||||
if (_engine is null) return;
|
||||
await _engine.SyncNowAsync(isAutomatic: false);
|
||||
}
|
||||
|
||||
private bool CanSyncNow() => _engine is not null && !IsSyncing;
|
||||
}
|
||||
Reference in New Issue
Block a user