/** * Der Weg vom Katalog zu Fehlerprotokoll und Fragenliste. * * Beide Dokumente holen ihre Fragen aus dem Anwendungskern und nicht aus der * Oberfläche – aus demselben Grund wie der Lernbericht: Ein Dokument nennt * eine Quelle und steht damit für seinen Inhalt gerade. Reichte die Oberfläche * die Fragen mit, könnte ein Anzeigefehler in ein Papier gelangen, das jemand * für bare Münze nimmt. * * **Die Fehlermenge kommt aus `lernstand.sitzung`**, nicht aus einer eigenen * Abfrage. Damit ist zugesichert, was der Auftrag verlangt: Was auf dem * Startbildschirm hinter „Nur Fehler“ steht, steht hier auf dem Papier. Eine * zweite Zählweise daneben wäre die Art Widerspruch, die in dieser Anwendung * schon zweimal eine Kennzahl gekostet hat. */ import type { BrowserWindow } from 'electron'; import { fundstelleText } from '../shared/erklaerungen'; import { fehlerprotokollBauen, fehlerprotokollDateiname, type Erklaerungstiefe, } from '../shared/druck/fehlerprotokoll'; import { fragenlisteBauen, fragenlisteDateiname } from '../shared/druck/fragenliste'; import type { Papierfrage } from '../shared/druck/frageblock'; import type { Druckergebnis } from '../shared/ipc'; import type { Frage, Katalog } from '../shared/katalog'; import { papierfragen, quelleAusKatalog, schriftgroessePruefen, speichern, type Zeitgeber, } from './dokument-export'; import { erklaerungenLaden } from './erklaerungen'; import type { Lernstand } from './lernstand'; export interface Fragendruckumgebung { readonly lernstand: Lernstand; readonly katalog: Katalog; readonly elternfenster: BrowserWindow | null; readonly jetzt: Zeitgeber; } /** Alternativtexte je Bild-ID; sie liegen bereits im Katalog. */ function alttexteAus(katalog: Katalog): ReadonlyMap { return new Map(katalog.bilder.map((b) => [b.id, b.alt ?? ''])); } /** Erklärungen in der Form, die der Papierbaustein erwartet. */ function erklaerungenFuer( ids: ReadonlySet, ): ReadonlyMap { const bestand = erklaerungenLaden().zuFrage; const karte = new Map(); for (const id of ids) { const e = bestand[id]; if (e === undefined) { continue; } karte.set(id, { kurz: e.kurz, text: e.text, ...(e.merksatz === undefined ? {} : { merksatz: e.merksatz }), fundstellen: e.fundstellen.map(fundstelleText), }); } return karte; } function tiefePruefen(wert: unknown): Erklaerungstiefe { return wert === 'voll' ? 'voll' : 'kurz'; } // ── Fehlerprotokoll ───────────────────────────────────────────────────── export async function fehlerprotokollExportieren( umgebung: Fragendruckumgebung, profilIdRoh: unknown, schriftgroesseRoh: unknown, tiefeRoh: unknown, ): Promise { const { lernstand, katalog, elternfenster, jetzt } = umgebung; /* Derselbe Weg wie der Startbildschirm, nur ohne Deckel und ohne Mischen: Das Dokument muss ALLE betroffenen Fragen enthalten, und zwar in Katalogreihenfolge. `sitzung` prüft die Profil-ID mit denselben Regeln wie überall sonst. */ const auswahl = lernstand.sitzung(profilIdRoh, { nurFehler: true, mischen: false, anzahl: katalog.fragen.length, }); const nachId = new Map(katalog.fragen.map((f) => [f.id, f])); const fragen = auswahl .map((s) => nachId.get(s.frageId)) .filter((f): f is Frage => f !== undefined); const profilId = typeof profilIdRoh === 'number' ? profilIdRoh : -1; const profilName = lernstand.profile().find((p) => p.id === profilId)?.name ?? 'Ohne Profil'; const erstelltAm = jetzt().toISOString(); const tiefe = tiefePruefen(tiefeRoh); const html = fehlerprotokollBauen({ fragen: papierfragen( fragen, katalog, alttexteAus(katalog), erklaerungenFuer(new Set(fragen.map((f) => f.id))), ), profilName, erstelltAm, tiefe, schriftgroesse: schriftgroessePruefen(schriftgroesseRoh), quelle: quelleAusKatalog(katalog, { art: 'wiedergabe', fragen: fragen.length, fragenGesamt: katalog.fragen.length, loesungen: 'alle', optionen: 'alle', }), }); return speichern({ html, quelle: quelleAusKatalog(katalog, { art: 'wiedergabe', fragen: fragen.length, fragenGesamt: katalog.fragen.length, loesungen: 'alle', optionen: 'alle', }), dialogtitel: 'Fehlerprotokoll speichern', dateiname: fehlerprotokollDateiname(profilName, erstelltAm), elternfenster, }); } // ── Fragenliste ───────────────────────────────────────────────────────── /** * Grenzt den Katalog auf die gewählten Bereiche ein. * * Eine leere Auswahl bedeutet **alles**. Das ist die einzige sinnvolle * Lesart: Wer nichts einschränkt, will den ganzen Bogen – und der * Quellenblock sagt dann „alle 575 Fragen“ statt „einen Teil“. */ function eingegrenzt(katalog: Katalog, bereiche: readonly string[]): Frage[] { if (bereiche.length === 0) { return [...katalog.fragen]; } const gewaehlt = new Set(bereiche); return katalog.fragen.filter( (f) => gewaehlt.has(f.kapitel) || (f.abschnitt !== null && gewaehlt.has(f.abschnitt)), ); } /** Überschrift der Auswahl – nennt Bereiche beim Titel, nicht bei der Kennung. */ function auswahlTitel(katalog: Katalog, bereiche: readonly string[]): string { if (bereiche.length === 0) { return 'gesamter Fragenkatalog'; } const titel = new Map(); for (const kapitel of katalog.kapitel) { titel.set(kapitel.id, `${kapitel.id} – ${kapitel.titel}`); for (const abschnitt of kapitel.abschnitte) { titel.set(abschnitt.id, `${abschnitt.id} – ${abschnitt.titel}`); } } return bereiche.map((b) => titel.get(b) ?? b).join(', '); } export async function fragenlisteExportieren( umgebung: Fragendruckumgebung, bereicheRoh: unknown, schriftgroesseRoh: unknown, mitLoesungenRoh: unknown, ): Promise { const { katalog, elternfenster, jetzt } = umgebung; const bereiche = Array.isArray(bereicheRoh) ? bereicheRoh.filter((b): b is string => typeof b === 'string') : []; const fragen = eingegrenzt(katalog, bereiche); /* Ohne ausdrückliche Angabe MIT Lösungsanhang: Der Fall „ganzer Katalog ohne Lösungen“ ist der einzige, in dem gar keine Auszugserwägung mehr trägt – 575 von 575 Fragen, 0 von 629 Markierungen. Solange die Anfrage beim Bundesverwaltungsamt aussteht, ist die vorsichtige Vorgabe die richtige. Siehe LICENSES/LicenseRef-Amtliches-Werk-BVA.txt Nummer 2a. */ const mitLoesungen = mitLoesungenRoh !== false; const erstelltAm = jetzt().toISOString(); const auswahl = auswahlTitel(katalog, bereiche); const umfang = { art: 'wiedergabe', fragen: fragen.length, fragenGesamt: katalog.fragen.length, loesungen: mitLoesungen ? ('alle' as const) : ('keine' as const), optionen: 'alle' as const, } as const; const html = fragenlisteBauen({ fragen: papierfragen(fragen, katalog, alttexteAus(katalog), new Map()), auswahl, erstelltAm, mitLoesungen, kapitel: katalog.kapitel, schriftgroesse: schriftgroessePruefen(schriftgroesseRoh), quelle: quelleAusKatalog(katalog, umfang), }); return speichern({ html, quelle: quelleAusKatalog(katalog, umfang), dialogtitel: 'Fragenliste speichern', dateiname: fragenlisteDateiname(auswahl, erstelltAm), elternfenster, }); }