feat: add planning JSON exchange
This commit is contained in:
@@ -0,0 +1,130 @@
|
|||||||
|
using LehrerApp.Core.Models;
|
||||||
|
using LehrerApp.Desktop.Services;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Tests;
|
||||||
|
|
||||||
|
public sealed class PlanningExchangeServiceTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void UnitRoundtrip_ErzeugtNeueObjekteUndErhaeltPlanungsdaten()
|
||||||
|
{
|
||||||
|
var units = new FakeUnits();
|
||||||
|
var lessons = new FakeLessons();
|
||||||
|
var shortPath = new AlternativeLessonPath { Name = "Kurzversion" };
|
||||||
|
var paths = new FakeAlternativeLessonPaths([shortPath]);
|
||||||
|
var service = new PlanningExchangeService(units, lessons, paths);
|
||||||
|
var sourceGroupId = Guid.NewGuid();
|
||||||
|
var targetGroupId = Guid.NewGuid();
|
||||||
|
var unit = new Unit
|
||||||
|
{
|
||||||
|
GroupId = sourceGroupId,
|
||||||
|
Title = "Optik",
|
||||||
|
StartDate = new DateOnly(2026, 9, 1),
|
||||||
|
EndDate = new DateOnly(2026, 10, 1),
|
||||||
|
Competencies = ["UF1", "E4"],
|
||||||
|
Status = UnitStatus.Active,
|
||||||
|
Notes = "Experimente",
|
||||||
|
};
|
||||||
|
units.Add(unit);
|
||||||
|
var sourceLesson = new Lesson
|
||||||
|
{
|
||||||
|
UnitId = unit.Id,
|
||||||
|
GroupId = sourceGroupId,
|
||||||
|
Date = new DateOnly(2026, 9, 8),
|
||||||
|
LessonNumber = 3,
|
||||||
|
Topic = "Reflexion",
|
||||||
|
StartTime = new TimeOnly(9, 50),
|
||||||
|
Status = LessonStatus.Planned,
|
||||||
|
Homework = "Aufgabe 2",
|
||||||
|
Phases =
|
||||||
|
[
|
||||||
|
new LessonPhaseStep
|
||||||
|
{
|
||||||
|
Name = "Experiment", DurationMinutes = 25, Activity = "Winkel messen",
|
||||||
|
Material = "Optikbox", Shorthand = "PA", AlternativePathId = shortPath.Id,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
lessons.Add(sourceLesson);
|
||||||
|
|
||||||
|
var json = service.ExportUnit(unit, new PlanningExchangeContext
|
||||||
|
{
|
||||||
|
Group = "8a", Subject = "Physik", GradeLevel = 8,
|
||||||
|
});
|
||||||
|
var result = service.ImportUnit(json, targetGroupId);
|
||||||
|
|
||||||
|
Assert.DoesNotContain(unit.Id.ToString(), json, StringComparison.OrdinalIgnoreCase);
|
||||||
|
Assert.DoesNotContain(sourceLesson.Id.ToString(), json, StringComparison.OrdinalIgnoreCase);
|
||||||
|
Assert.NotEqual(unit.Id, result.Unit.Id);
|
||||||
|
Assert.Equal(targetGroupId, result.Unit.GroupId);
|
||||||
|
Assert.Equal("Optik", result.Unit.Title);
|
||||||
|
Assert.Equal(UnitStatus.Active, result.Unit.Status);
|
||||||
|
Assert.Equal(["UF1", "E4"], result.Unit.Competencies);
|
||||||
|
var importedLesson = Assert.Single(result.Lessons);
|
||||||
|
Assert.NotEqual(sourceLesson.Id, importedLesson.Id);
|
||||||
|
Assert.Equal(result.Unit.Id, importedLesson.UnitId);
|
||||||
|
Assert.Equal(targetGroupId, importedLesson.GroupId);
|
||||||
|
Assert.Equal(new TimeOnly(9, 50), importedLesson.StartTime);
|
||||||
|
Assert.Equal(shortPath.Id, Assert.Single(importedLesson.Phases).AlternativePathId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LessonImport_AkzeptiertMarkdownCodeblockUndLegtNeueStundeAn()
|
||||||
|
{
|
||||||
|
var units = new FakeUnits();
|
||||||
|
var lessons = new FakeLessons();
|
||||||
|
var service = new PlanningExchangeService(units, lessons, new FakeAlternativeLessonPaths([]));
|
||||||
|
var source = new Lesson
|
||||||
|
{
|
||||||
|
UnitId = Guid.NewGuid(), GroupId = Guid.NewGuid(), Date = new DateOnly(2026, 9, 8),
|
||||||
|
Topic = "Reflexion", Phases = [new LessonPhaseStep { Name = "Einstieg", DurationMinutes = 10 }],
|
||||||
|
};
|
||||||
|
lessons.Add(source);
|
||||||
|
var json = service.ExportLesson(source, new PlanningExchangeContext());
|
||||||
|
var targetUnitId = Guid.NewGuid();
|
||||||
|
var targetGroupId = Guid.NewGuid();
|
||||||
|
|
||||||
|
var imported = service.ImportLesson($"```json\n{json}\n```", targetUnitId, targetGroupId);
|
||||||
|
|
||||||
|
Assert.NotEqual(source.Id, imported.Id);
|
||||||
|
Assert.Equal(targetUnitId, imported.UnitId);
|
||||||
|
Assert.Equal(targetGroupId, imported.GroupId);
|
||||||
|
Assert.Equal("Reflexion", imported.Topic);
|
||||||
|
Assert.Equal(10, Assert.Single(imported.Phases).DurationMinutes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ImportMitFalschemSchema_SchreibtKeineDaten()
|
||||||
|
{
|
||||||
|
var units = new FakeUnits();
|
||||||
|
var lessons = new FakeLessons();
|
||||||
|
var service = new PlanningExchangeService(units, lessons, new FakeAlternativeLessonPaths([]));
|
||||||
|
const string json = """
|
||||||
|
{ "schema": "anderes.format", "version": 1, "unit": { "title": "Optik" } }
|
||||||
|
""";
|
||||||
|
var groupId = Guid.NewGuid();
|
||||||
|
|
||||||
|
var error = Assert.Throws<PlanningExchangeException>(() => service.ImportUnit(json, groupId));
|
||||||
|
|
||||||
|
Assert.Contains("Falsches Format", error.Message);
|
||||||
|
Assert.Empty(units.GetByGroup(groupId));
|
||||||
|
Assert.Empty(lessons.GetByGroupAndRange(groupId, DateOnly.MinValue, DateOnly.MaxValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ExportVerwendetDokumentiertesSchemaUndCamelCase()
|
||||||
|
{
|
||||||
|
var service = new PlanningExchangeService(new FakeUnits(), new FakeLessons(),
|
||||||
|
new FakeAlternativeLessonPaths([]));
|
||||||
|
var lesson = new Lesson { Date = new DateOnly(2026, 9, 8), Topic = "Optik" };
|
||||||
|
|
||||||
|
using var json = JsonDocument.Parse(service.ExportLesson(lesson, new PlanningExchangeContext()));
|
||||||
|
|
||||||
|
Assert.Equal(PlanningExchangeService.LessonSchema, json.RootElement.GetProperty("schema").GetString());
|
||||||
|
Assert.Equal(1, json.RootElement.GetProperty("version").GetInt32());
|
||||||
|
Assert.Equal("2026-09-08", json.RootElement.GetProperty("lesson").GetProperty("date").GetString());
|
||||||
|
Assert.Equal("planned", json.RootElement.GetProperty("lesson").GetProperty("status").GetString());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -156,6 +156,7 @@ public static class AppBootstrapper
|
|||||||
services.AddSingleton(_ => new WorkloadSettingsService(appData));
|
services.AddSingleton(_ => new WorkloadSettingsService(appData));
|
||||||
services.AddSingleton(_ => new DashboardSettingsService(appData));
|
services.AddSingleton(_ => new DashboardSettingsService(appData));
|
||||||
services.AddSingleton(_ => new LetterTemplateService(appData));
|
services.AddSingleton(_ => new LetterTemplateService(appData));
|
||||||
|
services.AddSingleton<PlanningExchangeService>();
|
||||||
|
|
||||||
// ── KI-Unterstützung (4.5.9, optional – nur wenn in den Einstellungen aktiviert) ──────
|
// ── KI-Unterstützung (4.5.9, optional – nur wenn in den Einstellungen aktiviert) ──────
|
||||||
services.AddSingleton(_ => new AiSettingsService(appData));
|
services.AddSingleton(_ => new AiSettingsService(appData));
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
# LehrerApp-Austauschformat: Unterrichtseinheit
|
||||||
|
|
||||||
|
Dieses Dokument beschreibt Version 1 des JSON-Formats `lehrerapp.unit-planning`.
|
||||||
|
Es dient zum Austausch einer vollständigen Unterrichtseinheit einschließlich ihrer Stunden.
|
||||||
|
|
||||||
|
## Grundregeln
|
||||||
|
|
||||||
|
- Die Datei enthält reines UTF-8-JSON. Beim Import wird auch ein einzelner Markdown-Codeblock akzeptiert.
|
||||||
|
- Datumswerte verwenden `JJJJ-MM-TT`, Uhrzeiten `HH:MM` oder `HH:MM:SS`.
|
||||||
|
- Statuswerte sind `planned`, `active`, `completed` für Einheiten und `planned`, `conducted` für Stunden.
|
||||||
|
- Interne IDs werden nicht übertragen. Der Import legt eine neue Einheit und neue Stunden in der aktuell geöffneten Lerngruppe an.
|
||||||
|
- `context` hilft bei der Bearbeitung mit einer KI, wird beim Import aber nicht zur Zuordnung verwendet.
|
||||||
|
- Unbekannte zusätzliche Felder werden ignoriert.
|
||||||
|
|
||||||
|
## Beispiel
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"schema": "lehrerapp.unit-planning",
|
||||||
|
"version": 1,
|
||||||
|
"context": {
|
||||||
|
"group": "8a",
|
||||||
|
"subject": "Physik",
|
||||||
|
"gradeLevel": 8,
|
||||||
|
"unitTitle": null
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"title": "Optik: Reflexion und Brechung",
|
||||||
|
"startDate": "2026-09-01",
|
||||||
|
"endDate": "2026-10-06",
|
||||||
|
"competencies": ["UF1", "E4"],
|
||||||
|
"status": "planned",
|
||||||
|
"notes": "Experimente in Kleingruppen",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"date": "2026-09-01",
|
||||||
|
"lessonNumber": 3,
|
||||||
|
"topic": "Lichtausbreitung",
|
||||||
|
"startTime": "09:50:00",
|
||||||
|
"status": "planned",
|
||||||
|
"homework": "Aufgabe 2 beenden",
|
||||||
|
"reflection": null,
|
||||||
|
"phases": [
|
||||||
|
{
|
||||||
|
"name": "Einstieg",
|
||||||
|
"durationMinutes": 10,
|
||||||
|
"activity": "Impulsbild beschreiben und Vermutungen sammeln",
|
||||||
|
"material": "Beamer",
|
||||||
|
"shorthand": "Plenum",
|
||||||
|
"alternativePath": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Erarbeitung",
|
||||||
|
"durationMinutes": 25,
|
||||||
|
"activity": "Versuch zur geradlinigen Lichtausbreitung",
|
||||||
|
"material": "Experimentierbox",
|
||||||
|
"shorthand": "GA",
|
||||||
|
"alternativePath": "Kurzversion"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Felder
|
||||||
|
|
||||||
|
### Dokument
|
||||||
|
|
||||||
|
- `schema` (Pflicht): exakt `lehrerapp.unit-planning`
|
||||||
|
- `version` (Pflicht): aktuell `1`
|
||||||
|
- `context` (optional): `group`, `subject`, `gradeLevel` und optional `unitTitle`
|
||||||
|
- `unit` (Pflicht): die zu importierende Einheit
|
||||||
|
|
||||||
|
### Einheit
|
||||||
|
|
||||||
|
- `title` (Pflicht, Text)
|
||||||
|
- `startDate`, `endDate` (optional, Datum oder `null`); das Ende darf nicht vor dem Start liegen
|
||||||
|
- `competencies` (Liste von Kompetenzcodes oder Freitexten)
|
||||||
|
- `status` (optional, Standard `planned`)
|
||||||
|
- `notes` (optional, Text oder `null`)
|
||||||
|
- `lessons` (Liste, darf leer sein); Aufbau siehe Stundenformat unten
|
||||||
|
|
||||||
|
### Stunde
|
||||||
|
|
||||||
|
- `date` (Pflicht, Datum)
|
||||||
|
- `lessonNumber` (optional, Ganzzahl 1–20 oder `null`)
|
||||||
|
- `topic` (Pflicht, Text)
|
||||||
|
- `startTime` (optional, Uhrzeit oder `null`)
|
||||||
|
- `status` (optional: `planned` oder `conducted`, Standard `planned`)
|
||||||
|
- `homework`, `reflection` (optional, Text oder `null`)
|
||||||
|
- `phases` (Liste, darf leer sein)
|
||||||
|
|
||||||
|
### Phase
|
||||||
|
|
||||||
|
- `name`, `activity`, `material`, `shorthand` (Text; leere Werte sind erlaubt)
|
||||||
|
- `durationMinutes` (Ganzzahl 0–180)
|
||||||
|
- `alternativePath` (optional): lesbarer Name eines alternativen Verlaufs, z. B. `Kurzversion`. Beim Import wird ein vorhandener gleichnamiger Ablauf verwendet oder neu angelegt.
|
||||||
|
|
||||||
|
## Hinweise für KI-Bearbeitung
|
||||||
|
|
||||||
|
- JSON-Struktur, `schema` und `version` unverändert lassen.
|
||||||
|
- Nur gültige ISO-Daten verwenden.
|
||||||
|
- Phasendauern so wählen, dass ihre Summe zur verfügbaren Unterrichtszeit passt.
|
||||||
|
- Keine IDs ergänzen; Beziehungen werden beim Import automatisch hergestellt.
|
||||||
|
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# LehrerApp-Austauschformat: einzelne Stundenplanung
|
||||||
|
|
||||||
|
Dieses Dokument beschreibt Version 1 des JSON-Formats `lehrerapp.lesson-planning`.
|
||||||
|
Es dient zum Austausch genau einer Stunde mit ihrem Verlaufsplan.
|
||||||
|
|
||||||
|
## Grundregeln
|
||||||
|
|
||||||
|
- Die Datei enthält reines UTF-8-JSON. Beim Import wird auch ein einzelner Markdown-Codeblock akzeptiert.
|
||||||
|
- Datum: `JJJJ-MM-TT`; Uhrzeit: `HH:MM` oder `HH:MM:SS`.
|
||||||
|
- Interne IDs werden nicht übertragen. Der Import legt eine neue Stunde in der aktuell ausgewählten Einheit an.
|
||||||
|
- `context` ist nur Bearbeitungshilfe und entscheidet nicht über das Importziel.
|
||||||
|
- Unbekannte zusätzliche Felder werden ignoriert.
|
||||||
|
|
||||||
|
## Beispiel
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"schema": "lehrerapp.lesson-planning",
|
||||||
|
"version": 1,
|
||||||
|
"context": {
|
||||||
|
"group": "8a",
|
||||||
|
"subject": "Physik",
|
||||||
|
"gradeLevel": 8,
|
||||||
|
"unitTitle": "Optik"
|
||||||
|
},
|
||||||
|
"lesson": {
|
||||||
|
"date": "2026-09-08",
|
||||||
|
"lessonNumber": 3,
|
||||||
|
"topic": "Reflexionsgesetz",
|
||||||
|
"startTime": "09:50:00",
|
||||||
|
"status": "planned",
|
||||||
|
"homework": "Versuchsprotokoll fertigstellen",
|
||||||
|
"reflection": null,
|
||||||
|
"phases": [
|
||||||
|
{
|
||||||
|
"name": "Einstieg",
|
||||||
|
"durationMinutes": 8,
|
||||||
|
"activity": "Alltagsbeispiele für Reflexion sammeln",
|
||||||
|
"material": "Spiegel",
|
||||||
|
"shorthand": "UG",
|
||||||
|
"alternativePath": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Experiment",
|
||||||
|
"durationMinutes": 27,
|
||||||
|
"activity": "Einfalls- und Ausfallswinkel messen",
|
||||||
|
"material": "Optikbox, AB 03",
|
||||||
|
"shorthand": "PA",
|
||||||
|
"alternativePath": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Felder
|
||||||
|
|
||||||
|
### Dokument
|
||||||
|
|
||||||
|
- `schema` (Pflicht): exakt `lehrerapp.lesson-planning`
|
||||||
|
- `version` (Pflicht): aktuell `1`
|
||||||
|
- `context` (optional): `group`, `subject`, `gradeLevel`, `unitTitle`
|
||||||
|
- `lesson` (Pflicht): die neue Stundenplanung
|
||||||
|
|
||||||
|
### Stunde
|
||||||
|
|
||||||
|
- `date` (Pflicht, Datum)
|
||||||
|
- `lessonNumber` (optional, Ganzzahl 1–20 oder `null`)
|
||||||
|
- `topic` (Pflicht, Text)
|
||||||
|
- `startTime` (optional, Uhrzeit oder `null`)
|
||||||
|
- `status` (optional: `planned` oder `conducted`, Standard `planned`)
|
||||||
|
- `homework`, `reflection` (optional, Text oder `null`)
|
||||||
|
- `phases` (Liste, darf leer sein)
|
||||||
|
|
||||||
|
### Phase
|
||||||
|
|
||||||
|
- `name`: Bezeichnung, z. B. `Einstieg` oder `Sicherung`
|
||||||
|
- `durationMinutes`: Ganzzahl 0–180
|
||||||
|
- `activity`: Lehrer-/Schüler-Tätigkeit
|
||||||
|
- `material`: benötigte Medien und Materialien
|
||||||
|
- `shorthand`: Kurzsymbol oder Sozialform
|
||||||
|
- `alternativePath` (optional): Name eines alternativen Verlaufs. Ein vorhandener gleichnamiger Ablauf wird wiederverwendet, andernfalls neu angelegt.
|
||||||
|
|
||||||
|
## Hinweise für KI-Bearbeitung
|
||||||
|
|
||||||
|
- JSON-Struktur, `schema` und `version` unverändert lassen.
|
||||||
|
- Keine IDs ergänzen.
|
||||||
|
- Das Datum muss gesetzt sein; die Stundennummer darf fehlen.
|
||||||
|
- Die Summe der `durationMinutes` sollte zur verfügbaren Unterrichtszeit passen.
|
||||||
|
|
||||||
@@ -0,0 +1,278 @@
|
|||||||
|
using LehrerApp.Core.Interfaces;
|
||||||
|
using LehrerApp.Core.Models;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Portables, menschen- und KI-lesbares Austauschformat für Unterrichtseinheiten und
|
||||||
|
/// Einzelstunden. Interne Datenbank-IDs werden nie übertragen; jeder Import legt neue Objekte an.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PlanningExchangeService(IUnitRepository units, ILessonRepository lessons,
|
||||||
|
IAlternativeLessonPathRepository alternativePaths)
|
||||||
|
{
|
||||||
|
public const string UnitSchema = "lehrerapp.unit-planning";
|
||||||
|
public const string LessonSchema = "lehrerapp.lesson-planning";
|
||||||
|
public const int CurrentVersion = 1;
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
|
WriteIndented = true,
|
||||||
|
AllowTrailingCommas = true,
|
||||||
|
ReadCommentHandling = JsonCommentHandling.Skip,
|
||||||
|
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) },
|
||||||
|
};
|
||||||
|
|
||||||
|
public string ExportUnit(Unit unit, PlanningExchangeContext context)
|
||||||
|
{
|
||||||
|
var document = new UnitPlanningDocument
|
||||||
|
{
|
||||||
|
Schema = UnitSchema,
|
||||||
|
Version = CurrentVersion,
|
||||||
|
Context = context,
|
||||||
|
Unit = ToPayload(unit, lessons.GetByUnit(unit.Id)),
|
||||||
|
};
|
||||||
|
return JsonSerializer.Serialize(document, JsonOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ExportLesson(Lesson lesson, PlanningExchangeContext context)
|
||||||
|
{
|
||||||
|
var document = new LessonPlanningDocument
|
||||||
|
{
|
||||||
|
Schema = LessonSchema,
|
||||||
|
Version = CurrentVersion,
|
||||||
|
Context = context,
|
||||||
|
Lesson = ToPayload(lesson),
|
||||||
|
};
|
||||||
|
return JsonSerializer.Serialize(document, JsonOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
public UnitImportResult ImportUnit(string json, Guid targetGroupId)
|
||||||
|
{
|
||||||
|
var document = Deserialize<UnitPlanningDocument>(json, UnitSchema);
|
||||||
|
ValidateUnit(document.Unit);
|
||||||
|
foreach (var lesson in document.Unit.Lessons) ValidateLesson(lesson);
|
||||||
|
|
||||||
|
var unit = new Unit
|
||||||
|
{
|
||||||
|
GroupId = targetGroupId,
|
||||||
|
Title = document.Unit.Title.Trim(),
|
||||||
|
StartDate = document.Unit.StartDate,
|
||||||
|
EndDate = document.Unit.EndDate,
|
||||||
|
Competencies = document.Unit.Competencies
|
||||||
|
.Where(c => !string.IsNullOrWhiteSpace(c)).Select(c => c.Trim())
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase).ToList(),
|
||||||
|
Status = document.Unit.Status,
|
||||||
|
Notes = Clean(document.Unit.Notes),
|
||||||
|
};
|
||||||
|
units.Save(unit);
|
||||||
|
|
||||||
|
var importedLessons = new List<Lesson>();
|
||||||
|
foreach (var payload in document.Unit.Lessons)
|
||||||
|
{
|
||||||
|
var lesson = ToModel(payload, unit.Id, targetGroupId);
|
||||||
|
lessons.Save(lesson);
|
||||||
|
importedLessons.Add(lesson);
|
||||||
|
}
|
||||||
|
return new UnitImportResult(unit, importedLessons);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Lesson ImportLesson(string json, Guid targetUnitId, Guid targetGroupId)
|
||||||
|
{
|
||||||
|
var document = Deserialize<LessonPlanningDocument>(json, LessonSchema);
|
||||||
|
ValidateLesson(document.Lesson);
|
||||||
|
var lesson = ToModel(document.Lesson, targetUnitId, targetGroupId);
|
||||||
|
lessons.Save(lesson);
|
||||||
|
return lesson;
|
||||||
|
}
|
||||||
|
|
||||||
|
private UnitPlanningPayload ToPayload(Unit unit, List<Lesson> unitLessons) => new()
|
||||||
|
{
|
||||||
|
Title = unit.Title,
|
||||||
|
StartDate = unit.StartDate,
|
||||||
|
EndDate = unit.EndDate,
|
||||||
|
Competencies = [.. unit.Competencies],
|
||||||
|
Status = unit.Status,
|
||||||
|
Notes = unit.Notes,
|
||||||
|
Lessons = [.. unitLessons.Select(ToPayload)],
|
||||||
|
};
|
||||||
|
|
||||||
|
private LessonPlanningPayload ToPayload(Lesson lesson) => new()
|
||||||
|
{
|
||||||
|
Date = lesson.Date,
|
||||||
|
LessonNumber = lesson.LessonNumber,
|
||||||
|
Topic = lesson.Topic,
|
||||||
|
StartTime = lesson.StartTime,
|
||||||
|
Status = lesson.Status,
|
||||||
|
Homework = lesson.Homework,
|
||||||
|
Reflection = lesson.Reflection,
|
||||||
|
Phases = [.. lesson.Phases.Select(p => new LessonPhasePayload
|
||||||
|
{
|
||||||
|
Name = p.Name,
|
||||||
|
DurationMinutes = p.DurationMinutes,
|
||||||
|
Activity = p.Activity,
|
||||||
|
Material = p.Material,
|
||||||
|
Shorthand = p.Shorthand,
|
||||||
|
AlternativePath = p.AlternativePathId is Guid id ? alternativePaths.GetById(id)?.Name : null,
|
||||||
|
})],
|
||||||
|
};
|
||||||
|
|
||||||
|
private Lesson ToModel(LessonPlanningPayload payload, Guid unitId, Guid groupId) => new()
|
||||||
|
{
|
||||||
|
UnitId = unitId,
|
||||||
|
GroupId = groupId,
|
||||||
|
Date = payload.Date!.Value,
|
||||||
|
LessonNumber = payload.LessonNumber,
|
||||||
|
Topic = payload.Topic.Trim(),
|
||||||
|
StartTime = payload.StartTime,
|
||||||
|
Status = payload.Status,
|
||||||
|
Homework = Clean(payload.Homework),
|
||||||
|
Reflection = Clean(payload.Reflection),
|
||||||
|
Phases = [.. payload.Phases.Select(p => new LessonPhaseStep
|
||||||
|
{
|
||||||
|
Name = p.Name?.Trim() ?? "",
|
||||||
|
DurationMinutes = p.DurationMinutes,
|
||||||
|
Activity = p.Activity?.Trim() ?? "",
|
||||||
|
Material = p.Material?.Trim() ?? "",
|
||||||
|
Shorthand = p.Shorthand?.Trim() ?? "",
|
||||||
|
AlternativePathId = ResolveAlternativePath(p.AlternativePath),
|
||||||
|
})],
|
||||||
|
};
|
||||||
|
|
||||||
|
private Guid? ResolveAlternativePath(string? name)
|
||||||
|
{
|
||||||
|
name = Clean(name);
|
||||||
|
if (name is null) return null;
|
||||||
|
var existing = alternativePaths.GetAll().FirstOrDefault(p =>
|
||||||
|
string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (existing is not null) return existing.Id;
|
||||||
|
var created = new AlternativeLessonPath { Name = name };
|
||||||
|
alternativePaths.Save(created);
|
||||||
|
return created.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static T Deserialize<T>(string json, string expectedSchema) where T : PlanningDocument
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var document = JsonSerializer.Deserialize<T>(RemoveMarkdownFence(json), JsonOptions)
|
||||||
|
?? throw new PlanningExchangeException("Die JSON-Datei ist leer.");
|
||||||
|
if (!string.Equals(document.Schema, expectedSchema, StringComparison.Ordinal))
|
||||||
|
throw new PlanningExchangeException($"Falsches Format: Erwartet wird „{expectedSchema}“.");
|
||||||
|
if (document.Version != CurrentVersion)
|
||||||
|
throw new PlanningExchangeException($"Die Formatversion {document.Version} wird nicht unterstützt.");
|
||||||
|
return document;
|
||||||
|
}
|
||||||
|
catch (JsonException ex)
|
||||||
|
{
|
||||||
|
throw new PlanningExchangeException($"Die JSON-Datei ist ungültig: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string RemoveMarkdownFence(string json)
|
||||||
|
{
|
||||||
|
var value = json.Trim();
|
||||||
|
if (!value.StartsWith("```", StringComparison.Ordinal)) return value;
|
||||||
|
var firstLineEnd = value.IndexOf('\n');
|
||||||
|
if (firstLineEnd < 0) return value;
|
||||||
|
value = value[(firstLineEnd + 1)..];
|
||||||
|
var closingFence = value.LastIndexOf("```", StringComparison.Ordinal);
|
||||||
|
return closingFence >= 0 ? value[..closingFence].Trim() : value.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateUnit(UnitPlanningPayload? unit)
|
||||||
|
{
|
||||||
|
if (unit is null) throw new PlanningExchangeException("Das Feld „unit“ fehlt.");
|
||||||
|
if (string.IsNullOrWhiteSpace(unit.Title))
|
||||||
|
throw new PlanningExchangeException("Die Einheit benötigt einen Titel.");
|
||||||
|
if (unit.StartDate is not null && unit.EndDate is not null && unit.EndDate < unit.StartDate)
|
||||||
|
throw new PlanningExchangeException("Das Enddatum der Einheit liegt vor dem Startdatum.");
|
||||||
|
unit.Competencies ??= [];
|
||||||
|
unit.Lessons ??= [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateLesson(LessonPlanningPayload? lesson)
|
||||||
|
{
|
||||||
|
if (lesson is null) throw new PlanningExchangeException("Das Feld „lesson“ fehlt.");
|
||||||
|
if (lesson.Date is null) throw new PlanningExchangeException("Die Stunde benötigt ein Datum im Format JJJJ-MM-TT.");
|
||||||
|
if (string.IsNullOrWhiteSpace(lesson.Topic))
|
||||||
|
throw new PlanningExchangeException("Die Stunde benötigt ein Thema.");
|
||||||
|
if (lesson.LessonNumber is < 1 or > 20)
|
||||||
|
throw new PlanningExchangeException("Die Stundennummer muss zwischen 1 und 20 liegen.");
|
||||||
|
lesson.Phases ??= [];
|
||||||
|
if (lesson.Phases.Any(p => p.DurationMinutes is < 0 or > 180))
|
||||||
|
throw new PlanningExchangeException("Die Dauer jeder Phase muss zwischen 0 und 180 Minuten liegen.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? Clean(string? value) =>
|
||||||
|
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract class PlanningDocument
|
||||||
|
{
|
||||||
|
public string Schema { get; set; } = "";
|
||||||
|
public int Version { get; set; }
|
||||||
|
public PlanningExchangeContext? Context { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class UnitPlanningDocument : PlanningDocument
|
||||||
|
{
|
||||||
|
public UnitPlanningPayload Unit { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class LessonPlanningDocument : PlanningDocument
|
||||||
|
{
|
||||||
|
public LessonPlanningPayload Lesson { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class PlanningExchangeContext
|
||||||
|
{
|
||||||
|
public string Group { get; set; } = "";
|
||||||
|
public string Subject { get; set; } = "";
|
||||||
|
public int GradeLevel { get; set; }
|
||||||
|
public string? UnitTitle { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class UnitPlanningPayload
|
||||||
|
{
|
||||||
|
public string Title { get; set; } = "";
|
||||||
|
public DateOnly? StartDate { get; set; }
|
||||||
|
public DateOnly? EndDate { get; set; }
|
||||||
|
public List<string> Competencies { get; set; } = [];
|
||||||
|
public UnitStatus Status { get; set; } = UnitStatus.Planned;
|
||||||
|
public string? Notes { get; set; }
|
||||||
|
public List<LessonPlanningPayload> Lessons { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class LessonPlanningPayload
|
||||||
|
{
|
||||||
|
public DateOnly? Date { get; set; }
|
||||||
|
public int? LessonNumber { get; set; }
|
||||||
|
public string Topic { get; set; } = "";
|
||||||
|
public TimeOnly? StartTime { get; set; }
|
||||||
|
public LessonStatus Status { get; set; } = LessonStatus.Planned;
|
||||||
|
public string? Homework { get; set; }
|
||||||
|
public string? Reflection { get; set; }
|
||||||
|
public List<LessonPhasePayload> Phases { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class LessonPhasePayload
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public int DurationMinutes { get; set; }
|
||||||
|
public string Activity { get; set; } = "";
|
||||||
|
public string Material { get; set; } = "";
|
||||||
|
public string Shorthand { get; set; } = "";
|
||||||
|
public string? AlternativePath { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record UnitImportResult(Unit Unit, List<Lesson> Lessons);
|
||||||
|
|
||||||
|
public sealed class PlanningExchangeException : Exception
|
||||||
|
{
|
||||||
|
public PlanningExchangeException(string message) : base(message) { }
|
||||||
|
public PlanningExchangeException(string message, Exception innerException) : base(message, innerException) { }
|
||||||
|
}
|
||||||
@@ -49,6 +49,10 @@ public partial class PlanningTabViewModel : ObservableObject
|
|||||||
// Gruppenwechsel/Laden kurzzeitig null — ein verschachtelter Pfad würde dafür jedes Mal
|
// Gruppenwechsel/Laden kurzzeitig null — ein verschachtelter Pfad würde dafür jedes Mal
|
||||||
// einen Binding-Fehler loggen (siehe GroupDetailViewModel.IsDifferentiated für dasselbe Muster).
|
// einen Binding-Fehler loggen (siehe GroupDetailViewModel.IsDifferentiated für dasselbe Muster).
|
||||||
public string SelectedUnitTitleSuffix => SelectedUnit is null ? "" : $" – {SelectedUnit.Title}";
|
public string SelectedUnitTitleSuffix => SelectedUnit is null ? "" : $" – {SelectedUnit.Title}";
|
||||||
|
public bool HasUnitSelection => SelectedUnit is not null;
|
||||||
|
public bool HasLessonSelection => SelectedLesson is not null;
|
||||||
|
public bool CanImportUnitPlanning => !IsReadOnly;
|
||||||
|
public bool CanImportLessonPlanning => !IsReadOnly && SelectedUnit is not null;
|
||||||
|
|
||||||
public ObservableCollection<UnitSummary> Units { get; } = [];
|
public ObservableCollection<UnitSummary> Units { get; } = [];
|
||||||
public ObservableCollection<LessonSummary> Lessons { get; } = [];
|
public ObservableCollection<LessonSummary> Lessons { get; } = [];
|
||||||
@@ -123,6 +127,8 @@ public partial class PlanningTabViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
LoadLessons();
|
LoadLessons();
|
||||||
OnPropertyChanged(nameof(SelectedUnitTitleSuffix));
|
OnPropertyChanged(nameof(SelectedUnitTitleSuffix));
|
||||||
|
OnPropertyChanged(nameof(HasUnitSelection));
|
||||||
|
OnPropertyChanged(nameof(CanImportLessonPlanning));
|
||||||
EditUnitCommand.NotifyCanExecuteChanged();
|
EditUnitCommand.NotifyCanExecuteChanged();
|
||||||
DeleteUnitCommand.NotifyCanExecuteChanged();
|
DeleteUnitCommand.NotifyCanExecuteChanged();
|
||||||
CopyUnitCommand.NotifyCanExecuteChanged();
|
CopyUnitCommand.NotifyCanExecuteChanged();
|
||||||
@@ -143,6 +149,7 @@ public partial class PlanningTabViewModel : ObservableObject
|
|||||||
|
|
||||||
partial void OnSelectedLessonChanged(LessonSummary? value)
|
partial void OnSelectedLessonChanged(LessonSummary? value)
|
||||||
{
|
{
|
||||||
|
OnPropertyChanged(nameof(HasLessonSelection));
|
||||||
ShowLessonCommand.NotifyCanExecuteChanged();
|
ShowLessonCommand.NotifyCanExecuteChanged();
|
||||||
EditLessonCommand.NotifyCanExecuteChanged();
|
EditLessonCommand.NotifyCanExecuteChanged();
|
||||||
DeleteLessonCommand.NotifyCanExecuteChanged();
|
DeleteLessonCommand.NotifyCanExecuteChanged();
|
||||||
@@ -150,6 +157,23 @@ public partial class PlanningTabViewModel : ObservableObject
|
|||||||
AdvanceLessonStatusCommand.NotifyCanExecuteChanged();
|
AdvanceLessonStatusCommand.NotifyCanExecuteChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
partial void OnIsReadOnlyChanged(bool value)
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(CanImportUnitPlanning));
|
||||||
|
OnPropertyChanged(nameof(CanImportLessonPlanning));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RefreshPlanning(Guid? selectUnitId = null, Guid? selectLessonId = null)
|
||||||
|
{
|
||||||
|
selectUnitId ??= SelectedUnit?.Id;
|
||||||
|
selectLessonId ??= SelectedLesson?.Id;
|
||||||
|
LoadUnits();
|
||||||
|
if (selectUnitId is Guid unitId)
|
||||||
|
SelectedUnit = Units.FirstOrDefault(u => u.Id == unitId) ?? SelectedUnit;
|
||||||
|
if (selectLessonId is Guid lessonId)
|
||||||
|
SelectedLesson = Lessons.FirstOrDefault(l => l.Id == lessonId);
|
||||||
|
}
|
||||||
|
|
||||||
private bool HasSelectedUnit() => SelectedUnit is not null;
|
private bool HasSelectedUnit() => SelectedUnit is not null;
|
||||||
private bool HasSelectedLesson() => SelectedLesson is not null;
|
private bool HasSelectedLesson() => SelectedLesson is not null;
|
||||||
private bool CanAiAssist() => SelectedUnit is not null && _aiSettings.Enabled;
|
private bool CanAiAssist() => SelectedUnit is not null && _aiSettings.Enabled;
|
||||||
|
|||||||
@@ -8,8 +8,28 @@
|
|||||||
|
|
||||||
<!-- Einheiten-Toolbar -->
|
<!-- Einheiten-Toolbar -->
|
||||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto" Margin="0,0,0,8">
|
<Grid Grid.Row="0" ColumnDefinitions="*,Auto" Margin="0,0,0,8">
|
||||||
<TextBlock Grid.Column="0" Text="Unterrichtseinheiten" FontSize="14" FontWeight="SemiBold"
|
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
||||||
VerticalAlignment="Center"/>
|
<TextBlock Text="Unterrichtseinheiten" FontSize="14" FontWeight="SemiBold"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
<Button Content="↕ JSON" FontSize="11" Padding="7,3"
|
||||||
|
ToolTip.Tip="Einheiten und Stunden importieren, exportieren oder die Formatbeschreibung kopieren">
|
||||||
|
<Button.Flyout>
|
||||||
|
<MenuFlyout Placement="BottomEdgeAlignedLeft">
|
||||||
|
<MenuItem Header="Einheit mit Stunden exportieren…" Click="OnExportUnit"
|
||||||
|
IsEnabled="{Binding HasUnitSelection}"/>
|
||||||
|
<MenuItem Header="Neue Einheit importieren…" Click="OnImportUnit"
|
||||||
|
IsEnabled="{Binding CanImportUnitPlanning}"/>
|
||||||
|
<MenuItem Header="Formatbeschreibung Einheit kopieren" Click="OnCopyUnitFormat"/>
|
||||||
|
<Separator/>
|
||||||
|
<MenuItem Header="Ausgewählte Stunde exportieren…" Click="OnExportLesson"
|
||||||
|
IsEnabled="{Binding HasLessonSelection}"/>
|
||||||
|
<MenuItem Header="Neue Stunde importieren…" Click="OnImportLesson"
|
||||||
|
IsEnabled="{Binding CanImportLessonPlanning}"/>
|
||||||
|
<MenuItem Header="Formatbeschreibung Stunde kopieren" Click="OnCopyLessonFormat"/>
|
||||||
|
</MenuFlyout>
|
||||||
|
</Button.Flyout>
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="8">
|
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="8">
|
||||||
<Button Content="+ Einheit" Command="{Binding AddUnitCommand}" IsEnabled="{Binding !IsReadOnly}"/>
|
<Button Content="+ Einheit" Command="{Binding AddUnitCommand}" IsEnabled="{Binding !IsReadOnly}"/>
|
||||||
<Button Content="Bearbeiten" Command="{Binding EditUnitCommand}" IsEnabled="{Binding !IsReadOnly}"/>
|
<Button Content="Bearbeiten" Command="{Binding EditUnitCommand}" IsEnabled="{Binding !IsReadOnly}"/>
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Input.Platform;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using Avalonia.Platform;
|
||||||
|
using Avalonia.Platform.Storage;
|
||||||
using LehrerApp.Core.Interfaces;
|
using LehrerApp.Core.Interfaces;
|
||||||
using LehrerApp.Core.Models;
|
using LehrerApp.Core.Models;
|
||||||
using LehrerApp.Core.Services;
|
using LehrerApp.Core.Services;
|
||||||
@@ -11,6 +15,12 @@ namespace LehrerApp.Desktop.Views.Groups;
|
|||||||
|
|
||||||
public partial class PlanningTabView : UserControl
|
public partial class PlanningTabView : UserControl
|
||||||
{
|
{
|
||||||
|
private static readonly FilePickerFileType JsonFileType = new("JSON-Dateien")
|
||||||
|
{
|
||||||
|
Patterns = ["*.json"],
|
||||||
|
MimeTypes = ["application/json"],
|
||||||
|
};
|
||||||
|
|
||||||
public PlanningTabView() => InitializeComponent();
|
public PlanningTabView() => InitializeComponent();
|
||||||
|
|
||||||
protected override void OnDataContextChanged(EventArgs e)
|
protected override void OnDataContextChanged(EventArgs e)
|
||||||
@@ -167,4 +177,139 @@ public partial class PlanningTabView : UserControl
|
|||||||
await dialog.ShowDialog<bool>(owner);
|
await dialog.ShowDialog<bool>(owner);
|
||||||
return dialogVm.Result;
|
return dialogVm.Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async void OnExportUnit(object? sender, RoutedEventArgs e) => await RunExchange(async () =>
|
||||||
|
{
|
||||||
|
if (DataContext is not PlanningTabViewModel { SelectedUnit: { } selected } vm) return;
|
||||||
|
var topLevel = TopLevel.GetTopLevel(this);
|
||||||
|
if (topLevel is null) return;
|
||||||
|
var file = await topLevel.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
|
||||||
|
{
|
||||||
|
Title = "Unterrichtseinheit exportieren",
|
||||||
|
SuggestedFileName = $"Einheit_{SafeFileName(selected.Title)}.json",
|
||||||
|
FileTypeChoices = [JsonFileType],
|
||||||
|
});
|
||||||
|
if (file is null) return;
|
||||||
|
var json = Exchange.ExportUnit(selected.Model, CreateExchangeContext(vm, selected.Title));
|
||||||
|
await WriteTextAsync(file, json);
|
||||||
|
Notifications.ShowSuccess("Unterrichtseinheit als JSON exportiert.");
|
||||||
|
});
|
||||||
|
|
||||||
|
private async void OnImportUnit(object? sender, RoutedEventArgs e) => await RunExchange(async () =>
|
||||||
|
{
|
||||||
|
if (DataContext is not PlanningTabViewModel { CanImportUnitPlanning: true } vm) return;
|
||||||
|
var file = await PickJsonFile("Neue Unterrichtseinheit importieren");
|
||||||
|
if (file is null) return;
|
||||||
|
var result = Exchange.ImportUnit(await ReadTextAsync(file), vm.GroupId);
|
||||||
|
vm.RefreshPlanning(result.Unit.Id, result.Lessons.FirstOrDefault()?.Id);
|
||||||
|
Notifications.ShowSuccess($"Einheit „{result.Unit.Title}“ mit {result.Lessons.Count} Stunde(n) importiert.");
|
||||||
|
});
|
||||||
|
|
||||||
|
private async void OnExportLesson(object? sender, RoutedEventArgs e) => await RunExchange(async () =>
|
||||||
|
{
|
||||||
|
if (DataContext is not PlanningTabViewModel { SelectedLesson: { } lesson } vm) return;
|
||||||
|
var topLevel = TopLevel.GetTopLevel(this);
|
||||||
|
if (topLevel is null) return;
|
||||||
|
var file = await topLevel.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
|
||||||
|
{
|
||||||
|
Title = "Stundenplanung exportieren",
|
||||||
|
SuggestedFileName = $"Stunde_{lesson.Model.Date:yyyy-MM-dd}_{SafeFileName(lesson.Topic)}.json",
|
||||||
|
FileTypeChoices = [JsonFileType],
|
||||||
|
});
|
||||||
|
if (file is null) return;
|
||||||
|
var json = Exchange.ExportLesson(lesson.Model,
|
||||||
|
CreateExchangeContext(vm, vm.SelectedUnit?.Title));
|
||||||
|
await WriteTextAsync(file, json);
|
||||||
|
Notifications.ShowSuccess("Stundenplanung als JSON exportiert.");
|
||||||
|
});
|
||||||
|
|
||||||
|
private async void OnImportLesson(object? sender, RoutedEventArgs e) => await RunExchange(async () =>
|
||||||
|
{
|
||||||
|
if (DataContext is not PlanningTabViewModel
|
||||||
|
{ CanImportLessonPlanning: true, SelectedUnit: { } unit } vm) return;
|
||||||
|
var file = await PickJsonFile("Neue Stundenplanung importieren");
|
||||||
|
if (file is null) return;
|
||||||
|
var lesson = Exchange.ImportLesson(await ReadTextAsync(file), unit.Id, vm.GroupId);
|
||||||
|
vm.RefreshPlanning(unit.Id, lesson.Id);
|
||||||
|
Notifications.ShowSuccess($"Stunde „{lesson.Topic}“ importiert.");
|
||||||
|
});
|
||||||
|
|
||||||
|
private async void OnCopyUnitFormat(object? sender, RoutedEventArgs e) =>
|
||||||
|
await CopyFormatDescription("Einheitenplanung-Format.md", "Formatbeschreibung für Einheiten kopiert.");
|
||||||
|
|
||||||
|
private async void OnCopyLessonFormat(object? sender, RoutedEventArgs e) =>
|
||||||
|
await CopyFormatDescription("Stundenplanung-Format.md", "Formatbeschreibung für Stunden kopiert.");
|
||||||
|
|
||||||
|
private async Task CopyFormatDescription(string fileName, string successMessage) => await RunExchange(async () =>
|
||||||
|
{
|
||||||
|
var clipboard = TopLevel.GetTopLevel(this)?.Clipboard;
|
||||||
|
if (clipboard is null) throw new InvalidOperationException("Die Zwischenablage ist nicht verfügbar.");
|
||||||
|
var uri = new Uri($"avares://LehrerApp.Desktop/Assets/PlanningFormats/{fileName}");
|
||||||
|
await using var stream = AssetLoader.Open(uri);
|
||||||
|
using var reader = new StreamReader(stream);
|
||||||
|
await clipboard.SetTextAsync(await reader.ReadToEndAsync());
|
||||||
|
Notifications.ShowSuccess(successMessage);
|
||||||
|
});
|
||||||
|
|
||||||
|
private async Task<IStorageFile?> PickJsonFile(string title)
|
||||||
|
{
|
||||||
|
var topLevel = TopLevel.GetTopLevel(this);
|
||||||
|
if (topLevel is null) return null;
|
||||||
|
var files = await topLevel.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
||||||
|
{
|
||||||
|
Title = title,
|
||||||
|
AllowMultiple = false,
|
||||||
|
FileTypeFilter = [JsonFileType],
|
||||||
|
});
|
||||||
|
return files.FirstOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<string> ReadTextAsync(IStorageFile file)
|
||||||
|
{
|
||||||
|
await using var stream = await file.OpenReadAsync();
|
||||||
|
using var reader = new StreamReader(stream);
|
||||||
|
return await reader.ReadToEndAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task WriteTextAsync(IStorageFile file, string content)
|
||||||
|
{
|
||||||
|
await using var stream = await file.OpenWriteAsync();
|
||||||
|
stream.SetLength(0);
|
||||||
|
await using var writer = new StreamWriter(stream);
|
||||||
|
await writer.WriteAsync(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PlanningExchangeContext CreateExchangeContext(PlanningTabViewModel vm, string? unitTitle) => new()
|
||||||
|
{
|
||||||
|
Group = vm.GroupLabel,
|
||||||
|
Subject = vm.SubjectName,
|
||||||
|
GradeLevel = vm.GradeLevel,
|
||||||
|
UnitTitle = unitTitle,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string SafeFileName(string value)
|
||||||
|
{
|
||||||
|
var invalid = Path.GetInvalidFileNameChars().ToHashSet();
|
||||||
|
var safe = new string(value.Select(c => invalid.Contains(c) ? '_' : c).ToArray()).Trim();
|
||||||
|
return string.IsNullOrWhiteSpace(safe) ? "Planung" : safe;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RunExchange(Func<Task> action)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await action();
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is PlanningExchangeException or IOException
|
||||||
|
or UnauthorizedAccessException or InvalidOperationException)
|
||||||
|
{
|
||||||
|
Notifications.ShowError(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PlanningExchangeService Exchange =>
|
||||||
|
App.Services.GetRequiredService<PlanningExchangeService>();
|
||||||
|
|
||||||
|
private static NotificationService Notifications =>
|
||||||
|
App.Services.GetRequiredService<NotificationService>();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user