Mitarbeit bewerten begonnen, Schülerdaten, Gruppen
This commit is contained in:
@@ -85,3 +85,26 @@ public interface ITimeEntryRepository
|
|||||||
void Save(TimeEntry entry);
|
void Save(TimeEntry entry);
|
||||||
void Delete(Guid id);
|
void Delete(Guid id);
|
||||||
}
|
}
|
||||||
|
public interface IParticipationSessionRepository
|
||||||
|
{
|
||||||
|
List<ParticipationSession> GetByGroup(Guid groupId);
|
||||||
|
ParticipationSession? GetById(Guid id);
|
||||||
|
void Save(ParticipationSession session);
|
||||||
|
void Delete(Guid id);
|
||||||
|
}
|
||||||
|
public interface IParticipationRepository
|
||||||
|
{
|
||||||
|
List<ParticipationEntry> GetBySession(Guid sessionId);
|
||||||
|
List<ParticipationEntry> GetByStudent(Guid studentId);
|
||||||
|
ParticipationEntry? GetBySessionAndStudent(Guid sessionId, Guid studentId);
|
||||||
|
void Save(ParticipationEntry entry);
|
||||||
|
void SaveMany(List<ParticipationEntry> entries);
|
||||||
|
void DeleteBySession(Guid sessionId);
|
||||||
|
}
|
||||||
|
public interface IParticipationAspectRepository
|
||||||
|
{
|
||||||
|
List<ParticipationAspect> GetDefaults();
|
||||||
|
List<ParticipationAspect> GetByGroup(Guid groupId);
|
||||||
|
void Save(ParticipationAspect aspect);
|
||||||
|
void Delete(Guid id);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
namespace LehrerApp.Core.Models;
|
||||||
|
|
||||||
|
public class ParticipationSession
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
|
public Guid GroupId { get; set; }
|
||||||
|
public DateOnly Date { get; set; } = DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
public Guid? LessonId { get; set; }
|
||||||
|
public string? Comment { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ParticipationEntry
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
|
public Guid SessionId { get; set; }
|
||||||
|
public Guid GroupId { get; set; }
|
||||||
|
public Guid StudentId { get; set; }
|
||||||
|
public DateOnly Date { get; set; } = DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
public List<AspectRating> Ratings { get; set; } = [];
|
||||||
|
public string? Note { get; set; }
|
||||||
|
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AspectRating
|
||||||
|
{
|
||||||
|
public string Key { get; set; } = "";
|
||||||
|
public int Value { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ParticipationAspect
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
|
public Guid? GroupId { get; set; }
|
||||||
|
public string Key { get; set; } = "";
|
||||||
|
public string Label { get; set; } = "";
|
||||||
|
public AspectValueType ValueType { get; set; } = AspectValueType.Scale5;
|
||||||
|
public bool IsActive { get; set; } = true;
|
||||||
|
public int SortOrder { get; set; }
|
||||||
|
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum AspectValueType { Scale5, Scale3, Binary, Points }
|
||||||
|
|
||||||
|
public static class DefaultParticipationAspects
|
||||||
|
{
|
||||||
|
public static readonly IReadOnlyList<ParticipationAspect> All =
|
||||||
|
[
|
||||||
|
new() { Key = "quality", Label = "Qualität", ValueType = AspectValueType.Scale5, SortOrder = 0 },
|
||||||
|
new() { Key = "quantity", Label = "Quantität", ValueType = AspectValueType.Scale5, SortOrder = 1 },
|
||||||
|
new() { Key = "workphase", Label = "Arbeitsphase", ValueType = AspectValueType.Scale5, SortOrder = 2 },
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -16,9 +16,27 @@ public class Student
|
|||||||
}
|
}
|
||||||
public class Contact
|
public class Contact
|
||||||
{
|
{
|
||||||
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
public string Name { get; set; } = "";
|
public string Name { get; set; } = "";
|
||||||
public string Relation { get; set; } = "";
|
public string Relation { get; set; } = "";
|
||||||
public string? Phone { get; set; }
|
public string? Phone { get; set; }
|
||||||
public string? Email { get; set; }
|
public string? Email { get; set; }
|
||||||
|
public string? Street { get; set; }
|
||||||
|
public string? PostalCode { get; set; }
|
||||||
|
public string? City { get; set; }
|
||||||
|
public DateOnly? InvalidSince { get; set; }
|
||||||
|
public ContactInvalidReason? InvalidReason { get; set; }
|
||||||
|
public string? InvalidReasonDetails { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum ContactInvalidReason
|
||||||
|
{
|
||||||
|
Moved,
|
||||||
|
NewPhoneNumber,
|
||||||
|
LostCustody,
|
||||||
|
NewEmailAddress,
|
||||||
|
NoLongerResponsible,
|
||||||
|
Other,
|
||||||
|
}
|
||||||
|
|
||||||
public enum Gender { M, W, D }
|
public enum Gender { M, W, D }
|
||||||
|
|||||||
@@ -28,8 +28,11 @@ public class LiteDbContext : IDisposable
|
|||||||
public ILiteCollection<Unit> Units => _db.GetCollection<Unit>("units");
|
public ILiteCollection<Unit> Units => _db.GetCollection<Unit>("units");
|
||||||
public ILiteCollection<Lesson> Lessons => _db.GetCollection<Lesson>("lessons");
|
public ILiteCollection<Lesson> Lessons => _db.GetCollection<Lesson>("lessons");
|
||||||
public ILiteCollection<Documentation> Documentation => _db.GetCollection<Documentation>("documentation");
|
public ILiteCollection<Documentation> Documentation => _db.GetCollection<Documentation>("documentation");
|
||||||
public ILiteCollection<WorkTask> Tasks => _db.GetCollection<WorkTask>("tasks");
|
public ILiteCollection<WorkTask> Tasks => _db.GetCollection<WorkTask>("tasks");
|
||||||
public ILiteCollection<TimeEntry> TimeEntries => _db.GetCollection<TimeEntry>("time_entries");
|
public ILiteCollection<TimeEntry> TimeEntries => _db.GetCollection<TimeEntry>("time_entries");
|
||||||
|
public ILiteCollection<ParticipationSession> ParticipationSessions => _db.GetCollection<ParticipationSession>("participation_sessions");
|
||||||
|
public ILiteCollection<ParticipationEntry> ParticipationEntries => _db.GetCollection<ParticipationEntry>("participation");
|
||||||
|
public ILiteCollection<ParticipationAspect> ParticipationAspects => _db.GetCollection<ParticipationAspect>("participation_aspects");
|
||||||
|
|
||||||
public void Checkpoint() => _db.Checkpoint();
|
public void Checkpoint() => _db.Checkpoint();
|
||||||
|
|
||||||
@@ -54,6 +57,11 @@ public class LiteDbContext : IDisposable
|
|||||||
Documentation.EnsureIndex(x => x.StudentId);
|
Documentation.EnsureIndex(x => x.StudentId);
|
||||||
Tasks.EnsureIndex(x => x.Status);
|
Tasks.EnsureIndex(x => x.Status);
|
||||||
TimeEntries.EnsureIndex(x => x.Date);
|
TimeEntries.EnsureIndex(x => x.Date);
|
||||||
|
ParticipationSessions.EnsureIndex(x => x.GroupId);
|
||||||
|
ParticipationSessions.EnsureIndex(x => x.Date);
|
||||||
|
ParticipationEntries.EnsureIndex(x => x.SessionId);
|
||||||
|
ParticipationEntries.EnsureIndex(x => x.StudentId);
|
||||||
|
ParticipationAspects.EnsureIndex(x => x.GroupId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose() => _db.Dispose();
|
public void Dispose() => _db.Dispose();
|
||||||
|
|||||||
@@ -133,3 +133,49 @@ public class TimeEntryRepository(LiteDbContext db) : ITimeEntryRepository
|
|||||||
public void Save(TimeEntry e) => db.TimeEntries.Upsert(e);
|
public void Save(TimeEntry e) => db.TimeEntries.Upsert(e);
|
||||||
public void Delete(Guid id) => db.TimeEntries.Delete(id);
|
public void Delete(Guid id) => db.TimeEntries.Delete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class ParticipationSessionRepository(LiteDbContext db) : IParticipationSessionRepository
|
||||||
|
{
|
||||||
|
public List<ParticipationSession> GetByGroup(Guid groupId) =>
|
||||||
|
db.ParticipationSessions.Find(s => s.GroupId == groupId).OrderByDescending(s => s.Date).ToList();
|
||||||
|
public ParticipationSession? GetById(Guid id) => db.ParticipationSessions.FindById(id);
|
||||||
|
public void Save(ParticipationSession s) { s.UpdatedAt = DateTime.UtcNow; db.ParticipationSessions.Upsert(s); }
|
||||||
|
public void Delete(Guid id)
|
||||||
|
{
|
||||||
|
db.ParticipationSessions.Delete(id);
|
||||||
|
foreach (var e in db.ParticipationEntries.Find(e => e.SessionId == id).ToList())
|
||||||
|
db.ParticipationEntries.Delete(e.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ParticipationRepository(LiteDbContext db) : IParticipationRepository
|
||||||
|
{
|
||||||
|
public List<ParticipationEntry> GetBySession(Guid sessionId) =>
|
||||||
|
db.ParticipationEntries.Find(e => e.SessionId == sessionId).ToList();
|
||||||
|
public List<ParticipationEntry> GetByStudent(Guid studentId) =>
|
||||||
|
db.ParticipationEntries.Find(e => e.StudentId == studentId).ToList();
|
||||||
|
public ParticipationEntry? GetBySessionAndStudent(Guid sessionId, Guid studentId) =>
|
||||||
|
db.ParticipationEntries.FindOne(e => e.SessionId == sessionId && e.StudentId == studentId);
|
||||||
|
public void Save(ParticipationEntry e) { e.UpdatedAt = DateTime.UtcNow; db.ParticipationEntries.Upsert(e); }
|
||||||
|
public void SaveMany(List<ParticipationEntry> entries)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
foreach (var e in entries) e.UpdatedAt = now;
|
||||||
|
db.ParticipationEntries.Upsert(entries);
|
||||||
|
}
|
||||||
|
public void DeleteBySession(Guid sessionId)
|
||||||
|
{
|
||||||
|
foreach (var e in db.ParticipationEntries.Find(e => e.SessionId == sessionId).ToList())
|
||||||
|
db.ParticipationEntries.Delete(e.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ParticipationAspectRepository(LiteDbContext db) : IParticipationAspectRepository
|
||||||
|
{
|
||||||
|
public List<ParticipationAspect> GetDefaults() =>
|
||||||
|
db.ParticipationAspects.Find(a => a.GroupId == null && a.IsActive).OrderBy(a => a.SortOrder).ToList();
|
||||||
|
public List<ParticipationAspect> GetByGroup(Guid groupId) =>
|
||||||
|
db.ParticipationAspects.Find(a => a.GroupId == groupId && a.IsActive).OrderBy(a => a.SortOrder).ToList();
|
||||||
|
public void Save(ParticipationAspect a) { a.UpdatedAt = DateTime.UtcNow; db.ParticipationAspects.Upsert(a); }
|
||||||
|
public void Delete(Guid id) => db.ParticipationAspects.Delete(id);
|
||||||
|
}
|
||||||
|
|||||||
@@ -40,9 +40,19 @@ public class App : Application
|
|||||||
var dash = Services.GetRequiredService<DashboardViewModel>();
|
var dash = Services.GetRequiredService<DashboardViewModel>();
|
||||||
dash.OnNavigateToGroup = id => main.NavigateToGroupDetail(id);
|
dash.OnNavigateToGroup = id => main.NavigateToGroupDetail(id);
|
||||||
|
|
||||||
// StudentList → StudentDetail
|
// StudentList → StudentDetail + Anlegen
|
||||||
var sl = Services.GetRequiredService<StudentListViewModel>();
|
var sl = Services.GetRequiredService<StudentListViewModel>();
|
||||||
sl.OnNavigateToDetail = id => main.NavigateToStudent(id);
|
sl.OnNavigateToDetail = id => main.NavigateToStudent(id);
|
||||||
|
sl.OnAddStudent = () => ShowAddStudentDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task ShowAddStudentDialog()
|
||||||
|
{
|
||||||
|
var vm = new ViewModels.Students.AddStudentDialogViewModel(
|
||||||
|
Services.GetRequiredService<Core.Interfaces.IStudentRepository>());
|
||||||
|
var dialog = new Views.Students.AddStudentDialog { DataContext = vm };
|
||||||
|
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner })
|
||||||
|
await dialog.ShowDialog<bool>(owner);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async void ShowAddGroupDialog(GroupListViewModel groupList)
|
private static async void ShowAddGroupDialog(GroupListViewModel groupList)
|
||||||
|
|||||||
@@ -50,8 +50,11 @@ public static class AppBootstrapper
|
|||||||
services.AddSingleton<IUnitRepository, UnitRepository>();
|
services.AddSingleton<IUnitRepository, UnitRepository>();
|
||||||
services.AddSingleton<ILessonRepository, LessonRepository>();
|
services.AddSingleton<ILessonRepository, LessonRepository>();
|
||||||
services.AddSingleton<IDocumentationRepository, DocumentationRepository>();
|
services.AddSingleton<IDocumentationRepository, DocumentationRepository>();
|
||||||
services.AddSingleton<IWorkTaskRepository, WorkTaskRepository>();
|
services.AddSingleton<IWorkTaskRepository, WorkTaskRepository>();
|
||||||
services.AddSingleton<ITimeEntryRepository, TimeEntryRepository>();
|
services.AddSingleton<ITimeEntryRepository, TimeEntryRepository>();
|
||||||
|
services.AddSingleton<IParticipationSessionRepository, ParticipationSessionRepository>();
|
||||||
|
services.AddSingleton<IParticipationRepository, ParticipationRepository>();
|
||||||
|
services.AddSingleton<IParticipationAspectRepository, ParticipationAspectRepository>();
|
||||||
|
|
||||||
// ── Services ──────────────────────────────────────────────────────────
|
// ── Services ──────────────────────────────────────────────────────────
|
||||||
services.AddSingleton<GradingService>();
|
services.AddSingleton<GradingService>();
|
||||||
@@ -95,13 +98,15 @@ public static class AppBootstrapper
|
|||||||
// Singleton: einmal erstellt, überall dieselbe Instanz
|
// Singleton: einmal erstellt, überall dieselbe Instanz
|
||||||
services.AddSingleton<MainWindowViewModel>();
|
services.AddSingleton<MainWindowViewModel>();
|
||||||
services.AddSingleton<DashboardViewModel>();
|
services.AddSingleton<DashboardViewModel>();
|
||||||
services.AddSingleton<SyncStatusViewModel>();
|
services.AddSingleton(sp =>
|
||||||
|
new SyncStatusViewModel(sp.GetService<SyncEngine>()));
|
||||||
services.AddSingleton<GroupListViewModel>();
|
services.AddSingleton<GroupListViewModel>();
|
||||||
services.AddSingleton<StudentListViewModel>();
|
services.AddSingleton<StudentListViewModel>();
|
||||||
|
|
||||||
// Transient: neue Instanz pro Navigation (für Detailseiten)
|
// Transient: neue Instanz pro Navigation (für Detailseiten)
|
||||||
services.AddTransient<GroupDetailViewModel>();
|
services.AddTransient<GroupDetailViewModel>();
|
||||||
services.AddTransient<StudentDetailViewModel>();
|
services.AddTransient<StudentDetailViewModel>();
|
||||||
|
services.AddTransient<ParticipationTabViewModel>();
|
||||||
|
|
||||||
return services.BuildServiceProvider();
|
return services.BuildServiceProvider();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ public partial class GroupListViewModel : ObservableObject
|
|||||||
[ObservableProperty] private string _searchText = "";
|
[ObservableProperty] private string _searchText = "";
|
||||||
[ObservableProperty] private GroupListItem? _selectedGroup;
|
[ObservableProperty] private GroupListItem? _selectedGroup;
|
||||||
|
|
||||||
|
public string SelectedGroupDisplayName => SelectedGroup?.DisplayName ?? "";
|
||||||
|
public string SelectedGroupSubtitle => SelectedGroup?.Subtitle ?? "";
|
||||||
|
|
||||||
public ObservableCollection<string> SchoolYears { get; } = [];
|
public ObservableCollection<string> SchoolYears { get; } = [];
|
||||||
public ObservableCollection<GroupListItem> Groups { get; } = [];
|
public ObservableCollection<GroupListItem> Groups { get; } = [];
|
||||||
|
|
||||||
@@ -33,8 +36,12 @@ public partial class GroupListViewModel : ObservableObject
|
|||||||
|
|
||||||
partial void OnSelectedSchoolYearChanged(string value) => LoadGroups();
|
partial void OnSelectedSchoolYearChanged(string value) => LoadGroups();
|
||||||
partial void OnSearchTextChanged(string value) => LoadGroups();
|
partial void OnSearchTextChanged(string value) => LoadGroups();
|
||||||
partial void OnSelectedGroupChanged(GroupListItem? value) =>
|
partial void OnSelectedGroupChanged(GroupListItem? value)
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(SelectedGroupDisplayName));
|
||||||
|
OnPropertyChanged(nameof(SelectedGroupSubtitle));
|
||||||
NavigateToSectionCommand.NotifyCanExecuteChanged();
|
NavigateToSectionCommand.NotifyCanExecuteChanged();
|
||||||
|
}
|
||||||
|
|
||||||
public void LoadGroups()
|
public void LoadGroups()
|
||||||
{
|
{
|
||||||
@@ -88,6 +95,7 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
private readonly IGroupRepository _groups;
|
private readonly IGroupRepository _groups;
|
||||||
private readonly IStudentRepository _students;
|
private readonly IStudentRepository _students;
|
||||||
|
private readonly IEnrollmentRepository _enrollments;
|
||||||
private readonly IExamRepository _exams;
|
private readonly IExamRepository _exams;
|
||||||
private readonly IGradeRepository _grades;
|
private readonly IGradeRepository _grades;
|
||||||
|
|
||||||
@@ -96,14 +104,21 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
[ObservableProperty] private string _groupSubtitle = "";
|
[ObservableProperty] private string _groupSubtitle = "";
|
||||||
[ObservableProperty] private int _studentCount;
|
[ObservableProperty] private int _studentCount;
|
||||||
[ObservableProperty] private int _activeTabIndex = 0;
|
[ObservableProperty] private int _activeTabIndex = 0;
|
||||||
|
[ObservableProperty] private StudentSummary? _selectedStudent;
|
||||||
|
|
||||||
public ObservableCollection<StudentSummary> Students { get; } = [];
|
public ObservableCollection<StudentSummary> Students { get; } = [];
|
||||||
public ObservableCollection<ExamSummary> Exams { get; } = [];
|
public ObservableCollection<ExamSummary> Exams { get; } = [];
|
||||||
|
|
||||||
|
public ParticipationTabViewModel ParticipationTab { get; }
|
||||||
|
public Func<Task<bool>>? OnAddStudent { get; set; }
|
||||||
|
|
||||||
public GroupDetailViewModel(IGroupRepository groups, IStudentRepository students,
|
public GroupDetailViewModel(IGroupRepository groups, IStudentRepository students,
|
||||||
IExamRepository exams, IGradeRepository grades)
|
IEnrollmentRepository enrollments, IExamRepository exams, IGradeRepository grades,
|
||||||
|
ParticipationTabViewModel participationTab)
|
||||||
{
|
{
|
||||||
_groups = groups; _students = students; _exams = exams; _grades = grades;
|
_groups = groups; _students = students; _enrollments = enrollments;
|
||||||
|
_exams = exams; _grades = grades;
|
||||||
|
ParticipationTab = participationTab;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void LoadGroup(Guid id)
|
public void LoadGroup(Guid id)
|
||||||
@@ -114,19 +129,48 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
GroupSubtitle = $"{Group.SchoolYear} · " +
|
GroupSubtitle = $"{Group.SchoolYear} · " +
|
||||||
$"{(Group.Type == GroupType.Class ? "Klasse" : "Kurs")} · " +
|
$"{(Group.Type == GroupType.Class ? "Klasse" : "Kurs")} · " +
|
||||||
$"Stufe {Group.GradeLevel} · Noten {(Group.GradingSystem == GradingSystem.Grades1To6 ? "1–6" : "0–15")}";
|
$"Stufe {Group.GradeLevel} · Noten {(Group.GradingSystem == GradingSystem.Grades1To6 ? "1–6" : "0–15")}";
|
||||||
|
LoadStudents();
|
||||||
|
Exams.Clear();
|
||||||
|
foreach (var e in _exams.GetByGroup(Group.Id)) Exams.Add(new ExamSummary(e));
|
||||||
|
ParticipationTab.Initialize(Group.Id, Group.SchoolYear);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LoadStudents()
|
||||||
|
{
|
||||||
|
if (Group is null) return;
|
||||||
Students.Clear();
|
Students.Clear();
|
||||||
var enrolled = _students.GetByGroup(Group.Id, Group.SchoolYear);
|
var enrolled = _students.GetByGroup(Group.Id, Group.SchoolYear);
|
||||||
StudentCount = enrolled.Count;
|
StudentCount = enrolled.Count;
|
||||||
foreach (var s in enrolled) Students.Add(new StudentSummary(s));
|
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]
|
||||||
[RelayCommand] private void AddExam() { /* TODO */ }
|
private async Task AddStudent()
|
||||||
[RelayCommand] private void Refresh() { if (Group is not null) LoadGroup(Group.Id); }
|
{
|
||||||
|
if (OnAddStudent is null) return;
|
||||||
|
var confirmed = await OnAddStudent();
|
||||||
|
if (confirmed) LoadStudents();
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand(CanExecute = nameof(HasSelectedStudent))]
|
||||||
|
private void RemoveStudent()
|
||||||
|
{
|
||||||
|
if (Group is null || SelectedStudent is null) return;
|
||||||
|
var enrollment = _enrollments.GetByGroupAndYear(Group.Id, Group.SchoolYear)
|
||||||
|
.FirstOrDefault(e => e.StudentId == SelectedStudent.Id);
|
||||||
|
if (enrollment is null) return;
|
||||||
|
_enrollments.Delete(enrollment.Id);
|
||||||
|
LoadStudents();
|
||||||
|
SelectedStudent = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnSelectedStudentChanged(StudentSummary? value) =>
|
||||||
|
RemoveStudentCommand.NotifyCanExecuteChanged();
|
||||||
|
|
||||||
|
private bool HasSelectedStudent() => SelectedStudent is not null;
|
||||||
|
|
||||||
|
[RelayCommand] private void AddExam() { /* TODO */ }
|
||||||
|
[RelayCommand] private void Refresh() { if (Group is not null) LoadGroup(Group.Id); }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class StudentSummary
|
public class StudentSummary
|
||||||
@@ -156,6 +200,66 @@ public class ExamSummary
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Dialog: Schüler zur Gruppe hinzufügen ─────────────────────────────────────
|
||||||
|
|
||||||
|
public partial class AddStudentToGroupDialogViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
private readonly IStudentRepository _students;
|
||||||
|
private readonly IEnrollmentRepository _enrollments;
|
||||||
|
private readonly Guid _groupId;
|
||||||
|
private readonly string _schoolYear;
|
||||||
|
|
||||||
|
[ObservableProperty] private string _searchText = "";
|
||||||
|
[ObservableProperty] private StudentPickerItem? _selectedStudent;
|
||||||
|
[ObservableProperty] private string _validationMessage = "";
|
||||||
|
|
||||||
|
public ObservableCollection<StudentPickerItem> AvailableStudents { get; } = [];
|
||||||
|
public Enrollment? Result { get; private set; }
|
||||||
|
|
||||||
|
public AddStudentToGroupDialogViewModel(IStudentRepository students,
|
||||||
|
IEnrollmentRepository enrollments, Guid groupId, string schoolYear)
|
||||||
|
{
|
||||||
|
_students = students; _enrollments = enrollments;
|
||||||
|
_groupId = groupId; _schoolYear = schoolYear;
|
||||||
|
LoadAvailableStudents();
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnSearchTextChanged(string value) => LoadAvailableStudents();
|
||||||
|
|
||||||
|
private void LoadAvailableStudents()
|
||||||
|
{
|
||||||
|
var alreadyEnrolled = _enrollments.GetByGroupAndYear(_groupId, _schoolYear)
|
||||||
|
.Select(e => e.StudentId).ToHashSet();
|
||||||
|
var all = _students.GetAll();
|
||||||
|
var available = all
|
||||||
|
.Where(s => !alreadyEnrolled.Contains(s.Id))
|
||||||
|
.Where(s => string.IsNullOrWhiteSpace(SearchText) ||
|
||||||
|
s.FullName.Contains(SearchText, StringComparison.OrdinalIgnoreCase));
|
||||||
|
AvailableStudents.Clear();
|
||||||
|
foreach (var s in available) AvailableStudents.Add(new StudentPickerItem(s));
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void Save()
|
||||||
|
{
|
||||||
|
if (SelectedStudent is null) { ValidationMessage = "Bitte einen Schüler auswählen."; return; }
|
||||||
|
Result = new Enrollment
|
||||||
|
{
|
||||||
|
StudentId = SelectedStudent.Id,
|
||||||
|
GroupId = _groupId,
|
||||||
|
SchoolYear = _schoolYear,
|
||||||
|
};
|
||||||
|
_enrollments.Save(Result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class StudentPickerItem
|
||||||
|
{
|
||||||
|
public Guid Id { get; }
|
||||||
|
public string FullName { get; }
|
||||||
|
public StudentPickerItem(Student s) { Id = s.Id; FullName = s.FullName; }
|
||||||
|
}
|
||||||
|
|
||||||
// ── Dialog: Neue Lerngruppe anlegen ──────────────────────────────────────────
|
// ── Dialog: Neue Lerngruppe anlegen ──────────────────────────────────────────
|
||||||
|
|
||||||
public partial class AddGroupDialogViewModel : ObservableObject
|
public partial class AddGroupDialogViewModel : ObservableObject
|
||||||
|
|||||||
@@ -0,0 +1,463 @@
|
|||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using LehrerApp.Core.Interfaces;
|
||||||
|
using LehrerApp.Core.Models;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.ViewModels.Groups;
|
||||||
|
|
||||||
|
// ── Tab-ViewModel ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public partial class ParticipationTabViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
private readonly IParticipationSessionRepository _sessions;
|
||||||
|
private readonly IParticipationRepository _entries;
|
||||||
|
private readonly IParticipationAspectRepository _aspects;
|
||||||
|
private readonly IStudentRepository _students;
|
||||||
|
|
||||||
|
private Guid _groupId;
|
||||||
|
private string _schoolYear = "";
|
||||||
|
|
||||||
|
[ObservableProperty] private ParticipationSessionItem? _selectedSession;
|
||||||
|
[ObservableProperty] private string _noDataText = "Keine Bewertungssitzung ausgewählt.";
|
||||||
|
|
||||||
|
public string SelectedSessionDisplay => SelectedSession?.Display ?? "";
|
||||||
|
|
||||||
|
public ObservableCollection<ParticipationSessionItem> Sessions { get; } = [];
|
||||||
|
public ObservableCollection<ParticipationStudentRow> StudentRows { get; } = [];
|
||||||
|
public ObservableCollection<AspectColumnDef> Aspects { get; } = [];
|
||||||
|
|
||||||
|
public Func<Task<ParticipationSession?>>? OnAddSession { get; set; }
|
||||||
|
public Func<ParticipationTabViewModel, Task>? OnQuickInput { get; set; }
|
||||||
|
|
||||||
|
public ParticipationTabViewModel(
|
||||||
|
IParticipationSessionRepository sessions,
|
||||||
|
IParticipationRepository entries,
|
||||||
|
IParticipationAspectRepository aspects,
|
||||||
|
IStudentRepository students)
|
||||||
|
{
|
||||||
|
_sessions = sessions; _entries = entries;
|
||||||
|
_aspects = aspects; _students = students;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Initialize(Guid groupId, string schoolYear)
|
||||||
|
{
|
||||||
|
_groupId = groupId;
|
||||||
|
_schoolYear = schoolYear;
|
||||||
|
LoadAspects();
|
||||||
|
LoadSessions();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadAspects()
|
||||||
|
{
|
||||||
|
Aspects.Clear();
|
||||||
|
var defaults = _aspects.GetDefaults();
|
||||||
|
var specific = _aspects.GetByGroup(_groupId);
|
||||||
|
var all = defaults.Concat(specific).ToList();
|
||||||
|
|
||||||
|
if (!all.Any())
|
||||||
|
{
|
||||||
|
foreach (var a in DefaultParticipationAspects.All)
|
||||||
|
Aspects.Add(new AspectColumnDef(a));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var a in all) Aspects.Add(new AspectColumnDef(a));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LoadSessions()
|
||||||
|
{
|
||||||
|
Sessions.Clear();
|
||||||
|
foreach (var s in _sessions.GetByGroup(_groupId))
|
||||||
|
Sessions.Add(new ParticipationSessionItem(s));
|
||||||
|
if (SelectedSession is null && Sessions.Any())
|
||||||
|
SelectedSession = Sessions[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnSelectedSessionChanged(ParticipationSessionItem? value)
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(SelectedSessionDisplay));
|
||||||
|
if (value is null) { StudentRows.Clear(); QuickInputCommand.NotifyCanExecuteChanged(); return; }
|
||||||
|
LoadGrid(value.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadGrid(Guid sessionId)
|
||||||
|
{
|
||||||
|
StudentRows.Clear();
|
||||||
|
var students = _students.GetByGroup(_groupId, _schoolYear);
|
||||||
|
var entries = _entries.GetBySession(sessionId);
|
||||||
|
|
||||||
|
foreach (var s in students)
|
||||||
|
{
|
||||||
|
var entry = entries.FirstOrDefault(e => e.StudentId == s.Id)
|
||||||
|
?? new ParticipationEntry { SessionId = sessionId, GroupId = _groupId, StudentId = s.Id };
|
||||||
|
var row = new ParticipationStudentRow(s.Id, s.FullName, entry, Aspects.ToList());
|
||||||
|
row.OnRatingChanged = (studentId, key, val) => SaveRating(sessionId, studentId, key, val);
|
||||||
|
StudentRows.Add(row);
|
||||||
|
}
|
||||||
|
QuickInputCommand.NotifyCanExecuteChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveRating(Guid sessionId, Guid studentId, string key, int? value)
|
||||||
|
{
|
||||||
|
var session = _sessions.GetById(sessionId);
|
||||||
|
var entry = _entries.GetBySessionAndStudent(sessionId, studentId)
|
||||||
|
?? new ParticipationEntry
|
||||||
|
{
|
||||||
|
SessionId = sessionId,
|
||||||
|
GroupId = _groupId,
|
||||||
|
StudentId = studentId,
|
||||||
|
Date = session?.Date ?? DateOnly.FromDateTime(DateTime.Today),
|
||||||
|
};
|
||||||
|
|
||||||
|
var existing = entry.Ratings.FirstOrDefault(r => r.Key == key);
|
||||||
|
if (value is null)
|
||||||
|
{
|
||||||
|
if (existing is not null) entry.Ratings.Remove(existing);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (existing is null) entry.Ratings.Add(new AspectRating { Key = key, Value = value.Value });
|
||||||
|
else existing.Value = value.Value;
|
||||||
|
}
|
||||||
|
_entries.Save(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task AddSession()
|
||||||
|
{
|
||||||
|
if (OnAddSession is null) return;
|
||||||
|
var session = await OnAddSession();
|
||||||
|
if (session is null) return;
|
||||||
|
session.GroupId = _groupId;
|
||||||
|
_sessions.Save(session);
|
||||||
|
LoadSessions();
|
||||||
|
SelectedSession = Sessions.FirstOrDefault(s => s.Id == session.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand(CanExecute = nameof(CanQuickInput))]
|
||||||
|
private async Task QuickInput()
|
||||||
|
{
|
||||||
|
if (OnQuickInput is null) return;
|
||||||
|
await OnQuickInput(this);
|
||||||
|
if (SelectedSession is not null) LoadGrid(SelectedSession.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CanQuickInput() => SelectedSession is not null && StudentRows.Count > 0;
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void DeleteSession()
|
||||||
|
{
|
||||||
|
if (SelectedSession is null) return;
|
||||||
|
_sessions.Delete(SelectedSession.Id);
|
||||||
|
LoadSessions();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SaveNote(Guid sessionId, Guid studentId, string note)
|
||||||
|
{
|
||||||
|
var entry = _entries.GetBySessionAndStudent(sessionId, studentId);
|
||||||
|
if (entry is null) return;
|
||||||
|
entry.Note = note;
|
||||||
|
_entries.Save(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Zeilendaten für das Bewertungsraster ─────────────────────────────────────
|
||||||
|
|
||||||
|
public partial class ParticipationStudentRow : ObservableObject
|
||||||
|
{
|
||||||
|
public Guid StudentId { get; }
|
||||||
|
public string Name { get; }
|
||||||
|
|
||||||
|
private readonly ParticipationEntry _entry;
|
||||||
|
private readonly IReadOnlyList<AspectColumnDef> _aspectDefs;
|
||||||
|
|
||||||
|
public ObservableCollection<RatingCell> Cells { get; } = [];
|
||||||
|
public Action<Guid, string, int?>? OnRatingChanged { get; set; }
|
||||||
|
|
||||||
|
public ParticipationStudentRow(Guid id, string name, ParticipationEntry entry, List<AspectColumnDef> aspects)
|
||||||
|
{
|
||||||
|
StudentId = id;
|
||||||
|
Name = name;
|
||||||
|
_entry = entry;
|
||||||
|
_aspectDefs = aspects;
|
||||||
|
|
||||||
|
foreach (var a in aspects)
|
||||||
|
{
|
||||||
|
var existing = entry.Ratings.FirstOrDefault(r => r.Key == a.Key);
|
||||||
|
var cell = new RatingCell(id, a.Key, existing?.Value);
|
||||||
|
cell.OnChanged = (sid, key, val) => OnRatingChanged?.Invoke(sid, key, val);
|
||||||
|
Cells.Add(cell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int? GetRating(string key) =>
|
||||||
|
_entry.Ratings.FirstOrDefault(r => r.Key == key)?.Value;
|
||||||
|
|
||||||
|
public void SetRating(string key, int? value)
|
||||||
|
{
|
||||||
|
var cell = Cells.FirstOrDefault(c => c.AspectKey == key);
|
||||||
|
cell?.SetValue(value);
|
||||||
|
OnRatingChanged?.Invoke(StudentId, key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Eine Bewertungszelle ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public partial class RatingCell : ObservableObject
|
||||||
|
{
|
||||||
|
public Guid StudentId { get; }
|
||||||
|
public string AspectKey { get; }
|
||||||
|
|
||||||
|
[ObservableProperty] private int? _value;
|
||||||
|
[ObservableProperty] private string _displayLabel = "";
|
||||||
|
|
||||||
|
public Action<Guid, string, int?>? OnChanged { get; set; }
|
||||||
|
|
||||||
|
public RatingCell(Guid studentId, string key, int? value)
|
||||||
|
{
|
||||||
|
StudentId = studentId;
|
||||||
|
AspectKey = key;
|
||||||
|
_value = value;
|
||||||
|
UpdateLabel();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetValue(int? value)
|
||||||
|
{
|
||||||
|
Value = value;
|
||||||
|
UpdateLabel();
|
||||||
|
OnChanged?.Invoke(StudentId, AspectKey, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void CycleUp()
|
||||||
|
{
|
||||||
|
var next = Value is null ? -2 : Math.Min(2, Value.Value + 1);
|
||||||
|
SetValue(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void CycleDown()
|
||||||
|
{
|
||||||
|
var next = Value is null ? 2 : Math.Max(-2, Value.Value - 1);
|
||||||
|
SetValue(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void Clear() => SetValue(null);
|
||||||
|
|
||||||
|
private void UpdateLabel() => DisplayLabel = Value switch
|
||||||
|
{
|
||||||
|
2 => "++",
|
||||||
|
1 => "+",
|
||||||
|
0 => "~",
|
||||||
|
-1 => "−",
|
||||||
|
-2 => "−−",
|
||||||
|
_ => "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Hilfsklassen ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public class AspectColumnDef
|
||||||
|
{
|
||||||
|
public string Key { get; }
|
||||||
|
public string Label { get; }
|
||||||
|
public AspectColumnDef(ParticipationAspect a) { Key = a.Key; Label = a.Label; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ParticipationSessionItem
|
||||||
|
{
|
||||||
|
public Guid Id { get; }
|
||||||
|
public string Display { get; }
|
||||||
|
public string Comment { get; }
|
||||||
|
public DateOnly Date { get; }
|
||||||
|
|
||||||
|
public ParticipationSessionItem(ParticipationSession s)
|
||||||
|
{
|
||||||
|
Id = s.Id;
|
||||||
|
Date = s.Date;
|
||||||
|
Comment = s.Comment ?? "";
|
||||||
|
Display = s.Comment is { Length: > 0 }
|
||||||
|
? $"{s.Date:dd.MM.yyyy} – {s.Comment}"
|
||||||
|
: s.Date.ToString("dd.MM.yyyy");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Dialog: Sitzung anlegen ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public partial class AddSessionDialogViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
[ObservableProperty] private DateOnly _date = DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
[ObservableProperty] private string _comment = "";
|
||||||
|
[ObservableProperty] private string _dateText = DateOnly.FromDateTime(DateTime.Today).ToString("dd.MM.yyyy");
|
||||||
|
[ObservableProperty] private string _validationMessage = "";
|
||||||
|
|
||||||
|
public ParticipationSession? Result { get; private set; }
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void Save()
|
||||||
|
{
|
||||||
|
if (!DateOnly.TryParseExact(DateText, "dd.MM.yyyy", null,
|
||||||
|
System.Globalization.DateTimeStyles.None, out var date))
|
||||||
|
{
|
||||||
|
ValidationMessage = "Datum im Format TT.MM.JJJJ eingeben.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Result = new ParticipationSession { Date = date, Comment = Comment.Trim() };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Dialog: Schnelleingabe ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public partial class QuickInputViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
private readonly List<ParticipationStudentRow> _rows;
|
||||||
|
private readonly List<AspectColumnDef> _aspects;
|
||||||
|
|
||||||
|
[ObservableProperty] private int _studentIndex;
|
||||||
|
[ObservableProperty] private int _aspectIndex;
|
||||||
|
[ObservableProperty] private string _studentName = "";
|
||||||
|
[ObservableProperty] private string _currentAspectLabel = "";
|
||||||
|
[ObservableProperty] private string _currentValueLabel = "";
|
||||||
|
[ObservableProperty] private string _progressText = "";
|
||||||
|
|
||||||
|
public ObservableCollection<QuickAspectRow> AspectRows { get; } = [];
|
||||||
|
|
||||||
|
public QuickInputViewModel(List<ParticipationStudentRow> rows, List<AspectColumnDef> aspects)
|
||||||
|
{
|
||||||
|
_rows = rows;
|
||||||
|
_aspects = aspects;
|
||||||
|
if (rows.Any()) ShowStudent(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowStudent(int index)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= _rows.Count) return;
|
||||||
|
StudentIndex = index;
|
||||||
|
var row = _rows[index];
|
||||||
|
StudentName = row.Name;
|
||||||
|
ProgressText = $"{index + 1} / {_rows.Count}";
|
||||||
|
|
||||||
|
AspectRows.Clear();
|
||||||
|
foreach (var (a, i) in _aspects.Select((a, i) => (a, i)))
|
||||||
|
{
|
||||||
|
var val = row.GetRating(a.Key);
|
||||||
|
AspectRows.Add(new QuickAspectRow(i, a.Label, val, i == AspectIndex));
|
||||||
|
}
|
||||||
|
UpdateCurrentAspect();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateCurrentAspect()
|
||||||
|
{
|
||||||
|
if (!_aspects.Any()) return;
|
||||||
|
var safeIdx = Math.Clamp(AspectIndex, 0, _aspects.Count - 1);
|
||||||
|
CurrentAspectLabel = _aspects[safeIdx].Label;
|
||||||
|
var row = AspectRows.ElementAtOrDefault(safeIdx);
|
||||||
|
CurrentValueLabel = row?.DisplayLabel ?? "";
|
||||||
|
foreach (var r in AspectRows) r.IsActive = r.Index == safeIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetRatingByNumber(int num)
|
||||||
|
{
|
||||||
|
// 1=−−, 2=−, 3=~, 4=+, 5=++
|
||||||
|
var val = num switch { 1 => -2, 2 => -1, 3 => 0, 4 => 1, 5 => 2, _ => (int?)null };
|
||||||
|
if (val is null) return;
|
||||||
|
ApplyRating(val.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void IncrementRating()
|
||||||
|
{
|
||||||
|
var cell = GetCurrentCell();
|
||||||
|
if (cell is null) return;
|
||||||
|
var next = cell.Value is null ? -2 : Math.Min(2, cell.Value.Value + 1);
|
||||||
|
ApplyRating(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DecrementRating()
|
||||||
|
{
|
||||||
|
var cell = GetCurrentCell();
|
||||||
|
if (cell is null) return;
|
||||||
|
var next = cell.Value is null ? 2 : Math.Max(-2, cell.Value.Value - 1);
|
||||||
|
ApplyRating(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyRating(int val)
|
||||||
|
{
|
||||||
|
if (_rows.Count == 0 || !_aspects.Any()) return;
|
||||||
|
var key = _aspects[Math.Clamp(AspectIndex, 0, _aspects.Count - 1)].Key;
|
||||||
|
_rows[StudentIndex].SetRating(key, val);
|
||||||
|
var row = AspectRows.ElementAtOrDefault(AspectIndex);
|
||||||
|
if (row is not null)
|
||||||
|
{
|
||||||
|
row.Value = val;
|
||||||
|
row.UpdateLabel();
|
||||||
|
}
|
||||||
|
CurrentValueLabel = RatingLabel(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SelectAspect(int index)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= _aspects.Count) return;
|
||||||
|
AspectIndex = index;
|
||||||
|
UpdateCurrentAspect();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void NextAspect()
|
||||||
|
{
|
||||||
|
AspectIndex = (AspectIndex + 1) % _aspects.Count;
|
||||||
|
UpdateCurrentAspect();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void NextStudent()
|
||||||
|
{
|
||||||
|
if (StudentIndex >= _rows.Count - 1) return;
|
||||||
|
AspectIndex = 0;
|
||||||
|
ShowStudent(StudentIndex + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PreviousStudent()
|
||||||
|
{
|
||||||
|
if (StudentIndex <= 0) return;
|
||||||
|
AspectIndex = 0;
|
||||||
|
ShowStudent(StudentIndex - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsLastStudent => StudentIndex >= _rows.Count - 1;
|
||||||
|
|
||||||
|
private RatingCell? GetCurrentCell()
|
||||||
|
{
|
||||||
|
if (_rows.Count == 0 || !_aspects.Any()) return null;
|
||||||
|
var key = _aspects[Math.Clamp(AspectIndex, 0, _aspects.Count - 1)].Key;
|
||||||
|
return _rows[StudentIndex].Cells.FirstOrDefault(c => c.AspectKey == key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string RatingLabel(int? v) => v switch
|
||||||
|
{
|
||||||
|
2 => "++", 1 => "+", 0 => "~", -1 => "−", -2 => "−−", _ => "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial class QuickAspectRow : ObservableObject
|
||||||
|
{
|
||||||
|
public int Index { get; }
|
||||||
|
public string Label { get; }
|
||||||
|
[ObservableProperty] private bool _isActive;
|
||||||
|
[ObservableProperty] private string _displayLabel = "";
|
||||||
|
public int? Value { get; set; }
|
||||||
|
|
||||||
|
public QuickAspectRow(int index, string label, int? value, bool isActive)
|
||||||
|
{
|
||||||
|
Index = index;
|
||||||
|
Label = label;
|
||||||
|
Value = value;
|
||||||
|
IsActive = isActive;
|
||||||
|
UpdateLabel();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateLabel() => DisplayLabel = Value switch
|
||||||
|
{
|
||||||
|
2 => "++", 1 => "+", 0 => "~", -1 => "−", -2 => "−−", _ => "·",
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -15,10 +15,14 @@ public partial class MainWindowViewModel : ObservableObject
|
|||||||
[ObservableProperty] private NavItem _activeNavItem = NavItem.Dashboard;
|
[ObservableProperty] private NavItem _activeNavItem = NavItem.Dashboard;
|
||||||
[ObservableProperty] private string _currentSchoolYear = "";
|
[ObservableProperty] private string _currentSchoolYear = "";
|
||||||
|
|
||||||
|
public SyncStatusViewModel SyncStatus { get; }
|
||||||
|
|
||||||
public MainWindowViewModel(IServiceProvider services,
|
public MainWindowViewModel(IServiceProvider services,
|
||||||
DashboardViewModel dashboard, SchoolYearService sy)
|
DashboardViewModel dashboard, SchoolYearService sy,
|
||||||
|
SyncStatusViewModel syncStatus)
|
||||||
{
|
{
|
||||||
_services = services;
|
_services = services;
|
||||||
|
SyncStatus = syncStatus;
|
||||||
CurrentSchoolYear = sy.CurrentSchoolYear();
|
CurrentSchoolYear = sy.CurrentSchoolYear();
|
||||||
CurrentPage = dashboard;
|
CurrentPage = dashboard;
|
||||||
}
|
}
|
||||||
@@ -42,11 +46,11 @@ public partial class MainWindowViewModel : ObservableObject
|
|||||||
|
|
||||||
public void NavigateToGroupDetail(Guid groupId, int initialTab = 0)
|
public void NavigateToGroupDetail(Guid groupId, int initialTab = 0)
|
||||||
{
|
{
|
||||||
ActiveNavItem = NavItem.Groups;
|
ActiveNavItem = NavItem.Groups;
|
||||||
var vm = _services.GetRequiredService<GroupDetailViewModel>();
|
var vm = _services.GetRequiredService<GroupDetailViewModel>();
|
||||||
vm.LoadGroup(groupId);
|
|
||||||
vm.ActiveTabIndex = initialTab;
|
vm.ActiveTabIndex = initialTab;
|
||||||
CurrentPage = vm;
|
CurrentPage = vm; // View bindet zuerst → Callbacks werden registriert
|
||||||
|
vm.LoadGroup(groupId); // dann Daten laden
|
||||||
}
|
}
|
||||||
|
|
||||||
public void NavigateToStudent(Guid studentId)
|
public void NavigateToStudent(Guid studentId)
|
||||||
|
|||||||
@@ -40,8 +40,17 @@ public partial class StudentListViewModel : ObservableObject
|
|||||||
foreach (var s in f) Students.Add(new StudentListItem(s));
|
foreach (var s in f) Students.Add(new StudentListItem(s));
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand] private void AddStudent() { /* TODO */ }
|
public Func<Task>? OnAddStudent { get; set; }
|
||||||
[RelayCommand] private void Refresh() => LoadStudents();
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task AddStudent()
|
||||||
|
{
|
||||||
|
if (OnAddStudent is null) return;
|
||||||
|
await OnAddStudent();
|
||||||
|
LoadStudents();
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand] private void Refresh() => LoadStudents();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class StudentListItem
|
public class StudentListItem
|
||||||
@@ -68,9 +77,14 @@ public partial class StudentDetailViewModel : ObservableObject
|
|||||||
[ObservableProperty] private bool _isEditing;
|
[ObservableProperty] private bool _isEditing;
|
||||||
[ObservableProperty] private string _editFirstName = "";
|
[ObservableProperty] private string _editFirstName = "";
|
||||||
[ObservableProperty] private string _editLastName = "";
|
[ObservableProperty] private string _editLastName = "";
|
||||||
|
[ObservableProperty] private ContactItem? _selectedContact;
|
||||||
|
|
||||||
public ObservableCollection<EnrollmentEntry> Enrollments { get; } = [];
|
public ObservableCollection<EnrollmentEntry> Enrollments { get; } = [];
|
||||||
public ObservableCollection<DocEntry> Documentation { get; } = [];
|
public ObservableCollection<DocEntry> Documentation { get; } = [];
|
||||||
|
public ObservableCollection<ContactItem> Contacts { get; } = [];
|
||||||
|
public bool HasNoContacts => Contacts.Count == 0;
|
||||||
|
public Func<Contact?, Task<Contact?>>? OnEditContact { get; set; }
|
||||||
|
public Action<ContactItem>? OnViewAddress { get; set; }
|
||||||
|
|
||||||
public StudentDetailViewModel(IStudentRepository students,
|
public StudentDetailViewModel(IStudentRepository students,
|
||||||
IEnrollmentRepository enrollments, IGroupRepository groups,
|
IEnrollmentRepository enrollments, IGroupRepository groups,
|
||||||
@@ -96,6 +110,8 @@ public partial class StudentDetailViewModel : ObservableObject
|
|||||||
Enrollments.Add(new() { SchoolYear = e.SchoolYear, GroupName = g.Name, Subject = g.Subject ?? "" });
|
Enrollments.Add(new() { SchoolYear = e.SchoolYear, GroupName = g.Name, Subject = g.Subject ?? "" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LoadContacts();
|
||||||
|
|
||||||
Documentation.Clear();
|
Documentation.Clear();
|
||||||
foreach (var d in _docs.GetByStudent(Student.Id))
|
foreach (var d in _docs.GetByStudent(Student.Id))
|
||||||
Documentation.Add(new() { Date = d.Date.ToString("dd.MM.yyyy"), Title = d.Title,
|
Documentation.Add(new() { Date = d.Date.ToString("dd.MM.yyyy"), Title = d.Title,
|
||||||
@@ -125,7 +141,309 @@ public partial class StudentDetailViewModel : ObservableObject
|
|||||||
StudentTitle = Student.FullName;
|
StudentTitle = Student.FullName;
|
||||||
IsEditing = false;
|
IsEditing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void LoadContacts(Guid? selectedId = null)
|
||||||
|
{
|
||||||
|
Contacts.Clear();
|
||||||
|
if (Student is null) return;
|
||||||
|
foreach (var c in Student.Contacts
|
||||||
|
.OrderBy(c => c.InvalidSince.HasValue)
|
||||||
|
.ThenBy(c => c.Name))
|
||||||
|
Contacts.Add(new ContactItem(c));
|
||||||
|
OnPropertyChanged(nameof(HasNoContacts));
|
||||||
|
SelectedContact = selectedId.HasValue
|
||||||
|
? Contacts.FirstOrDefault(c => c.Id == selectedId.Value)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task AddContact()
|
||||||
|
{
|
||||||
|
if (Student is null || OnEditContact is null) return;
|
||||||
|
var contact = await OnEditContact(null);
|
||||||
|
if (contact is null) return;
|
||||||
|
Student.Contacts.Add(contact);
|
||||||
|
_students.Save(Student);
|
||||||
|
LoadContacts(contact.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand(CanExecute = nameof(HasSelectedContact))]
|
||||||
|
private async Task EditContact()
|
||||||
|
{
|
||||||
|
if (Student is null || SelectedContact is null || OnEditContact is null) return;
|
||||||
|
var edited = await OnEditContact(SelectedContact.Model);
|
||||||
|
if (edited is null) return;
|
||||||
|
var index = Student.Contacts.FindIndex(c => c.Id == edited.Id);
|
||||||
|
if (index >= 0) Student.Contacts[index] = edited;
|
||||||
|
_students.Save(Student);
|
||||||
|
LoadContacts(edited.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand(CanExecute = nameof(CanViewSelectedAddress))]
|
||||||
|
private void ViewSelectedAddress()
|
||||||
|
{
|
||||||
|
if (SelectedContact is not null) OnViewAddress?.Invoke(SelectedContact);
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnSelectedContactChanged(ContactItem? value)
|
||||||
|
{
|
||||||
|
EditContactCommand.NotifyCanExecuteChanged();
|
||||||
|
ViewSelectedAddressCommand.NotifyCanExecuteChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool HasSelectedContact() => SelectedContact is not null;
|
||||||
|
private bool CanViewSelectedAddress() => SelectedContact?.HasAddress == true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class EnrollmentEntry { public string SchoolYear { get; set; } = ""; public string GroupName { get; set; } = ""; public string Subject { get; set; } = ""; }
|
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; } }
|
public class DocEntry { public string Date { get; set; } = ""; public string Title { get; set; } = ""; public string TypeLabel { get; set; } = ""; public bool IsConfidential { get; set; } }
|
||||||
|
|
||||||
|
public class ContactItem
|
||||||
|
{
|
||||||
|
public Contact Model { get; }
|
||||||
|
public Guid Id => Model.Id;
|
||||||
|
public string Name { get; }
|
||||||
|
public string Relation { get; }
|
||||||
|
public string? Phone { get; }
|
||||||
|
public string? Email { get; }
|
||||||
|
public string Address { get; }
|
||||||
|
public bool HasPhone => !string.IsNullOrEmpty(Phone);
|
||||||
|
public bool HasEmail => !string.IsNullOrEmpty(Email);
|
||||||
|
public bool HasAddress => !string.IsNullOrEmpty(Address);
|
||||||
|
public bool IsInvalid => Model.InvalidSince.HasValue;
|
||||||
|
public bool IsValid => !IsInvalid;
|
||||||
|
public string PhoneDisplay => Phone ?? "";
|
||||||
|
public string EmailDisplay => Email ?? "";
|
||||||
|
public string StatusText => IsInvalid
|
||||||
|
? $"Ungültig seit {Model.InvalidSince:dd.MM.yyyy} · {InvalidReasonText(Model.InvalidReason)}"
|
||||||
|
: "Aktuell";
|
||||||
|
|
||||||
|
public IRelayCommand CallCommand { get; }
|
||||||
|
public IRelayCommand MailCommand { get; }
|
||||||
|
|
||||||
|
public ContactItem(Contact c)
|
||||||
|
{
|
||||||
|
Model = c;
|
||||||
|
Name = c.Name;
|
||||||
|
Relation = c.Relation;
|
||||||
|
Phone = c.Phone;
|
||||||
|
Email = c.Email;
|
||||||
|
Address = FormatAddress(c);
|
||||||
|
CallCommand = new RelayCommand(() => OpenUri($"tel:{Phone}"), () => HasPhone);
|
||||||
|
MailCommand = new RelayCommand(() => OpenUri($"mailto:{Email}"), () => HasEmail);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string InvalidReasonText(ContactInvalidReason? reason) => reason switch
|
||||||
|
{
|
||||||
|
ContactInvalidReason.Moved => "Umzug",
|
||||||
|
ContactInvalidReason.NewPhoneNumber => "Telefonnummer neu",
|
||||||
|
ContactInvalidReason.LostCustody => "Sorgerecht verloren",
|
||||||
|
ContactInvalidReason.NewEmailAddress => "E-Mail-Adresse neu",
|
||||||
|
ContactInvalidReason.NoLongerResponsible => "Nicht mehr zuständig",
|
||||||
|
ContactInvalidReason.Other => "Sonstiges",
|
||||||
|
_ => "Kein Grund angegeben",
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string FormatAddress(Contact c)
|
||||||
|
{
|
||||||
|
var cityLine = string.Join(" ", new[] { c.PostalCode, c.City }
|
||||||
|
.Where(value => !string.IsNullOrWhiteSpace(value)));
|
||||||
|
return string.Join(Environment.NewLine, new[] { c.Street, cityLine }
|
||||||
|
.Where(value => !string.IsNullOrWhiteSpace(value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OpenUri(string uri) =>
|
||||||
|
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(uri) { UseShellExecute = true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Dialog: Schüler anlegen ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public partial class AddStudentDialogViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
private readonly IStudentRepository _students;
|
||||||
|
|
||||||
|
[ObservableProperty] private string _firstName = "";
|
||||||
|
[ObservableProperty] private string _lastName = "";
|
||||||
|
[ObservableProperty] private string _dateOfBirthText = "";
|
||||||
|
[ObservableProperty] private string _selectedGender = "";
|
||||||
|
[ObservableProperty] private string _notes = "";
|
||||||
|
[ObservableProperty] private string _validationMessage = "";
|
||||||
|
|
||||||
|
public List<string> GenderOptions { get; } = ["", "M – männlich", "W – weiblich", "D – divers"];
|
||||||
|
public List<string> RelationPresets { get; } = ["Schüler/in", "Mutter", "Vater", "Elternteil", "Erziehungsberechtigte/r", "Sonstige"];
|
||||||
|
|
||||||
|
public ObservableCollection<ContactEntryViewModel> Contacts { get; } = [];
|
||||||
|
public Student? Result { get; private set; }
|
||||||
|
|
||||||
|
public AddStudentDialogViewModel(IStudentRepository students) => _students = students;
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void AddContact() =>
|
||||||
|
Contacts.Add(new ContactEntryViewModel(this) { Relation = "Elternteil" });
|
||||||
|
|
||||||
|
public void RemoveContact(ContactEntryViewModel c) => Contacts.Remove(c);
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void Save()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(LastName)) { ValidationMessage = "Nachname erforderlich."; return; }
|
||||||
|
if (string.IsNullOrWhiteSpace(FirstName)) { ValidationMessage = "Vorname erforderlich."; return; }
|
||||||
|
|
||||||
|
DateOnly? dob = null;
|
||||||
|
if (!string.IsNullOrWhiteSpace(DateOfBirthText))
|
||||||
|
{
|
||||||
|
if (!DateOnly.TryParseExact(DateOfBirthText, "dd.MM.yyyy", null,
|
||||||
|
System.Globalization.DateTimeStyles.None, out var d))
|
||||||
|
{
|
||||||
|
ValidationMessage = "Geburtsdatum im Format TT.MM.JJJJ."; return;
|
||||||
|
}
|
||||||
|
dob = d;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result = new Student
|
||||||
|
{
|
||||||
|
FirstName = FirstName.Trim(),
|
||||||
|
LastName = LastName.Trim(),
|
||||||
|
DateOfBirth = dob,
|
||||||
|
Gender = SelectedGender switch
|
||||||
|
{
|
||||||
|
"M – männlich" => Gender.M,
|
||||||
|
"W – weiblich" => Gender.W,
|
||||||
|
"D – divers" => Gender.D,
|
||||||
|
_ => (Gender?)null,
|
||||||
|
},
|
||||||
|
Notes = string.IsNullOrWhiteSpace(Notes) ? null : Notes.Trim(),
|
||||||
|
Contacts = Contacts.Select(c => c.ToModel()).Where(c => !string.IsNullOrWhiteSpace(c.Name)).ToList(),
|
||||||
|
};
|
||||||
|
_students.Save(Result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial class ContactEntryViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
private readonly AddStudentDialogViewModel _parent;
|
||||||
|
|
||||||
|
[ObservableProperty] private string _name = "";
|
||||||
|
[ObservableProperty] private string _relation = "";
|
||||||
|
[ObservableProperty] private string _phone = "";
|
||||||
|
[ObservableProperty] private string _email = "";
|
||||||
|
[ObservableProperty] private string _street = "";
|
||||||
|
[ObservableProperty] private string _postalCode = "";
|
||||||
|
[ObservableProperty] private string _city = "";
|
||||||
|
|
||||||
|
public ContactEntryViewModel(AddStudentDialogViewModel parent) => _parent = parent;
|
||||||
|
|
||||||
|
[RelayCommand] private void Remove() => _parent.RemoveContact(this);
|
||||||
|
|
||||||
|
public Contact ToModel() => new()
|
||||||
|
{
|
||||||
|
Name = Name.Trim(),
|
||||||
|
Relation = Relation.Trim(),
|
||||||
|
Phone = string.IsNullOrWhiteSpace(Phone) ? null : Phone.Trim(),
|
||||||
|
Email = string.IsNullOrWhiteSpace(Email) ? null : Email.Trim(),
|
||||||
|
Street = string.IsNullOrWhiteSpace(Street) ? null : Street.Trim(),
|
||||||
|
PostalCode = string.IsNullOrWhiteSpace(PostalCode) ? null : PostalCode.Trim(),
|
||||||
|
City = string.IsNullOrWhiteSpace(City) ? null : City.Trim(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial class ContactEditDialogViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
private readonly Contact? _source;
|
||||||
|
|
||||||
|
[ObservableProperty] private string _name = "";
|
||||||
|
[ObservableProperty] private string _relation = "Elternteil";
|
||||||
|
[ObservableProperty] private string _phone = "";
|
||||||
|
[ObservableProperty] private string _email = "";
|
||||||
|
[ObservableProperty] private string _street = "";
|
||||||
|
[ObservableProperty] private string _postalCode = "";
|
||||||
|
[ObservableProperty] private string _city = "";
|
||||||
|
[ObservableProperty] private bool _isInvalid;
|
||||||
|
[ObservableProperty] private string _invalidSinceText = "";
|
||||||
|
[ObservableProperty] private string _selectedInvalidReason = "";
|
||||||
|
[ObservableProperty] private string _invalidReasonDetails = "";
|
||||||
|
[ObservableProperty] private string _validationMessage = "";
|
||||||
|
|
||||||
|
public string DialogTitle => _source is null ? "Kontakt anlegen" : "Kontakt bearbeiten";
|
||||||
|
public List<string> RelationPresets { get; } =
|
||||||
|
["Schüler/in", "Mutter", "Vater", "Elternteil", "Erziehungsberechtigte/r", "Sonstige"];
|
||||||
|
public List<string> InvalidReasonOptions { get; } =
|
||||||
|
["Umzug", "Telefonnummer neu", "Sorgerecht verloren", "E-Mail-Adresse neu",
|
||||||
|
"Nicht mehr zuständig", "Sonstiges"];
|
||||||
|
public Contact? Result { get; private set; }
|
||||||
|
|
||||||
|
public ContactEditDialogViewModel(Contact? source)
|
||||||
|
{
|
||||||
|
_source = source;
|
||||||
|
if (source is null) return;
|
||||||
|
Name = source.Name;
|
||||||
|
Relation = source.Relation;
|
||||||
|
Phone = source.Phone ?? "";
|
||||||
|
Email = source.Email ?? "";
|
||||||
|
Street = source.Street ?? "";
|
||||||
|
PostalCode = source.PostalCode ?? "";
|
||||||
|
City = source.City ?? "";
|
||||||
|
IsInvalid = source.InvalidSince.HasValue;
|
||||||
|
InvalidSinceText = source.InvalidSince?.ToString("dd.MM.yyyy") ?? "";
|
||||||
|
SelectedInvalidReason = ContactItem.InvalidReasonText(source.InvalidReason);
|
||||||
|
InvalidReasonDetails = source.InvalidReasonDetails ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnIsInvalidChanged(bool value)
|
||||||
|
{
|
||||||
|
if (value && string.IsNullOrWhiteSpace(InvalidSinceText))
|
||||||
|
InvalidSinceText = DateTime.Today.ToString("dd.MM.yyyy");
|
||||||
|
if (value && string.IsNullOrWhiteSpace(SelectedInvalidReason))
|
||||||
|
SelectedInvalidReason = InvalidReasonOptions[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void Save()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(Name))
|
||||||
|
{
|
||||||
|
ValidationMessage = "Name erforderlich.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DateOnly? invalidSince = null;
|
||||||
|
if (IsInvalid)
|
||||||
|
{
|
||||||
|
if (!DateOnly.TryParseExact(InvalidSinceText, "dd.MM.yyyy", null,
|
||||||
|
System.Globalization.DateTimeStyles.None, out var parsedDate))
|
||||||
|
{
|
||||||
|
ValidationMessage = "Ungültig seit im Format TT.MM.JJJJ angeben.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
invalidSince = parsedDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result = new Contact
|
||||||
|
{
|
||||||
|
Id = _source?.Id ?? Guid.NewGuid(),
|
||||||
|
Name = Name.Trim(),
|
||||||
|
Relation = Relation.Trim(),
|
||||||
|
Phone = NullIfEmpty(Phone),
|
||||||
|
Email = NullIfEmpty(Email),
|
||||||
|
Street = NullIfEmpty(Street),
|
||||||
|
PostalCode = NullIfEmpty(PostalCode),
|
||||||
|
City = NullIfEmpty(City),
|
||||||
|
InvalidSince = invalidSince,
|
||||||
|
InvalidReason = IsInvalid ? ParseInvalidReason(SelectedInvalidReason) : null,
|
||||||
|
InvalidReasonDetails = IsInvalid ? NullIfEmpty(InvalidReasonDetails) : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? NullIfEmpty(string value) =>
|
||||||
|
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||||
|
|
||||||
|
private static ContactInvalidReason ParseInvalidReason(string value) => value switch
|
||||||
|
{
|
||||||
|
"Umzug" => ContactInvalidReason.Moved,
|
||||||
|
"Telefonnummer neu" => ContactInvalidReason.NewPhoneNumber,
|
||||||
|
"Sorgerecht verloren" => ContactInvalidReason.LostCustody,
|
||||||
|
"E-Mail-Adresse neu" => ContactInvalidReason.NewEmailAddress,
|
||||||
|
"Nicht mehr zuständig" => ContactInvalidReason.NoLongerResponsible,
|
||||||
|
_ => ContactInvalidReason.Other,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
|
||||||
|
x:Class="LehrerApp.Desktop.Views.Groups.AddSessionDialog"
|
||||||
|
x:DataType="vm:AddSessionDialogViewModel"
|
||||||
|
Title="Bewertungszeitpunkt anlegen"
|
||||||
|
Width="380" SizeToContent="Height"
|
||||||
|
CanResize="False" WindowStartupLocation="CenterOwner">
|
||||||
|
|
||||||
|
<Grid RowDefinitions="*,Auto" Margin="24">
|
||||||
|
<StackPanel Grid.Row="0" Spacing="14">
|
||||||
|
<TextBlock Text="Neuer Bewertungszeitpunkt" FontSize="18" FontWeight="SemiBold"/>
|
||||||
|
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBlock Text="Datum *" FontSize="12" Opacity="0.7"/>
|
||||||
|
<TextBox Text="{Binding DateText}" PlaceholderText="TT.MM.JJJJ" x:Name="DateBox"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBlock Text="Kommentar (optional)" FontSize="12" Opacity="0.7"/>
|
||||||
|
<TextBox Text="{Binding Comment}" PlaceholderText="z. B. Stunde 12 – Säure-Base-Reaktion"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<TextBlock Text="{Binding ValidationMessage}" Foreground="Red" FontSize="12"
|
||||||
|
IsVisible="{Binding ValidationMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Grid Grid.Row="1" ColumnDefinitions="*,8,*" Margin="0,20,0,0">
|
||||||
|
<Button Grid.Column="0" Content="Abbrechen" HorizontalAlignment="Stretch" Click="OnCancel"/>
|
||||||
|
<Button Grid.Column="2" Content="Anlegen" HorizontalAlignment="Stretch" Click="OnSave"/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Groups;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Views.Groups;
|
||||||
|
|
||||||
|
public partial class AddSessionDialog : Window
|
||||||
|
{
|
||||||
|
public AddSessionDialog() => InitializeComponent();
|
||||||
|
|
||||||
|
protected override void OnOpened(EventArgs e)
|
||||||
|
{
|
||||||
|
base.OnOpened(e);
|
||||||
|
this.FindControl<TextBox>("DateBox")?.Focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSave(object? s, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is AddSessionDialogViewModel vm && vm.SaveCommand.CanExecute(null))
|
||||||
|
{
|
||||||
|
vm.SaveCommand.Execute(null);
|
||||||
|
if (vm.Result is not null) Close(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCancel(object? s, RoutedEventArgs e) => Close(false);
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
|
||||||
|
x:Class="LehrerApp.Desktop.Views.Groups.AddStudentToGroupDialog"
|
||||||
|
x:DataType="vm:AddStudentToGroupDialogViewModel"
|
||||||
|
Title="Schüler hinzufügen"
|
||||||
|
Width="420" Height="500"
|
||||||
|
CanResize="False" WindowStartupLocation="CenterOwner">
|
||||||
|
|
||||||
|
<Grid RowDefinitions="Auto,*,Auto" Margin="24">
|
||||||
|
|
||||||
|
<!-- Überschrift + Suche -->
|
||||||
|
<StackPanel Grid.Row="0" Spacing="12" Margin="0,0,0,12">
|
||||||
|
<TextBlock Text="Schüler zur Gruppe hinzufügen" FontSize="18" FontWeight="SemiBold"/>
|
||||||
|
<TextBox Text="{Binding SearchText}"
|
||||||
|
PlaceholderText="Schüler suchen …"
|
||||||
|
x:Name="SearchBox"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Schülerliste -->
|
||||||
|
<ListBox Grid.Row="1"
|
||||||
|
ItemsSource="{Binding AvailableStudents}"
|
||||||
|
SelectedItem="{Binding SelectedStudent}"
|
||||||
|
BorderThickness="1">
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:StudentPickerItem">
|
||||||
|
<TextBlock Text="{Binding FullName}" Padding="4,2"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
</ListBox>
|
||||||
|
|
||||||
|
<!-- Validation + Buttons -->
|
||||||
|
<StackPanel Grid.Row="2" Spacing="12" Margin="0,16,0,0">
|
||||||
|
<TextBlock Text="{Binding ValidationMessage}" Foreground="Red" FontSize="12"
|
||||||
|
IsVisible="{Binding ValidationMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
<Grid ColumnDefinitions="*,8,*">
|
||||||
|
<Button Grid.Column="0" Content="Abbrechen" HorizontalAlignment="Stretch" Click="OnCancel"/>
|
||||||
|
<Button Grid.Column="2" Content="Hinzufügen" HorizontalAlignment="Stretch" Click="OnSave"/>
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Groups;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Views.Groups;
|
||||||
|
|
||||||
|
public partial class AddStudentToGroupDialog : Window
|
||||||
|
{
|
||||||
|
public AddStudentToGroupDialog() => InitializeComponent();
|
||||||
|
|
||||||
|
protected override void OnOpened(EventArgs e)
|
||||||
|
{
|
||||||
|
base.OnOpened(e);
|
||||||
|
this.FindControl<TextBox>("SearchBox")?.Focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSave(object? s, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is AddStudentToGroupDialogViewModel vm && vm.SaveCommand.CanExecute(null))
|
||||||
|
{
|
||||||
|
vm.SaveCommand.Execute(null);
|
||||||
|
if (vm.Result is not null) Close(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCancel(object? s, RoutedEventArgs e) => Close(false);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<UserControl xmlns="https://github.com/avaloniaui"
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
|
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
|
||||||
|
xmlns:views="clr-namespace:LehrerApp.Desktop.Views.Groups"
|
||||||
x:Class="LehrerApp.Desktop.Views.Groups.GroupDetailView"
|
x:Class="LehrerApp.Desktop.Views.Groups.GroupDetailView"
|
||||||
x:DataType="vm:GroupDetailViewModel">
|
x:DataType="vm:GroupDetailViewModel">
|
||||||
|
|
||||||
@@ -20,17 +21,14 @@
|
|||||||
<Run Text="{Binding StudentCount}"/>
|
<Run Text="{Binding StudentCount}"/>
|
||||||
<Run Text=" Schüler"/>
|
<Run Text=" Schüler"/>
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
<Button Content="+ Schüler" Command="{Binding AddStudentCommand}"/>
|
<Button Content="+ Schüler" Command="{Binding AddStudentCommand}"/>
|
||||||
<Button Content="+ Klausur" Command="{Binding AddExamCommand}"/>
|
<Button Content="− Austragen" Command="{Binding RemoveStudentCommand}"
|
||||||
|
IsVisible="{Binding SelectedStudent, Converter={x:Static ObjectConverters.IsNotNull}}"/>
|
||||||
|
<Button Content="+ Klausur" Command="{Binding AddExamCommand}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!--
|
|
||||||
Avalonia 12: TabbedPage ersetzt manuelles Tab-System.
|
|
||||||
Kein ActiveTab-Property, kein IsVisible-Binding, keine eigenen Tab-Buttons.
|
|
||||||
TabPlacement="Top" → Tabs oben (wie Browser-Tabs).
|
|
||||||
-->
|
|
||||||
<TabbedPage Grid.Row="1" TabPlacement="Top" SelectedIndex="{Binding ActiveTabIndex}">
|
<TabbedPage Grid.Row="1" TabPlacement="Top" SelectedIndex="{Binding ActiveTabIndex}">
|
||||||
|
|
||||||
<!-- Tab: Übersicht -->
|
<!-- Tab: Übersicht -->
|
||||||
@@ -47,6 +45,7 @@
|
|||||||
<!-- Tab: Schüler -->
|
<!-- Tab: Schüler -->
|
||||||
<ContentPage Header="Schüler">
|
<ContentPage Header="Schüler">
|
||||||
<DataGrid ItemsSource="{Binding Students}"
|
<DataGrid ItemsSource="{Binding Students}"
|
||||||
|
SelectedItem="{Binding SelectedStudent}"
|
||||||
AutoGenerateColumns="False"
|
AutoGenerateColumns="False"
|
||||||
IsReadOnly="True"
|
IsReadOnly="True"
|
||||||
GridLinesVisibility="Horizontal"
|
GridLinesVisibility="Horizontal"
|
||||||
@@ -61,6 +60,11 @@
|
|||||||
</DataGrid>
|
</DataGrid>
|
||||||
</ContentPage>
|
</ContentPage>
|
||||||
|
|
||||||
|
<!-- Tab: Mitarbeit -->
|
||||||
|
<ContentPage Header="Mitarbeit">
|
||||||
|
<views:ParticipationTabView DataContext="{Binding ParticipationTab}"/>
|
||||||
|
</ContentPage>
|
||||||
|
|
||||||
<!-- Tab: Klausuren -->
|
<!-- Tab: Klausuren -->
|
||||||
<ContentPage Header="Klausuren">
|
<ContentPage Header="Klausuren">
|
||||||
<DataGrid ItemsSource="{Binding Exams}"
|
<DataGrid ItemsSource="{Binding Exams}"
|
||||||
|
|||||||
@@ -1,3 +1,35 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using LehrerApp.Core.Interfaces;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Groups;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace LehrerApp.Desktop.Views.Groups;
|
namespace LehrerApp.Desktop.Views.Groups;
|
||||||
public partial class GroupDetailView : UserControl { public GroupDetailView() => InitializeComponent(); }
|
|
||||||
|
public partial class GroupDetailView : UserControl
|
||||||
|
{
|
||||||
|
public GroupDetailView() => InitializeComponent();
|
||||||
|
|
||||||
|
protected override void OnDataContextChanged(EventArgs e)
|
||||||
|
{
|
||||||
|
base.OnDataContextChanged(e);
|
||||||
|
if (DataContext is GroupDetailViewModel vm)
|
||||||
|
vm.OnAddStudent = ShowAddStudentDialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> ShowAddStudentDialog()
|
||||||
|
{
|
||||||
|
if (DataContext is not GroupDetailViewModel vm || vm.Group is null) return false;
|
||||||
|
|
||||||
|
var dialogVm = new AddStudentToGroupDialogViewModel(
|
||||||
|
App.Services.GetRequiredService<IStudentRepository>(),
|
||||||
|
App.Services.GetRequiredService<IEnrollmentRepository>(),
|
||||||
|
vm.Group.Id,
|
||||||
|
vm.Group.SchoolYear);
|
||||||
|
|
||||||
|
var dialog = new AddStudentToGroupDialog { DataContext = dialogVm };
|
||||||
|
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||||
|
if (owner is null) return false;
|
||||||
|
|
||||||
|
return await dialog.ShowDialog<bool>(owner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@
|
|||||||
<!-- Rechte Spalte: Platzhalter wenn keine Auswahl -->
|
<!-- Rechte Spalte: Platzhalter wenn keine Auswahl -->
|
||||||
<StackPanel Grid.Column="1" HorizontalAlignment="Center"
|
<StackPanel Grid.Column="1" HorizontalAlignment="Center"
|
||||||
VerticalAlignment="Center" Spacing="8"
|
VerticalAlignment="Center" Spacing="8"
|
||||||
IsVisible="{Binding !SelectedGroup}">
|
IsVisible="{Binding SelectedGroup, Converter={x:Static ObjectConverters.IsNull}}">
|
||||||
<TextBlock Text="Gruppe auswählen" FontSize="16" Opacity="0.4"
|
<TextBlock Text="Gruppe auswählen" FontSize="16" Opacity="0.4"
|
||||||
HorizontalAlignment="Center"/>
|
HorizontalAlignment="Center"/>
|
||||||
<TextBlock Text="oder + Neue Gruppe anlegen" FontSize="12" Opacity="0.3"
|
<TextBlock Text="oder + Neue Gruppe anlegen" FontSize="12" Opacity="0.3"
|
||||||
@@ -74,14 +74,15 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- Rechte Spalte: Gruppen-Übersicht wenn ausgewählt -->
|
<!-- Rechte Spalte: Gruppen-Übersicht wenn ausgewählt -->
|
||||||
<ScrollViewer Grid.Column="1" IsVisible="{Binding SelectedGroup}">
|
<ScrollViewer Grid.Column="1"
|
||||||
|
IsVisible="{Binding SelectedGroup, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||||
<StackPanel Margin="28,24" Spacing="20">
|
<StackPanel Margin="28,24" Spacing="20">
|
||||||
|
|
||||||
<!-- Gruppenname und Kurzinfos -->
|
<!-- Gruppenname und Kurzinfos -->
|
||||||
<StackPanel Spacing="4">
|
<StackPanel Spacing="4">
|
||||||
<TextBlock Text="{Binding SelectedGroup.DisplayName}"
|
<TextBlock Text="{Binding SelectedGroupDisplayName}"
|
||||||
FontSize="24" FontWeight="SemiBold" TextWrapping="Wrap"/>
|
FontSize="24" FontWeight="SemiBold" TextWrapping="Wrap"/>
|
||||||
<TextBlock Text="{Binding SelectedGroup.Subtitle}"
|
<TextBlock Text="{Binding SelectedGroupSubtitle}"
|
||||||
FontSize="12" Opacity="0.55"/>
|
FontSize="12" Opacity="0.55"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
|
||||||
|
x:Class="LehrerApp.Desktop.Views.Groups.ParticipationQuickInputDialog"
|
||||||
|
x:DataType="vm:QuickInputViewModel"
|
||||||
|
Title="Schnelleingabe Mitarbeit"
|
||||||
|
Width="420" SizeToContent="Height"
|
||||||
|
CanResize="False" WindowStartupLocation="CenterOwner">
|
||||||
|
|
||||||
|
<Grid RowDefinitions="Auto,*,Auto" Margin="24">
|
||||||
|
|
||||||
|
<!-- Schülername + Fortschritt -->
|
||||||
|
<Grid Grid.Row="0" ColumnDefinitions="*,Auto" Margin="0,0,0,16">
|
||||||
|
<StackPanel Grid.Column="0">
|
||||||
|
<TextBlock Text="{Binding StudentName}" FontSize="22" FontWeight="SemiBold"/>
|
||||||
|
<TextBlock Text="{Binding CurrentAspectLabel}" FontSize="13" Opacity="0.5"/>
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock Grid.Column="1" Text="{Binding ProgressText}"
|
||||||
|
VerticalAlignment="Top" FontSize="13" Opacity="0.4"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Aspektliste -->
|
||||||
|
<ItemsControl Grid.Row="1" ItemsSource="{Binding AspectRows}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:QuickAspectRow">
|
||||||
|
<Grid ColumnDefinitions="4,8,*,44" Margin="0,3">
|
||||||
|
<!-- Aktiv-Balken links -->
|
||||||
|
<Border Grid.Column="0" CornerRadius="2"
|
||||||
|
Background="{DynamicResource SystemAccentColor}"
|
||||||
|
IsVisible="{Binding IsActive}"/>
|
||||||
|
<!-- Index-Badge -->
|
||||||
|
<TextBlock Grid.Column="1" Text="{Binding Index, StringFormat='[{0}]'}"
|
||||||
|
FontFamily="Monospace" FontSize="11" Opacity="0.4"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,6,0"/>
|
||||||
|
<!-- Aspektname: bold wenn aktiv, gedimmt wenn nicht -->
|
||||||
|
<TextBlock Grid.Column="2" Text="{Binding Label}" FontWeight="SemiBold"
|
||||||
|
IsVisible="{Binding IsActive}" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Grid.Column="2" Text="{Binding Label}" Opacity="0.55"
|
||||||
|
IsVisible="{Binding IsActive, Converter={x:Static BoolConverters.Not}}"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
<!-- Aktueller Wert -->
|
||||||
|
<TextBlock Grid.Column="3" Text="{Binding DisplayLabel}"
|
||||||
|
FontFamily="Monospace" FontSize="16" FontWeight="Bold"
|
||||||
|
TextAlignment="Right" VerticalAlignment="Center"/>
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
|
||||||
|
<!-- Tastenkürzel-Legende + Schließen-Button -->
|
||||||
|
<StackPanel Grid.Row="2" Margin="0,16,0,0" Spacing="6">
|
||||||
|
<TextBlock Opacity="0.35" FontSize="11" TextWrapping="Wrap"
|
||||||
|
Text="1–5 bewerten · Q/W/E/R/T Aspekt wählen · Leertaste nächster Aspekt · Enter nächster Schüler · Backspace vorheriger · +/− anpassen · Esc schließen"/>
|
||||||
|
<Button Content="Schließen" HorizontalAlignment="Stretch" Click="OnClose"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Input;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Groups;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Views.Groups;
|
||||||
|
|
||||||
|
public partial class ParticipationQuickInputDialog : Window
|
||||||
|
{
|
||||||
|
public ParticipationQuickInputDialog() => InitializeComponent();
|
||||||
|
|
||||||
|
protected override void OnOpened(EventArgs e)
|
||||||
|
{
|
||||||
|
base.OnOpened(e);
|
||||||
|
Focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnKeyDown(KeyEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is not QuickInputViewModel vm) { base.OnKeyDown(e); return; }
|
||||||
|
|
||||||
|
switch (e.Key)
|
||||||
|
{
|
||||||
|
case Key.D1 or Key.NumPad1: vm.SetRatingByNumber(1); e.Handled = true; break;
|
||||||
|
case Key.D2 or Key.NumPad2: vm.SetRatingByNumber(2); e.Handled = true; break;
|
||||||
|
case Key.D3 or Key.NumPad3: vm.SetRatingByNumber(3); e.Handled = true; break;
|
||||||
|
case Key.D4 or Key.NumPad4: vm.SetRatingByNumber(4); e.Handled = true; break;
|
||||||
|
case Key.D5 or Key.NumPad5: vm.SetRatingByNumber(5); e.Handled = true; break;
|
||||||
|
|
||||||
|
case Key.Q: vm.SelectAspect(0); e.Handled = true; break;
|
||||||
|
case Key.W: vm.SelectAspect(1); e.Handled = true; break;
|
||||||
|
case Key.E: vm.SelectAspect(2); e.Handled = true; break;
|
||||||
|
case Key.R: vm.SelectAspect(3); e.Handled = true; break;
|
||||||
|
case Key.T: vm.SelectAspect(4); e.Handled = true; break;
|
||||||
|
|
||||||
|
case Key.Space: vm.NextAspect(); e.Handled = true; break;
|
||||||
|
case Key.Enter:
|
||||||
|
if (vm.IsLastStudent) Close();
|
||||||
|
else vm.NextStudent();
|
||||||
|
e.Handled = true;
|
||||||
|
break;
|
||||||
|
case Key.Back: vm.PreviousStudent(); e.Handled = true; break;
|
||||||
|
|
||||||
|
case Key.OemPlus or Key.Add: vm.IncrementRating(); e.Handled = true; break;
|
||||||
|
case Key.OemMinus or Key.Subtract: vm.DecrementRating(); e.Handled = true; break;
|
||||||
|
|
||||||
|
case Key.Escape: Close(); e.Handled = true; break;
|
||||||
|
|
||||||
|
default: base.OnKeyDown(e); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnClose(object? s, RoutedEventArgs e) => Close();
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
|
||||||
|
x:Class="LehrerApp.Desktop.Views.Groups.ParticipationTabView"
|
||||||
|
x:DataType="vm:ParticipationTabViewModel">
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="220,*">
|
||||||
|
|
||||||
|
<!-- Linke Seite: Sitzungsliste -->
|
||||||
|
<Border Grid.Column="0"
|
||||||
|
BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||||
|
BorderThickness="0,0,1,0">
|
||||||
|
<Grid RowDefinitions="Auto,*">
|
||||||
|
|
||||||
|
<StackPanel Grid.Row="0" Orientation="Horizontal" Spacing="6" Margin="10,10,10,6">
|
||||||
|
<Button Content="+ Sitzung" Command="{Binding AddSessionCommand}" HorizontalAlignment="Stretch"/>
|
||||||
|
<Button Content="Schnell" Command="{Binding QuickInputCommand}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<ListBox Grid.Row="1"
|
||||||
|
ItemsSource="{Binding Sessions}"
|
||||||
|
SelectedItem="{Binding SelectedSession}"
|
||||||
|
BorderThickness="0">
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:ParticipationSessionItem">
|
||||||
|
<TextBlock Text="{Binding Display}" TextWrapping="Wrap" Padding="4,3" FontSize="12"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
</ListBox>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Rechte Seite: Bewertungsraster (wird per Code-Behind aufgebaut) -->
|
||||||
|
<Grid Grid.Column="1" RowDefinitions="Auto,*">
|
||||||
|
<TextBlock Grid.Row="0"
|
||||||
|
Text="{Binding SelectedSessionDisplay}"
|
||||||
|
FontSize="13" FontWeight="SemiBold" Margin="12,10,12,4"
|
||||||
|
IsVisible="{Binding SelectedSession, Converter={x:Static ObjectConverters.IsNotNull}}"/>
|
||||||
|
|
||||||
|
<!-- DataGrid: Spalten werden in ParticipationTabView.axaml.cs dynamisch erzeugt -->
|
||||||
|
<DataGrid Grid.Row="1"
|
||||||
|
x:Name="RatingGrid"
|
||||||
|
ItemsSource="{Binding StudentRows}"
|
||||||
|
AutoGenerateColumns="False"
|
||||||
|
IsReadOnly="True"
|
||||||
|
GridLinesVisibility="All"
|
||||||
|
CanUserReorderColumns="False"
|
||||||
|
CanUserResizeColumns="True"
|
||||||
|
IsVisible="{Binding SelectedSession, Converter={x:Static ObjectConverters.IsNotNull}}"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="1"
|
||||||
|
Text="{Binding NoDataText}"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||||
|
Opacity="0.35" FontSize="14"
|
||||||
|
IsVisible="{Binding SelectedSession, Converter={x:Static ObjectConverters.IsNull}}"/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.Templates;
|
||||||
|
using Avalonia.Data;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Groups;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Views.Groups;
|
||||||
|
|
||||||
|
public partial class ParticipationTabView : UserControl
|
||||||
|
{
|
||||||
|
private ParticipationTabViewModel? _vm;
|
||||||
|
|
||||||
|
public ParticipationTabView() => InitializeComponent();
|
||||||
|
|
||||||
|
protected override void OnDataContextChanged(EventArgs e)
|
||||||
|
{
|
||||||
|
base.OnDataContextChanged(e);
|
||||||
|
|
||||||
|
if (DataContext is ParticipationTabViewModel vm)
|
||||||
|
{
|
||||||
|
_vm = vm;
|
||||||
|
vm.OnAddSession = ShowAddSessionDialog;
|
||||||
|
vm.OnQuickInput = ShowQuickInputDialog;
|
||||||
|
vm.Aspects.CollectionChanged += (_, _) => BuildColumns();
|
||||||
|
BuildColumns();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildColumns()
|
||||||
|
{
|
||||||
|
var grid = this.FindControl<DataGrid>("RatingGrid");
|
||||||
|
if (grid is null || _vm is null) return;
|
||||||
|
|
||||||
|
grid.Columns.Clear();
|
||||||
|
|
||||||
|
grid.Columns.Add(new DataGridTextColumn
|
||||||
|
{
|
||||||
|
Header = "Schüler",
|
||||||
|
Binding = new Binding("Name"),
|
||||||
|
Width = new DataGridLength(160, DataGridLengthUnitType.Pixel),
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (var (aspect, i) in _vm.Aspects.Select((a, i) => (a, i)))
|
||||||
|
{
|
||||||
|
var capturedIndex = i;
|
||||||
|
grid.Columns.Add(new DataGridTemplateColumn
|
||||||
|
{
|
||||||
|
Header = $"{aspect.Label} [{AspectShortcut(i)}]",
|
||||||
|
Width = new DataGridLength(1, DataGridLengthUnitType.Star),
|
||||||
|
CellTemplate = BuildCellTemplate(capturedIndex),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IDataTemplate BuildCellTemplate(int aspectIndex)
|
||||||
|
{
|
||||||
|
return new FuncDataTemplate<ParticipationStudentRow>((row, _) =>
|
||||||
|
{
|
||||||
|
if (row is null) return new TextBlock();
|
||||||
|
|
||||||
|
var cell = row.Cells.ElementAtOrDefault(aspectIndex);
|
||||||
|
if (cell is null) return new TextBlock();
|
||||||
|
|
||||||
|
var panel = new StackPanel
|
||||||
|
{
|
||||||
|
Orientation = Avalonia.Layout.Orientation.Horizontal,
|
||||||
|
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
|
||||||
|
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
|
||||||
|
Spacing = 2,
|
||||||
|
Margin = new Avalonia.Thickness(0, 2),
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var (label, val) in new[] { ("−−", -2), ("−", -1), ("∼", 0), ("+", 1), ("++", 2) })
|
||||||
|
{
|
||||||
|
var btn = new Button
|
||||||
|
{
|
||||||
|
Content = label,
|
||||||
|
Padding = new Avalonia.Thickness(5, 1),
|
||||||
|
FontSize = 11,
|
||||||
|
Opacity = cell.Value == val ? 1.0 : 0.3,
|
||||||
|
};
|
||||||
|
var capturedVal = val;
|
||||||
|
btn.Click += (_, _) => cell.SetValue(capturedVal);
|
||||||
|
cell.PropertyChanged += (_, pe) =>
|
||||||
|
{
|
||||||
|
if (pe.PropertyName == nameof(RatingCell.Value))
|
||||||
|
btn.Opacity = cell.Value == capturedVal ? 1.0 : 0.3;
|
||||||
|
};
|
||||||
|
panel.Children.Add(btn);
|
||||||
|
}
|
||||||
|
|
||||||
|
return panel;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string AspectShortcut(int i) => i switch
|
||||||
|
{
|
||||||
|
0 => "Q", 1 => "W", 2 => "E", 3 => "R", 4 => "T", _ => ""
|
||||||
|
};
|
||||||
|
|
||||||
|
private async Task<LehrerApp.Core.Models.ParticipationSession?> ShowAddSessionDialog()
|
||||||
|
{
|
||||||
|
var vm = new AddSessionDialogViewModel();
|
||||||
|
var dialog = new AddSessionDialog { DataContext = vm };
|
||||||
|
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||||
|
if (owner is null) return null;
|
||||||
|
var ok = await dialog.ShowDialog<bool>(owner);
|
||||||
|
return ok ? vm.Result : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ShowQuickInputDialog(ParticipationTabViewModel tabVm)
|
||||||
|
{
|
||||||
|
if (tabVm.StudentRows.Count == 0) return;
|
||||||
|
var quickVm = new QuickInputViewModel(
|
||||||
|
tabVm.StudentRows.ToList(),
|
||||||
|
tabVm.Aspects.ToList());
|
||||||
|
var dialog = new ParticipationQuickInputDialog { DataContext = quickVm };
|
||||||
|
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||||
|
if (owner is not null)
|
||||||
|
await dialog.ShowDialog(owner);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
<Border DockPanel.Dock="Bottom" Padding="12,8"
|
<Border DockPanel.Dock="Bottom" Padding="12,8"
|
||||||
BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||||
BorderThickness="0,1,0,0">
|
BorderThickness="0,1,0,0">
|
||||||
<views:SyncStatusBar/>
|
<views:SyncStatusBar DataContext="{Binding SyncStatus}"/>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<ScrollViewer>
|
<ScrollViewer>
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Students"
|
||||||
|
x:Class="LehrerApp.Desktop.Views.Students.AddStudentDialog"
|
||||||
|
x:DataType="vm:AddStudentDialogViewModel"
|
||||||
|
Title="Neuer Schüler"
|
||||||
|
Width="500" Height="640"
|
||||||
|
CanResize="True" WindowStartupLocation="CenterOwner">
|
||||||
|
|
||||||
|
<Grid RowDefinitions="*,Auto" Margin="24,20,24,20">
|
||||||
|
|
||||||
|
<ScrollViewer Grid.Row="0" VerticalScrollBarVisibility="Auto">
|
||||||
|
<StackPanel Spacing="14">
|
||||||
|
|
||||||
|
<TextBlock Text="Neuen Schüler anlegen" FontSize="18" FontWeight="SemiBold"/>
|
||||||
|
|
||||||
|
<!-- Name -->
|
||||||
|
<Grid ColumnDefinitions="*,10,*">
|
||||||
|
<StackPanel Grid.Column="0" Spacing="4">
|
||||||
|
<TextBlock Text="Nachname *" FontSize="12" Opacity="0.7"/>
|
||||||
|
<TextBox Text="{Binding LastName}" PlaceholderText="Mustermann" x:Name="LastNameBox"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Column="2" Spacing="4">
|
||||||
|
<TextBlock Text="Vorname *" FontSize="12" Opacity="0.7"/>
|
||||||
|
<TextBox Text="{Binding FirstName}" PlaceholderText="Max"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Geburtsdatum + Geschlecht -->
|
||||||
|
<Grid ColumnDefinitions="*,10,*">
|
||||||
|
<StackPanel Grid.Column="0" Spacing="4">
|
||||||
|
<TextBlock Text="Geburtsdatum" FontSize="12" Opacity="0.7"/>
|
||||||
|
<TextBox Text="{Binding DateOfBirthText}" PlaceholderText="TT.MM.JJJJ"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Column="2" Spacing="4">
|
||||||
|
<TextBlock Text="Geschlecht" FontSize="12" Opacity="0.7"/>
|
||||||
|
<ComboBox ItemsSource="{Binding GenderOptions}"
|
||||||
|
SelectedItem="{Binding SelectedGender}"
|
||||||
|
HorizontalAlignment="Stretch"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Kontakte -->
|
||||||
|
<StackPanel Spacing="6">
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<TextBlock Grid.Column="0" Text="Kontakte" FontSize="14" FontWeight="SemiBold"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
<Button Grid.Column="1" Content="+ Kontakt" Command="{Binding AddContactCommand}"
|
||||||
|
FontSize="12" Padding="8,4"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<ItemsControl ItemsSource="{Binding Contacts}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:ContactEntryViewModel">
|
||||||
|
<Border Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||||
|
CornerRadius="6" Padding="10,8" Margin="0,0,0,6">
|
||||||
|
<StackPanel Spacing="6">
|
||||||
|
<!-- Zeile 1: Name + Beziehung + Löschen -->
|
||||||
|
<Grid ColumnDefinitions="*,10,*,10,Auto">
|
||||||
|
<TextBox Grid.Column="0" Text="{Binding Name}"
|
||||||
|
PlaceholderText="Name" FontSize="12"/>
|
||||||
|
<ComboBox Grid.Column="2"
|
||||||
|
ItemsSource="{Binding $parent[Window].DataContext.RelationPresets}"
|
||||||
|
SelectedItem="{Binding Relation}"
|
||||||
|
HorizontalAlignment="Stretch" FontSize="12"/>
|
||||||
|
<Button Grid.Column="4" Content="×" Padding="6,2"
|
||||||
|
Command="{Binding RemoveCommand}" FontSize="14"/>
|
||||||
|
</Grid>
|
||||||
|
<!-- Zeile 2: Telefon + E-Mail -->
|
||||||
|
<Grid ColumnDefinitions="*,10,*">
|
||||||
|
<TextBox Grid.Column="0" Text="{Binding Phone}"
|
||||||
|
PlaceholderText="Telefon / Handy" FontSize="12"/>
|
||||||
|
<TextBox Grid.Column="2" Text="{Binding Email}"
|
||||||
|
PlaceholderText="E-Mail" FontSize="12"/>
|
||||||
|
</Grid>
|
||||||
|
<!-- Zeile 3: Adresse -->
|
||||||
|
<TextBox Text="{Binding Street}" PlaceholderText="Straße und Hausnummer" FontSize="12"/>
|
||||||
|
<Grid ColumnDefinitions="100,10,*">
|
||||||
|
<TextBox Grid.Column="0" Text="{Binding PostalCode}"
|
||||||
|
PlaceholderText="PLZ" FontSize="12"/>
|
||||||
|
<TextBox Grid.Column="2" Text="{Binding City}"
|
||||||
|
PlaceholderText="Ort" FontSize="12"/>
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
|
||||||
|
<TextBlock Text="Noch keine Kontakte. Über + Kontakt hinzufügen."
|
||||||
|
Opacity="0.35" FontSize="12"
|
||||||
|
IsVisible="{Binding !Contacts.Count}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Notizen -->
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBlock Text="Notizen" FontSize="12" Opacity="0.7"/>
|
||||||
|
<TextBox Text="{Binding Notes}" PlaceholderText="Interne Anmerkungen …"
|
||||||
|
AcceptsReturn="True" Height="70" TextWrapping="Wrap"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Validierungsfehler -->
|
||||||
|
<TextBlock Text="{Binding ValidationMessage}" Foreground="Red" FontSize="12"
|
||||||
|
IsVisible="{Binding ValidationMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
<!-- Buttons -->
|
||||||
|
<Grid Grid.Row="1" ColumnDefinitions="*,10,*" Margin="0,16,0,0">
|
||||||
|
<Button Grid.Column="0" Content="Abbrechen" HorizontalAlignment="Stretch" Click="OnCancel"/>
|
||||||
|
<Button Grid.Column="2" Content="Anlegen" HorizontalAlignment="Stretch" Click="OnSave"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Students;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Views.Students;
|
||||||
|
|
||||||
|
public partial class AddStudentDialog : Window
|
||||||
|
{
|
||||||
|
public AddStudentDialog() => InitializeComponent();
|
||||||
|
|
||||||
|
protected override void OnOpened(EventArgs e)
|
||||||
|
{
|
||||||
|
base.OnOpened(e);
|
||||||
|
this.FindControl<TextBox>("LastNameBox")?.Focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSave(object? s, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is AddStudentDialogViewModel vm && vm.SaveCommand.CanExecute(null))
|
||||||
|
{
|
||||||
|
vm.SaveCommand.Execute(null);
|
||||||
|
if (vm.Result is not null) Close(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCancel(object? s, RoutedEventArgs e) => Close(false);
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Students"
|
||||||
|
x:Class="LehrerApp.Desktop.Views.Students.AddressViewerWindow"
|
||||||
|
x:DataType="vm:ContactItem"
|
||||||
|
Title="Adresse"
|
||||||
|
Width="340" Height="230"
|
||||||
|
MinWidth="280" MinHeight="180"
|
||||||
|
CanResize="True"
|
||||||
|
ShowInTaskbar="False"
|
||||||
|
WindowStartupLocation="Manual">
|
||||||
|
|
||||||
|
<Grid RowDefinitions="Auto,*,Auto" Margin="18">
|
||||||
|
<StackPanel Grid.Row="0" Spacing="3">
|
||||||
|
<TextBlock Text="{Binding Name}" FontSize="16" FontWeight="SemiBold"/>
|
||||||
|
<TextBlock Text="{Binding Relation}" FontSize="12" Opacity="0.55"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Border Grid.Row="1" Margin="0,14" Padding="12"
|
||||||
|
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||||
|
CornerRadius="7">
|
||||||
|
<TextBox Text="{Binding Address}" IsReadOnly="True"
|
||||||
|
AcceptsReturn="True" TextWrapping="Wrap"
|
||||||
|
BorderThickness="0" Background="Transparent"
|
||||||
|
VerticalContentAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2"
|
||||||
|
Text="Das Fenster kann verschoben und parallel weiter geöffnet bleiben."
|
||||||
|
FontSize="10" Opacity="0.45" TextWrapping="Wrap"/>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Views.Students;
|
||||||
|
|
||||||
|
public partial class AddressViewerWindow : Window
|
||||||
|
{
|
||||||
|
public AddressViewerWindow() => InitializeComponent();
|
||||||
|
|
||||||
|
protected override void OnOpened(EventArgs e)
|
||||||
|
{
|
||||||
|
base.OnOpened(e);
|
||||||
|
if (Owner is not Window owner) return;
|
||||||
|
Position = new PixelPoint(
|
||||||
|
owner.Position.X + Math.Max(20, (int)owner.Bounds.Width - (int)Width - 24),
|
||||||
|
owner.Position.Y + Math.Max(20, (int)owner.Bounds.Height - (int)Height - 48));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Students"
|
||||||
|
x:Class="LehrerApp.Desktop.Views.Students.ContactEditDialog"
|
||||||
|
x:DataType="vm:ContactEditDialogViewModel"
|
||||||
|
Title="{Binding DialogTitle}"
|
||||||
|
Width="520" Height="650"
|
||||||
|
MinWidth="460" MinHeight="560"
|
||||||
|
CanResize="True" WindowStartupLocation="CenterOwner">
|
||||||
|
|
||||||
|
<Grid RowDefinitions="*,Auto" Margin="24,20">
|
||||||
|
<ScrollViewer Grid.Row="0" VerticalScrollBarVisibility="Auto">
|
||||||
|
<StackPanel Spacing="16">
|
||||||
|
<TextBlock Text="{Binding DialogTitle}" FontSize="18" FontWeight="SemiBold"/>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*,10,*">
|
||||||
|
<StackPanel Grid.Column="0" Spacing="4">
|
||||||
|
<TextBlock Text="Name *" FontSize="12" Opacity="0.7"/>
|
||||||
|
<TextBox x:Name="NameBox" Text="{Binding Name}" PlaceholderText="Name"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Column="2" Spacing="4">
|
||||||
|
<TextBlock Text="Beziehung" FontSize="12" Opacity="0.7"/>
|
||||||
|
<ComboBox ItemsSource="{Binding RelationPresets}"
|
||||||
|
SelectedItem="{Binding Relation}"
|
||||||
|
HorizontalAlignment="Stretch"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*,10,*">
|
||||||
|
<StackPanel Grid.Column="0" Spacing="4">
|
||||||
|
<TextBlock Text="Telefon" FontSize="12" Opacity="0.7"/>
|
||||||
|
<TextBox Text="{Binding Phone}" PlaceholderText="Telefon / Handy"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Column="2" Spacing="4">
|
||||||
|
<TextBlock Text="E-Mail" FontSize="12" Opacity="0.7"/>
|
||||||
|
<TextBox Text="{Binding Email}" PlaceholderText="E-Mail"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<StackPanel Spacing="8">
|
||||||
|
<TextBlock Text="Adresse" FontSize="14" FontWeight="SemiBold"/>
|
||||||
|
<TextBox Text="{Binding Street}" PlaceholderText="Straße und Hausnummer"/>
|
||||||
|
<Grid ColumnDefinitions="110,10,*">
|
||||||
|
<TextBox Grid.Column="0" Text="{Binding PostalCode}" PlaceholderText="PLZ"/>
|
||||||
|
<TextBox Grid.Column="2" Text="{Binding City}" PlaceholderText="Ort"/>
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Separator/>
|
||||||
|
|
||||||
|
<CheckBox Content="Dieser Kontakt gilt nicht mehr"
|
||||||
|
IsChecked="{Binding IsInvalid}"/>
|
||||||
|
|
||||||
|
<StackPanel Spacing="10" IsVisible="{Binding IsInvalid}">
|
||||||
|
<Grid ColumnDefinitions="*,10,*">
|
||||||
|
<StackPanel Grid.Column="0" Spacing="4">
|
||||||
|
<TextBlock Text="Ungültig seit *" FontSize="12" Opacity="0.7"/>
|
||||||
|
<TextBox Text="{Binding InvalidSinceText}" PlaceholderText="TT.MM.JJJJ"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Column="2" Spacing="4">
|
||||||
|
<TextBlock Text="Grund" FontSize="12" Opacity="0.7"/>
|
||||||
|
<ComboBox ItemsSource="{Binding InvalidReasonOptions}"
|
||||||
|
SelectedItem="{Binding SelectedInvalidReason}"
|
||||||
|
HorizontalAlignment="Stretch"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBlock Text="Ergänzung zum Grund" FontSize="12" Opacity="0.7"/>
|
||||||
|
<TextBox Text="{Binding InvalidReasonDetails}"
|
||||||
|
PlaceholderText="Optionaler Hinweis"
|
||||||
|
AcceptsReturn="True" Height="60" TextWrapping="Wrap"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<TextBlock Text="{Binding ValidationMessage}" Foreground="Red" FontSize="12"
|
||||||
|
IsVisible="{Binding ValidationMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
<Grid Grid.Row="1" ColumnDefinitions="*,10,*" Margin="0,18,0,0">
|
||||||
|
<Button Grid.Column="0" Content="Abbrechen" HorizontalAlignment="Stretch"
|
||||||
|
Click="OnCancel"/>
|
||||||
|
<Button Grid.Column="2" Content="Speichern" HorizontalAlignment="Stretch"
|
||||||
|
Click="OnSave"/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Students;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Views.Students;
|
||||||
|
|
||||||
|
public partial class ContactEditDialog : Window
|
||||||
|
{
|
||||||
|
public ContactEditDialog() => InitializeComponent();
|
||||||
|
|
||||||
|
protected override void OnOpened(EventArgs e)
|
||||||
|
{
|
||||||
|
base.OnOpened(e);
|
||||||
|
this.FindControl<TextBox>("NameBox")?.Focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSave(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is not ContactEditDialogViewModel vm) return;
|
||||||
|
vm.SaveCommand.Execute(null);
|
||||||
|
if (vm.Result is not null) Close(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCancel(object? sender, RoutedEventArgs e) => Close(false);
|
||||||
|
}
|
||||||
@@ -60,6 +60,74 @@
|
|||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</ContentPage>
|
</ContentPage>
|
||||||
|
|
||||||
|
<ContentPage Header="Kontakte">
|
||||||
|
<Grid RowDefinitions="Auto,*" Margin="20">
|
||||||
|
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto,Auto" Margin="0,0,0,10">
|
||||||
|
<TextBlock Grid.Column="0" Text="Kontaktdaten" FontSize="15"
|
||||||
|
FontWeight="SemiBold" VerticalAlignment="Center"/>
|
||||||
|
<Button Grid.Column="1" Content="+ Neu" Command="{Binding AddContactCommand}"
|
||||||
|
Margin="0,0,8,0"/>
|
||||||
|
<Button Grid.Column="2" Content="Bearbeiten" Command="{Binding EditContactCommand}"
|
||||||
|
Margin="0,0,8,0"/>
|
||||||
|
<Button Grid.Column="3" Content="Adresse anzeigen"
|
||||||
|
Command="{Binding ViewSelectedAddressCommand}"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<ListBox Grid.Row="1" x:Name="ContactList"
|
||||||
|
ItemsSource="{Binding Contacts}"
|
||||||
|
SelectedItem="{Binding SelectedContact}"
|
||||||
|
DoubleTapped="OnContactDoubleTapped">
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:ContactItem">
|
||||||
|
<Border Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||||
|
CornerRadius="8" Padding="14,10" Margin="2,3">
|
||||||
|
<Grid ColumnDefinitions="220,*,Auto" RowDefinitions="Auto,Auto">
|
||||||
|
<StackPanel Grid.Column="0" Grid.RowSpan="2" Spacing="4">
|
||||||
|
<TextBlock Text="{Binding Name}" FontWeight="SemiBold" FontSize="14"
|
||||||
|
TextWrapping="Wrap"/>
|
||||||
|
<Border Background="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||||
|
CornerRadius="4" Padding="6,2" HorizontalAlignment="Left">
|
||||||
|
<TextBlock Text="{Binding Relation}" FontSize="11" Opacity="0.7"/>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="1" Grid.Row="0" Orientation="Horizontal" Spacing="8">
|
||||||
|
<Button Content="{Binding PhoneDisplay}"
|
||||||
|
Command="{Binding CallCommand}"
|
||||||
|
IsVisible="{Binding HasPhone}"
|
||||||
|
FontSize="12" Padding="8,4"/>
|
||||||
|
<Button Content="{Binding EmailDisplay}"
|
||||||
|
Command="{Binding MailCommand}"
|
||||||
|
IsVisible="{Binding HasEmail}"
|
||||||
|
FontSize="12" Padding="8,4"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<TextBlock Grid.Column="1" Grid.Row="1" Text="{Binding Address}"
|
||||||
|
IsVisible="{Binding HasAddress}" FontSize="12" Opacity="0.65"
|
||||||
|
TextWrapping="Wrap" Margin="4,4,12,0"/>
|
||||||
|
|
||||||
|
<Border Grid.Column="2" Grid.RowSpan="2" CornerRadius="4" Padding="7,3"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
Background="{DynamicResource SystemControlBackgroundAltHighBrush}">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="{Binding StatusText}" FontSize="11"
|
||||||
|
IsVisible="{Binding IsInvalid}" Foreground="IndianRed"/>
|
||||||
|
<TextBlock Text="Aktuell" FontSize="11" Opacity="0.5"
|
||||||
|
IsVisible="{Binding IsValid}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
</ListBox>
|
||||||
|
|
||||||
|
<TextBlock Text="Keine Kontakte erfasst." Opacity="0.4"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||||
|
IsVisible="{Binding HasNoContacts}"/>
|
||||||
|
</Grid>
|
||||||
|
</ContentPage>
|
||||||
|
|
||||||
<ContentPage Header="Noten">
|
<ContentPage Header="Noten">
|
||||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||||
<TextBlock Text="Notenübersicht" FontSize="16" Opacity="0.4" HorizontalAlignment="Center"/>
|
<TextBlock Text="Notenübersicht" FontSize="16" Opacity="0.4" HorizontalAlignment="Center"/>
|
||||||
|
|||||||
@@ -1,3 +1,48 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Input;
|
||||||
|
using LehrerApp.Core.Models;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Students;
|
||||||
|
|
||||||
namespace LehrerApp.Desktop.Views.Students;
|
namespace LehrerApp.Desktop.Views.Students;
|
||||||
public partial class StudentDetailView : UserControl { public StudentDetailView() => InitializeComponent(); }
|
|
||||||
|
public partial class StudentDetailView : UserControl
|
||||||
|
{
|
||||||
|
public StudentDetailView() => InitializeComponent();
|
||||||
|
|
||||||
|
protected override void OnDataContextChanged(EventArgs e)
|
||||||
|
{
|
||||||
|
base.OnDataContextChanged(e);
|
||||||
|
if (DataContext is not StudentDetailViewModel vm) return;
|
||||||
|
vm.OnEditContact = ShowContactDialog;
|
||||||
|
vm.OnViewAddress = ShowAddressViewer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Contact?> ShowContactDialog(Contact? contact)
|
||||||
|
{
|
||||||
|
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||||
|
if (owner is null) return null;
|
||||||
|
|
||||||
|
var vm = new ContactEditDialogViewModel(contact);
|
||||||
|
var dialog = new ContactEditDialog { DataContext = vm };
|
||||||
|
var saved = await dialog.ShowDialog<bool>(owner);
|
||||||
|
return saved ? vm.Result : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowAddressViewer(ContactItem contact)
|
||||||
|
{
|
||||||
|
if (!contact.HasAddress) return;
|
||||||
|
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||||
|
var viewer = new AddressViewerWindow { DataContext = contact };
|
||||||
|
if (owner is not null)
|
||||||
|
viewer.Show(owner);
|
||||||
|
else
|
||||||
|
viewer.Show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnContactDoubleTapped(object? sender, TappedEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is StudentDetailViewModel vm &&
|
||||||
|
vm.ViewSelectedAddressCommand.CanExecute(null))
|
||||||
|
vm.ViewSelectedAddressCommand.Execute(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,16 +1,8 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using LehrerApp.Desktop.ViewModels;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
namespace LehrerApp.Desktop.Views;
|
namespace LehrerApp.Desktop.Views;
|
||||||
|
|
||||||
public partial class SyncStatusBar : UserControl
|
public partial class SyncStatusBar : UserControl
|
||||||
{
|
{
|
||||||
public SyncStatusBar()
|
public SyncStatusBar() => InitializeComponent();
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
Loaded += (_, _) =>
|
|
||||||
{
|
|
||||||
if (DataContext is null)
|
|
||||||
DataContext = App.Services.GetRequiredService<SyncStatusViewModel>();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user