CI / build-and-test (push) Canceled after 0s
Eine auf einem Gerät zurückgestellte Warnung stand auf den anderen weiter rot oben, weil DashboardWarningSnooze bewusst lokal gehalten war. Save löst jetzt db.OnChange aus, EventApplier übernimmt den Typ. Die Id wird aus (StudentId, WarningTag) abgeleitet, damit zwei Geräte denselben Datensatz beschreiben statt am Unique-Index ux_student_tag zu kollidieren; Schema-Migration 7 schlüsselt bestehende lokale Einträge um. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1058 lines
49 KiB
C#
1058 lines
49 KiB
C#
using LehrerApp.Core.Interfaces;
|
|
using LehrerApp.Core.Models;
|
|
|
|
namespace LehrerApp.Data.Repositories;
|
|
|
|
internal static class ArchivedGroupWriteGuard
|
|
{
|
|
public const string Message =
|
|
"Diese Lerngruppe ist archiviert. Bitte reaktiviere sie, bevor du Änderungen vornimmst.";
|
|
|
|
public static void EnsureActive(LiteDbContext db, Guid groupId)
|
|
{
|
|
if (db.Groups.FindById(groupId) is { IsActive: false })
|
|
throw new InvalidOperationException(Message);
|
|
}
|
|
}
|
|
|
|
public class StudentRepository(LiteDbContext db) : IStudentRepository
|
|
{
|
|
public Student? GetById(Guid id) => db.Students.FindById(id);
|
|
public List<Student> GetAll(bool includeInactive = false) =>
|
|
(includeInactive ? db.Students.FindAll() : db.Students.Find(s => s.IsActive))
|
|
.OrderBy(s => s.LastName).ToList();
|
|
public List<Student> GetByGroup(Guid groupId)
|
|
{
|
|
var ids = db.Memberships
|
|
.Find(e => e.GroupId == groupId)
|
|
.Select(e => e.StudentId).ToHashSet();
|
|
return db.Students.Find(s => ids.Contains(s.Id)).OrderBy(s => s.LastName).ToList();
|
|
}
|
|
public StudentReferenceSummary GetReferenceSummary(Guid studentId) => new(
|
|
db.Memberships.Count(m => m.StudentId == studentId),
|
|
db.ExamResults.Count(r => r.StudentId == studentId),
|
|
db.Grades.Count(g => g.StudentId == studentId),
|
|
db.ReportGrades.Count(g => g.StudentId == studentId),
|
|
db.ParticipationEntries.Count(e => e.StudentId == studentId),
|
|
db.Documentation.FindAll().Count(d => d.Concerns(studentId)));
|
|
public void Save(Student s)
|
|
{
|
|
s.ExternalIds ??= [];
|
|
s.Contacts ??= [];
|
|
s.UpdatedAt = DateTime.UtcNow;
|
|
db.Students.Upsert(s);
|
|
db.OnChange?.Invoke(nameof(Student), s.Id.ToString(), "Save", s);
|
|
}
|
|
public void Delete(Guid id)
|
|
{
|
|
var references = GetReferenceSummary(id);
|
|
if (references.HasReferences)
|
|
throw new InvalidOperationException(
|
|
"Der Schüler besitzt verknüpfte Daten und kann nur deaktiviert werden.");
|
|
db.Students.Delete(id);
|
|
db.OnChange?.Invoke(nameof(Student), id.ToString(), "Delete", null);
|
|
}
|
|
}
|
|
|
|
public class GroupRepository(LiteDbContext db) : IGroupRepository
|
|
{
|
|
public LearningGroup? GetById(Guid id) => db.Groups.FindById(id);
|
|
public List<LearningGroup> GetAll(bool includeInactive = false) =>
|
|
(includeInactive ? db.Groups.FindAll() : db.Groups.Find(g => g.IsActive))
|
|
.OrderBy(g => g.SchoolYear).ThenBy(g => g.Name).ToList();
|
|
public List<LearningGroup> GetBySchoolYear(string schoolYear, bool includeInactive = false) =>
|
|
(includeInactive
|
|
? db.Groups.Find(g => g.SchoolYear == schoolYear)
|
|
: db.Groups.Find(g => g.SchoolYear == schoolYear && g.IsActive))
|
|
.OrderBy(g => g.Name).ToList();
|
|
public void Save(LearningGroup g)
|
|
{
|
|
var existing = db.Groups.FindById(g.Id);
|
|
if (existing is { IsActive: false } && !g.IsActive)
|
|
throw new InvalidOperationException(ArchivedGroupWriteGuard.Message);
|
|
if (existing is { IsActive: false } && g.IsActive
|
|
&& (existing.Name != g.Name || existing.Type != g.Type || existing.SubjectId != g.SubjectId
|
|
|| existing.SchoolYear != g.SchoolYear || existing.GradeLevel != g.GradeLevel
|
|
|| existing.GradingSystem != g.GradingSystem || existing.HoursPerWeek != g.HoursPerWeek
|
|
|| existing.IsOwnClass != g.IsOwnClass || existing.IsDifferentiated != g.IsDifferentiated))
|
|
throw new InvalidOperationException(
|
|
"Bitte reaktiviere die Lerngruppe zuerst und nimm die Änderungen anschließend vor.");
|
|
if (g.SubjectId is Guid subjectId && db.Subjects.FindById(subjectId) is null)
|
|
throw new InvalidOperationException("Das gewählte Fach existiert nicht mehr.");
|
|
g.UpdatedAt = DateTime.UtcNow;
|
|
db.Groups.Upsert(g);
|
|
db.OnChange?.Invoke(nameof(LearningGroup), g.Id.ToString(), "Save", g);
|
|
}
|
|
public void Delete(Guid id)
|
|
{
|
|
ArchivedGroupWriteGuard.EnsureActive(db, id);
|
|
db.CascadeDeleteGroup(id);
|
|
db.OnChange?.Invoke(nameof(LearningGroup), id.ToString(), "Delete", null);
|
|
}
|
|
}
|
|
|
|
public class SeatingPlanRepository(LiteDbContext db) : ISeatingPlanRepository
|
|
{
|
|
public SeatingPlan? GetById(Guid id) => db.SeatingPlans.FindById(id);
|
|
|
|
public List<SeatingPlan> GetByGroup(Guid groupId) =>
|
|
db.SeatingPlans.Find(p => p.GroupId == groupId)
|
|
.OrderBy(p => p.Name).ThenBy(p => p.Room).ToList();
|
|
|
|
public void Save(SeatingPlan plan)
|
|
{
|
|
ArchivedGroupWriteGuard.EnsureActive(db, plan.GroupId);
|
|
// LiteDB can deserialize missing/legacy optional string fields as null even
|
|
// though the current model initializes them with an empty string.
|
|
plan.Name = plan.Name?.Trim() ?? "";
|
|
plan.Room = plan.Room?.Trim() ?? "";
|
|
plan.Assignments ??= [];
|
|
plan.ColumnGapWidths ??= [];
|
|
plan.HiddenSeats ??= [];
|
|
if (plan.Name.Length == 0)
|
|
throw new ArgumentException("Der Name des Sitzplans darf nicht leer sein.");
|
|
if (plan.Rows is < 1 or > 10 || plan.Columns is < 1 or > 10)
|
|
throw new ArgumentOutOfRangeException(nameof(plan), "Ein Sitzplan muss zwischen 1 und 10 Reihen und Spalten haben.");
|
|
if (db.Groups.FindById(plan.GroupId) is null)
|
|
throw new InvalidOperationException("Die zugehörige Lerngruppe existiert nicht.");
|
|
|
|
plan.ColumnGapWidths = Enumerable.Range(0, Math.Max(0, plan.Columns - 1))
|
|
.Select(i => i < plan.ColumnGapWidths.Count && double.IsFinite(plan.ColumnGapWidths[i])
|
|
? Math.Clamp(plan.ColumnGapWidths[i], 0, 300)
|
|
: 0)
|
|
.ToList();
|
|
|
|
var duplicateName = db.SeatingPlans.Find(p => p.GroupId == plan.GroupId)
|
|
.FirstOrDefault(p => p.Id != plan.Id
|
|
&& string.Equals(p.Name, plan.Name, StringComparison.OrdinalIgnoreCase)
|
|
&& string.Equals(p.Room, plan.Room, StringComparison.OrdinalIgnoreCase));
|
|
if (duplicateName is not null)
|
|
throw new InvalidOperationException("Für diese Lerngruppe existiert bereits ein gleichnamiger Sitzplan in diesem Raum.");
|
|
|
|
plan.Assignments = plan.Assignments
|
|
.Where(a => a.Row >= 0 && a.Row < plan.Rows && a.Column >= 0 && a.Column < plan.Columns)
|
|
.ToList();
|
|
if (plan.Assignments.GroupBy(a => (a.Row, a.Column)).Any(g => g.Count() > 1))
|
|
throw new InvalidOperationException("Ein Sitzplatz darf nur einmal belegt werden.");
|
|
if (plan.Assignments.GroupBy(a => a.StudentId).Any(g => g.Count() > 1))
|
|
throw new InvalidOperationException("Ein Schüler darf in einem Sitzplan nur einmal vorkommen.");
|
|
|
|
var memberIds = db.Memberships.Find(m => m.GroupId == plan.GroupId)
|
|
.Select(m => m.StudentId).ToHashSet();
|
|
if (plan.Assignments.Any(a => !memberIds.Contains(a.StudentId)))
|
|
throw new InvalidOperationException("Der Sitzplan enthält einen Schüler, der nicht zur Lerngruppe gehört.");
|
|
|
|
// Ein ausgeblendeter Platz darf nie belegt sein - die Zuordnung eines Schülers ist
|
|
// wertvoller als das Ausblenden, deshalb wird hier stillschweigend wieder eingeblendet
|
|
// statt die Zuordnung zu verwerfen. Sollte über die UI (SeatingPlanTabViewModel verhindert
|
|
// das Belegen ausgeblendeter Plätze) nie vorkommen - reine Absicherung bei fehlerhaften
|
|
// oder von einem älteren Client synchronisierten Daten.
|
|
var assignedPositions = plan.Assignments.Select(a => (a.Row, a.Column)).ToHashSet();
|
|
plan.HiddenSeats = plan.HiddenSeats
|
|
.Where(h => h.Row >= 0 && h.Row < plan.Rows && h.Column >= 0 && h.Column < plan.Columns)
|
|
.Where(h => !assignedPositions.Contains((h.Row, h.Column)))
|
|
.DistinctBy(h => (h.Row, h.Column))
|
|
.ToList();
|
|
|
|
plan.UpdatedAt = DateTime.UtcNow;
|
|
db.SeatingPlans.Upsert(plan);
|
|
db.OnChange?.Invoke(nameof(SeatingPlan), plan.Id.ToString(), "Save", plan);
|
|
}
|
|
|
|
public void Delete(Guid id)
|
|
{
|
|
if (db.SeatingPlans.FindById(id) is not { } plan) return;
|
|
ArchivedGroupWriteGuard.EnsureActive(db, plan.GroupId);
|
|
db.MoveToTrash(nameof(SeatingPlan), id, plan,
|
|
string.IsNullOrWhiteSpace(plan.Room) ? plan.Name : $"{plan.Name} (Raum {plan.Room})");
|
|
db.SeatingPlans.Delete(id);
|
|
db.OnChange?.Invoke(nameof(SeatingPlan), id.ToString(), "Delete", null);
|
|
}
|
|
|
|
public void Restore(Guid trashId)
|
|
{
|
|
if (db.RestoreFromTrash<SeatingPlan>(trashId) is { } plan) Save(plan);
|
|
}
|
|
}
|
|
|
|
public class GroupMembershipRepository(LiteDbContext db) : IGroupMembershipRepository
|
|
{
|
|
public List<GroupMembership> GetByStudent(Guid id) =>
|
|
db.Memberships.Find(e => e.StudentId == id).ToList();
|
|
public List<GroupMembership> GetByGroup(Guid id) =>
|
|
db.Memberships.Find(e => e.GroupId == id).ToList();
|
|
public GroupMembership? GetByStudentAndGroup(Guid studentId, Guid groupId) =>
|
|
db.Memberships.FindOne(e => e.StudentId == studentId && e.GroupId == groupId);
|
|
public void Save(GroupMembership membership)
|
|
{
|
|
ArchivedGroupWriteGuard.EnsureActive(db, membership.GroupId);
|
|
var existing = GetByStudentAndGroup(membership.StudentId, membership.GroupId);
|
|
if (existing is not null && existing.Id != membership.Id)
|
|
throw new InvalidOperationException("Der Schüler ist dieser Lerngruppe bereits zugeordnet.");
|
|
db.Memberships.Upsert(membership);
|
|
db.OnChange?.Invoke(nameof(GroupMembership), membership.Id.ToString(), "Save", membership);
|
|
}
|
|
public void Delete(Guid id)
|
|
{
|
|
if (db.Memberships.FindById(id) is { } membership)
|
|
ArchivedGroupWriteGuard.EnsureActive(db, membership.GroupId);
|
|
db.Memberships.Delete(id);
|
|
db.OnChange?.Invoke(nameof(GroupMembership), id.ToString(), "Delete", null);
|
|
}
|
|
}
|
|
|
|
public class ExamRepository(LiteDbContext db) : IExamRepository
|
|
{
|
|
public Exam? GetById(Guid id) => db.Exams.FindById(id);
|
|
public List<Exam> GetAll() => db.Exams.FindAll().OrderBy(e => e.Date).ToList();
|
|
public List<Exam> GetByGroup(Guid groupId) =>
|
|
db.Exams.Find(e => e.GroupId == groupId).OrderByDescending(e => e.Date).ToList();
|
|
public void Save(Exam e)
|
|
{
|
|
ArchivedGroupWriteGuard.EnsureActive(db, e.GroupId);
|
|
e.UpdatedAt = DateTime.UtcNow;
|
|
db.Exams.Upsert(e);
|
|
db.OnChange?.Invoke(nameof(Exam), e.Id.ToString(), "Save", e);
|
|
}
|
|
public void Delete(Guid id)
|
|
{
|
|
if (db.Exams.FindById(id) is { } exam)
|
|
ArchivedGroupWriteGuard.EnsureActive(db, exam.GroupId);
|
|
db.CascadeDeleteExam(id);
|
|
db.OnChange?.Invoke(nameof(Exam), id.ToString(), "Delete", null);
|
|
}
|
|
}
|
|
|
|
public class ExamResultRepository(LiteDbContext db) : IExamResultRepository
|
|
{
|
|
public List<ExamResult> GetByExam(Guid id) =>
|
|
db.ExamResults.Find(r => r.ExamId == id).ToList();
|
|
public List<ExamResult> GetByStudent(Guid id) =>
|
|
db.ExamResults.Find(r => r.StudentId == id).ToList();
|
|
public ExamResult? GetByExamAndStudent(Guid examId, Guid studentId) =>
|
|
db.ExamResults.FindOne(r => r.ExamId == examId && r.StudentId == studentId);
|
|
public void Save(ExamResult r)
|
|
{
|
|
if (db.Exams.FindById(r.ExamId) is { } exam)
|
|
ArchivedGroupWriteGuard.EnsureActive(db, exam.GroupId);
|
|
r.UpdatedAt = DateTime.UtcNow;
|
|
db.ExamResults.Upsert(r);
|
|
db.OnChange?.Invoke(nameof(ExamResult), r.Id.ToString(), "Save", r);
|
|
}
|
|
public void SaveMany(List<ExamResult> results)
|
|
{
|
|
foreach (var examId in results.Select(r => r.ExamId).Distinct())
|
|
if (db.Exams.FindById(examId) is { } exam)
|
|
ArchivedGroupWriteGuard.EnsureActive(db, exam.GroupId);
|
|
var now = DateTime.UtcNow;
|
|
foreach (var r in results) r.UpdatedAt = now;
|
|
db.ExamResults.Upsert(results);
|
|
foreach (var r in results)
|
|
db.OnChange?.Invoke(nameof(ExamResult), r.Id.ToString(), "Save", r);
|
|
}
|
|
}
|
|
|
|
public class GradingKeyTemplateRepository(LiteDbContext db) : IGradingKeyTemplateRepository
|
|
{
|
|
public List<GradingKeyTemplate> GetAll() =>
|
|
db.GradingKeyTemplates.FindAll().OrderBy(t => t.Name).ToList();
|
|
public List<GradingKeyTemplate> GetByGradingSystem(GradingSystem system) =>
|
|
db.GradingKeyTemplates.Find(t => t.GradingSystem == system).OrderBy(t => t.Name).ToList();
|
|
public GradingKeyTemplate? GetById(Guid id) => db.GradingKeyTemplates.FindById(id);
|
|
public void Save(GradingKeyTemplate t)
|
|
{
|
|
t.UpdatedAt = DateTime.UtcNow;
|
|
db.GradingKeyTemplates.Upsert(t);
|
|
db.OnChange?.Invoke(nameof(GradingKeyTemplate), t.Id.ToString(), "Save", t);
|
|
}
|
|
public void Delete(Guid id)
|
|
{
|
|
if (db.GradingKeyTemplates.FindById(id) is { } template)
|
|
db.MoveToTrash(nameof(GradingKeyTemplate), id, template, template.Name);
|
|
db.GradingKeyTemplates.Delete(id);
|
|
db.OnChange?.Invoke(nameof(GradingKeyTemplate), id.ToString(), "Delete", null);
|
|
}
|
|
|
|
public void Restore(Guid trashId)
|
|
{
|
|
if (db.RestoreFromTrash<GradingKeyTemplate>(trashId) is { } template) Save(template);
|
|
}
|
|
}
|
|
|
|
public class GradeRepository(LiteDbContext db) : IGradeRepository
|
|
{
|
|
public List<Grade> GetByStudentAndGroup(Guid sid, Guid gid) =>
|
|
db.Grades.Find(g => g.StudentId == sid && g.GroupId == gid).OrderBy(g => g.Date).ToList();
|
|
public List<Grade> GetByGroup(Guid id) =>
|
|
db.Grades.Find(g => g.GroupId == id).OrderBy(g => g.Date).ToList();
|
|
public void Save(Grade g)
|
|
{
|
|
ArchivedGroupWriteGuard.EnsureActive(db, g.GroupId);
|
|
db.Grades.Upsert(g);
|
|
db.OnChange?.Invoke(nameof(Grade), g.Id.ToString(), "Save", g);
|
|
}
|
|
public void Delete(Guid id)
|
|
{
|
|
if (db.Grades.FindById(id) is not { } grade) return;
|
|
ArchivedGroupWriteGuard.EnsureActive(db, grade.GroupId);
|
|
db.MoveToTrash(nameof(Grade), id, grade,
|
|
$"Note {grade.Value} ({grade.Date:dd.MM.yyyy})");
|
|
db.Grades.Delete(id);
|
|
db.OnChange?.Invoke(nameof(Grade), id.ToString(), "Delete", null);
|
|
}
|
|
|
|
public void Restore(Guid trashId)
|
|
{
|
|
if (db.RestoreFromTrash<Grade>(trashId) is { } grade) Save(grade);
|
|
}
|
|
}
|
|
|
|
public class GradingSchemeRepository(LiteDbContext db) : IGradingSchemeRepository
|
|
{
|
|
public GradingScheme? GetByGroup(Guid groupId) => db.GradingSchemes.FindOne(s => s.GroupId == groupId);
|
|
public GradingScheme? GetDefaultForType(GroupType type) =>
|
|
db.GradingSchemes.FindOne(s => s.GroupId == null && s.GroupType == type);
|
|
public void Save(GradingScheme s)
|
|
{
|
|
if (s.GroupId is Guid groupId) ArchivedGroupWriteGuard.EnsureActive(db, groupId);
|
|
s.UpdatedAt = DateTime.UtcNow;
|
|
db.GradingSchemes.Upsert(s);
|
|
db.OnChange?.Invoke(nameof(GradingScheme), s.Id.ToString(), "Save", s);
|
|
}
|
|
public void Delete(Guid id)
|
|
{
|
|
if (db.GradingSchemes.FindById(id)?.GroupId is Guid groupId)
|
|
ArchivedGroupWriteGuard.EnsureActive(db, groupId);
|
|
db.GradingSchemes.Delete(id);
|
|
db.OnChange?.Invoke(nameof(GradingScheme), id.ToString(), "Delete", null);
|
|
}
|
|
}
|
|
|
|
public class ReportGradeRepository(LiteDbContext db) : IReportGradeRepository
|
|
{
|
|
public List<ReportGrade> GetByGroup(Guid groupId) => db.ReportGrades.Find(r => r.GroupId == groupId).ToList();
|
|
public ReportGrade? GetByStudentGroupPeriod(Guid studentId, Guid groupId, string period) =>
|
|
db.ReportGrades.FindOne(r => r.StudentId == studentId && r.GroupId == groupId && r.Period == period);
|
|
public void Save(ReportGrade r)
|
|
{
|
|
ArchivedGroupWriteGuard.EnsureActive(db, r.GroupId);
|
|
r.UpdatedAt = DateTime.UtcNow;
|
|
db.ReportGrades.Upsert(r);
|
|
db.OnChange?.Invoke(nameof(ReportGrade), r.Id.ToString(), "Save", r);
|
|
}
|
|
public void Delete(Guid id)
|
|
{
|
|
if (db.ReportGrades.FindById(id) is { } grade)
|
|
ArchivedGroupWriteGuard.EnsureActive(db, grade.GroupId);
|
|
db.ReportGrades.Delete(id);
|
|
db.OnChange?.Invoke(nameof(ReportGrade), id.ToString(), "Delete", null);
|
|
}
|
|
}
|
|
|
|
public class UnitRepository(LiteDbContext db) : IUnitRepository
|
|
{
|
|
public Unit? GetById(Guid id) => db.Units.FindById(id);
|
|
public List<Unit> GetByGroup(Guid id) =>
|
|
db.Units.Find(u => u.GroupId == id).OrderBy(u => u.StartDate).ToList();
|
|
public void Save(Unit u)
|
|
{
|
|
ArchivedGroupWriteGuard.EnsureActive(db, u.GroupId);
|
|
// Persistenz-Invariante: auch Importe und künftig hinzukommende Aufrufer dürfen keine
|
|
// zweite laufende Einheit in derselben Lerngruppe hinterlassen. Der Einheiten-Dialog
|
|
// kündigt diese automatische Ablösung vorher sichtbar an.
|
|
if (u.Status == UnitStatus.Active)
|
|
{
|
|
foreach (var previous in db.Units.Find(x => x.GroupId == u.GroupId &&
|
|
x.Status == UnitStatus.Active && x.Id != u.Id))
|
|
{
|
|
previous.Status = UnitStatus.Completed;
|
|
previous.UpdatedAt = DateTime.UtcNow;
|
|
db.Units.Upsert(previous);
|
|
db.OnChange?.Invoke(nameof(Unit), previous.Id.ToString(), "Save", previous);
|
|
}
|
|
}
|
|
u.UpdatedAt = DateTime.UtcNow;
|
|
db.Units.Upsert(u);
|
|
db.OnChange?.Invoke(nameof(Unit), u.Id.ToString(), "Save", u);
|
|
}
|
|
public void Delete(Guid id)
|
|
{
|
|
if (db.Units.FindById(id) is { } unit)
|
|
ArchivedGroupWriteGuard.EnsureActive(db, unit.GroupId);
|
|
db.Units.Delete(id);
|
|
db.OnChange?.Invoke(nameof(Unit), id.ToString(), "Delete", null);
|
|
}
|
|
}
|
|
|
|
public class LessonRepository(LiteDbContext db) : ILessonRepository
|
|
{
|
|
public Lesson? GetById(Guid id) => db.Lessons.FindById(id);
|
|
public List<Lesson> GetByUnit(Guid id) =>
|
|
db.Lessons.Find(l => l.UnitId == id).OrderBy(l => l.Date).ThenBy(l => l.LessonNumber).ToList();
|
|
public List<Lesson> GetByGroupAndDate(Guid gid, DateOnly date) =>
|
|
db.Lessons.Find(l => l.GroupId == gid && l.Date == date).ToList();
|
|
public List<Lesson> GetByGroupAndRange(Guid gid, DateOnly from, DateOnly to) =>
|
|
db.Lessons.Find(l => l.GroupId == gid && l.Date >= from && l.Date <= to)
|
|
.OrderBy(l => l.Date).ToList();
|
|
public void Save(Lesson l)
|
|
{
|
|
ArchivedGroupWriteGuard.EnsureActive(db, l.GroupId);
|
|
l.UpdatedAt = DateTime.UtcNow;
|
|
db.Lessons.Upsert(l);
|
|
db.OnChange?.Invoke(nameof(Lesson), l.Id.ToString(), "Save", l);
|
|
}
|
|
public void Delete(Guid id)
|
|
{
|
|
if (db.Lessons.FindById(id) is { } lesson)
|
|
{
|
|
ArchivedGroupWriteGuard.EnsureActive(db, lesson.GroupId);
|
|
foreach (var attachment in lesson.Attachments) db.Attachments.Delete(attachment.StorageId);
|
|
}
|
|
db.Lessons.Delete(id);
|
|
db.OnChange?.Invoke(nameof(Lesson), id.ToString(), "Delete", null);
|
|
}
|
|
}
|
|
|
|
public class DocumentationRepository(LiteDbContext db) : IDocumentationRepository
|
|
{
|
|
// Filter auf AdditionalStudentIds bewusst im Speicher statt als LiteDB-Ausdruck — die Menge
|
|
// an Dokumentation pro Lehrkraft ist klein, und so gilt exakt dieselbe Regel wie Documentation.Concerns.
|
|
public List<Documentation> GetByStudent(Guid id) =>
|
|
db.Documentation.Find(d => !d.IsDeleted).Where(d => d.Concerns(id))
|
|
.OrderByDescending(d => d.Date).ToList();
|
|
public List<Documentation> GetByStudentAndType(Guid sid, DocumentationType type) =>
|
|
db.Documentation.Find(d => d.Type == type && !d.IsDeleted).Where(d => d.Concerns(sid))
|
|
.OrderByDescending(d => d.Date).ToList();
|
|
public List<Documentation> GetAll() =>
|
|
db.Documentation.Find(d => !d.IsDeleted).OrderByDescending(d => d.Date).ToList();
|
|
public void Save(Documentation d)
|
|
{
|
|
d.UpdatedAt = DateTime.UtcNow;
|
|
db.Documentation.Upsert(d);
|
|
db.OnChange?.Invoke(nameof(Documentation), d.Id.ToString(), "Save", d);
|
|
}
|
|
public void Delete(Guid id)
|
|
{
|
|
var doc = db.Documentation.FindById(id);
|
|
if (doc is null) return;
|
|
doc.IsDeleted = true;
|
|
doc.DeletedAt = DateTime.UtcNow;
|
|
db.Documentation.Update(doc);
|
|
// Weiches Löschen ist inhaltlich eine Änderung, kein Entfernen -> "Save", damit ein
|
|
// anwendendes Gerät den IsDeleted-Stand einfach übernimmt statt den Datensatz zu entfernen.
|
|
db.OnChange?.Invoke(nameof(Documentation), id.ToString(), "Save", doc);
|
|
}
|
|
public void HardDelete(Guid id)
|
|
{
|
|
db.CascadeHardDeleteDocumentation(id);
|
|
db.OnChange?.Invoke(nameof(Documentation), id.ToString(), "Delete", null);
|
|
}
|
|
}
|
|
|
|
public class VorgangRepository(LiteDbContext db) : IVorgangRepository
|
|
{
|
|
public List<Vorgang> GetAll() =>
|
|
db.Vorgaenge.Find(v => !v.IsDeleted).OrderByDescending(v => v.UpdatedAt).ToList();
|
|
public List<Vorgang> GetByStudent(Guid studentId) =>
|
|
GetAll().Where(v => v.StudentIds.Contains(studentId)).ToList();
|
|
public Vorgang? GetById(Guid id)
|
|
{
|
|
var vorgang = db.Vorgaenge.FindById(id);
|
|
return vorgang is null || vorgang.IsDeleted ? null : vorgang;
|
|
}
|
|
public void Save(Vorgang vorgang)
|
|
{
|
|
vorgang.UpdatedAt = DateTime.UtcNow;
|
|
db.Vorgaenge.Upsert(vorgang);
|
|
db.OnChange?.Invoke(nameof(Vorgang), vorgang.Id.ToString(), "Save", vorgang);
|
|
}
|
|
public void Delete(Guid id)
|
|
{
|
|
var vorgang = db.Vorgaenge.FindById(id);
|
|
if (vorgang is null) return;
|
|
vorgang.IsDeleted = true;
|
|
vorgang.DeletedAt = DateTime.UtcNow;
|
|
db.Vorgaenge.Update(vorgang);
|
|
// Weiches Löschen ist inhaltlich eine Änderung, kein Entfernen -> "Save", damit ein
|
|
// anwendendes Gerät den IsDeleted-Stand einfach übernimmt statt den Datensatz zu entfernen
|
|
// (gleiches Muster wie DocumentationRepository.Delete).
|
|
db.OnChange?.Invoke(nameof(Vorgang), id.ToString(), "Save", vorgang);
|
|
}
|
|
}
|
|
|
|
public class WorkTaskRepository(LiteDbContext db) : IWorkTaskRepository
|
|
{
|
|
public List<WorkTask> GetByStatus(WorkTaskStatus s) =>
|
|
db.Tasks.Find(t => t.Status == s).OrderBy(t => t.DueDate).ToList();
|
|
public List<WorkTask> GetAll() =>
|
|
db.Tasks.FindAll().OrderBy(t => t.Status).ThenBy(t => t.DueDate).ToList();
|
|
public List<WorkTask> GetByGroup(Guid groupId) =>
|
|
db.Tasks.Find(t => t.GroupId == groupId).OrderBy(t => t.Status).ThenBy(t => t.DueDate).ToList();
|
|
public void Save(WorkTask t)
|
|
{
|
|
t.UpdatedAt = DateTime.UtcNow;
|
|
db.Tasks.Upsert(t);
|
|
db.OnChange?.Invoke(nameof(WorkTask), t.Id.ToString(), "Save", t);
|
|
}
|
|
public void Delete(Guid id)
|
|
{
|
|
if (db.Tasks.FindById(id) is { } task)
|
|
db.MoveToTrash(nameof(WorkTask), id, task, task.Title);
|
|
db.Tasks.Delete(id);
|
|
db.OnChange?.Invoke(nameof(WorkTask), id.ToString(), "Delete", null);
|
|
}
|
|
|
|
public void Restore(Guid trashId)
|
|
{
|
|
if (db.RestoreFromTrash<WorkTask>(trashId) is { } task) Save(task);
|
|
}
|
|
}
|
|
|
|
public class TimeEntryRepository(LiteDbContext db) : ITimeEntryRepository
|
|
{
|
|
public List<TimeEntry> GetByDate(DateOnly date) =>
|
|
db.TimeEntries.Find(e => e.Date == date).OrderBy(e => e.StartTime).ToList();
|
|
public List<TimeEntry> GetByDateRange(DateOnly from, DateOnly to) =>
|
|
db.TimeEntries.Find(e => e.Date >= from && e.Date <= to).OrderBy(e => e.Date).ToList();
|
|
public List<TimeEntry> GetByTask(Guid id) =>
|
|
db.TimeEntries.Find(e => e.TaskId == id).ToList();
|
|
public void Save(TimeEntry e)
|
|
{
|
|
db.TimeEntries.Upsert(e);
|
|
db.OnChange?.Invoke(nameof(TimeEntry), e.Id.ToString(), "Save", e);
|
|
}
|
|
public void Delete(Guid id)
|
|
{
|
|
if (db.TimeEntries.FindById(id) is { } entry)
|
|
db.MoveToTrash(nameof(TimeEntry), id, entry,
|
|
$"{entry.DurationMinutes} Min. {entry.Category} ({entry.Date:dd.MM.yyyy})");
|
|
db.TimeEntries.Delete(id);
|
|
db.OnChange?.Invoke(nameof(TimeEntry), id.ToString(), "Delete", null);
|
|
}
|
|
|
|
public void Restore(Guid trashId)
|
|
{
|
|
if (db.RestoreFromTrash<TimeEntry>(trashId) is { } entry) Save(entry);
|
|
}
|
|
}
|
|
|
|
public class ParticipationSessionRepository(LiteDbContext db) : IParticipationSessionRepository
|
|
{
|
|
public List<ParticipationSession> GetByGroup(Guid groupId) =>
|
|
db.ParticipationSessions.Find(s => s.GroupId == groupId).OrderByDescending(s => s.Date).ToList();
|
|
public ParticipationSession? GetById(Guid id) => db.ParticipationSessions.FindById(id);
|
|
public List<ParticipationSession> GetAll() => db.ParticipationSessions.FindAll().ToList();
|
|
public void Save(ParticipationSession s)
|
|
{
|
|
ArchivedGroupWriteGuard.EnsureActive(db, s.GroupId);
|
|
s.UpdatedAt = DateTime.UtcNow;
|
|
db.ParticipationSessions.Upsert(s);
|
|
db.OnChange?.Invoke(nameof(ParticipationSession), s.Id.ToString(), "Save", s);
|
|
}
|
|
public void Delete(Guid id)
|
|
{
|
|
if (db.ParticipationSessions.FindById(id) is { } session)
|
|
ArchivedGroupWriteGuard.EnsureActive(db, session.GroupId);
|
|
db.CascadeDeleteParticipationSession(id);
|
|
db.OnChange?.Invoke(nameof(ParticipationSession), id.ToString(), "Delete", null);
|
|
}
|
|
}
|
|
|
|
public class ParticipationRepository(LiteDbContext db) : IParticipationRepository
|
|
{
|
|
public List<ParticipationEntry> GetBySession(Guid sessionId) =>
|
|
db.ParticipationEntries.Find(e => e.SessionId == sessionId).ToList();
|
|
public List<ParticipationEntry> GetByStudent(Guid studentId) =>
|
|
db.ParticipationEntries.Find(e => e.StudentId == studentId).ToList();
|
|
public ParticipationEntry? GetBySessionAndStudent(Guid sessionId, Guid studentId) =>
|
|
db.ParticipationEntries.FindOne(e => e.SessionId == sessionId && e.StudentId == studentId);
|
|
public void Save(ParticipationEntry e)
|
|
{
|
|
if (db.ParticipationSessions.FindById(e.SessionId) is { } session)
|
|
ArchivedGroupWriteGuard.EnsureActive(db, session.GroupId);
|
|
e.UpdatedAt = DateTime.UtcNow;
|
|
db.ParticipationEntries.Upsert(e);
|
|
db.OnChange?.Invoke(nameof(ParticipationEntry), e.Id.ToString(), "Save", e);
|
|
}
|
|
public void SaveMany(List<ParticipationEntry> entries)
|
|
{
|
|
foreach (var sessionId in entries.Select(e => e.SessionId).Distinct())
|
|
if (db.ParticipationSessions.FindById(sessionId) is { } session)
|
|
ArchivedGroupWriteGuard.EnsureActive(db, session.GroupId);
|
|
var now = DateTime.UtcNow;
|
|
foreach (var e in entries) e.UpdatedAt = now;
|
|
db.ParticipationEntries.Upsert(entries);
|
|
foreach (var e in entries)
|
|
db.OnChange?.Invoke(nameof(ParticipationEntry), e.Id.ToString(), "Save", e);
|
|
}
|
|
public void DeleteBySession(Guid sessionId)
|
|
{
|
|
if (db.ParticipationSessions.FindById(sessionId) is { } session)
|
|
ArchivedGroupWriteGuard.EnsureActive(db, session.GroupId);
|
|
foreach (var e in db.ParticipationEntries.Find(e => e.SessionId == sessionId).ToList())
|
|
{
|
|
db.ParticipationEntries.Delete(e.Id);
|
|
db.OnChange?.Invoke(nameof(ParticipationEntry), e.Id.ToString(), "Delete", null);
|
|
}
|
|
}
|
|
}
|
|
|
|
public class ParticipationAspectRepository(LiteDbContext db) : IParticipationAspectRepository
|
|
{
|
|
public List<ParticipationAspect> GetDefaults() =>
|
|
db.ParticipationAspects.Find(a => a.GroupId == null && a.IsActive).OrderBy(a => a.SortOrder).ToList();
|
|
public List<ParticipationAspect> GetByGroup(Guid groupId) =>
|
|
db.ParticipationAspects.Find(a => a.GroupId == groupId && a.IsActive).OrderBy(a => a.SortOrder).ToList();
|
|
public List<ParticipationAspect> GetAllByGroup(Guid groupId) =>
|
|
db.ParticipationAspects.Find(a => a.GroupId == groupId).OrderBy(a => a.SortOrder).ToList();
|
|
public void Save(ParticipationAspect a)
|
|
{
|
|
if (a.GroupId is Guid groupId) ArchivedGroupWriteGuard.EnsureActive(db, groupId);
|
|
a.Key = a.Key.Trim();
|
|
a.Label = a.Label.Trim();
|
|
if (a.Key.Length == 0) throw new ArgumentException("Der Schlüssel darf nicht leer sein.");
|
|
if (a.Label.Length == 0) throw new ArgumentException("Die Bezeichnung darf nicht leer sein.");
|
|
// Ein Schlüssel muss innerhalb dessen, was für eine Gruppe tatsächlich gilt, eindeutig
|
|
// sein — das sind die globalen Standardaspekte UND die gruppenspezifischen zusammen
|
|
// (siehe ParticipationTabViewModel.LoadAspects, das beide konkateniert). Sonst entstünde
|
|
// in der Bewertungsübersicht eine mehrdeutige Spalte mit identischem Schlüssel.
|
|
var relevant = db.ParticipationAspects.Find(x => x.GroupId == null || x.GroupId == a.GroupId);
|
|
var duplicate = relevant.FirstOrDefault(x =>
|
|
x.Id != a.Id && string.Equals(x.Key, a.Key, StringComparison.OrdinalIgnoreCase));
|
|
if (duplicate is not null)
|
|
throw new InvalidOperationException("Ein Aspekt mit diesem Schlüssel existiert für diese Gruppe bereits.");
|
|
a.UpdatedAt = DateTime.UtcNow;
|
|
db.ParticipationAspects.Upsert(a);
|
|
db.OnChange?.Invoke(nameof(ParticipationAspect), a.Id.ToString(), "Save", a);
|
|
}
|
|
public void Delete(Guid id)
|
|
{
|
|
if (db.ParticipationAspects.FindById(id)?.GroupId is Guid groupId)
|
|
ArchivedGroupWriteGuard.EnsureActive(db, groupId);
|
|
db.ParticipationAspects.Delete(id);
|
|
db.OnChange?.Invoke(nameof(ParticipationAspect), id.ToString(), "Delete", null);
|
|
}
|
|
}
|
|
|
|
public class ParticipationSectionRepository(LiteDbContext db) : IParticipationSectionRepository
|
|
{
|
|
public List<ParticipationSection> GetByGroup(Guid groupId) =>
|
|
db.ParticipationSections.Find(s => s.GroupId == groupId).OrderBy(s => s.StartDate).ToList();
|
|
public void Save(ParticipationSection s)
|
|
{
|
|
ArchivedGroupWriteGuard.EnsureActive(db, s.GroupId);
|
|
db.ParticipationSections.Upsert(s);
|
|
db.OnChange?.Invoke(nameof(ParticipationSection), s.Id.ToString(), "Save", s);
|
|
}
|
|
public void Delete(Guid id)
|
|
{
|
|
if (db.ParticipationSections.FindById(id) is { } section)
|
|
ArchivedGroupWriteGuard.EnsureActive(db, section.GroupId);
|
|
db.ParticipationSections.Delete(id);
|
|
db.OnChange?.Invoke(nameof(ParticipationSection), id.ToString(), "Delete", null);
|
|
}
|
|
}
|
|
|
|
public class SubjectRepository(LiteDbContext db) : ISubjectRepository
|
|
{
|
|
public List<Subject> GetAll() => db.Subjects.FindAll().OrderBy(s => s.Name).ToList();
|
|
public Subject? GetById(Guid id) => db.Subjects.FindById(id);
|
|
public Subject? GetByName(string name) => db.Subjects.FindAll().FirstOrDefault(s =>
|
|
string.Equals(s.Name.Trim(), name.Trim(), StringComparison.OrdinalIgnoreCase));
|
|
public void Save(Subject s)
|
|
{
|
|
s.Name = s.Name.Trim();
|
|
s.ShortName = s.ShortName.Trim();
|
|
if (s.Name.Length == 0) throw new ArgumentException("Der Fachname darf nicht leer sein.");
|
|
var duplicate = GetByName(s.Name);
|
|
if (duplicate is not null && duplicate.Id != s.Id)
|
|
throw new InvalidOperationException("Ein Fach mit diesem Namen existiert bereits.");
|
|
s.UpdatedAt = DateTime.UtcNow;
|
|
db.Subjects.Upsert(s);
|
|
db.OnChange?.Invoke(nameof(Subject), s.Id.ToString(), "Save", s);
|
|
}
|
|
public void Delete(Guid id)
|
|
{
|
|
if (db.Groups.Exists(g => g.SubjectId == id) || db.CompetencyDomains.Exists(d => d.SubjectId == id)
|
|
|| db.CompetencyCatalogVersions.Exists(v => v.SubjectId == id))
|
|
throw new InvalidOperationException("Das Fach wird noch von einer Lerngruppe, einem Kompetenzkatalog oder einer Bildungsplan-Version verwendet.");
|
|
db.Subjects.Delete(id);
|
|
db.OnChange?.Invoke(nameof(Subject), id.ToString(), "Delete", null);
|
|
}
|
|
}
|
|
|
|
public class ShorthandCodeRepository(LiteDbContext db) : IShorthandCodeRepository
|
|
{
|
|
public List<ShorthandCode> GetAll() => db.ShorthandCodes.FindAll().OrderBy(c => c.Code).ToList();
|
|
public void Save(ShorthandCode c)
|
|
{
|
|
c.Code = c.Code.Trim();
|
|
c.Label = c.Label.Trim();
|
|
if (c.Code.Length == 0) throw new ArgumentException("Das Kürzel darf nicht leer sein.");
|
|
var duplicate = db.ShorthandCodes.FindAll()
|
|
.FirstOrDefault(x => string.Equals(x.Code, c.Code, StringComparison.OrdinalIgnoreCase));
|
|
if (duplicate is not null && duplicate.Id != c.Id)
|
|
throw new InvalidOperationException("Ein Kürzel mit diesem Code existiert bereits.");
|
|
c.UpdatedAt = DateTime.UtcNow;
|
|
db.ShorthandCodes.Upsert(c);
|
|
db.OnChange?.Invoke(nameof(ShorthandCode), c.Id.ToString(), "Save", c);
|
|
}
|
|
public void Delete(Guid id)
|
|
{
|
|
db.ShorthandCodes.Delete(id);
|
|
db.OnChange?.Invoke(nameof(ShorthandCode), id.ToString(), "Delete", null);
|
|
}
|
|
}
|
|
|
|
public class AlternativeLessonPathRepository(LiteDbContext db) : IAlternativeLessonPathRepository
|
|
{
|
|
public List<AlternativeLessonPath> GetAll() => db.AlternativeLessonPaths.FindAll().OrderBy(p => p.Name).ToList();
|
|
public AlternativeLessonPath? GetById(Guid id) => db.AlternativeLessonPaths.FindById(id);
|
|
public void Save(AlternativeLessonPath p)
|
|
{
|
|
p.Name = p.Name.Trim();
|
|
p.Description = string.IsNullOrWhiteSpace(p.Description) ? null : p.Description.Trim();
|
|
if (p.Name.Length == 0) throw new ArgumentException("Der Name darf nicht leer sein.");
|
|
var duplicate = db.AlternativeLessonPaths.FindAll()
|
|
.FirstOrDefault(x => string.Equals(x.Name, p.Name, StringComparison.OrdinalIgnoreCase));
|
|
if (duplicate is not null && duplicate.Id != p.Id)
|
|
throw new InvalidOperationException("Ein alternativer Ablauf mit diesem Namen existiert bereits.");
|
|
p.UpdatedAt = DateTime.UtcNow;
|
|
db.AlternativeLessonPaths.Upsert(p);
|
|
db.OnChange?.Invoke(nameof(AlternativeLessonPath), p.Id.ToString(), "Save", p);
|
|
}
|
|
public void Delete(Guid id)
|
|
{
|
|
db.AlternativeLessonPaths.Delete(id);
|
|
db.OnChange?.Invoke(nameof(AlternativeLessonPath), id.ToString(), "Delete", null);
|
|
}
|
|
}
|
|
|
|
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)
|
|
{
|
|
ArchivedGroupWriteGuard.EnsureActive(db, slot.GroupId);
|
|
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);
|
|
db.OnChange?.Invoke(nameof(TimetableSlot), slot.Id.ToString(), "Save", slot);
|
|
}
|
|
public void Delete(Guid id)
|
|
{
|
|
if (db.TimetableSlots.FindById(id) is { } slot)
|
|
ArchivedGroupWriteGuard.EnsureActive(db, slot.GroupId);
|
|
db.TimetableSlots.Delete(id);
|
|
db.OnChange?.Invoke(nameof(TimetableSlot), id.ToString(), "Delete", null);
|
|
}
|
|
}
|
|
|
|
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);
|
|
db.OnChange?.Invoke(nameof(SchoolHoliday), holiday.Id.ToString(), "Save", holiday);
|
|
}
|
|
public void Delete(Guid id)
|
|
{
|
|
db.SchoolHolidays.Delete(id);
|
|
db.OnChange?.Invoke(nameof(SchoolHoliday), id.ToString(), "Delete", null);
|
|
}
|
|
}
|
|
|
|
public class SupervisionDutyRepository(LiteDbContext db) : ISupervisionDutyRepository
|
|
{
|
|
public List<SupervisionDuty> GetAll() =>
|
|
db.SupervisionDuties.FindAll().OrderBy(d => d.Weekday).ThenBy(d => d.AfterPeriod).ToList();
|
|
public void Save(SupervisionDuty duty)
|
|
{
|
|
var occupied = db.SupervisionDuties.FindAll()
|
|
.FirstOrDefault(d => d.Weekday == duty.Weekday && d.AfterPeriod == duty.AfterPeriod);
|
|
if (occupied is not null && occupied.Id != duty.Id)
|
|
throw new InvalidOperationException("Für diese Pause ist bereits eine Aufsicht eingetragen.");
|
|
db.SupervisionDuties.Upsert(duty);
|
|
db.OnChange?.Invoke(nameof(SupervisionDuty), duty.Id.ToString(), "Save", duty);
|
|
}
|
|
public void Delete(Guid id)
|
|
{
|
|
db.SupervisionDuties.Delete(id);
|
|
db.OnChange?.Invoke(nameof(SupervisionDuty), id.ToString(), "Delete", null);
|
|
}
|
|
}
|
|
|
|
public class SubstitutionEntryRepository(LiteDbContext db) : ISubstitutionEntryRepository
|
|
{
|
|
public List<SubstitutionEntry> GetAll() => db.SubstitutionEntries.FindAll().OrderBy(e => e.Date).ToList();
|
|
public List<SubstitutionEntry> GetByDate(DateOnly date) =>
|
|
db.SubstitutionEntries.Find(e => e.Date == date).ToList();
|
|
public SubstitutionEntry? GetByExternalId(string externalId) =>
|
|
db.SubstitutionEntries.FindOne(e => e.ExternalId == externalId);
|
|
public void Save(SubstitutionEntry entry)
|
|
{
|
|
db.SubstitutionEntries.Upsert(entry);
|
|
db.OnChange?.Invoke(nameof(SubstitutionEntry), entry.Id.ToString(), "Save", entry);
|
|
}
|
|
public void Delete(Guid id)
|
|
{
|
|
db.SubstitutionEntries.Delete(id);
|
|
db.OnChange?.Invoke(nameof(SubstitutionEntry), id.ToString(), "Delete", null);
|
|
}
|
|
}
|
|
|
|
// Bewusst kein db.OnChange hier (anders als sonst überall): UntisSnapshotEntry ist eine rein
|
|
// lokale, hochfrequente Abgleich-Zwischenablage (jeder Poll aktualisiert alle Zeilen), deren
|
|
// Sync nur Rauschen erzeugen würde. UntisSlotMapping ist an die pro Gerät hinterlegte
|
|
// WebUntis-URL gebunden (WebUntisSettingsService, wie SyncSettingsService nicht synchronisiert)
|
|
// und deshalb ebenfalls sinnvollerweise lokal — jedes Gerät bestätigt seine Zuordnung einmal
|
|
// selbst über den Review-Dialog.
|
|
public class UntisSnapshotRepository(LiteDbContext db) : IUntisSnapshotRepository
|
|
{
|
|
public List<UntisSnapshotEntry> GetAll() => db.UntisSnapshotEntries.FindAll().ToList();
|
|
public void Save(UntisSnapshotEntry entry) => db.UntisSnapshotEntries.Upsert(entry);
|
|
public void Delete(Guid id) => db.UntisSnapshotEntries.Delete(id);
|
|
}
|
|
|
|
public class UntisSlotMappingRepository(LiteDbContext db) : IUntisSlotMappingRepository
|
|
{
|
|
public List<UntisSlotMapping> GetAll() => db.UntisSlotMappings.FindAll().ToList();
|
|
public void Save(UntisSlotMapping mapping) => db.UntisSlotMappings.Upsert(mapping);
|
|
public void Delete(Guid id) => db.UntisSlotMappings.Delete(id);
|
|
}
|
|
|
|
// Wie UntisSnapshotEntry rein lokal: Der Jahresplan ist ein wiederherstellbarer Cache eines
|
|
// externen Feeds. db.OnChange würde bei jedem vollständigen Abruf hunderte Sync-Ereignisse erzeugen.
|
|
public class AnnualPlanEventRepository(LiteDbContext db) : IAnnualPlanEventRepository
|
|
{
|
|
public List<AnnualPlanEvent> GetAll() =>
|
|
db.AnnualPlanEvents.FindAll().OrderBy(e => e.StartDate).ThenBy(e => e.StartTime).ToList();
|
|
|
|
public List<AnnualPlanEvent> GetByRange(DateOnly from, DateOnly to) =>
|
|
db.AnnualPlanEvents.Find(e => e.StartDate <= to && e.EndDate >= from)
|
|
.OrderBy(e => e.StartDate).ThenBy(e => e.StartTime).ToList();
|
|
|
|
public AnnualPlanEvent? GetByExternalId(string externalId) =>
|
|
db.AnnualPlanEvents.FindOne(e => e.ExternalId == externalId);
|
|
|
|
public void Save(AnnualPlanEvent entry) => db.AnnualPlanEvents.Upsert(entry);
|
|
public void Delete(Guid id) => db.AnnualPlanEvents.Delete(id);
|
|
}
|
|
|
|
// Wie UntisSnapshotEntry/AnnualPlanEvent rein lokal, bewusst kein db.OnChange: Fehlzeiten- und
|
|
// Klassenbuch-Cache fürs Klassenlehrer-Feature (siehe TODO.md) wachsen über ein Schuljahr auf viele
|
|
// hundert Zeilen an - über Sync würde das nur Rauschen erzeugen, und jedes Gerät ruft WebUntis
|
|
// ohnehin selbst ab (siehe UntisReportCacheService).
|
|
public class UntisAbsenceCacheRepository(LiteDbContext db) : IUntisAbsenceCacheRepository
|
|
{
|
|
public List<UntisAbsenceCacheEntry> GetByClassAndRange(string className, int startDate, int endDate) =>
|
|
db.UntisAbsenceCache
|
|
.Find(e => e.ClassName == className && e.Date >= startDate && e.Date <= endDate)
|
|
.ToList();
|
|
|
|
public void ReplaceRange(string className, int startDate, int endDate, IEnumerable<UntisAbsenceCacheEntry> entries)
|
|
{
|
|
db.UntisAbsenceCache.DeleteMany(e => e.ClassName == className && e.Date >= startDate && e.Date <= endDate);
|
|
db.UntisAbsenceCache.InsertBulk(entries);
|
|
}
|
|
|
|
public void InsertRange(IEnumerable<UntisAbsenceCacheEntry> entries) => db.UntisAbsenceCache.InsertBulk(entries);
|
|
}
|
|
|
|
public class UntisClassRegisterCacheRepository(LiteDbContext db) : IUntisClassRegisterCacheRepository
|
|
{
|
|
public List<UntisClassRegisterCacheEntry> GetByClassAndRange(string className, int startDate, int endDate) =>
|
|
db.UntisClassRegisterCache
|
|
.Find(e => e.ClassName == className && e.Date >= startDate && e.Date <= endDate)
|
|
.ToList();
|
|
|
|
public void ReplaceRange(string className, int startDate, int endDate, IEnumerable<UntisClassRegisterCacheEntry> entries)
|
|
{
|
|
db.UntisClassRegisterCache.DeleteMany(e => e.ClassName == className && e.Date >= startDate && e.Date <= endDate);
|
|
db.UntisClassRegisterCache.InsertBulk(entries);
|
|
}
|
|
|
|
public void InsertRange(IEnumerable<UntisClassRegisterCacheEntry> entries) => db.UntisClassRegisterCache.InsertBulk(entries);
|
|
}
|
|
|
|
// Gleiches "bewusst kein db.OnChange"-Prinzip wie UntisAbsenceCacheRepository oben, auch wenn der
|
|
// Roster selbst klein bleibt (aktuelle Klassenliste, keine Historie) - jedes Gerät ruft ihn ohnehin
|
|
// selbst ab.
|
|
public class UntisStudentRosterCacheRepository(LiteDbContext db) : IUntisStudentRosterCacheRepository
|
|
{
|
|
public List<UntisStudentRosterCacheEntry> GetByClass(string className) =>
|
|
db.UntisStudentRosterCache.Find(e => e.ClassName == className).ToList();
|
|
|
|
public void ReplaceAll(string className, IEnumerable<UntisStudentRosterCacheEntry> entries)
|
|
{
|
|
db.UntisStudentRosterCache.DeleteMany(e => e.ClassName == className);
|
|
db.UntisStudentRosterCache.InsertBulk(entries);
|
|
}
|
|
}
|
|
|
|
public class UntisCacheFetchStateRepository(LiteDbContext db) : IUntisCacheFetchStateRepository
|
|
{
|
|
public UntisCacheFetchState? Get(string className, UntisCacheKind kind) =>
|
|
db.UntisCacheFetchStates.FindOne(s => s.ClassName == className && s.Kind == kind);
|
|
|
|
public void Save(UntisCacheFetchState state) => db.UntisCacheFetchStates.Upsert(state);
|
|
}
|
|
|
|
public class DashboardWarningSnoozeRepository(LiteDbContext db) : IDashboardWarningSnoozeRepository
|
|
{
|
|
public DashboardWarningSnooze? Get(Guid studentId, string warningTag) =>
|
|
db.DashboardWarningSnoozes.FindOne(s => s.StudentId == studentId && s.WarningTag == warningTag);
|
|
public List<DashboardWarningSnooze> GetAll() => db.DashboardWarningSnoozes.FindAll().ToList();
|
|
public void Save(DashboardWarningSnooze snooze)
|
|
{
|
|
snooze.Id = DashboardWarningSnooze.IdFor(snooze.StudentId, snooze.WarningTag);
|
|
db.DashboardWarningSnoozes.Upsert(snooze);
|
|
db.OnChange?.Invoke(nameof(DashboardWarningSnooze), snooze.Id.ToString(), "Save", snooze);
|
|
}
|
|
}
|
|
|
|
public class UntisHubJobStateRepository(LiteDbContext db) : IUntisHubJobStateRepository
|
|
{
|
|
public UntisHubJobState? Get(UntisHubJobKind kind, Guid? groupId) =>
|
|
db.UntisHubJobStates.FindOne(s => s.Kind == kind && s.GroupId == groupId);
|
|
|
|
public List<UntisHubJobState> GetAll() => db.UntisHubJobStates.FindAll().ToList();
|
|
|
|
public void Save(UntisHubJobState state)
|
|
{
|
|
db.UntisHubJobStates.Upsert(state);
|
|
db.OnChange?.Invoke(nameof(UntisHubJobState), state.Id.ToString(), "Save", state);
|
|
}
|
|
}
|
|
|
|
public class CompetencyDomainRepository(LiteDbContext db) : ICompetencyDomainRepository
|
|
{
|
|
public List<CompetencyDomain> GetBySubjectAndGrade(Guid subjectId, int gradeLevel, Guid versionId) =>
|
|
db.CompetencyDomains
|
|
.Find(d => d.SubjectId == subjectId && d.GradeLevel == gradeLevel && d.VersionId == versionId)
|
|
.OrderBy(d => d.SortOrder)
|
|
.ToList();
|
|
public List<CompetencyDomain> GetBySubjectAndVersion(Guid subjectId, Guid versionId) =>
|
|
db.CompetencyDomains
|
|
.Find(d => d.SubjectId == subjectId && d.VersionId == versionId)
|
|
.OrderBy(d => d.GradeLevel).ThenBy(d => d.SortOrder)
|
|
.ToList();
|
|
public List<CompetencyDomain> GetAll() => db.CompetencyDomains.FindAll().ToList();
|
|
public CompetencyDomain? GetById(Guid id) => db.CompetencyDomains.FindById(id);
|
|
public void Save(CompetencyDomain d)
|
|
{
|
|
d.UpdatedAt = DateTime.UtcNow;
|
|
db.CompetencyDomains.Upsert(d);
|
|
db.OnChange?.Invoke(nameof(CompetencyDomain), d.Id.ToString(), "Save", d);
|
|
}
|
|
public void Delete(Guid id)
|
|
{
|
|
db.CompetencyDomains.Delete(id);
|
|
db.OnChange?.Invoke(nameof(CompetencyDomain), id.ToString(), "Delete", null);
|
|
}
|
|
public void DeleteBySubjectGradeAndVersion(Guid subjectId, int gradeLevel, Guid versionId)
|
|
{
|
|
foreach (var d in db.CompetencyDomains
|
|
.Find(d => d.SubjectId == subjectId && d.GradeLevel == gradeLevel && d.VersionId == versionId)
|
|
.ToList())
|
|
{
|
|
db.CompetencyDomains.Delete(d.Id);
|
|
db.OnChange?.Invoke(nameof(CompetencyDomain), d.Id.ToString(), "Delete", null);
|
|
}
|
|
}
|
|
public void ReplaceForSubjectGradeAndVersion(Guid subjectId, int gradeLevel, Guid versionId, List<CompetencyDomain> domains)
|
|
{
|
|
db.ExecuteInTransaction(() =>
|
|
{
|
|
foreach (var domain in db.CompetencyDomains
|
|
.Find(d => d.SubjectId == subjectId && d.GradeLevel == gradeLevel && d.VersionId == versionId).ToList())
|
|
{
|
|
db.CompetencyDomains.Delete(domain.Id);
|
|
db.OnChange?.Invoke(nameof(CompetencyDomain), domain.Id.ToString(), "Delete", null);
|
|
}
|
|
var now = DateTime.UtcNow;
|
|
foreach (var domain in domains)
|
|
{
|
|
domain.SubjectId = subjectId;
|
|
domain.GradeLevel = gradeLevel;
|
|
domain.VersionId = versionId;
|
|
domain.UpdatedAt = now;
|
|
db.CompetencyDomains.Upsert(domain);
|
|
db.OnChange?.Invoke(nameof(CompetencyDomain), domain.Id.ToString(), "Save", domain);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
public class CompetencyCatalogVersionRepository(LiteDbContext db) : ICompetencyCatalogVersionRepository
|
|
{
|
|
public List<CompetencyCatalogVersion> GetBySubject(Guid subjectId) =>
|
|
db.CompetencyCatalogVersions
|
|
.Find(v => v.SubjectId == subjectId)
|
|
.OrderBy(v => v.SortOrder)
|
|
.ToList();
|
|
public CompetencyCatalogVersion? GetById(Guid id) => db.CompetencyCatalogVersions.FindById(id);
|
|
public CompetencyCatalogVersion GetOrCreateDefault(Guid subjectId)
|
|
{
|
|
var existing = GetBySubject(subjectId).FirstOrDefault(v => v.IsDefault);
|
|
if (existing is not null) return existing;
|
|
var version = new CompetencyCatalogVersion
|
|
{
|
|
SubjectId = subjectId, Name = "Bestehender Kompetenzkatalog", IsDefault = true,
|
|
};
|
|
Save(version);
|
|
return version;
|
|
}
|
|
public void Save(CompetencyCatalogVersion v)
|
|
{
|
|
v.Name = v.Name.Trim();
|
|
if (v.Name.Length == 0) throw new ArgumentException("Der Name der Version darf nicht leer sein.");
|
|
if (v.IsDefault)
|
|
{
|
|
// Höchstens eine Standardversion je Fach - beim Setzen alle anderen des Fachs zurücknehmen.
|
|
foreach (var other in db.CompetencyCatalogVersions.Find(o => o.SubjectId == v.SubjectId && o.Id != v.Id && o.IsDefault).ToList())
|
|
{
|
|
other.IsDefault = false;
|
|
other.UpdatedAt = DateTime.UtcNow;
|
|
db.CompetencyCatalogVersions.Upsert(other);
|
|
db.OnChange?.Invoke(nameof(CompetencyCatalogVersion), other.Id.ToString(), "Save", other);
|
|
}
|
|
}
|
|
v.UpdatedAt = DateTime.UtcNow;
|
|
db.CompetencyCatalogVersions.Upsert(v);
|
|
db.OnChange?.Invoke(nameof(CompetencyCatalogVersion), v.Id.ToString(), "Save", v);
|
|
}
|
|
public void Delete(Guid id)
|
|
{
|
|
var version = db.CompetencyCatalogVersions.FindById(id);
|
|
if (version is null) return;
|
|
if (version.IsDefault)
|
|
throw new InvalidOperationException("Die Standardversion eines Fachs kann nicht gelöscht werden. Erst eine andere Version als Standard setzen.");
|
|
if (db.CompetencyDomains.Exists(d => d.VersionId == id) || db.Groups.Exists(g => g.CompetencyCatalogVersionId == id))
|
|
throw new InvalidOperationException("Die Version wird noch von einem Kompetenzkatalog oder einem Kurs verwendet.");
|
|
db.CompetencyCatalogVersions.Delete(id);
|
|
db.OnChange?.Invoke(nameof(CompetencyCatalogVersion), id.ToString(), "Delete", null);
|
|
}
|
|
}
|
|
|
|
/// <summary>Papierkorb (14.3) — bewusst nicht sync-fähig: TrashedItem wird nie über
|
|
/// db.OnChange gemeldet, bleibt also rein lokal auf dem Gerät, auf dem gelöscht wurde. Ein
|
|
/// "Fehlklick sofort rückgängig machen"-Werkzeug, kein geräteübergreifendes Archiv.</summary>
|
|
public class TrashRepository(LiteDbContext db) : ITrashRepository
|
|
{
|
|
public List<TrashedItem> GetAll() =>
|
|
db.TrashedItems.FindAll().OrderByDescending(t => t.DeletedAt).ToList();
|
|
|
|
public void PurgeOlderThan(DateTime cutoffUtc)
|
|
{
|
|
foreach (var item in db.TrashedItems.Find(t => t.DeletedAt < cutoffUtc).ToList())
|
|
db.TrashedItems.Delete(item.Id);
|
|
}
|
|
}
|