290 lines
15 KiB
C#
290 lines
15 KiB
C#
using LehrerApp.Core.Interfaces;
|
||
using LehrerApp.Core.Importing;
|
||
using LehrerApp.Core.Services;
|
||
using LehrerApp.Data;
|
||
using LehrerApp.Data.Repositories;
|
||
using LehrerApp.Desktop.Services;
|
||
using LehrerApp.Desktop.ViewModels;
|
||
using LehrerApp.Desktop.ViewModels.Groups;
|
||
using LehrerApp.Desktop.ViewModels.Planning;
|
||
using LehrerApp.Desktop.ViewModels.Settings;
|
||
using LehrerApp.Desktop.ViewModels.Students;
|
||
using LehrerApp.Desktop.ViewModels.Workload;
|
||
using LehrerApp.Sync;
|
||
using LehrerApp.Sync.Crypto;
|
||
using LehrerApp.Sync.Models;
|
||
using Microsoft.Extensions.DependencyInjection;
|
||
|
||
namespace LehrerApp.Desktop;
|
||
|
||
/// <summary>
|
||
/// Konfiguriert den DI-Container für den Desktop-Client.
|
||
///
|
||
/// WICHTIG: Alle Repositories sind Singleton – eine LiteDB-Datei pro Nutzer.
|
||
/// Sync-Services werden nur registriert wenn eine Server-URL konfiguriert ist.
|
||
/// </summary>
|
||
public static class AppBootstrapper
|
||
{
|
||
public static string DbPath { get; private set; } = "";
|
||
public static string AppDataPath { get; private set; } = "";
|
||
|
||
/// <summary>
|
||
/// Feste URL des KI-Backends (ai-backend/, TODO 4.5.9) — bewusst nicht in den Einstellungen
|
||
/// editierbar, siehe Planungsdokument. Vor dem ersten produktiven Einsatz durch die tatsächlich
|
||
/// deployte Domain ersetzen.
|
||
/// </summary>
|
||
public const string AiBackendUrl = "https://backapi.science-teaching.de/";
|
||
|
||
/// <summary>
|
||
/// Vor <see cref="BuildServices"/> gesetzt, wenn die Datenbank passwortgeschützt ist
|
||
/// (siehe App.axaml.cs: Passwort-Abfrage vor dem Öffnen der Datenbank).
|
||
/// </summary>
|
||
public static string? DbPassword { get; set; }
|
||
|
||
/// <summary>
|
||
/// Vor der eigentlichen DI-Konfiguration verfügbar (z.B. für den globalen
|
||
/// Exception-Handler in Program.cs, der schon vor <see cref="BuildServices"/> greifen muss).
|
||
/// </summary>
|
||
public static AppLogger Logger { get; private set; } = null!;
|
||
|
||
public static string ResolveAppDataPath()
|
||
{
|
||
if (!string.IsNullOrEmpty(AppDataPath)) return AppDataPath;
|
||
var appData = Path.Combine(
|
||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||
"LehrerApp");
|
||
Directory.CreateDirectory(appData);
|
||
AppDataPath = appData;
|
||
return appData;
|
||
}
|
||
|
||
/// Vor dem Öffnen der Datenbank verfügbar (z.B. für die Passwort-Abfrage beim Start).
|
||
public static string ResolveDbPath()
|
||
{
|
||
if (string.IsNullOrEmpty(DbPath))
|
||
DbPath = Path.Combine(ResolveAppDataPath(), "lehrerapp.db");
|
||
return DbPath;
|
||
}
|
||
|
||
public static AppLogger EnsureLogger()
|
||
{
|
||
Logger ??= new AppLogger(ResolveAppDataPath());
|
||
return Logger;
|
||
}
|
||
|
||
public static bool IsDbEncrypted() =>
|
||
new DatabaseEncryptionService().IsEncrypted(ResolveDbPath());
|
||
|
||
/// Beendet den Prozess und startet ihn neu (z.B. nach Wiederherstellung eines Backups
|
||
/// oder einer Änderung der Datenbank-Verschlüsselung — die laufende LiteDB-Verbindung
|
||
/// kann nicht sicher "heiß" auf eine andere Datei umgehängt werden).
|
||
public static void RestartApplication()
|
||
{
|
||
var exePath = Environment.ProcessPath;
|
||
if (!string.IsNullOrEmpty(exePath))
|
||
{
|
||
var psi = new System.Diagnostics.ProcessStartInfo(exePath) { UseShellExecute = false };
|
||
// Unter "dotnet run"/IDE-Debug zeigt ProcessPath auf den SDK-Host (dotnet[.exe]) statt
|
||
// auf die App selbst - ein Neustart ohne Argumente würde nur die dotnet-CLI-Hilfe
|
||
// anzeigen statt die App neu zu starten. Die ursprünglichen Kommandozeilenargumente
|
||
// (u.a. der DLL-Pfad) erneut mitgeben deckt auch diesen Fall ab.
|
||
if (Path.GetFileNameWithoutExtension(exePath).Equals("dotnet", StringComparison.OrdinalIgnoreCase))
|
||
foreach (var arg in Environment.GetCommandLineArgs())
|
||
psi.ArgumentList.Add(arg);
|
||
System.Diagnostics.Process.Start(psi);
|
||
}
|
||
Environment.Exit(0);
|
||
}
|
||
|
||
public static ServiceProvider BuildServices()
|
||
{
|
||
var services = new ServiceCollection();
|
||
|
||
// ── Pfade ─────────────────────────────────────────────────────────────
|
||
var appData = ResolveAppDataPath();
|
||
ResolveDbPath();
|
||
var queuePath = Path.Combine(appData, "syncqueue.db");
|
||
var keyPath = Path.Combine(appData, "sync.key");
|
||
|
||
// ── Logging & Benachrichtigungen ─────────────────────────────────────────
|
||
EnsureLogger();
|
||
services.AddSingleton(Logger);
|
||
services.AddSingleton<NotificationService>();
|
||
services.AddSingleton<ExportService>();
|
||
|
||
// ── Datensicherheit (13.3) ───────────────────────────────────────────
|
||
// Backup läuft vor dem Öffnen der Datenbank, rein dateibasiert (unabhängig von
|
||
// Verschlüsselung) — reine Datei-Kopie, kein offener LiteDB-Handle nötig.
|
||
var backup = new BackupService(appData);
|
||
backup.CreateBackup(DbPath);
|
||
services.AddSingleton(backup);
|
||
services.AddSingleton(_ => new AppLockService(appData));
|
||
services.AddSingleton<DatabaseEncryptionService>();
|
||
services.AddSingleton(_ => new PrivacySettingsService(appData));
|
||
services.AddSingleton<AttendanceBalanceService>();
|
||
services.AddSingleton<PersonalDataExportService>();
|
||
services.AddSingleton<IImportHandler<ImportedStudent>, StudentMasterDataCsvImportHandler>();
|
||
services.AddSingleton<IImportHandler<ImportedStudent>, LessonStudentListCsvImportHandler>();
|
||
services.AddSingleton<IImportHandler<ImportedStudent>, MarksPerLessonCsvImportHandler>();
|
||
services.AddSingleton<StudentImportService>();
|
||
|
||
// ── Datenbank ─────────────────────────────────────────────────────────
|
||
services.AddSingleton(_ => new LiteDbContext(DbPath, DbPassword));
|
||
|
||
// ── Repositories ──────────────────────────────────────────────────────
|
||
services.AddSingleton<IStudentRepository, StudentRepository>();
|
||
services.AddSingleton<IGroupRepository, GroupRepository>();
|
||
services.AddSingleton<ISeatingPlanRepository, SeatingPlanRepository>();
|
||
services.AddSingleton<IGroupMembershipRepository, GroupMembershipRepository>();
|
||
services.AddSingleton<IExamRepository, ExamRepository>();
|
||
services.AddSingleton<IExamResultRepository, ExamResultRepository>();
|
||
services.AddSingleton<IGradeRepository, GradeRepository>();
|
||
services.AddSingleton<IGradingSchemeRepository, GradingSchemeRepository>();
|
||
services.AddSingleton<IReportGradeRepository, ReportGradeRepository>();
|
||
services.AddSingleton<IGradingKeyTemplateRepository, GradingKeyTemplateRepository>();
|
||
services.AddSingleton<IUnitRepository, UnitRepository>();
|
||
services.AddSingleton<ILessonRepository, LessonRepository>();
|
||
services.AddSingleton<IDocumentationRepository, DocumentationRepository>();
|
||
services.AddSingleton<IWorkTaskRepository, WorkTaskRepository>();
|
||
services.AddSingleton<ITimeEntryRepository, TimeEntryRepository>();
|
||
services.AddSingleton<IParticipationSessionRepository, ParticipationSessionRepository>();
|
||
services.AddSingleton<IParticipationRepository, ParticipationRepository>();
|
||
services.AddSingleton<IParticipationAspectRepository, ParticipationAspectRepository>();
|
||
services.AddSingleton<IParticipationSectionRepository, ParticipationSectionRepository>();
|
||
services.AddSingleton<ISubjectRepository, SubjectRepository>();
|
||
services.AddSingleton<ICompetencyDomainRepository, CompetencyDomainRepository>();
|
||
services.AddSingleton<IShorthandCodeRepository, ShorthandCodeRepository>();
|
||
services.AddSingleton<IAlternativeLessonPathRepository, AlternativeLessonPathRepository>();
|
||
services.AddSingleton<IAttachmentStorage, LiteAttachmentStorage>();
|
||
services.AddSingleton<ITimetableSlotRepository, TimetableSlotRepository>();
|
||
services.AddSingleton<ISchoolHolidayRepository, SchoolHolidayRepository>();
|
||
services.AddSingleton<ISupervisionDutyRepository, SupervisionDutyRepository>();
|
||
services.AddSingleton<ISubstitutionEntryRepository, SubstitutionEntryRepository>();
|
||
|
||
// ── Services ──────────────────────────────────────────────────────────
|
||
services.AddSingleton<GradingService>();
|
||
services.AddSingleton<CompetencyAnalysisService>();
|
||
services.AddSingleton<SchoolYearService>();
|
||
services.AddSingleton<GroupRolloverService>();
|
||
services.AddSingleton<PublicHolidayService>();
|
||
services.AddSingleton(_ => new SchoolCalendarSettingsService(appData));
|
||
services.AddSingleton(_ => new PeriodScheduleService(appData));
|
||
services.AddSingleton(_ => new WorkloadSettingsService(appData));
|
||
services.AddSingleton(_ => new DashboardSettingsService(appData));
|
||
services.AddSingleton(_ => new LetterTemplateService(appData));
|
||
services.AddSingleton<PlanningExchangeService>();
|
||
|
||
// ── KI-Unterstützung (4.5.9, optional – nur wenn in den Einstellungen aktiviert) ──────
|
||
services.AddSingleton(_ => new AiSettingsService(appData));
|
||
services.AddSingleton(_ => new HttpClient { BaseAddress = new Uri(AiBackendUrl) });
|
||
services.AddSingleton<AiPlanningService>();
|
||
|
||
// ── Sync (optional – nur wenn Server konfiguriert) ────────────────────
|
||
var syncSettings = new SyncSettingsService(appData);
|
||
services.AddSingleton(syncSettings);
|
||
services.AddSingleton(_ => new SyncAuthService(new HttpClient()));
|
||
|
||
services.AddSingleton(_ => new EventQueue(queuePath));
|
||
services.AddSingleton(sp => new ConflictResolver(sp.GetRequiredService<EventQueue>()));
|
||
services.AddSingleton<byte[]>(_ =>
|
||
{
|
||
var key = SyncCrypto.LoadKey(keyPath) ?? SyncCrypto.GenerateKey();
|
||
SyncCrypto.SaveKey(key, keyPath);
|
||
return key;
|
||
});
|
||
|
||
var serverUrl = syncSettings.ServerUrl;
|
||
var deviceId = LoadOrCreateDeviceId(appData);
|
||
|
||
if (!string.IsNullOrEmpty(serverUrl))
|
||
{
|
||
services.AddSingleton(sp => new EventApplier(
|
||
sp.GetRequiredService<LiteDbContext>(), sp.GetRequiredService<byte[]>(),
|
||
BuildHttp(serverUrl, syncSettings)));
|
||
services.AddSingleton(sp => new SyncEventPublisher(
|
||
sp.GetRequiredService<EventQueue>(), deviceId, sp.GetRequiredService<byte[]>()));
|
||
services.AddSingleton(sp => new AttachmentSyncer(
|
||
sp.GetRequiredService<LiteDbContext>(), BuildHttp(serverUrl, syncSettings),
|
||
sp.GetRequiredService<byte[]>()));
|
||
|
||
services.AddSingleton<SyncEngine>(sp => new SyncEngine(
|
||
sp.GetRequiredService<EventQueue>(),
|
||
sp.GetRequiredService<ConflictResolver>(),
|
||
sp.GetRequiredService<EventApplier>(),
|
||
sp.GetRequiredService<AttachmentSyncer>(),
|
||
BuildHttp(serverUrl, syncSettings),
|
||
new SyncConfig
|
||
{
|
||
ServerUrl = serverUrl,
|
||
DeviceId = deviceId,
|
||
DeviceType = DeviceType.Desktop,
|
||
AutoSyncIntervalMinutes = 5,
|
||
}));
|
||
|
||
services.AddSingleton<SnapshotService>(sp => new SnapshotService(
|
||
BuildHttp(serverUrl, syncSettings),
|
||
sp.GetRequiredService<LiteDbContext>(),
|
||
sp.GetRequiredService<byte[]>(),
|
||
DeviceType.Desktop, DbPath, keyPath));
|
||
}
|
||
|
||
// ── ViewModels ────────────────────────────────────────────────────────
|
||
// Singleton: einmal erstellt, überall dieselbe Instanz
|
||
services.AddSingleton<AppLockViewModel>();
|
||
services.AddSingleton<MainWindowViewModel>();
|
||
services.AddSingleton<DashboardViewModel>();
|
||
services.AddSingleton(sp =>
|
||
new SyncStatusViewModel(sp.GetService<SyncEngine>()));
|
||
services.AddSingleton<GroupListViewModel>();
|
||
services.AddSingleton<StudentListViewModel>();
|
||
services.AddSingleton<TimetableViewModel>();
|
||
services.AddSingleton<WorkTaskListViewModel>();
|
||
services.AddSingleton<TimeTrackingViewModel>();
|
||
services.AddSingleton<WorkloadEvaluationViewModel>();
|
||
services.AddSingleton<WorkloadViewModel>();
|
||
|
||
// Transient: neue Instanz pro Navigation (für Detailseiten)
|
||
services.AddTransient<GroupDetailViewModel>();
|
||
services.AddTransient<StudentDetailViewModel>();
|
||
services.AddTransient<ParticipationTabViewModel>();
|
||
services.AddTransient<GradeOverviewTabViewModel>();
|
||
services.AddTransient<PlanningTabViewModel>();
|
||
services.AddTransient<CompetencyOverviewTabViewModel>();
|
||
services.AddTransient<SeatingPlanTabViewModel>();
|
||
services.AddTransient<GroupDocumentationTabViewModel>();
|
||
services.AddTransient<AddGroupDialogViewModel>();
|
||
services.AddTransient<SettingsViewModel>();
|
||
|
||
var provider = services.BuildServiceProvider();
|
||
|
||
// Sync-agnostischer Hook auf LiteDbContext (siehe LiteDbContext.OnChange) wird erst hier,
|
||
// außerhalb der Repository-Registrierung, mit der tatsächlichen Sync-Logik verbunden.
|
||
if (!string.IsNullOrEmpty(serverUrl))
|
||
provider.GetRequiredService<LiteDbContext>().OnChange =
|
||
provider.GetRequiredService<SyncEventPublisher>().Publish;
|
||
|
||
return provider;
|
||
}
|
||
|
||
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
|
||
|
||
private static HttpClient BuildHttp(string url, SyncSettingsService syncSettings)
|
||
{
|
||
var http = new HttpClient { BaseAddress = new Uri(url) };
|
||
var token = syncSettings.GetToken();
|
||
if (token is not null)
|
||
http.DefaultRequestHeaders.Authorization =
|
||
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
|
||
return http;
|
||
}
|
||
|
||
private static string LoadOrCreateDeviceId(string appData)
|
||
{
|
||
var p = Path.Combine(appData, "device.id");
|
||
if (File.Exists(p)) return File.ReadAllText(p).Trim();
|
||
var id = Guid.NewGuid().ToString();
|
||
File.WriteAllText(p, id);
|
||
return id;
|
||
}
|
||
}
|