Unterrichtsplanung: Serienerzeugung, Stundenraster, Aufsichten & Vertretung (Kapitel 4.2/4.3 Nachtrag)
Stunden serienweise aus dem Stundenplan erzeugen (4.2.5); Stundenraster (Uhrzeiten je Stunde) in den Einstellungen mit Zeitbedarf-Rückmeldung im Verlaufsplan-Editor; wiederkehrende Pausenaufsicht; neuer "Vertretung eintragen"-Dialog für einmalige Vertretungsaufsicht, Vertretungsstunde, Sondereinsätze (Ausflüge, Berufsmessen) und schlichten Stundenausfall. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -133,12 +133,15 @@ public static class AppBootstrapper
|
||||
services.AddSingleton<IAttachmentStorage, LiteAttachmentStorage>();
|
||||
services.AddSingleton<ITimetableSlotRepository, TimetableSlotRepository>();
|
||||
services.AddSingleton<ISchoolHolidayRepository, SchoolHolidayRepository>();
|
||||
services.AddSingleton<ISupervisionDutyRepository, SupervisionDutyRepository>();
|
||||
services.AddSingleton<ISubstitutionEntryRepository, SubstitutionEntryRepository>();
|
||||
|
||||
// ── Services ──────────────────────────────────────────────────────────
|
||||
services.AddSingleton<GradingService>();
|
||||
services.AddSingleton<SchoolYearService>();
|
||||
services.AddSingleton<PublicHolidayService>();
|
||||
services.AddSingleton(_ => new SchoolCalendarSettingsService(appData));
|
||||
services.AddSingleton(_ => new PeriodScheduleService(appData));
|
||||
|
||||
// ── Sync (optional – nur wenn Server konfiguriert) ────────────────────
|
||||
services.AddSingleton(_ => new EventQueue(queuePath));
|
||||
|
||||
@@ -2,16 +2,24 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using LehrerApp.Core.Interfaces;
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Core.Services;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
|
||||
namespace LehrerApp.Desktop.ViewModels.Groups;
|
||||
|
||||
// ── Ergebnisse der Verschieben-/Kopieren-Dialoge (4.2.4 / 4.1.4) ─────────────
|
||||
// ── Ergebnisse der Verschieben-/Kopieren-/Serienerzeugungs-Dialoge (4.2.4 / 4.1.4 / 4.2.5) ───
|
||||
|
||||
public record MoveLessonTarget(DateOnly NewDate, bool ShiftFollowing);
|
||||
public record CopyUnitTarget(Guid TargetGroupId, DateOnly AnchorDate);
|
||||
|
||||
public record LessonSeriesResult(int Created, int SkippedHoliday, int SkippedExisting)
|
||||
{
|
||||
public string Summary => $"{Created} Stunde(n) angelegt" +
|
||||
(SkippedHoliday > 0 ? $", {SkippedHoliday} durch Ferien/Feiertage übersprungen" : "") +
|
||||
(SkippedExisting > 0 ? $", {SkippedExisting} bereits vorhanden" : "") + ".";
|
||||
}
|
||||
|
||||
// ── Tab-ViewModel: Unterrichtsplanung (4.1 Einheiten / 4.2 Einzelstunden) ────
|
||||
|
||||
public partial class PlanningTabViewModel : ObservableObject
|
||||
@@ -56,6 +64,7 @@ public partial class PlanningTabViewModel : ObservableObject
|
||||
public Func<LessonSummary, Task<bool>>? OnConfirmDeleteLesson { get; set; }
|
||||
public Func<Lesson, Task<MoveLessonTarget?>>? OnPickMoveTarget { get; set; }
|
||||
public Func<Lesson, Task>? OnShowLesson { get; set; }
|
||||
public Func<Unit, Task<LessonSeriesResult?>>? OnGenerateLessonSeries { get; set; }
|
||||
|
||||
public PlanningTabViewModel(IUnitRepository units, ILessonRepository lessons,
|
||||
IGroupRepository groups, ISubjectRepository subjects,
|
||||
@@ -109,6 +118,7 @@ public partial class PlanningTabViewModel : ObservableObject
|
||||
DeleteUnitCommand.NotifyCanExecuteChanged();
|
||||
CopyUnitCommand.NotifyCanExecuteChanged();
|
||||
AddLessonCommand.NotifyCanExecuteChanged();
|
||||
GenerateLessonSeriesCommand.NotifyCanExecuteChanged();
|
||||
}
|
||||
|
||||
private void LoadLessons()
|
||||
@@ -231,6 +241,16 @@ public partial class PlanningTabViewModel : ObservableObject
|
||||
if (await OnAddLesson(SelectedUnit.Id, _groupId, KnownMaterials, KnownShorthands)) LoadUnits();
|
||||
}
|
||||
|
||||
/// Serienerzeugung von Stunden aus dem Stundenplan (4.2.5) — die eigentliche Logik läuft im
|
||||
/// Dialog (<see cref="GenerateLessonSeriesDialogViewModel"/>), hier wird nur nachgeladen.
|
||||
[RelayCommand(CanExecute = nameof(HasSelectedUnit))]
|
||||
private async Task GenerateLessonSeries()
|
||||
{
|
||||
if (OnGenerateLessonSeries is null || SelectedUnit is null) return;
|
||||
var result = await OnGenerateLessonSeries(SelectedUnit.Model);
|
||||
if (result is not null) LoadUnits();
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(HasSelectedLesson))]
|
||||
private async Task EditLesson()
|
||||
{
|
||||
@@ -558,6 +578,8 @@ public partial class LessonDialogViewModel : ObservableObject
|
||||
{
|
||||
private readonly ILessonRepository _lessons;
|
||||
private readonly IAlternativeLessonPathRepository _alternativePaths;
|
||||
private readonly ITimetableSlotRepository _timetableSlots;
|
||||
private readonly PeriodScheduleService _periodSchedule;
|
||||
private readonly Guid _unitId;
|
||||
private readonly Guid _groupId;
|
||||
private readonly Lesson? _editingLesson;
|
||||
@@ -573,6 +595,9 @@ public partial class LessonDialogViewModel : ObservableObject
|
||||
[ObservableProperty] private string _topicError = "";
|
||||
[ObservableProperty] private string _startTimeTextError = "";
|
||||
[ObservableProperty] private string _totalDurationDisplay = "0 Minuten gesamt";
|
||||
[ObservableProperty] private string _timeBudgetLabel = "";
|
||||
[ObservableProperty] private string _timeBudgetColorHex = "#9E9E9E";
|
||||
[ObservableProperty] private bool _hasTimeBudgetInfo;
|
||||
|
||||
public string[] StatusOptions => LessonStatusDisplay.Options;
|
||||
public string[] MaterialSuggestions { get; }
|
||||
@@ -589,10 +614,12 @@ public partial class LessonDialogViewModel : ObservableObject
|
||||
public string SaveButtonText => _editingLesson is null ? "Anlegen" : "Speichern";
|
||||
|
||||
public LessonDialogViewModel(ILessonRepository lessons, IShorthandCodeRepository shorthandCodes,
|
||||
IAlternativeLessonPathRepository alternativePaths, Guid unitId, Guid groupId,
|
||||
IAlternativeLessonPathRepository alternativePaths, ITimetableSlotRepository timetableSlots,
|
||||
PeriodScheduleService periodSchedule, Guid unitId, Guid groupId,
|
||||
List<string> materialSuggestions, List<string> shorthandHistorySuggestions, Lesson? editingLesson)
|
||||
{
|
||||
_lessons = lessons; _alternativePaths = alternativePaths;
|
||||
_timetableSlots = timetableSlots; _periodSchedule = periodSchedule;
|
||||
_unitId = unitId; _groupId = groupId; _editingLesson = editingLesson;
|
||||
MaterialSuggestions = [.. materialSuggestions];
|
||||
|
||||
@@ -672,6 +699,18 @@ public partial class LessonDialogViewModel : ObservableObject
|
||||
}
|
||||
|
||||
partial void OnStartTimeTextChanged(string value) => RecomputeTimes();
|
||||
/// Übernimmt beim Wählen der Stundennummer auch gleich den Beginn aus dem Stundenraster
|
||||
/// (Einstellungen), sofern noch keiner eingetragen ist — beim Laden einer vorhandenen Stunde
|
||||
/// wird das direkt danach vom tatsächlich gespeicherten <c>StartTime</c> überschrieben (auch
|
||||
/// wenn das "kein Beginn hinterlegt" bedeutet), ein bereits eingetippter Beginn bleibt unangetastet.
|
||||
partial void OnLessonNumberChanged(int? value)
|
||||
{
|
||||
if (value is int period && string.IsNullOrWhiteSpace(StartTimeText) &&
|
||||
_periodSchedule.GetTimes(period) is { } times)
|
||||
StartTimeText = times.Start.ToString("HH:mm");
|
||||
RecomputeTimes();
|
||||
}
|
||||
partial void OnDateTextChanged(string value) => RecomputeTimes();
|
||||
|
||||
/// Dauer ist die primäre Eingabe je Phase; die Uhrzeit wird daraus nur zur Anzeige
|
||||
/// abgeleitet — kumulativ ab "Beginn", sofern gesetzt (sonst bleibt sie leer).
|
||||
@@ -690,8 +729,61 @@ public partial class LessonDialogViewModel : ObservableObject
|
||||
p.ComputedTimeDisplay = cursor is { } c ? $"ab {c:HH:mm}" : "";
|
||||
if (cursor is { } cc) cursor = cc.AddMinutes(p.DurationMinutes);
|
||||
}
|
||||
|
||||
RecomputeTimeBudget(total);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Vergleicht die geplante Gesamtdauer mit der laut Stundenraster (Einstellungen) tatsächlich
|
||||
/// verfügbaren Zeit — Doppelstunden werden erkannt, indem ab der eingetragenen Stundennummer
|
||||
/// so lange die jeweils nächste Periode addiert wird, wie der Stundenplan (4.3) für dieselbe
|
||||
/// Gruppe/denselben Wochentag auch dort einen Slot hat (siehe <see cref="TimetableSlot"/>).
|
||||
/// Ohne erkennbare Stunde/Datum oder ohne im Stundenraster hinterlegte Uhrzeiten bleibt die
|
||||
/// Rückmeldung schlicht ausgeblendet, statt eine erfundene Dauer vorzutäuschen.
|
||||
/// </summary>
|
||||
private void RecomputeTimeBudget(int plannedMinutes)
|
||||
{
|
||||
if (LessonNumber is not int startPeriod ||
|
||||
!DateOnly.TryParseExact(DateText, "dd.MM.yyyy", null, DateTimeStyles.None, out var date))
|
||||
{ HasTimeBudgetInfo = false; return; }
|
||||
|
||||
var weekday = date.DayOfWeek;
|
||||
var groupSlotsByPeriod = _timetableSlots.GetByGroup(_groupId)
|
||||
.Where(s => s.Weekday == weekday)
|
||||
.ToDictionary(s => s.PeriodNumber);
|
||||
|
||||
var available = _periodSchedule.GetDurationMinutes(startPeriod);
|
||||
var period = startPeriod + 1;
|
||||
while (groupSlotsByPeriod.ContainsKey(period))
|
||||
{
|
||||
available += _periodSchedule.GetDurationMinutes(period);
|
||||
period++;
|
||||
}
|
||||
|
||||
if (available <= 0) { HasTimeBudgetInfo = false; return; }
|
||||
|
||||
var utilizationPercent = (double)plannedMinutes / available * 100;
|
||||
TimeBudgetColorHex = TimeBudgetColor(utilizationPercent);
|
||||
TimeBudgetLabel = $"{plannedMinutes} von {available} Minuten geplant ({utilizationPercent:0}%)";
|
||||
HasTimeBudgetInfo = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Farbskala für die Auslastung (geplante / verfügbare Minuten): 93–96 % gilt als guter
|
||||
/// Zielbereich (grün) — ein kleiner Puffer, da 100 % erfahrungsgemäß schon knapp ist. Darüber
|
||||
/// wird es zunehmend rötlich, deutlich über 100 % kräftig rot. Deutlich unter 93 % (zu viel
|
||||
/// Luft) ist bewusst neutral/blau statt rot gehalten — kein Fehler, nur "hier geht noch was".
|
||||
/// </summary>
|
||||
private static string TimeBudgetColor(double utilizationPercent) => utilizationPercent switch
|
||||
{
|
||||
< 70 => "#90A4AE", // Blaugrau: deutlich zu wenig geplant
|
||||
< 93 => "#FFC107", // Gelb: noch Luft nach oben
|
||||
<= 96 => "#43A047", // Grün: guter Zielbereich
|
||||
<= 100 => "#FB8C00", // Orange: knapp, kaum Puffer
|
||||
<= 115 => "#E64A19", // Rotorange: leicht überplant
|
||||
_ => "#B71C1C", // Dunkelrot: deutlich überplant
|
||||
};
|
||||
|
||||
[RelayCommand]
|
||||
private void Save()
|
||||
{
|
||||
@@ -886,6 +978,93 @@ public partial class MoveLessonDialogViewModel : ObservableObject
|
||||
}
|
||||
}
|
||||
|
||||
// ── Dialog: Stunden serienweise aus dem Stundenplan erzeugen (4.2.5) ────────
|
||||
|
||||
public partial class GenerateLessonSeriesDialogViewModel : ObservableObject
|
||||
{
|
||||
private readonly ITimetableSlotRepository _slots;
|
||||
private readonly ILessonRepository _lessons;
|
||||
private readonly ISchoolHolidayRepository _schoolHolidays;
|
||||
private readonly PublicHolidayService _publicHolidays;
|
||||
private readonly SchoolCalendarSettingsService _calendarSettings;
|
||||
private readonly Guid _unitId;
|
||||
private readonly Guid _groupId;
|
||||
|
||||
[ObservableProperty] private string _fromDateText;
|
||||
[ObservableProperty] private string _toDateText;
|
||||
[ObservableProperty] private string _dateError = "";
|
||||
|
||||
public LessonSeriesResult? Result { get; private set; }
|
||||
|
||||
public GenerateLessonSeriesDialogViewModel(ITimetableSlotRepository slots, ILessonRepository lessons,
|
||||
ISchoolHolidayRepository schoolHolidays, PublicHolidayService publicHolidays,
|
||||
SchoolCalendarSettingsService calendarSettings, Guid unitId, Guid groupId,
|
||||
DateOnly? defaultFrom, DateOnly? defaultTo)
|
||||
{
|
||||
_slots = slots; _lessons = lessons; _schoolHolidays = schoolHolidays;
|
||||
_publicHolidays = publicHolidays; _calendarSettings = calendarSettings;
|
||||
_unitId = unitId; _groupId = groupId;
|
||||
_fromDateText = (defaultFrom ?? DateOnly.FromDateTime(DateTime.Today)).ToString("dd.MM.yyyy");
|
||||
_toDateText = (defaultTo ?? DateOnly.FromDateTime(DateTime.Today).AddMonths(1)).ToString("dd.MM.yyyy");
|
||||
}
|
||||
|
||||
/// Legt für jeden Wochentag/Stunde, den die Gruppe laut Stundenplan (4.3) hat, im gewählten
|
||||
/// Zeitraum eine neue Lesson an. Schulferien/Feiertage werden übersprungen (dieselbe Prüfung
|
||||
/// wie im Stundenplan-Wochenraster, siehe TimetableViewModel.IsFreeDay); für ein Datum, an dem
|
||||
/// die Gruppe laut Stundenplan bereits eine Lesson hat (gleiches Datum + gleiche Stundennummer,
|
||||
/// unabhängig von der Einheit — ein Lehrer kann an einem Termin nur eine tatsächliche Stunde
|
||||
/// halten), wird nichts doppelt angelegt. Neue Stunden bekommen bewusst kein Thema — die
|
||||
/// sonst übliche "Thema erforderlich"-Regel des manuellen "+Stunde"-Dialogs gilt hier nicht,
|
||||
/// da diese Platzhalter zum späteren Ausfüllen gedacht sind.
|
||||
[RelayCommand]
|
||||
private void Save()
|
||||
{
|
||||
DateError = "";
|
||||
|
||||
if (!DateOnly.TryParseExact(FromDateText, "dd.MM.yyyy", CultureInfo.InvariantCulture, DateTimeStyles.None, out var from) ||
|
||||
!DateOnly.TryParseExact(ToDateText, "dd.MM.yyyy", CultureInfo.InvariantCulture, DateTimeStyles.None, out var to))
|
||||
{ DateError = "Bitte Beginn und Ende im Format TT.MM.JJJJ angeben."; return; }
|
||||
if (to < from) { DateError = "Das Ende darf nicht vor dem Beginn liegen."; return; }
|
||||
|
||||
var slotsForGroup = _slots.GetByGroup(_groupId);
|
||||
if (slotsForGroup.Count == 0)
|
||||
{ DateError = "Für diese Gruppe ist noch keine Stunde im Stundenplan eingetragen."; return; }
|
||||
|
||||
var schoolHolidays = _schoolHolidays.GetAll();
|
||||
var publicHolidayDates = new HashSet<DateOnly>();
|
||||
for (var year = from.Year; year <= to.Year; year++)
|
||||
foreach (var h in _publicHolidays.GetHolidays(year, _calendarSettings.State)) publicHolidayDates.Add(h.Date);
|
||||
|
||||
var existing = _lessons.GetByGroupAndRange(_groupId, from, to)
|
||||
.Select(l => (l.Date, l.LessonNumber)).ToHashSet();
|
||||
|
||||
int created = 0, skippedHoliday = 0, skippedExisting = 0;
|
||||
for (var date = from; date <= to; date = date.AddDays(1))
|
||||
{
|
||||
var isFreeDay = publicHolidayDates.Contains(date) ||
|
||||
schoolHolidays.Any(h => date >= h.StartDate && date <= h.EndDate);
|
||||
|
||||
foreach (var slot in slotsForGroup.Where(s => s.Weekday == date.DayOfWeek))
|
||||
{
|
||||
if (isFreeDay) { skippedHoliday++; continue; }
|
||||
if (existing.Contains((date, (int?)slot.PeriodNumber))) { skippedExisting++; continue; }
|
||||
|
||||
_lessons.Save(new Lesson
|
||||
{
|
||||
UnitId = _unitId,
|
||||
GroupId = _groupId,
|
||||
Date = date,
|
||||
LessonNumber = slot.PeriodNumber,
|
||||
Topic = "",
|
||||
});
|
||||
created++;
|
||||
}
|
||||
}
|
||||
|
||||
Result = new LessonSeriesResult(created, skippedHoliday, skippedExisting);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Dialog: Einheit als Vorlage in andere Gruppe kopieren (4.1.4) ────────────
|
||||
|
||||
public partial class CopyUnitDialogViewModel : ObservableObject
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using LehrerApp.Core.Interfaces;
|
||||
using LehrerApp.Core.Models;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
|
||||
namespace LehrerApp.Desktop.ViewModels.Planning;
|
||||
|
||||
public static class SubstitutionKindDisplay
|
||||
{
|
||||
public static string[] Options { get; } = ["Aufsicht", "Stunde", "Sondereinsatz", "Ausfall"];
|
||||
|
||||
public static string ToName(SubstitutionKind k) => k switch
|
||||
{
|
||||
SubstitutionKind.Lesson => "Stunde",
|
||||
SubstitutionKind.SpecialAssignment => "Sondereinsatz",
|
||||
SubstitutionKind.Cancelled => "Ausfall",
|
||||
_ => "Aufsicht",
|
||||
};
|
||||
|
||||
public static SubstitutionKind FromName(string? name) => name switch
|
||||
{
|
||||
"Stunde" => SubstitutionKind.Lesson,
|
||||
"Sondereinsatz" => SubstitutionKind.SpecialAssignment,
|
||||
"Ausfall" => SubstitutionKind.Cancelled,
|
||||
_ => SubstitutionKind.Supervision,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dialog "Vertretung eintragen" (4.3 Nachtrag, Sonderfälle): einmalige Vertretungsaufsicht,
|
||||
/// Vertretungsstunde in einer eigenen oder fremden Lerngruppe, ein Sondereinsatz (Ausflug,
|
||||
/// Berufsmesse, Exkursion, ...), oder ein schlichter Stundenausfall (z.B. weil die betroffene
|
||||
/// Gruppe selbst auf Klassenfahrt ist — nicht die eigene Abwesenheit, sondern die der Gruppe).
|
||||
/// Bei eigener Gruppe bleibt der einfache Weg (nur Plan-Eintrag mit Thema) der Standard —
|
||||
/// "Direkt als Stunde in der Einheit übernehmen" ist ein bewusstes Opt-in, nur bei
|
||||
/// Vertretungsstunden, damit die meist beiläufigen Vertretungsstunden nicht automatisch die
|
||||
/// Fortschrittsanzeige/Reihenfolge der Einheit durcheinanderbringen (siehe TODO.md, Nachtrag zu
|
||||
/// 4.3). Sondereinsatz und Ausfall sind bewusst nie als Einheiten-Stunde übernehmbar — ein
|
||||
/// Ausflug ist inhaltlich kein Verlaufsplan-Eintrag, und ein Ausfall ist per Definition keine
|
||||
/// gehaltene Stunde.
|
||||
/// </summary>
|
||||
public partial class SubstitutionEntryDialogViewModel : ObservableObject
|
||||
{
|
||||
private readonly ISubstitutionEntryRepository _substitutions;
|
||||
private readonly IUnitRepository _units;
|
||||
private readonly ILessonRepository _lessons;
|
||||
|
||||
[ObservableProperty] private string _dateText = DateOnly.FromDateTime(DateTime.Today).ToString("dd.MM.yyyy");
|
||||
[ObservableProperty] private string _kindName = SubstitutionKindDisplay.Options[0];
|
||||
[ObservableProperty] private int _afterPeriod;
|
||||
[ObservableProperty] private int _periodNumber = 1;
|
||||
[ObservableProperty] private bool _isAllDay = true;
|
||||
[ObservableProperty] private int _fromPeriod = 1;
|
||||
[ObservableProperty] private int _toPeriod = 10;
|
||||
[ObservableProperty] private LearningGroup? _selectedOwnGroup;
|
||||
[ObservableProperty] private string _groupLabel = "";
|
||||
[ObservableProperty] private string _description = "";
|
||||
[ObservableProperty] private bool _promoteToLesson;
|
||||
[ObservableProperty] private Unit? _selectedUnit;
|
||||
[ObservableProperty] private string _dateError = "";
|
||||
[ObservableProperty] private string _validationError = "";
|
||||
|
||||
public string[] KindOptions => SubstitutionKindDisplay.Options;
|
||||
public bool IsSupervisionKind => KindName == "Aufsicht";
|
||||
public bool IsLessonKind => KindName == "Stunde";
|
||||
public bool IsSpecialAssignmentKind => KindName == "Sondereinsatz";
|
||||
public bool IsCancelledKind => KindName == "Ausfall";
|
||||
public bool CanPromoteToLesson => IsLessonKind && SelectedOwnGroup is not null && UnitsOfSelectedGroup.Count > 0;
|
||||
|
||||
public ObservableCollection<LearningGroup> OwnGroups { get; } = [];
|
||||
public ObservableCollection<Unit> UnitsOfSelectedGroup { get; } = [];
|
||||
|
||||
public SubstitutionEntry? Result { get; private set; }
|
||||
|
||||
public SubstitutionEntryDialogViewModel(ISubstitutionEntryRepository substitutions, IGroupRepository groups,
|
||||
IUnitRepository units, ILessonRepository lessons)
|
||||
{
|
||||
_substitutions = substitutions; _units = units; _lessons = lessons;
|
||||
foreach (var g in groups.GetAll().OrderBy(g => g.Name)) OwnGroups.Add(g);
|
||||
}
|
||||
|
||||
partial void OnKindNameChanged(string value)
|
||||
{
|
||||
OnPropertyChanged(nameof(IsSupervisionKind));
|
||||
OnPropertyChanged(nameof(IsLessonKind));
|
||||
OnPropertyChanged(nameof(IsSpecialAssignmentKind));
|
||||
OnPropertyChanged(nameof(IsCancelledKind));
|
||||
OnPropertyChanged(nameof(CanPromoteToLesson));
|
||||
}
|
||||
|
||||
partial void OnSelectedOwnGroupChanged(LearningGroup? value)
|
||||
{
|
||||
UnitsOfSelectedGroup.Clear();
|
||||
if (value is not null)
|
||||
{
|
||||
GroupLabel = value.Name;
|
||||
foreach (var u in _units.GetByGroup(value.Id)) UnitsOfSelectedGroup.Add(u);
|
||||
}
|
||||
SelectedUnit = UnitsOfSelectedGroup.FirstOrDefault();
|
||||
PromoteToLesson = false;
|
||||
OnPropertyChanged(nameof(CanPromoteToLesson));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Save()
|
||||
{
|
||||
DateError = ""; ValidationError = "";
|
||||
|
||||
if (!DateOnly.TryParseExact(DateText, "dd.MM.yyyy", CultureInfo.InvariantCulture, DateTimeStyles.None, out var date))
|
||||
{ DateError = "Format TT.MM.JJJJ."; return; }
|
||||
|
||||
var kind = SubstitutionKindDisplay.FromName(KindName);
|
||||
var entry = new SubstitutionEntry { Date = date, Kind = kind };
|
||||
|
||||
switch (kind)
|
||||
{
|
||||
case SubstitutionKind.Supervision:
|
||||
if (AfterPeriod is < 0 or > 10) { ValidationError = "Pause muss zwischen 0 und 10 liegen."; return; }
|
||||
if (string.IsNullOrWhiteSpace(Description)) { ValidationError = "Grund/Ort erforderlich."; return; }
|
||||
entry.AfterPeriod = AfterPeriod;
|
||||
entry.Description = Description.Trim();
|
||||
break;
|
||||
|
||||
case SubstitutionKind.Lesson:
|
||||
if (PeriodNumber is < 1 or > 10) { ValidationError = "Stunde muss zwischen 1 und 10 liegen."; return; }
|
||||
if (string.IsNullOrWhiteSpace(GroupLabel)) { ValidationError = "Gruppe/Bezeichnung erforderlich."; return; }
|
||||
if (string.IsNullOrWhiteSpace(Description)) { ValidationError = "Thema erforderlich."; return; }
|
||||
entry.PeriodNumber = PeriodNumber;
|
||||
entry.GroupId = SelectedOwnGroup?.Id;
|
||||
entry.GroupLabel = GroupLabel.Trim();
|
||||
entry.Description = Description.Trim();
|
||||
break;
|
||||
|
||||
case SubstitutionKind.SpecialAssignment:
|
||||
if (string.IsNullOrWhiteSpace(Description)) { ValidationError = "Bezeichnung erforderlich."; return; }
|
||||
if (!IsAllDay)
|
||||
{
|
||||
if (FromPeriod is < 1 or > 10 || ToPeriod is < 1 or > 10)
|
||||
{ ValidationError = "Stunden müssen zwischen 1 und 10 liegen."; return; }
|
||||
if (ToPeriod < FromPeriod)
|
||||
{ ValidationError = "Die Bis-Stunde darf nicht vor der Von-Stunde liegen."; return; }
|
||||
entry.FromPeriod = FromPeriod;
|
||||
entry.ToPeriod = ToPeriod;
|
||||
}
|
||||
entry.IsAllDay = IsAllDay;
|
||||
entry.GroupId = SelectedOwnGroup?.Id;
|
||||
entry.GroupLabel = GroupLabel.Trim();
|
||||
entry.Description = Description.Trim();
|
||||
break;
|
||||
|
||||
case SubstitutionKind.Cancelled:
|
||||
if (PeriodNumber is < 1 or > 10) { ValidationError = "Stunde muss zwischen 1 und 10 liegen."; return; }
|
||||
entry.PeriodNumber = PeriodNumber;
|
||||
entry.Description = Description.Trim();
|
||||
break;
|
||||
}
|
||||
|
||||
_substitutions.Save(entry);
|
||||
|
||||
if (kind == SubstitutionKind.Lesson && PromoteToLesson && SelectedOwnGroup is not null && SelectedUnit is not null)
|
||||
{
|
||||
_lessons.Save(new Lesson
|
||||
{
|
||||
UnitId = SelectedUnit.Id,
|
||||
GroupId = SelectedOwnGroup.Id,
|
||||
Date = date,
|
||||
LessonNumber = PeriodNumber,
|
||||
Topic = Description.Trim(),
|
||||
});
|
||||
}
|
||||
|
||||
Result = entry;
|
||||
}
|
||||
}
|
||||
@@ -58,11 +58,15 @@ public partial class TimetableViewModel : ObservableObject
|
||||
private readonly SchoolCalendarSettingsService _calendarSettings;
|
||||
private readonly PublicHolidayService _publicHolidays;
|
||||
private readonly SchoolYearService _schoolYear;
|
||||
private readonly ISupervisionDutyRepository _supervisionDuties;
|
||||
private readonly ISubstitutionEntryRepository _substitutions;
|
||||
|
||||
public ObservableCollection<TimetableCellItem> Cells { get; } = [];
|
||||
public ObservableCollection<WeekCellItem> WeekItems { get; } = [];
|
||||
public ObservableCollection<HoursWarningItem> HoursWarnings { get; } = [];
|
||||
public ObservableCollection<TodayLessonItem> TodayItems { get; } = [];
|
||||
public ObservableCollection<TodaySupervisionItem> TodaySupervisions { get; } = [];
|
||||
public ObservableCollection<TodaySpecialAssignmentItem> TodaySpecialAssignments { get; } = [];
|
||||
|
||||
[ObservableProperty] private int _activeTabIndex;
|
||||
[ObservableProperty] private string _todayLabel = "";
|
||||
@@ -72,15 +76,18 @@ public partial class TimetableViewModel : ObservableObject
|
||||
|
||||
public Func<TimetableCellItem, Task>? OnEditSlot { get; set; }
|
||||
public Action<Guid>? OnNavigateToGroup { get; set; }
|
||||
public Func<Task>? OnAddSubstitution { get; set; }
|
||||
|
||||
public TimetableViewModel(ITimetableSlotRepository slots, IGroupRepository groups,
|
||||
ISubjectRepository subjects, ILessonRepository lessons, IExamRepository exams,
|
||||
ISchoolHolidayRepository schoolHolidays, SchoolCalendarSettingsService calendarSettings,
|
||||
PublicHolidayService publicHolidays, SchoolYearService schoolYear)
|
||||
PublicHolidayService publicHolidays, SchoolYearService schoolYear,
|
||||
ISupervisionDutyRepository supervisionDuties, ISubstitutionEntryRepository substitutions)
|
||||
{
|
||||
_slots = slots; _groups = groups; _subjects = subjects; _lessons = lessons; _exams = exams;
|
||||
_schoolHolidays = schoolHolidays; _calendarSettings = calendarSettings;
|
||||
_publicHolidays = publicHolidays; _schoolYear = schoolYear;
|
||||
_supervisionDuties = supervisionDuties; _substitutions = substitutions;
|
||||
Load();
|
||||
}
|
||||
|
||||
@@ -95,6 +102,14 @@ public partial class TimetableViewModel : ObservableObject
|
||||
[RelayCommand]
|
||||
private void CurrentWeek() { WeekOffset = 0; Load(); }
|
||||
|
||||
[RelayCommand]
|
||||
private async Task AddSubstitution()
|
||||
{
|
||||
if (OnAddSubstitution is null) return;
|
||||
await OnAddSubstitution();
|
||||
Load();
|
||||
}
|
||||
|
||||
public void Load()
|
||||
{
|
||||
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||
@@ -106,33 +121,90 @@ public partial class TimetableViewModel : ObservableObject
|
||||
|
||||
var holidayBadges = ComputeHolidayBadges(today, publicHolidayDates);
|
||||
var examProximity = ComputeExamProximity(today, publicHolidayDates);
|
||||
var duties = _supervisionDuties.GetAll();
|
||||
|
||||
BuildGrid(holidayBadges);
|
||||
BuildWeekOverview(today, publicHolidayDates);
|
||||
BuildGrid(holidayBadges, duties);
|
||||
BuildWeekOverview(today, publicHolidayDates, duties);
|
||||
BuildToday(today);
|
||||
BuildHoursWarnings();
|
||||
}
|
||||
|
||||
// ── "Heute": Tagesliste ──────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Vertretungsstunden (<see cref="SubstitutionEntry"/>, Kind Lesson) überschreiben für ihre
|
||||
/// Stunde die normale, aus dem Stundenplan abgeleitete Anzeige — sie beschreiben, was an
|
||||
/// diesem konkreten Tag tatsächlich stattfindet. Stunden ohne passenden Stundenplan-Slot (z.B.
|
||||
/// Vertretung in einer fremden Gruppe) werden zusätzlich angehängt.
|
||||
/// </summary>
|
||||
private void BuildToday(DateOnly today)
|
||||
{
|
||||
TodayItems.Clear();
|
||||
var slotsToday = _slots.GetAll().Where(s => s.Weekday == today.DayOfWeek).OrderBy(s => s.PeriodNumber).ToList();
|
||||
var substitutionsToday = _substitutions.GetByDate(today);
|
||||
var slotsToday = _slots.GetAll().Where(s => s.Weekday == today.DayOfWeek).ToList();
|
||||
var items = new List<TodayLessonItem>();
|
||||
var coveredPeriods = new HashSet<int>();
|
||||
|
||||
foreach (var slot in slotsToday)
|
||||
{
|
||||
coveredPeriods.Add(slot.PeriodNumber);
|
||||
var lessonSub = substitutionsToday.FirstOrDefault(s => s.Kind == SubstitutionKind.Lesson && s.PeriodNumber == slot.PeriodNumber);
|
||||
if (lessonSub is not null) { items.Add(TodayLessonItem.ForSubstitution(slot.PeriodNumber, lessonSub)); continue; }
|
||||
|
||||
var group = _groups.GetById(slot.GroupId);
|
||||
if (group is null) continue;
|
||||
|
||||
var cancelled = substitutionsToday.FirstOrDefault(s => s.Kind == SubstitutionKind.Cancelled && s.PeriodNumber == slot.PeriodNumber);
|
||||
if (cancelled is not null) { items.Add(TodayLessonItem.ForCancelled(slot.GroupId, slot.PeriodNumber, group.Name, cancelled)); 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,
|
||||
items.Add(new TodayLessonItem(slot.GroupId, slot.PeriodNumber, group.Name,
|
||||
slot.Room ?? "", ColorFor(group.Name), lesson?.Topic, exam?.Title));
|
||||
}
|
||||
|
||||
foreach (var sub in substitutionsToday.Where(s => s.Kind == SubstitutionKind.Lesson &&
|
||||
s.PeriodNumber is int p && !coveredPeriods.Contains(p)))
|
||||
items.Add(TodayLessonItem.ForSubstitution(sub.PeriodNumber!.Value, sub));
|
||||
|
||||
TodayItems.Clear();
|
||||
foreach (var item in items.OrderBy(i => i.PeriodNumber)) TodayItems.Add(item);
|
||||
|
||||
TodaySupervisions.Clear();
|
||||
foreach (var item in BuildTodaySupervisionItems(today, substitutionsToday)) TodaySupervisions.Add(item);
|
||||
|
||||
TodaySpecialAssignments.Clear();
|
||||
foreach (var sub in substitutionsToday.Where(s => s.Kind == SubstitutionKind.SpecialAssignment))
|
||||
{
|
||||
var periodLabel = sub.IsAllDay ? "Ganztägig" : $"{sub.FromPeriod}.–{sub.ToPeriod}. Stunde";
|
||||
TodaySpecialAssignments.Add(new TodaySpecialAssignmentItem(periodLabel, sub.Description, sub.GroupLabel));
|
||||
}
|
||||
}
|
||||
|
||||
private List<TodaySupervisionItem> BuildTodaySupervisionItems(DateOnly today, List<SubstitutionEntry> substitutionsToday)
|
||||
{
|
||||
var dutiesToday = _supervisionDuties.GetAll().Where(d => d.Weekday == today.DayOfWeek).ToList();
|
||||
var subsToday = substitutionsToday.Where(s => s.Kind == SubstitutionKind.Supervision).ToList();
|
||||
var afterPeriods = dutiesToday.Select(d => d.AfterPeriod)
|
||||
.Concat(subsToday.Select(s => s.AfterPeriod!.Value))
|
||||
.Distinct().OrderBy(p => p);
|
||||
|
||||
var result = new List<TodaySupervisionItem>();
|
||||
foreach (var afterPeriod in afterPeriods)
|
||||
{
|
||||
var sub = subsToday.FirstOrDefault(s => s.AfterPeriod == afterPeriod);
|
||||
if (sub is not null) { result.Add(new TodaySupervisionItem(afterPeriod, sub.Description, isSubstitution: true)); continue; }
|
||||
var duty = dutiesToday.First(d => d.AfterPeriod == afterPeriod);
|
||||
result.Add(new TodaySupervisionItem(afterPeriod, duty.Location, isSubstitution: false));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void OpenGroup(Guid groupId) => OnNavigateToGroup?.Invoke(groupId);
|
||||
private void OpenGroup(Guid groupId)
|
||||
{
|
||||
if (groupId == Guid.Empty) return; // Vertretung in fremder Gruppe ohne echte GroupId
|
||||
OnNavigateToGroup?.Invoke(groupId);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ShowEditor() => ActiveTabIndex = 1;
|
||||
@@ -146,9 +218,11 @@ public partial class TimetableViewModel : ObservableObject
|
||||
/// 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.
|
||||
/// das eigentlich zu einer ganz anderen Woche gehört. Aufsicht-Zeilen (wiederkehrend + einmalige
|
||||
/// Vertretungsaufsicht) werden zwischen den betroffenen Stundenzeilen eingefügt, Vertretungsstunden
|
||||
/// ersetzen für ihre Stunde die sonst aus dem Stundenplan abgeleitete Anzeige.
|
||||
/// </summary>
|
||||
private void BuildWeekOverview(DateOnly today, HashSet<DateOnly> publicHolidayDates)
|
||||
private void BuildWeekOverview(DateOnly today, HashSet<DateOnly> publicHolidayDates, List<SupervisionDuty> duties)
|
||||
{
|
||||
WeekItems.Clear();
|
||||
var currentWeekMonday = today.AddDays(-((int)today.DayOfWeek + 6) % 7);
|
||||
@@ -160,22 +234,54 @@ public partial class TimetableViewModel : ObservableObject
|
||||
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));
|
||||
var dutiesByPeriod = duties.ToLookup(d => d.AfterPeriod);
|
||||
var substitutionsThisWeek = dateByWeekday.Values.SelectMany(d => _substitutions.GetByDate(d)).ToList();
|
||||
|
||||
WeekItems.Add(WeekCellItem.Corner());
|
||||
foreach (var weekday in Weekdays)
|
||||
WeekItems.Add(WeekCellItem.WeekdayHeader(weekday, dateByWeekday[weekday], dateByWeekday[weekday] == today));
|
||||
|
||||
AddWeekSupervisionRowIfAny(0, dutiesByPeriod, substitutionsThisWeek, dateByWeekday);
|
||||
|
||||
for (var period = FirstPeriod; period <= LastPeriod; period++)
|
||||
{
|
||||
WeekItems.Add(WeekCellItem.PeriodLabel(period));
|
||||
foreach (var weekday in Weekdays)
|
||||
{
|
||||
var date = dateByWeekday[weekday];
|
||||
var lessonSub = substitutionsThisWeek.FirstOrDefault(s =>
|
||||
s.Kind == SubstitutionKind.Lesson && s.Date == date && s.PeriodNumber == period);
|
||||
if (lessonSub is not null)
|
||||
{
|
||||
WeekItems.Add(WeekCellItem.ForSubstitutionLesson(weekday, period, date == today, lessonSub));
|
||||
continue;
|
||||
}
|
||||
|
||||
var specialAssignment = substitutionsThisWeek.FirstOrDefault(s =>
|
||||
s.Kind == SubstitutionKind.SpecialAssignment && s.Date == date &&
|
||||
(s.IsAllDay || (period >= s.FromPeriod && period <= s.ToPeriod)));
|
||||
if (specialAssignment is not null)
|
||||
{
|
||||
WeekItems.Add(WeekCellItem.ForSpecialAssignment(weekday, period, date == today, specialAssignment));
|
||||
continue;
|
||||
}
|
||||
|
||||
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 cancelled = substitutionsThisWeek.FirstOrDefault(s =>
|
||||
s.Kind == SubstitutionKind.Cancelled && s.Date == date && s.PeriodNumber == period);
|
||||
if (cancelled is not null)
|
||||
{
|
||||
WeekItems.Add(WeekCellItem.ForCancelled(weekday, period, date == today, cancelled,
|
||||
subject?.ShortName is { Length: > 0 } csn ? csn : subject?.Name ?? "",
|
||||
group?.Name ?? "?", slot.GroupId));
|
||||
continue;
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -189,6 +295,30 @@ public partial class TimetableViewModel : ObservableObject
|
||||
colorHex, holidayBadge, hasExam, isLastBeforeExam,
|
||||
MentionsExperiment(lesson), slot.GroupId, isHoliday));
|
||||
}
|
||||
AddWeekSupervisionRowIfAny(period, dutiesByPeriod, substitutionsThisWeek, dateByWeekday);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddWeekSupervisionRowIfAny(int afterPeriod, ILookup<int, SupervisionDuty> dutiesByPeriod,
|
||||
List<SubstitutionEntry> substitutionsThisWeek, Dictionary<DayOfWeek, DateOnly> dateByWeekday)
|
||||
{
|
||||
var hasAny = dutiesByPeriod.Contains(afterPeriod) ||
|
||||
substitutionsThisWeek.Any(s => s.Kind == SubstitutionKind.Supervision && s.AfterPeriod == afterPeriod);
|
||||
if (!hasAny) return;
|
||||
|
||||
WeekItems.Add(WeekCellItem.SupervisionRowLabel(afterPeriod));
|
||||
foreach (var weekday in Weekdays)
|
||||
{
|
||||
var date = dateByWeekday[weekday];
|
||||
var substitution = substitutionsThisWeek.FirstOrDefault(s =>
|
||||
s.Kind == SubstitutionKind.Supervision && s.Date == date && s.AfterPeriod == afterPeriod);
|
||||
if (substitution is not null)
|
||||
{
|
||||
WeekItems.Add(WeekCellItem.SupervisionCell(substitution.Description, isSubstitution: true));
|
||||
continue;
|
||||
}
|
||||
var duty = dutiesByPeriod[afterPeriod].FirstOrDefault(d => d.Weekday == weekday);
|
||||
WeekItems.Add(WeekCellItem.SupervisionCell(duty?.Location ?? "", isSubstitution: false));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,16 +356,19 @@ public partial class TimetableViewModel : ObservableObject
|
||||
|
||||
// ── Bearbeiten-Raster ─────────────────────────────────────────────────────
|
||||
|
||||
private void BuildGrid(Dictionary<(DayOfWeek Weekday, Guid GroupId), string> badges)
|
||||
private void BuildGrid(Dictionary<(DayOfWeek Weekday, Guid GroupId), string> badges, List<SupervisionDuty> duties)
|
||||
{
|
||||
Cells.Clear();
|
||||
var allSlots = _slots.GetAll();
|
||||
var groupNames = _groups.GetAll(includeInactive: true).ToDictionary(g => g.Id, g => g.Name);
|
||||
var dutiesByPeriod = duties.ToLookup(d => d.AfterPeriod);
|
||||
|
||||
Cells.Add(TimetableCellItem.Corner());
|
||||
foreach (var weekday in Weekdays)
|
||||
Cells.Add(TimetableCellItem.WeekdayHeader(weekday));
|
||||
|
||||
AddGridSupervisionRowIfAny(0, dutiesByPeriod);
|
||||
|
||||
for (var period = FirstPeriod; period <= LastPeriod; period++)
|
||||
{
|
||||
Cells.Add(TimetableCellItem.PeriodLabel(period));
|
||||
@@ -246,6 +379,21 @@ public partial class TimetableViewModel : ObservableObject
|
||||
var badge = slot is not null ? badges.GetValueOrDefault((weekday, slot.GroupId), "") : "";
|
||||
Cells.Add(TimetableCellItem.ForSlot(weekday, period, slot, groupName, ColorFor(groupName), badge));
|
||||
}
|
||||
AddGridSupervisionRowIfAny(period, dutiesByPeriod);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Aufsicht wird nur in den Einstellungen gepflegt (siehe SettingsView) — im
|
||||
/// Bearbeiten-Raster ist die Zeile bewusst nur Anzeige, kein weiterer Klick-Dialog nötig für
|
||||
/// eine so kleine, seltene Pflegeaufgabe.</summary>
|
||||
private void AddGridSupervisionRowIfAny(int afterPeriod, ILookup<int, SupervisionDuty> dutiesByPeriod)
|
||||
{
|
||||
if (!dutiesByPeriod.Contains(afterPeriod)) return;
|
||||
Cells.Add(TimetableCellItem.SupervisionRowLabel(afterPeriod));
|
||||
foreach (var weekday in Weekdays)
|
||||
{
|
||||
var duty = dutiesByPeriod[afterPeriod].FirstOrDefault(d => d.Weekday == weekday);
|
||||
Cells.Add(TimetableCellItem.SupervisionCell(duty?.Location ?? ""));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,6 +493,8 @@ public class TimetableCellItem
|
||||
{
|
||||
public bool IsHeader { get; private init; }
|
||||
public bool IsPeriodLabel { get; private init; }
|
||||
public bool IsSupervisionRow { get; private init; }
|
||||
public bool IsSupervisionCell { get; private init; }
|
||||
public string Text { get; private init; } = "";
|
||||
public DayOfWeek? Weekday { get; private init; }
|
||||
public int PeriodNumber { get; private init; }
|
||||
@@ -352,9 +502,11 @@ public class TimetableCellItem
|
||||
public string GroupName { get; private init; } = "";
|
||||
public string ColorHex { get; private init; } = "#9E9E9E";
|
||||
public string BadgeText { get; private init; } = "";
|
||||
public string SupervisionLocation { get; private init; } = "";
|
||||
public bool HasSupervision => SupervisionLocation.Length > 0;
|
||||
public bool HasBadge => BadgeText.Length > 0;
|
||||
public bool IsAssigned => Slot is not null;
|
||||
public bool IsSlotCell => !IsHeader && !IsPeriodLabel;
|
||||
public bool IsSlotCell => !IsHeader && !IsPeriodLabel && !IsSupervisionRow && !IsSupervisionCell;
|
||||
|
||||
public static TimetableCellItem Corner() => new() { IsHeader = true, Text = "" };
|
||||
|
||||
@@ -370,6 +522,18 @@ public class TimetableCellItem
|
||||
|
||||
public static TimetableCellItem PeriodLabel(int period) => new() { IsPeriodLabel = true, Text = period.ToString() };
|
||||
|
||||
public static TimetableCellItem SupervisionRowLabel(int afterPeriod) => new()
|
||||
{
|
||||
IsSupervisionRow = true,
|
||||
Text = afterPeriod == 0 ? "Aufsicht (vor 1.)" : $"Aufsicht (n. {afterPeriod}.)",
|
||||
};
|
||||
|
||||
public static TimetableCellItem SupervisionCell(string location) => new()
|
||||
{
|
||||
IsSupervisionCell = true,
|
||||
SupervisionLocation = location,
|
||||
};
|
||||
|
||||
public static TimetableCellItem ForSlot(DayOfWeek day, int period, TimetableSlot? slot, string groupName,
|
||||
string colorHex, string badgeText) => new()
|
||||
{
|
||||
@@ -384,8 +548,14 @@ public class WeekCellItem
|
||||
{
|
||||
public bool IsHeader { get; private init; }
|
||||
public bool IsPeriodLabel { get; private init; }
|
||||
public bool IsSlotCell => !IsHeader && !IsPeriodLabel;
|
||||
public bool IsSupervisionRow { get; private init; }
|
||||
public bool IsSupervisionCell { get; private init; }
|
||||
public bool IsSlotCell => !IsHeader && !IsPeriodLabel && !IsSupervisionRow && !IsSupervisionCell;
|
||||
public bool IsAssigned { get; private init; }
|
||||
public bool IsSubstitutionLesson { get; private init; }
|
||||
public bool IsSubstitutionSupervision { get; private init; }
|
||||
public bool IsSpecialAssignment { get; private init; }
|
||||
public bool IsCancelled { get; private init; }
|
||||
public string Text { get; private init; } = "";
|
||||
public DayOfWeek? Weekday { get; private init; }
|
||||
public int PeriodNumber { get; private init; }
|
||||
@@ -402,6 +572,8 @@ public class WeekCellItem
|
||||
public bool IsLastBeforeExam { get; private init; }
|
||||
public bool HasExperiment { get; private init; }
|
||||
public bool IsHoliday { get; private init; }
|
||||
public string SupervisionLocation { get; private init; } = "";
|
||||
public bool HasSupervision => SupervisionLocation.Length > 0;
|
||||
public bool HasRoom => !string.IsNullOrWhiteSpace(Room);
|
||||
public bool HasTopic => !string.IsNullOrWhiteSpace(Topic);
|
||||
|
||||
@@ -422,6 +594,19 @@ public class WeekCellItem
|
||||
|
||||
public static WeekCellItem Empty(DayOfWeek day, int period) => new() { Weekday = day, PeriodNumber = period };
|
||||
|
||||
public static WeekCellItem SupervisionRowLabel(int afterPeriod) => new()
|
||||
{
|
||||
IsSupervisionRow = true,
|
||||
Text = afterPeriod == 0 ? "Aufsicht (vor 1.)" : $"Aufsicht (n. {afterPeriod}.)",
|
||||
};
|
||||
|
||||
public static WeekCellItem SupervisionCell(string location, bool isSubstitution) => new()
|
||||
{
|
||||
IsSupervisionCell = true,
|
||||
SupervisionLocation = location,
|
||||
IsSubstitutionSupervision = isSubstitution,
|
||||
};
|
||||
|
||||
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()
|
||||
@@ -432,6 +617,30 @@ public class WeekCellItem
|
||||
IsLastBeforeExam = isLastBeforeExam, HasExperiment = hasExperiment, GroupId = groupId,
|
||||
IsHoliday = isHoliday,
|
||||
};
|
||||
|
||||
public static WeekCellItem ForSubstitutionLesson(DayOfWeek day, int period, bool isToday, SubstitutionEntry entry) => new()
|
||||
{
|
||||
Weekday = day, PeriodNumber = period, IsAssigned = true, IsToday = isToday, IsSubstitutionLesson = true,
|
||||
SubjectLabel = "Vertretung", GroupName = entry.GroupLabel, Topic = entry.Description,
|
||||
ColorHex = "#8E24AA", GroupId = entry.GroupId ?? Guid.Empty,
|
||||
};
|
||||
|
||||
public static WeekCellItem ForSpecialAssignment(DayOfWeek day, int period, bool isToday, SubstitutionEntry entry) => new()
|
||||
{
|
||||
Weekday = day, PeriodNumber = period, IsAssigned = true, IsToday = isToday, IsSpecialAssignment = true,
|
||||
SubjectLabel = "Sondereinsatz",
|
||||
GroupName = string.IsNullOrWhiteSpace(entry.GroupLabel) ? "" : entry.GroupLabel,
|
||||
Topic = entry.Description,
|
||||
ColorHex = "#00838F", GroupId = entry.GroupId ?? Guid.Empty,
|
||||
};
|
||||
|
||||
public static WeekCellItem ForCancelled(DayOfWeek day, int period, bool isToday, SubstitutionEntry entry,
|
||||
string subjectLabel, string groupName, Guid groupId) => new()
|
||||
{
|
||||
Weekday = day, PeriodNumber = period, IsAssigned = true, IsToday = isToday, IsCancelled = true,
|
||||
SubjectLabel = subjectLabel, GroupName = groupName, Topic = entry.Description,
|
||||
ColorHex = "#757575", GroupId = groupId,
|
||||
};
|
||||
}
|
||||
|
||||
public class HoursWarningItem(string groupName, int assigned, int expected)
|
||||
@@ -440,17 +649,53 @@ public class HoursWarningItem(string groupName, int assigned, int expected)
|
||||
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 class TodayLessonItem
|
||||
{
|
||||
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 Guid GroupId { get; private init; }
|
||||
public int PeriodNumber { get; private init; }
|
||||
public string GroupName { get; private init; } = "";
|
||||
public string Room { get; private init; } = "";
|
||||
public string ColorHex { get; private init; } = "#9E9E9E";
|
||||
public string? LessonTopic { get; private init; }
|
||||
public string? ExamTitle { get; private init; }
|
||||
public bool IsSubstitution { get; private init; }
|
||||
public bool IsCancelled { get; private init; }
|
||||
public bool HasRoom => !string.IsNullOrWhiteSpace(Room);
|
||||
public bool HasLessonTopic => !string.IsNullOrWhiteSpace(LessonTopic);
|
||||
public bool HasExam => ExamTitle is not null;
|
||||
public bool HasGroupId => GroupId != Guid.Empty;
|
||||
|
||||
public TodayLessonItem(Guid groupId, int periodNumber, string groupName, string room,
|
||||
string colorHex, string? lessonTopic, string? examTitle)
|
||||
{
|
||||
GroupId = groupId; PeriodNumber = periodNumber; GroupName = groupName; Room = room;
|
||||
ColorHex = colorHex; LessonTopic = lessonTopic; ExamTitle = examTitle;
|
||||
}
|
||||
|
||||
public static TodayLessonItem ForSubstitution(int periodNumber, SubstitutionEntry entry) => new(
|
||||
entry.GroupId ?? Guid.Empty, periodNumber, entry.GroupLabel, "", "#8E24AA", entry.Description, null)
|
||||
{ IsSubstitution = true };
|
||||
|
||||
public static TodayLessonItem ForCancelled(Guid groupId, int periodNumber, string groupName, SubstitutionEntry entry) => new(
|
||||
groupId, periodNumber, groupName, "", "#757575",
|
||||
string.IsNullOrWhiteSpace(entry.Description) ? null : entry.Description, null)
|
||||
{ IsCancelled = true };
|
||||
}
|
||||
|
||||
public class TodaySupervisionItem(int afterPeriod, string description, bool isSubstitution)
|
||||
{
|
||||
public int AfterPeriod { get; } = afterPeriod;
|
||||
public string PeriodLabel { get; } = afterPeriod == 0 ? "Vor der 1. Stunde" : $"Nach der {afterPeriod}. Stunde";
|
||||
public string Description { get; } = description;
|
||||
public bool IsSubstitution { get; } = isSubstitution;
|
||||
}
|
||||
|
||||
public class TodaySpecialAssignmentItem(string periodLabel, string description, string groupLabel)
|
||||
{
|
||||
public string PeriodLabel { get; } = periodLabel;
|
||||
public string Description { get; } = description;
|
||||
public string GroupLabel { get; } = groupLabel;
|
||||
public bool HasGroupLabel => !string.IsNullOrWhiteSpace(GroupLabel);
|
||||
public string DisplayText { get; } =
|
||||
$"{periodLabel} — {description}" + (string.IsNullOrWhiteSpace(groupLabel) ? "" : $" ({groupLabel})");
|
||||
}
|
||||
|
||||
@@ -127,10 +127,29 @@ public partial class SettingsViewModel : ObservableObject
|
||||
public List<string> StateOptions { get; } = GermanStateDisplay.Options.ToList();
|
||||
public ObservableCollection<SchoolHolidayItem> SchoolHolidayEntries { get; } = [];
|
||||
|
||||
// ── Stundenraster: Uhrzeiten je Einzelstunde (4.2.2 Nachtrag) ────────────
|
||||
|
||||
[ObservableProperty] private string _periodTimesError = "";
|
||||
[ObservableProperty] private string _periodTimesStatus = "";
|
||||
|
||||
public ObservableCollection<PeriodTimeEditItem> PeriodTimes { get; } = [];
|
||||
|
||||
// ── Aufsichten: wiederkehrende Pausenaufsicht (4.3 Nachtrag) ─────────────
|
||||
|
||||
[ObservableProperty] private string _newDutyWeekdayName = WeekdayDisplay.Options[0];
|
||||
[ObservableProperty] private int _newDutyAfterPeriod;
|
||||
[ObservableProperty] private string _newDutyLocation = "";
|
||||
[ObservableProperty] private string _newDutyError = "";
|
||||
|
||||
public string[] WeekdayOptions { get; } = WeekdayDisplay.Options;
|
||||
public ObservableCollection<SupervisionDutyItem> SupervisionDuties { get; } = [];
|
||||
|
||||
// ── Konstruktor ───────────────────────────────────────────────────────────
|
||||
|
||||
private readonly ISchoolHolidayRepository _schoolHolidays;
|
||||
private readonly SchoolCalendarSettingsService _calendarSettings;
|
||||
private readonly PeriodScheduleService _periodSchedule;
|
||||
private readonly ISupervisionDutyRepository _supervisionDuties;
|
||||
|
||||
public SettingsViewModel(ISubjectRepository subjects, ICompetencyDomainRepository domainRepo,
|
||||
IGradingKeyTemplateRepository gradingKeyTemplates, IGradingSchemeRepository gradingSchemes,
|
||||
@@ -138,7 +157,8 @@ public partial class SettingsViewModel : ObservableObject
|
||||
AppLockService appLock, LiteDbContext dbContext, PrivacySettingsService privacy,
|
||||
IDocumentationRepository documentation, IStudentRepository students,
|
||||
IShorthandCodeRepository shorthandCodes, ISchoolHolidayRepository schoolHolidays,
|
||||
SchoolCalendarSettingsService calendarSettings)
|
||||
SchoolCalendarSettingsService calendarSettings, PeriodScheduleService periodSchedule,
|
||||
ISupervisionDutyRepository supervisionDuties)
|
||||
{
|
||||
_subjects = subjects;
|
||||
_domainRepo = domainRepo;
|
||||
@@ -155,6 +175,8 @@ public partial class SettingsViewModel : ObservableObject
|
||||
_shorthandCodes = shorthandCodes;
|
||||
_schoolHolidays = schoolHolidays;
|
||||
_calendarSettings = calendarSettings;
|
||||
_periodSchedule = periodSchedule;
|
||||
_supervisionDuties = supervisionDuties;
|
||||
LoadSubjects();
|
||||
LoadShorthandCodes();
|
||||
LoadGradingKeyTemplates();
|
||||
@@ -167,6 +189,84 @@ public partial class SettingsViewModel : ObservableObject
|
||||
LoadExpiredDocuments();
|
||||
SelectedStateName = GermanStateDisplay.Label(_calendarSettings.State);
|
||||
LoadSchoolHolidays();
|
||||
LoadPeriodTimes();
|
||||
LoadSupervisionDuties();
|
||||
}
|
||||
|
||||
// ── Aufsichten: Laden / Hinzufügen / Löschen ─────────────────────────────
|
||||
|
||||
private void LoadSupervisionDuties()
|
||||
{
|
||||
SupervisionDuties.Clear();
|
||||
foreach (var d in _supervisionDuties.GetAll())
|
||||
SupervisionDuties.Add(new SupervisionDutyItem(d));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void AddSupervisionDuty()
|
||||
{
|
||||
NewDutyError = "";
|
||||
if (string.IsNullOrWhiteSpace(NewDutyLocation)) { NewDutyError = "Ort/Bezeichnung erforderlich."; return; }
|
||||
if (NewDutyAfterPeriod is < 0 or > 10) { NewDutyError = "Muss zwischen 0 und 10 liegen."; return; }
|
||||
|
||||
try
|
||||
{
|
||||
_supervisionDuties.Save(new SupervisionDuty
|
||||
{
|
||||
Weekday = WeekdayDisplay.FromLabel(NewDutyWeekdayName),
|
||||
AfterPeriod = NewDutyAfterPeriod,
|
||||
Location = NewDutyLocation.Trim(),
|
||||
});
|
||||
}
|
||||
catch (InvalidOperationException ex) { NewDutyError = ex.Message; return; }
|
||||
|
||||
NewDutyLocation = ""; NewDutyAfterPeriod = 0;
|
||||
LoadSupervisionDuties();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void RemoveSupervisionDuty(SupervisionDutyItem? item)
|
||||
{
|
||||
if (item is null) return;
|
||||
_supervisionDuties.Delete(item.Id);
|
||||
SupervisionDuties.Remove(item);
|
||||
}
|
||||
|
||||
// ── Stundenraster: Laden / Speichern ─────────────────────────────────────
|
||||
|
||||
private void LoadPeriodTimes()
|
||||
{
|
||||
PeriodTimes.Clear();
|
||||
for (var period = 1; period <= 10; period++)
|
||||
{
|
||||
var times = _periodSchedule.GetTimes(period);
|
||||
PeriodTimes.Add(new PeriodTimeEditItem(period, times?.Start, times?.End));
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void SavePeriodTimes()
|
||||
{
|
||||
PeriodTimesError = ""; PeriodTimesStatus = "";
|
||||
var entries = new List<PeriodTimeEntry>();
|
||||
|
||||
foreach (var item in PeriodTimes)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(item.StartText) && string.IsNullOrWhiteSpace(item.EndText))
|
||||
continue; // Stunde bewusst nicht konfiguriert — ok, keine Pflicht für alle 10.
|
||||
|
||||
if (!TimeOnly.TryParseExact(item.StartText, "HH:mm", CultureInfo.InvariantCulture, DateTimeStyles.None, out var start) ||
|
||||
!TimeOnly.TryParseExact(item.EndText, "HH:mm", CultureInfo.InvariantCulture, DateTimeStyles.None, out var end))
|
||||
{ PeriodTimesError = $"{item.PeriodLabel}: Format HH:MM."; return; }
|
||||
|
||||
if (end <= start)
|
||||
{ PeriodTimesError = $"{item.PeriodLabel}: Ende muss nach dem Beginn liegen."; return; }
|
||||
|
||||
entries.Add(new PeriodTimeEntry { PeriodNumber = item.PeriodNumber, Start = start, End = end });
|
||||
}
|
||||
|
||||
_periodSchedule.SetPeriods(entries);
|
||||
PeriodTimesStatus = "Gespeichert.";
|
||||
}
|
||||
|
||||
// ── Ferien & Feiertage: Bundesland / Schulferien pflegen ─────────────────
|
||||
@@ -842,6 +942,47 @@ public class SchoolHolidayItem(SchoolHoliday h)
|
||||
public string RangeDisplay { get; } = $"{h.StartDate:dd.MM.yyyy} – {h.EndDate:dd.MM.yyyy}";
|
||||
}
|
||||
|
||||
public partial class PeriodTimeEditItem : ObservableObject
|
||||
{
|
||||
public int PeriodNumber { get; }
|
||||
public string PeriodLabel { get; }
|
||||
|
||||
[ObservableProperty] private string _startText;
|
||||
[ObservableProperty] private string _endText;
|
||||
|
||||
public PeriodTimeEditItem(int periodNumber, TimeOnly? start, TimeOnly? end)
|
||||
{
|
||||
PeriodNumber = periodNumber;
|
||||
PeriodLabel = $"{periodNumber}. Stunde";
|
||||
_startText = start?.ToString("HH:mm") ?? "";
|
||||
_endText = end?.ToString("HH:mm") ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
// ── Wochentag: deutsche Anzeige (Mo–Fr, für Aufsichten) ────────────────────────
|
||||
|
||||
public static class WeekdayDisplay
|
||||
{
|
||||
private static readonly (DayOfWeek Day, string Name)[] Entries =
|
||||
[
|
||||
(DayOfWeek.Monday, "Montag"), (DayOfWeek.Tuesday, "Dienstag"), (DayOfWeek.Wednesday, "Mittwoch"),
|
||||
(DayOfWeek.Thursday, "Donnerstag"), (DayOfWeek.Friday, "Freitag"),
|
||||
];
|
||||
|
||||
public static string[] Options { get; } = Entries.Select(e => e.Name).ToArray();
|
||||
public static string Label(DayOfWeek d) => Entries.First(e => e.Day == d).Name;
|
||||
public static DayOfWeek FromLabel(string label) => Entries.FirstOrDefault(e => e.Name == label).Day;
|
||||
}
|
||||
|
||||
public class SupervisionDutyItem(SupervisionDuty d)
|
||||
{
|
||||
public Guid Id { get; } = d.Id;
|
||||
public string WeekdayLabel { get; } = WeekdayDisplay.Label(d.Weekday);
|
||||
public int AfterPeriod { get; } = d.AfterPeriod;
|
||||
public string PeriodLabel { get; } = d.AfterPeriod == 0 ? "Vor der 1. Stunde" : $"Nach der {d.AfterPeriod}. Stunde";
|
||||
public string Location { get; } = d.Location;
|
||||
}
|
||||
|
||||
// ── JSON DTOs ─────────────────────────────────────────────────────────────────
|
||||
|
||||
internal class CatalogDto
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<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.GenerateLessonSeriesDialog"
|
||||
x:DataType="vm:GenerateLessonSeriesDialogViewModel"
|
||||
Title="Stunden aus Stundenplan erzeugen"
|
||||
Width="420" Height="300" MinWidth="380" MinHeight="280"
|
||||
CanResize="False" WindowStartupLocation="CenterOwner">
|
||||
|
||||
<Grid RowDefinitions="*,Auto" Margin="24">
|
||||
<StackPanel Grid.Row="0" Spacing="14">
|
||||
<TextBlock Text="Stunden aus Stundenplan erzeugen" Classes="dialogtitle"/>
|
||||
<TextBlock FontSize="12" Opacity="0.6" TextWrapping="Wrap"
|
||||
Text="Legt für jeden Wochentag/Stunde aus dem Stundenplan dieser Gruppe eine neue Stunde im gewählten Zeitraum an. Schulferien und Feiertage werden übersprungen, bereits vorhandene Termine nicht doppelt angelegt."/>
|
||||
|
||||
<Grid ColumnDefinitions="*,8,*">
|
||||
<StackPanel Grid.Column="0" Spacing="4">
|
||||
<TextBlock Text="Von *" FontSize="12" Opacity="0.7"/>
|
||||
<TextBox Text="{Binding FromDateText}" PlaceholderText="TT.MM.JJJJ"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="2" Spacing="4">
|
||||
<TextBlock Text="Bis *" FontSize="12" Opacity="0.7"/>
|
||||
<TextBox Text="{Binding ToDateText}" PlaceholderText="TT.MM.JJJJ"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<TextBlock Text="{Binding DateError}" Foreground="Red" FontSize="11"
|
||||
TextWrapping="Wrap"
|
||||
IsVisible="{Binding DateError, 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="Erzeugen" HorizontalAlignment="Stretch" Click="OnSave"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,21 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using LehrerApp.Desktop.ViewModels.Groups;
|
||||
|
||||
namespace LehrerApp.Desktop.Views.Groups;
|
||||
|
||||
public partial class GenerateLessonSeriesDialog : Window
|
||||
{
|
||||
public GenerateLessonSeriesDialog() => InitializeComponent();
|
||||
|
||||
private void OnSave(object? s, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is GenerateLessonSeriesDialogViewModel 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);
|
||||
}
|
||||
@@ -50,7 +50,13 @@
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="10" VerticalAlignment="Center">
|
||||
<TextBlock Text="Verlaufsplan" FontSize="14" FontWeight="SemiBold"/>
|
||||
<TextBlock Text="{Binding TotalDurationDisplay}" FontSize="12" Opacity="0.6"/>
|
||||
<TextBlock Text="{Binding TotalDurationDisplay}" FontSize="12" Opacity="0.6"
|
||||
IsVisible="{Binding !HasTimeBudgetInfo}"/>
|
||||
<Border Background="{Binding TimeBudgetColorHex}" CornerRadius="10" Padding="9,3"
|
||||
IsVisible="{Binding HasTimeBudgetInfo}"
|
||||
ToolTip.Tip="Geplante Zeit im Vergleich zur laut Stundenraster (Einstellungen) verfügbaren Zeit — bei Doppelstunden werden beide Perioden zusammengezählt.">
|
||||
<TextBlock Text="{Binding TimeBudgetLabel}" FontSize="12" FontWeight="SemiBold" Foreground="White"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" Content="+ Phase" Command="{Binding AddPhaseCommand}"/>
|
||||
</Grid>
|
||||
|
||||
@@ -63,6 +63,8 @@
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="8">
|
||||
<Button Content="+ Stunde" Command="{Binding AddLessonCommand}"/>
|
||||
<Button Content="Serie erzeugen" Command="{Binding GenerateLessonSeriesCommand}"
|
||||
ToolTip.Tip="Stunden für alle Termine aus dem Stundenplan im gewählten Zeitraum anlegen."/>
|
||||
<Button Content="Anzeigen" Command="{Binding ShowLessonCommand}"
|
||||
ToolTip.Tip="Verlaufsplan schreibgeschützt und größer anzeigen — zum Mitnehmen in den Unterricht."/>
|
||||
<Button Content="Bearbeiten" Command="{Binding EditLessonCommand}"/>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using Avalonia.Controls;
|
||||
using LehrerApp.Core.Interfaces;
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Core.Services;
|
||||
using LehrerApp.Desktop.Services;
|
||||
using LehrerApp.Desktop.ViewModels.Groups;
|
||||
using LehrerApp.Desktop.Views.Shared;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -27,6 +29,7 @@ public partial class PlanningTabView : UserControl
|
||||
vm.OnConfirmDeleteLesson = ShowDeleteLessonDialog;
|
||||
vm.OnPickMoveTarget = ShowMoveLessonDialog;
|
||||
vm.OnShowLesson = ShowLessonViewerDialog;
|
||||
vm.OnGenerateLessonSeries = ShowGenerateLessonSeriesDialog;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +83,8 @@ public partial class PlanningTabView : UserControl
|
||||
App.Services.GetRequiredService<ILessonRepository>(),
|
||||
App.Services.GetRequiredService<IShorthandCodeRepository>(),
|
||||
App.Services.GetRequiredService<IAlternativeLessonPathRepository>(),
|
||||
App.Services.GetRequiredService<ITimetableSlotRepository>(),
|
||||
App.Services.GetRequiredService<PeriodScheduleService>(),
|
||||
unitId, groupId, materialSuggestions, shorthandHistorySuggestions, editingLesson);
|
||||
|
||||
var dialog = new LessonDialog { DataContext = dialogVm };
|
||||
@@ -122,4 +127,24 @@ public partial class PlanningTabView : UserControl
|
||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||
if (owner is not null) await dialog.ShowDialog(owner);
|
||||
}
|
||||
|
||||
private async Task<LessonSeriesResult?> ShowGenerateLessonSeriesDialog(Unit unit)
|
||||
{
|
||||
var dialogVm = new GenerateLessonSeriesDialogViewModel(
|
||||
App.Services.GetRequiredService<ITimetableSlotRepository>(),
|
||||
App.Services.GetRequiredService<ILessonRepository>(),
|
||||
App.Services.GetRequiredService<ISchoolHolidayRepository>(),
|
||||
App.Services.GetRequiredService<PublicHolidayService>(),
|
||||
App.Services.GetRequiredService<SchoolCalendarSettingsService>(),
|
||||
unit.Id, unit.GroupId, unit.StartDate, unit.EndDate);
|
||||
|
||||
var dialog = new GenerateLessonSeriesDialog { DataContext = dialogVm };
|
||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||
if (owner is null) return null;
|
||||
|
||||
var ok = await dialog.ShowDialog<bool>(owner);
|
||||
if (ok && dialogVm.Result is { } result)
|
||||
App.Services.GetRequiredService<NotificationService>().ShowSuccess(result.Summary);
|
||||
return ok ? dialogVm.Result : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Planning"
|
||||
xmlns:models="clr-namespace:LehrerApp.Core.Models;assembly=LehrerApp.Core"
|
||||
x:Class="LehrerApp.Desktop.Views.Planning.SubstitutionEntryDialog"
|
||||
x:DataType="vm:SubstitutionEntryDialogViewModel"
|
||||
Title="Vertretung eintragen"
|
||||
Width="440" Height="560" MinWidth="400" MinHeight="480"
|
||||
CanResize="True" WindowStartupLocation="CenterOwner">
|
||||
|
||||
<Grid RowDefinitions="*,Auto" Margin="24">
|
||||
<ScrollViewer Grid.Row="0">
|
||||
<StackPanel Spacing="14" Margin="0,0,12,0">
|
||||
<TextBlock Text="Vertretung eintragen" Classes="dialogtitle"/>
|
||||
<TextBlock FontSize="12" Opacity="0.6" TextWrapping="Wrap"
|
||||
Text="Für einmalige Ausnahmen — Vertretungsaufsicht, Vertretungsstunde, Sondereinsatz (Ausflug, Berufsmesse, ...) oder schlichter Ausfall an einem konkreten Tag. Wiederkehrende Aufsicht wird in den Einstellungen gepflegt."/>
|
||||
|
||||
<Grid ColumnDefinitions="*,8,*">
|
||||
<StackPanel Grid.Column="0" Spacing="4">
|
||||
<TextBlock Text="Datum *" FontSize="12" Opacity="0.7"/>
|
||||
<TextBox Text="{Binding DateText}" PlaceholderText="TT.MM.JJJJ"/>
|
||||
<TextBlock Text="{Binding DateError}" Foreground="Red" FontSize="11"
|
||||
IsVisible="{Binding DateError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="2" Spacing="4">
|
||||
<TextBlock Text="Art *" FontSize="12" Opacity="0.7"/>
|
||||
<ComboBox ItemsSource="{Binding KindOptions}" SelectedItem="{Binding KindName}"
|
||||
HorizontalAlignment="Stretch"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- Aufsicht -->
|
||||
<StackPanel Spacing="14" IsVisible="{Binding IsSupervisionKind}">
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Nach Stunde (0 = davor) *" FontSize="12" Opacity="0.7"/>
|
||||
<NumericUpDown Value="{Binding AfterPeriod}" Minimum="0" Maximum="10" FormatString="0"
|
||||
Width="140" HorizontalAlignment="Left"/>
|
||||
</StackPanel>
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Grund / Ort *" FontSize="12" Opacity="0.7"/>
|
||||
<TextBox Text="{Binding Description}" PlaceholderText="z.B. Vertretung für Hr. Müller, Pausenhof"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Stunde -->
|
||||
<StackPanel Spacing="14" IsVisible="{Binding IsLessonKind}">
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Stunde Nr. *" FontSize="12" Opacity="0.7"/>
|
||||
<NumericUpDown Value="{Binding PeriodNumber}" Minimum="1" Maximum="10" FormatString="0"
|
||||
Width="140" HorizontalAlignment="Left"/>
|
||||
</StackPanel>
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Eigene Gruppe (optional)" FontSize="12" Opacity="0.7"/>
|
||||
<ComboBox ItemsSource="{Binding OwnGroups}" SelectedItem="{Binding SelectedOwnGroup}"
|
||||
HorizontalAlignment="Stretch" PlaceholderText="Fremde/unbekannte Gruppe">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="models:LearningGroup">
|
||||
<TextBlock Text="{Binding Name}"/>
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Bezeichnung *" FontSize="12" Opacity="0.7"
|
||||
ToolTip.Tip="Wird im Stundenplan angezeigt, z.B. Klasse/Kurs-Kürzel."/>
|
||||
<TextBox Text="{Binding GroupLabel}" PlaceholderText="z.B. 8a"/>
|
||||
</StackPanel>
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Thema *" FontSize="12" Opacity="0.7"/>
|
||||
<TextBox Text="{Binding Description}" PlaceholderText="z.B. Stillarbeit, Erdkunde-Vertretung"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Spacing="6" IsVisible="{Binding CanPromoteToLesson}">
|
||||
<Separator/>
|
||||
<CheckBox Content="Direkt als Stunde in der Einheit übernehmen" IsChecked="{Binding PromoteToLesson}"
|
||||
ToolTip.Tip="Nur sinnvoll, wenn tatsächlich echter Stoff aus der Einheit behandelt wurde — sonst bleibt es beim einfachen Plan-Eintrag."/>
|
||||
<ComboBox ItemsSource="{Binding UnitsOfSelectedGroup}" SelectedItem="{Binding SelectedUnit}"
|
||||
IsVisible="{Binding PromoteToLesson}" HorizontalAlignment="Stretch">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="models:Unit">
|
||||
<TextBlock Text="{Binding Title}"/>
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Sondereinsatz -->
|
||||
<StackPanel Spacing="14" IsVisible="{Binding IsSpecialAssignmentKind}">
|
||||
<CheckBox Content="Ganztägig" IsChecked="{Binding IsAllDay}"/>
|
||||
<Grid ColumnDefinitions="*,8,*" IsVisible="{Binding !IsAllDay}">
|
||||
<StackPanel Grid.Column="0" Spacing="4">
|
||||
<TextBlock Text="Von Stunde *" FontSize="12" Opacity="0.7"/>
|
||||
<NumericUpDown Value="{Binding FromPeriod}" Minimum="1" Maximum="10" FormatString="0"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="2" Spacing="4">
|
||||
<TextBlock Text="Bis Stunde *" FontSize="12" Opacity="0.7"/>
|
||||
<NumericUpDown Value="{Binding ToPeriod}" Minimum="1" Maximum="10" FormatString="0"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Eigene Gruppe (optional)" FontSize="12" Opacity="0.7"
|
||||
ToolTip.Tip="Nur ausfüllen, wenn der Sondereinsatz eine konkrete Lerngruppe betrifft (z.B. Ausflug) — bei Einsätzen ohne Gruppenbezug (z.B. Berufsmesse) leer lassen."/>
|
||||
<ComboBox ItemsSource="{Binding OwnGroups}" SelectedItem="{Binding SelectedOwnGroup}"
|
||||
HorizontalAlignment="Stretch" PlaceholderText="Kein Gruppenbezug">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="models:LearningGroup">
|
||||
<TextBlock Text="{Binding Name}"/>
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Bezeichnung *" FontSize="12" Opacity="0.7"/>
|
||||
<TextBox Text="{Binding Description}" PlaceholderText="z.B. Ausflug ins Museum, Berufsmesse"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Ausfall -->
|
||||
<StackPanel Spacing="14" IsVisible="{Binding IsCancelledKind}">
|
||||
<TextBlock FontSize="12" Opacity="0.6" TextWrapping="Wrap"
|
||||
Text="Für Stunden, die schlicht ausfallen, ohne dass jemand vertritt — z.B. weil die andere Gruppe selbst nicht da ist (Klassenfahrt, Exkursion o.ä.). Fach/Klasse werden automatisch aus dem Stundenplan übernommen."/>
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Stunde Nr. *" FontSize="12" Opacity="0.7"/>
|
||||
<NumericUpDown Value="{Binding PeriodNumber}" Minimum="1" Maximum="10" FormatString="0"
|
||||
Width="140" HorizontalAlignment="Left"/>
|
||||
</StackPanel>
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Grund (optional)" FontSize="12" Opacity="0.7"/>
|
||||
<TextBox Text="{Binding Description}" PlaceholderText="z.B. 6a auf Klassenfahrt"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Text="{Binding ValidationError}" Foreground="Red" FontSize="12" TextWrapping="Wrap"
|
||||
IsVisible="{Binding ValidationError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<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="Eintragen" HorizontalAlignment="Stretch" Click="OnSave"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,21 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using LehrerApp.Desktop.ViewModels.Planning;
|
||||
|
||||
namespace LehrerApp.Desktop.Views.Planning;
|
||||
|
||||
public partial class SubstitutionEntryDialog : Window
|
||||
{
|
||||
public SubstitutionEntryDialog() => InitializeComponent();
|
||||
|
||||
private void OnSave(object? s, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is SubstitutionEntryDialogViewModel 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);
|
||||
}
|
||||
@@ -15,6 +15,12 @@
|
||||
<Style Selector="Border.weekheader.today">
|
||||
<Setter Property="Background" Value="{DynamicResource SystemAccentColor}"/>
|
||||
</Style>
|
||||
<Style Selector="Border.supervisioncell">
|
||||
<Setter Property="Background" Value="#616161"/>
|
||||
</Style>
|
||||
<Style Selector="Border.supervisioncell.substitution">
|
||||
<Setter Property="Background" Value="#8E24AA"/>
|
||||
</Style>
|
||||
</UserControl.Styles>
|
||||
|
||||
<Grid RowDefinitions="Auto,*">
|
||||
@@ -33,6 +39,36 @@
|
||||
CornerRadius="8" Padding="16" MaxHeight="240">
|
||||
<DockPanel>
|
||||
<TextBlock DockPanel.Dock="Top" Text="{Binding TodayLabel}" FontSize="15" FontWeight="SemiBold" Margin="0,0,0,8"/>
|
||||
<ItemsControl DockPanel.Dock="Top" ItemsSource="{Binding TodaySupervisions}" Margin="0,0,0,6"
|
||||
IsVisible="{Binding TodaySupervisions.Count}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:TodaySupervisionItem">
|
||||
<Grid ColumnDefinitions="Auto,*" Margin="0,2">
|
||||
<Border Grid.Column="0" Classes="supervisioncell" Classes.substitution="{Binding IsSubstitution}"
|
||||
CornerRadius="4" Padding="6,2" Margin="0,0,8,0">
|
||||
<TextBlock Text="👁" FontSize="11" Foreground="White"/>
|
||||
</Border>
|
||||
<TextBlock Grid.Column="1" FontSize="12" VerticalAlignment="Center">
|
||||
<Run Text="{Binding PeriodLabel}"/><Run Text=" — "/><Run Text="{Binding Description}"/>
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<ItemsControl DockPanel.Dock="Top" ItemsSource="{Binding TodaySpecialAssignments}" Margin="0,0,0,6"
|
||||
IsVisible="{Binding TodaySpecialAssignments.Count}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:TodaySpecialAssignmentItem">
|
||||
<Grid ColumnDefinitions="Auto,*" Margin="0,2">
|
||||
<Border Grid.Column="0" Background="#00838F" CornerRadius="4" Padding="6,2" Margin="0,0,8,0">
|
||||
<TextBlock Text="📌" FontSize="11" Foreground="White"/>
|
||||
</Border>
|
||||
<TextBlock Grid.Column="1" FontSize="12" VerticalAlignment="Center" TextWrapping="Wrap"
|
||||
Text="{Binding DisplayText}"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<ScrollViewer>
|
||||
<ItemsControl ItemsSource="{Binding TodayItems}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
@@ -44,7 +80,15 @@
|
||||
<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"/>
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<TextBlock Text="{Binding GroupName}" FontSize="14" FontWeight="SemiBold"/>
|
||||
<Border Background="#8E24AA" CornerRadius="7" Padding="5,0" IsVisible="{Binding IsSubstitution}">
|
||||
<TextBlock Text="Vertretung" FontSize="10" Foreground="White"/>
|
||||
</Border>
|
||||
<Border Background="#757575" CornerRadius="7" Padding="5,0" IsVisible="{Binding IsCancelled}">
|
||||
<TextBlock Text="Ausfall" FontSize="10" Foreground="White"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<TextBlock FontSize="11" Opacity="0.6" IsVisible="{Binding HasRoom}">
|
||||
<Run Text="Raum "/><Run Text="{Binding Room}"/>
|
||||
</TextBlock>
|
||||
@@ -54,7 +98,7 @@
|
||||
IsVisible="{Binding HasExam}"/>
|
||||
</StackPanel>
|
||||
<Button Grid.Column="3" Content="Zur Lerngruppe" FontSize="11" Padding="9,4"
|
||||
VerticalAlignment="Center"
|
||||
VerticalAlignment="Center" IsVisible="{Binding HasGroupId}"
|
||||
Command="{Binding $parent[ItemsControl].((vm:TimetableViewModel)DataContext).OpenGroupCommand}"
|
||||
CommandParameter="{Binding GroupId}"/>
|
||||
</Grid>
|
||||
@@ -72,7 +116,7 @@
|
||||
<ScrollViewer>
|
||||
<StackPanel Spacing="14">
|
||||
|
||||
<Grid ColumnDefinitions="Auto,Auto,*,Auto,Auto">
|
||||
<Grid ColumnDefinitions="Auto,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"
|
||||
@@ -83,7 +127,9 @@
|
||||
</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}"/>
|
||||
<Button Grid.Column="4" Content="Vertretung eintragen" Margin="0,0,8,0"
|
||||
Command="{Binding AddSubstitutionCommand}"/>
|
||||
<Button Grid.Column="5" Content="Stundenplan bearbeiten" Command="{Binding ShowEditorCommand}"/>
|
||||
</Grid>
|
||||
|
||||
<ItemsControl ItemsSource="{Binding WeekItems}">
|
||||
@@ -101,6 +147,16 @@
|
||||
<TextBlock Text="{Binding Text}" IsVisible="{Binding IsPeriodLabel}"
|
||||
FontWeight="SemiBold" FontSize="13"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
<TextBlock Text="{Binding Text}" IsVisible="{Binding IsSupervisionRow}"
|
||||
FontSize="10" FontWeight="SemiBold" Opacity="0.55" TextWrapping="Wrap"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
<Border Classes="supervisioncell" Classes.substitution="{Binding IsSubstitutionSupervision}"
|
||||
CornerRadius="4" Padding="4" IsVisible="{Binding IsSupervisionCell}"
|
||||
HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
|
||||
<TextBlock Text="{Binding SupervisionLocation}" FontSize="10" Foreground="White"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" TextWrapping="Wrap"
|
||||
IsVisible="{Binding HasSupervision}"/>
|
||||
</Border>
|
||||
<Border Classes="timetablecell" Classes.assigned="{Binding IsAssigned}"
|
||||
CornerRadius="6" IsVisible="{Binding IsSlotCell}">
|
||||
<Button Background="{Binding ColorHex}" IsVisible="{Binding IsAssigned}"
|
||||
@@ -118,6 +174,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"/>
|
||||
<TextBlock Text="Ausfall" FontSize="10" Foreground="White" FontWeight="SemiBold"
|
||||
Opacity="0.9" IsVisible="{Binding IsCancelled}" 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}">
|
||||
@@ -172,6 +230,16 @@
|
||||
<TextBlock Text="{Binding Text}" IsVisible="{Binding IsPeriodLabel}"
|
||||
FontWeight="SemiBold" FontSize="13"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
<TextBlock Text="{Binding Text}" IsVisible="{Binding IsSupervisionRow}"
|
||||
FontSize="10" FontWeight="SemiBold" Opacity="0.55" TextWrapping="Wrap"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
<Border Classes="supervisioncell" CornerRadius="4" Padding="4"
|
||||
IsVisible="{Binding IsSupervisionCell}"
|
||||
HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
|
||||
<TextBlock Text="{Binding SupervisionLocation}" FontSize="10" Foreground="White"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" TextWrapping="Wrap"
|
||||
IsVisible="{Binding HasSupervision}"/>
|
||||
</Border>
|
||||
<Border Classes="timetablecell" Classes.assigned="{Binding IsAssigned}"
|
||||
CornerRadius="6" IsVisible="{Binding IsSlotCell}">
|
||||
<Panel>
|
||||
|
||||
@@ -13,7 +13,11 @@ public partial class TimetableView : UserControl
|
||||
protected override void OnDataContextChanged(EventArgs e)
|
||||
{
|
||||
base.OnDataContextChanged(e);
|
||||
if (DataContext is TimetableViewModel vm) vm.OnEditSlot = ShowSlotDialog;
|
||||
if (DataContext is TimetableViewModel vm)
|
||||
{
|
||||
vm.OnEditSlot = ShowSlotDialog;
|
||||
vm.OnAddSubstitution = ShowSubstitutionDialog;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ShowSlotDialog(TimetableCellItem cell)
|
||||
@@ -30,4 +34,19 @@ public partial class TimetableView : UserControl
|
||||
var dialog = new TimetableSlotDialog { DataContext = vm };
|
||||
await dialog.ShowDialog(owner);
|
||||
}
|
||||
|
||||
private async Task ShowSubstitutionDialog()
|
||||
{
|
||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||
if (owner is null) return;
|
||||
|
||||
var vm = new SubstitutionEntryDialogViewModel(
|
||||
App.Services.GetRequiredService<ISubstitutionEntryRepository>(),
|
||||
App.Services.GetRequiredService<IGroupRepository>(),
|
||||
App.Services.GetRequiredService<IUnitRepository>(),
|
||||
App.Services.GetRequiredService<ILessonRepository>());
|
||||
|
||||
var dialog = new SubstitutionEntryDialog { DataContext = vm };
|
||||
await dialog.ShowDialog(owner);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -536,6 +536,100 @@
|
||||
</ScrollViewer>
|
||||
</ContentPage>
|
||||
|
||||
<!-- Tab: Stundenraster (4.2.2 Nachtrag) -->
|
||||
<ContentPage Header="Stundenraster">
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="32,20,32,28" Spacing="14" MaxWidth="420">
|
||||
|
||||
<TextBlock Text="Uhrzeiten der Einzelstunden" FontSize="16" FontWeight="SemiBold"/>
|
||||
<TextBlock FontSize="12" Opacity="0.6" TextWrapping="Wrap"
|
||||
Text="Grundlage für die Zeitbedarf-Rückmeldung im Verlaufsplan-Editor. Nicht alle Stunden müssen eingetragen sein — für unkonfigurierte Stunden bleibt die Rückmeldung dort einfach aus."/>
|
||||
|
||||
<Grid ColumnDefinitions="70,*,8,*" Margin="0,4,0,0">
|
||||
<TextBlock Grid.Column="1" Text="Beginn" FontSize="11" FontWeight="Bold" Opacity="0.5"/>
|
||||
<TextBlock Grid.Column="3" Text="Ende" FontSize="11" FontWeight="Bold" Opacity="0.5"/>
|
||||
</Grid>
|
||||
<ItemsControl ItemsSource="{Binding PeriodTimes}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="vm:PeriodTimeEditItem">
|
||||
<Grid ColumnDefinitions="70,*,8,*" Margin="0,3">
|
||||
<TextBlock Grid.Column="0" Text="{Binding PeriodLabel}" FontSize="13" VerticalAlignment="Center"/>
|
||||
<TextBox Grid.Column="1" Text="{Binding StartText}" PlaceholderText="HH:MM"/>
|
||||
<TextBox Grid.Column="3" Text="{Binding EndText}" PlaceholderText="HH:MM"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<TextBlock Text="{Binding PeriodTimesError}" Foreground="Red" FontSize="12"
|
||||
IsVisible="{Binding PeriodTimesError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
<Button Content="Speichern" Command="{Binding SavePeriodTimesCommand}" HorizontalAlignment="Left"/>
|
||||
<TextBlock Text="{Binding PeriodTimesStatus}" Foreground="Green" FontSize="12"
|
||||
IsVisible="{Binding PeriodTimesStatus, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</ContentPage>
|
||||
|
||||
<!-- Tab: Aufsichten (4.3 Nachtrag) -->
|
||||
<ContentPage Header="Aufsichten">
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="32,20,32,28" Spacing="14" MaxWidth="480">
|
||||
|
||||
<TextBlock Text="Wiederkehrende Pausenaufsicht" FontSize="16" FontWeight="SemiBold"/>
|
||||
<TextBlock FontSize="12" Opacity="0.6" TextWrapping="Wrap"
|
||||
Text="Wird im Stundenplan zwischen den betroffenen Stunden angezeigt. Einmalige Vertretungsaufsichten trägst du direkt im Stundenplan (Heute-Ansicht) ein, nicht hier."/>
|
||||
|
||||
<ItemsControl ItemsSource="{Binding SupervisionDuties}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="vm:SupervisionDutyItem">
|
||||
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||
BorderThickness="0,0,0,1" Padding="0,7">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0">
|
||||
<TextBlock FontSize="13" FontWeight="SemiBold">
|
||||
<Run Text="{Binding WeekdayLabel}"/><Run Text=" — "/><Run Text="{Binding PeriodLabel}"/>
|
||||
</TextBlock>
|
||||
<TextBlock Text="{Binding Location}" FontSize="12" Opacity="0.6"/>
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" Content="×" FontSize="14" Padding="9,3"
|
||||
Command="{Binding $parent[ItemsControl].((vm:SettingsViewModel)DataContext).RemoveSupervisionDutyCommand}"
|
||||
CommandParameter="{Binding}"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<TextBlock Text="Noch keine Aufsicht hinterlegt." Classes="emptyhint"
|
||||
IsVisible="{Binding !SupervisionDuties.Count}"/>
|
||||
|
||||
<Separator Margin="0,4"/>
|
||||
|
||||
<Grid ColumnDefinitions="*,8,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="4">
|
||||
<TextBlock Text="Wochentag" FontSize="12" Opacity="0.7"/>
|
||||
<ComboBox ItemsSource="{Binding WeekdayOptions}" SelectedItem="{Binding NewDutyWeekdayName}"
|
||||
HorizontalAlignment="Stretch"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="2" Spacing="4">
|
||||
<TextBlock Text="Nach Stunde (0 = davor)" FontSize="12" Opacity="0.7"/>
|
||||
<NumericUpDown Value="{Binding NewDutyAfterPeriod}" Minimum="0" Maximum="10" FormatString="0"
|
||||
Width="140" ShowButtonSpinner="True"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Ort / Bezeichnung" FontSize="12" Opacity="0.7"/>
|
||||
<TextBox Text="{Binding NewDutyLocation}" PlaceholderText="z.B. Pausenhof"/>
|
||||
</StackPanel>
|
||||
<TextBlock Text="{Binding NewDutyError}" Foreground="Red" FontSize="12"
|
||||
IsVisible="{Binding NewDutyError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
<Button Content="+ Aufsicht hinzufügen" Command="{Binding AddSupervisionDutyCommand}"
|
||||
HorizontalAlignment="Left"/>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</ContentPage>
|
||||
|
||||
</TabbedPage>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
||||
Reference in New Issue
Block a user