Stundenplan: Wochenraster, Wochennavigation, Ferien/Feiertage (Kapitel 4.3)
Neuer Stundenplan mit "Heute"-Standardansicht (Tagesliste unten angedockt, gruppenübergreifendes Wochenraster mit Fach/Klasse/Raum/Thema, Vor-/Zurück- Navigation zwischen Kalenderwochen) und separatem Bearbeiten-Raster für die wöchentliche Zuordnung. Badges für Ferien-/Klausur-Nähe und ausgegraute Ferientage direkt im Plan statt einer separaten Liste. Ferien-/Feiertage- Pflege (Bundesland, Schulferien) sitzt jetzt in den Einstellungen statt im Stundenplan selbst. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ using LehrerApp.Data;
|
||||
using LehrerApp.Desktop.Services;
|
||||
using LehrerApp.Desktop.ViewModels;
|
||||
using LehrerApp.Desktop.ViewModels.Groups;
|
||||
using LehrerApp.Desktop.ViewModels.Planning;
|
||||
using LehrerApp.Desktop.ViewModels.Students;
|
||||
using LehrerApp.Desktop.Views;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -103,6 +104,10 @@ public class App : Application
|
||||
var sl = Services.GetRequiredService<StudentListViewModel>();
|
||||
sl.OnNavigateToDetail = id => main.NavigateToStudent(id);
|
||||
sl.OnAddStudent = () => ShowAddStudentDialog();
|
||||
|
||||
// Stundenplan "Heute" → GroupDetail (Tab "Planung")
|
||||
var timetable = Services.GetRequiredService<TimetableViewModel>();
|
||||
timetable.OnNavigateToGroup = id => main.NavigateToGroupDetail(id, 5);
|
||||
}
|
||||
|
||||
private static async Task ShowAddStudentDialog()
|
||||
|
||||
@@ -5,6 +5,7 @@ using LehrerApp.Data.Repositories;
|
||||
using LehrerApp.Desktop.Services;
|
||||
using LehrerApp.Desktop.ViewModels;
|
||||
using LehrerApp.Desktop.ViewModels.Groups;
|
||||
using LehrerApp.Desktop.ViewModels.Planning;
|
||||
using LehrerApp.Desktop.ViewModels.Settings;
|
||||
using LehrerApp.Desktop.ViewModels.Students;
|
||||
using LehrerApp.Sync;
|
||||
@@ -130,10 +131,14 @@ public static class AppBootstrapper
|
||||
services.AddSingleton<IShorthandCodeRepository, ShorthandCodeRepository>();
|
||||
services.AddSingleton<IAlternativeLessonPathRepository, AlternativeLessonPathRepository>();
|
||||
services.AddSingleton<IAttachmentStorage, LiteAttachmentStorage>();
|
||||
services.AddSingleton<ITimetableSlotRepository, TimetableSlotRepository>();
|
||||
services.AddSingleton<ISchoolHolidayRepository, SchoolHolidayRepository>();
|
||||
|
||||
// ── Services ──────────────────────────────────────────────────────────
|
||||
services.AddSingleton<GradingService>();
|
||||
services.AddSingleton<SchoolYearService>();
|
||||
services.AddSingleton<PublicHolidayService>();
|
||||
services.AddSingleton(_ => new SchoolCalendarSettingsService(appData));
|
||||
|
||||
// ── Sync (optional – nur wenn Server konfiguriert) ────────────────────
|
||||
services.AddSingleton(_ => new EventQueue(queuePath));
|
||||
@@ -178,6 +183,7 @@ public static class AppBootstrapper
|
||||
new SyncStatusViewModel(sp.GetService<SyncEngine>()));
|
||||
services.AddSingleton<GroupListViewModel>();
|
||||
services.AddSingleton<StudentListViewModel>();
|
||||
services.AddSingleton<TimetableViewModel>();
|
||||
|
||||
// Transient: neue Instanz pro Navigation (für Detailseiten)
|
||||
services.AddTransient<GroupDetailViewModel>();
|
||||
|
||||
@@ -3,6 +3,7 @@ using CommunityToolkit.Mvvm.Input;
|
||||
using LehrerApp.Core.Services;
|
||||
using LehrerApp.Desktop.Services;
|
||||
using LehrerApp.Desktop.ViewModels.Groups;
|
||||
using LehrerApp.Desktop.ViewModels.Planning;
|
||||
using LehrerApp.Desktop.ViewModels.Settings;
|
||||
using LehrerApp.Desktop.ViewModels.Students;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -64,7 +65,7 @@ public partial class MainWindowViewModel : ObservableObject
|
||||
NavItem.Groups => _services.GetRequiredService<GroupListViewModel>(),
|
||||
NavItem.Students => _services.GetRequiredService<StudentListViewModel>(),
|
||||
NavItem.Exams => new PlaceholderViewModel { Title = "Klausuren", Icon = "📝" },
|
||||
NavItem.Planner => new PlaceholderViewModel { Title = "Unterrichtsplanung", Icon = "📅" },
|
||||
NavItem.Planner => GetTimetable(),
|
||||
NavItem.Workload => new PlaceholderViewModel { Title = "Arbeitszeit", Icon = "⏱" },
|
||||
NavItem.Settings => _services.GetRequiredService<SettingsViewModel>(),
|
||||
_ => CurrentPage,
|
||||
@@ -78,6 +79,15 @@ public partial class MainWindowViewModel : ObservableObject
|
||||
return dashboard;
|
||||
}
|
||||
|
||||
private TimetableViewModel GetTimetable()
|
||||
{
|
||||
var timetable = _services.GetRequiredService<TimetableViewModel>();
|
||||
timetable.WeekOffset = 0;
|
||||
timetable.Load();
|
||||
timetable.ActiveTabIndex = 0;
|
||||
return timetable;
|
||||
}
|
||||
|
||||
public void NavigateToGroupDetail(Guid groupId, int initialTab = 0)
|
||||
{
|
||||
ActiveNavItem = NavItem.Groups;
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using LehrerApp.Core.Interfaces;
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Core.Services;
|
||||
|
||||
namespace LehrerApp.Desktop.ViewModels.Planning;
|
||||
|
||||
/// <summary>Zuweisen/Bearbeiten/Entfernen eines Stundenplan-Termins (4.3.3).</summary>
|
||||
public partial class TimetableSlotDialogViewModel : ObservableObject
|
||||
{
|
||||
private readonly ITimetableSlotRepository _slots;
|
||||
private readonly Dictionary<string, Guid> _groupIdsByName;
|
||||
private readonly TimetableSlot? _editing;
|
||||
|
||||
public DayOfWeek Weekday { get; }
|
||||
public int PeriodNumber { get; }
|
||||
public string WeekdayLabel { get; }
|
||||
public string DialogTitle { get; }
|
||||
public bool IsEditing => _editing is not null;
|
||||
|
||||
[ObservableProperty] private string _selectedGroupName = "";
|
||||
[ObservableProperty] private string _room = "";
|
||||
[ObservableProperty] private string _groupError = "";
|
||||
|
||||
public string[] GroupOptions { get; }
|
||||
|
||||
/// null = unverändert/Abbruch, sonst das neue/aktualisierte Ergebnis.
|
||||
public TimetableSlot? Result { get; private set; }
|
||||
public bool Deleted { get; private set; }
|
||||
|
||||
public TimetableSlotDialogViewModel(ITimetableSlotRepository slots, IGroupRepository groups,
|
||||
SchoolYearService schoolYear, DayOfWeek weekday, int periodNumber, TimetableSlot? editing)
|
||||
{
|
||||
_slots = slots;
|
||||
_editing = editing;
|
||||
Weekday = weekday;
|
||||
PeriodNumber = periodNumber;
|
||||
WeekdayLabel = weekday switch
|
||||
{
|
||||
DayOfWeek.Monday => "Montag", DayOfWeek.Tuesday => "Dienstag",
|
||||
DayOfWeek.Wednesday => "Mittwoch", DayOfWeek.Thursday => "Donnerstag",
|
||||
DayOfWeek.Friday => "Freitag", _ => weekday.ToString(),
|
||||
};
|
||||
DialogTitle = $"{WeekdayLabel}, {periodNumber}. Stunde";
|
||||
|
||||
var availableGroups = groups.GetBySchoolYear(schoolYear.CurrentSchoolYear()).OrderBy(g => g.Name).ToList();
|
||||
_groupIdsByName = availableGroups.ToDictionary(g => g.Name, g => g.Id);
|
||||
GroupOptions = availableGroups.Select(g => g.Name).ToArray();
|
||||
|
||||
if (editing is not null)
|
||||
{
|
||||
SelectedGroupName = availableGroups.FirstOrDefault(g => g.Id == editing.GroupId)?.Name ?? "";
|
||||
Room = editing.Room ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Save()
|
||||
{
|
||||
GroupError = "";
|
||||
if (string.IsNullOrWhiteSpace(SelectedGroupName) || !_groupIdsByName.TryGetValue(SelectedGroupName, out var groupId))
|
||||
{
|
||||
GroupError = "Bitte eine Gruppe auswählen.";
|
||||
return;
|
||||
}
|
||||
|
||||
var slot = _editing ?? new TimetableSlot { Weekday = Weekday, PeriodNumber = PeriodNumber };
|
||||
slot.GroupId = groupId;
|
||||
slot.Room = string.IsNullOrWhiteSpace(Room) ? null : Room.Trim();
|
||||
|
||||
try
|
||||
{
|
||||
_slots.Save(slot);
|
||||
Result = slot;
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
GroupError = ex.Message;
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Delete()
|
||||
{
|
||||
if (_editing is null) return;
|
||||
_slots.Delete(_editing.Id);
|
||||
Deleted = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,456 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using LehrerApp.Core.Interfaces;
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Core.Services;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace LehrerApp.Desktop.ViewModels.Planning;
|
||||
|
||||
// ── Bundesland: deutsche Anzeige (4.3.5) ─────────────────────────────────────
|
||||
// Wird sowohl vom Stundenplan (Anzeige) als auch von den Einstellungen (Pflege) verwendet.
|
||||
|
||||
public static class GermanStateDisplay
|
||||
{
|
||||
private static readonly (GermanState State, string Name)[] Entries =
|
||||
[
|
||||
(GermanState.BW, "Baden-Württemberg"),
|
||||
(GermanState.BY, "Bayern"),
|
||||
(GermanState.BE, "Berlin"),
|
||||
(GermanState.BB, "Brandenburg"),
|
||||
(GermanState.HB, "Bremen"),
|
||||
(GermanState.HH, "Hamburg"),
|
||||
(GermanState.HE, "Hessen"),
|
||||
(GermanState.MV, "Mecklenburg-Vorpommern"),
|
||||
(GermanState.NI, "Niedersachsen"),
|
||||
(GermanState.NW, "Nordrhein-Westfalen"),
|
||||
(GermanState.RP, "Rheinland-Pfalz"),
|
||||
(GermanState.SL, "Saarland"),
|
||||
(GermanState.SN, "Sachsen"),
|
||||
(GermanState.ST, "Sachsen-Anhalt"),
|
||||
(GermanState.SH, "Schleswig-Holstein"),
|
||||
(GermanState.TH, "Thüringen"),
|
||||
];
|
||||
|
||||
public static string[] Options { get; } = Entries.Select(e => e.Name).ToArray();
|
||||
public static string Label(GermanState s) => Entries.First(e => e.State == s).Name;
|
||||
public static GermanState FromLabel(string label) =>
|
||||
Entries.FirstOrDefault(e => e.Name == label).State;
|
||||
}
|
||||
|
||||
// ── Stundenplan: "Heute"-Übersicht (Standardansicht, Wochenraster + Tagesliste) + Bearbeiten (4.3) ──
|
||||
|
||||
public partial class TimetableViewModel : ObservableObject
|
||||
{
|
||||
private const int FirstPeriod = 1;
|
||||
private const int LastPeriod = 10;
|
||||
private static readonly DayOfWeek[] Weekdays =
|
||||
[DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday];
|
||||
private static readonly string[] WeekdayColorPalette =
|
||||
["#7F77DD", "#1D9E75", "#D85A30", "#D4537E", "#378ADD", "#639922", "#EF9F27", "#4C86A8"];
|
||||
|
||||
private readonly ITimetableSlotRepository _slots;
|
||||
private readonly IGroupRepository _groups;
|
||||
private readonly ISubjectRepository _subjects;
|
||||
private readonly ILessonRepository _lessons;
|
||||
private readonly IExamRepository _exams;
|
||||
private readonly ISchoolHolidayRepository _schoolHolidays;
|
||||
private readonly SchoolCalendarSettingsService _calendarSettings;
|
||||
private readonly PublicHolidayService _publicHolidays;
|
||||
private readonly SchoolYearService _schoolYear;
|
||||
|
||||
public ObservableCollection<TimetableCellItem> Cells { get; } = [];
|
||||
public ObservableCollection<WeekCellItem> WeekItems { get; } = [];
|
||||
public ObservableCollection<HoursWarningItem> HoursWarnings { get; } = [];
|
||||
public ObservableCollection<TodayLessonItem> TodayItems { get; } = [];
|
||||
|
||||
[ObservableProperty] private int _activeTabIndex;
|
||||
[ObservableProperty] private string _todayLabel = "";
|
||||
[ObservableProperty] private string _weekRangeLabel = "";
|
||||
[ObservableProperty] private int _weekOffset;
|
||||
public bool IsCurrentWeek => WeekOffset == 0;
|
||||
|
||||
public Func<TimetableCellItem, Task>? OnEditSlot { get; set; }
|
||||
public Action<Guid>? OnNavigateToGroup { get; set; }
|
||||
|
||||
public TimetableViewModel(ITimetableSlotRepository slots, IGroupRepository groups,
|
||||
ISubjectRepository subjects, ILessonRepository lessons, IExamRepository exams,
|
||||
ISchoolHolidayRepository schoolHolidays, SchoolCalendarSettingsService calendarSettings,
|
||||
PublicHolidayService publicHolidays, SchoolYearService schoolYear)
|
||||
{
|
||||
_slots = slots; _groups = groups; _subjects = subjects; _lessons = lessons; _exams = exams;
|
||||
_schoolHolidays = schoolHolidays; _calendarSettings = calendarSettings;
|
||||
_publicHolidays = publicHolidays; _schoolYear = schoolYear;
|
||||
Load();
|
||||
}
|
||||
|
||||
partial void OnWeekOffsetChanged(int value) => OnPropertyChanged(nameof(IsCurrentWeek));
|
||||
|
||||
[RelayCommand]
|
||||
private void PreviousWeek() { WeekOffset--; Load(); }
|
||||
|
||||
[RelayCommand]
|
||||
private void NextWeek() { WeekOffset++; Load(); }
|
||||
|
||||
[RelayCommand]
|
||||
private void CurrentWeek() { WeekOffset = 0; Load(); }
|
||||
|
||||
public void Load()
|
||||
{
|
||||
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||
TodayLabel = today.ToString("dddd, dd.MM.yyyy", System.Globalization.CultureInfo.GetCultureInfo("de-DE"));
|
||||
|
||||
var publicHolidayDates = new HashSet<DateOnly>();
|
||||
foreach (var year in new[] { today.Year - 1, today.Year, today.Year + 1 })
|
||||
foreach (var h in _publicHolidays.GetHolidays(year, _calendarSettings.State)) publicHolidayDates.Add(h.Date);
|
||||
|
||||
var holidayBadges = ComputeHolidayBadges(today, publicHolidayDates);
|
||||
var examProximity = ComputeExamProximity(today, publicHolidayDates);
|
||||
|
||||
BuildGrid(holidayBadges);
|
||||
BuildWeekOverview(today, publicHolidayDates);
|
||||
BuildToday(today);
|
||||
BuildHoursWarnings();
|
||||
}
|
||||
|
||||
// ── "Heute": Tagesliste ──────────────────────────────────────────────────
|
||||
|
||||
private void BuildToday(DateOnly today)
|
||||
{
|
||||
TodayItems.Clear();
|
||||
var slotsToday = _slots.GetAll().Where(s => s.Weekday == today.DayOfWeek).OrderBy(s => s.PeriodNumber).ToList();
|
||||
|
||||
foreach (var slot in slotsToday)
|
||||
{
|
||||
var group = _groups.GetById(slot.GroupId);
|
||||
if (group is null) continue;
|
||||
var lesson = _lessons.GetByGroupAndDate(slot.GroupId, today).FirstOrDefault();
|
||||
var exam = _exams.GetByGroup(slot.GroupId).FirstOrDefault(e => e.Date == today);
|
||||
TodayItems.Add(new TodayLessonItem(slot.GroupId, slot.PeriodNumber, group.Name,
|
||||
slot.Room ?? "", ColorFor(group.Name), lesson?.Topic, exam?.Title));
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void OpenGroup(Guid groupId) => OnNavigateToGroup?.Invoke(groupId);
|
||||
|
||||
[RelayCommand]
|
||||
private void ShowEditor() => ActiveTabIndex = 1;
|
||||
|
||||
// ── "Heute": Wochenraster (Nutzer-Feedback) — wie das Bearbeiten-Raster, aber nur Anzeige ──
|
||||
|
||||
/// <summary>
|
||||
/// Zeigt Fach, Klasse, Raum und (falls für den Tag hinterlegt) das Thema der Stunde für die
|
||||
/// per <see cref="WeekOffset"/> gewählte Kalenderwoche (Mo–Fr, wie das Bearbeiten-Raster) —
|
||||
/// anders als die Tagesliste auch für Tage, die noch nicht "heute" sind, damit z.B. der
|
||||
/// parallele Kurs oder die nächste Stunde in der Woche auf einen Blick sichtbar sind. Die
|
||||
/// Badges (Ferien-Nähe, Klausur-Nähe) werden je Zelle am dort angezeigten Datum ausgerichtet,
|
||||
/// nicht am realen "heute" — sonst würde beim Blättern in andere Wochen ein Badge angezeigt,
|
||||
/// das eigentlich zu einer ganz anderen Woche gehört.
|
||||
/// </summary>
|
||||
private void BuildWeekOverview(DateOnly today, HashSet<DateOnly> publicHolidayDates)
|
||||
{
|
||||
WeekItems.Clear();
|
||||
var currentWeekMonday = today.AddDays(-((int)today.DayOfWeek + 6) % 7);
|
||||
var monday = currentWeekMonday.AddDays(WeekOffset * 7);
|
||||
var friday = monday.AddDays(4);
|
||||
WeekRangeLabel = $"{monday:dd.MM.} – {friday:dd.MM.yyyy}";
|
||||
|
||||
var allSlots = _slots.GetAll();
|
||||
var groups = _groups.GetAll(includeInactive: true).ToDictionary(g => g.Id);
|
||||
var schoolHolidays = _schoolHolidays.GetAll();
|
||||
var dateByWeekday = Weekdays.ToDictionary(w => w, w => monday.AddDays((int)w - (int)DayOfWeek.Monday));
|
||||
|
||||
WeekItems.Add(WeekCellItem.Corner());
|
||||
foreach (var weekday in Weekdays)
|
||||
WeekItems.Add(WeekCellItem.WeekdayHeader(weekday, dateByWeekday[weekday], dateByWeekday[weekday] == today));
|
||||
|
||||
for (var period = FirstPeriod; period <= LastPeriod; period++)
|
||||
{
|
||||
WeekItems.Add(WeekCellItem.PeriodLabel(period));
|
||||
foreach (var weekday in Weekdays)
|
||||
{
|
||||
var slot = allSlots.FirstOrDefault(s => s.Weekday == weekday && s.PeriodNumber == period);
|
||||
if (slot is null) { WeekItems.Add(WeekCellItem.Empty(weekday, period)); continue; }
|
||||
|
||||
var date = dateByWeekday[weekday];
|
||||
var group = groups.GetValueOrDefault(slot.GroupId);
|
||||
var subject = group?.SubjectId is { } subjectId ? _subjects.GetById(subjectId) : null;
|
||||
var lesson = _lessons.GetByGroupAndDate(slot.GroupId, date).FirstOrDefault();
|
||||
var hasExam = _exams.GetByGroup(slot.GroupId).Any(e => e.Date == date);
|
||||
var isHoliday = IsFreeDay(date, schoolHolidays, publicHolidayDates);
|
||||
var colorHex = isHoliday ? "#BDBDBD" : ColorFor(group?.Name ?? "");
|
||||
var holidayBadge = HolidayBadgeFor(date, weekday, schoolHolidays, publicHolidayDates);
|
||||
var isLastBeforeExam = IsLastBeforeExamFor(date, weekday, slot.GroupId, publicHolidayDates);
|
||||
|
||||
WeekItems.Add(WeekCellItem.ForSlot(weekday, period, date == today,
|
||||
subject?.ShortName is { Length: > 0 } sn ? sn : subject?.Name ?? "",
|
||||
group?.Name ?? "?", slot.Room ?? "", lesson?.Topic ?? "",
|
||||
colorHex, holidayBadge, hasExam, isLastBeforeExam,
|
||||
MentionsExperiment(lesson), slot.GroupId, isHoliday));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Badge "1"/"2" für die Zelle mit Datum <paramref name="date"/> selbst, analog zu
|
||||
/// <see cref="ComputeHolidayBadges"/>, aber an diesem konkreten Datum statt an "heute"
|
||||
/// ausgerichtet — nötig, damit das Badge beim Blättern durch die Wochen zur richtigen Woche
|
||||
/// gehört.</summary>
|
||||
private string HolidayBadgeFor(DateOnly date, DayOfWeek weekday, List<SchoolHoliday> schoolHolidays,
|
||||
HashSet<DateOnly> publicHolidayDates)
|
||||
{
|
||||
var nextHoliday = schoolHolidays.Where(h => h.StartDate > date).MinBy(h => h.StartDate);
|
||||
if (nextHoliday is null) return "";
|
||||
var count = CountOccurrences(weekday, date, nextHoliday.StartDate, publicHolidayDates);
|
||||
return count is 1 or 2 ? count.ToString() : "";
|
||||
}
|
||||
|
||||
/// <summary>Analog zu <see cref="ComputeExamProximity"/>, aber am Zelldatum statt an "heute"
|
||||
/// ausgerichtet.</summary>
|
||||
private bool IsLastBeforeExamFor(DateOnly date, DayOfWeek weekday, Guid groupId, HashSet<DateOnly> publicHolidayDates)
|
||||
{
|
||||
var nextExam = _exams.GetByGroup(groupId).Where(e => e.Date >= date).MinBy(e => e.Date);
|
||||
if (nextExam is null) return false;
|
||||
return CountOccurrences(weekday, date, nextExam.Date, publicHolidayDates) == 1;
|
||||
}
|
||||
|
||||
private static bool MentionsExperiment(Lesson? lesson) =>
|
||||
lesson is not null && lesson.Phases.Any(p =>
|
||||
p.Name.Contains("Experiment", StringComparison.OrdinalIgnoreCase) ||
|
||||
p.Activity.Contains("Experiment", StringComparison.OrdinalIgnoreCase) ||
|
||||
p.Material.Contains("Experiment", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
/// <summary>Fällt <paramref name="date"/> auf einen gesetzlichen Feiertag oder in Schulferien?</summary>
|
||||
private static bool IsFreeDay(DateOnly date, List<SchoolHoliday> schoolHolidays, HashSet<DateOnly> publicHolidayDates) =>
|
||||
publicHolidayDates.Contains(date) || schoolHolidays.Any(h => date >= h.StartDate && date <= h.EndDate);
|
||||
|
||||
// ── Bearbeiten-Raster ─────────────────────────────────────────────────────
|
||||
|
||||
private void BuildGrid(Dictionary<(DayOfWeek Weekday, Guid GroupId), string> badges)
|
||||
{
|
||||
Cells.Clear();
|
||||
var allSlots = _slots.GetAll();
|
||||
var groupNames = _groups.GetAll(includeInactive: true).ToDictionary(g => g.Id, g => g.Name);
|
||||
|
||||
Cells.Add(TimetableCellItem.Corner());
|
||||
foreach (var weekday in Weekdays)
|
||||
Cells.Add(TimetableCellItem.WeekdayHeader(weekday));
|
||||
|
||||
for (var period = FirstPeriod; period <= LastPeriod; period++)
|
||||
{
|
||||
Cells.Add(TimetableCellItem.PeriodLabel(period));
|
||||
foreach (var weekday in Weekdays)
|
||||
{
|
||||
var slot = allSlots.FirstOrDefault(s => s.Weekday == weekday && s.PeriodNumber == period);
|
||||
var groupName = slot is not null ? groupNames.GetValueOrDefault(slot.GroupId, "?") : "";
|
||||
var badge = slot is not null ? badges.GetValueOrDefault((weekday, slot.GroupId), "") : "";
|
||||
Cells.Add(TimetableCellItem.ForSlot(weekday, period, slot, groupName, ColorFor(groupName), badge));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string ColorFor(string name)
|
||||
{
|
||||
if (name.Length == 0) return "#9E9E9E";
|
||||
var hash = 0;
|
||||
foreach (var c in name) hash = hash * 31 + c;
|
||||
return WeekdayColorPalette[Math.Abs(hash) % WeekdayColorPalette.Length];
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task EditCell(TimetableCellItem? cell)
|
||||
{
|
||||
if (cell is null || !cell.IsSlotCell || OnEditSlot is null) return;
|
||||
await OnEditSlot(cell);
|
||||
Load();
|
||||
}
|
||||
|
||||
// ── Abgleich mit Wochenstunden (4.3.4) ────────────────────────────────────
|
||||
|
||||
private void BuildHoursWarnings()
|
||||
{
|
||||
HoursWarnings.Clear();
|
||||
var currentYear = _schoolYear.CurrentSchoolYear();
|
||||
var slotCounts = _slots.GetAll().GroupBy(s => s.GroupId).ToDictionary(g => g.Key, g => g.Count());
|
||||
|
||||
foreach (var group in _groups.GetBySchoolYear(currentYear).Where(g => g.HoursPerWeek.HasValue))
|
||||
{
|
||||
var assigned = slotCounts.GetValueOrDefault(group.Id, 0);
|
||||
if (assigned != group.HoursPerWeek!.Value)
|
||||
HoursWarnings.Add(new HoursWarningItem(group.Name, assigned, group.HoursPerWeek.Value));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Badges: letzte/vorletzte Stunde vor Ferien, letzte Stunde vor Klausur ────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Zählt, wie oft <paramref name="weekday"/> zwischen (inkl.) <paramref name="from"/> und
|
||||
/// (exkl.) <paramref name="until"/> eintritt — gesetzliche Feiertage werden übersprungen, da
|
||||
/// an ihnen ohnehin kein Unterricht stattfindet.
|
||||
/// </summary>
|
||||
private static int CountOccurrences(DayOfWeek weekday, DateOnly from, DateOnly until, HashSet<DateOnly> publicHolidays)
|
||||
{
|
||||
var count = 0;
|
||||
for (var date = from; date < until; date = date.AddDays(1))
|
||||
if (date.DayOfWeek == weekday && !publicHolidays.Contains(date)) count++;
|
||||
return count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Badge "1"/"2" für die letzte bzw. vorletzte Stunde eines Wochentags vor den nächsten
|
||||
/// anstehenden Schulferien. Pro (Wochentag, Gruppe) statt pro einzelnem <see cref="TimetableSlot"/>
|
||||
/// berechnet: eine Doppelstunde besteht aus zwei Slots mit demselben Wochentag/derselben
|
||||
/// Gruppe und bekommt dadurch automatisch dasselbe Badge, ohne gesonderte Blockerkennung.
|
||||
/// </summary>
|
||||
private Dictionary<(DayOfWeek Weekday, Guid GroupId), string> ComputeHolidayBadges(
|
||||
DateOnly today, HashSet<DateOnly> publicHolidayDates)
|
||||
{
|
||||
var result = new Dictionary<(DayOfWeek, Guid), string>();
|
||||
var nextHoliday = _schoolHolidays.GetAll().Where(h => h.StartDate > today).MinBy(h => h.StartDate);
|
||||
if (nextHoliday is null) return result;
|
||||
|
||||
foreach (var group in _slots.GetAll().GroupBy(s => (s.Weekday, s.GroupId)))
|
||||
{
|
||||
var count = CountOccurrences(group.Key.Weekday, today, nextHoliday.StartDate, publicHolidayDates);
|
||||
if (count is 1 or 2) result[group.Key] = count.ToString();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Markiert die letzte Stunde eines Wochentags vor der nächsten anstehenden Klausur derselben
|
||||
/// Gruppe (sofern eine ansteht) — analog zum Ferien-Badge, aber je Gruppe an deren eigenem
|
||||
/// nächsten Klausurtermin statt an einem gemeinsamen Ferientermin ausgerichtet.
|
||||
/// </summary>
|
||||
private Dictionary<(DayOfWeek Weekday, Guid GroupId), bool> ComputeExamProximity(
|
||||
DateOnly today, HashSet<DateOnly> publicHolidayDates)
|
||||
{
|
||||
var result = new Dictionary<(DayOfWeek, Guid), bool>();
|
||||
foreach (var groupSlots in _slots.GetAll().GroupBy(s => s.GroupId))
|
||||
{
|
||||
var nextExam = _exams.GetByGroup(groupSlots.Key).Where(e => e.Date >= today).MinBy(e => e.Date);
|
||||
if (nextExam is null) continue;
|
||||
|
||||
foreach (var weekday in groupSlots.Select(s => s.Weekday).Distinct())
|
||||
{
|
||||
var count = CountOccurrences(weekday, today, nextExam.Date, publicHolidayDates);
|
||||
if (count == 1) result[(weekday, groupSlots.Key)] = true;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public class TimetableCellItem
|
||||
{
|
||||
public bool IsHeader { get; private init; }
|
||||
public bool IsPeriodLabel { get; private init; }
|
||||
public string Text { get; private init; } = "";
|
||||
public DayOfWeek? Weekday { get; private init; }
|
||||
public int PeriodNumber { get; private init; }
|
||||
public TimetableSlot? Slot { get; private init; }
|
||||
public string GroupName { get; private init; } = "";
|
||||
public string ColorHex { get; private init; } = "#9E9E9E";
|
||||
public string BadgeText { get; private init; } = "";
|
||||
public bool HasBadge => BadgeText.Length > 0;
|
||||
public bool IsAssigned => Slot is not null;
|
||||
public bool IsSlotCell => !IsHeader && !IsPeriodLabel;
|
||||
|
||||
public static TimetableCellItem Corner() => new() { IsHeader = true, Text = "" };
|
||||
|
||||
public static TimetableCellItem WeekdayHeader(DayOfWeek day) => new()
|
||||
{
|
||||
IsHeader = true,
|
||||
Text = day.ToString() switch
|
||||
{
|
||||
"Monday" => "Mo", "Tuesday" => "Di", "Wednesday" => "Mi",
|
||||
"Thursday" => "Do", "Friday" => "Fr", _ => day.ToString(),
|
||||
},
|
||||
};
|
||||
|
||||
public static TimetableCellItem PeriodLabel(int period) => new() { IsPeriodLabel = true, Text = period.ToString() };
|
||||
|
||||
public static TimetableCellItem ForSlot(DayOfWeek day, int period, TimetableSlot? slot, string groupName,
|
||||
string colorHex, string badgeText) => new()
|
||||
{
|
||||
Weekday = day, PeriodNumber = period, Slot = slot, GroupName = groupName, ColorHex = colorHex,
|
||||
BadgeText = badgeText,
|
||||
Text = slot is null ? "" : groupName + (string.IsNullOrWhiteSpace(slot.Room) ? "" : $" · {slot.Room}"),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Zelle im schreibgeschützten Wochenraster der "Heute"-Ansicht.</summary>
|
||||
public class WeekCellItem
|
||||
{
|
||||
public bool IsHeader { get; private init; }
|
||||
public bool IsPeriodLabel { get; private init; }
|
||||
public bool IsSlotCell => !IsHeader && !IsPeriodLabel;
|
||||
public bool IsAssigned { get; private init; }
|
||||
public string Text { get; private init; } = "";
|
||||
public DayOfWeek? Weekday { get; private init; }
|
||||
public int PeriodNumber { get; private init; }
|
||||
public bool IsToday { get; private init; }
|
||||
public Guid GroupId { get; private init; }
|
||||
public string SubjectLabel { get; private init; } = "";
|
||||
public string GroupName { get; private init; } = "";
|
||||
public string Room { get; private init; } = "";
|
||||
public string Topic { get; private init; } = "";
|
||||
public string ColorHex { get; private init; } = "#9E9E9E";
|
||||
public string HolidayBadge { get; private init; } = "";
|
||||
public bool HasHolidayBadge => HolidayBadge.Length > 0;
|
||||
public bool HasExam { get; private init; }
|
||||
public bool IsLastBeforeExam { get; private init; }
|
||||
public bool HasExperiment { get; private init; }
|
||||
public bool IsHoliday { get; private init; }
|
||||
public bool HasRoom => !string.IsNullOrWhiteSpace(Room);
|
||||
public bool HasTopic => !string.IsNullOrWhiteSpace(Topic);
|
||||
|
||||
public static WeekCellItem Corner() => new() { IsHeader = true };
|
||||
|
||||
public static WeekCellItem WeekdayHeader(DayOfWeek day, DateOnly date, bool isToday) => new()
|
||||
{
|
||||
IsHeader = true,
|
||||
IsToday = isToday,
|
||||
Text = (day switch
|
||||
{
|
||||
DayOfWeek.Monday => "Mo", DayOfWeek.Tuesday => "Di", DayOfWeek.Wednesday => "Mi",
|
||||
DayOfWeek.Thursday => "Do", DayOfWeek.Friday => "Fr", _ => day.ToString(),
|
||||
}) + $" {date:dd.MM.}",
|
||||
};
|
||||
|
||||
public static WeekCellItem PeriodLabel(int period) => new() { IsPeriodLabel = true, Text = period.ToString() };
|
||||
|
||||
public static WeekCellItem Empty(DayOfWeek day, int period) => new() { Weekday = day, PeriodNumber = period };
|
||||
|
||||
public static WeekCellItem ForSlot(DayOfWeek day, int period, bool isToday, string subjectLabel,
|
||||
string groupName, string room, string topic, string colorHex, string holidayBadge,
|
||||
bool hasExam, bool isLastBeforeExam, bool hasExperiment, Guid groupId, bool isHoliday) => new()
|
||||
{
|
||||
Weekday = day, PeriodNumber = period, IsAssigned = true, IsToday = isToday,
|
||||
SubjectLabel = subjectLabel, GroupName = groupName, Room = room, Topic = topic,
|
||||
ColorHex = colorHex, HolidayBadge = holidayBadge, HasExam = hasExam,
|
||||
IsLastBeforeExam = isLastBeforeExam, HasExperiment = hasExperiment, GroupId = groupId,
|
||||
IsHoliday = isHoliday,
|
||||
};
|
||||
}
|
||||
|
||||
public class HoursWarningItem(string groupName, int assigned, int expected)
|
||||
{
|
||||
public string GroupName { get; } = groupName;
|
||||
public string Text { get; } = $"{groupName}: {assigned} von {expected} Wochenstunden eingetragen";
|
||||
}
|
||||
|
||||
public class TodayLessonItem(Guid groupId, int periodNumber, string groupName, string room,
|
||||
string colorHex, string? lessonTopic, string? examTitle)
|
||||
{
|
||||
public Guid GroupId { get; } = groupId;
|
||||
public int PeriodNumber { get; } = periodNumber;
|
||||
public string GroupName { get; } = groupName;
|
||||
public string Room { get; } = room;
|
||||
public string ColorHex { get; } = colorHex;
|
||||
public string? LessonTopic { get; } = lessonTopic;
|
||||
public string? ExamTitle { get; } = examTitle;
|
||||
public bool HasRoom => !string.IsNullOrWhiteSpace(Room);
|
||||
public bool HasLessonTopic => !string.IsNullOrWhiteSpace(LessonTopic);
|
||||
public bool HasExam => ExamTitle is not null;
|
||||
}
|
||||
@@ -4,7 +4,9 @@ using LehrerApp.Core.Interfaces;
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Core.Services;
|
||||
using LehrerApp.Data;
|
||||
using LehrerApp.Desktop.ViewModels.Planning;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
@@ -113,14 +115,30 @@ public partial class SettingsViewModel : ObservableObject
|
||||
/// Vom Code-Behind gesetzt: zeigt einen Bestätigungsdialog vor dem endgültigen Löschen.
|
||||
public Func<ExpiredDocumentItem, Task<bool>>? OnConfirmHardDelete { get; set; }
|
||||
|
||||
// ── Ferien & Feiertage (4.3.5, aus dem Stundenplan hierher verschoben) ───
|
||||
|
||||
[ObservableProperty] private string _selectedStateName = "";
|
||||
[ObservableProperty] private string _newHolidayName = "";
|
||||
[ObservableProperty] private string _newHolidayStartText = "";
|
||||
[ObservableProperty] private string _newHolidayEndText = "";
|
||||
[ObservableProperty] private string _holidayNameError = "";
|
||||
[ObservableProperty] private string _holidayDateError = "";
|
||||
|
||||
public List<string> StateOptions { get; } = GermanStateDisplay.Options.ToList();
|
||||
public ObservableCollection<SchoolHolidayItem> SchoolHolidayEntries { get; } = [];
|
||||
|
||||
// ── Konstruktor ───────────────────────────────────────────────────────────
|
||||
|
||||
private readonly ISchoolHolidayRepository _schoolHolidays;
|
||||
private readonly SchoolCalendarSettingsService _calendarSettings;
|
||||
|
||||
public SettingsViewModel(ISubjectRepository subjects, ICompetencyDomainRepository domainRepo,
|
||||
IGradingKeyTemplateRepository gradingKeyTemplates, IGradingSchemeRepository gradingSchemes,
|
||||
GradingService grading, BackupService backups, DatabaseEncryptionService dbEncryption,
|
||||
AppLockService appLock, LiteDbContext dbContext, PrivacySettingsService privacy,
|
||||
IDocumentationRepository documentation, IStudentRepository students,
|
||||
IShorthandCodeRepository shorthandCodes)
|
||||
IShorthandCodeRepository shorthandCodes, ISchoolHolidayRepository schoolHolidays,
|
||||
SchoolCalendarSettingsService calendarSettings)
|
||||
{
|
||||
_subjects = subjects;
|
||||
_domainRepo = domainRepo;
|
||||
@@ -135,6 +153,8 @@ public partial class SettingsViewModel : ObservableObject
|
||||
_documentation = documentation;
|
||||
_students = students;
|
||||
_shorthandCodes = shorthandCodes;
|
||||
_schoolHolidays = schoolHolidays;
|
||||
_calendarSettings = calendarSettings;
|
||||
LoadSubjects();
|
||||
LoadShorthandCodes();
|
||||
LoadGradingKeyTemplates();
|
||||
@@ -145,6 +165,51 @@ public partial class SettingsViewModel : ObservableObject
|
||||
AppLockTimeoutMinutes = _appLock.TimeoutMinutes;
|
||||
RetentionYears = _privacy.RetentionYears;
|
||||
LoadExpiredDocuments();
|
||||
SelectedStateName = GermanStateDisplay.Label(_calendarSettings.State);
|
||||
LoadSchoolHolidays();
|
||||
}
|
||||
|
||||
// ── Ferien & Feiertage: Bundesland / Schulferien pflegen ─────────────────
|
||||
|
||||
partial void OnSelectedStateNameChanged(string value) =>
|
||||
_calendarSettings.SetState(GermanStateDisplay.FromLabel(value));
|
||||
|
||||
private void LoadSchoolHolidays()
|
||||
{
|
||||
SchoolHolidayEntries.Clear();
|
||||
foreach (var h in _schoolHolidays.GetAll().OrderBy(h => h.StartDate))
|
||||
SchoolHolidayEntries.Add(new SchoolHolidayItem(h));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void AddSchoolHoliday()
|
||||
{
|
||||
HolidayNameError = ""; HolidayDateError = "";
|
||||
var valid = true;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(NewHolidayName)) { HolidayNameError = "Name erforderlich."; valid = false; }
|
||||
|
||||
var hasStart = DateOnly.TryParseExact(NewHolidayStartText, "dd.MM.yyyy", CultureInfo.InvariantCulture,
|
||||
System.Globalization.DateTimeStyles.None, out var start);
|
||||
var hasEnd = DateOnly.TryParseExact(NewHolidayEndText, "dd.MM.yyyy", CultureInfo.InvariantCulture,
|
||||
System.Globalization.DateTimeStyles.None, out var end);
|
||||
|
||||
if (!hasStart || !hasEnd) { HolidayDateError = "Bitte Beginn und Ende im Format TT.MM.JJJJ angeben."; valid = false; }
|
||||
else if (end < start) { HolidayDateError = "Das Ende darf nicht vor dem Beginn liegen."; valid = false; }
|
||||
|
||||
if (!valid) return;
|
||||
|
||||
_schoolHolidays.Save(new SchoolHoliday { Name = NewHolidayName.Trim(), StartDate = start, EndDate = end });
|
||||
NewHolidayName = ""; NewHolidayStartText = ""; NewHolidayEndText = "";
|
||||
LoadSchoolHolidays();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void RemoveSchoolHoliday(SchoolHolidayItem? item)
|
||||
{
|
||||
if (item is null) return;
|
||||
_schoolHolidays.Delete(item.Id);
|
||||
SchoolHolidayEntries.Remove(item);
|
||||
}
|
||||
|
||||
// ── Datenschutz: Löschfristen ─────────────────────────────────────────────
|
||||
@@ -770,6 +835,13 @@ public class ExpiredDocumentItem(Documentation d, string studentName)
|
||||
public string CreatedAtDisplay { get; } = d.CreatedAt.ToLocalTime().ToString("dd.MM.yyyy");
|
||||
}
|
||||
|
||||
public class SchoolHolidayItem(SchoolHoliday h)
|
||||
{
|
||||
public Guid Id { get; } = h.Id;
|
||||
public string Name { get; } = h.Name;
|
||||
public string RangeDisplay { get; } = $"{h.StartDate:dd.MM.yyyy} – {h.EndDate:dd.MM.yyyy}";
|
||||
}
|
||||
|
||||
// ── JSON DTOs ─────────────────────────────────────────────────────────────────
|
||||
|
||||
internal class CatalogDto
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
xmlns:vs="clr-namespace:LehrerApp.Desktop.Views.Students"
|
||||
xmlns:vset="clr-namespace:LehrerApp.Desktop.Views.Settings"
|
||||
xmlns:vmset="clr-namespace:LehrerApp.Desktop.ViewModels.Settings"
|
||||
xmlns:vp="clr-namespace:LehrerApp.Desktop.Views.Planning"
|
||||
xmlns:vmp="clr-namespace:LehrerApp.Desktop.ViewModels.Planning"
|
||||
xmlns:svc="clr-namespace:LehrerApp.Desktop.Services"
|
||||
x:Class="LehrerApp.Desktop.Views.MainWindow"
|
||||
x:DataType="vm:MainWindowViewModel"
|
||||
@@ -48,6 +50,9 @@
|
||||
<DataTemplate DataType="vmset:SettingsViewModel">
|
||||
<vset:SettingsView/>
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="vmp:TimetableViewModel">
|
||||
<vp:TimetableView/>
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="vm:PlaceholderViewModel">
|
||||
<views:PlaceholderView/>
|
||||
</DataTemplate>
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Planning"
|
||||
x:Class="LehrerApp.Desktop.Views.Planning.TimetableSlotDialog"
|
||||
x:DataType="vm:TimetableSlotDialogViewModel"
|
||||
Title="{Binding DialogTitle}"
|
||||
Width="360" SizeToContent="Height"
|
||||
CanResize="False" WindowStartupLocation="CenterOwner">
|
||||
|
||||
<StackPanel Margin="24" Spacing="12">
|
||||
<TextBlock Text="{Binding DialogTitle}" Classes="dialogtitle"/>
|
||||
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Lerngruppe *" FontSize="12" Opacity="0.7"/>
|
||||
<ComboBox ItemsSource="{Binding GroupOptions}" SelectedItem="{Binding SelectedGroupName}"
|
||||
HorizontalAlignment="Stretch"/>
|
||||
<TextBlock Text="{Binding GroupError}" Foreground="Red" FontSize="11"
|
||||
IsVisible="{Binding GroupError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Raum (optional)" FontSize="12" Opacity="0.7"/>
|
||||
<TextBox Text="{Binding Room}"/>
|
||||
</StackPanel>
|
||||
|
||||
<Button Content="Eintrag entfernen" HorizontalAlignment="Stretch" Margin="0,10,0,0"
|
||||
IsVisible="{Binding IsEditing}" Click="OnDelete"/>
|
||||
|
||||
<Grid ColumnDefinitions="*,8,*" Margin="0,4,0,0">
|
||||
<Button Grid.Column="0" Content="Abbrechen" HorizontalAlignment="Stretch" Click="OnCancel"/>
|
||||
<Button Grid.Column="2" Content="Speichern" HorizontalAlignment="Stretch" Click="OnSave"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Window>
|
||||
@@ -0,0 +1,30 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using LehrerApp.Desktop.ViewModels.Planning;
|
||||
|
||||
namespace LehrerApp.Desktop.Views.Planning;
|
||||
|
||||
public partial class TimetableSlotDialog : Window
|
||||
{
|
||||
public TimetableSlotDialog() => InitializeComponent();
|
||||
|
||||
private void OnSave(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is TimetableSlotDialogViewModel vm)
|
||||
{
|
||||
vm.SaveCommand.Execute(null);
|
||||
if (vm.Result is not null) Close();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDelete(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is TimetableSlotDialogViewModel vm)
|
||||
{
|
||||
vm.DeleteCommand.Execute(null);
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCancel(object? sender, RoutedEventArgs e) => Close();
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Planning"
|
||||
xmlns:shared="clr-namespace:LehrerApp.Desktop.Views.Shared"
|
||||
x:Class="LehrerApp.Desktop.Views.Planning.TimetableView"
|
||||
x:DataType="vm:TimetableViewModel">
|
||||
|
||||
<UserControl.Styles>
|
||||
<Style Selector="Border.timetablecell">
|
||||
<Setter Property="Background" Value="{DynamicResource SystemControlBackgroundBaseLowBrush}"/>
|
||||
</Style>
|
||||
<Style Selector="Border.timetablecell.assigned">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
</Style>
|
||||
<Style Selector="Border.weekheader.today">
|
||||
<Setter Property="Background" Value="{DynamicResource SystemAccentColor}"/>
|
||||
</Style>
|
||||
</UserControl.Styles>
|
||||
|
||||
<Grid RowDefinitions="Auto,*">
|
||||
<shared:PageHeader Grid.Row="0" Margin="32,28,32,0" Title="Stundenplan"
|
||||
Subtitle="Wiederkehrendes wöchentliches Muster, keine konkreten Termine"/>
|
||||
|
||||
<TabbedPage Grid.Row="1" TabPlacement="Top" SelectedIndex="{Binding ActiveTabIndex}">
|
||||
|
||||
<!-- Tab: Heute (Standardansicht) -->
|
||||
<ContentPage Header="Heute">
|
||||
<DockPanel Margin="32,20,32,20">
|
||||
|
||||
<!-- Unten angedockt: heutige Stunden im Detail -->
|
||||
<Border DockPanel.Dock="Bottom" Margin="0,16,0,0"
|
||||
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||
CornerRadius="8" Padding="16" MaxHeight="240">
|
||||
<DockPanel>
|
||||
<TextBlock DockPanel.Dock="Top" Text="{Binding TodayLabel}" FontSize="15" FontWeight="SemiBold" Margin="0,0,0,8"/>
|
||||
<ScrollViewer>
|
||||
<ItemsControl ItemsSource="{Binding TodayItems}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:TodayLessonItem">
|
||||
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||
BorderThickness="0,0,0,1" Padding="0,8">
|
||||
<Grid ColumnDefinitions="6,Auto,*,Auto">
|
||||
<Rectangle Grid.Column="0" Fill="{Binding ColorHex}" Width="5" HorizontalAlignment="Left"/>
|
||||
<TextBlock Grid.Column="1" Text="{Binding PeriodNumber}" FontSize="17" FontWeight="Bold"
|
||||
Width="30" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="8,0"/>
|
||||
<StackPanel Grid.Column="2" Spacing="2">
|
||||
<TextBlock Text="{Binding GroupName}" FontSize="14" FontWeight="SemiBold"/>
|
||||
<TextBlock FontSize="11" Opacity="0.6" IsVisible="{Binding HasRoom}">
|
||||
<Run Text="Raum "/><Run Text="{Binding Room}"/>
|
||||
</TextBlock>
|
||||
<TextBlock Text="{Binding LessonTopic}" FontSize="11" Opacity="0.75"
|
||||
TextWrapping="Wrap" IsVisible="{Binding HasLessonTopic}"/>
|
||||
<TextBlock Text="{Binding ExamTitle}" FontSize="11" Foreground="#D85A30" FontWeight="SemiBold"
|
||||
IsVisible="{Binding HasExam}"/>
|
||||
</StackPanel>
|
||||
<Button Grid.Column="3" Content="Zur Lerngruppe" FontSize="11" Padding="9,4"
|
||||
VerticalAlignment="Center"
|
||||
Command="{Binding $parent[ItemsControl].((vm:TimetableViewModel)DataContext).OpenGroupCommand}"
|
||||
CommandParameter="{Binding GroupId}"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
<TextBlock DockPanel.Dock="Bottom" Text="Heute sind keine Stunden im Stundenplan eingetragen." Classes="emptyhint"
|
||||
IsVisible="{Binding !TodayItems.Count}" Margin="0,8,0,0"/>
|
||||
|
||||
<!-- Rest: Wochenüberblick (nicht editierbar) -->
|
||||
<ScrollViewer>
|
||||
<StackPanel Spacing="14">
|
||||
|
||||
<Grid ColumnDefinitions="Auto,Auto,*,Auto,Auto">
|
||||
<Button Grid.Column="0" Content="‹" FontWeight="Bold" Padding="10,4"
|
||||
Command="{Binding PreviousWeekCommand}" ToolTip.Tip="Vorherige Woche"/>
|
||||
<Button Grid.Column="1" Content="›" FontWeight="Bold" Padding="10,4" Margin="4,0,0,0"
|
||||
Command="{Binding NextWeekCommand}" ToolTip.Tip="Nächste Woche"/>
|
||||
<TextBlock Grid.Column="2" FontSize="16" FontWeight="SemiBold"
|
||||
VerticalAlignment="Center" Margin="12,0,0,0">
|
||||
<Run Text="Woche "/><Run Text="{Binding WeekRangeLabel}"/>
|
||||
</TextBlock>
|
||||
<Button Grid.Column="3" Content="Diese Woche" Margin="0,0,8,0"
|
||||
Command="{Binding CurrentWeekCommand}" IsVisible="{Binding !IsCurrentWeek}"/>
|
||||
<Button Grid.Column="4" Content="Stundenplan bearbeiten" Command="{Binding ShowEditorCommand}"/>
|
||||
</Grid>
|
||||
|
||||
<ItemsControl ItemsSource="{Binding WeekItems}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate><UniformGrid Columns="6"/></ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:WeekCellItem">
|
||||
<Grid Margin="2" MinHeight="76">
|
||||
<Border Classes="weekheader" Classes.today="{Binding IsToday}" CornerRadius="4"
|
||||
IsVisible="{Binding IsHeader}">
|
||||
<TextBlock Text="{Binding Text}" FontWeight="SemiBold" FontSize="12" Opacity="0.75"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" Margin="4"/>
|
||||
</Border>
|
||||
<TextBlock Text="{Binding Text}" IsVisible="{Binding IsPeriodLabel}"
|
||||
FontWeight="SemiBold" FontSize="13"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
<Border Classes="timetablecell" Classes.assigned="{Binding IsAssigned}"
|
||||
CornerRadius="6" IsVisible="{Binding IsSlotCell}">
|
||||
<Button Background="{Binding ColorHex}" IsVisible="{Binding IsAssigned}"
|
||||
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Stretch" CornerRadius="6" Padding="6"
|
||||
Command="{Binding $parent[ItemsControl].((vm:TimetableViewModel)DataContext).OpenGroupCommand}"
|
||||
CommandParameter="{Binding GroupId}">
|
||||
<StackPanel Spacing="1">
|
||||
<TextBlock FontSize="11" FontWeight="Bold" Foreground="White" TextWrapping="Wrap">
|
||||
<Run Text="{Binding SubjectLabel}"/><Run Text=" · "/><Run Text="{Binding GroupName}"/>
|
||||
</TextBlock>
|
||||
<TextBlock Text="{Binding Room}" FontSize="10" Foreground="White" Opacity="0.9"
|
||||
IsVisible="{Binding HasRoom}"/>
|
||||
<TextBlock Text="{Binding Topic}" FontSize="10" Foreground="White" Opacity="0.8"
|
||||
TextWrapping="Wrap" IsVisible="{Binding HasTopic}"/>
|
||||
<TextBlock Text="Ferien" FontSize="10" Foreground="White" FontWeight="SemiBold"
|
||||
Opacity="0.9" IsVisible="{Binding IsHoliday}" Margin="0,2,0,0"/>
|
||||
<StackPanel Orientation="Horizontal" Spacing="3" Margin="0,2,0,0"
|
||||
IsVisible="{Binding !IsHoliday}">
|
||||
<Border Background="#B71C1C" CornerRadius="7" Padding="4,0" IsVisible="{Binding HasHolidayBadge}">
|
||||
<TextBlock Text="{Binding HolidayBadge}" FontSize="9" FontWeight="Bold" Foreground="White"/>
|
||||
</Border>
|
||||
<TextBlock Text="📝" FontSize="11" IsVisible="{Binding HasExam}" ToolTip.Tip="Klausur"/>
|
||||
<TextBlock Text="⏰" FontSize="11" IsVisible="{Binding IsLastBeforeExam}" ToolTip.Tip="Letzte Stunde vor der Klausur"/>
|
||||
<TextBlock Text="🧪" FontSize="11" IsVisible="{Binding HasExperiment}" ToolTip.Tip="Experiment geplant"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Border>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<Border Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||
CornerRadius="8" Padding="16" IsVisible="{Binding HoursWarnings.Count}">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Text="ABWEICHENDE WOCHENSTUNDEN" FontSize="11" FontWeight="Bold" Opacity="0.5"/>
|
||||
<ItemsControl ItemsSource="{Binding HoursWarnings}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:HoursWarningItem">
|
||||
<TextBlock Text="{Binding Text}" FontSize="12" Foreground="#FB8C00"
|
||||
TextWrapping="Wrap" Margin="0,3"/>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</DockPanel>
|
||||
</ContentPage>
|
||||
|
||||
<!-- Tab: Bearbeiten (Raster) -->
|
||||
<ContentPage Header="Bearbeiten">
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="32,20,32,28" Spacing="10">
|
||||
<ItemsControl ItemsSource="{Binding Cells}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate><UniformGrid Columns="6"/></ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:TimetableCellItem">
|
||||
<Grid Margin="2" MinHeight="46">
|
||||
<TextBlock Text="{Binding Text}" IsVisible="{Binding IsHeader}"
|
||||
FontWeight="SemiBold" FontSize="12" Opacity="0.6"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
<TextBlock Text="{Binding Text}" IsVisible="{Binding IsPeriodLabel}"
|
||||
FontWeight="SemiBold" FontSize="13"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
<Border Classes="timetablecell" Classes.assigned="{Binding IsAssigned}"
|
||||
CornerRadius="6" IsVisible="{Binding IsSlotCell}">
|
||||
<Panel>
|
||||
<Button Background="{Binding ColorHex}"
|
||||
IsVisible="{Binding IsAssigned}"
|
||||
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center" CornerRadius="6"
|
||||
Command="{Binding $parent[ItemsControl].((vm:TimetableViewModel)DataContext).EditCellCommand}"
|
||||
CommandParameter="{Binding}">
|
||||
<TextBlock Text="{Binding Text}" FontSize="11" Foreground="White"
|
||||
TextWrapping="Wrap" TextAlignment="Center"/>
|
||||
</Button>
|
||||
<Button Content="+" Opacity="0.35"
|
||||
IsVisible="{Binding !IsAssigned}"
|
||||
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center" Background="Transparent"
|
||||
Command="{Binding $parent[ItemsControl].((vm:TimetableViewModel)DataContext).EditCellCommand}"
|
||||
CommandParameter="{Binding}"/>
|
||||
<Border Background="#D85A30" CornerRadius="8" Padding="5,1"
|
||||
IsVisible="{Binding HasBadge}"
|
||||
HorizontalAlignment="Right" VerticalAlignment="Top" Margin="2">
|
||||
<TextBlock Text="{Binding BadgeText}" FontSize="10" FontWeight="Bold" Foreground="White"/>
|
||||
</Border>
|
||||
</Panel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</ContentPage>
|
||||
|
||||
</TabbedPage>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,33 @@
|
||||
using Avalonia.Controls;
|
||||
using LehrerApp.Core.Interfaces;
|
||||
using LehrerApp.Core.Services;
|
||||
using LehrerApp.Desktop.ViewModels.Planning;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace LehrerApp.Desktop.Views.Planning;
|
||||
|
||||
public partial class TimetableView : UserControl
|
||||
{
|
||||
public TimetableView() => InitializeComponent();
|
||||
|
||||
protected override void OnDataContextChanged(EventArgs e)
|
||||
{
|
||||
base.OnDataContextChanged(e);
|
||||
if (DataContext is TimetableViewModel vm) vm.OnEditSlot = ShowSlotDialog;
|
||||
}
|
||||
|
||||
private async Task ShowSlotDialog(TimetableCellItem cell)
|
||||
{
|
||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||
if (owner is null || cell.Weekday is null) return;
|
||||
|
||||
var vm = new TimetableSlotDialogViewModel(
|
||||
App.Services.GetRequiredService<ITimetableSlotRepository>(),
|
||||
App.Services.GetRequiredService<IGroupRepository>(),
|
||||
App.Services.GetRequiredService<SchoolYearService>(),
|
||||
cell.Weekday.Value, cell.PeriodNumber, cell.Slot);
|
||||
|
||||
var dialog = new TimetableSlotDialog { DataContext = vm };
|
||||
await dialog.ShowDialog(owner);
|
||||
}
|
||||
}
|
||||
@@ -478,6 +478,64 @@
|
||||
</ScrollViewer>
|
||||
</ContentPage>
|
||||
|
||||
<!-- Tab: Ferien & Feiertage (4.3.5) -->
|
||||
<ContentPage Header="Ferien & Feiertage">
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="32,20,32,28" Spacing="14" MaxWidth="480">
|
||||
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Bundesland (für Feiertage)" FontSize="13" FontWeight="SemiBold"/>
|
||||
<ComboBox ItemsSource="{Binding StateOptions}" SelectedItem="{Binding SelectedStateName}"
|
||||
HorizontalAlignment="Stretch"/>
|
||||
</StackPanel>
|
||||
|
||||
<Separator Margin="0,4"/>
|
||||
|
||||
<TextBlock Text="Schulferien" FontSize="16" FontWeight="SemiBold"/>
|
||||
<TextBlock Text="Werden im Stundenplan als unterrichtsfreie Tage angezeigt."
|
||||
FontSize="12" Opacity="0.6" TextWrapping="Wrap"/>
|
||||
|
||||
<ItemsControl ItemsSource="{Binding SchoolHolidayEntries}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="vm:SchoolHolidayItem">
|
||||
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||
BorderThickness="0,0,0,1" Padding="0,7">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0">
|
||||
<TextBlock Text="{Binding Name}" FontSize="13" FontWeight="SemiBold"/>
|
||||
<TextBlock Text="{Binding RangeDisplay}" FontSize="12" Opacity="0.6"/>
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" Content="×" FontSize="14" Padding="9,3"
|
||||
Command="{Binding $parent[ItemsControl].((vm:SettingsViewModel)DataContext).RemoveSchoolHolidayCommand}"
|
||||
CommandParameter="{Binding}"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<TextBlock Text="Noch keine Schulferien hinterlegt." Classes="emptyhint"
|
||||
IsVisible="{Binding !SchoolHolidayEntries.Count}"/>
|
||||
|
||||
<Separator Margin="0,4"/>
|
||||
|
||||
<StackPanel Spacing="4">
|
||||
<TextBox Text="{Binding NewHolidayName}" PlaceholderText="Name (z.B. Sommerferien)"/>
|
||||
<TextBlock Text="{Binding HolidayNameError}" Foreground="Red" FontSize="11"
|
||||
IsVisible="{Binding HolidayNameError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
<Grid ColumnDefinitions="*,8,*">
|
||||
<TextBox Grid.Column="0" Text="{Binding NewHolidayStartText}" PlaceholderText="Beginn TT.MM.JJJJ"/>
|
||||
<TextBox Grid.Column="2" Text="{Binding NewHolidayEndText}" PlaceholderText="Ende TT.MM.JJJJ"/>
|
||||
</Grid>
|
||||
<TextBlock Text="{Binding HolidayDateError}" Foreground="Red" FontSize="11"
|
||||
IsVisible="{Binding HolidayDateError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
<Button Content="+ Schulferien hinzufügen" Command="{Binding AddSchoolHolidayCommand}"
|
||||
HorizontalAlignment="Left"/>
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</ContentPage>
|
||||
|
||||
</TabbedPage>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
||||
Reference in New Issue
Block a user