KI-gestützte Planungsunterstützung (4.5.9) + Kompetenzkatalog-Import (8.1.2)

KI-Unterstützung: neuer Einstellungen-Tab (Anmeldung, Guthaben) und Button im
Planungs-Tab, der Einheiten+Stunden als JSON an ein neues PHP-Backend (ai-backend/)
sendet und die Antwort als prüfbare Vorschlagsliste zurückbringt. Provider-Aufruf,
Guthabenverwaltung und Abrechnung nach echten Token-Kosten laufen serverseitig, der
Desktop-Client sieht nie einen LLM-API-Key. Zentral abgesichert: eine von der KI
zurückgegebene Stunden-Id, die zu keiner echten Lesson der Einheit passt, wird nie
als Update übernommen, sondern immer als neue Stunde behandelt.

Kompetenzkatalog-Import (8.1.2): JSON-Export/Import für Kompetenzkataloge.
This commit is contained in:
2026-08-16 01:31:14 +02:00
parent 2b4fda7bb3
commit 8495e1b8d0
40 changed files with 2326 additions and 51 deletions
+36 -5
View File
@@ -1,11 +1,33 @@
using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Models;
using LehrerApp.Desktop.Services;
namespace LehrerApp.Desktop.Tests;
// Einfache In-Memory-Fakes der Repository-Schnittstellen, damit ViewModel-Tests ohne echte
// LiteDB-Anbindung laufen. Bewusst schlank gehalten: nur was die getesteten ViewModels brauchen.
public static class TestSupport
{
/// Für Tests, die eine echte AiSettingsService-Instanz brauchen (dateibasiert wie
/// PeriodScheduleService & Co.) — eigenes Temp-Verzeichnis je Aufruf, damit Tests sich nicht
/// gegenseitig über dieselbe ai-settings.json/ai-token.key stören.
public static AiSettingsService BuildAiSettingsService()
{
var tempPath = Path.Combine(Path.GetTempPath(), $"lehrerapp-aisettings-tests-{Guid.NewGuid():N}");
Directory.CreateDirectory(tempPath);
return new AiSettingsService(tempPath);
}
/// Für Tests, die nur einen validen AiPlanningService zum Durchreichen brauchen (z.B. weil
/// SettingsViewModel/PlanningTabViewModel ihn im Konstruktor verlangen), nicht seine eigentliche
/// Funktionalität testen — leere Fakes genügen, es wird kein echter HTTP-Aufruf ausgelöst,
/// solange AiSettingsService.IsLoggedIn false ist.
public static AiPlanningService BuildAiPlanningService() => new(
new HttpClient(), new FakeLessons(), new FakeGroups([]), new FakeSubjects([]),
new FakeCompetencyDomains(), new FakeAlternativeLessonPaths([]));
}
public class FakeStudents(List<Student> all) : IStudentRepository
{
private readonly Dictionary<Guid, StudentReferenceSummary> _references = [];
@@ -209,11 +231,20 @@ public class FakeSubjects(List<Subject> all) : ISubjectRepository
public class FakeCompetencyDomains : ICompetencyDomainRepository
{
public List<CompetencyDomain> GetBySubjectAndGrade(Guid subjectId, int gradeLevel) => [];
public CompetencyDomain? GetById(Guid id) => null;
public void Save(CompetencyDomain domain) { }
public void Delete(Guid id) { }
public void DeleteBySubjectAndGrade(Guid subjectId, int gradeLevel) { }
private readonly List<CompetencyDomain> _all = [];
public void Add(CompetencyDomain d) => _all.Add(d);
public List<CompetencyDomain> GetBySubjectAndGrade(Guid subjectId, int gradeLevel) =>
_all.Where(d => d.SubjectId == subjectId && d.GradeLevel == gradeLevel).ToList();
public CompetencyDomain? GetById(Guid id) => _all.FirstOrDefault(d => d.Id == id);
public void Save(CompetencyDomain domain) { _all.RemoveAll(d => d.Id == domain.Id); _all.Add(domain); }
public void Delete(Guid id) => _all.RemoveAll(d => d.Id == id);
public void DeleteBySubjectAndGrade(Guid subjectId, int gradeLevel) =>
_all.RemoveAll(d => d.SubjectId == subjectId && d.GradeLevel == gradeLevel);
public void ReplaceForSubjectAndGrade(Guid subjectId, int gradeLevel, List<CompetencyDomain> domains)
{
DeleteBySubjectAndGrade(subjectId, gradeLevel);
_all.AddRange(domains);
}
}
public class FakeShorthandCodes(List<ShorthandCode> all) : IShorthandCodeRepository