Arbeitszeit: Auswertung nach Kategorie/Gruppe + Pflichtstunden-Abgleich (Kapitel 6.3)
Dritter Tab "Auswertung": Zeitraum wahlweise Monat oder Schuljahr, Balken je Kategorie und Gruppe. Neuer WorkloadSettingsService (gleiches JSON-Muster wie PeriodScheduleService) für die Pflichtstundenzahl pro Woche, bewusst direkt im Auswertungs-Tab editierbar statt in den Einstellungen, da das Feld nur dort gebraucht wird. Export (6.3.3) bleibt offen, da er an das noch fehlende Kapitel 11 (Export-Infrastruktur) hängt.
This commit is contained in:
@@ -0,0 +1,41 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace LehrerApp.Core.Services;
|
||||||
|
|
||||||
|
internal class WorkloadSettingsConfig
|
||||||
|
{
|
||||||
|
public double RequiredWeeklyHours { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Pflichtstundenzahl pro Woche für den Ist-Soll-Abgleich in der Arbeitszeitauswertung (6.3.2).</summary>
|
||||||
|
public class WorkloadSettingsService
|
||||||
|
{
|
||||||
|
private readonly string _configPath;
|
||||||
|
private WorkloadSettingsConfig _config;
|
||||||
|
|
||||||
|
public double RequiredWeeklyHours => _config.RequiredWeeklyHours;
|
||||||
|
|
||||||
|
public WorkloadSettingsService(string appDataPath)
|
||||||
|
{
|
||||||
|
_configPath = Path.Combine(appDataPath, "workloadsettings.json");
|
||||||
|
_config = Load();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetRequiredWeeklyHours(double hours)
|
||||||
|
{
|
||||||
|
_config.RequiredWeeklyHours = hours;
|
||||||
|
File.WriteAllText(_configPath, JsonSerializer.Serialize(_config));
|
||||||
|
}
|
||||||
|
|
||||||
|
private WorkloadSettingsConfig Load()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(_configPath))
|
||||||
|
return JsonSerializer.Deserialize<WorkloadSettingsConfig>(File.ReadAllText(_configPath))
|
||||||
|
?? new WorkloadSettingsConfig();
|
||||||
|
}
|
||||||
|
catch { /* beschädigte Konfiguration -> Standardwert */ }
|
||||||
|
return new WorkloadSettingsConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using LehrerApp.Core.Models;
|
using LehrerApp.Core.Models;
|
||||||
|
using LehrerApp.Core.Services;
|
||||||
using LehrerApp.Desktop.ViewModels.Workload;
|
using LehrerApp.Desktop.ViewModels.Workload;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
@@ -346,3 +347,102 @@ public sealed class AddTimeEntryDialogViewModelTests
|
|||||||
Assert.NotEmpty(vm.TimeError);
|
Assert.NotEmpty(vm.TimeError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class WorkloadEvaluationViewModelTests
|
||||||
|
{
|
||||||
|
private static WorkloadSettingsService BuildSettingsService()
|
||||||
|
{
|
||||||
|
var tempPath = System.IO.Path.Combine(
|
||||||
|
System.IO.Path.GetTempPath(), $"lehrerapp-workloadsettings-tests-{Guid.NewGuid():N}");
|
||||||
|
Directory.CreateDirectory(tempPath);
|
||||||
|
return new WorkloadSettingsService(tempPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MonatModus_ZeigtNurEintraegeDesGewaehltenMonats()
|
||||||
|
{
|
||||||
|
var entries = new FakeTimeEntries();
|
||||||
|
entries.Add(new TimeEntry { Category = "Korrektur", Date = new DateOnly(2026, 3, 10), DurationMinutes = 30 });
|
||||||
|
entries.Add(new TimeEntry { Category = "Korrektur", Date = new DateOnly(2026, 4, 1), DurationMinutes = 45 });
|
||||||
|
|
||||||
|
var vm = new WorkloadEvaluationViewModel(entries, new FakeGroups([]), BuildSettingsService(), new SchoolYearService())
|
||||||
|
{
|
||||||
|
SelectedYear = 2026,
|
||||||
|
};
|
||||||
|
vm.SelectedMonth = vm.MonthOptions[2]; // März
|
||||||
|
|
||||||
|
Assert.Equal("30 min (0.5 h)", vm.TotalMinutesDisplay);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CategorySummaries_GruppiertProKategorie()
|
||||||
|
{
|
||||||
|
var entries = new FakeTimeEntries();
|
||||||
|
entries.Add(new TimeEntry { Category = "Korrektur", Date = new DateOnly(2026, 3, 5), DurationMinutes = 30 });
|
||||||
|
entries.Add(new TimeEntry { Category = "Verwaltung", Date = new DateOnly(2026, 3, 6), DurationMinutes = 20 });
|
||||||
|
|
||||||
|
var vm = new WorkloadEvaluationViewModel(entries, new FakeGroups([]), BuildSettingsService(), new SchoolYearService())
|
||||||
|
{
|
||||||
|
SelectedYear = 2026,
|
||||||
|
};
|
||||||
|
vm.SelectedMonth = vm.MonthOptions[2];
|
||||||
|
|
||||||
|
Assert.Equal(2, vm.CategorySummaries.Count);
|
||||||
|
Assert.Contains(vm.CategorySummaries, s => s.Category == "Korrektur" && s.MinutesDisplay == "30 min");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GroupSummaries_ZeigtNurEintraegeMitGruppe()
|
||||||
|
{
|
||||||
|
var group = new LearningGroup { Name = "8a" };
|
||||||
|
var entries = new FakeTimeEntries();
|
||||||
|
entries.Add(new TimeEntry { Category = "X", GroupId = group.Id, Date = new DateOnly(2026, 3, 5), DurationMinutes = 30 });
|
||||||
|
entries.Add(new TimeEntry { Category = "X", GroupId = null, Date = new DateOnly(2026, 3, 6), DurationMinutes = 20 });
|
||||||
|
|
||||||
|
var vm = new WorkloadEvaluationViewModel(entries, new FakeGroups([group]), BuildSettingsService(), new SchoolYearService())
|
||||||
|
{
|
||||||
|
SelectedYear = 2026,
|
||||||
|
};
|
||||||
|
vm.SelectedMonth = vm.MonthOptions[2];
|
||||||
|
|
||||||
|
var summary = Assert.Single(vm.GroupSummaries);
|
||||||
|
Assert.Equal("8a", summary.GroupName);
|
||||||
|
Assert.Equal("30 min", summary.MinutesDisplay);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SchuljahrModus_UmfasstAugustBisJuli()
|
||||||
|
{
|
||||||
|
var entries = new FakeTimeEntries();
|
||||||
|
entries.Add(new TimeEntry { Category = "X", Date = new DateOnly(2025, 9, 1), DurationMinutes = 30 }); // im Schuljahr 2025/26
|
||||||
|
entries.Add(new TimeEntry { Category = "X", Date = new DateOnly(2026, 7, 30), DurationMinutes = 15 }); // im Schuljahr 2025/26
|
||||||
|
entries.Add(new TimeEntry { Category = "X", Date = new DateOnly(2026, 8, 5), DurationMinutes = 99 }); // schon Schuljahr 2026/27
|
||||||
|
|
||||||
|
var vm = new WorkloadEvaluationViewModel(entries, new FakeGroups([]), BuildSettingsService(), new SchoolYearService())
|
||||||
|
{
|
||||||
|
SelectedYear = 2025,
|
||||||
|
PeriodMode = WorkloadEvaluationViewModel.SchoolYearMode,
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Equal("45 min (0.8 h)", vm.TotalMinutesDisplay);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SaveRequiredWeeklyHours_AktualisiertSollIstVergleich()
|
||||||
|
{
|
||||||
|
var entries = new FakeTimeEntries();
|
||||||
|
entries.Add(new TimeEntry { Category = "X", Date = new DateOnly(2026, 3, 2), DurationMinutes = 60 });
|
||||||
|
|
||||||
|
var vm = new WorkloadEvaluationViewModel(entries, new FakeGroups([]), BuildSettingsService(), new SchoolYearService())
|
||||||
|
{
|
||||||
|
SelectedYear = 2026,
|
||||||
|
RequiredWeeklyHoursText = "10",
|
||||||
|
};
|
||||||
|
vm.SelectedMonth = vm.MonthOptions[2];
|
||||||
|
|
||||||
|
vm.SaveRequiredWeeklyHoursCommand.Execute(null);
|
||||||
|
|
||||||
|
Assert.Contains("Soll:", vm.RequiredVsActualDisplay);
|
||||||
|
Assert.Contains("Ist: 1 h", vm.RequiredVsActualDisplay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ public static class AppBootstrapper
|
|||||||
services.AddSingleton<PublicHolidayService>();
|
services.AddSingleton<PublicHolidayService>();
|
||||||
services.AddSingleton(_ => new SchoolCalendarSettingsService(appData));
|
services.AddSingleton(_ => new SchoolCalendarSettingsService(appData));
|
||||||
services.AddSingleton(_ => new PeriodScheduleService(appData));
|
services.AddSingleton(_ => new PeriodScheduleService(appData));
|
||||||
|
services.AddSingleton(_ => new WorkloadSettingsService(appData));
|
||||||
|
|
||||||
// ── Sync (optional – nur wenn Server konfiguriert) ────────────────────
|
// ── Sync (optional – nur wenn Server konfiguriert) ────────────────────
|
||||||
services.AddSingleton(_ => new EventQueue(queuePath));
|
services.AddSingleton(_ => new EventQueue(queuePath));
|
||||||
@@ -190,6 +191,7 @@ public static class AppBootstrapper
|
|||||||
services.AddSingleton<TimetableViewModel>();
|
services.AddSingleton<TimetableViewModel>();
|
||||||
services.AddSingleton<WorkTaskListViewModel>();
|
services.AddSingleton<WorkTaskListViewModel>();
|
||||||
services.AddSingleton<TimeTrackingViewModel>();
|
services.AddSingleton<TimeTrackingViewModel>();
|
||||||
|
services.AddSingleton<WorkloadEvaluationViewModel>();
|
||||||
services.AddSingleton<WorkloadViewModel>();
|
services.AddSingleton<WorkloadViewModel>();
|
||||||
|
|
||||||
// Transient: neue Instanz pro Navigation (für Detailseiten)
|
// Transient: neue Instanz pro Navigation (für Detailseiten)
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ public partial class MainWindowViewModel : ObservableObject
|
|||||||
var workload = _services.GetRequiredService<WorkloadViewModel>();
|
var workload = _services.GetRequiredService<WorkloadViewModel>();
|
||||||
workload.Tasks.Load();
|
workload.Tasks.Load();
|
||||||
workload.TimeTracking.Load();
|
workload.TimeTracking.Load();
|
||||||
|
workload.Evaluation.Load();
|
||||||
workload.ActiveTabIndex = 0;
|
workload.ActiveTabIndex = 0;
|
||||||
return workload;
|
return workload;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
|||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using LehrerApp.Core.Interfaces;
|
using LehrerApp.Core.Interfaces;
|
||||||
using LehrerApp.Core.Models;
|
using LehrerApp.Core.Models;
|
||||||
|
using LehrerApp.Core.Services;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
|
||||||
@@ -299,7 +300,7 @@ public partial class TimeTrackingViewModel : ObservableObject
|
|||||||
: "";
|
: "";
|
||||||
|
|
||||||
public ObservableCollection<TimeEntryListItem> WeekEntries { get; } = [];
|
public ObservableCollection<TimeEntryListItem> WeekEntries { get; } = [];
|
||||||
public ObservableCollection<CategoryWeekSummary> CategorySummaries { get; } = [];
|
public ObservableCollection<CategoryTimeSummary> CategorySummaries { get; } = [];
|
||||||
public string TotalWeekMinutesDisplay => $"{WeekEntries.Sum(e => e.Model.DurationMinutes)} min diese Woche";
|
public string TotalWeekMinutesDisplay => $"{WeekEntries.Sum(e => e.Model.DurationMinutes)} min diese Woche";
|
||||||
|
|
||||||
public Func<Task<TimeEntry?>>? OnAddEntry { get; set; }
|
public Func<Task<TimeEntry?>>? OnAddEntry { get; set; }
|
||||||
@@ -343,7 +344,7 @@ public partial class TimeTrackingViewModel : ObservableObject
|
|||||||
foreach (var group in weekEntries.GroupBy(e => e.Category).OrderByDescending(g => g.Sum(e => e.DurationMinutes)))
|
foreach (var group in weekEntries.GroupBy(e => e.Category).OrderByDescending(g => g.Sum(e => e.DurationMinutes)))
|
||||||
{
|
{
|
||||||
var minutes = group.Sum(e => e.DurationMinutes);
|
var minutes = group.Sum(e => e.DurationMinutes);
|
||||||
CategorySummaries.Add(new CategoryWeekSummary(
|
CategorySummaries.Add(new CategoryTimeSummary(
|
||||||
string.IsNullOrWhiteSpace(group.Key) ? "Ohne Kategorie" : group.Key,
|
string.IsNullOrWhiteSpace(group.Key) ? "Ohne Kategorie" : group.Key,
|
||||||
minutes, maxMinutes > 0 ? minutes / (double)maxMinutes : 0));
|
minutes, maxMinutes > 0 ? minutes / (double)maxMinutes : 0));
|
||||||
}
|
}
|
||||||
@@ -413,7 +414,7 @@ public class TimeEntryListItem(TimeEntry model, string? taskTitle)
|
|||||||
public string Description => Model.Description ?? "";
|
public string Description => Model.Description ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CategoryWeekSummary(string category, int minutes, double barFraction)
|
public class CategoryTimeSummary(string category, int minutes, double barFraction)
|
||||||
{
|
{
|
||||||
public string Category { get; } = category;
|
public string Category { get; } = category;
|
||||||
public string MinutesDisplay { get; } = $"{minutes} min";
|
public string MinutesDisplay { get; } = $"{minutes} min";
|
||||||
@@ -489,12 +490,154 @@ public partial class AddTimeEntryDialogViewModel : ObservableObject
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Container: Arbeitszeit-Seite mit Tabs "Aufgaben"/"Zeiterfassung" ─────────
|
// ── Auswertung (6.3) ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
public partial class WorkloadViewModel(WorkTaskListViewModel tasks, TimeTrackingViewModel timeTracking) : ObservableObject
|
public partial class WorkloadEvaluationViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
private readonly ITimeEntryRepository _entries;
|
||||||
|
private readonly IGroupRepository _groups;
|
||||||
|
private readonly WorkloadSettingsService _workloadSettings;
|
||||||
|
private readonly SchoolYearService _schoolYear;
|
||||||
|
|
||||||
|
public const string MonthMode = "Monat";
|
||||||
|
public const string SchoolYearMode = "Schuljahr";
|
||||||
|
|
||||||
|
[ObservableProperty] private string _periodMode = MonthMode;
|
||||||
|
[ObservableProperty] private string _selectedMonth;
|
||||||
|
[ObservableProperty] private int _selectedYear = DateTime.Today.Year;
|
||||||
|
[ObservableProperty] private string _requiredWeeklyHoursText = "";
|
||||||
|
|
||||||
|
public List<string> PeriodModeOptions { get; } = [MonthMode, SchoolYearMode];
|
||||||
|
public bool IsMonthMode => PeriodMode == MonthMode;
|
||||||
|
public List<string> MonthOptions { get; } = Enumerable.Range(1, 12)
|
||||||
|
.Select(m => CultureInfo.GetCultureInfo("de-DE").DateTimeFormat.GetMonthName(m)).ToList();
|
||||||
|
public List<int> YearOptions { get; } = Enumerable.Range(DateTime.Today.Year - 4, 5).Reverse().ToList();
|
||||||
|
|
||||||
|
public ObservableCollection<CategoryTimeSummary> CategorySummaries { get; } = [];
|
||||||
|
public ObservableCollection<GroupTimeSummary> GroupSummaries { get; } = [];
|
||||||
|
public string TotalMinutesDisplay { get; private set; } = "0 min";
|
||||||
|
public string RequiredVsActualDisplay { get; private set; } = "";
|
||||||
|
|
||||||
|
public WorkloadEvaluationViewModel(ITimeEntryRepository entries, IGroupRepository groups,
|
||||||
|
WorkloadSettingsService workloadSettings, SchoolYearService schoolYear)
|
||||||
|
{
|
||||||
|
_entries = entries;
|
||||||
|
_groups = groups;
|
||||||
|
_workloadSettings = workloadSettings;
|
||||||
|
_schoolYear = schoolYear;
|
||||||
|
_selectedMonth = MonthOptions[DateTime.Today.Month - 1];
|
||||||
|
Load();
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnPeriodModeChanged(string value)
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(IsMonthMode));
|
||||||
|
Refresh();
|
||||||
|
}
|
||||||
|
partial void OnSelectedMonthChanged(string value) => Refresh();
|
||||||
|
partial void OnSelectedYearChanged(int value) => Refresh();
|
||||||
|
|
||||||
|
public void Load()
|
||||||
|
{
|
||||||
|
RequiredWeeklyHoursText = _workloadSettings.RequiredWeeklyHours > 0
|
||||||
|
? _workloadSettings.RequiredWeeklyHours.ToString(CultureInfo.InvariantCulture) : "";
|
||||||
|
Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void SaveRequiredWeeklyHours()
|
||||||
|
{
|
||||||
|
if (double.TryParse(RequiredWeeklyHoursText, NumberStyles.Number, CultureInfo.InvariantCulture, out var hours) && hours >= 0)
|
||||||
|
_workloadSettings.SetRequiredWeeklyHours(hours);
|
||||||
|
Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
private (DateOnly From, DateOnly To) CurrentPeriod()
|
||||||
|
{
|
||||||
|
if (PeriodMode == SchoolYearMode)
|
||||||
|
{
|
||||||
|
// SelectedYear ist hier das Startjahr des Schuljahres (z.B. 2025 -> Schuljahr 2025/26).
|
||||||
|
var sy = _schoolYear.FormatSchoolYear(SelectedYear);
|
||||||
|
return (_schoolYear.SchoolYearStart(sy), _schoolYear.SchoolYearEnd(sy));
|
||||||
|
}
|
||||||
|
|
||||||
|
var month = MonthOptions.IndexOf(SelectedMonth) + 1;
|
||||||
|
var from = new DateOnly(SelectedYear, month, 1);
|
||||||
|
return (from, from.AddMonths(1).AddDays(-1));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Refresh()
|
||||||
|
{
|
||||||
|
var (from, to) = CurrentPeriod();
|
||||||
|
var periodEntries = _entries.GetByDateRange(from, to);
|
||||||
|
var groupNames = _groups.GetAll(includeInactive: true).ToDictionary(g => g.Id, g => g.Name);
|
||||||
|
|
||||||
|
CategorySummaries.Clear();
|
||||||
|
var maxCategoryMinutes = periodEntries.Count > 0
|
||||||
|
? periodEntries.GroupBy(e => e.Category).Max(g => g.Sum(e => e.DurationMinutes)) : 0;
|
||||||
|
foreach (var group in periodEntries.GroupBy(e => e.Category).OrderByDescending(g => g.Sum(e => e.DurationMinutes)))
|
||||||
|
{
|
||||||
|
var minutes = group.Sum(e => e.DurationMinutes);
|
||||||
|
CategorySummaries.Add(new CategoryTimeSummary(
|
||||||
|
string.IsNullOrWhiteSpace(group.Key) ? "Ohne Kategorie" : group.Key,
|
||||||
|
minutes, maxCategoryMinutes > 0 ? minutes / (double)maxCategoryMinutes : 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
GroupSummaries.Clear();
|
||||||
|
var withGroup = periodEntries.Where(e => e.GroupId.HasValue).ToList();
|
||||||
|
var maxGroupMinutes = withGroup.Count > 0
|
||||||
|
? withGroup.GroupBy(e => e.GroupId).Max(g => g.Sum(e => e.DurationMinutes)) : 0;
|
||||||
|
foreach (var group in withGroup.GroupBy(e => e.GroupId).OrderByDescending(g => g.Sum(e => e.DurationMinutes)))
|
||||||
|
{
|
||||||
|
var minutes = group.Sum(e => e.DurationMinutes);
|
||||||
|
GroupSummaries.Add(new GroupTimeSummary(
|
||||||
|
groupNames.GetValueOrDefault(group.Key!.Value, "Unbekannte Gruppe"),
|
||||||
|
minutes, maxGroupMinutes > 0 ? minutes / (double)maxGroupMinutes : 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalMinutes = periodEntries.Sum(e => e.DurationMinutes);
|
||||||
|
TotalMinutesDisplay = $"{totalMinutes} min ({(totalMinutes / 60.0).ToString("0.#", CultureInfo.InvariantCulture)} h)";
|
||||||
|
|
||||||
|
// Pflichtstunden-Abgleich (6.3.2): auf die Anzahl Wochen im Zeitraum hochgerechnet.
|
||||||
|
if (_workloadSettings.RequiredWeeklyHours > 0)
|
||||||
|
{
|
||||||
|
var weeks = (to.DayNumber - from.DayNumber + 1) / 7.0;
|
||||||
|
var requiredHours = _workloadSettings.RequiredWeeklyHours * weeks;
|
||||||
|
var actualHours = totalMinutes / 60.0;
|
||||||
|
var diff = actualHours - requiredHours;
|
||||||
|
var diffText = diff >= 0
|
||||||
|
? $"+{diff.ToString("0.#", CultureInfo.InvariantCulture)} h"
|
||||||
|
: $"{diff.ToString("0.#", CultureInfo.InvariantCulture)} h";
|
||||||
|
RequiredVsActualDisplay =
|
||||||
|
$"Soll: {requiredHours.ToString("0.#", CultureInfo.InvariantCulture)} h · " +
|
||||||
|
$"Ist: {actualHours.ToString("0.#", CultureInfo.InvariantCulture)} h · {diffText}";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
RequiredVsActualDisplay = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
OnPropertyChanged(nameof(TotalMinutesDisplay));
|
||||||
|
OnPropertyChanged(nameof(RequiredVsActualDisplay));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GroupTimeSummary(string groupName, int minutes, double barFraction)
|
||||||
|
{
|
||||||
|
public string GroupName { get; } = groupName;
|
||||||
|
public string MinutesDisplay { get; } = $"{minutes} min";
|
||||||
|
public double BarFraction { get; } = barFraction;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Container: Arbeitszeit-Seite mit Tabs "Aufgaben"/"Zeiterfassung"/"Auswertung" ─
|
||||||
|
|
||||||
|
public partial class WorkloadViewModel(
|
||||||
|
WorkTaskListViewModel tasks, TimeTrackingViewModel timeTracking, WorkloadEvaluationViewModel evaluation)
|
||||||
|
: ObservableObject
|
||||||
{
|
{
|
||||||
[ObservableProperty] private int _activeTabIndex;
|
[ObservableProperty] private int _activeTabIndex;
|
||||||
|
|
||||||
public WorkTaskListViewModel Tasks { get; } = tasks;
|
public WorkTaskListViewModel Tasks { get; } = tasks;
|
||||||
public TimeTrackingViewModel TimeTracking { get; } = timeTracking;
|
public TimeTrackingViewModel TimeTracking { get; } = timeTracking;
|
||||||
|
public WorkloadEvaluationViewModel Evaluation { get; } = evaluation;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
<ItemsControl ItemsSource="{Binding CategorySummaries}">
|
<ItemsControl ItemsSource="{Binding CategorySummaries}">
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate x:DataType="vm:CategoryWeekSummary">
|
<DataTemplate x:DataType="vm:CategoryTimeSummary">
|
||||||
<Grid ColumnDefinitions="120,*,60" Margin="0,3">
|
<Grid ColumnDefinitions="120,*,60" Margin="0,3">
|
||||||
<TextBlock Grid.Column="0" Text="{Binding Category}" FontSize="12" VerticalAlignment="Center"/>
|
<TextBlock Grid.Column="0" Text="{Binding Category}" FontSize="12" VerticalAlignment="Center"/>
|
||||||
<ProgressBar Grid.Column="1" Value="{Binding BarFraction}" Maximum="1" Height="8"
|
<ProgressBar Grid.Column="1" Value="{Binding BarFraction}" Maximum="1" Height="8"
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Workload"
|
||||||
|
x:Class="LehrerApp.Desktop.Views.Workload.WorkloadEvaluationView"
|
||||||
|
x:DataType="vm:WorkloadEvaluationViewModel">
|
||||||
|
<ScrollViewer>
|
||||||
|
<StackPanel Margin="20,16" Spacing="16">
|
||||||
|
|
||||||
|
<!-- Zeitraum -->
|
||||||
|
<Grid ColumnDefinitions="Auto,Auto,Auto" HorizontalAlignment="Left">
|
||||||
|
<ComboBox Grid.Column="0" ItemsSource="{Binding PeriodModeOptions}"
|
||||||
|
SelectedItem="{Binding PeriodMode}" Margin="0,0,8,0"/>
|
||||||
|
<ComboBox Grid.Column="1" ItemsSource="{Binding MonthOptions}" SelectedItem="{Binding SelectedMonth}"
|
||||||
|
Margin="0,0,8,0" IsVisible="{Binding IsMonthMode}"/>
|
||||||
|
<ComboBox Grid.Column="2" ItemsSource="{Binding YearOptions}" SelectedItem="{Binding SelectedYear}"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Pflichtstunden (6.3.2) -->
|
||||||
|
<Border Background="{DynamicResource SystemControlBackgroundAltHighBrush}" CornerRadius="8" Padding="16">
|
||||||
|
<StackPanel Spacing="10">
|
||||||
|
<TextBlock Text="Pflichtstunden pro Woche" FontWeight="SemiBold" FontSize="14"/>
|
||||||
|
<Grid ColumnDefinitions="140,Auto">
|
||||||
|
<TextBox Grid.Column="0" Text="{Binding RequiredWeeklyHoursText}" PlaceholderText="z.B. 25"/>
|
||||||
|
<Button Grid.Column="1" Content="Speichern" Margin="8,0,0,0"
|
||||||
|
Command="{Binding SaveRequiredWeeklyHoursCommand}"/>
|
||||||
|
</Grid>
|
||||||
|
<TextBlock Text="{Binding RequiredVsActualDisplay}" FontSize="13" FontWeight="SemiBold"
|
||||||
|
IsVisible="{Binding RequiredVsActualDisplay, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Gesamtsumme -->
|
||||||
|
<TextBlock Text="{Binding TotalMinutesDisplay}" FontSize="18" FontWeight="SemiBold"/>
|
||||||
|
|
||||||
|
<!-- Nach Kategorie (6.3.1) -->
|
||||||
|
<Border Background="{DynamicResource SystemControlBackgroundAltHighBrush}" CornerRadius="8" Padding="16">
|
||||||
|
<StackPanel Spacing="10">
|
||||||
|
<TextBlock Text="Nach Kategorie" FontWeight="SemiBold" FontSize="14"/>
|
||||||
|
<ItemsControl ItemsSource="{Binding CategorySummaries}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:CategoryTimeSummary">
|
||||||
|
<Grid ColumnDefinitions="120,*,60" Margin="0,3">
|
||||||
|
<TextBlock Grid.Column="0" Text="{Binding Category}" FontSize="12" VerticalAlignment="Center"/>
|
||||||
|
<ProgressBar Grid.Column="1" Value="{Binding BarFraction}" Maximum="1" Height="8"
|
||||||
|
VerticalAlignment="Center" Margin="8,0"/>
|
||||||
|
<TextBlock Grid.Column="2" Text="{Binding MinutesDisplay}" FontSize="12"
|
||||||
|
HorizontalAlignment="Right" VerticalAlignment="Center"/>
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
<TextBlock Text="Keine Zeiten in diesem Zeitraum erfasst." Opacity="0.6" FontSize="12"
|
||||||
|
IsVisible="{Binding !CategorySummaries.Count}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Nach Gruppe (6.3.1) -->
|
||||||
|
<Border Background="{DynamicResource SystemControlBackgroundAltHighBrush}" CornerRadius="8" Padding="16">
|
||||||
|
<StackPanel Spacing="10">
|
||||||
|
<TextBlock Text="Nach Gruppe" FontWeight="SemiBold" FontSize="14"/>
|
||||||
|
<ItemsControl ItemsSource="{Binding GroupSummaries}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:GroupTimeSummary">
|
||||||
|
<Grid ColumnDefinitions="120,*,60" Margin="0,3">
|
||||||
|
<TextBlock Grid.Column="0" Text="{Binding GroupName}" FontSize="12" VerticalAlignment="Center"/>
|
||||||
|
<ProgressBar Grid.Column="1" Value="{Binding BarFraction}" Maximum="1" Height="8"
|
||||||
|
VerticalAlignment="Center" Margin="8,0"/>
|
||||||
|
<TextBlock Grid.Column="2" Text="{Binding MinutesDisplay}" FontSize="12"
|
||||||
|
HorizontalAlignment="Right" VerticalAlignment="Center"/>
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
<TextBlock Text="Keine gruppenbezogenen Zeiten in diesem Zeitraum." Opacity="0.6" FontSize="12"
|
||||||
|
IsVisible="{Binding !GroupSummaries.Count}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Views.Workload;
|
||||||
|
|
||||||
|
public partial class WorkloadEvaluationView : UserControl
|
||||||
|
{
|
||||||
|
public WorkloadEvaluationView() => InitializeComponent();
|
||||||
|
}
|
||||||
@@ -19,6 +19,9 @@
|
|||||||
<ContentPage Header="Zeiterfassung">
|
<ContentPage Header="Zeiterfassung">
|
||||||
<views:TimeTrackingView DataContext="{Binding TimeTracking}"/>
|
<views:TimeTrackingView DataContext="{Binding TimeTracking}"/>
|
||||||
</ContentPage>
|
</ContentPage>
|
||||||
|
<ContentPage Header="Auswertung">
|
||||||
|
<views:WorkloadEvaluationView DataContext="{Binding Evaluation}"/>
|
||||||
|
</ContentPage>
|
||||||
</TabbedPage>
|
</TabbedPage>
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@@ -778,9 +778,10 @@ dadurch faktisch schon (Quick-Input-Dialog bzw. "Offene Entschuldigungen" im Das
|
|||||||
## 6. Arbeitszeit & Aufgaben
|
## 6. Arbeitszeit & Aufgaben
|
||||||
|
|
||||||
Modelle `WorkTask` und `TimeEntry` existieren, Repositories ebenfalls.
|
Modelle `WorkTask` und `TimeEntry` existieren, Repositories ebenfalls.
|
||||||
Navigationspunkt "Arbeitszeit" zeigt inzwischen Aufgabenverwaltung (6.1) und Zeiterfassung (6.2)
|
Navigationspunkt "Arbeitszeit" zeigt Aufgabenverwaltung (6.1), Zeiterfassung (6.2) und Auswertung
|
||||||
als zwei Tabs (`WorkloadViewModel`/`WorkloadView`, gleiches Container-Tab-Muster wie
|
(6.3) als drei Tabs (`WorkloadViewModel`/`WorkloadView`, gleiches Container-Tab-Muster wie
|
||||||
`GroupDetailViewModel`); nur die Monats-/Jahresauswertung (6.3) ist noch offen.
|
`GroupDetailViewModel`) — nur der Export der Auswertung (6.3.3) ist noch offen, da er an das noch
|
||||||
|
nicht existierende Kapitel 11 (Export-Infrastruktur) hängt.
|
||||||
|
|
||||||
### 6.1 Aufgabenverwaltung
|
### 6.1 Aufgabenverwaltung
|
||||||
- [x] **6.1.1** Aufgabenliste mit Filter nach Status, Kategorie, Gruppe und Fälligkeit —
|
- [x] **6.1.1** Aufgabenliste mit Filter nach Status, Kategorie, Gruppe und Fälligkeit —
|
||||||
@@ -809,7 +810,7 @@ als zwei Tabs (`WorkloadViewModel`/`WorkloadView`, gleiches Container-Tab-Muster
|
|||||||
`AddTimeEntryDialog`: entweder Dauer direkt eintragen oder Von/Bis angeben (daraus wird
|
`AddTimeEntryDialog`: entweder Dauer direkt eintragen oder Von/Bis angeben (daraus wird
|
||||||
die Dauer berechnet); mindestens eines von beidem ist Pflicht.
|
die Dauer berechnet); mindestens eines von beidem ist Pflicht.
|
||||||
- [x] **6.2.3** Wochenübersicht der erfassten Zeit, Summen je Kategorie — Balken je Kategorie
|
- [x] **6.2.3** Wochenübersicht der erfassten Zeit, Summen je Kategorie — Balken je Kategorie
|
||||||
(`CategoryWeekSummary`, `TimeEntry.Category` ist bewusst ein freier String statt
|
(`CategoryTimeSummary`, `TimeEntry.Category` ist bewusst ein freier String statt
|
||||||
`TaskCategory`-Enum wie bei `WorkTask`, da das Domänenmodell das schon so vorgab; die
|
`TaskCategory`-Enum wie bei `WorkTask`, da das Domänenmodell das schon so vorgab; die
|
||||||
ComboBox schlägt trotzdem dieselben Kategorienamen vor). Nur die aktuelle Kalenderwoche,
|
ComboBox schlägt trotzdem dieselben Kategorienamen vor). Nur die aktuelle Kalenderwoche,
|
||||||
keine Wochennavigation wie im Stundenplan — bei Bedarf später ergänzbar.
|
keine Wochennavigation wie im Stundenplan — bei Bedarf später ergänzbar.
|
||||||
@@ -819,9 +820,22 @@ als zwei Tabs (`WorkloadViewModel`/`WorkloadView`, gleiches Container-Tab-Muster
|
|||||||
`ITimeEntryRepository`.
|
`ITimeEntryRepository`.
|
||||||
|
|
||||||
### 6.3 Auswertung
|
### 6.3 Auswertung
|
||||||
- [ ] **6.3.1** Monats-/Jahresauswertung nach Kategorie und Gruppe (Diagramm + Tabelle).
|
- [x] **6.3.1** Monats-/Jahresauswertung nach Kategorie und Gruppe (Diagramm + Tabelle) —
|
||||||
- [ ] **6.3.2** Abgleich mit der Pflichtstundenzahl (in Einstellungen hinterlegt).
|
dritter Tab "Auswertung" (`WorkloadEvaluationViewModel`). Zeitraum wahlweise Monat
|
||||||
- [ ] **6.3.3** Export der Arbeitszeitauswertung (siehe 11.2).
|
(Jahr+Monat) oder Schuljahr (nutzt `SchoolYearService`, 1.8.–31.7. wie überall sonst in der
|
||||||
|
App — bewusst kein Kalenderjahr, um konsistent zur restlichen Schuljahres-Logik zu bleiben);
|
||||||
|
"Diagramm" als Balken je Kategorie/Gruppe (`ProgressBar`, gleiches Muster wie die
|
||||||
|
Wochenübersicht in 6.2.3), keine eigene Chart-Bibliothek.
|
||||||
|
- [x] **6.3.2** Abgleich mit der Pflichtstundenzahl — neuer `WorkloadSettingsService`
|
||||||
|
(`workloadsettings.json`, gleiches Muster wie `PeriodScheduleService`/
|
||||||
|
`SchoolCalendarSettingsService`). Bewusst **nicht** in den Einstellungen (Kapitel 12)
|
||||||
|
hinterlegt, sondern direkt auf dem Auswertungs-Tab editierbar: das Feld wird ausschließlich
|
||||||
|
dort verwendet, ein Sprung in eine andere Seite für eine einzelne Zahl wäre reine
|
||||||
|
Indirektion (siehe auch das Nutzer-Feedback zum "Stundenplan bearbeiten"-Knopf oben). Die
|
||||||
|
Pflichtstunden pro Woche werden auf die Wochenzahl des gewählten Zeitraums hochgerechnet und
|
||||||
|
der erfassten Ist-Zeit gegenübergestellt.
|
||||||
|
- [ ] **6.3.3** Export der Arbeitszeitauswertung (siehe 11.2) — noch offen, da Kapitel 11
|
||||||
|
(Export-Infrastruktur) noch nicht existiert.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user