Unterrichtsplanung: Einheiten, Verlaufsplan-Editor, Kürzel-Katalog (Kapitel 4.1/4.2)
Neuer Tab "Planung" in GroupDetailView ersetzt den Platzhalter: Einheiten anlegen/bearbeiten/ als Vorlage in andere Gruppe kopieren, Stunden je Einheit mit Verschieben (inkl. Nachrücken der Folgestunden). Stundeneditor als tabellarischer Verlaufsplan (Phase/Dauer/Tätigkeit/ Material/Kurzsymbol je Zeile, Uhrzeit aus optionalem Stundenbeginn abgeleitet) statt eines einzelnen Phase-Felds mit Methoden-/Materialien-Chips — Kurzsymbol als Freitext mit Vorschlägen aus neuem Kürzel-Katalog (Einstellungen) plus bisher verwendeten Werten. Schema-Migrationen v1-v3 überführen bestehende Daten verlustfrei. 4.2.5 bewusst offen gelassen (hängt an Stundenplan, Kapitel 4.3). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -127,6 +127,7 @@ public static class AppBootstrapper
|
||||
services.AddSingleton<IParticipationSectionRepository, ParticipationSectionRepository>();
|
||||
services.AddSingleton<ISubjectRepository, SubjectRepository>();
|
||||
services.AddSingleton<ICompetencyDomainRepository, CompetencyDomainRepository>();
|
||||
services.AddSingleton<IShorthandCodeRepository, ShorthandCodeRepository>();
|
||||
services.AddSingleton<IAttachmentStorage, LiteAttachmentStorage>();
|
||||
|
||||
// ── Services ──────────────────────────────────────────────────────────
|
||||
@@ -182,6 +183,7 @@ public static class AppBootstrapper
|
||||
services.AddTransient<StudentDetailViewModel>();
|
||||
services.AddTransient<ParticipationTabViewModel>();
|
||||
services.AddTransient<GradeOverviewTabViewModel>();
|
||||
services.AddTransient<PlanningTabViewModel>();
|
||||
services.AddTransient<AddGroupDialogViewModel>();
|
||||
services.AddTransient<SettingsViewModel>();
|
||||
|
||||
|
||||
@@ -183,6 +183,7 @@ public partial class GroupDetailViewModel : ObservableObject
|
||||
|
||||
public ParticipationTabViewModel ParticipationTab { get; }
|
||||
public GradeOverviewTabViewModel GradeOverviewTab { get; }
|
||||
public PlanningTabViewModel PlanningTab { get; }
|
||||
public Func<Task<bool>>? OnAddStudent { get; set; }
|
||||
public Func<Guid, Task<bool>>? OnAddExam { get; set; }
|
||||
public Func<Exam, Task<bool>>? OnEditExam { get; set; }
|
||||
@@ -194,12 +195,14 @@ public partial class GroupDetailViewModel : ObservableObject
|
||||
public GroupDetailViewModel(IGroupRepository groups, IStudentRepository students,
|
||||
IGroupMembershipRepository memberships, ISubjectRepository subjects,
|
||||
IExamRepository exams, IGradeRepository grades,
|
||||
ParticipationTabViewModel participationTab, GradeOverviewTabViewModel gradeOverviewTab)
|
||||
ParticipationTabViewModel participationTab, GradeOverviewTabViewModel gradeOverviewTab,
|
||||
PlanningTabViewModel planningTab)
|
||||
{
|
||||
_groups = groups; _students = students; _memberships = memberships; _subjects = subjects;
|
||||
_exams = exams; _grades = grades;
|
||||
ParticipationTab = participationTab;
|
||||
GradeOverviewTab = gradeOverviewTab;
|
||||
PlanningTab = planningTab;
|
||||
}
|
||||
|
||||
public void LoadGroup(Guid id)
|
||||
@@ -217,6 +220,7 @@ public partial class GroupDetailViewModel : ObservableObject
|
||||
ReloadExams();
|
||||
ParticipationTab.Initialize(Group.Id, Group.SchoolYear);
|
||||
GradeOverviewTab.Initialize(Group.Id, Group.GradingSystem, Group.Type, GroupTitle);
|
||||
PlanningTab.Initialize(Group.Id);
|
||||
}
|
||||
|
||||
private void ReloadExams()
|
||||
|
||||
@@ -0,0 +1,796 @@
|
||||
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.Groups;
|
||||
|
||||
// ── Ergebnisse der Verschieben-/Kopieren-Dialoge (4.2.4 / 4.1.4) ─────────────
|
||||
|
||||
public record MoveLessonTarget(DateOnly NewDate, bool ShiftFollowing);
|
||||
public record CopyUnitTarget(Guid TargetGroupId, DateOnly AnchorDate);
|
||||
|
||||
// ── Tab-ViewModel: Unterrichtsplanung (4.1 Einheiten / 4.2 Einzelstunden) ────
|
||||
|
||||
public partial class PlanningTabViewModel : ObservableObject
|
||||
{
|
||||
private readonly IUnitRepository _units;
|
||||
private readonly ILessonRepository _lessons;
|
||||
private readonly IGroupRepository _groups;
|
||||
private readonly ISubjectRepository _subjects;
|
||||
private readonly ICompetencyDomainRepository _competencyDomains;
|
||||
|
||||
private Guid _groupId;
|
||||
|
||||
public Guid GroupId => _groupId;
|
||||
public Guid? SubjectId { get; private set; }
|
||||
public int GradeLevel { get; private set; }
|
||||
public string SubjectName { get; private set; } = "";
|
||||
|
||||
[ObservableProperty] private UnitSummary? _selectedUnit;
|
||||
[ObservableProperty] private LessonSummary? _selectedLesson;
|
||||
|
||||
// Eigene Property statt "SelectedUnit.Title" im Binding-Pfad: SelectedUnit ist zwischen
|
||||
// Gruppenwechsel/Laden kurzzeitig null — ein verschachtelter Pfad würde dafür jedes Mal
|
||||
// einen Binding-Fehler loggen (siehe GroupDetailViewModel.IsDifferentiated für dasselbe Muster).
|
||||
public string SelectedUnitTitleSuffix => SelectedUnit is null ? "" : $" – {SelectedUnit.Title}";
|
||||
|
||||
public ObservableCollection<UnitSummary> Units { get; } = [];
|
||||
public ObservableCollection<LessonSummary> Lessons { get; } = [];
|
||||
|
||||
/// Aus den Material-/Kurzsymbol-Werten aller bereits vorhandenen Stunden-Phasen der Gruppe
|
||||
/// zusammengestellt (4.2.2 Autovervollständigung im Verlaufsplan-Editor).
|
||||
public List<string> KnownMaterials { get; private set; } = [];
|
||||
public List<string> KnownShorthands { get; private set; } = [];
|
||||
|
||||
public Func<Guid, Task<bool>>? OnAddUnit { get; set; }
|
||||
public Func<Unit, Task<bool>>? OnEditUnit { get; set; }
|
||||
public Func<UnitSummary, Task<bool>>? OnConfirmDeleteUnit { get; set; }
|
||||
public Func<Unit, Task<CopyUnitTarget?>>? OnPickCopyTarget { get; set; }
|
||||
public Func<Guid, Guid, List<string>, List<string>, Task<bool>>? OnAddLesson { get; set; }
|
||||
public Func<Lesson, List<string>, List<string>, Task<bool>>? OnEditLesson { get; set; }
|
||||
public Func<LessonSummary, Task<bool>>? OnConfirmDeleteLesson { get; set; }
|
||||
public Func<Lesson, Task<MoveLessonTarget?>>? OnPickMoveTarget { get; set; }
|
||||
|
||||
public PlanningTabViewModel(IUnitRepository units, ILessonRepository lessons,
|
||||
IGroupRepository groups, ISubjectRepository subjects,
|
||||
ICompetencyDomainRepository competencyDomains)
|
||||
{
|
||||
_units = units; _lessons = lessons; _groups = groups;
|
||||
_subjects = subjects; _competencyDomains = competencyDomains;
|
||||
}
|
||||
|
||||
public void Initialize(Guid groupId)
|
||||
{
|
||||
_groupId = groupId;
|
||||
var group = _groups.GetById(groupId);
|
||||
SubjectId = group?.SubjectId;
|
||||
GradeLevel = group?.GradeLevel ?? 0;
|
||||
SubjectName = SubjectId is Guid sid ? _subjects.GetById(sid)?.Name ?? "" : "";
|
||||
LoadUnits();
|
||||
}
|
||||
|
||||
private void LoadUnits()
|
||||
{
|
||||
var selectedId = SelectedUnit?.Id;
|
||||
Units.Clear();
|
||||
|
||||
var materials = new HashSet<string>();
|
||||
var shorthands = new HashSet<string>();
|
||||
|
||||
foreach (var unit in _units.GetByGroup(_groupId))
|
||||
{
|
||||
var unitLessons = _lessons.GetByUnit(unit.Id);
|
||||
foreach (var l in unitLessons)
|
||||
foreach (var p in l.Phases)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(p.Material)) materials.Add(p.Material);
|
||||
if (!string.IsNullOrWhiteSpace(p.Shorthand)) shorthands.Add(p.Shorthand);
|
||||
}
|
||||
Units.Add(new UnitSummary(unit, unitLessons));
|
||||
}
|
||||
|
||||
KnownMaterials = materials.OrderBy(m => m, StringComparer.CurrentCultureIgnoreCase).ToList();
|
||||
KnownShorthands = shorthands.OrderBy(s => s, StringComparer.CurrentCultureIgnoreCase).ToList();
|
||||
|
||||
SelectedUnit = Units.FirstOrDefault(u => u.Id == selectedId) ?? Units.FirstOrDefault();
|
||||
}
|
||||
|
||||
partial void OnSelectedUnitChanged(UnitSummary? value)
|
||||
{
|
||||
LoadLessons();
|
||||
OnPropertyChanged(nameof(SelectedUnitTitleSuffix));
|
||||
EditUnitCommand.NotifyCanExecuteChanged();
|
||||
DeleteUnitCommand.NotifyCanExecuteChanged();
|
||||
CopyUnitCommand.NotifyCanExecuteChanged();
|
||||
AddLessonCommand.NotifyCanExecuteChanged();
|
||||
}
|
||||
|
||||
private void LoadLessons()
|
||||
{
|
||||
var selectedId = SelectedLesson?.Id;
|
||||
Lessons.Clear();
|
||||
if (SelectedUnit is not null)
|
||||
foreach (var l in _lessons.GetByUnit(SelectedUnit.Id))
|
||||
Lessons.Add(new LessonSummary(l));
|
||||
SelectedLesson = Lessons.FirstOrDefault(l => l.Id == selectedId);
|
||||
}
|
||||
|
||||
partial void OnSelectedLessonChanged(LessonSummary? value)
|
||||
{
|
||||
EditLessonCommand.NotifyCanExecuteChanged();
|
||||
DeleteLessonCommand.NotifyCanExecuteChanged();
|
||||
MoveLessonCommand.NotifyCanExecuteChanged();
|
||||
AdvanceLessonStatusCommand.NotifyCanExecuteChanged();
|
||||
}
|
||||
|
||||
private bool HasSelectedUnit() => SelectedUnit is not null;
|
||||
private bool HasSelectedLesson() => SelectedLesson is not null;
|
||||
|
||||
// ── Einheiten (4.1) ────────────────────────────────────────────────────────
|
||||
|
||||
[RelayCommand]
|
||||
private async Task AddUnit()
|
||||
{
|
||||
if (OnAddUnit is null) return;
|
||||
if (await OnAddUnit(_groupId)) LoadUnits();
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(HasSelectedUnit))]
|
||||
private async Task EditUnit()
|
||||
{
|
||||
if (OnEditUnit is null || SelectedUnit is null) return;
|
||||
if (await OnEditUnit(SelectedUnit.Model)) LoadUnits();
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(HasSelectedUnit))]
|
||||
private async Task DeleteUnit()
|
||||
{
|
||||
if (OnConfirmDeleteUnit is null || SelectedUnit is null) return;
|
||||
var unit = SelectedUnit;
|
||||
if (!await OnConfirmDeleteUnit(unit)) return;
|
||||
foreach (var l in _lessons.GetByUnit(unit.Id)) _lessons.Delete(l.Id);
|
||||
_units.Delete(unit.Id);
|
||||
LoadUnits();
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(HasSelectedUnit))]
|
||||
private async Task CopyUnit()
|
||||
{
|
||||
if (OnPickCopyTarget is null || SelectedUnit is null) return;
|
||||
var unit = SelectedUnit.Model;
|
||||
var target = await OnPickCopyTarget(unit);
|
||||
if (target is null) return;
|
||||
CopyUnitAsTemplate(unit, _lessons.GetByUnit(unit.Id), target.TargetGroupId, target.AnchorDate);
|
||||
LoadUnits();
|
||||
}
|
||||
|
||||
/// Kopiert eine Einheit inkl. Stunden in eine andere Gruppe (4.1.4). "Ohne Datumsbezug"
|
||||
/// bedeutet: die relativen Tages-Abstände der Stunden zueinander bleiben erhalten, werden
|
||||
/// aber auf das neu gewählte Startdatum re-verankert statt die alten Kalendertage zu übernehmen.
|
||||
/// Reflexion wird geleert, Status auf "Geplant" zurückgesetzt. GroupId wird auf jeder neuen
|
||||
/// Lesson explizit auf die Zielgruppe gesetzt (siehe docs/Datenmodell.md).
|
||||
private void CopyUnitAsTemplate(Unit source, List<Lesson> sourceLessons, Guid targetGroupId, DateOnly anchorDate)
|
||||
{
|
||||
var earliest = sourceLessons.Count > 0
|
||||
? sourceLessons.Min(l => l.Date)
|
||||
: source.StartDate ?? anchorDate;
|
||||
|
||||
var newUnit = new Unit
|
||||
{
|
||||
GroupId = targetGroupId,
|
||||
Title = source.Title,
|
||||
Competencies = [.. source.Competencies],
|
||||
Status = UnitStatus.Planned,
|
||||
Notes = source.Notes,
|
||||
StartDate = source.StartDate.HasValue
|
||||
? anchorDate.AddDays(source.StartDate.Value.DayNumber - earliest.DayNumber) : null,
|
||||
EndDate = source.EndDate.HasValue
|
||||
? anchorDate.AddDays(source.EndDate.Value.DayNumber - earliest.DayNumber) : null,
|
||||
};
|
||||
_units.Save(newUnit);
|
||||
|
||||
foreach (var lesson in sourceLessons)
|
||||
{
|
||||
_lessons.Save(new Lesson
|
||||
{
|
||||
UnitId = newUnit.Id,
|
||||
GroupId = targetGroupId,
|
||||
Date = anchorDate.AddDays(lesson.Date.DayNumber - earliest.DayNumber),
|
||||
LessonNumber = lesson.LessonNumber,
|
||||
Topic = lesson.Topic,
|
||||
StartTime = lesson.StartTime,
|
||||
Phases = [.. lesson.Phases.Select(p => new LessonPhaseStep
|
||||
{
|
||||
Name = p.Name,
|
||||
DurationMinutes = p.DurationMinutes,
|
||||
Activity = p.Activity,
|
||||
Material = p.Material,
|
||||
Shorthand = p.Shorthand,
|
||||
})],
|
||||
Homework = lesson.Homework,
|
||||
Reflection = null,
|
||||
Status = LessonStatus.Planned,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── Einzelstunden (4.2) ───────────────────────────────────────────────────
|
||||
|
||||
[RelayCommand(CanExecute = nameof(HasSelectedUnit))]
|
||||
private async Task AddLesson()
|
||||
{
|
||||
if (OnAddLesson is null || SelectedUnit is null) return;
|
||||
if (await OnAddLesson(SelectedUnit.Id, _groupId, KnownMaterials, KnownShorthands)) LoadUnits();
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(HasSelectedLesson))]
|
||||
private async Task EditLesson()
|
||||
{
|
||||
if (OnEditLesson is null || SelectedLesson is null) return;
|
||||
if (await OnEditLesson(SelectedLesson.Model, KnownMaterials, KnownShorthands)) LoadUnits();
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(HasSelectedLesson))]
|
||||
private async Task DeleteLesson()
|
||||
{
|
||||
if (OnConfirmDeleteLesson is null || SelectedLesson is null) return;
|
||||
var lesson = SelectedLesson;
|
||||
if (!await OnConfirmDeleteLesson(lesson)) return;
|
||||
_lessons.Delete(lesson.Id);
|
||||
LoadUnits();
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(HasSelectedLesson))]
|
||||
private async Task MoveLesson()
|
||||
{
|
||||
if (OnPickMoveTarget is null || SelectedLesson is null) return;
|
||||
var lesson = SelectedLesson.Model;
|
||||
var target = await OnPickMoveTarget(lesson);
|
||||
if (target is null) return;
|
||||
MoveLessonInternal(lesson, target.NewDate, target.ShiftFollowing);
|
||||
LoadUnits();
|
||||
}
|
||||
|
||||
/// Verschiebt eine Stunde auf ein neues Datum (4.2.4). Ist "Nachrücken" aktiv, verschieben
|
||||
/// sich alle anderen noch geplanten Stunden derselben Einheit, die ursprünglich NACH der
|
||||
/// verschobenen Stunde lagen, um denselben Tages-Delta. Bereits durchgeführte Stunden werden
|
||||
/// nie angefasst — nur Date ändert sich, UnitId/GroupId bleiben unverändert.
|
||||
private void MoveLessonInternal(Lesson moved, DateOnly newDate, bool shiftFollowing)
|
||||
{
|
||||
var oldDate = moved.Date;
|
||||
var delta = newDate.DayNumber - oldDate.DayNumber;
|
||||
|
||||
if (shiftFollowing && delta != 0)
|
||||
{
|
||||
foreach (var other in _lessons.GetByUnit(moved.UnitId))
|
||||
{
|
||||
if (other.Id == moved.Id) continue;
|
||||
if (other.Status != LessonStatus.Planned) continue;
|
||||
if (other.Date <= oldDate) continue;
|
||||
other.Date = other.Date.AddDays(delta);
|
||||
_lessons.Save(other);
|
||||
}
|
||||
}
|
||||
|
||||
moved.Date = newDate;
|
||||
_lessons.Save(moved);
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(HasSelectedLesson))]
|
||||
private void AdvanceLessonStatus()
|
||||
{
|
||||
if (SelectedLesson is null || SelectedLesson.Model.Status != LessonStatus.Planned) return;
|
||||
var lesson = SelectedLesson.Model;
|
||||
lesson.Status = LessonStatus.Conducted;
|
||||
_lessons.Save(lesson);
|
||||
LoadUnits();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Anzeige-DTOs ──────────────────────────────────────────────────────────────
|
||||
|
||||
public class UnitSummary
|
||||
{
|
||||
public Guid Id { get; }
|
||||
public Unit Model { get; }
|
||||
public string Title { get; }
|
||||
public string DateRangeDisplay { get; }
|
||||
public UnitStatus Status { get; }
|
||||
public string StatusLabel { get; }
|
||||
public string StatusColorHex { get; }
|
||||
public string CompetencyCountLabel { get; }
|
||||
public int ConductedCount { get; }
|
||||
public int TotalCount { get; }
|
||||
public double ProgressFraction { get; }
|
||||
public string ProgressText { get; }
|
||||
|
||||
public UnitSummary(Unit u, List<Lesson> lessons)
|
||||
{
|
||||
Id = u.Id;
|
||||
Model = u;
|
||||
Title = u.Title;
|
||||
DateRangeDisplay = (u.StartDate, u.EndDate) switch
|
||||
{
|
||||
({ } start, { } end) => $"{start:dd.MM.yyyy} – {end:dd.MM.yyyy}",
|
||||
({ } start, null) => $"ab {start:dd.MM.yyyy}",
|
||||
(null, { } end) => $"bis {end:dd.MM.yyyy}",
|
||||
_ => "–",
|
||||
};
|
||||
Status = u.Status;
|
||||
StatusLabel = u.Status switch
|
||||
{
|
||||
UnitStatus.Planned => "Geplant",
|
||||
UnitStatus.Active => "Laufend",
|
||||
UnitStatus.Completed => "Abgeschlossen",
|
||||
_ => "",
|
||||
};
|
||||
StatusColorHex = u.Status switch
|
||||
{
|
||||
UnitStatus.Planned => "#9E9E9E",
|
||||
UnitStatus.Active => "#FB8C00",
|
||||
UnitStatus.Completed => "#43A047",
|
||||
_ => "#9E9E9E",
|
||||
};
|
||||
CompetencyCountLabel = u.Competencies.Count == 0 ? "–" : $"{u.Competencies.Count} Kompetenz(en)";
|
||||
|
||||
TotalCount = lessons.Count;
|
||||
ConductedCount = lessons.Count(l => l.Status == LessonStatus.Conducted);
|
||||
ProgressFraction = TotalCount == 0 ? 0 : (double)ConductedCount / TotalCount;
|
||||
ProgressText = TotalCount == 0 ? "Keine Stunden" : $"{ConductedCount} / {TotalCount} Stunden gehalten";
|
||||
}
|
||||
}
|
||||
|
||||
public class LessonSummary
|
||||
{
|
||||
public Guid Id { get; }
|
||||
public Lesson Model { get; }
|
||||
public Guid UnitId { get; }
|
||||
public string DateDisplay { get; }
|
||||
public int? LessonNumber { get; }
|
||||
public string Topic { get; }
|
||||
public string StartTimeDisplay { get; }
|
||||
public LessonStatus Status { get; }
|
||||
public string StatusLabel { get; }
|
||||
public string StatusColorHex { get; }
|
||||
public string PhaseCountLabel { get; }
|
||||
public string TotalDurationLabel { get; }
|
||||
public string MaterialsDisplay { get; }
|
||||
public bool HasHomework { get; }
|
||||
|
||||
public LessonSummary(Lesson l)
|
||||
{
|
||||
Id = l.Id;
|
||||
Model = l;
|
||||
UnitId = l.UnitId;
|
||||
DateDisplay = l.Date.ToString("dd.MM.yyyy");
|
||||
LessonNumber = l.LessonNumber;
|
||||
Topic = l.Topic;
|
||||
StartTimeDisplay = l.StartTime?.ToString("HH:mm") ?? "–";
|
||||
Status = l.Status;
|
||||
StatusLabel = l.Status == LessonStatus.Conducted ? "Durchgeführt" : "Geplant";
|
||||
StatusColorHex = l.Status == LessonStatus.Conducted ? "#43A047" : "#9E9E9E";
|
||||
PhaseCountLabel = l.Phases.Count == 0 ? "–" : $"{l.Phases.Count} Phasen";
|
||||
var totalMinutes = l.Phases.Sum(p => p.DurationMinutes);
|
||||
TotalDurationLabel = totalMinutes == 0 ? "–" : $"{totalMinutes} Min.";
|
||||
MaterialsDisplay = string.Join(", ", l.Phases
|
||||
.Select(p => p.Material)
|
||||
.Where(m => !string.IsNullOrWhiteSpace(m))
|
||||
.Distinct());
|
||||
HasHomework = !string.IsNullOrWhiteSpace(l.Homework);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Status-Anzeige (Einheiten/Stunden, analog NiveauDisplay) ──────────────────
|
||||
|
||||
public static class UnitStatusDisplay
|
||||
{
|
||||
public static string[] Options { get; } = ["Geplant", "Laufend", "Abgeschlossen"];
|
||||
|
||||
public static string ToName(UnitStatus s) => s switch
|
||||
{
|
||||
UnitStatus.Active => "Laufend",
|
||||
UnitStatus.Completed => "Abgeschlossen",
|
||||
_ => "Geplant",
|
||||
};
|
||||
|
||||
public static UnitStatus FromName(string? name) => name switch
|
||||
{
|
||||
"Laufend" => UnitStatus.Active,
|
||||
"Abgeschlossen" => UnitStatus.Completed,
|
||||
_ => UnitStatus.Planned,
|
||||
};
|
||||
}
|
||||
|
||||
public static class LessonStatusDisplay
|
||||
{
|
||||
public static string[] Options { get; } = ["Geplant", "Durchgeführt"];
|
||||
|
||||
public static string ToName(LessonStatus s) => s == LessonStatus.Conducted ? "Durchgeführt" : "Geplant";
|
||||
|
||||
public static LessonStatus FromName(string? name) =>
|
||||
name == "Durchgeführt" ? LessonStatus.Conducted : LessonStatus.Planned;
|
||||
}
|
||||
|
||||
// ── Dialog: Einheit anlegen / bearbeiten (4.1.2 / 4.1.3) ─────────────────────
|
||||
|
||||
public partial class UnitDialogViewModel : ObservableObject
|
||||
{
|
||||
private readonly IUnitRepository _units;
|
||||
private readonly ICompetencyDomainRepository _competencyDomains;
|
||||
private readonly Guid _groupId;
|
||||
private readonly Guid? _subjectId;
|
||||
private readonly int _gradeLevel;
|
||||
private readonly Unit? _editingUnit;
|
||||
private readonly List<string> _competencyCodes;
|
||||
|
||||
[ObservableProperty] private string _title = "";
|
||||
[ObservableProperty] private string _startDateText = "";
|
||||
[ObservableProperty] private string _endDateText = "";
|
||||
[ObservableProperty] private string _statusName = UnitStatusDisplay.Options[0];
|
||||
[ObservableProperty] private string _notes = "";
|
||||
[ObservableProperty] private bool _isCompetencyPanelOpen;
|
||||
[ObservableProperty] private string _titleError = "";
|
||||
[ObservableProperty] private string _startDateTextError = "";
|
||||
[ObservableProperty] private string _endDateTextError = "";
|
||||
|
||||
public string[] StatusOptions => UnitStatusDisplay.Options;
|
||||
public ObservableCollection<CompetencyTagGroup> CompetencyTagGroups { get; } = [];
|
||||
public string CompetencySummary => _competencyCodes.Count == 0
|
||||
? "Keine Kompetenzen" : $"{_competencyCodes.Count} Kompetenz(en)";
|
||||
|
||||
/// Fach kommt von der Lerngruppe, nicht editierbar (jede Gruppe unterrichtet ein Fach).
|
||||
public string SubjectDisplay { get; }
|
||||
|
||||
public Unit? Result { get; private set; }
|
||||
public string DialogTitle => _editingUnit is null ? "Neue Einheit anlegen" : "Einheit bearbeiten";
|
||||
public string SaveButtonText => _editingUnit is null ? "Anlegen" : "Speichern";
|
||||
|
||||
public UnitDialogViewModel(IUnitRepository units, ICompetencyDomainRepository competencyDomains,
|
||||
Guid groupId, Guid? subjectId, int gradeLevel, string subjectName, Unit? editingUnit)
|
||||
{
|
||||
_units = units; _competencyDomains = competencyDomains;
|
||||
_groupId = groupId; _subjectId = subjectId; _gradeLevel = gradeLevel;
|
||||
_editingUnit = editingUnit;
|
||||
SubjectDisplay = string.IsNullOrWhiteSpace(subjectName)
|
||||
? "Kein Fach hinterlegt (siehe Lerngruppe)" : $"Fach: {subjectName}";
|
||||
|
||||
_competencyCodes = editingUnit is not null ? [.. editingUnit.Competencies] : [];
|
||||
BuildCompetencyTagGroups();
|
||||
|
||||
if (editingUnit is not null)
|
||||
{
|
||||
Title = editingUnit.Title;
|
||||
StartDateText = editingUnit.StartDate?.ToString("dd.MM.yyyy") ?? "";
|
||||
EndDateText = editingUnit.EndDate?.ToString("dd.MM.yyyy") ?? "";
|
||||
StatusName = UnitStatusDisplay.ToName(editingUnit.Status);
|
||||
Notes = editingUnit.Notes ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildCompetencyTagGroups()
|
||||
{
|
||||
CompetencyTagGroups.Clear();
|
||||
if (!_subjectId.HasValue) return;
|
||||
var selected = _competencyCodes.ToHashSet();
|
||||
foreach (var domain in _competencyDomains.GetBySubjectAndGrade(_subjectId.Value, _gradeLevel))
|
||||
{
|
||||
var group = new CompetencyTagGroup(domain.Name, domain.Code);
|
||||
foreach (var item in domain.Items.OrderBy(i => i.SortOrder))
|
||||
{
|
||||
var tag = new CompetencyTag(item.Code, item.Description, selected.Contains(item.Code));
|
||||
tag.OnChanged = OnCompetencyToggled;
|
||||
group.Items.Add(tag);
|
||||
}
|
||||
if (group.Items.Count > 0) CompetencyTagGroups.Add(group);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCompetencyToggled(string code, bool selected)
|
||||
{
|
||||
if (selected) { if (!_competencyCodes.Contains(code)) _competencyCodes.Add(code); }
|
||||
else _competencyCodes.Remove(code);
|
||||
OnPropertyChanged(nameof(CompetencySummary));
|
||||
}
|
||||
|
||||
[RelayCommand] private void ToggleCompetencyPanel() => IsCompetencyPanelOpen = !IsCompetencyPanelOpen;
|
||||
|
||||
[RelayCommand]
|
||||
private void Save()
|
||||
{
|
||||
TitleError = ""; StartDateTextError = ""; EndDateTextError = "";
|
||||
var valid = true;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Title)) { TitleError = "Titel erforderlich."; valid = false; }
|
||||
|
||||
DateOnly? startDate = null;
|
||||
if (!string.IsNullOrWhiteSpace(StartDateText))
|
||||
{
|
||||
if (!DateOnly.TryParseExact(StartDateText, "dd.MM.yyyy", null, DateTimeStyles.None, out var s))
|
||||
{ StartDateTextError = "Format TT.MM.JJJJ."; valid = false; }
|
||||
else startDate = s;
|
||||
}
|
||||
|
||||
DateOnly? endDate = null;
|
||||
if (!string.IsNullOrWhiteSpace(EndDateText))
|
||||
{
|
||||
if (!DateOnly.TryParseExact(EndDateText, "dd.MM.yyyy", null, DateTimeStyles.None, out var e))
|
||||
{ EndDateTextError = "Format TT.MM.JJJJ."; valid = false; }
|
||||
else endDate = e;
|
||||
}
|
||||
|
||||
if (valid && startDate is not null && endDate is not null && endDate < startDate)
|
||||
{ EndDateTextError = "Ende darf nicht vor dem Start liegen."; valid = false; }
|
||||
|
||||
if (!valid) return;
|
||||
|
||||
Result = _editingUnit ?? new Unit { GroupId = _groupId };
|
||||
Result.Title = Title.Trim();
|
||||
Result.StartDate = startDate;
|
||||
Result.EndDate = endDate;
|
||||
Result.Status = UnitStatusDisplay.FromName(StatusName);
|
||||
Result.Notes = string.IsNullOrWhiteSpace(Notes) ? null : Notes.Trim();
|
||||
Result.Competencies = _competencyCodes;
|
||||
_units.Save(Result);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Dialog: Stunde anlegen / bearbeiten (4.2.2) — tabellarischer Verlaufsplan ────
|
||||
|
||||
public partial class LessonDialogViewModel : ObservableObject
|
||||
{
|
||||
private readonly ILessonRepository _lessons;
|
||||
private readonly Guid _unitId;
|
||||
private readonly Guid _groupId;
|
||||
private readonly Lesson? _editingLesson;
|
||||
|
||||
[ObservableProperty] private string _dateText = DateOnly.FromDateTime(DateTime.Today).ToString("dd.MM.yyyy");
|
||||
[ObservableProperty] private int? _lessonNumber;
|
||||
[ObservableProperty] private string _topic = "";
|
||||
[ObservableProperty] private string _startTimeText = "";
|
||||
[ObservableProperty] private string _homework = "";
|
||||
[ObservableProperty] private string _reflection = "";
|
||||
[ObservableProperty] private string _statusName = LessonStatusDisplay.Options[0];
|
||||
[ObservableProperty] private string _dateTextError = "";
|
||||
[ObservableProperty] private string _topicError = "";
|
||||
[ObservableProperty] private string _startTimeTextError = "";
|
||||
[ObservableProperty] private string _totalDurationDisplay = "0 Minuten gesamt";
|
||||
|
||||
public string[] StatusOptions => LessonStatusDisplay.Options;
|
||||
public string[] MaterialSuggestions { get; }
|
||||
public string[] ShorthandSuggestions { get; }
|
||||
public ObservableCollection<PhaseStepEditItem> Phases { get; } = [];
|
||||
|
||||
public Lesson? Result { get; private set; }
|
||||
public string DialogTitle => _editingLesson is null ? "Neue Stunde anlegen" : "Stunde bearbeiten";
|
||||
public string SaveButtonText => _editingLesson is null ? "Anlegen" : "Speichern";
|
||||
|
||||
public LessonDialogViewModel(ILessonRepository lessons, IShorthandCodeRepository shorthandCodes,
|
||||
Guid unitId, Guid groupId, List<string> materialSuggestions, List<string> shorthandHistorySuggestions,
|
||||
Lesson? editingLesson)
|
||||
{
|
||||
_lessons = lessons; _unitId = unitId; _groupId = groupId; _editingLesson = editingLesson;
|
||||
MaterialSuggestions = [.. materialSuggestions];
|
||||
|
||||
// Vorschläge kommen sowohl aus dem gepflegten Kürzel-Katalog (Einstellungen) als auch aus
|
||||
// bereits in anderen Stunden dieser Gruppe frei getippten Kurzsymbolen — ein Kurzsymbol
|
||||
// muss nicht vorab im Katalog stehen, um beim nächsten Mal wieder vorgeschlagen zu werden.
|
||||
var codes = shorthandCodes.GetAll();
|
||||
var catalogCodes = (codes.Count > 0 ? codes : DefaultShorthandCodes.All).Select(c => c.Code);
|
||||
ShorthandSuggestions = catalogCodes.Concat(shorthandHistorySuggestions)
|
||||
.Where(s => !string.IsNullOrWhiteSpace(s))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(s => s, StringComparer.CurrentCultureIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
if (editingLesson is not null)
|
||||
{
|
||||
DateText = editingLesson.Date.ToString("dd.MM.yyyy");
|
||||
LessonNumber = editingLesson.LessonNumber;
|
||||
Topic = editingLesson.Topic;
|
||||
StartTimeText = editingLesson.StartTime?.ToString("HH:mm") ?? "";
|
||||
Homework = editingLesson.Homework ?? "";
|
||||
Reflection = editingLesson.Reflection ?? "";
|
||||
StatusName = LessonStatusDisplay.ToName(editingLesson.Status);
|
||||
foreach (var p in editingLesson.Phases) AddPhaseInternal(p);
|
||||
}
|
||||
RecomputeTimes();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void AddPhase()
|
||||
{
|
||||
AddPhaseInternal(null);
|
||||
RecomputeTimes();
|
||||
}
|
||||
|
||||
private void AddPhaseInternal(LessonPhaseStep? source)
|
||||
{
|
||||
var item = new PhaseStepEditItem
|
||||
{
|
||||
Name = source?.Name ?? "",
|
||||
DurationMinutes = source?.DurationMinutes ?? 5,
|
||||
Activity = source?.Activity ?? "",
|
||||
Material = source?.Material ?? "",
|
||||
Shorthand = source?.Shorthand ?? "",
|
||||
};
|
||||
item.OnChanged = RecomputeTimes;
|
||||
item.OnRemove = RemovePhase;
|
||||
item.OnMoveUp = MovePhaseUp;
|
||||
item.OnMoveDown = MovePhaseDown;
|
||||
Phases.Add(item);
|
||||
}
|
||||
|
||||
private void RemovePhase(PhaseStepEditItem item) { Phases.Remove(item); RecomputeTimes(); }
|
||||
|
||||
private void MovePhaseUp(PhaseStepEditItem item)
|
||||
{
|
||||
var idx = Phases.IndexOf(item);
|
||||
if (idx > 0) Phases.Move(idx, idx - 1);
|
||||
RecomputeTimes();
|
||||
}
|
||||
|
||||
private void MovePhaseDown(PhaseStepEditItem item)
|
||||
{
|
||||
var idx = Phases.IndexOf(item);
|
||||
if (idx >= 0 && idx < Phases.Count - 1) Phases.Move(idx, idx + 1);
|
||||
RecomputeTimes();
|
||||
}
|
||||
|
||||
partial void OnStartTimeTextChanged(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).
|
||||
private void RecomputeTimes()
|
||||
{
|
||||
var total = Phases.Sum(p => p.DurationMinutes);
|
||||
TotalDurationDisplay = $"{total} Minuten gesamt";
|
||||
|
||||
TimeOnly? cursor = null;
|
||||
if (!string.IsNullOrWhiteSpace(StartTimeText) &&
|
||||
TimeOnly.TryParseExact(StartTimeText, "HH:mm", null, DateTimeStyles.None, out var start))
|
||||
cursor = start;
|
||||
|
||||
foreach (var p in Phases)
|
||||
{
|
||||
p.ComputedTimeDisplay = cursor is { } c ? $"ab {c:HH:mm}" : "";
|
||||
if (cursor is { } cc) cursor = cc.AddMinutes(p.DurationMinutes);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Save()
|
||||
{
|
||||
DateTextError = ""; TopicError = ""; StartTimeTextError = "";
|
||||
var valid = true;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Topic)) { TopicError = "Thema erforderlich."; valid = false; }
|
||||
|
||||
DateOnly date = default;
|
||||
if (!DateOnly.TryParseExact(DateText, "dd.MM.yyyy", null, DateTimeStyles.None, out date))
|
||||
{ DateTextError = "Format TT.MM.JJJJ."; valid = false; }
|
||||
|
||||
TimeOnly? startTime = null;
|
||||
if (!string.IsNullOrWhiteSpace(StartTimeText))
|
||||
{
|
||||
if (!TimeOnly.TryParseExact(StartTimeText, "HH:mm", null, DateTimeStyles.None, out var t))
|
||||
{ StartTimeTextError = "Format HH:MM."; valid = false; }
|
||||
else startTime = t;
|
||||
}
|
||||
|
||||
if (!valid) return;
|
||||
|
||||
Result = _editingLesson ?? new Lesson { UnitId = _unitId, GroupId = _groupId };
|
||||
Result.UnitId = _unitId;
|
||||
Result.GroupId = _groupId;
|
||||
Result.Date = date;
|
||||
Result.LessonNumber = LessonNumber;
|
||||
Result.Topic = Topic.Trim();
|
||||
Result.StartTime = startTime;
|
||||
Result.Phases = Phases.Select(p => p.ToModel()).ToList();
|
||||
Result.Homework = string.IsNullOrWhiteSpace(Homework) ? null : Homework.Trim();
|
||||
Result.Reflection = string.IsNullOrWhiteSpace(Reflection) ? null : Reflection.Trim();
|
||||
Result.Status = LessonStatusDisplay.FromName(StatusName);
|
||||
_lessons.Save(Result);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Zeile im Verlaufsplan-Editor (4.2.2) ──────────────────────────────────────
|
||||
|
||||
public partial class PhaseStepEditItem : ObservableObject
|
||||
{
|
||||
[ObservableProperty] private string _name = "";
|
||||
[ObservableProperty] private int _durationMinutes = 5;
|
||||
[ObservableProperty] private string _activity = "";
|
||||
[ObservableProperty] private string _material = "";
|
||||
[ObservableProperty] private string _shorthand = "";
|
||||
[ObservableProperty] private string _computedTimeDisplay = "";
|
||||
|
||||
public Action? OnChanged { get; set; }
|
||||
public Action<PhaseStepEditItem>? OnRemove { get; set; }
|
||||
public Action<PhaseStepEditItem>? OnMoveUp { get; set; }
|
||||
public Action<PhaseStepEditItem>? OnMoveDown { get; set; }
|
||||
|
||||
partial void OnDurationMinutesChanged(int value) => OnChanged?.Invoke();
|
||||
|
||||
[RelayCommand] private void Remove() => OnRemove?.Invoke(this);
|
||||
[RelayCommand] private void MoveUp() => OnMoveUp?.Invoke(this);
|
||||
[RelayCommand] private void MoveDown() => OnMoveDown?.Invoke(this);
|
||||
|
||||
public LessonPhaseStep ToModel() => new()
|
||||
{
|
||||
Name = Name.Trim(),
|
||||
DurationMinutes = DurationMinutes,
|
||||
Activity = Activity.Trim(),
|
||||
Material = Material.Trim(),
|
||||
Shorthand = Shorthand.Trim(),
|
||||
};
|
||||
}
|
||||
|
||||
// ── Dialog: Stunde verschieben (4.2.4) ────────────────────────────────────────
|
||||
|
||||
public partial class MoveLessonDialogViewModel : ObservableObject
|
||||
{
|
||||
[ObservableProperty] private string _newDateText;
|
||||
[ObservableProperty] private bool _shiftFollowingPlanned = true;
|
||||
[ObservableProperty] private string _newDateTextError = "";
|
||||
|
||||
public string CurrentDateDisplay { get; }
|
||||
public MoveLessonTarget? Result { get; private set; }
|
||||
|
||||
public MoveLessonDialogViewModel(DateOnly currentDate)
|
||||
{
|
||||
CurrentDateDisplay = currentDate.ToString("dd.MM.yyyy");
|
||||
_newDateText = currentDate.ToString("dd.MM.yyyy");
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Save()
|
||||
{
|
||||
NewDateTextError = "";
|
||||
if (!DateOnly.TryParseExact(NewDateText, "dd.MM.yyyy", null, DateTimeStyles.None, out var date))
|
||||
{
|
||||
NewDateTextError = "Format TT.MM.JJJJ.";
|
||||
return;
|
||||
}
|
||||
Result = new MoveLessonTarget(date, ShiftFollowingPlanned);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Dialog: Einheit als Vorlage in andere Gruppe kopieren (4.1.4) ────────────
|
||||
|
||||
public partial class CopyUnitDialogViewModel : ObservableObject
|
||||
{
|
||||
[ObservableProperty] private LearningGroup? _selectedGroup;
|
||||
[ObservableProperty] private string _newStartDateText = DateOnly.FromDateTime(DateTime.Today).ToString("dd.MM.yyyy");
|
||||
[ObservableProperty] private string _selectedGroupError = "";
|
||||
[ObservableProperty] private string _newStartDateTextError = "";
|
||||
|
||||
public ObservableCollection<LearningGroup> AvailableGroups { get; } = [];
|
||||
public CopyUnitTarget? Result { get; private set; }
|
||||
|
||||
public CopyUnitDialogViewModel(IGroupRepository groups, Guid excludeGroupId)
|
||||
{
|
||||
foreach (var g in groups.GetAll().Where(g => g.Id != excludeGroupId).OrderBy(g => g.Name))
|
||||
AvailableGroups.Add(g);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Save()
|
||||
{
|
||||
SelectedGroupError = ""; NewStartDateTextError = "";
|
||||
var valid = true;
|
||||
|
||||
if (SelectedGroup is null) { SelectedGroupError = "Zielgruppe auswählen."; valid = false; }
|
||||
|
||||
DateOnly anchor = default;
|
||||
if (!DateOnly.TryParseExact(NewStartDateText, "dd.MM.yyyy", null, DateTimeStyles.None, out anchor))
|
||||
{ NewStartDateTextError = "Format TT.MM.JJJJ."; valid = false; }
|
||||
|
||||
if (!valid) return;
|
||||
|
||||
Result = new CopyUnitTarget(SelectedGroup!.Id, anchor);
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ public partial class SettingsViewModel : ObservableObject
|
||||
private readonly PrivacySettingsService _privacy;
|
||||
private readonly IDocumentationRepository _documentation;
|
||||
private readonly IStudentRepository _students;
|
||||
private readonly IShorthandCodeRepository _shorthandCodes;
|
||||
|
||||
// ── Fächer ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -35,6 +36,14 @@ public partial class SettingsViewModel : ObservableObject
|
||||
|
||||
public ObservableCollection<SubjectListItem> Subjects { get; } = [];
|
||||
|
||||
// ── Kürzel-Katalog (Stundenverlaufsplan, 4.2.2) ──────────────────────────
|
||||
|
||||
[ObservableProperty] private string _newShorthandCode = "";
|
||||
[ObservableProperty] private string _newShorthandLabel = "";
|
||||
[ObservableProperty] private string _newShorthandCodeError = "";
|
||||
|
||||
public ObservableCollection<ShorthandCodeListItem> ShorthandCodes { get; } = [];
|
||||
|
||||
// ── Kompetenzkatalog ──────────────────────────────────────────────────────
|
||||
|
||||
[ObservableProperty] private SubjectListItem? _catalogSubject;
|
||||
@@ -110,7 +119,8 @@ public partial class SettingsViewModel : ObservableObject
|
||||
IGradingKeyTemplateRepository gradingKeyTemplates, IGradingSchemeRepository gradingSchemes,
|
||||
GradingService grading, BackupService backups, DatabaseEncryptionService dbEncryption,
|
||||
AppLockService appLock, LiteDbContext dbContext, PrivacySettingsService privacy,
|
||||
IDocumentationRepository documentation, IStudentRepository students)
|
||||
IDocumentationRepository documentation, IStudentRepository students,
|
||||
IShorthandCodeRepository shorthandCodes)
|
||||
{
|
||||
_subjects = subjects;
|
||||
_domainRepo = domainRepo;
|
||||
@@ -124,7 +134,9 @@ public partial class SettingsViewModel : ObservableObject
|
||||
_privacy = privacy;
|
||||
_documentation = documentation;
|
||||
_students = students;
|
||||
_shorthandCodes = shorthandCodes;
|
||||
LoadSubjects();
|
||||
LoadShorthandCodes();
|
||||
LoadGradingKeyTemplates();
|
||||
LoadGradingSchemes();
|
||||
LoadBackups();
|
||||
@@ -381,6 +393,40 @@ public partial class SettingsViewModel : ObservableObject
|
||||
LoadSubjects();
|
||||
}
|
||||
|
||||
// ── Kürzel-Katalog: Laden / Hinzufügen / Löschen ─────────────────────────
|
||||
|
||||
public void LoadShorthandCodes()
|
||||
{
|
||||
ShorthandCodes.Clear();
|
||||
foreach (var c in _shorthandCodes.GetAll())
|
||||
ShorthandCodes.Add(new ShorthandCodeListItem(c));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void AddShorthandCode()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(NewShorthandCode)) { NewShorthandCodeError = "Kürzel erforderlich."; return; }
|
||||
try
|
||||
{
|
||||
_shorthandCodes.Save(new ShorthandCode { Code = NewShorthandCode.Trim(), Label = NewShorthandLabel.Trim() });
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
NewShorthandCodeError = ex.Message;
|
||||
return;
|
||||
}
|
||||
NewShorthandCode = ""; NewShorthandLabel = ""; NewShorthandCodeError = "";
|
||||
LoadShorthandCodes();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void DeleteShorthandCode(ShorthandCodeListItem? item)
|
||||
{
|
||||
if (item is null) return;
|
||||
_shorthandCodes.Delete(item.Id);
|
||||
LoadShorthandCodes();
|
||||
}
|
||||
|
||||
// ── Katalog: Laden ────────────────────────────────────────────────────────
|
||||
|
||||
partial void OnCatalogSubjectChanged(SubjectListItem? value) => LoadCatalog();
|
||||
@@ -703,6 +749,13 @@ public class SubjectListItem(Subject s)
|
||||
public string ShortName { get; } = s.ShortName;
|
||||
}
|
||||
|
||||
public class ShorthandCodeListItem(ShorthandCode c)
|
||||
{
|
||||
public Guid Id { get; } = c.Id;
|
||||
public string Code { get; } = c.Code;
|
||||
public string Label { get; } = c.Label;
|
||||
}
|
||||
|
||||
public class BackupListItem(BackupInfo info)
|
||||
{
|
||||
public string Path { get; } = info.Path;
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
|
||||
xmlns:models="clr-namespace:LehrerApp.Core.Models;assembly=LehrerApp.Core"
|
||||
x:Class="LehrerApp.Desktop.Views.Groups.CopyUnitDialog"
|
||||
x:DataType="vm:CopyUnitDialogViewModel"
|
||||
Title="Einheit als Vorlage kopieren"
|
||||
Width="440" Height="320" MinWidth="380" MinHeight="280"
|
||||
CanResize="False" WindowStartupLocation="CenterOwner">
|
||||
|
||||
<Grid RowDefinitions="*,Auto" Margin="24">
|
||||
<StackPanel Grid.Row="0" Spacing="14">
|
||||
<TextBlock Text="Einheit als Vorlage kopieren" Classes="dialogtitle"/>
|
||||
<TextBlock Text="Titel, Aufbau und die zeitlichen Abstände der Stunden zueinander werden übernommen, alle Kalendertage neu ab dem gewählten Startdatum berechnet."
|
||||
FontSize="12" Opacity="0.6" TextWrapping="Wrap"/>
|
||||
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Zielgruppe *" FontSize="12" Opacity="0.7"/>
|
||||
<ComboBox ItemsSource="{Binding AvailableGroups}" SelectedItem="{Binding SelectedGroup}"
|
||||
HorizontalAlignment="Stretch" PlaceholderText="Gruppe auswählen">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="models:LearningGroup">
|
||||
<TextBlock Text="{Binding Name}"/>
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
<TextBlock Text="{Binding SelectedGroupError}" Foreground="Red" FontSize="11"
|
||||
IsVisible="{Binding SelectedGroupError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Neues Startdatum *" FontSize="12" Opacity="0.7"/>
|
||||
<TextBox Text="{Binding NewStartDateText}" PlaceholderText="TT.MM.JJJJ"/>
|
||||
<TextBlock Text="{Binding NewStartDateTextError}" Foreground="Red" FontSize="11"
|
||||
IsVisible="{Binding NewStartDateTextError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
</StackPanel>
|
||||
</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="Kopieren" 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 CopyUnitDialog : Window
|
||||
{
|
||||
public CopyUnitDialog() => InitializeComponent();
|
||||
|
||||
private void OnSave(object? s, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is CopyUnitDialogViewModel 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);
|
||||
}
|
||||
@@ -154,12 +154,7 @@
|
||||
|
||||
<!-- Tab: Planung -->
|
||||
<ContentPage Header="Planung">
|
||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<TextBlock Text="Unterrichtseinheiten" FontSize="16" Opacity="0.4"
|
||||
HorizontalAlignment="Center"/>
|
||||
<TextBlock Text="Wird implementiert." FontSize="12" Opacity="0.3"
|
||||
HorizontalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<views:PlanningTabView DataContext="{Binding PlanningTab}"/>
|
||||
</ContentPage>
|
||||
|
||||
<!-- Tab: Dokumentation -->
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
<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.LessonDialog"
|
||||
x:DataType="vm:LessonDialogViewModel"
|
||||
Title="{Binding DialogTitle}"
|
||||
Width="960" Height="760" MinWidth="720" 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="{Binding DialogTitle}" Classes="dialogtitle"/>
|
||||
|
||||
<Grid ColumnDefinitions="*,12,*,12,*,12,*">
|
||||
<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 DateTextError}" Foreground="Red" FontSize="11"
|
||||
IsVisible="{Binding DateTextError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="2" Spacing="4">
|
||||
<TextBlock Text="Stunde Nr." FontSize="12" Opacity="0.7"/>
|
||||
<NumericUpDown Value="{Binding LessonNumber}" Minimum="1" Maximum="20" FormatString="0"
|
||||
ShowButtonSpinner="False"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="4" Spacing="4">
|
||||
<TextBlock Text="Beginn" FontSize="12" Opacity="0.7"
|
||||
ToolTip.Tip="Optional. Solange es noch keinen Stundenplan gibt: von Hand eintragen, damit die Uhrzeiten je Phase unten abgeleitet werden können."/>
|
||||
<TextBox Text="{Binding StartTimeText}" PlaceholderText="HH:MM"/>
|
||||
<TextBlock Text="{Binding StartTimeTextError}" Foreground="Red" FontSize="11"
|
||||
IsVisible="{Binding StartTimeTextError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="6" Spacing="4">
|
||||
<TextBlock Text="Status" FontSize="12" Opacity="0.7"/>
|
||||
<ComboBox ItemsSource="{Binding StatusOptions}" SelectedItem="{Binding StatusName}"
|
||||
HorizontalAlignment="Stretch"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Thema *" FontSize="12" Opacity="0.7"/>
|
||||
<TextBox Text="{Binding Topic}" PlaceholderText="z.B. Brechung an planparallelen Platten"/>
|
||||
<TextBlock Text="{Binding TopicError}" Foreground="Red" FontSize="11"
|
||||
IsVisible="{Binding TopicError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
</StackPanel>
|
||||
|
||||
<Separator Margin="0,4"/>
|
||||
|
||||
<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"/>
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" Content="+ Phase" Command="{Binding AddPhaseCommand}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- Tabellenkopf -->
|
||||
<Grid ColumnDefinitions="140,130,*,110,100,26,26,26" Margin="4,0,0,0">
|
||||
<TextBlock Grid.Column="0" Text="Phase" FontSize="11" FontWeight="SemiBold" Opacity="0.6"/>
|
||||
<TextBlock Grid.Column="1" Text="Dauer / Zeit" FontSize="11" FontWeight="SemiBold" Opacity="0.6"/>
|
||||
<TextBlock Grid.Column="2" Text="Tätigkeit" FontSize="11" FontWeight="SemiBold" Opacity="0.6"/>
|
||||
<TextBlock Grid.Column="3" Text="Material" FontSize="11" FontWeight="SemiBold" Opacity="0.6"/>
|
||||
<TextBlock Grid.Column="4" Text="Kurzsymbol" FontSize="11" FontWeight="SemiBold" Opacity="0.6"/>
|
||||
</Grid>
|
||||
<Separator/>
|
||||
|
||||
<!-- Eine Zeile je Phase, echte Tabellenspalten nebeneinander statt gestapelter Karten. -->
|
||||
<ItemsControl ItemsSource="{Binding Phases}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:PhaseStepEditItem">
|
||||
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||
BorderThickness="0,0,0,1" Padding="0,8">
|
||||
<Grid ColumnDefinitions="140,130,*,110,100,26,26,26">
|
||||
<TextBox Grid.Column="0" Text="{Binding Name}" PlaceholderText="z.B. Erarbeitung"
|
||||
VerticalAlignment="Top" Margin="0,0,6,0"/>
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="6"
|
||||
VerticalAlignment="Top" Margin="0,0,6,0">
|
||||
<NumericUpDown Value="{Binding DurationMinutes}" Minimum="0" Maximum="180" Width="62"
|
||||
FormatString="0" ShowButtonSpinner="False" ToolTip.Tip="Dauer in Minuten"/>
|
||||
<TextBlock Text="{Binding ComputedTimeDisplay}" FontSize="12" Opacity="0.6"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<TextBox Grid.Column="2" Text="{Binding Activity}" AcceptsReturn="True" TextWrapping="Wrap"
|
||||
Height="56" VerticalAlignment="Top" Margin="0,0,6,0"
|
||||
PlaceholderText="Lehrer-/Schüler-Tätigkeit"
|
||||
ToolTip.Tip="Bei mehr Text scrollt das Feld intern."/>
|
||||
<AutoCompleteBox Grid.Column="3" Text="{Binding Material}"
|
||||
ItemsSource="{Binding $parent[ItemsControl].((vm:LessonDialogViewModel)DataContext).MaterialSuggestions}"
|
||||
FilterMode="Contains" MinimumPrefixLength="0" VerticalAlignment="Top"
|
||||
Margin="0,0,6,0" PlaceholderText="z.B. AB01"/>
|
||||
<AutoCompleteBox Grid.Column="4" Text="{Binding Shorthand}"
|
||||
ItemsSource="{Binding $parent[ItemsControl].((vm:LessonDialogViewModel)DataContext).ShorthandSuggestions}"
|
||||
FilterMode="Contains" MinimumPrefixLength="0" VerticalAlignment="Top"
|
||||
Margin="0,0,6,0" PlaceholderText="z.B. AB001->S, Plenum, LDE"
|
||||
ToolTip.Tip="Freitext für den schnellen Überblick — mal ein Materialfluss-Pfeil (AB001->S), mal nur eine Sozialform (Plenum, LDE)."/>
|
||||
<Button Grid.Column="5" Content="↑" Command="{Binding MoveUpCommand}" Padding="4,2"
|
||||
VerticalAlignment="Top" ToolTip.Tip="Nach oben"/>
|
||||
<Button Grid.Column="6" Content="↓" Command="{Binding MoveDownCommand}" Padding="4,2"
|
||||
VerticalAlignment="Top" Margin="2,0,0,0" ToolTip.Tip="Nach unten"/>
|
||||
<Button Grid.Column="7" Content="✕" Command="{Binding RemoveCommand}" Padding="4,2"
|
||||
VerticalAlignment="Top" Margin="2,0,0,0" ToolTip.Tip="Entfernen"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<Separator Margin="0,4"/>
|
||||
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Hausaufgabe" FontSize="12" Opacity="0.7"/>
|
||||
<TextBox Text="{Binding Homework}" AcceptsReturn="True" Height="48" TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Reflexion" FontSize="12" Opacity="0.7"/>
|
||||
<TextBox Text="{Binding Reflection}" AcceptsReturn="True" Height="56" TextWrapping="Wrap"
|
||||
PlaceholderText="Nach der Stunde: was lief gut, was nicht?"/>
|
||||
</StackPanel>
|
||||
</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="{Binding SaveButtonText}"
|
||||
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 LessonDialog : Window
|
||||
{
|
||||
public LessonDialog() => InitializeComponent();
|
||||
|
||||
private void OnSave(object? s, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is LessonDialogViewModel vm && vm.SaveCommand.CanExecute(null))
|
||||
{
|
||||
vm.SaveCommand.Execute(null);
|
||||
if (vm.Result is not null) Close(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCancel(object? s, RoutedEventArgs e) => Close(false);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<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.MoveLessonDialog"
|
||||
x:DataType="vm:MoveLessonDialogViewModel"
|
||||
Title="Stunde verschieben"
|
||||
Width="400" Height="260" MinWidth="360" MinHeight="240"
|
||||
CanResize="False" WindowStartupLocation="CenterOwner">
|
||||
|
||||
<Grid RowDefinitions="*,Auto" Margin="24">
|
||||
<StackPanel Grid.Row="0" Spacing="14">
|
||||
<TextBlock Text="Stunde verschieben" Classes="dialogtitle"/>
|
||||
<TextBlock FontSize="12" Opacity="0.6">
|
||||
<Run Text="Bisheriges Datum: "/>
|
||||
<Run Text="{Binding CurrentDateDisplay}"/>
|
||||
</TextBlock>
|
||||
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Neues Datum *" FontSize="12" Opacity="0.7"/>
|
||||
<TextBox Text="{Binding NewDateText}" PlaceholderText="TT.MM.JJJJ"/>
|
||||
<TextBlock Text="{Binding NewDateTextError}" Foreground="Red" FontSize="11"
|
||||
IsVisible="{Binding NewDateTextError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
</StackPanel>
|
||||
|
||||
<CheckBox Content="Folgestunden automatisch verschieben"
|
||||
IsChecked="{Binding ShiftFollowingPlanned}"
|
||||
ToolTip.Tip="Verschiebt alle noch geplanten Stunden derselben Einheit, die nach dieser Stunde liegen, um denselben Zeitraum. Bereits durchgeführte Stunden bleiben unverändert."/>
|
||||
</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="Verschieben" 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 MoveLessonDialog : Window
|
||||
{
|
||||
public MoveLessonDialog() => InitializeComponent();
|
||||
|
||||
private void OnSave(object? s, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is MoveLessonDialogViewModel vm && vm.SaveCommand.CanExecute(null))
|
||||
{
|
||||
vm.SaveCommand.Execute(null);
|
||||
if (vm.Result is not null) Close(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCancel(object? s, RoutedEventArgs e) => Close(false);
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
|
||||
x:Class="LehrerApp.Desktop.Views.Groups.PlanningTabView"
|
||||
x:DataType="vm:PlanningTabViewModel">
|
||||
|
||||
<Grid RowDefinitions="Auto,2*,Auto,Auto,2*" Margin="16">
|
||||
|
||||
<!-- Einheiten-Toolbar -->
|
||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto" Margin="0,0,0,8">
|
||||
<TextBlock Grid.Column="0" Text="Unterrichtseinheiten" FontSize="14" FontWeight="SemiBold"
|
||||
VerticalAlignment="Center"/>
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="8">
|
||||
<Button Content="+ Einheit" Command="{Binding AddUnitCommand}"/>
|
||||
<Button Content="Bearbeiten" Command="{Binding EditUnitCommand}"/>
|
||||
<Button Content="Als Vorlage kopieren" Command="{Binding CopyUnitCommand}"/>
|
||||
<Button Content="Löschen" Command="{Binding DeleteUnitCommand}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- Einheiten-Tabelle -->
|
||||
<DataGrid Grid.Row="1" ItemsSource="{Binding Units}"
|
||||
SelectedItem="{Binding SelectedUnit}"
|
||||
AutoGenerateColumns="False"
|
||||
IsReadOnly="True"
|
||||
GridLinesVisibility="Horizontal"
|
||||
CanUserReorderColumns="False"
|
||||
Margin="0,0,0,8">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="Titel" Binding="{Binding Title}" Width="*"/>
|
||||
<DataGridTextColumn Header="Zeitraum" Binding="{Binding DateRangeDisplay}" Width="200"/>
|
||||
<DataGridTemplateColumn Header="Status" Width="130">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate x:DataType="vm:UnitSummary">
|
||||
<Border Background="{Binding StatusColorHex}" CornerRadius="4"
|
||||
Padding="8,2" HorizontalAlignment="Left">
|
||||
<TextBlock Text="{Binding StatusLabel}" FontSize="12" Foreground="White"/>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
<DataGridTemplateColumn Header="Fortschritt" Width="180">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate x:DataType="vm:UnitSummary">
|
||||
<StackPanel Spacing="2" VerticalAlignment="Center" Margin="0,4">
|
||||
<ProgressBar Value="{Binding ProgressFraction}" Maximum="1" Height="6"/>
|
||||
<TextBlock Text="{Binding ProgressText}" FontSize="11" Opacity="0.7"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
<DataGridTextColumn Header="Kompetenzen" Binding="{Binding CompetencyCountLabel}" Width="120"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
<Separator Grid.Row="2" Margin="0,0,0,8"/>
|
||||
|
||||
<!-- Stunden-Toolbar -->
|
||||
<Grid Grid.Row="3" ColumnDefinitions="*,Auto" Margin="0,0,0,8">
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="Stunden" FontSize="14" FontWeight="SemiBold"/>
|
||||
<TextBlock Text="{Binding SelectedUnitTitleSuffix}" FontSize="14" FontWeight="SemiBold"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="8">
|
||||
<Button Content="+ Stunde" Command="{Binding AddLessonCommand}"/>
|
||||
<Button Content="Bearbeiten" Command="{Binding EditLessonCommand}"/>
|
||||
<Button Content="Verschieben" Command="{Binding MoveLessonCommand}"/>
|
||||
<Button Content="Status → Durchgeführt" Command="{Binding AdvanceLessonStatusCommand}"/>
|
||||
<Button Content="Löschen" Command="{Binding DeleteLessonCommand}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- Stunden-Tabelle -->
|
||||
<DataGrid Grid.Row="4" ItemsSource="{Binding Lessons}"
|
||||
SelectedItem="{Binding SelectedLesson}"
|
||||
AutoGenerateColumns="False"
|
||||
IsReadOnly="True"
|
||||
GridLinesVisibility="Horizontal"
|
||||
CanUserReorderColumns="False">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="Datum" Binding="{Binding DateDisplay}" Width="100"/>
|
||||
<DataGridTextColumn Header="Nr." Binding="{Binding LessonNumber}" Width="50"/>
|
||||
<DataGridTextColumn Header="Thema" Binding="{Binding Topic}" Width="*"/>
|
||||
<DataGridTextColumn Header="Beginn" Binding="{Binding StartTimeDisplay}" Width="70"/>
|
||||
<DataGridTemplateColumn Header="Status" Width="130">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate x:DataType="vm:LessonSummary">
|
||||
<Border Background="{Binding StatusColorHex}" CornerRadius="4"
|
||||
Padding="8,2" HorizontalAlignment="Left">
|
||||
<TextBlock Text="{Binding StatusLabel}" FontSize="12" Foreground="White"/>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
<DataGridTextColumn Header="Phasen" Binding="{Binding PhaseCountLabel}" Width="80"/>
|
||||
<DataGridTextColumn Header="Dauer" Binding="{Binding TotalDurationLabel}" Width="80"/>
|
||||
<DataGridTextColumn Header="Materialien" Binding="{Binding MaterialsDisplay}" Width="150"/>
|
||||
<DataGridTemplateColumn Header="HA" Width="40">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate x:DataType="vm:LessonSummary">
|
||||
<TextBlock Text="✓" IsVisible="{Binding HasHomework}" HorizontalAlignment="Center"/>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,114 @@
|
||||
using Avalonia.Controls;
|
||||
using LehrerApp.Core.Interfaces;
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Desktop.ViewModels.Groups;
|
||||
using LehrerApp.Desktop.Views.Shared;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace LehrerApp.Desktop.Views.Groups;
|
||||
|
||||
public partial class PlanningTabView : UserControl
|
||||
{
|
||||
public PlanningTabView() => InitializeComponent();
|
||||
|
||||
protected override void OnDataContextChanged(EventArgs e)
|
||||
{
|
||||
base.OnDataContextChanged(e);
|
||||
if (DataContext is PlanningTabViewModel vm)
|
||||
{
|
||||
vm.OnAddUnit = groupId => ShowUnitDialog(groupId, editingUnit: null);
|
||||
vm.OnEditUnit = unit => ShowUnitDialog(unit.GroupId, editingUnit: unit);
|
||||
vm.OnConfirmDeleteUnit = ShowDeleteUnitDialog;
|
||||
vm.OnPickCopyTarget = ShowCopyUnitDialog;
|
||||
vm.OnAddLesson = (unitId, groupId, materials, shorthands) =>
|
||||
ShowLessonDialog(unitId, groupId, materials, shorthands, editingLesson: null);
|
||||
vm.OnEditLesson = (lesson, materials, shorthands) =>
|
||||
ShowLessonDialog(lesson.UnitId, lesson.GroupId, materials, shorthands, editingLesson: lesson);
|
||||
vm.OnConfirmDeleteLesson = ShowDeleteLessonDialog;
|
||||
vm.OnPickMoveTarget = ShowMoveLessonDialog;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> ShowUnitDialog(Guid groupId, Unit? editingUnit)
|
||||
{
|
||||
if (DataContext is not PlanningTabViewModel vm) return false;
|
||||
|
||||
var dialogVm = new UnitDialogViewModel(
|
||||
App.Services.GetRequiredService<IUnitRepository>(),
|
||||
App.Services.GetRequiredService<ICompetencyDomainRepository>(),
|
||||
groupId, vm.SubjectId, vm.GradeLevel, vm.SubjectName, editingUnit);
|
||||
|
||||
var dialog = new UnitDialog { DataContext = dialogVm };
|
||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||
if (owner is null) return false;
|
||||
|
||||
var ok = await dialog.ShowDialog<bool>(owner);
|
||||
return ok && dialogVm.Result is not null;
|
||||
}
|
||||
|
||||
private async Task<bool> ShowDeleteUnitDialog(UnitSummary unit)
|
||||
{
|
||||
var info = new ConfirmDialogInfo
|
||||
{
|
||||
Title = "Einheit löschen?",
|
||||
Message = $"\"{unit.Title}\" wird inkl. aller zugehörigen Stunden endgültig gelöscht.",
|
||||
ConfirmText = "Löschen",
|
||||
};
|
||||
var dialog = new ConfirmDialog { DataContext = info };
|
||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||
return owner is not null && await dialog.ShowDialog<bool>(owner);
|
||||
}
|
||||
|
||||
private async Task<CopyUnitTarget?> ShowCopyUnitDialog(Unit unit)
|
||||
{
|
||||
var dialogVm = new CopyUnitDialogViewModel(
|
||||
App.Services.GetRequiredService<IGroupRepository>(), unit.GroupId);
|
||||
|
||||
var dialog = new CopyUnitDialog { DataContext = dialogVm };
|
||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||
if (owner is null) return null;
|
||||
|
||||
var ok = await dialog.ShowDialog<bool>(owner);
|
||||
return ok ? dialogVm.Result : null;
|
||||
}
|
||||
|
||||
private async Task<bool> ShowLessonDialog(Guid unitId, Guid groupId,
|
||||
List<string> materialSuggestions, List<string> shorthandHistorySuggestions, Lesson? editingLesson)
|
||||
{
|
||||
var dialogVm = new LessonDialogViewModel(
|
||||
App.Services.GetRequiredService<ILessonRepository>(),
|
||||
App.Services.GetRequiredService<IShorthandCodeRepository>(),
|
||||
unitId, groupId, materialSuggestions, shorthandHistorySuggestions, editingLesson);
|
||||
|
||||
var dialog = new LessonDialog { DataContext = dialogVm };
|
||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||
if (owner is null) return false;
|
||||
|
||||
var ok = await dialog.ShowDialog<bool>(owner);
|
||||
return ok && dialogVm.Result is not null;
|
||||
}
|
||||
|
||||
private async Task<bool> ShowDeleteLessonDialog(LessonSummary lesson)
|
||||
{
|
||||
var info = new ConfirmDialogInfo
|
||||
{
|
||||
Title = "Stunde löschen?",
|
||||
Message = $"Die Stunde vom {lesson.DateDisplay} wird endgültig gelöscht.",
|
||||
ConfirmText = "Löschen",
|
||||
};
|
||||
var dialog = new ConfirmDialog { DataContext = info };
|
||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||
return owner is not null && await dialog.ShowDialog<bool>(owner);
|
||||
}
|
||||
|
||||
private async Task<MoveLessonTarget?> ShowMoveLessonDialog(Lesson lesson)
|
||||
{
|
||||
var dialogVm = new MoveLessonDialogViewModel(lesson.Date);
|
||||
var dialog = new MoveLessonDialog { DataContext = dialogVm };
|
||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||
if (owner is null) return null;
|
||||
|
||||
var ok = await dialog.ShowDialog<bool>(owner);
|
||||
return ok ? dialogVm.Result : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
|
||||
x:Class="LehrerApp.Desktop.Views.Groups.UnitDialog"
|
||||
x:DataType="vm:UnitDialogViewModel"
|
||||
Title="{Binding DialogTitle}"
|
||||
Width="520" Height="600" MinWidth="440" MinHeight="380"
|
||||
CanResize="True" WindowStartupLocation="CenterOwner">
|
||||
|
||||
<Grid RowDefinitions="*,Auto" Margin="24">
|
||||
<ScrollViewer Grid.Row="0">
|
||||
<StackPanel Spacing="14" Margin="0,0,12,0">
|
||||
<StackPanel Spacing="2">
|
||||
<TextBlock Text="{Binding DialogTitle}" Classes="dialogtitle"/>
|
||||
<TextBlock Text="{Binding SubjectDisplay}" FontSize="12" Opacity="0.6"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Titel *" FontSize="12" Opacity="0.7"/>
|
||||
<TextBox Text="{Binding Title}" PlaceholderText="z.B. Optik: Reflexion und Brechung"/>
|
||||
<TextBlock Text="{Binding TitleError}" Foreground="Red" FontSize="11"
|
||||
IsVisible="{Binding TitleError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
</StackPanel>
|
||||
|
||||
<Grid ColumnDefinitions="*,12,*">
|
||||
<StackPanel Grid.Column="0" Spacing="4">
|
||||
<TextBlock Text="Start" FontSize="12" Opacity="0.7"/>
|
||||
<TextBox Text="{Binding StartDateText}" PlaceholderText="TT.MM.JJJJ"/>
|
||||
<TextBlock Text="{Binding StartDateTextError}" Foreground="Red" FontSize="11"
|
||||
IsVisible="{Binding StartDateTextError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="2" Spacing="4">
|
||||
<TextBlock Text="Ende" FontSize="12" Opacity="0.7"/>
|
||||
<TextBox Text="{Binding EndDateText}" PlaceholderText="TT.MM.JJJJ"/>
|
||||
<TextBlock Text="{Binding EndDateTextError}" Foreground="Red" FontSize="11"
|
||||
IsVisible="{Binding EndDateTextError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Status" FontSize="12" Opacity="0.7"/>
|
||||
<ComboBox ItemsSource="{Binding StatusOptions}" SelectedItem="{Binding StatusName}"
|
||||
HorizontalAlignment="Stretch"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Notizen" FontSize="12" Opacity="0.7"/>
|
||||
<TextBox Text="{Binding Notes}" AcceptsReturn="True" Height="56" TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
|
||||
<Separator Margin="0,4"/>
|
||||
|
||||
<Button Content="{Binding CompetencySummary}" Command="{Binding ToggleCompetencyPanelCommand}"
|
||||
HorizontalAlignment="Left" FontSize="12" Padding="8,4"/>
|
||||
|
||||
<ItemsControl ItemsSource="{Binding CompetencyTagGroups}" IsVisible="{Binding IsCompetencyPanelOpen}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:CompetencyTagGroup">
|
||||
<StackPanel Margin="4,2">
|
||||
<TextBlock Text="{Binding DisplayName}" FontSize="11" Opacity="0.6"/>
|
||||
<ItemsControl ItemsSource="{Binding Items}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:CompetencyTag">
|
||||
<ToggleButton Content="{Binding Display}" IsChecked="{Binding IsSelected}"
|
||||
Margin="0,2,6,2" FontSize="11" Padding="6,2"/>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</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="{Binding SaveButtonText}"
|
||||
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 UnitDialog : Window
|
||||
{
|
||||
public UnitDialog() => InitializeComponent();
|
||||
|
||||
private void OnSave(object? s, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is UnitDialogViewModel 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);
|
||||
}
|
||||
@@ -56,6 +56,54 @@
|
||||
</ScrollViewer>
|
||||
</ContentPage>
|
||||
|
||||
<!-- Tab: Kürzel-Katalog -->
|
||||
<ContentPage Header="Kürzel-Katalog">
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="32,20,32,28" Spacing="14" MaxWidth="640">
|
||||
|
||||
<TextBlock Text="Kürzel für den Stundenverlaufsplan (Kapitel 4.2), z.B. Tb = Tafelbild. Im Stundeneditor werden Von/Nach aus diesen Kürzeln zu einem Kurzsymbol wie "Tb->SH" kombiniert."
|
||||
FontSize="12" Opacity="0.6" TextWrapping="Wrap"/>
|
||||
|
||||
<ItemsControl ItemsSource="{Binding ShorthandCodes}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="vm:ShorthandCodeListItem">
|
||||
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||
BorderThickness="0,0,0,1" Padding="0,7">
|
||||
<Grid ColumnDefinitions="80,*,Auto">
|
||||
<TextBlock Grid.Column="0" Text="{Binding Code}"
|
||||
VerticalAlignment="Center" FontSize="14" FontWeight="SemiBold"/>
|
||||
<TextBlock Grid.Column="1" Text="{Binding Label}"
|
||||
VerticalAlignment="Center" Opacity="0.7" FontSize="13"/>
|
||||
<Button Grid.Column="2" Content="Löschen" FontSize="12" Padding="10,4"
|
||||
Command="{Binding $parent[ItemsControl].((vm:SettingsViewModel)DataContext).DeleteShorthandCodeCommand}"
|
||||
CommandParameter="{Binding}"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||
BorderThickness="1" CornerRadius="6" Padding="14,12">
|
||||
<StackPanel Spacing="10">
|
||||
<TextBlock Text="Neues Kürzel" FontSize="12" FontWeight="SemiBold" Opacity="0.7"/>
|
||||
<Grid ColumnDefinitions="100,12,*,12,Auto">
|
||||
<TextBox Grid.Column="0" Text="{Binding NewShorthandCode}"
|
||||
PlaceholderText="Kürzel (z.B. Tb)"/>
|
||||
<TextBox Grid.Column="2" Text="{Binding NewShorthandLabel}"
|
||||
PlaceholderText="Bedeutung (z.B. Tafelbild)"/>
|
||||
<Button Grid.Column="4" Content="Hinzufügen"
|
||||
Command="{Binding AddShorthandCodeCommand}"/>
|
||||
</Grid>
|
||||
<TextBlock Text="{Binding NewShorthandCodeError}" Foreground="Red" FontSize="12"
|
||||
IsVisible="{Binding NewShorthandCodeError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</ContentPage>
|
||||
|
||||
<!-- Tab: Kompetenzen -->
|
||||
<ContentPage Header="Kompetenzen">
|
||||
<ScrollViewer>
|
||||
|
||||
Reference in New Issue
Block a user