init 1.0.0

This commit is contained in:
2026-06-19 00:42:00 +02:00
commit 5ca960746b
67 changed files with 3261 additions and 0 deletions
@@ -0,0 +1,72 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Models;
using LehrerApp.Core.Services;
using System.Collections.ObjectModel;
using System.Globalization;
namespace LehrerApp.Desktop.ViewModels;
public partial class DashboardViewModel : ObservableObject
{
private readonly IGroupRepository _groups;
private readonly ILessonRepository _lessons;
private readonly IWorkTaskRepository _tasks;
private readonly SchoolYearService _sy;
[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; } = [];
// Navigation-Callback wird von App.axaml.cs verdrahtet
public Action<Guid>? OnNavigateToGroup { get; set; }
public DashboardViewModel(IGroupRepository groups, ILessonRepository lessons,
IWorkTaskRepository tasks, SchoolYearService sy)
{
_groups = groups; _lessons = lessons; _tasks = tasks; _sy = sy;
Load();
}
private void Load()
{
var now = DateTime.Now;
var today = DateOnly.FromDateTime(now);
CurrentDate = now.ToString("dddd, d. MMMM yyyy", new CultureInfo("de-DE"));
CurrentSchoolYear = _sy.CurrentSchoolYear();
Greeting = now.Hour < 12 ? "Guten Morgen" : now.Hour < 18 ? "Guten Tag" : "Guten Abend";
TodaysLessons.Clear();
var groups = _groups.GetBySchoolYear(_sy.CurrentSchoolYear()).ToDictionary(g => g.Id);
foreach (var l in groups.Keys.SelectMany(gid => _lessons.GetByGroupAndDate(gid, today))
.OrderBy(l => l.LessonNumber))
{
if (groups.TryGetValue(l.GroupId, out var g))
TodaysLessons.Add(new() { GroupName = g.Name, Topic = l.Topic });
}
OpenTasks.Clear();
foreach (var t in _tasks.GetByStatus(WorkTaskStatus.Open)
.Concat(_tasks.GetByStatus(WorkTaskStatus.InProgress))
.OrderBy(t => t.DueDate ?? DateOnly.MaxValue).Take(5))
OpenTasks.Add(new() { Title = t.Title,
DueDate = t.DueDate?.ToString("dd.MM.") ?? "",
IsOverdue = t.DueDate.HasValue && t.DueDate < today });
CurrentGroups.Clear();
foreach (var g in groups.Values.OrderBy(g => g.Name))
CurrentGroups.Add(new() { GroupId = g.Id, Name = g.Name, Subject = g.Subject ?? "" });
}
[RelayCommand] private void OpenGroup(GroupChip? c) { if (c is not null) OnNavigateToGroup?.Invoke(c.GroupId); }
[RelayCommand] private void Refresh() => Load();
}
public class LessonItem { public string GroupName { get; set; } = ""; public string Topic { get; set; } = ""; }
public class TaskItem { public string Title { get; set; } = ""; public string DueDate { get; set; } = ""; public bool IsOverdue { get; set; } }
public class GroupChip { public Guid GroupId { get; set; } public string Name { get; set; } = ""; public string Subject { get; set; } = ""; }
@@ -0,0 +1,196 @@
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 _sy;
public Action<Guid>? OnNavigateToDetail { get; set; }
public Action? OnAddGroup { get; set; }
[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 sy)
{
_groups = groups; _sy = sy;
foreach (var y in sy.RecentSchoolYears()) SchoolYears.Add(y);
SelectedSchoolYear = sy.CurrentSchoolYear();
}
partial void OnSelectedSchoolYearChanged(string value) => LoadGroups();
partial void OnSearchTextChanged(string value) => LoadGroups();
partial void OnSelectedGroupChanged(GroupListItem? value)
{
if (value is not null) OnNavigateToDetail?.Invoke(value.Id);
}
public 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.OrderBy(g => g.Name))
Groups.Add(new GroupListItem(g));
}
[RelayCommand] private void AddGroup() => OnAddGroup?.Invoke();
[RelayCommand] private void Refresh() => LoadGroups();
}
public class GroupListItem
{
public Guid Id { get; }
public string Name { get; }
public string Subject { get; }
public string DisplayName { get; }
public string TypeLabel { get; }
public string GradingLabel { get; }
public GroupListItem(LearningGroup g)
{
Id = g.Id;
Name = g.Name;
Subject = g.Subject ?? "";
TypeLabel = g.Type == GroupType.Class ? "Klasse" : "Kurs";
GradingLabel = g.GradingSystem == GradingSystem.Grades1To6 ? "16" : "015";
DisplayName = string.IsNullOrEmpty(Subject) ? Name : $"{Name} · {Subject}";
}
}
// ── Gruppendetail ─────────────────────────────────────────────────────────────
public partial class GroupDetailViewModel : ObservableObject
{
private readonly IGroupRepository _groups;
private readonly IStudentRepository _students;
private readonly IExamRepository _exams;
private readonly IGradeRepository _grades;
[ObservableProperty] private LearningGroup? _group;
[ObservableProperty] private string _groupTitle = "";
[ObservableProperty] private string _groupSubtitle = "";
[ObservableProperty] private int _studentCount;
public ObservableCollection<StudentSummary> Students { get; } = [];
public ObservableCollection<ExamSummary> Exams { get; } = [];
public GroupDetailViewModel(IGroupRepository groups, IStudentRepository students,
IExamRepository exams, IGradeRepository grades)
{
_groups = groups; _students = students; _exams = exams; _grades = grades;
}
public void LoadGroup(Guid id)
{
Group = _groups.GetById(id);
if (Group is null) return;
GroupTitle = Group.Name;
GroupSubtitle = $"{Group.SchoolYear} · " +
$"{(Group.Type == GroupType.Class ? "Klasse" : "Kurs")} · " +
$"Stufe {Group.GradeLevel} · Noten {(Group.GradingSystem == GradingSystem.Grades1To6 ? "16" : "015")}";
Students.Clear();
var enrolled = _students.GetByGroup(Group.Id, Group.SchoolYear);
StudentCount = enrolled.Count;
foreach (var s in enrolled) Students.Add(new StudentSummary(s));
Exams.Clear();
foreach (var e in _exams.GetByGroup(Group.Id)) Exams.Add(new ExamSummary(e));
}
[RelayCommand] private void AddStudent() { /* TODO */ }
[RelayCommand] private void AddExam() { /* TODO */ }
[RelayCommand] private void Refresh() { if (Group is not null) LoadGroup(Group.Id); }
}
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 StatusLabel { get; }
public ExamSummary(Core.Models.Exam e)
{
Id = e.Id; Title = e.Title; Date = e.Date.ToString("dd.MM.yyyy");
StatusLabel = e.Status switch
{
ExamStatus.Planned => "Geplant",
ExamStatus.Conducted => "Durchgeführt",
ExamStatus.Graded => "Korrigiert",
ExamStatus.Returned => "Zurückgegeben",
_ => "",
};
}
}
// ── Dialog: Neue Lerngruppe anlegen ──────────────────────────────────────────
public partial class AddGroupDialogViewModel : ObservableObject
{
private readonly IGroupRepository _groups;
private readonly SchoolYearService _sy;
[ObservableProperty] private string _name = "";
[ObservableProperty] private string _subject = "";
[ObservableProperty] private int _gradeLevel = 10;
[ObservableProperty] private GradingSystem _gradingSystem = GradingSystem.Grades1To6;
[ObservableProperty] private string _selectedSchoolYear = "";
[ObservableProperty] private string _validationMessage = "";
public List<string> SchoolYears { get; }
public LearningGroup? Result { get; private set; }
public AddGroupDialogViewModel(IGroupRepository groups, SchoolYearService sy)
{
_groups = groups; _sy = sy;
SchoolYears = sy.RecentSchoolYears(3);
SelectedSchoolYear = sy.CurrentSchoolYear();
// Notensystem automatisch nach Klassenstufe
PropertyChanged += (_, e) =>
{
if (e.PropertyName == nameof(GradeLevel))
GradingSystem = GradeLevel >= 11 ? GradingSystem.Points0To15
: GradingSystem.Grades1To6;
};
}
[RelayCommand]
private void Save()
{
if (string.IsNullOrWhiteSpace(Name)) { ValidationMessage = "Name erforderlich."; return; }
if (GradeLevel is < 1 or > 13) { ValidationMessage = "Klassenstufe 113."; return; }
Result = new LearningGroup
{
Name = Name.Trim(),
Subject = string.IsNullOrWhiteSpace(Subject) ? null : Subject.Trim(),
Type = GroupType.Course,
GradeLevel = GradeLevel,
GradingSystem = GradingSystem,
SchoolYear = SelectedSchoolYear,
};
_groups.Save(Result);
}
}
@@ -0,0 +1,66 @@
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;
[ObservableProperty] private string _currentSchoolYear = "";
public MainWindowViewModel(IServiceProvider services,
DashboardViewModel dashboard, SchoolYearService sy)
{
_services = services;
CurrentSchoolYear = sy.CurrentSchoolYear();
CurrentPage = dashboard;
}
[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 => new PlaceholderViewModel { Title = "Klausuren", Icon = "📝" },
NavItem.Planner => new PlaceholderViewModel { Title = "Unterrichtsplanung", Icon = "📅" },
NavItem.Workload => new PlaceholderViewModel { Title = "Arbeitszeit", Icon = "⏱" },
NavItem.Settings => new PlaceholderViewModel { Title = "Einstellungen", Icon = "⚙️" },
_ => CurrentPage,
};
}
public void NavigateToGroupDetail(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;
}
}
public enum NavItem { Dashboard, Groups, Students, Exams, Planner, Workload, Settings }
public partial class PlaceholderViewModel : ObservableObject
{
[ObservableProperty] private string _title = "";
[ObservableProperty] private string _icon = "";
}
@@ -0,0 +1,131 @@
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;
public partial class StudentListViewModel : ObservableObject
{
private readonly IStudentRepository _students;
public Action<Guid>? OnNavigateToDetail { get; set; }
[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();
partial void OnSelectedStudentChanged(StudentListItem? value)
{
if (value is not null) OnNavigateToDetail?.Invoke(value.Id);
}
public void LoadStudents()
{
Students.Clear();
var all = _students.GetAll(ShowInactive);
var f = string.IsNullOrWhiteSpace(SearchText) ? all
: all.Where(s => s.LastName.Contains(SearchText, StringComparison.OrdinalIgnoreCase)
|| s.FirstName.Contains(SearchText, StringComparison.OrdinalIgnoreCase));
foreach (var s in f) Students.Add(new StudentListItem(s));
}
[RelayCommand] private void AddStudent() { /* TODO */ }
[RelayCommand] private void Refresh() => LoadStudents();
}
public class StudentListItem
{
public Guid Id { get; }
public string FullName { get; }
public string DateOfBirth { get; }
public StudentListItem(Student s)
{
Id = s.Id; FullName = s.FullName;
DateOfBirth = s.DateOfBirth?.ToString("dd.MM.yyyy") ?? "";
}
}
public partial class StudentDetailViewModel : ObservableObject
{
private readonly IStudentRepository _students;
private readonly IEnrollmentRepository _enrollments;
private readonly IGroupRepository _groups;
private readonly IDocumentationRepository _docs;
[ObservableProperty] private Student? _student;
[ObservableProperty] private string _studentTitle = "";
[ObservableProperty] private bool _isEditing;
[ObservableProperty] private string _editFirstName = "";
[ObservableProperty] private string _editLastName = "";
public ObservableCollection<EnrollmentEntry> Enrollments { get; } = [];
public ObservableCollection<DocEntry> Documentation { get; } = [];
public StudentDetailViewModel(IStudentRepository students,
IEnrollmentRepository enrollments, IGroupRepository groups,
IDocumentationRepository docs)
{
_students = students; _enrollments = enrollments;
_groups = groups; _docs = docs;
}
public void LoadStudent(Guid id)
{
Student = _students.GetById(id);
if (Student is null) return;
StudentTitle = Student.FullName;
EditFirstName = Student.FirstName;
EditLastName = Student.LastName;
Enrollments.Clear();
foreach (var e in _enrollments.GetByStudent(Student.Id))
{
var g = _groups.GetById(e.GroupId);
if (g is null) continue;
Enrollments.Add(new() { SchoolYear = e.SchoolYear, GroupName = g.Name, Subject = g.Subject ?? "" });
}
Documentation.Clear();
foreach (var d in _docs.GetByStudent(Student.Id))
Documentation.Add(new() { Date = d.Date.ToString("dd.MM.yyyy"), Title = d.Title,
TypeLabel = d.Type switch
{
DocumentationType.Conversation => "Gespräch",
DocumentationType.Incident => "Vorkommnis",
DocumentationType.SupportPlan => "Förderplan",
DocumentationType.Absence => "Fehlzeit",
_ => "",
},
IsConfidential = d.IsConfidential });
}
[RelayCommand] private void StartEdit() => IsEditing = true;
[RelayCommand] private void CancelEdit()
{
if (Student is null) return;
EditFirstName = Student.FirstName; EditLastName = Student.LastName;
IsEditing = false;
}
[RelayCommand] private void SaveEdit()
{
if (Student is null) return;
Student.FirstName = EditFirstName; Student.LastName = EditLastName;
_students.Save(Student);
StudentTitle = Student.FullName;
IsEditing = false;
}
}
public class EnrollmentEntry { public string SchoolYear { get; set; } = ""; public string GroupName { get; set; } = ""; public string Subject { 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; } }
@@ -0,0 +1,43 @@
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 bool _isSyncing;
[ObservableProperty] private bool _isServerConfigured;
[ObservableProperty] private int _pendingCount;
public SyncStatusViewModel(SyncEngine? engine)
{
_engine = engine;
IsServerConfigured = engine is not null;
if (_engine is not null) _engine.StatusChanged += OnStatus;
}
private void OnStatus(SyncStatus s)
{
IsSyncing = s.State == SyncState.Syncing;
PendingCount = s.PendingEvents;
StatusText = s.State switch
{
SyncState.Idle => PendingCount > 0 ? $"{PendingCount} ausstehend" : "Synchronisiert",
SyncState.Syncing => "Synchronisiere…",
SyncState.Offline => "Offline",
SyncState.Error => $"Fehler: {s.ErrorMessage}",
_ => "",
};
LastSyncText = s.LastSyncAt.HasValue ? $"Zuletzt: {s.LastSyncAt:HH:mm}" : "Noch nie";
}
[RelayCommand(CanExecute = nameof(CanSync))]
private async Task SyncNow() { if (_engine is not null) await _engine.SyncNowAsync(); }
private bool CanSync() => _engine is not null && !IsSyncing;
}