Stundenplan: Wochenraster, Wochennavigation, Ferien/Feiertage (Kapitel 4.3)
Neuer Stundenplan mit "Heute"-Standardansicht (Tagesliste unten angedockt, gruppenübergreifendes Wochenraster mit Fach/Klasse/Raum/Thema, Vor-/Zurück- Navigation zwischen Kalenderwochen) und separatem Bearbeiten-Raster für die wöchentliche Zuordnung. Badges für Ferien-/Klausur-Nähe und ausgegraute Ferientage direkt im Plan statt einer separaten Liste. Ferien-/Feiertage- Pflege (Bundesland, Schulferien) sitzt jetzt in den Einstellungen statt im Stundenplan selbst. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -85,6 +85,19 @@ public interface ILessonRepository
|
|||||||
void Save(Lesson lesson);
|
void Save(Lesson lesson);
|
||||||
void Delete(Guid id);
|
void Delete(Guid id);
|
||||||
}
|
}
|
||||||
|
public interface ITimetableSlotRepository
|
||||||
|
{
|
||||||
|
List<TimetableSlot> GetAll();
|
||||||
|
List<TimetableSlot> GetByGroup(Guid groupId);
|
||||||
|
void Save(TimetableSlot slot);
|
||||||
|
void Delete(Guid id);
|
||||||
|
}
|
||||||
|
public interface ISchoolHolidayRepository
|
||||||
|
{
|
||||||
|
List<SchoolHoliday> GetAll();
|
||||||
|
void Save(SchoolHoliday holiday);
|
||||||
|
void Delete(Guid id);
|
||||||
|
}
|
||||||
public interface IDocumentationRepository
|
public interface IDocumentationRepository
|
||||||
{
|
{
|
||||||
List<Documentation> GetByStudent(Guid studentId);
|
List<Documentation> GetByStudent(Guid studentId);
|
||||||
|
|||||||
@@ -110,6 +110,39 @@ public class AlternativeLessonPath
|
|||||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ein fester Termin im wöchentlichen Stundenplan (4.3): eine Lerngruppe trifft sich an einem
|
||||||
|
/// Wochentag zu einer bestimmten Stunde. Wiederkehrendes Muster, kein konkretes Datum — für
|
||||||
|
/// tatsächlich gehaltene Einzelstunden siehe <see cref="Lesson"/>. Pro Wochentag/Stunde ist
|
||||||
|
/// höchstens eine Gruppe eingetragen (ein Lehrer kann nicht gleichzeitig an zwei Orten sein).
|
||||||
|
/// </summary>
|
||||||
|
public class TimetableSlot
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
|
public Guid GroupId { get; set; }
|
||||||
|
public DayOfWeek Weekday { get; set; }
|
||||||
|
public int PeriodNumber { get; set; }
|
||||||
|
public string? Room { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unterrichtsfreier Zeitraum (4.3.5): Schulferien werden manuell gepflegt (nicht algorithmisch
|
||||||
|
/// herleitbar, jährlich neu von den Bundesländern festgelegt). Gesetzliche Feiertage werden
|
||||||
|
/// dagegen berechnet (PublicHolidayService) und nicht in der Datenbank gespeichert.
|
||||||
|
/// </summary>
|
||||||
|
public class SchoolHoliday
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public DateOnly StartDate { get; set; }
|
||||||
|
public DateOnly EndDate { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum GermanState
|
||||||
|
{
|
||||||
|
BW, BY, BE, BB, HB, HH, HE, MV, NI, NW, RP, SL, SN, ST, SH, TH
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Zeugnisnote eines Schülers in einer Lerngruppe für einen Zeitraum (Halbjahr/Gesamtjahr).
|
/// Zeugnisnote eines Schülers in einer Lerngruppe für einen Zeitraum (Halbjahr/Gesamtjahr).
|
||||||
/// <see cref="CalculatedValue"/> ist das zuletzt berechnete Ergebnis; <see cref="OverrideValue"/>
|
/// <see cref="CalculatedValue"/> ist das zuletzt berechnete Ergebnis; <see cref="OverrideValue"/>
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
using LehrerApp.Core.Models;
|
||||||
|
|
||||||
|
namespace LehrerApp.Core.Services;
|
||||||
|
|
||||||
|
public record PublicHoliday(DateOnly Date, string Name);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Berechnet die gesetzlichen Feiertage eines Bundeslands für ein Kalenderjahr (4.3.5).
|
||||||
|
/// Im Gegensatz zu Schulferien sind Feiertage über eine feste Regel je Bundesland herleitbar und
|
||||||
|
/// werden deshalb nicht in der Datenbank gespeichert.
|
||||||
|
/// </summary>
|
||||||
|
public class PublicHolidayService
|
||||||
|
{
|
||||||
|
public List<PublicHoliday> GetHolidays(int year, GermanState state)
|
||||||
|
{
|
||||||
|
var easterSunday = EasterSunday(year);
|
||||||
|
var holidays = new List<PublicHoliday>
|
||||||
|
{
|
||||||
|
new(new DateOnly(year, 1, 1), "Neujahr"),
|
||||||
|
new(easterSunday.AddDays(-2), "Karfreitag"),
|
||||||
|
new(easterSunday.AddDays(1), "Ostermontag"),
|
||||||
|
new(new DateOnly(year, 5, 1), "Tag der Arbeit"),
|
||||||
|
new(easterSunday.AddDays(39), "Christi Himmelfahrt"),
|
||||||
|
new(easterSunday.AddDays(50), "Pfingstmontag"),
|
||||||
|
new(new DateOnly(year, 10, 3), "Tag der Deutschen Einheit"),
|
||||||
|
new(new DateOnly(year, 12, 25), "1. Weihnachtstag"),
|
||||||
|
new(new DateOnly(year, 12, 26), "2. Weihnachtstag"),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (state is GermanState.BW or GermanState.BY or GermanState.ST)
|
||||||
|
holidays.Add(new(new DateOnly(year, 1, 6), "Heilige Drei Könige"));
|
||||||
|
|
||||||
|
if (state is GermanState.BE)
|
||||||
|
holidays.Add(new(new DateOnly(year, 3, 8), "Internationaler Frauentag"));
|
||||||
|
|
||||||
|
if (state is GermanState.BW or GermanState.BY or GermanState.HE or GermanState.NW
|
||||||
|
or GermanState.RP or GermanState.SL)
|
||||||
|
holidays.Add(new(easterSunday.AddDays(60), "Fronleichnam"));
|
||||||
|
|
||||||
|
if (state is GermanState.SL)
|
||||||
|
holidays.Add(new(new DateOnly(year, 8, 15), "Mariä Himmelfahrt"));
|
||||||
|
|
||||||
|
if (state is GermanState.BB or GermanState.MV or GermanState.SN or GermanState.ST
|
||||||
|
or GermanState.TH or GermanState.HB or GermanState.HH or GermanState.NI
|
||||||
|
or GermanState.SH)
|
||||||
|
holidays.Add(new(new DateOnly(year, 10, 31), "Reformationstag"));
|
||||||
|
|
||||||
|
if (state is GermanState.BW or GermanState.BY or GermanState.NW or GermanState.RP
|
||||||
|
or GermanState.SL)
|
||||||
|
holidays.Add(new(new DateOnly(year, 11, 1), "Allerheiligen"));
|
||||||
|
|
||||||
|
if (state is GermanState.SN)
|
||||||
|
holidays.Add(new(BussUndBettag(year), "Buß- und Bettag"));
|
||||||
|
|
||||||
|
return holidays.OrderBy(h => h.Date).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gauß'sche Osterformel.
|
||||||
|
private static DateOnly EasterSunday(int year)
|
||||||
|
{
|
||||||
|
int a = year % 19, b = year / 100, c = year % 100;
|
||||||
|
int d = b / 4, e = b % 4, f = (b + 8) / 25, g = (b - f + 1) / 3;
|
||||||
|
int h = (19 * a + b - d - g + 15) % 30;
|
||||||
|
int i = c / 4, k = c % 4, l = (32 + 2 * e + 2 * i - h - k) % 7;
|
||||||
|
int m = (a + 11 * h + 22 * l) / 451;
|
||||||
|
int month = (h + l - 7 * m + 114) / 31;
|
||||||
|
int day = (h + l - 7 * m + 114) % 31 + 1;
|
||||||
|
return new DateOnly(year, month, day);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Buß- und Bettag: Mittwoch vor dem 23. November (entspricht dem letzten Mittwoch vor dem
|
||||||
|
/// ersten Advent).
|
||||||
|
private static DateOnly BussUndBettag(int year)
|
||||||
|
{
|
||||||
|
var date = new DateOnly(year, 11, 23);
|
||||||
|
while (date.DayOfWeek != DayOfWeek.Wednesday) date = date.AddDays(-1);
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using LehrerApp.Core.Models;
|
||||||
|
|
||||||
|
namespace LehrerApp.Core.Services;
|
||||||
|
|
||||||
|
internal class SchoolCalendarConfig
|
||||||
|
{
|
||||||
|
public GermanState State { get; set; } = GermanState.NW;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Welches Bundesland für die Feiertagsberechnung (4.3.5) gilt.</summary>
|
||||||
|
public class SchoolCalendarSettingsService
|
||||||
|
{
|
||||||
|
private readonly string _configPath;
|
||||||
|
private SchoolCalendarConfig _config;
|
||||||
|
|
||||||
|
public GermanState State => _config.State;
|
||||||
|
|
||||||
|
public SchoolCalendarSettingsService(string appDataPath)
|
||||||
|
{
|
||||||
|
_configPath = Path.Combine(appDataPath, "schoolcalendar.json");
|
||||||
|
_config = Load();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetState(GermanState state)
|
||||||
|
{
|
||||||
|
_config.State = state;
|
||||||
|
File.WriteAllText(_configPath, JsonSerializer.Serialize(_config));
|
||||||
|
}
|
||||||
|
|
||||||
|
private SchoolCalendarConfig Load()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(_configPath))
|
||||||
|
return JsonSerializer.Deserialize<SchoolCalendarConfig>(File.ReadAllText(_configPath))
|
||||||
|
?? new SchoolCalendarConfig();
|
||||||
|
}
|
||||||
|
catch { /* beschädigte Konfiguration -> Standardwert */ }
|
||||||
|
return new SchoolCalendarConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -529,4 +529,63 @@ public sealed class RepositoryTests
|
|||||||
|
|
||||||
Assert.Empty(repo.GetAll());
|
Assert.Empty(repo.GetAll());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── TimetableSlotRepository ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TimetableSlotRepository_GetByGroup_FindetNurSlotsDerGruppe()
|
||||||
|
{
|
||||||
|
using var db = NewInMemoryContext();
|
||||||
|
var repo = new TimetableSlotRepository(db);
|
||||||
|
var groupA = Guid.NewGuid();
|
||||||
|
var groupB = Guid.NewGuid();
|
||||||
|
repo.Save(new TimetableSlot { GroupId = groupA, Weekday = DayOfWeek.Monday, PeriodNumber = 1 });
|
||||||
|
repo.Save(new TimetableSlot { GroupId = groupB, Weekday = DayOfWeek.Monday, PeriodNumber = 2 });
|
||||||
|
|
||||||
|
var result = repo.GetByGroup(groupA);
|
||||||
|
|
||||||
|
Assert.Single(result);
|
||||||
|
Assert.Equal(groupA, result[0].GroupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TimetableSlotRepository_Save_LehntDoppelbelegungDerselbenStundeAb()
|
||||||
|
{
|
||||||
|
using var db = NewInMemoryContext();
|
||||||
|
var repo = new TimetableSlotRepository(db);
|
||||||
|
repo.Save(new TimetableSlot { GroupId = Guid.NewGuid(), Weekday = DayOfWeek.Tuesday, PeriodNumber = 3 });
|
||||||
|
|
||||||
|
Assert.Throws<InvalidOperationException>(() =>
|
||||||
|
repo.Save(new TimetableSlot { GroupId = Guid.NewGuid(), Weekday = DayOfWeek.Tuesday, PeriodNumber = 3 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TimetableSlotRepository_Save_AktualisierenDesselbenSlotsIstErlaubt()
|
||||||
|
{
|
||||||
|
using var db = NewInMemoryContext();
|
||||||
|
var repo = new TimetableSlotRepository(db);
|
||||||
|
var slot = new TimetableSlot { GroupId = Guid.NewGuid(), Weekday = DayOfWeek.Tuesday, PeriodNumber = 3 };
|
||||||
|
repo.Save(slot);
|
||||||
|
|
||||||
|
slot.Room = "R204";
|
||||||
|
repo.Save(slot);
|
||||||
|
|
||||||
|
Assert.Equal("R204", repo.GetAll().Single().Room);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── SchoolHolidayRepository ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SchoolHolidayRepository_GetAll_SortiertNachStartdatum()
|
||||||
|
{
|
||||||
|
using var db = NewInMemoryContext();
|
||||||
|
var repo = new SchoolHolidayRepository(db);
|
||||||
|
repo.Save(new SchoolHoliday { Name = "Sommerferien", StartDate = new DateOnly(2026, 7, 1), EndDate = new DateOnly(2026, 8, 10) });
|
||||||
|
repo.Save(new SchoolHoliday { Name = "Osterferien", StartDate = new DateOnly(2026, 3, 30), EndDate = new DateOnly(2026, 4, 10) });
|
||||||
|
|
||||||
|
var result = repo.GetAll();
|
||||||
|
|
||||||
|
Assert.Equal("Osterferien", result[0].Name);
|
||||||
|
Assert.Equal("Sommerferien", result[1].Name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ public class LiteDbContext : IDisposable
|
|||||||
public ILiteCollection<CompetencyDomain> CompetencyDomains => _db.GetCollection<CompetencyDomain>("competency_domains");
|
public ILiteCollection<CompetencyDomain> CompetencyDomains => _db.GetCollection<CompetencyDomain>("competency_domains");
|
||||||
public ILiteCollection<ShorthandCode> ShorthandCodes => _db.GetCollection<ShorthandCode>("shorthand_codes");
|
public ILiteCollection<ShorthandCode> ShorthandCodes => _db.GetCollection<ShorthandCode>("shorthand_codes");
|
||||||
public ILiteCollection<AlternativeLessonPath> AlternativeLessonPaths => _db.GetCollection<AlternativeLessonPath>("alternative_lesson_paths");
|
public ILiteCollection<AlternativeLessonPath> AlternativeLessonPaths => _db.GetCollection<AlternativeLessonPath>("alternative_lesson_paths");
|
||||||
|
public ILiteCollection<TimetableSlot> TimetableSlots => _db.GetCollection<TimetableSlot>("timetable_slots");
|
||||||
|
public ILiteCollection<SchoolHoliday> SchoolHolidays => _db.GetCollection<SchoolHoliday>("school_holidays");
|
||||||
|
|
||||||
public void Checkpoint() => _db.Checkpoint();
|
public void Checkpoint() => _db.Checkpoint();
|
||||||
|
|
||||||
@@ -369,6 +371,9 @@ public class LiteDbContext : IDisposable
|
|||||||
CompetencyDomains.EnsureIndex(x => x.GradeLevel);
|
CompetencyDomains.EnsureIndex(x => x.GradeLevel);
|
||||||
ShorthandCodes.EnsureIndex("ux_shorthand_code", BsonExpression.Create("LOWER(TRIM($.Code))"), unique: true);
|
ShorthandCodes.EnsureIndex("ux_shorthand_code", BsonExpression.Create("LOWER(TRIM($.Code))"), unique: true);
|
||||||
AlternativeLessonPaths.EnsureIndex("ux_alt_lesson_path_name", BsonExpression.Create("LOWER(TRIM($.Name))"), unique: true);
|
AlternativeLessonPaths.EnsureIndex("ux_alt_lesson_path_name", BsonExpression.Create("LOWER(TRIM($.Name))"), unique: true);
|
||||||
|
TimetableSlots.EnsureIndex(x => x.GroupId);
|
||||||
|
TimetableSlots.EnsureIndex("ux_timetable_weekday_period",
|
||||||
|
BsonExpression.Create("STRING($.Weekday) + ':' + STRING($.PeriodNumber)"), unique: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose() => _db.Dispose();
|
public void Dispose() => _db.Dispose();
|
||||||
|
|||||||
@@ -386,6 +386,30 @@ public class AlternativeLessonPathRepository(LiteDbContext db) : IAlternativeLes
|
|||||||
public void Delete(Guid id) => db.AlternativeLessonPaths.Delete(id);
|
public void Delete(Guid id) => db.AlternativeLessonPaths.Delete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class TimetableSlotRepository(LiteDbContext db) : ITimetableSlotRepository
|
||||||
|
{
|
||||||
|
public List<TimetableSlot> GetAll() =>
|
||||||
|
db.TimetableSlots.FindAll().OrderBy(s => s.Weekday).ThenBy(s => s.PeriodNumber).ToList();
|
||||||
|
public List<TimetableSlot> GetByGroup(Guid groupId) =>
|
||||||
|
db.TimetableSlots.Find(s => s.GroupId == groupId).OrderBy(s => s.Weekday).ThenBy(s => s.PeriodNumber).ToList();
|
||||||
|
public void Save(TimetableSlot slot)
|
||||||
|
{
|
||||||
|
var occupied = db.TimetableSlots.FindAll()
|
||||||
|
.FirstOrDefault(s => s.Weekday == slot.Weekday && s.PeriodNumber == slot.PeriodNumber);
|
||||||
|
if (occupied is not null && occupied.Id != slot.Id)
|
||||||
|
throw new InvalidOperationException("Diese Stunde ist bereits belegt.");
|
||||||
|
db.TimetableSlots.Upsert(slot);
|
||||||
|
}
|
||||||
|
public void Delete(Guid id) => db.TimetableSlots.Delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SchoolHolidayRepository(LiteDbContext db) : ISchoolHolidayRepository
|
||||||
|
{
|
||||||
|
public List<SchoolHoliday> GetAll() => db.SchoolHolidays.FindAll().OrderBy(h => h.StartDate).ToList();
|
||||||
|
public void Save(SchoolHoliday holiday) => db.SchoolHolidays.Upsert(holiday);
|
||||||
|
public void Delete(Guid id) => db.SchoolHolidays.Delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
public class CompetencyDomainRepository(LiteDbContext db) : ICompetencyDomainRepository
|
public class CompetencyDomainRepository(LiteDbContext db) : ICompetencyDomainRepository
|
||||||
{
|
{
|
||||||
public List<CompetencyDomain> GetBySubjectAndGrade(Guid subjectId, int gradeLevel) =>
|
public List<CompetencyDomain> GetBySubjectAndGrade(Guid subjectId, int gradeLevel) =>
|
||||||
|
|||||||
@@ -98,6 +98,17 @@ public class FakeResults : IExamResultRepository
|
|||||||
public void SaveMany(List<ExamResult> results) { }
|
public void SaveMany(List<ExamResult> results) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class FakeGradingKeyTemplates : IGradingKeyTemplateRepository
|
||||||
|
{
|
||||||
|
private readonly List<GradingKeyTemplate> _all = [];
|
||||||
|
public List<GradingKeyTemplate> GetAll() => _all;
|
||||||
|
public List<GradingKeyTemplate> GetByGradingSystem(GradingSystem system) =>
|
||||||
|
_all.Where(t => t.GradingSystem == system).ToList();
|
||||||
|
public GradingKeyTemplate? GetById(Guid id) => _all.FirstOrDefault(t => t.Id == id);
|
||||||
|
public void Save(GradingKeyTemplate template) { _all.RemoveAll(t => t.Id == template.Id); _all.Add(template); }
|
||||||
|
public void Delete(Guid id) => _all.RemoveAll(t => t.Id == id);
|
||||||
|
}
|
||||||
|
|
||||||
public class FakeSchemes : IGradingSchemeRepository
|
public class FakeSchemes : IGradingSchemeRepository
|
||||||
{
|
{
|
||||||
private readonly Dictionary<Guid, GradingScheme> _byGroup = [];
|
private readonly Dictionary<Guid, GradingScheme> _byGroup = [];
|
||||||
@@ -206,6 +217,32 @@ public class FakeAlternativeLessonPaths(List<AlternativeLessonPath> all) : IAlte
|
|||||||
public void Delete(Guid id) => all.RemoveAll(p => p.Id == id);
|
public void Delete(Guid id) => all.RemoveAll(p => p.Id == id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class FakeTimetableSlots : ITimetableSlotRepository
|
||||||
|
{
|
||||||
|
private readonly List<TimetableSlot> _all = [];
|
||||||
|
public void Add(TimetableSlot s) => _all.Add(s);
|
||||||
|
public List<TimetableSlot> GetAll() => _all.ToList();
|
||||||
|
public List<TimetableSlot> GetByGroup(Guid groupId) => _all.Where(s => s.GroupId == groupId).ToList();
|
||||||
|
public void Save(TimetableSlot slot)
|
||||||
|
{
|
||||||
|
var occupied = _all.FirstOrDefault(s => s.Weekday == slot.Weekday && s.PeriodNumber == slot.PeriodNumber);
|
||||||
|
if (occupied is not null && occupied.Id != slot.Id)
|
||||||
|
throw new InvalidOperationException("Diese Stunde ist bereits belegt.");
|
||||||
|
_all.RemoveAll(s => s.Id == slot.Id);
|
||||||
|
_all.Add(slot);
|
||||||
|
}
|
||||||
|
public void Delete(Guid id) => _all.RemoveAll(s => s.Id == id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class FakeSchoolHolidays : ISchoolHolidayRepository
|
||||||
|
{
|
||||||
|
private readonly List<SchoolHoliday> _all = [];
|
||||||
|
public void Add(SchoolHoliday h) => _all.Add(h);
|
||||||
|
public List<SchoolHoliday> GetAll() => _all.ToList();
|
||||||
|
public void Save(SchoolHoliday holiday) { _all.RemoveAll(h => h.Id == holiday.Id); _all.Add(holiday); }
|
||||||
|
public void Delete(Guid id) => _all.RemoveAll(h => h.Id == id);
|
||||||
|
}
|
||||||
|
|
||||||
public class FakeReportGrades : IReportGradeRepository
|
public class FakeReportGrades : IReportGradeRepository
|
||||||
{
|
{
|
||||||
private readonly List<ReportGrade> _all = [];
|
private readonly List<ReportGrade> _all = [];
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
using LehrerApp.Core.Models;
|
||||||
|
using LehrerApp.Core.Services;
|
||||||
|
using LehrerApp.Data;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Settings;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Tests;
|
||||||
|
|
||||||
|
public sealed class SettingsViewModelTests
|
||||||
|
{
|
||||||
|
private static SettingsViewModel BuildViewModel(FakeSchoolHolidays? holidays = null)
|
||||||
|
{
|
||||||
|
// Bewusst kein "using": SchoolCalendarSettingsService liest den Pfad erst bei SetState,
|
||||||
|
// das Verzeichnis muss über die Lebensdauer des ViewModels bestehen bleiben.
|
||||||
|
var tempPath = System.IO.Path.Combine(
|
||||||
|
System.IO.Path.GetTempPath(), $"lehrerapp-settingsvm-tests-{Guid.NewGuid():N}");
|
||||||
|
Directory.CreateDirectory(tempPath);
|
||||||
|
|
||||||
|
return new SettingsViewModel(
|
||||||
|
new FakeSubjects([]), new FakeCompetencyDomains(), new FakeGradingKeyTemplates(),
|
||||||
|
new FakeSchemes(), new GradingService(), new BackupService(tempPath),
|
||||||
|
new DatabaseEncryptionService(), new AppLockService(tempPath),
|
||||||
|
new LiteDbContext(new MemoryStream()), new PrivacySettingsService(tempPath),
|
||||||
|
new FakeDocumentation(), new FakeStudents([]), new FakeShorthandCodes([]),
|
||||||
|
holidays ?? new FakeSchoolHolidays(), new SchoolCalendarSettingsService(tempPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddSchoolHoliday_GueltigeEingabe_WirdGespeichertUndInListeAngezeigt()
|
||||||
|
{
|
||||||
|
var holidays = new FakeSchoolHolidays();
|
||||||
|
var vm = BuildViewModel(holidays);
|
||||||
|
vm.NewHolidayName = "Sommerferien";
|
||||||
|
vm.NewHolidayStartText = "01.07.2026";
|
||||||
|
vm.NewHolidayEndText = "10.08.2026";
|
||||||
|
|
||||||
|
vm.AddSchoolHolidayCommand.Execute(null);
|
||||||
|
|
||||||
|
Assert.Single(holidays.GetAll());
|
||||||
|
Assert.Single(vm.SchoolHolidayEntries);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddSchoolHoliday_EndeVorBeginn_SetztFehlerUndSpeichertNicht()
|
||||||
|
{
|
||||||
|
var holidays = new FakeSchoolHolidays();
|
||||||
|
var vm = BuildViewModel(holidays);
|
||||||
|
vm.NewHolidayName = "Ungültig";
|
||||||
|
vm.NewHolidayStartText = "10.08.2026";
|
||||||
|
vm.NewHolidayEndText = "01.07.2026";
|
||||||
|
|
||||||
|
vm.AddSchoolHolidayCommand.Execute(null);
|
||||||
|
|
||||||
|
Assert.Empty(holidays.GetAll());
|
||||||
|
Assert.NotEqual("", vm.HolidayDateError);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddSchoolHoliday_FehlenderName_SetztFehlerUndSpeichertNicht()
|
||||||
|
{
|
||||||
|
var holidays = new FakeSchoolHolidays();
|
||||||
|
var vm = BuildViewModel(holidays);
|
||||||
|
vm.NewHolidayStartText = "01.07.2026";
|
||||||
|
vm.NewHolidayEndText = "10.08.2026";
|
||||||
|
|
||||||
|
vm.AddSchoolHolidayCommand.Execute(null);
|
||||||
|
|
||||||
|
Assert.Empty(holidays.GetAll());
|
||||||
|
Assert.NotEqual("", vm.HolidayNameError);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RemoveSchoolHoliday_EntferntEintragAusRepositoryUndListe()
|
||||||
|
{
|
||||||
|
var holidays = new FakeSchoolHolidays();
|
||||||
|
var holiday = new SchoolHoliday { Name = "Herbstferien", StartDate = new DateOnly(2026, 10, 12), EndDate = new DateOnly(2026, 10, 24) };
|
||||||
|
holidays.Add(holiday);
|
||||||
|
var vm = BuildViewModel(holidays);
|
||||||
|
|
||||||
|
vm.RemoveSchoolHolidayCommand.Execute(vm.SchoolHolidayEntries[0]);
|
||||||
|
|
||||||
|
Assert.Empty(holidays.GetAll());
|
||||||
|
Assert.Empty(vm.SchoolHolidayEntries);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SelectedStateName_Aendern_PersistiertUeberSchoolCalendarSettings()
|
||||||
|
{
|
||||||
|
var tempPath = System.IO.Path.Combine(
|
||||||
|
System.IO.Path.GetTempPath(), $"lehrerapp-settingsvm-state-tests-{Guid.NewGuid():N}");
|
||||||
|
Directory.CreateDirectory(tempPath);
|
||||||
|
var calendarSettings = new SchoolCalendarSettingsService(tempPath);
|
||||||
|
|
||||||
|
var vm = new SettingsViewModel(
|
||||||
|
new FakeSubjects([]), new FakeCompetencyDomains(), new FakeGradingKeyTemplates(),
|
||||||
|
new FakeSchemes(), new GradingService(), new BackupService(tempPath),
|
||||||
|
new DatabaseEncryptionService(), new AppLockService(tempPath),
|
||||||
|
new LiteDbContext(new MemoryStream()), new PrivacySettingsService(tempPath),
|
||||||
|
new FakeDocumentation(), new FakeStudents([]), new FakeShorthandCodes([]),
|
||||||
|
new FakeSchoolHolidays(), calendarSettings);
|
||||||
|
|
||||||
|
vm.SelectedStateName = "Bayern";
|
||||||
|
|
||||||
|
Assert.Equal(GermanState.BY, calendarSettings.State);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
using LehrerApp.Core.Models;
|
||||||
|
using LehrerApp.Core.Services;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Planning;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Tests;
|
||||||
|
|
||||||
|
public sealed class TimetableSlotDialogViewModelTests
|
||||||
|
{
|
||||||
|
private static SchoolYearService SchoolYear() => new();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Save_OhneAusgewaehlteGruppe_SetztFehlerUndSpeichertNicht()
|
||||||
|
{
|
||||||
|
var slots = new FakeTimetableSlots();
|
||||||
|
var groups = new FakeGroups([]);
|
||||||
|
var vm = new TimetableSlotDialogViewModel(slots, groups, SchoolYear(), DayOfWeek.Monday, 1, null);
|
||||||
|
|
||||||
|
vm.SaveCommand.Execute(null);
|
||||||
|
|
||||||
|
Assert.Null(vm.Result);
|
||||||
|
Assert.NotEqual("", vm.GroupError);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Save_MitAusgewaehlterGruppe_LegtNeuenSlotAn()
|
||||||
|
{
|
||||||
|
var group = new LearningGroup { Name = "Q1 Chemie" };
|
||||||
|
var slots = new FakeTimetableSlots();
|
||||||
|
var groups = new FakeGroups([group]);
|
||||||
|
var vm = new TimetableSlotDialogViewModel(slots, groups, SchoolYear(), DayOfWeek.Monday, 1, null)
|
||||||
|
{
|
||||||
|
SelectedGroupName = "Q1 Chemie", Room = "R204",
|
||||||
|
};
|
||||||
|
|
||||||
|
vm.SaveCommand.Execute(null);
|
||||||
|
|
||||||
|
Assert.NotNull(vm.Result);
|
||||||
|
Assert.Equal(group.Id, vm.Result!.GroupId);
|
||||||
|
Assert.Equal("R204", vm.Result.Room);
|
||||||
|
Assert.Single(slots.GetAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Save_BelegteStunde_ZeigtFreundlicheFehlermeldungStattAbsturz()
|
||||||
|
{
|
||||||
|
var groupA = new LearningGroup { Name = "Q1 Chemie" };
|
||||||
|
var groupB = new LearningGroup { Name = "Q1 Physik" };
|
||||||
|
var slots = new FakeTimetableSlots();
|
||||||
|
slots.Add(new TimetableSlot { GroupId = groupA.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1 });
|
||||||
|
var groups = new FakeGroups([groupA, groupB]);
|
||||||
|
var vm = new TimetableSlotDialogViewModel(slots, groups, SchoolYear(), DayOfWeek.Monday, 1, null)
|
||||||
|
{
|
||||||
|
SelectedGroupName = "Q1 Physik",
|
||||||
|
};
|
||||||
|
|
||||||
|
vm.SaveCommand.Execute(null);
|
||||||
|
|
||||||
|
Assert.Null(vm.Result);
|
||||||
|
Assert.Equal("Diese Stunde ist bereits belegt.", vm.GroupError);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Delete_BeimBearbeitenEinesVorhandenenSlots_EntferntIhn()
|
||||||
|
{
|
||||||
|
var group = new LearningGroup { Name = "Q1 Chemie" };
|
||||||
|
var slot = new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1 };
|
||||||
|
var slots = new FakeTimetableSlots();
|
||||||
|
slots.Add(slot);
|
||||||
|
var groups = new FakeGroups([group]);
|
||||||
|
var vm = new TimetableSlotDialogViewModel(slots, groups, SchoolYear(), DayOfWeek.Monday, 1, slot);
|
||||||
|
|
||||||
|
vm.DeleteCommand.Execute(null);
|
||||||
|
|
||||||
|
Assert.True(vm.Deleted);
|
||||||
|
Assert.Empty(slots.GetAll());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,413 @@
|
|||||||
|
using LehrerApp.Core.Models;
|
||||||
|
using LehrerApp.Core.Services;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Planning;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Tests;
|
||||||
|
|
||||||
|
public sealed class TimetableViewModelTests
|
||||||
|
{
|
||||||
|
private static TimetableViewModel BuildViewModel(
|
||||||
|
FakeTimetableSlots slots, FakeGroups groups, FakeSchoolHolidays? holidays = null,
|
||||||
|
FakeSubjects? subjects = null, FakeLessons? lessons = null, FakeExams? exams = null,
|
||||||
|
SchoolCalendarSettingsService? calendarSettings = null)
|
||||||
|
{
|
||||||
|
// Bewusst kein "using": SchoolCalendarSettingsService liest den Pfad nur bei Bedarf
|
||||||
|
// (SetState), das Verzeichnis muss über die Lebensdauer des ViewModels bestehen bleiben.
|
||||||
|
var tempPath = System.IO.Path.Combine(
|
||||||
|
System.IO.Path.GetTempPath(), $"lehrerapp-timetablevm-tests-{Guid.NewGuid():N}");
|
||||||
|
Directory.CreateDirectory(tempPath);
|
||||||
|
return new TimetableViewModel(
|
||||||
|
slots, groups, subjects ?? new FakeSubjects([]),
|
||||||
|
lessons ?? new FakeLessons(), exams ?? new FakeExams([]),
|
||||||
|
holidays ?? new FakeSchoolHolidays(),
|
||||||
|
calendarSettings ?? new SchoolCalendarSettingsService(tempPath),
|
||||||
|
new PublicHolidayService(), new SchoolYearService());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Nächstes Datum ab (inkl.) <paramref name="from"/>, das auf einen Wochentag Mo-Fr fällt —
|
||||||
|
/// nur diese Tage haben eine Spalte im Raster. Für Badge-Tests reicht ein beliebiger
|
||||||
|
/// Mo-Fr-Wochentag: die Badges hängen nur am Wochentag (nicht am konkreten Kalenderdatum),
|
||||||
|
/// da `TimetableSlot` ein wiederkehrendes Muster ohne Datum ist.
|
||||||
|
private static DateOnly NextGridWeekday(DateOnly from)
|
||||||
|
{
|
||||||
|
var d = from;
|
||||||
|
while (d.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday) d = d.AddDays(1);
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Datum des angegebenen Wochentags in der laufenden Kalenderwoche — exakt wie
|
||||||
|
/// `TimetableViewModel.BuildWeekOverview` es berechnet, für Tests der Wochenkachel-Inhalte
|
||||||
|
/// (Thema, Klausur am Tag), die anders als die Badges an ein konkretes Datum gebunden sind.
|
||||||
|
private static DateOnly DateInCurrentWeek(DayOfWeek weekday)
|
||||||
|
{
|
||||||
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
var monday = today.AddDays(-((int)today.DayOfWeek + 6) % 7);
|
||||||
|
return monday.AddDays((int)weekday - (int)DayOfWeek.Monday);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Load_BautEineZelleProWochentagUndStundePlusKopfzeilen()
|
||||||
|
{
|
||||||
|
var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([]));
|
||||||
|
|
||||||
|
// 1 Ecke + 5 Wochentage + 10 Stunden × (1 Label + 5 Zellen)
|
||||||
|
Assert.Equal(6 + 10 * 6, vm.Cells.Count);
|
||||||
|
Assert.Equal(6 + 10 * 6, vm.WeekItems.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Load_ZeigtZugewieseneGruppeInDerPassendenZelle()
|
||||||
|
{
|
||||||
|
var weekday = NextGridWeekday(DateOnly.FromDateTime(DateTime.Today));
|
||||||
|
var group = new LearningGroup { Name = "Q1 Chemie" };
|
||||||
|
var slots = new FakeTimetableSlots();
|
||||||
|
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = weekday.DayOfWeek, PeriodNumber = 1, Room = "R204" });
|
||||||
|
var vm = BuildViewModel(slots, new FakeGroups([group]));
|
||||||
|
|
||||||
|
var cell = vm.Cells.Single(c => c.IsSlotCell && c.Weekday == weekday.DayOfWeek && c.PeriodNumber == 1);
|
||||||
|
|
||||||
|
Assert.True(cell.IsAssigned);
|
||||||
|
Assert.Equal("Q1 Chemie · R204", cell.Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HoursWarnings_WeichtDieZugewieseneStundenzahlAb_ZeigtWarnung()
|
||||||
|
{
|
||||||
|
var group = new LearningGroup { Name = "Q1 Chemie", SchoolYear = new SchoolYearService().CurrentSchoolYear(), HoursPerWeek = 3 };
|
||||||
|
var slots = new FakeTimetableSlots();
|
||||||
|
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1 });
|
||||||
|
var vm = BuildViewModel(slots, new FakeGroups([group]));
|
||||||
|
|
||||||
|
Assert.Single(vm.HoursWarnings);
|
||||||
|
Assert.Contains("1 von 3", vm.HoursWarnings[0].Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HoursWarnings_PassendeStundenzahl_KeineWarnung()
|
||||||
|
{
|
||||||
|
var group = new LearningGroup { Name = "Q1 Chemie", SchoolYear = new SchoolYearService().CurrentSchoolYear(), HoursPerWeek = 1 };
|
||||||
|
var slots = new FakeTimetableSlots();
|
||||||
|
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1 });
|
||||||
|
var vm = BuildViewModel(slots, new FakeGroups([group]));
|
||||||
|
|
||||||
|
Assert.Empty(vm.HoursWarnings);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── "Heute"-Ansicht: Tagesliste (Nutzer-Feedback: nicht-editierende Standardansicht) ────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Load_ZeigtHeutigeStundeMitGruppeUndRaum()
|
||||||
|
{
|
||||||
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
var group = new LearningGroup { Name = "Q1 Chemie" };
|
||||||
|
var slots = new FakeTimetableSlots();
|
||||||
|
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 3, Room = "R204" });
|
||||||
|
var vm = BuildViewModel(slots, new FakeGroups([group]));
|
||||||
|
|
||||||
|
var item = Assert.Single(vm.TodayItems);
|
||||||
|
Assert.Equal("Q1 Chemie", item.GroupName);
|
||||||
|
Assert.Equal("R204", item.Room);
|
||||||
|
Assert.Equal(3, item.PeriodNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Load_HeutigeStundeMitLektionUndKlausur_ZeigtBeides()
|
||||||
|
{
|
||||||
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
var group = new LearningGroup { Name = "Q1 Chemie" };
|
||||||
|
var slots = new FakeTimetableSlots();
|
||||||
|
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 1 });
|
||||||
|
var lessons = new FakeLessons();
|
||||||
|
lessons.Add(new Lesson { GroupId = group.Id, Date = today, Topic = "Redoxreaktionen" });
|
||||||
|
var exams = new FakeExams([new Exam { GroupId = group.Id, Date = today, Title = "Klausur Nr. 2" }]);
|
||||||
|
var vm = BuildViewModel(slots, new FakeGroups([group]), lessons: lessons, exams: exams);
|
||||||
|
|
||||||
|
var item = Assert.Single(vm.TodayItems);
|
||||||
|
Assert.Equal("Redoxreaktionen", item.LessonTopic);
|
||||||
|
Assert.Equal("Klausur Nr. 2", item.ExamTitle);
|
||||||
|
Assert.True(item.HasExam);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OpenGroup_RuftOnNavigateToGroupMitDerGroupIdAuf()
|
||||||
|
{
|
||||||
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
var group = new LearningGroup { Name = "Q1 Chemie" };
|
||||||
|
var slots = new FakeTimetableSlots();
|
||||||
|
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 1 });
|
||||||
|
var vm = BuildViewModel(slots, new FakeGroups([group]));
|
||||||
|
Guid? navigatedTo = null;
|
||||||
|
vm.OnNavigateToGroup = id => navigatedTo = id;
|
||||||
|
|
||||||
|
vm.OpenGroupCommand.Execute(vm.TodayItems[0].GroupId);
|
||||||
|
|
||||||
|
Assert.Equal(group.Id, navigatedTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ShowEditor_SchaltetAufBearbeiten_Tab()
|
||||||
|
{
|
||||||
|
var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([]));
|
||||||
|
|
||||||
|
vm.ShowEditorCommand.Execute(null);
|
||||||
|
|
||||||
|
Assert.Equal(1, vm.ActiveTabIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── "Heute"-Ansicht: Wochenraster (Nutzer-Feedback, zweite Iteration) ────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Load_Wochenkachel_ZeigtFachKlasseRaumUndThema()
|
||||||
|
{
|
||||||
|
var subject = new Subject { Name = "Chemie", ShortName = "Ch" };
|
||||||
|
var group = new LearningGroup { Name = "Q1 Chemie", SubjectId = subject.Id };
|
||||||
|
var date = DateInCurrentWeek(DayOfWeek.Monday);
|
||||||
|
var slots = new FakeTimetableSlots();
|
||||||
|
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1, Room = "R204" });
|
||||||
|
var lessons = new FakeLessons();
|
||||||
|
lessons.Add(new Lesson { GroupId = group.Id, Date = date, Topic = "Redoxreaktionen" });
|
||||||
|
var vm = BuildViewModel(slots, new FakeGroups([group]), subjects: new FakeSubjects([subject]), lessons: lessons);
|
||||||
|
|
||||||
|
var cell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 1);
|
||||||
|
Assert.Equal("Ch", cell.SubjectLabel);
|
||||||
|
Assert.Equal("Q1 Chemie", cell.GroupName);
|
||||||
|
Assert.Equal("R204", cell.Room);
|
||||||
|
Assert.Equal("Redoxreaktionen", cell.Topic);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Load_Wochenkachel_UnbelegteZelle_IstNichtZugewiesen()
|
||||||
|
{
|
||||||
|
var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([]));
|
||||||
|
|
||||||
|
var cell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 1);
|
||||||
|
Assert.False(cell.IsAssigned);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Load_Wochenkachel_KlausurAmTag_ZeigtKlausurIcon()
|
||||||
|
{
|
||||||
|
var group = new LearningGroup { Name = "Q1 Chemie" };
|
||||||
|
var date = DateInCurrentWeek(DayOfWeek.Monday);
|
||||||
|
var slots = new FakeTimetableSlots();
|
||||||
|
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1 });
|
||||||
|
var exams = new FakeExams([new Exam { GroupId = group.Id, Date = date, Title = "Klausur" }]);
|
||||||
|
var vm = BuildViewModel(slots, new FakeGroups([group]), exams: exams);
|
||||||
|
|
||||||
|
var cell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 1);
|
||||||
|
Assert.True(cell.HasExam);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Load_Wochenkachel_TagInSchulferien_WirdAusgegraut()
|
||||||
|
{
|
||||||
|
var group = new LearningGroup { Name = "Q1 Chemie" };
|
||||||
|
var date = DateInCurrentWeek(DayOfWeek.Monday);
|
||||||
|
var slots = new FakeTimetableSlots();
|
||||||
|
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1 });
|
||||||
|
var holidays = new FakeSchoolHolidays();
|
||||||
|
holidays.Add(new SchoolHoliday { Name = "Ferien", StartDate = date, EndDate = date.AddDays(5) });
|
||||||
|
var vm = BuildViewModel(slots, new FakeGroups([group]), holidays);
|
||||||
|
|
||||||
|
var cell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 1);
|
||||||
|
Assert.True(cell.IsHoliday);
|
||||||
|
Assert.Equal("#BDBDBD", cell.ColorHex);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Load_Wochenkachel_TagAusserhalbVonFerien_WirdNichtAusgegraut()
|
||||||
|
{
|
||||||
|
var group = new LearningGroup { Name = "Q1 Chemie" };
|
||||||
|
var slots = new FakeTimetableSlots();
|
||||||
|
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1 });
|
||||||
|
var vm = BuildViewModel(slots, new FakeGroups([group]));
|
||||||
|
|
||||||
|
var cell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 1);
|
||||||
|
Assert.False(cell.IsHoliday);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Load_Wochenkachel_ExperimentInPhaseGeplant_ZeigtExperimentIcon()
|
||||||
|
{
|
||||||
|
var group = new LearningGroup { Name = "Q1 Chemie" };
|
||||||
|
var date = DateInCurrentWeek(DayOfWeek.Monday);
|
||||||
|
var slots = new FakeTimetableSlots();
|
||||||
|
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1 });
|
||||||
|
var lessons = new FakeLessons();
|
||||||
|
lessons.Add(new Lesson { GroupId = group.Id, Date = date, Phases = [new LessonPhaseStep { Activity = "Experiment: Redoxreihe" }] });
|
||||||
|
var vm = BuildViewModel(slots, new FakeGroups([group]), lessons: lessons);
|
||||||
|
|
||||||
|
var cell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 1);
|
||||||
|
Assert.True(cell.HasExperiment);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Load_Wochenkachel_OhneExperimentErwaehnung_KeinExperimentIcon()
|
||||||
|
{
|
||||||
|
var group = new LearningGroup { Name = "Q1 Chemie" };
|
||||||
|
var date = DateInCurrentWeek(DayOfWeek.Monday);
|
||||||
|
var slots = new FakeTimetableSlots();
|
||||||
|
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1 });
|
||||||
|
var lessons = new FakeLessons();
|
||||||
|
lessons.Add(new Lesson { GroupId = group.Id, Date = date, Phases = [new LessonPhaseStep { Activity = "Stillarbeit" }] });
|
||||||
|
var vm = BuildViewModel(slots, new FakeGroups([group]), lessons: lessons);
|
||||||
|
|
||||||
|
var cell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 1);
|
||||||
|
Assert.False(cell.HasExperiment);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Load_Wochenkachel_LetzteStundeVorKlausur_ZeigtIcon()
|
||||||
|
{
|
||||||
|
var date = DateInCurrentWeek(DayOfWeek.Monday);
|
||||||
|
var group = new LearningGroup { Name = "Q1 Chemie" };
|
||||||
|
var slots = new FakeTimetableSlots();
|
||||||
|
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1 });
|
||||||
|
var exams = new FakeExams([new Exam { GroupId = group.Id, Date = date.AddDays(1), Title = "Klausur" }]);
|
||||||
|
var vm = BuildViewModel(slots, new FakeGroups([group]), exams: exams);
|
||||||
|
|
||||||
|
var cell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 1);
|
||||||
|
Assert.True(cell.IsLastBeforeExam);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Load_Wochenkachel_KeineAnstehendeKlausur_KeinIcon()
|
||||||
|
{
|
||||||
|
var group = new LearningGroup { Name = "Q1 Chemie" };
|
||||||
|
var slots = new FakeTimetableSlots();
|
||||||
|
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1 });
|
||||||
|
var vm = BuildViewModel(slots, new FakeGroups([group]));
|
||||||
|
|
||||||
|
var cell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 1);
|
||||||
|
Assert.False(cell.IsLastBeforeExam);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PreviousWeek_ZeigtVorherigeKalenderwoche()
|
||||||
|
{
|
||||||
|
var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([]));
|
||||||
|
var initialLabel = vm.WeekRangeLabel;
|
||||||
|
|
||||||
|
vm.PreviousWeekCommand.Execute(null);
|
||||||
|
|
||||||
|
Assert.NotEqual(initialLabel, vm.WeekRangeLabel);
|
||||||
|
Assert.False(vm.IsCurrentWeek);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NextWeek_GefolgtVonCurrentWeek_KehrtZurLaufendenWocheZurueck()
|
||||||
|
{
|
||||||
|
var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([]));
|
||||||
|
var initialLabel = vm.WeekRangeLabel;
|
||||||
|
|
||||||
|
vm.NextWeekCommand.Execute(null);
|
||||||
|
Assert.NotEqual(initialLabel, vm.WeekRangeLabel);
|
||||||
|
|
||||||
|
vm.CurrentWeekCommand.Execute(null);
|
||||||
|
Assert.Equal(initialLabel, vm.WeekRangeLabel);
|
||||||
|
Assert.True(vm.IsCurrentWeek);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NextWeek_BadgeGehoertZurAngezeigtenWoche_NichtZuHeute()
|
||||||
|
{
|
||||||
|
// Klausur liegt in der übernächsten Woche relativ zu "heute" — im Wochenraster einer
|
||||||
|
// Woche dahinter (also der übernächsten Woche selbst) muss "letzte Stunde vor Klausur"
|
||||||
|
// erscheinen, nicht bereits in der aktuellen Woche.
|
||||||
|
var group = new LearningGroup { Name = "Q1 Chemie" };
|
||||||
|
var currentWeekMonday = DateInCurrentWeek(DayOfWeek.Monday);
|
||||||
|
var examWeekMonday = currentWeekMonday.AddDays(14);
|
||||||
|
var slots = new FakeTimetableSlots();
|
||||||
|
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1 });
|
||||||
|
var exams = new FakeExams([new Exam { GroupId = group.Id, Date = examWeekMonday.AddDays(1), Title = "Klausur" }]);
|
||||||
|
var vm = BuildViewModel(slots, new FakeGroups([group]), exams: exams);
|
||||||
|
|
||||||
|
var currentWeekCell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 1);
|
||||||
|
Assert.False(currentWeekCell.IsLastBeforeExam);
|
||||||
|
|
||||||
|
vm.NextWeekCommand.Execute(null);
|
||||||
|
vm.NextWeekCommand.Execute(null);
|
||||||
|
var examWeekCell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 1);
|
||||||
|
Assert.True(examWeekCell.IsLastBeforeExam);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Badge: letzte/vorletzte Stunde vor Ferien (Bearbeiten-Raster) ────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Load_LetzteStundeVorFerien_ZeigtBadge1()
|
||||||
|
{
|
||||||
|
var weekday = NextGridWeekday(DateOnly.FromDateTime(DateTime.Today));
|
||||||
|
var group = new LearningGroup { Name = "Q1 Chemie" };
|
||||||
|
var slots = new FakeTimetableSlots();
|
||||||
|
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = weekday.DayOfWeek, PeriodNumber = 1 });
|
||||||
|
var holidays = new FakeSchoolHolidays();
|
||||||
|
holidays.Add(new SchoolHoliday { Name = "Ferien", StartDate = weekday.AddDays(1), EndDate = weekday.AddDays(10) });
|
||||||
|
var vm = BuildViewModel(slots, new FakeGroups([group]), holidays);
|
||||||
|
|
||||||
|
var cell = vm.Cells.Single(c => c.IsSlotCell && c.Weekday == weekday.DayOfWeek && c.PeriodNumber == 1);
|
||||||
|
Assert.Equal("1", cell.BadgeText);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Load_Wochenkachel_LetzteStundeVorFerien_ZeigtBadge1()
|
||||||
|
{
|
||||||
|
var date = DateInCurrentWeek(DayOfWeek.Monday);
|
||||||
|
var group = new LearningGroup { Name = "Q1 Chemie" };
|
||||||
|
var slots = new FakeTimetableSlots();
|
||||||
|
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1 });
|
||||||
|
var holidays = new FakeSchoolHolidays();
|
||||||
|
holidays.Add(new SchoolHoliday { Name = "Ferien", StartDate = date.AddDays(1), EndDate = date.AddDays(10) });
|
||||||
|
var vm = BuildViewModel(slots, new FakeGroups([group]), holidays);
|
||||||
|
|
||||||
|
var weekCell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 1);
|
||||||
|
Assert.Equal("1", weekCell.HolidayBadge);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Load_VorletzteStundeVorFerien_ZeigtBadge2()
|
||||||
|
{
|
||||||
|
var firstOccurrence = NextGridWeekday(DateOnly.FromDateTime(DateTime.Today));
|
||||||
|
var secondOccurrence = firstOccurrence.AddDays(7);
|
||||||
|
var group = new LearningGroup { Name = "Q1 Chemie" };
|
||||||
|
var slots = new FakeTimetableSlots();
|
||||||
|
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = firstOccurrence.DayOfWeek, PeriodNumber = 1 });
|
||||||
|
var holidays = new FakeSchoolHolidays();
|
||||||
|
holidays.Add(new SchoolHoliday { Name = "Ferien", StartDate = secondOccurrence.AddDays(1), EndDate = secondOccurrence.AddDays(10) });
|
||||||
|
var vm = BuildViewModel(slots, new FakeGroups([group]), holidays);
|
||||||
|
|
||||||
|
var cell = vm.Cells.Single(c => c.IsSlotCell && c.Weekday == firstOccurrence.DayOfWeek && c.PeriodNumber == 1);
|
||||||
|
Assert.Equal("2", cell.BadgeText);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Load_Doppelstunde_BeideStundenZeigenDasselbeBadge()
|
||||||
|
{
|
||||||
|
var weekday = NextGridWeekday(DateOnly.FromDateTime(DateTime.Today));
|
||||||
|
var group = new LearningGroup { Name = "Q1 Chemie" };
|
||||||
|
var slots = new FakeTimetableSlots();
|
||||||
|
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = weekday.DayOfWeek, PeriodNumber = 1 });
|
||||||
|
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = weekday.DayOfWeek, PeriodNumber = 2 });
|
||||||
|
var holidays = new FakeSchoolHolidays();
|
||||||
|
holidays.Add(new SchoolHoliday { Name = "Ferien", StartDate = weekday.AddDays(1), EndDate = weekday.AddDays(10) });
|
||||||
|
var vm = BuildViewModel(slots, new FakeGroups([group]), holidays);
|
||||||
|
|
||||||
|
var period1 = vm.Cells.Single(c => c.IsSlotCell && c.Weekday == weekday.DayOfWeek && c.PeriodNumber == 1);
|
||||||
|
var period2 = vm.Cells.Single(c => c.IsSlotCell && c.Weekday == weekday.DayOfWeek && c.PeriodNumber == 2);
|
||||||
|
Assert.Equal("1", period1.BadgeText);
|
||||||
|
Assert.Equal("1", period2.BadgeText);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Load_KeineAnstehendenFerien_KeinBadge()
|
||||||
|
{
|
||||||
|
var weekday = NextGridWeekday(DateOnly.FromDateTime(DateTime.Today));
|
||||||
|
var group = new LearningGroup { Name = "Q1 Chemie" };
|
||||||
|
var slots = new FakeTimetableSlots();
|
||||||
|
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = weekday.DayOfWeek, PeriodNumber = 1 });
|
||||||
|
var vm = BuildViewModel(slots, new FakeGroups([group]));
|
||||||
|
|
||||||
|
var cell = vm.Cells.Single(c => c.IsSlotCell && c.Weekday == weekday.DayOfWeek && c.PeriodNumber == 1);
|
||||||
|
Assert.False(cell.HasBadge);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ using LehrerApp.Data;
|
|||||||
using LehrerApp.Desktop.Services;
|
using LehrerApp.Desktop.Services;
|
||||||
using LehrerApp.Desktop.ViewModels;
|
using LehrerApp.Desktop.ViewModels;
|
||||||
using LehrerApp.Desktop.ViewModels.Groups;
|
using LehrerApp.Desktop.ViewModels.Groups;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Planning;
|
||||||
using LehrerApp.Desktop.ViewModels.Students;
|
using LehrerApp.Desktop.ViewModels.Students;
|
||||||
using LehrerApp.Desktop.Views;
|
using LehrerApp.Desktop.Views;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
@@ -103,6 +104,10 @@ public class App : Application
|
|||||||
var sl = Services.GetRequiredService<StudentListViewModel>();
|
var sl = Services.GetRequiredService<StudentListViewModel>();
|
||||||
sl.OnNavigateToDetail = id => main.NavigateToStudent(id);
|
sl.OnNavigateToDetail = id => main.NavigateToStudent(id);
|
||||||
sl.OnAddStudent = () => ShowAddStudentDialog();
|
sl.OnAddStudent = () => ShowAddStudentDialog();
|
||||||
|
|
||||||
|
// Stundenplan "Heute" → GroupDetail (Tab "Planung")
|
||||||
|
var timetable = Services.GetRequiredService<TimetableViewModel>();
|
||||||
|
timetable.OnNavigateToGroup = id => main.NavigateToGroupDetail(id, 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task ShowAddStudentDialog()
|
private static async Task ShowAddStudentDialog()
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using LehrerApp.Data.Repositories;
|
|||||||
using LehrerApp.Desktop.Services;
|
using LehrerApp.Desktop.Services;
|
||||||
using LehrerApp.Desktop.ViewModels;
|
using LehrerApp.Desktop.ViewModels;
|
||||||
using LehrerApp.Desktop.ViewModels.Groups;
|
using LehrerApp.Desktop.ViewModels.Groups;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Planning;
|
||||||
using LehrerApp.Desktop.ViewModels.Settings;
|
using LehrerApp.Desktop.ViewModels.Settings;
|
||||||
using LehrerApp.Desktop.ViewModels.Students;
|
using LehrerApp.Desktop.ViewModels.Students;
|
||||||
using LehrerApp.Sync;
|
using LehrerApp.Sync;
|
||||||
@@ -130,10 +131,14 @@ public static class AppBootstrapper
|
|||||||
services.AddSingleton<IShorthandCodeRepository, ShorthandCodeRepository>();
|
services.AddSingleton<IShorthandCodeRepository, ShorthandCodeRepository>();
|
||||||
services.AddSingleton<IAlternativeLessonPathRepository, AlternativeLessonPathRepository>();
|
services.AddSingleton<IAlternativeLessonPathRepository, AlternativeLessonPathRepository>();
|
||||||
services.AddSingleton<IAttachmentStorage, LiteAttachmentStorage>();
|
services.AddSingleton<IAttachmentStorage, LiteAttachmentStorage>();
|
||||||
|
services.AddSingleton<ITimetableSlotRepository, TimetableSlotRepository>();
|
||||||
|
services.AddSingleton<ISchoolHolidayRepository, SchoolHolidayRepository>();
|
||||||
|
|
||||||
// ── Services ──────────────────────────────────────────────────────────
|
// ── Services ──────────────────────────────────────────────────────────
|
||||||
services.AddSingleton<GradingService>();
|
services.AddSingleton<GradingService>();
|
||||||
services.AddSingleton<SchoolYearService>();
|
services.AddSingleton<SchoolYearService>();
|
||||||
|
services.AddSingleton<PublicHolidayService>();
|
||||||
|
services.AddSingleton(_ => new SchoolCalendarSettingsService(appData));
|
||||||
|
|
||||||
// ── Sync (optional – nur wenn Server konfiguriert) ────────────────────
|
// ── Sync (optional – nur wenn Server konfiguriert) ────────────────────
|
||||||
services.AddSingleton(_ => new EventQueue(queuePath));
|
services.AddSingleton(_ => new EventQueue(queuePath));
|
||||||
@@ -178,6 +183,7 @@ public static class AppBootstrapper
|
|||||||
new SyncStatusViewModel(sp.GetService<SyncEngine>()));
|
new SyncStatusViewModel(sp.GetService<SyncEngine>()));
|
||||||
services.AddSingleton<GroupListViewModel>();
|
services.AddSingleton<GroupListViewModel>();
|
||||||
services.AddSingleton<StudentListViewModel>();
|
services.AddSingleton<StudentListViewModel>();
|
||||||
|
services.AddSingleton<TimetableViewModel>();
|
||||||
|
|
||||||
// Transient: neue Instanz pro Navigation (für Detailseiten)
|
// Transient: neue Instanz pro Navigation (für Detailseiten)
|
||||||
services.AddTransient<GroupDetailViewModel>();
|
services.AddTransient<GroupDetailViewModel>();
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using CommunityToolkit.Mvvm.Input;
|
|||||||
using LehrerApp.Core.Services;
|
using LehrerApp.Core.Services;
|
||||||
using LehrerApp.Desktop.Services;
|
using LehrerApp.Desktop.Services;
|
||||||
using LehrerApp.Desktop.ViewModels.Groups;
|
using LehrerApp.Desktop.ViewModels.Groups;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Planning;
|
||||||
using LehrerApp.Desktop.ViewModels.Settings;
|
using LehrerApp.Desktop.ViewModels.Settings;
|
||||||
using LehrerApp.Desktop.ViewModels.Students;
|
using LehrerApp.Desktop.ViewModels.Students;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
@@ -64,7 +65,7 @@ public partial class MainWindowViewModel : ObservableObject
|
|||||||
NavItem.Groups => _services.GetRequiredService<GroupListViewModel>(),
|
NavItem.Groups => _services.GetRequiredService<GroupListViewModel>(),
|
||||||
NavItem.Students => _services.GetRequiredService<StudentListViewModel>(),
|
NavItem.Students => _services.GetRequiredService<StudentListViewModel>(),
|
||||||
NavItem.Exams => new PlaceholderViewModel { Title = "Klausuren", Icon = "📝" },
|
NavItem.Exams => new PlaceholderViewModel { Title = "Klausuren", Icon = "📝" },
|
||||||
NavItem.Planner => new PlaceholderViewModel { Title = "Unterrichtsplanung", Icon = "📅" },
|
NavItem.Planner => GetTimetable(),
|
||||||
NavItem.Workload => new PlaceholderViewModel { Title = "Arbeitszeit", Icon = "⏱" },
|
NavItem.Workload => new PlaceholderViewModel { Title = "Arbeitszeit", Icon = "⏱" },
|
||||||
NavItem.Settings => _services.GetRequiredService<SettingsViewModel>(),
|
NavItem.Settings => _services.GetRequiredService<SettingsViewModel>(),
|
||||||
_ => CurrentPage,
|
_ => CurrentPage,
|
||||||
@@ -78,6 +79,15 @@ public partial class MainWindowViewModel : ObservableObject
|
|||||||
return dashboard;
|
return dashboard;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private TimetableViewModel GetTimetable()
|
||||||
|
{
|
||||||
|
var timetable = _services.GetRequiredService<TimetableViewModel>();
|
||||||
|
timetable.WeekOffset = 0;
|
||||||
|
timetable.Load();
|
||||||
|
timetable.ActiveTabIndex = 0;
|
||||||
|
return timetable;
|
||||||
|
}
|
||||||
|
|
||||||
public void NavigateToGroupDetail(Guid groupId, int initialTab = 0)
|
public void NavigateToGroupDetail(Guid groupId, int initialTab = 0)
|
||||||
{
|
{
|
||||||
ActiveNavItem = NavItem.Groups;
|
ActiveNavItem = NavItem.Groups;
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using LehrerApp.Core.Interfaces;
|
||||||
|
using LehrerApp.Core.Models;
|
||||||
|
using LehrerApp.Core.Services;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.ViewModels.Planning;
|
||||||
|
|
||||||
|
/// <summary>Zuweisen/Bearbeiten/Entfernen eines Stundenplan-Termins (4.3.3).</summary>
|
||||||
|
public partial class TimetableSlotDialogViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
private readonly ITimetableSlotRepository _slots;
|
||||||
|
private readonly Dictionary<string, Guid> _groupIdsByName;
|
||||||
|
private readonly TimetableSlot? _editing;
|
||||||
|
|
||||||
|
public DayOfWeek Weekday { get; }
|
||||||
|
public int PeriodNumber { get; }
|
||||||
|
public string WeekdayLabel { get; }
|
||||||
|
public string DialogTitle { get; }
|
||||||
|
public bool IsEditing => _editing is not null;
|
||||||
|
|
||||||
|
[ObservableProperty] private string _selectedGroupName = "";
|
||||||
|
[ObservableProperty] private string _room = "";
|
||||||
|
[ObservableProperty] private string _groupError = "";
|
||||||
|
|
||||||
|
public string[] GroupOptions { get; }
|
||||||
|
|
||||||
|
/// null = unverändert/Abbruch, sonst das neue/aktualisierte Ergebnis.
|
||||||
|
public TimetableSlot? Result { get; private set; }
|
||||||
|
public bool Deleted { get; private set; }
|
||||||
|
|
||||||
|
public TimetableSlotDialogViewModel(ITimetableSlotRepository slots, IGroupRepository groups,
|
||||||
|
SchoolYearService schoolYear, DayOfWeek weekday, int periodNumber, TimetableSlot? editing)
|
||||||
|
{
|
||||||
|
_slots = slots;
|
||||||
|
_editing = editing;
|
||||||
|
Weekday = weekday;
|
||||||
|
PeriodNumber = periodNumber;
|
||||||
|
WeekdayLabel = weekday switch
|
||||||
|
{
|
||||||
|
DayOfWeek.Monday => "Montag", DayOfWeek.Tuesday => "Dienstag",
|
||||||
|
DayOfWeek.Wednesday => "Mittwoch", DayOfWeek.Thursday => "Donnerstag",
|
||||||
|
DayOfWeek.Friday => "Freitag", _ => weekday.ToString(),
|
||||||
|
};
|
||||||
|
DialogTitle = $"{WeekdayLabel}, {periodNumber}. Stunde";
|
||||||
|
|
||||||
|
var availableGroups = groups.GetBySchoolYear(schoolYear.CurrentSchoolYear()).OrderBy(g => g.Name).ToList();
|
||||||
|
_groupIdsByName = availableGroups.ToDictionary(g => g.Name, g => g.Id);
|
||||||
|
GroupOptions = availableGroups.Select(g => g.Name).ToArray();
|
||||||
|
|
||||||
|
if (editing is not null)
|
||||||
|
{
|
||||||
|
SelectedGroupName = availableGroups.FirstOrDefault(g => g.Id == editing.GroupId)?.Name ?? "";
|
||||||
|
Room = editing.Room ?? "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void Save()
|
||||||
|
{
|
||||||
|
GroupError = "";
|
||||||
|
if (string.IsNullOrWhiteSpace(SelectedGroupName) || !_groupIdsByName.TryGetValue(SelectedGroupName, out var groupId))
|
||||||
|
{
|
||||||
|
GroupError = "Bitte eine Gruppe auswählen.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var slot = _editing ?? new TimetableSlot { Weekday = Weekday, PeriodNumber = PeriodNumber };
|
||||||
|
slot.GroupId = groupId;
|
||||||
|
slot.Room = string.IsNullOrWhiteSpace(Room) ? null : Room.Trim();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_slots.Save(slot);
|
||||||
|
Result = slot;
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
GroupError = ex.Message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void Delete()
|
||||||
|
{
|
||||||
|
if (_editing is null) return;
|
||||||
|
_slots.Delete(_editing.Id);
|
||||||
|
Deleted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,456 @@
|
|||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using LehrerApp.Core.Interfaces;
|
||||||
|
using LehrerApp.Core.Models;
|
||||||
|
using LehrerApp.Core.Services;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.ViewModels.Planning;
|
||||||
|
|
||||||
|
// ── Bundesland: deutsche Anzeige (4.3.5) ─────────────────────────────────────
|
||||||
|
// Wird sowohl vom Stundenplan (Anzeige) als auch von den Einstellungen (Pflege) verwendet.
|
||||||
|
|
||||||
|
public static class GermanStateDisplay
|
||||||
|
{
|
||||||
|
private static readonly (GermanState State, string Name)[] Entries =
|
||||||
|
[
|
||||||
|
(GermanState.BW, "Baden-Württemberg"),
|
||||||
|
(GermanState.BY, "Bayern"),
|
||||||
|
(GermanState.BE, "Berlin"),
|
||||||
|
(GermanState.BB, "Brandenburg"),
|
||||||
|
(GermanState.HB, "Bremen"),
|
||||||
|
(GermanState.HH, "Hamburg"),
|
||||||
|
(GermanState.HE, "Hessen"),
|
||||||
|
(GermanState.MV, "Mecklenburg-Vorpommern"),
|
||||||
|
(GermanState.NI, "Niedersachsen"),
|
||||||
|
(GermanState.NW, "Nordrhein-Westfalen"),
|
||||||
|
(GermanState.RP, "Rheinland-Pfalz"),
|
||||||
|
(GermanState.SL, "Saarland"),
|
||||||
|
(GermanState.SN, "Sachsen"),
|
||||||
|
(GermanState.ST, "Sachsen-Anhalt"),
|
||||||
|
(GermanState.SH, "Schleswig-Holstein"),
|
||||||
|
(GermanState.TH, "Thüringen"),
|
||||||
|
];
|
||||||
|
|
||||||
|
public static string[] Options { get; } = Entries.Select(e => e.Name).ToArray();
|
||||||
|
public static string Label(GermanState s) => Entries.First(e => e.State == s).Name;
|
||||||
|
public static GermanState FromLabel(string label) =>
|
||||||
|
Entries.FirstOrDefault(e => e.Name == label).State;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Stundenplan: "Heute"-Übersicht (Standardansicht, Wochenraster + Tagesliste) + Bearbeiten (4.3) ──
|
||||||
|
|
||||||
|
public partial class TimetableViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
private const int FirstPeriod = 1;
|
||||||
|
private const int LastPeriod = 10;
|
||||||
|
private static readonly DayOfWeek[] Weekdays =
|
||||||
|
[DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday];
|
||||||
|
private static readonly string[] WeekdayColorPalette =
|
||||||
|
["#7F77DD", "#1D9E75", "#D85A30", "#D4537E", "#378ADD", "#639922", "#EF9F27", "#4C86A8"];
|
||||||
|
|
||||||
|
private readonly ITimetableSlotRepository _slots;
|
||||||
|
private readonly IGroupRepository _groups;
|
||||||
|
private readonly ISubjectRepository _subjects;
|
||||||
|
private readonly ILessonRepository _lessons;
|
||||||
|
private readonly IExamRepository _exams;
|
||||||
|
private readonly ISchoolHolidayRepository _schoolHolidays;
|
||||||
|
private readonly SchoolCalendarSettingsService _calendarSettings;
|
||||||
|
private readonly PublicHolidayService _publicHolidays;
|
||||||
|
private readonly SchoolYearService _schoolYear;
|
||||||
|
|
||||||
|
public ObservableCollection<TimetableCellItem> Cells { get; } = [];
|
||||||
|
public ObservableCollection<WeekCellItem> WeekItems { get; } = [];
|
||||||
|
public ObservableCollection<HoursWarningItem> HoursWarnings { get; } = [];
|
||||||
|
public ObservableCollection<TodayLessonItem> TodayItems { get; } = [];
|
||||||
|
|
||||||
|
[ObservableProperty] private int _activeTabIndex;
|
||||||
|
[ObservableProperty] private string _todayLabel = "";
|
||||||
|
[ObservableProperty] private string _weekRangeLabel = "";
|
||||||
|
[ObservableProperty] private int _weekOffset;
|
||||||
|
public bool IsCurrentWeek => WeekOffset == 0;
|
||||||
|
|
||||||
|
public Func<TimetableCellItem, Task>? OnEditSlot { get; set; }
|
||||||
|
public Action<Guid>? OnNavigateToGroup { get; set; }
|
||||||
|
|
||||||
|
public TimetableViewModel(ITimetableSlotRepository slots, IGroupRepository groups,
|
||||||
|
ISubjectRepository subjects, ILessonRepository lessons, IExamRepository exams,
|
||||||
|
ISchoolHolidayRepository schoolHolidays, SchoolCalendarSettingsService calendarSettings,
|
||||||
|
PublicHolidayService publicHolidays, SchoolYearService schoolYear)
|
||||||
|
{
|
||||||
|
_slots = slots; _groups = groups; _subjects = subjects; _lessons = lessons; _exams = exams;
|
||||||
|
_schoolHolidays = schoolHolidays; _calendarSettings = calendarSettings;
|
||||||
|
_publicHolidays = publicHolidays; _schoolYear = schoolYear;
|
||||||
|
Load();
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnWeekOffsetChanged(int value) => OnPropertyChanged(nameof(IsCurrentWeek));
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void PreviousWeek() { WeekOffset--; Load(); }
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void NextWeek() { WeekOffset++; Load(); }
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void CurrentWeek() { WeekOffset = 0; Load(); }
|
||||||
|
|
||||||
|
public void Load()
|
||||||
|
{
|
||||||
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
TodayLabel = today.ToString("dddd, dd.MM.yyyy", System.Globalization.CultureInfo.GetCultureInfo("de-DE"));
|
||||||
|
|
||||||
|
var publicHolidayDates = new HashSet<DateOnly>();
|
||||||
|
foreach (var year in new[] { today.Year - 1, today.Year, today.Year + 1 })
|
||||||
|
foreach (var h in _publicHolidays.GetHolidays(year, _calendarSettings.State)) publicHolidayDates.Add(h.Date);
|
||||||
|
|
||||||
|
var holidayBadges = ComputeHolidayBadges(today, publicHolidayDates);
|
||||||
|
var examProximity = ComputeExamProximity(today, publicHolidayDates);
|
||||||
|
|
||||||
|
BuildGrid(holidayBadges);
|
||||||
|
BuildWeekOverview(today, publicHolidayDates);
|
||||||
|
BuildToday(today);
|
||||||
|
BuildHoursWarnings();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── "Heute": Tagesliste ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void BuildToday(DateOnly today)
|
||||||
|
{
|
||||||
|
TodayItems.Clear();
|
||||||
|
var slotsToday = _slots.GetAll().Where(s => s.Weekday == today.DayOfWeek).OrderBy(s => s.PeriodNumber).ToList();
|
||||||
|
|
||||||
|
foreach (var slot in slotsToday)
|
||||||
|
{
|
||||||
|
var group = _groups.GetById(slot.GroupId);
|
||||||
|
if (group is null) continue;
|
||||||
|
var lesson = _lessons.GetByGroupAndDate(slot.GroupId, today).FirstOrDefault();
|
||||||
|
var exam = _exams.GetByGroup(slot.GroupId).FirstOrDefault(e => e.Date == today);
|
||||||
|
TodayItems.Add(new TodayLessonItem(slot.GroupId, slot.PeriodNumber, group.Name,
|
||||||
|
slot.Room ?? "", ColorFor(group.Name), lesson?.Topic, exam?.Title));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void OpenGroup(Guid groupId) => OnNavigateToGroup?.Invoke(groupId);
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void ShowEditor() => ActiveTabIndex = 1;
|
||||||
|
|
||||||
|
// ── "Heute": Wochenraster (Nutzer-Feedback) — wie das Bearbeiten-Raster, aber nur Anzeige ──
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Zeigt Fach, Klasse, Raum und (falls für den Tag hinterlegt) das Thema der Stunde für die
|
||||||
|
/// per <see cref="WeekOffset"/> gewählte Kalenderwoche (Mo–Fr, wie das Bearbeiten-Raster) —
|
||||||
|
/// anders als die Tagesliste auch für Tage, die noch nicht "heute" sind, damit z.B. der
|
||||||
|
/// parallele Kurs oder die nächste Stunde in der Woche auf einen Blick sichtbar sind. Die
|
||||||
|
/// Badges (Ferien-Nähe, Klausur-Nähe) werden je Zelle am dort angezeigten Datum ausgerichtet,
|
||||||
|
/// nicht am realen "heute" — sonst würde beim Blättern in andere Wochen ein Badge angezeigt,
|
||||||
|
/// das eigentlich zu einer ganz anderen Woche gehört.
|
||||||
|
/// </summary>
|
||||||
|
private void BuildWeekOverview(DateOnly today, HashSet<DateOnly> publicHolidayDates)
|
||||||
|
{
|
||||||
|
WeekItems.Clear();
|
||||||
|
var currentWeekMonday = today.AddDays(-((int)today.DayOfWeek + 6) % 7);
|
||||||
|
var monday = currentWeekMonday.AddDays(WeekOffset * 7);
|
||||||
|
var friday = monday.AddDays(4);
|
||||||
|
WeekRangeLabel = $"{monday:dd.MM.} – {friday:dd.MM.yyyy}";
|
||||||
|
|
||||||
|
var allSlots = _slots.GetAll();
|
||||||
|
var groups = _groups.GetAll(includeInactive: true).ToDictionary(g => g.Id);
|
||||||
|
var schoolHolidays = _schoolHolidays.GetAll();
|
||||||
|
var dateByWeekday = Weekdays.ToDictionary(w => w, w => monday.AddDays((int)w - (int)DayOfWeek.Monday));
|
||||||
|
|
||||||
|
WeekItems.Add(WeekCellItem.Corner());
|
||||||
|
foreach (var weekday in Weekdays)
|
||||||
|
WeekItems.Add(WeekCellItem.WeekdayHeader(weekday, dateByWeekday[weekday], dateByWeekday[weekday] == today));
|
||||||
|
|
||||||
|
for (var period = FirstPeriod; period <= LastPeriod; period++)
|
||||||
|
{
|
||||||
|
WeekItems.Add(WeekCellItem.PeriodLabel(period));
|
||||||
|
foreach (var weekday in Weekdays)
|
||||||
|
{
|
||||||
|
var slot = allSlots.FirstOrDefault(s => s.Weekday == weekday && s.PeriodNumber == period);
|
||||||
|
if (slot is null) { WeekItems.Add(WeekCellItem.Empty(weekday, period)); continue; }
|
||||||
|
|
||||||
|
var date = dateByWeekday[weekday];
|
||||||
|
var group = groups.GetValueOrDefault(slot.GroupId);
|
||||||
|
var subject = group?.SubjectId is { } subjectId ? _subjects.GetById(subjectId) : null;
|
||||||
|
var lesson = _lessons.GetByGroupAndDate(slot.GroupId, date).FirstOrDefault();
|
||||||
|
var hasExam = _exams.GetByGroup(slot.GroupId).Any(e => e.Date == date);
|
||||||
|
var isHoliday = IsFreeDay(date, schoolHolidays, publicHolidayDates);
|
||||||
|
var colorHex = isHoliday ? "#BDBDBD" : ColorFor(group?.Name ?? "");
|
||||||
|
var holidayBadge = HolidayBadgeFor(date, weekday, schoolHolidays, publicHolidayDates);
|
||||||
|
var isLastBeforeExam = IsLastBeforeExamFor(date, weekday, slot.GroupId, publicHolidayDates);
|
||||||
|
|
||||||
|
WeekItems.Add(WeekCellItem.ForSlot(weekday, period, date == today,
|
||||||
|
subject?.ShortName is { Length: > 0 } sn ? sn : subject?.Name ?? "",
|
||||||
|
group?.Name ?? "?", slot.Room ?? "", lesson?.Topic ?? "",
|
||||||
|
colorHex, holidayBadge, hasExam, isLastBeforeExam,
|
||||||
|
MentionsExperiment(lesson), slot.GroupId, isHoliday));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Badge "1"/"2" für die Zelle mit Datum <paramref name="date"/> selbst, analog zu
|
||||||
|
/// <see cref="ComputeHolidayBadges"/>, aber an diesem konkreten Datum statt an "heute"
|
||||||
|
/// ausgerichtet — nötig, damit das Badge beim Blättern durch die Wochen zur richtigen Woche
|
||||||
|
/// gehört.</summary>
|
||||||
|
private string HolidayBadgeFor(DateOnly date, DayOfWeek weekday, List<SchoolHoliday> schoolHolidays,
|
||||||
|
HashSet<DateOnly> publicHolidayDates)
|
||||||
|
{
|
||||||
|
var nextHoliday = schoolHolidays.Where(h => h.StartDate > date).MinBy(h => h.StartDate);
|
||||||
|
if (nextHoliday is null) return "";
|
||||||
|
var count = CountOccurrences(weekday, date, nextHoliday.StartDate, publicHolidayDates);
|
||||||
|
return count is 1 or 2 ? count.ToString() : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Analog zu <see cref="ComputeExamProximity"/>, aber am Zelldatum statt an "heute"
|
||||||
|
/// ausgerichtet.</summary>
|
||||||
|
private bool IsLastBeforeExamFor(DateOnly date, DayOfWeek weekday, Guid groupId, HashSet<DateOnly> publicHolidayDates)
|
||||||
|
{
|
||||||
|
var nextExam = _exams.GetByGroup(groupId).Where(e => e.Date >= date).MinBy(e => e.Date);
|
||||||
|
if (nextExam is null) return false;
|
||||||
|
return CountOccurrences(weekday, date, nextExam.Date, publicHolidayDates) == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool MentionsExperiment(Lesson? lesson) =>
|
||||||
|
lesson is not null && lesson.Phases.Any(p =>
|
||||||
|
p.Name.Contains("Experiment", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
p.Activity.Contains("Experiment", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
p.Material.Contains("Experiment", StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
/// <summary>Fällt <paramref name="date"/> auf einen gesetzlichen Feiertag oder in Schulferien?</summary>
|
||||||
|
private static bool IsFreeDay(DateOnly date, List<SchoolHoliday> schoolHolidays, HashSet<DateOnly> publicHolidayDates) =>
|
||||||
|
publicHolidayDates.Contains(date) || schoolHolidays.Any(h => date >= h.StartDate && date <= h.EndDate);
|
||||||
|
|
||||||
|
// ── Bearbeiten-Raster ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void BuildGrid(Dictionary<(DayOfWeek Weekday, Guid GroupId), string> badges)
|
||||||
|
{
|
||||||
|
Cells.Clear();
|
||||||
|
var allSlots = _slots.GetAll();
|
||||||
|
var groupNames = _groups.GetAll(includeInactive: true).ToDictionary(g => g.Id, g => g.Name);
|
||||||
|
|
||||||
|
Cells.Add(TimetableCellItem.Corner());
|
||||||
|
foreach (var weekday in Weekdays)
|
||||||
|
Cells.Add(TimetableCellItem.WeekdayHeader(weekday));
|
||||||
|
|
||||||
|
for (var period = FirstPeriod; period <= LastPeriod; period++)
|
||||||
|
{
|
||||||
|
Cells.Add(TimetableCellItem.PeriodLabel(period));
|
||||||
|
foreach (var weekday in Weekdays)
|
||||||
|
{
|
||||||
|
var slot = allSlots.FirstOrDefault(s => s.Weekday == weekday && s.PeriodNumber == period);
|
||||||
|
var groupName = slot is not null ? groupNames.GetValueOrDefault(slot.GroupId, "?") : "";
|
||||||
|
var badge = slot is not null ? badges.GetValueOrDefault((weekday, slot.GroupId), "") : "";
|
||||||
|
Cells.Add(TimetableCellItem.ForSlot(weekday, period, slot, groupName, ColorFor(groupName), badge));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ColorFor(string name)
|
||||||
|
{
|
||||||
|
if (name.Length == 0) return "#9E9E9E";
|
||||||
|
var hash = 0;
|
||||||
|
foreach (var c in name) hash = hash * 31 + c;
|
||||||
|
return WeekdayColorPalette[Math.Abs(hash) % WeekdayColorPalette.Length];
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task EditCell(TimetableCellItem? cell)
|
||||||
|
{
|
||||||
|
if (cell is null || !cell.IsSlotCell || OnEditSlot is null) return;
|
||||||
|
await OnEditSlot(cell);
|
||||||
|
Load();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Abgleich mit Wochenstunden (4.3.4) ────────────────────────────────────
|
||||||
|
|
||||||
|
private void BuildHoursWarnings()
|
||||||
|
{
|
||||||
|
HoursWarnings.Clear();
|
||||||
|
var currentYear = _schoolYear.CurrentSchoolYear();
|
||||||
|
var slotCounts = _slots.GetAll().GroupBy(s => s.GroupId).ToDictionary(g => g.Key, g => g.Count());
|
||||||
|
|
||||||
|
foreach (var group in _groups.GetBySchoolYear(currentYear).Where(g => g.HoursPerWeek.HasValue))
|
||||||
|
{
|
||||||
|
var assigned = slotCounts.GetValueOrDefault(group.Id, 0);
|
||||||
|
if (assigned != group.HoursPerWeek!.Value)
|
||||||
|
HoursWarnings.Add(new HoursWarningItem(group.Name, assigned, group.HoursPerWeek.Value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Badges: letzte/vorletzte Stunde vor Ferien, letzte Stunde vor Klausur ────────────────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Zählt, wie oft <paramref name="weekday"/> zwischen (inkl.) <paramref name="from"/> und
|
||||||
|
/// (exkl.) <paramref name="until"/> eintritt — gesetzliche Feiertage werden übersprungen, da
|
||||||
|
/// an ihnen ohnehin kein Unterricht stattfindet.
|
||||||
|
/// </summary>
|
||||||
|
private static int CountOccurrences(DayOfWeek weekday, DateOnly from, DateOnly until, HashSet<DateOnly> publicHolidays)
|
||||||
|
{
|
||||||
|
var count = 0;
|
||||||
|
for (var date = from; date < until; date = date.AddDays(1))
|
||||||
|
if (date.DayOfWeek == weekday && !publicHolidays.Contains(date)) count++;
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Badge "1"/"2" für die letzte bzw. vorletzte Stunde eines Wochentags vor den nächsten
|
||||||
|
/// anstehenden Schulferien. Pro (Wochentag, Gruppe) statt pro einzelnem <see cref="TimetableSlot"/>
|
||||||
|
/// berechnet: eine Doppelstunde besteht aus zwei Slots mit demselben Wochentag/derselben
|
||||||
|
/// Gruppe und bekommt dadurch automatisch dasselbe Badge, ohne gesonderte Blockerkennung.
|
||||||
|
/// </summary>
|
||||||
|
private Dictionary<(DayOfWeek Weekday, Guid GroupId), string> ComputeHolidayBadges(
|
||||||
|
DateOnly today, HashSet<DateOnly> publicHolidayDates)
|
||||||
|
{
|
||||||
|
var result = new Dictionary<(DayOfWeek, Guid), string>();
|
||||||
|
var nextHoliday = _schoolHolidays.GetAll().Where(h => h.StartDate > today).MinBy(h => h.StartDate);
|
||||||
|
if (nextHoliday is null) return result;
|
||||||
|
|
||||||
|
foreach (var group in _slots.GetAll().GroupBy(s => (s.Weekday, s.GroupId)))
|
||||||
|
{
|
||||||
|
var count = CountOccurrences(group.Key.Weekday, today, nextHoliday.StartDate, publicHolidayDates);
|
||||||
|
if (count is 1 or 2) result[group.Key] = count.ToString();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Markiert die letzte Stunde eines Wochentags vor der nächsten anstehenden Klausur derselben
|
||||||
|
/// Gruppe (sofern eine ansteht) — analog zum Ferien-Badge, aber je Gruppe an deren eigenem
|
||||||
|
/// nächsten Klausurtermin statt an einem gemeinsamen Ferientermin ausgerichtet.
|
||||||
|
/// </summary>
|
||||||
|
private Dictionary<(DayOfWeek Weekday, Guid GroupId), bool> ComputeExamProximity(
|
||||||
|
DateOnly today, HashSet<DateOnly> publicHolidayDates)
|
||||||
|
{
|
||||||
|
var result = new Dictionary<(DayOfWeek, Guid), bool>();
|
||||||
|
foreach (var groupSlots in _slots.GetAll().GroupBy(s => s.GroupId))
|
||||||
|
{
|
||||||
|
var nextExam = _exams.GetByGroup(groupSlots.Key).Where(e => e.Date >= today).MinBy(e => e.Date);
|
||||||
|
if (nextExam is null) continue;
|
||||||
|
|
||||||
|
foreach (var weekday in groupSlots.Select(s => s.Weekday).Distinct())
|
||||||
|
{
|
||||||
|
var count = CountOccurrences(weekday, today, nextExam.Date, publicHolidayDates);
|
||||||
|
if (count == 1) result[(weekday, groupSlots.Key)] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TimetableCellItem
|
||||||
|
{
|
||||||
|
public bool IsHeader { get; private init; }
|
||||||
|
public bool IsPeriodLabel { get; private init; }
|
||||||
|
public string Text { get; private init; } = "";
|
||||||
|
public DayOfWeek? Weekday { get; private init; }
|
||||||
|
public int PeriodNumber { get; private init; }
|
||||||
|
public TimetableSlot? Slot { get; private init; }
|
||||||
|
public string GroupName { get; private init; } = "";
|
||||||
|
public string ColorHex { get; private init; } = "#9E9E9E";
|
||||||
|
public string BadgeText { get; private init; } = "";
|
||||||
|
public bool HasBadge => BadgeText.Length > 0;
|
||||||
|
public bool IsAssigned => Slot is not null;
|
||||||
|
public bool IsSlotCell => !IsHeader && !IsPeriodLabel;
|
||||||
|
|
||||||
|
public static TimetableCellItem Corner() => new() { IsHeader = true, Text = "" };
|
||||||
|
|
||||||
|
public static TimetableCellItem WeekdayHeader(DayOfWeek day) => new()
|
||||||
|
{
|
||||||
|
IsHeader = true,
|
||||||
|
Text = day.ToString() switch
|
||||||
|
{
|
||||||
|
"Monday" => "Mo", "Tuesday" => "Di", "Wednesday" => "Mi",
|
||||||
|
"Thursday" => "Do", "Friday" => "Fr", _ => day.ToString(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
public static TimetableCellItem PeriodLabel(int period) => new() { IsPeriodLabel = true, Text = period.ToString() };
|
||||||
|
|
||||||
|
public static TimetableCellItem ForSlot(DayOfWeek day, int period, TimetableSlot? slot, string groupName,
|
||||||
|
string colorHex, string badgeText) => new()
|
||||||
|
{
|
||||||
|
Weekday = day, PeriodNumber = period, Slot = slot, GroupName = groupName, ColorHex = colorHex,
|
||||||
|
BadgeText = badgeText,
|
||||||
|
Text = slot is null ? "" : groupName + (string.IsNullOrWhiteSpace(slot.Room) ? "" : $" · {slot.Room}"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Zelle im schreibgeschützten Wochenraster der "Heute"-Ansicht.</summary>
|
||||||
|
public class WeekCellItem
|
||||||
|
{
|
||||||
|
public bool IsHeader { get; private init; }
|
||||||
|
public bool IsPeriodLabel { get; private init; }
|
||||||
|
public bool IsSlotCell => !IsHeader && !IsPeriodLabel;
|
||||||
|
public bool IsAssigned { get; private init; }
|
||||||
|
public string Text { get; private init; } = "";
|
||||||
|
public DayOfWeek? Weekday { get; private init; }
|
||||||
|
public int PeriodNumber { get; private init; }
|
||||||
|
public bool IsToday { get; private init; }
|
||||||
|
public Guid GroupId { get; private init; }
|
||||||
|
public string SubjectLabel { get; private init; } = "";
|
||||||
|
public string GroupName { get; private init; } = "";
|
||||||
|
public string Room { get; private init; } = "";
|
||||||
|
public string Topic { get; private init; } = "";
|
||||||
|
public string ColorHex { get; private init; } = "#9E9E9E";
|
||||||
|
public string HolidayBadge { get; private init; } = "";
|
||||||
|
public bool HasHolidayBadge => HolidayBadge.Length > 0;
|
||||||
|
public bool HasExam { get; private init; }
|
||||||
|
public bool IsLastBeforeExam { get; private init; }
|
||||||
|
public bool HasExperiment { get; private init; }
|
||||||
|
public bool IsHoliday { get; private init; }
|
||||||
|
public bool HasRoom => !string.IsNullOrWhiteSpace(Room);
|
||||||
|
public bool HasTopic => !string.IsNullOrWhiteSpace(Topic);
|
||||||
|
|
||||||
|
public static WeekCellItem Corner() => new() { IsHeader = true };
|
||||||
|
|
||||||
|
public static WeekCellItem WeekdayHeader(DayOfWeek day, DateOnly date, bool isToday) => new()
|
||||||
|
{
|
||||||
|
IsHeader = true,
|
||||||
|
IsToday = isToday,
|
||||||
|
Text = (day switch
|
||||||
|
{
|
||||||
|
DayOfWeek.Monday => "Mo", DayOfWeek.Tuesday => "Di", DayOfWeek.Wednesday => "Mi",
|
||||||
|
DayOfWeek.Thursday => "Do", DayOfWeek.Friday => "Fr", _ => day.ToString(),
|
||||||
|
}) + $" {date:dd.MM.}",
|
||||||
|
};
|
||||||
|
|
||||||
|
public static WeekCellItem PeriodLabel(int period) => new() { IsPeriodLabel = true, Text = period.ToString() };
|
||||||
|
|
||||||
|
public static WeekCellItem Empty(DayOfWeek day, int period) => new() { Weekday = day, PeriodNumber = period };
|
||||||
|
|
||||||
|
public static WeekCellItem ForSlot(DayOfWeek day, int period, bool isToday, string subjectLabel,
|
||||||
|
string groupName, string room, string topic, string colorHex, string holidayBadge,
|
||||||
|
bool hasExam, bool isLastBeforeExam, bool hasExperiment, Guid groupId, bool isHoliday) => new()
|
||||||
|
{
|
||||||
|
Weekday = day, PeriodNumber = period, IsAssigned = true, IsToday = isToday,
|
||||||
|
SubjectLabel = subjectLabel, GroupName = groupName, Room = room, Topic = topic,
|
||||||
|
ColorHex = colorHex, HolidayBadge = holidayBadge, HasExam = hasExam,
|
||||||
|
IsLastBeforeExam = isLastBeforeExam, HasExperiment = hasExperiment, GroupId = groupId,
|
||||||
|
IsHoliday = isHoliday,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public class HoursWarningItem(string groupName, int assigned, int expected)
|
||||||
|
{
|
||||||
|
public string GroupName { get; } = groupName;
|
||||||
|
public string Text { get; } = $"{groupName}: {assigned} von {expected} Wochenstunden eingetragen";
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TodayLessonItem(Guid groupId, int periodNumber, string groupName, string room,
|
||||||
|
string colorHex, string? lessonTopic, string? examTitle)
|
||||||
|
{
|
||||||
|
public Guid GroupId { get; } = groupId;
|
||||||
|
public int PeriodNumber { get; } = periodNumber;
|
||||||
|
public string GroupName { get; } = groupName;
|
||||||
|
public string Room { get; } = room;
|
||||||
|
public string ColorHex { get; } = colorHex;
|
||||||
|
public string? LessonTopic { get; } = lessonTopic;
|
||||||
|
public string? ExamTitle { get; } = examTitle;
|
||||||
|
public bool HasRoom => !string.IsNullOrWhiteSpace(Room);
|
||||||
|
public bool HasLessonTopic => !string.IsNullOrWhiteSpace(LessonTopic);
|
||||||
|
public bool HasExam => ExamTitle is not null;
|
||||||
|
}
|
||||||
@@ -4,7 +4,9 @@ using LehrerApp.Core.Interfaces;
|
|||||||
using LehrerApp.Core.Models;
|
using LehrerApp.Core.Models;
|
||||||
using LehrerApp.Core.Services;
|
using LehrerApp.Core.Services;
|
||||||
using LehrerApp.Data;
|
using LehrerApp.Data;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Planning;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Globalization;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
@@ -113,14 +115,30 @@ public partial class SettingsViewModel : ObservableObject
|
|||||||
/// Vom Code-Behind gesetzt: zeigt einen Bestätigungsdialog vor dem endgültigen Löschen.
|
/// Vom Code-Behind gesetzt: zeigt einen Bestätigungsdialog vor dem endgültigen Löschen.
|
||||||
public Func<ExpiredDocumentItem, Task<bool>>? OnConfirmHardDelete { get; set; }
|
public Func<ExpiredDocumentItem, Task<bool>>? OnConfirmHardDelete { get; set; }
|
||||||
|
|
||||||
|
// ── Ferien & Feiertage (4.3.5, aus dem Stundenplan hierher verschoben) ───
|
||||||
|
|
||||||
|
[ObservableProperty] private string _selectedStateName = "";
|
||||||
|
[ObservableProperty] private string _newHolidayName = "";
|
||||||
|
[ObservableProperty] private string _newHolidayStartText = "";
|
||||||
|
[ObservableProperty] private string _newHolidayEndText = "";
|
||||||
|
[ObservableProperty] private string _holidayNameError = "";
|
||||||
|
[ObservableProperty] private string _holidayDateError = "";
|
||||||
|
|
||||||
|
public List<string> StateOptions { get; } = GermanStateDisplay.Options.ToList();
|
||||||
|
public ObservableCollection<SchoolHolidayItem> SchoolHolidayEntries { get; } = [];
|
||||||
|
|
||||||
// ── Konstruktor ───────────────────────────────────────────────────────────
|
// ── Konstruktor ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private readonly ISchoolHolidayRepository _schoolHolidays;
|
||||||
|
private readonly SchoolCalendarSettingsService _calendarSettings;
|
||||||
|
|
||||||
public SettingsViewModel(ISubjectRepository subjects, ICompetencyDomainRepository domainRepo,
|
public SettingsViewModel(ISubjectRepository subjects, ICompetencyDomainRepository domainRepo,
|
||||||
IGradingKeyTemplateRepository gradingKeyTemplates, IGradingSchemeRepository gradingSchemes,
|
IGradingKeyTemplateRepository gradingKeyTemplates, IGradingSchemeRepository gradingSchemes,
|
||||||
GradingService grading, BackupService backups, DatabaseEncryptionService dbEncryption,
|
GradingService grading, BackupService backups, DatabaseEncryptionService dbEncryption,
|
||||||
AppLockService appLock, LiteDbContext dbContext, PrivacySettingsService privacy,
|
AppLockService appLock, LiteDbContext dbContext, PrivacySettingsService privacy,
|
||||||
IDocumentationRepository documentation, IStudentRepository students,
|
IDocumentationRepository documentation, IStudentRepository students,
|
||||||
IShorthandCodeRepository shorthandCodes)
|
IShorthandCodeRepository shorthandCodes, ISchoolHolidayRepository schoolHolidays,
|
||||||
|
SchoolCalendarSettingsService calendarSettings)
|
||||||
{
|
{
|
||||||
_subjects = subjects;
|
_subjects = subjects;
|
||||||
_domainRepo = domainRepo;
|
_domainRepo = domainRepo;
|
||||||
@@ -135,6 +153,8 @@ public partial class SettingsViewModel : ObservableObject
|
|||||||
_documentation = documentation;
|
_documentation = documentation;
|
||||||
_students = students;
|
_students = students;
|
||||||
_shorthandCodes = shorthandCodes;
|
_shorthandCodes = shorthandCodes;
|
||||||
|
_schoolHolidays = schoolHolidays;
|
||||||
|
_calendarSettings = calendarSettings;
|
||||||
LoadSubjects();
|
LoadSubjects();
|
||||||
LoadShorthandCodes();
|
LoadShorthandCodes();
|
||||||
LoadGradingKeyTemplates();
|
LoadGradingKeyTemplates();
|
||||||
@@ -145,6 +165,51 @@ public partial class SettingsViewModel : ObservableObject
|
|||||||
AppLockTimeoutMinutes = _appLock.TimeoutMinutes;
|
AppLockTimeoutMinutes = _appLock.TimeoutMinutes;
|
||||||
RetentionYears = _privacy.RetentionYears;
|
RetentionYears = _privacy.RetentionYears;
|
||||||
LoadExpiredDocuments();
|
LoadExpiredDocuments();
|
||||||
|
SelectedStateName = GermanStateDisplay.Label(_calendarSettings.State);
|
||||||
|
LoadSchoolHolidays();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Ferien & Feiertage: Bundesland / Schulferien pflegen ─────────────────
|
||||||
|
|
||||||
|
partial void OnSelectedStateNameChanged(string value) =>
|
||||||
|
_calendarSettings.SetState(GermanStateDisplay.FromLabel(value));
|
||||||
|
|
||||||
|
private void LoadSchoolHolidays()
|
||||||
|
{
|
||||||
|
SchoolHolidayEntries.Clear();
|
||||||
|
foreach (var h in _schoolHolidays.GetAll().OrderBy(h => h.StartDate))
|
||||||
|
SchoolHolidayEntries.Add(new SchoolHolidayItem(h));
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void AddSchoolHoliday()
|
||||||
|
{
|
||||||
|
HolidayNameError = ""; HolidayDateError = "";
|
||||||
|
var valid = true;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(NewHolidayName)) { HolidayNameError = "Name erforderlich."; valid = false; }
|
||||||
|
|
||||||
|
var hasStart = DateOnly.TryParseExact(NewHolidayStartText, "dd.MM.yyyy", CultureInfo.InvariantCulture,
|
||||||
|
System.Globalization.DateTimeStyles.None, out var start);
|
||||||
|
var hasEnd = DateOnly.TryParseExact(NewHolidayEndText, "dd.MM.yyyy", CultureInfo.InvariantCulture,
|
||||||
|
System.Globalization.DateTimeStyles.None, out var end);
|
||||||
|
|
||||||
|
if (!hasStart || !hasEnd) { HolidayDateError = "Bitte Beginn und Ende im Format TT.MM.JJJJ angeben."; valid = false; }
|
||||||
|
else if (end < start) { HolidayDateError = "Das Ende darf nicht vor dem Beginn liegen."; valid = false; }
|
||||||
|
|
||||||
|
if (!valid) return;
|
||||||
|
|
||||||
|
_schoolHolidays.Save(new SchoolHoliday { Name = NewHolidayName.Trim(), StartDate = start, EndDate = end });
|
||||||
|
NewHolidayName = ""; NewHolidayStartText = ""; NewHolidayEndText = "";
|
||||||
|
LoadSchoolHolidays();
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void RemoveSchoolHoliday(SchoolHolidayItem? item)
|
||||||
|
{
|
||||||
|
if (item is null) return;
|
||||||
|
_schoolHolidays.Delete(item.Id);
|
||||||
|
SchoolHolidayEntries.Remove(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Datenschutz: Löschfristen ─────────────────────────────────────────────
|
// ── Datenschutz: Löschfristen ─────────────────────────────────────────────
|
||||||
@@ -770,6 +835,13 @@ public class ExpiredDocumentItem(Documentation d, string studentName)
|
|||||||
public string CreatedAtDisplay { get; } = d.CreatedAt.ToLocalTime().ToString("dd.MM.yyyy");
|
public string CreatedAtDisplay { get; } = d.CreatedAt.ToLocalTime().ToString("dd.MM.yyyy");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class SchoolHolidayItem(SchoolHoliday h)
|
||||||
|
{
|
||||||
|
public Guid Id { get; } = h.Id;
|
||||||
|
public string Name { get; } = h.Name;
|
||||||
|
public string RangeDisplay { get; } = $"{h.StartDate:dd.MM.yyyy} – {h.EndDate:dd.MM.yyyy}";
|
||||||
|
}
|
||||||
|
|
||||||
// ── JSON DTOs ─────────────────────────────────────────────────────────────────
|
// ── JSON DTOs ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
internal class CatalogDto
|
internal class CatalogDto
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
xmlns:vs="clr-namespace:LehrerApp.Desktop.Views.Students"
|
xmlns:vs="clr-namespace:LehrerApp.Desktop.Views.Students"
|
||||||
xmlns:vset="clr-namespace:LehrerApp.Desktop.Views.Settings"
|
xmlns:vset="clr-namespace:LehrerApp.Desktop.Views.Settings"
|
||||||
xmlns:vmset="clr-namespace:LehrerApp.Desktop.ViewModels.Settings"
|
xmlns:vmset="clr-namespace:LehrerApp.Desktop.ViewModels.Settings"
|
||||||
|
xmlns:vp="clr-namespace:LehrerApp.Desktop.Views.Planning"
|
||||||
|
xmlns:vmp="clr-namespace:LehrerApp.Desktop.ViewModels.Planning"
|
||||||
xmlns:svc="clr-namespace:LehrerApp.Desktop.Services"
|
xmlns:svc="clr-namespace:LehrerApp.Desktop.Services"
|
||||||
x:Class="LehrerApp.Desktop.Views.MainWindow"
|
x:Class="LehrerApp.Desktop.Views.MainWindow"
|
||||||
x:DataType="vm:MainWindowViewModel"
|
x:DataType="vm:MainWindowViewModel"
|
||||||
@@ -48,6 +50,9 @@
|
|||||||
<DataTemplate DataType="vmset:SettingsViewModel">
|
<DataTemplate DataType="vmset:SettingsViewModel">
|
||||||
<vset:SettingsView/>
|
<vset:SettingsView/>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
|
<DataTemplate DataType="vmp:TimetableViewModel">
|
||||||
|
<vp:TimetableView/>
|
||||||
|
</DataTemplate>
|
||||||
<DataTemplate DataType="vm:PlaceholderViewModel">
|
<DataTemplate DataType="vm:PlaceholderViewModel">
|
||||||
<views:PlaceholderView/>
|
<views:PlaceholderView/>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Planning"
|
||||||
|
x:Class="LehrerApp.Desktop.Views.Planning.TimetableSlotDialog"
|
||||||
|
x:DataType="vm:TimetableSlotDialogViewModel"
|
||||||
|
Title="{Binding DialogTitle}"
|
||||||
|
Width="360" SizeToContent="Height"
|
||||||
|
CanResize="False" WindowStartupLocation="CenterOwner">
|
||||||
|
|
||||||
|
<StackPanel Margin="24" Spacing="12">
|
||||||
|
<TextBlock Text="{Binding DialogTitle}" Classes="dialogtitle"/>
|
||||||
|
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBlock Text="Lerngruppe *" FontSize="12" Opacity="0.7"/>
|
||||||
|
<ComboBox ItemsSource="{Binding GroupOptions}" SelectedItem="{Binding SelectedGroupName}"
|
||||||
|
HorizontalAlignment="Stretch"/>
|
||||||
|
<TextBlock Text="{Binding GroupError}" Foreground="Red" FontSize="11"
|
||||||
|
IsVisible="{Binding GroupError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBlock Text="Raum (optional)" FontSize="12" Opacity="0.7"/>
|
||||||
|
<TextBox Text="{Binding Room}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Button Content="Eintrag entfernen" HorizontalAlignment="Stretch" Margin="0,10,0,0"
|
||||||
|
IsVisible="{Binding IsEditing}" Click="OnDelete"/>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*,8,*" Margin="0,4,0,0">
|
||||||
|
<Button Grid.Column="0" Content="Abbrechen" HorizontalAlignment="Stretch" Click="OnCancel"/>
|
||||||
|
<Button Grid.Column="2" Content="Speichern" HorizontalAlignment="Stretch" Click="OnSave"/>
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Planning;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Views.Planning;
|
||||||
|
|
||||||
|
public partial class TimetableSlotDialog : Window
|
||||||
|
{
|
||||||
|
public TimetableSlotDialog() => InitializeComponent();
|
||||||
|
|
||||||
|
private void OnSave(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is TimetableSlotDialogViewModel vm)
|
||||||
|
{
|
||||||
|
vm.SaveCommand.Execute(null);
|
||||||
|
if (vm.Result is not null) Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDelete(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is TimetableSlotDialogViewModel vm)
|
||||||
|
{
|
||||||
|
vm.DeleteCommand.Execute(null);
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCancel(object? sender, RoutedEventArgs e) => Close();
|
||||||
|
}
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Planning"
|
||||||
|
xmlns:shared="clr-namespace:LehrerApp.Desktop.Views.Shared"
|
||||||
|
x:Class="LehrerApp.Desktop.Views.Planning.TimetableView"
|
||||||
|
x:DataType="vm:TimetableViewModel">
|
||||||
|
|
||||||
|
<UserControl.Styles>
|
||||||
|
<Style Selector="Border.timetablecell">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource SystemControlBackgroundBaseLowBrush}"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Border.timetablecell.assigned">
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Border.weekheader.today">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource SystemAccentColor}"/>
|
||||||
|
</Style>
|
||||||
|
</UserControl.Styles>
|
||||||
|
|
||||||
|
<Grid RowDefinitions="Auto,*">
|
||||||
|
<shared:PageHeader Grid.Row="0" Margin="32,28,32,0" Title="Stundenplan"
|
||||||
|
Subtitle="Wiederkehrendes wöchentliches Muster, keine konkreten Termine"/>
|
||||||
|
|
||||||
|
<TabbedPage Grid.Row="1" TabPlacement="Top" SelectedIndex="{Binding ActiveTabIndex}">
|
||||||
|
|
||||||
|
<!-- Tab: Heute (Standardansicht) -->
|
||||||
|
<ContentPage Header="Heute">
|
||||||
|
<DockPanel Margin="32,20,32,20">
|
||||||
|
|
||||||
|
<!-- Unten angedockt: heutige Stunden im Detail -->
|
||||||
|
<Border DockPanel.Dock="Bottom" Margin="0,16,0,0"
|
||||||
|
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||||
|
CornerRadius="8" Padding="16" MaxHeight="240">
|
||||||
|
<DockPanel>
|
||||||
|
<TextBlock DockPanel.Dock="Top" Text="{Binding TodayLabel}" FontSize="15" FontWeight="SemiBold" Margin="0,0,0,8"/>
|
||||||
|
<ScrollViewer>
|
||||||
|
<ItemsControl ItemsSource="{Binding TodayItems}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:TodayLessonItem">
|
||||||
|
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||||
|
BorderThickness="0,0,0,1" Padding="0,8">
|
||||||
|
<Grid ColumnDefinitions="6,Auto,*,Auto">
|
||||||
|
<Rectangle Grid.Column="0" Fill="{Binding ColorHex}" Width="5" HorizontalAlignment="Left"/>
|
||||||
|
<TextBlock Grid.Column="1" Text="{Binding PeriodNumber}" FontSize="17" FontWeight="Bold"
|
||||||
|
Width="30" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="8,0"/>
|
||||||
|
<StackPanel Grid.Column="2" Spacing="2">
|
||||||
|
<TextBlock Text="{Binding GroupName}" FontSize="14" FontWeight="SemiBold"/>
|
||||||
|
<TextBlock FontSize="11" Opacity="0.6" IsVisible="{Binding HasRoom}">
|
||||||
|
<Run Text="Raum "/><Run Text="{Binding Room}"/>
|
||||||
|
</TextBlock>
|
||||||
|
<TextBlock Text="{Binding LessonTopic}" FontSize="11" Opacity="0.75"
|
||||||
|
TextWrapping="Wrap" IsVisible="{Binding HasLessonTopic}"/>
|
||||||
|
<TextBlock Text="{Binding ExamTitle}" FontSize="11" Foreground="#D85A30" FontWeight="SemiBold"
|
||||||
|
IsVisible="{Binding HasExam}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<Button Grid.Column="3" Content="Zur Lerngruppe" FontSize="11" Padding="9,4"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Command="{Binding $parent[ItemsControl].((vm:TimetableViewModel)DataContext).OpenGroupCommand}"
|
||||||
|
CommandParameter="{Binding GroupId}"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</ScrollViewer>
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
|
<TextBlock DockPanel.Dock="Bottom" Text="Heute sind keine Stunden im Stundenplan eingetragen." Classes="emptyhint"
|
||||||
|
IsVisible="{Binding !TodayItems.Count}" Margin="0,8,0,0"/>
|
||||||
|
|
||||||
|
<!-- Rest: Wochenüberblick (nicht editierbar) -->
|
||||||
|
<ScrollViewer>
|
||||||
|
<StackPanel Spacing="14">
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="Auto,Auto,*,Auto,Auto">
|
||||||
|
<Button Grid.Column="0" Content="‹" FontWeight="Bold" Padding="10,4"
|
||||||
|
Command="{Binding PreviousWeekCommand}" ToolTip.Tip="Vorherige Woche"/>
|
||||||
|
<Button Grid.Column="1" Content="›" FontWeight="Bold" Padding="10,4" Margin="4,0,0,0"
|
||||||
|
Command="{Binding NextWeekCommand}" ToolTip.Tip="Nächste Woche"/>
|
||||||
|
<TextBlock Grid.Column="2" FontSize="16" FontWeight="SemiBold"
|
||||||
|
VerticalAlignment="Center" Margin="12,0,0,0">
|
||||||
|
<Run Text="Woche "/><Run Text="{Binding WeekRangeLabel}"/>
|
||||||
|
</TextBlock>
|
||||||
|
<Button Grid.Column="3" Content="Diese Woche" Margin="0,0,8,0"
|
||||||
|
Command="{Binding CurrentWeekCommand}" IsVisible="{Binding !IsCurrentWeek}"/>
|
||||||
|
<Button Grid.Column="4" Content="Stundenplan bearbeiten" Command="{Binding ShowEditorCommand}"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<ItemsControl ItemsSource="{Binding WeekItems}">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate><UniformGrid Columns="6"/></ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:WeekCellItem">
|
||||||
|
<Grid Margin="2" MinHeight="76">
|
||||||
|
<Border Classes="weekheader" Classes.today="{Binding IsToday}" CornerRadius="4"
|
||||||
|
IsVisible="{Binding IsHeader}">
|
||||||
|
<TextBlock Text="{Binding Text}" FontWeight="SemiBold" FontSize="12" Opacity="0.75"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center" Margin="4"/>
|
||||||
|
</Border>
|
||||||
|
<TextBlock Text="{Binding Text}" IsVisible="{Binding IsPeriodLabel}"
|
||||||
|
FontWeight="SemiBold" FontSize="13"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
<Border Classes="timetablecell" Classes.assigned="{Binding IsAssigned}"
|
||||||
|
CornerRadius="6" IsVisible="{Binding IsSlotCell}">
|
||||||
|
<Button Background="{Binding ColorHex}" IsVisible="{Binding IsAssigned}"
|
||||||
|
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
|
||||||
|
HorizontalContentAlignment="Stretch" CornerRadius="6" Padding="6"
|
||||||
|
Command="{Binding $parent[ItemsControl].((vm:TimetableViewModel)DataContext).OpenGroupCommand}"
|
||||||
|
CommandParameter="{Binding GroupId}">
|
||||||
|
<StackPanel Spacing="1">
|
||||||
|
<TextBlock FontSize="11" FontWeight="Bold" Foreground="White" TextWrapping="Wrap">
|
||||||
|
<Run Text="{Binding SubjectLabel}"/><Run Text=" · "/><Run Text="{Binding GroupName}"/>
|
||||||
|
</TextBlock>
|
||||||
|
<TextBlock Text="{Binding Room}" FontSize="10" Foreground="White" Opacity="0.9"
|
||||||
|
IsVisible="{Binding HasRoom}"/>
|
||||||
|
<TextBlock Text="{Binding Topic}" FontSize="10" Foreground="White" Opacity="0.8"
|
||||||
|
TextWrapping="Wrap" IsVisible="{Binding HasTopic}"/>
|
||||||
|
<TextBlock Text="Ferien" FontSize="10" Foreground="White" FontWeight="SemiBold"
|
||||||
|
Opacity="0.9" IsVisible="{Binding IsHoliday}" Margin="0,2,0,0"/>
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="3" Margin="0,2,0,0"
|
||||||
|
IsVisible="{Binding !IsHoliday}">
|
||||||
|
<Border Background="#B71C1C" CornerRadius="7" Padding="4,0" IsVisible="{Binding HasHolidayBadge}">
|
||||||
|
<TextBlock Text="{Binding HolidayBadge}" FontSize="9" FontWeight="Bold" Foreground="White"/>
|
||||||
|
</Border>
|
||||||
|
<TextBlock Text="📝" FontSize="11" IsVisible="{Binding HasExam}" ToolTip.Tip="Klausur"/>
|
||||||
|
<TextBlock Text="⏰" FontSize="11" IsVisible="{Binding IsLastBeforeExam}" ToolTip.Tip="Letzte Stunde vor der Klausur"/>
|
||||||
|
<TextBlock Text="🧪" FontSize="11" IsVisible="{Binding HasExperiment}" ToolTip.Tip="Experiment geplant"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
|
||||||
|
<Border Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||||
|
CornerRadius="8" Padding="16" IsVisible="{Binding HoursWarnings.Count}">
|
||||||
|
<StackPanel Spacing="8">
|
||||||
|
<TextBlock Text="ABWEICHENDE WOCHENSTUNDEN" FontSize="11" FontWeight="Bold" Opacity="0.5"/>
|
||||||
|
<ItemsControl ItemsSource="{Binding HoursWarnings}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:HoursWarningItem">
|
||||||
|
<TextBlock Text="{Binding Text}" FontSize="12" Foreground="#FB8C00"
|
||||||
|
TextWrapping="Wrap" Margin="0,3"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</DockPanel>
|
||||||
|
</ContentPage>
|
||||||
|
|
||||||
|
<!-- Tab: Bearbeiten (Raster) -->
|
||||||
|
<ContentPage Header="Bearbeiten">
|
||||||
|
<ScrollViewer>
|
||||||
|
<StackPanel Margin="32,20,32,28" Spacing="10">
|
||||||
|
<ItemsControl ItemsSource="{Binding Cells}">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate><UniformGrid Columns="6"/></ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:TimetableCellItem">
|
||||||
|
<Grid Margin="2" MinHeight="46">
|
||||||
|
<TextBlock Text="{Binding Text}" IsVisible="{Binding IsHeader}"
|
||||||
|
FontWeight="SemiBold" FontSize="12" Opacity="0.6"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Text="{Binding Text}" IsVisible="{Binding IsPeriodLabel}"
|
||||||
|
FontWeight="SemiBold" FontSize="13"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
<Border Classes="timetablecell" Classes.assigned="{Binding IsAssigned}"
|
||||||
|
CornerRadius="6" IsVisible="{Binding IsSlotCell}">
|
||||||
|
<Panel>
|
||||||
|
<Button Background="{Binding ColorHex}"
|
||||||
|
IsVisible="{Binding IsAssigned}"
|
||||||
|
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
|
||||||
|
HorizontalContentAlignment="Center" CornerRadius="6"
|
||||||
|
Command="{Binding $parent[ItemsControl].((vm:TimetableViewModel)DataContext).EditCellCommand}"
|
||||||
|
CommandParameter="{Binding}">
|
||||||
|
<TextBlock Text="{Binding Text}" FontSize="11" Foreground="White"
|
||||||
|
TextWrapping="Wrap" TextAlignment="Center"/>
|
||||||
|
</Button>
|
||||||
|
<Button Content="+" Opacity="0.35"
|
||||||
|
IsVisible="{Binding !IsAssigned}"
|
||||||
|
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
|
||||||
|
HorizontalContentAlignment="Center" Background="Transparent"
|
||||||
|
Command="{Binding $parent[ItemsControl].((vm:TimetableViewModel)DataContext).EditCellCommand}"
|
||||||
|
CommandParameter="{Binding}"/>
|
||||||
|
<Border Background="#D85A30" CornerRadius="8" Padding="5,1"
|
||||||
|
IsVisible="{Binding HasBadge}"
|
||||||
|
HorizontalAlignment="Right" VerticalAlignment="Top" Margin="2">
|
||||||
|
<TextBlock Text="{Binding BadgeText}" FontSize="10" FontWeight="Bold" Foreground="White"/>
|
||||||
|
</Border>
|
||||||
|
</Panel>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</ContentPage>
|
||||||
|
|
||||||
|
</TabbedPage>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using LehrerApp.Core.Interfaces;
|
||||||
|
using LehrerApp.Core.Services;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Planning;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Views.Planning;
|
||||||
|
|
||||||
|
public partial class TimetableView : UserControl
|
||||||
|
{
|
||||||
|
public TimetableView() => InitializeComponent();
|
||||||
|
|
||||||
|
protected override void OnDataContextChanged(EventArgs e)
|
||||||
|
{
|
||||||
|
base.OnDataContextChanged(e);
|
||||||
|
if (DataContext is TimetableViewModel vm) vm.OnEditSlot = ShowSlotDialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ShowSlotDialog(TimetableCellItem cell)
|
||||||
|
{
|
||||||
|
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||||
|
if (owner is null || cell.Weekday is null) return;
|
||||||
|
|
||||||
|
var vm = new TimetableSlotDialogViewModel(
|
||||||
|
App.Services.GetRequiredService<ITimetableSlotRepository>(),
|
||||||
|
App.Services.GetRequiredService<IGroupRepository>(),
|
||||||
|
App.Services.GetRequiredService<SchoolYearService>(),
|
||||||
|
cell.Weekday.Value, cell.PeriodNumber, cell.Slot);
|
||||||
|
|
||||||
|
var dialog = new TimetableSlotDialog { DataContext = vm };
|
||||||
|
await dialog.ShowDialog(owner);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -478,6 +478,64 @@
|
|||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</ContentPage>
|
</ContentPage>
|
||||||
|
|
||||||
|
<!-- Tab: Ferien & Feiertage (4.3.5) -->
|
||||||
|
<ContentPage Header="Ferien & Feiertage">
|
||||||
|
<ScrollViewer>
|
||||||
|
<StackPanel Margin="32,20,32,28" Spacing="14" MaxWidth="480">
|
||||||
|
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBlock Text="Bundesland (für Feiertage)" FontSize="13" FontWeight="SemiBold"/>
|
||||||
|
<ComboBox ItemsSource="{Binding StateOptions}" SelectedItem="{Binding SelectedStateName}"
|
||||||
|
HorizontalAlignment="Stretch"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Separator Margin="0,4"/>
|
||||||
|
|
||||||
|
<TextBlock Text="Schulferien" FontSize="16" FontWeight="SemiBold"/>
|
||||||
|
<TextBlock Text="Werden im Stundenplan als unterrichtsfreie Tage angezeigt."
|
||||||
|
FontSize="12" Opacity="0.6" TextWrapping="Wrap"/>
|
||||||
|
|
||||||
|
<ItemsControl ItemsSource="{Binding SchoolHolidayEntries}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate DataType="vm:SchoolHolidayItem">
|
||||||
|
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||||
|
BorderThickness="0,0,0,1" Padding="0,7">
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<StackPanel Grid.Column="0">
|
||||||
|
<TextBlock Text="{Binding Name}" FontSize="13" FontWeight="SemiBold"/>
|
||||||
|
<TextBlock Text="{Binding RangeDisplay}" FontSize="12" Opacity="0.6"/>
|
||||||
|
</StackPanel>
|
||||||
|
<Button Grid.Column="1" Content="×" FontSize="14" Padding="9,3"
|
||||||
|
Command="{Binding $parent[ItemsControl].((vm:SettingsViewModel)DataContext).RemoveSchoolHolidayCommand}"
|
||||||
|
CommandParameter="{Binding}"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
<TextBlock Text="Noch keine Schulferien hinterlegt." Classes="emptyhint"
|
||||||
|
IsVisible="{Binding !SchoolHolidayEntries.Count}"/>
|
||||||
|
|
||||||
|
<Separator Margin="0,4"/>
|
||||||
|
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBox Text="{Binding NewHolidayName}" PlaceholderText="Name (z.B. Sommerferien)"/>
|
||||||
|
<TextBlock Text="{Binding HolidayNameError}" Foreground="Red" FontSize="11"
|
||||||
|
IsVisible="{Binding HolidayNameError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
<Grid ColumnDefinitions="*,8,*">
|
||||||
|
<TextBox Grid.Column="0" Text="{Binding NewHolidayStartText}" PlaceholderText="Beginn TT.MM.JJJJ"/>
|
||||||
|
<TextBox Grid.Column="2" Text="{Binding NewHolidayEndText}" PlaceholderText="Ende TT.MM.JJJJ"/>
|
||||||
|
</Grid>
|
||||||
|
<TextBlock Text="{Binding HolidayDateError}" Foreground="Red" FontSize="11"
|
||||||
|
IsVisible="{Binding HolidayDateError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
<Button Content="+ Schulferien hinzufügen" Command="{Binding AddSchoolHolidayCommand}"
|
||||||
|
HorizontalAlignment="Left"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</ContentPage>
|
||||||
|
|
||||||
</TabbedPage>
|
</TabbedPage>
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
using LehrerApp.Core.Models;
|
||||||
|
using LehrerApp.Core.Services;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace LehrerApp.Tests;
|
||||||
|
|
||||||
|
public sealed class PublicHolidayServiceTests
|
||||||
|
{
|
||||||
|
private readonly PublicHolidayService _service = new();
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(2023, "2023-04-09")]
|
||||||
|
[InlineData(2024, "2024-03-31")]
|
||||||
|
[InlineData(2025, "2025-04-20")]
|
||||||
|
[InlineData(2026, "2026-04-05")]
|
||||||
|
[InlineData(2027, "2027-03-28")]
|
||||||
|
public void GetHolidays_Ostermontag_LiegtEinenTagNachDemBekanntenOstersonntag(int year, string easterSundayIso)
|
||||||
|
{
|
||||||
|
var easterSunday = DateOnly.Parse(easterSundayIso);
|
||||||
|
var holidays = _service.GetHolidays(year, GermanState.NW);
|
||||||
|
|
||||||
|
Assert.Contains(holidays, h => h.Name == "Ostermontag" && h.Date == easterSunday.AddDays(1));
|
||||||
|
Assert.Contains(holidays, h => h.Name == "Karfreitag" && h.Date == easterSunday.AddDays(-2));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetHolidays_AlleBundeslaender_EnthaltenDieBundesweitenFeiertage()
|
||||||
|
{
|
||||||
|
foreach (var state in Enum.GetValues<GermanState>())
|
||||||
|
{
|
||||||
|
var holidays = _service.GetHolidays(2026, state);
|
||||||
|
Assert.Contains(holidays, h => h.Date == new DateOnly(2026, 1, 1)); // Neujahr
|
||||||
|
Assert.Contains(holidays, h => h.Date == new DateOnly(2026, 5, 1)); // Tag der Arbeit
|
||||||
|
Assert.Contains(holidays, h => h.Date == new DateOnly(2026, 10, 3)); // Tag der Deutschen Einheit
|
||||||
|
Assert.Contains(holidays, h => h.Date == new DateOnly(2026, 12, 25));
|
||||||
|
Assert.Contains(holidays, h => h.Date == new DateOnly(2026, 12, 26));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetHolidays_Fronleichnam_NurInDenZustaendigenBundeslaendern()
|
||||||
|
{
|
||||||
|
var nrw = _service.GetHolidays(2026, GermanState.NW);
|
||||||
|
var berlin = _service.GetHolidays(2026, GermanState.BE);
|
||||||
|
|
||||||
|
Assert.Contains(nrw, h => h.Name == "Fronleichnam");
|
||||||
|
Assert.DoesNotContain(berlin, h => h.Name == "Fronleichnam");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetHolidays_Reformationstag_NurInDenZustaendigenBundeslaendern()
|
||||||
|
{
|
||||||
|
var sachsen = _service.GetHolidays(2026, GermanState.SN);
|
||||||
|
var bayern = _service.GetHolidays(2026, GermanState.BY);
|
||||||
|
|
||||||
|
Assert.Contains(sachsen, h => h.Name == "Reformationstag");
|
||||||
|
Assert.DoesNotContain(bayern, h => h.Name == "Reformationstag");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetHolidays_SindNachDatumSortiert()
|
||||||
|
{
|
||||||
|
var holidays = _service.GetHolidays(2026, GermanState.BY);
|
||||||
|
|
||||||
|
Assert.Equal(holidays.OrderBy(h => h.Date).Select(h => h.Date), holidays.Select(h => h.Date));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using LehrerApp.Core.Models;
|
||||||
|
using LehrerApp.Core.Services;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace LehrerApp.Tests;
|
||||||
|
|
||||||
|
public sealed class SchoolCalendarSettingsServiceTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void NeueKonfiguration_HatNordrheinWestfalenAlsStandard()
|
||||||
|
{
|
||||||
|
using var temp = new TempAppData();
|
||||||
|
var service = new SchoolCalendarSettingsService(temp.Path);
|
||||||
|
|
||||||
|
Assert.Equal(GermanState.NW, service.State);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetState_WirdUeberNeueInstanzHinwegPersistiert()
|
||||||
|
{
|
||||||
|
using var temp = new TempAppData();
|
||||||
|
new SchoolCalendarSettingsService(temp.Path).SetState(GermanState.BY);
|
||||||
|
|
||||||
|
var second = new SchoolCalendarSettingsService(temp.Path);
|
||||||
|
|
||||||
|
Assert.Equal(GermanState.BY, second.State);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TempAppData : IDisposable
|
||||||
|
{
|
||||||
|
public string Path { get; } = System.IO.Path.Combine(
|
||||||
|
System.IO.Path.GetTempPath(), $"lehrerapp-schoolcal-tests-{Guid.NewGuid():N}");
|
||||||
|
|
||||||
|
public TempAppData() => Directory.CreateDirectory(Path);
|
||||||
|
public void Dispose() { if (Directory.Exists(Path)) Directory.Delete(Path, recursive: true); }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -224,10 +224,10 @@ zusammenziehen, daraus die Halbjahresnote bilden":
|
|||||||
## 4. Unterrichtsplanung
|
## 4. Unterrichtsplanung
|
||||||
|
|
||||||
Modelle `Unit` und `Lesson` existieren, `UnitRepository`/`LessonRepository` ebenfalls.
|
Modelle `Unit` und `Lesson` existieren, `UnitRepository`/`LessonRepository` ebenfalls.
|
||||||
Navigationspunkt "Unterrichtsplanung" ist weiterhin ein `PlaceholderViewModel`
|
Navigationspunkt "Unterrichtsplanung" zeigt seit 4.3 den Stundenplan (`TimetableView`,
|
||||||
([MainWindowViewModel.cs](LehrerApp.Desktop/ViewModels/MainWindowViewModel.cs)) — er bleibt
|
Standardtab "Heute" + Bearbeiten-Raster) statt eines `PlaceholderViewModel`. Eine
|
||||||
bewusst Platzhalter, bis die gruppenübergreifende Kalenderübersicht aus 4.4 steht. 4.1/4.2 leben
|
gruppenübergreifende Wochenansicht fehlt noch, siehe 4.4. 4.1/4.2 leben unabhängig davon im
|
||||||
stattdessen im gruppenspezifischen Tab "Planung" (`GroupDetailView`), siehe unten.
|
gruppenspezifischen Tab "Planung" (`GroupDetailView`), siehe unten.
|
||||||
|
|
||||||
### 4.1 Unterrichtseinheiten (`Unit`)
|
### 4.1 Unterrichtseinheiten (`Unit`)
|
||||||
- [x] **4.1.1** Listenansicht der Einheiten je Gruppe mit Status und Zeitraum
|
- [x] **4.1.1** Listenansicht der Einheiten je Gruppe mit Status und Zeitraum
|
||||||
@@ -366,16 +366,146 @@ Redesign:
|
|||||||
Hauptweg — deutlich einfacher und für den schnellen Überblick ausreichend.
|
Hauptweg — deutlich einfacher und für den schnellen Überblick ausreichend.
|
||||||
|
|
||||||
### 4.3 Stundenplan
|
### 4.3 Stundenplan
|
||||||
- [ ] **4.3.1** Neues Modell `TimetableSlot` (Gruppe, Wochentag, Stunde, Raum) + Repository.
|
- [x] **4.3.1** Neues Modell `TimetableSlot` (Gruppe, Wochentag, Stunde, Raum) + Repository —
|
||||||
- [ ] **4.3.2** Wochenstundenplan-Ansicht als Raster mit Farbcodierung je Gruppe.
|
[Planning.cs](LehrerApp.Core/Models/Planning.cs),
|
||||||
- [ ] **4.3.3** Bearbeitung per Klick/Drag im Raster.
|
[TimetableSlotRepository](LehrerApp.Data/Repositories/AllRepositories.cs). Wiederkehrendes
|
||||||
- [ ] **4.3.4** Abgleich mit `LearningGroup.HoursPerWeek` (Warnung bei Abweichung).
|
Muster (kein Datum) — für konkret gehaltene Stunden bleibt `Lesson` (4.2) zuständig. Pro
|
||||||
- [ ] **4.3.5** Schulferien und Feiertage hinterlegen (Bundesland wählbar) und aus der Planung ausnehmen.
|
Wochentag/Stunde höchstens eine Gruppe (eindeutiger Index + freundliche Fehlermeldung im
|
||||||
|
Repository selbst, nicht erst über eine rohe `LiteException`): ein Lehrer kann nicht
|
||||||
|
gleichzeitig an zwei Orten unterrichten.
|
||||||
|
- [x] **4.3.2** Wochenstundenplan-Ansicht als Raster mit Farbcodierung je Gruppe — neuer
|
||||||
|
Navigationspunkt "Unterrichtsplanung" (bisher `PlaceholderViewModel`) zeigt jetzt
|
||||||
|
[TimetableView.axaml](LehrerApp.Desktop/Views/Planning/TimetableView.axaml). Mo–Fr ×
|
||||||
|
1.–10. Stunde, Farbe deterministisch aus dem Gruppennamen (gleiches Hash-in-Palette-Muster
|
||||||
|
wie bei den alternativen Unterrichtsabläufen in 4.2.2).
|
||||||
|
- [x] **4.3.3** Bearbeitung per Klick im Raster — Klick auf eine leere Zelle öffnet
|
||||||
|
`TimetableSlotDialog` zum Zuweisen (Gruppenauswahl + optionaler Raum), Klick auf eine
|
||||||
|
belegte Zelle öffnet denselben Dialog zum Ändern/Entfernen. **Drag bewusst nicht
|
||||||
|
umgesetzt:** Klick deckt die vollständige Bearbeitung (Zuweisen/Ändern/Entfernen) bereits
|
||||||
|
ab: Drag wäre nur eine schnellere Geste für "Zuordnung an eine andere Zelle verschieben",
|
||||||
|
kein zusätzlicher Funktionsumfang — bei Bedarf später ergänzbar.
|
||||||
|
- [x] **4.3.4** Abgleich mit `LearningGroup.HoursPerWeek` — Seitenleiste "Abweichende
|
||||||
|
Wochenstunden" listet jede Gruppe der aktuellen Schuljahres, deren eingetragene
|
||||||
|
Slot-Anzahl nicht der hinterlegten Wochenstundenzahl entspricht.
|
||||||
|
- [x] **4.3.5** Schulferien und Feiertage — gesetzliche Feiertage werden je Bundesland berechnet
|
||||||
|
([PublicHolidayService.cs](LehrerApp.Core/Services/PublicHolidayService.cs), Gauß'sche
|
||||||
|
Osterformel + bundeslandspezifische Zusatzfeiertage, empirisch gegen bekannte
|
||||||
|
Ostersonntage 2023–2027 verifiziert) statt gespeichert — anders als Schulferien sind sie
|
||||||
|
algorithmisch herleitbar. Schulferien selbst sind **nicht** herleitbar (jährlich neu von
|
||||||
|
den Bundesländern festgelegt) und werden deshalb manuell gepflegt
|
||||||
|
(`SchoolHoliday`-Repository, CRUD in der Seitenleiste). Bundesland als persistente
|
||||||
|
Einstellung ([SchoolCalendarSettingsService.cs](LehrerApp.Core/Services/SchoolCalendarSettingsService.cs)).
|
||||||
|
"Aus der Planung ausnehmen" ist für dieses Kapitel als "nächste unterrichtsfreie Tage"
|
||||||
|
sichtbar (kombinierte, sortierte Liste aus beiden Quellen) — die eigentliche Ausnahme aus
|
||||||
|
generierten Stunden ist erst mit der (bewusst zurückgestellten) Serienerzeugung 4.2.5
|
||||||
|
relevant und verwendet dieselben zwei Datenquellen.
|
||||||
|
|
||||||
|
**Nachtrag zu 4.3 (Nutzer-Feedback nach Erstumsetzung):**
|
||||||
|
- **Ferien/Feiertage-Pflege verschoben:** Die Bundesland-Auswahl und das Schulferien-CRUD standen
|
||||||
|
ursprünglich in der Seitenleiste des Stundenplans selbst — das wirkte dort deplatziert, da es
|
||||||
|
eine einmalige Einstellung statt einer täglich genutzten Ansicht ist. Beides ist jetzt ein
|
||||||
|
eigener Tab "Ferien & Feiertage" in den Einstellungen
|
||||||
|
([SettingsViewModel.cs](LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.cs),
|
||||||
|
[SettingsView.axaml](LehrerApp.Desktop/Views/Settings/SettingsView.axaml)). Der Stundenplan
|
||||||
|
selbst zeigt Bundesland und nächste unterrichtsfreie Tage nur noch lesend an
|
||||||
|
([TimetableViewModel.cs](LehrerApp.Desktop/ViewModels/Planning/TimetableViewModel.cs)).
|
||||||
|
- **Badge "1"/"2" vor Ferien:** Die letzte bzw. vorletzte Stunde eines Wochentags vor den nächsten
|
||||||
|
Schulferien wird im Bearbeiten-Raster mit einem kleinen Badge markiert — Erinnerung, rechtzeitig
|
||||||
|
abzuschließen. Berechnet pro (Wochentag, Gruppe) statt pro einzelnem `TimetableSlot`: eine
|
||||||
|
Doppelstunde besteht aus zwei Slots mit demselben Wochentag/derselben Gruppe und bekommt dadurch
|
||||||
|
automatisch dasselbe Badge, ohne gesonderte Blockerkennung. Gesetzliche Feiertage werden beim
|
||||||
|
Abzählen der verbleibenden Termine übersprungen. Bewusst nur an `SchoolHoliday` geknüpft, nicht
|
||||||
|
an einzelne `PublicHoliday`-Tage — es geht um das Vorbereiten auf eine längere Unterbrechung,
|
||||||
|
nicht um einen einzelnen freien Tag.
|
||||||
|
- **"Heute" als neue Standardansicht:** Der Stundenplan öffnet jetzt auf einem
|
||||||
|
nicht-editierenden Tab "Heute" (heutige Stunden mit Gruppe, Raum, Thema aus `Lesson.Topic` falls
|
||||||
|
vorhanden, sowie ein Hinweis, wenn an dem Tag eine Klausur ansteht). Klick auf eine Stunde
|
||||||
|
navigiert in die Lerngruppe (Tab "Planung"). Das bisherige Bearbeiten-Raster aus 4.3.2/4.3.3
|
||||||
|
ist über einen Button ("Stundenplan bearbeiten") als zweiter Tab weiterhin erreichbar.
|
||||||
|
|
||||||
|
**Nachtrag zu 4.3, zweite Iteration (Wochenraster in der "Heute"-Ansicht):** Die Tagesliste allein
|
||||||
|
beantwortete nicht "was steht in der Woche noch an" bzw. "was macht der parallele Kurs" — beides
|
||||||
|
Fragen, die nur ein Blick über alle Gruppen hinweg beantwortet. Der Tab "Heute" zeigt deshalb jetzt
|
||||||
|
zusätzlich ein schreibgeschütztes Wochenraster (Mo–Fr × 1.–10. Stunde, wie das Bearbeiten-Raster,
|
||||||
|
aber ohne Klick-zum-Bearbeiten). Jede Kachel zeigt Fach (`Subject.ShortName`), Klasse, Raum und
|
||||||
|
– falls für den Tag hinterlegt – das Thema der Stunde, dazu Symbole für: Ferien-Badge ("1"/"2",
|
||||||
|
wie im Bearbeiten-Raster), Klausur an dem Tag (📝), letzte Stunde vor der nächsten Klausur dieser
|
||||||
|
Gruppe (⏰) und ein geplantes Experiment (🧪). Die Tagesliste ist per `DockPanel` unten an die
|
||||||
|
Seite angedockt, das Wochenraster füllt den verbleibenden Platz darüber
|
||||||
|
([TimetableViewModel.cs](LehrerApp.Desktop/ViewModels/Planning/TimetableViewModel.cs),
|
||||||
|
[TimetableView.axaml](LehrerApp.Desktop/Views/Planning/TimetableView.axaml)). Klick auf eine
|
||||||
|
Kachel navigiert wie in der Tagesliste zur Lerngruppe.
|
||||||
|
- **"Letzte Stunde vor Klausur"** wird wie das Ferien-Badge pro (Wochentag, Gruppe) berechnet,
|
||||||
|
aber am nächsten `Exam.Date` der jeweiligen Gruppe statt an einem gemeinsamen Ferientermin
|
||||||
|
ausgerichtet — beide Badges teilen sich denselben Zähl-Helfer (`CountOccurrences`).
|
||||||
|
- **"Experiment geplant"** hat kein eigenes Datenfeld — es ist eine Texterkennung über die
|
||||||
|
bereits im Verlaufsplan-Editor (4.2.2) gepflegten `LessonPhaseStep`-Felder (Name/Aktivität/
|
||||||
|
Material enthält "Experiment", ohne Groß-/Kleinschreibung). Bewusst kein neues Modellfeld:
|
||||||
|
die Information steckt in den meisten Fällen schon in der ohnehin gepflegten Phasenplanung.
|
||||||
|
- Das Wochenraster zeigt bewusst nur die laufende Kalenderwoche ohne Vor-/Zurück-Navigation —
|
||||||
|
für einen Blick auf zukünftige Wochen bleibt vorerst das Bearbeiten-Raster (zeigt das
|
||||||
|
wiederkehrende Muster unabhängig vom Datum).
|
||||||
|
|
||||||
|
**Nachtrag zu 4.3, dritte Iteration (Ferientage im Wochenraster ausgegraut statt eigener Liste):**
|
||||||
|
Die Box "Nächste unterrichtsfreie Tage" wirkte neben dem neuen Wochenraster redundant — welche
|
||||||
|
Tage frei sind, sieht man dort jetzt direkt an den betroffenen Stunden. Die Box (samt
|
||||||
|
`UpcomingFreeDayItem`, `UpcomingFreeDays`, `BuildUpcomingFreeDays`, `BundeslandLabel`) wurde
|
||||||
|
entfernt. Stattdessen werden Wochenkacheln, deren Datum in Schulferien fällt oder ein gesetzlicher
|
||||||
|
Feiertag ist, grau eingefärbt (`WeekCellItem.IsHoliday`, Prüfung gegen `SchoolHoliday`-Zeitraum und
|
||||||
|
die bereits für die Badges berechneten `PublicHoliday`-Daten) und zeigen statt der Klausur-/
|
||||||
|
Experiment-Symbole nur noch die Aufschrift "Ferien" — die Symbole wären an einem unterrichtsfreien
|
||||||
|
Tag ohnehin nicht sinnvoll interpretierbar.
|
||||||
|
|
||||||
|
**Nachtrag zu 4.3, vierte Iteration (Wochennavigation):** Schalter "‹"/"›" plus "Diese Woche"
|
||||||
|
erlauben jetzt, im Wochenraster vor und zurück zu blättern (`TimetableViewModel.WeekOffset`,
|
||||||
|
`PreviousWeekCommand`/`NextWeekCommand`/`CurrentWeekCommand`). Die Ferien-/Klausur-Nähe-Badges
|
||||||
|
("1"/"2" vor Ferien, ⏰ vor Klausur) waren bis dahin an "heute" verankert und pro (Wochentag,
|
||||||
|
Gruppe) einmalig berechnet — das wäre beim Blättern falsch geworden (ein Badge hätte in jeder
|
||||||
|
angezeigten Woche geklebt, nicht nur in der einen Woche, zu der es gehört). Für das Wochenraster
|
||||||
|
berechnen `HolidayBadgeFor`/`IsLastBeforeExamFor` die Badges deshalb jetzt je Zelle am dort
|
||||||
|
gezeigten Datum statt an "heute" — das Bearbeiten-Raster (zeigt ohnehin nur das wiederkehrende
|
||||||
|
Muster ohne Datum) behält die alte, "heute"-verankerte Berechnung. Beim erneuten Navigieren in den
|
||||||
|
Stundenplan (Sidebar-Klick) springt die Ansicht wieder auf die laufende Woche zurück.
|
||||||
|
|
||||||
### 4.4 Wochen-/Tagesansicht
|
### 4.4 Wochen-/Tagesansicht
|
||||||
- [ ] **4.4.1** Kalenderansicht über alle Gruppen: Woche und Tag.
|
- [x] **4.4.1** Kalenderansicht über alle Gruppen: Woche und Tag — siehe Nachtrag zu 4.3
|
||||||
- [ ] **4.4.2** Sprung von einer Stunde direkt in die Mitarbeitserfassung dieser Gruppe.
|
("Heute"-Tab: Tagesliste unten angedockt, gruppenübergreifendes Wochenraster darüber, inkl.
|
||||||
- [ ] **4.4.3** Anzeige anstehender Klausurtermine und Abgabefristen im Kalender.
|
Vor-/Zurück-Navigation zwischen Kalenderwochen seit der vierten Iteration).
|
||||||
|
**Nicht enthalten:** ein eigenständiger Kalendermonat-Überblick — bei Bedarf später
|
||||||
|
ergänzbar.
|
||||||
|
- [x] **4.4.2** Sprung von einer Stunde direkt in die Lerngruppe — siehe Nachtrag zu 4.3
|
||||||
|
("Heute"-Tab, Klick navigiert zur Planung der Gruppe statt direkt in die
|
||||||
|
Mitarbeitserfassung, da das für den Vorbereitungs-Kontext des Tages passender ist).
|
||||||
|
- [ ] **4.4.3** Anzeige anstehender Klausurtermine und Abgabefristen im Kalender. *(Heutige
|
||||||
|
Klausuren bereits im "Heute"-Tab sichtbar — mehrtägiger Vorausblick fehlt noch.)*
|
||||||
|
|
||||||
|
### 4.5 Vernetzung Stundenplan ↔ Unterrichtsplanung (zurückgestellt)
|
||||||
|
|
||||||
|
Nutzer-Feedback nach der 4.3-Iterationen: Stundenplan (4.3, wiederkehrendes Muster) und
|
||||||
|
Unterrichtsplanung (4.1/4.2, `Unit`/`Lesson` je Gruppe) laufen bisher zu getrennt nebeneinander.
|
||||||
|
Ausdrücklich als Idee für später festgehalten, **nicht** jetzt umsetzen:
|
||||||
|
|
||||||
|
- [ ] **4.5.1** Terminvorschlag beim Anlegen einer `Lesson`: Tag/Stunde anhand des Stundenplans
|
||||||
|
vorschlagen, an dem die betroffene Gruppe laut `TimetableSlot` tatsächlich Unterricht hat,
|
||||||
|
statt Datum/Stunde komplett frei einzutragen.
|
||||||
|
- [ ] **4.5.2** Vom Stundenplan (Wochenraster oder Bearbeiten-Raster) direkt in einen
|
||||||
|
"Viewer" der zugehörigen `Lesson` springen können — und von dort auch eine neue `Lesson`
|
||||||
|
anlegen können. **Offene Frage, vor Umsetzung zu klären:** `Lesson` hängt an einer
|
||||||
|
übergeordneten `Unit` (4.1) — beim Anlegen direkt aus dem Stundenplan heraus ist unklar,
|
||||||
|
welcher `Unit` die neue Lesson zugeordnet werden soll (aktuellste offene Einheit der Gruppe?
|
||||||
|
Rückfrage an die Lehrkraft?). Muss überdacht werden, bevor das gebaut wird.
|
||||||
|
- [ ] **4.5.3** Aus diesem Lesson-Viewer heraus weiter verzweigen können: in die Zeugnisnote/
|
||||||
|
Bewertung der Gruppe, und in die Schnelldialoge für Mitarbeit sowie Anwesenheit/Hausaufgaben.
|
||||||
|
- [ ] **4.5.4** Badges am Stundenplan/Lesson-Viewer, die anzeigen, ob eine Stunde bereits
|
||||||
|
"kontrolliert" ist — u.a. ob in der vorherigen Stunde Hausaufgaben erteilt wurden und ob
|
||||||
|
diese noch nicht kontrolliert sind. Muss sich pro Fall abschalten lassen: manchmal wird eine
|
||||||
|
Hausaufgabe bewusst nicht kontrolliert, ohne dass bis Schuljahresende ständig daran erinnert
|
||||||
|
werden soll.
|
||||||
|
- [ ] **4.5.5** Stunden aus dieser Ansicht heraus verschieben können, wenn kurzfristig etwas
|
||||||
|
dazwischenkommt.
|
||||||
|
- [ ] **4.5.6** Bei Doppelstunden (90-Minuten-Planung laut Verlaufsplan, 4.2.2) den Inhalt
|
||||||
|
sinnvoll auf die beiden Kacheln/Perioden aufteilen, damit z.B. gezielt nur die zweite Stunde
|
||||||
|
eines Blocks verschoben werden kann, ohne den ganzen Block anzufassen.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -892,8 +1022,12 @@ Die Abschnitte sind thematisch, nicht chronologisch nummeriert. Sinnvolle Bearbe
|
|||||||
erledigt (13.1–13.4 vollständig; 13.4.2 bewusst zurückgestellt, siehe dort).
|
erledigt (13.1–13.4 vollständig; 13.4.2 bewusst zurückgestellt, siehe dort).
|
||||||
5. ~~**Kapitel 5** (Schülerdokumentation)~~ — erledigt (5.2 als Auswertung des bestehenden
|
5. ~~**Kapitel 5** (Schülerdokumentation)~~ — erledigt (5.2 als Auswertung des bestehenden
|
||||||
Anwesenheits-Trackings statt zweiter Erfassung, siehe dort).
|
Anwesenheits-Trackings statt zweiter Erfassung, siehe dort).
|
||||||
6. ~~**Kapitel 4.1 + 4.2** (Unterrichtsplanung: Einheiten & Einzelstunden)~~ — erledigt.
|
6. ~~**Kapitel 4.1 + 4.2 + 4.3** (Unterrichtsplanung: Einheiten, Einzelstunden, Stundenplan)~~ —
|
||||||
**Kapitel 4.3** (Stundenplan, neues Modell `TimetableSlot` + Ferien/Feiertage) und **4.4**
|
erledigt, inkl. mehrerer Nachtrag-Iterationen aus Nutzer-Feedback (Ferien-Pflege in den
|
||||||
(Wochen-/Tageskalender über alle Gruppen) bewusst zurückgestellt — eigenständige neue
|
Einstellungen, Wochenraster mit Wochennavigation, Ferientage ausgegraut). ~~**Kapitel 4.4.1/
|
||||||
Subsysteme, kein Ausbau der bestehenden Unit/Lesson-UI. **→ nächster sinnvoller Schritt.**
|
4.4.2**~~ — erledigt (im Zuge der 4.3-Nachträge miterledigt). Offen bleiben **4.4.3**
|
||||||
|
(Abgabefristen im Kalender), **4.2.5** (Serienerzeugung von Stunden aus dem Stundenplan) und
|
||||||
|
**4.5** (engere Vernetzung Stundenplan ↔ Lesson-Planung — ausdrücklich vom Nutzer
|
||||||
|
zurückgestellt, nicht aus Unklarheit).
|
||||||
|
**→ nächster sinnvoller Schritt: 4.4.3 oder 4.2.5, je nach Bedarf.**
|
||||||
7. **Kapitel 6** (Arbeitszeit), **11** (Export), **10** (Sync) — danach.
|
7. **Kapitel 6** (Arbeitszeit), **11** (Export), **10** (Sync) — danach.
|
||||||
|
|||||||
Reference in New Issue
Block a user