waffensachkunde

Waffensachkunde – Lernsoftware für die Sachkundeprüfung nach § 7 WaffG. Barrierefrei, offline, EUPL-1.2.

/ app src main fragendruck-export.ts

7,9 KB Rohdatei
app/src/main/fragendruck-export.ts — 228 Zeilen
1 /**
2 * Der Weg vom Katalog zu Fehlerprotokoll und Fragenliste.
3 *
4 * Beide Dokumente holen ihre Fragen aus dem Anwendungskern und nicht aus der
5 * Oberfläche – aus demselben Grund wie der Lernbericht: Ein Dokument nennt
6 * eine Quelle und steht damit für seinen Inhalt gerade. Reichte die Oberfläche
7 * die Fragen mit, könnte ein Anzeigefehler in ein Papier gelangen, das jemand
8 * für bare Münze nimmt.
9 *
10 * **Die Fehlermenge kommt aus `lernstand.sitzung`**, nicht aus einer eigenen
11 * Abfrage. Damit ist zugesichert, was der Auftrag verlangt: Was auf dem
12 * Startbildschirm hinter „Nur Fehler“ steht, steht hier auf dem Papier. Eine
13 * zweite Zählweise daneben wäre die Art Widerspruch, die in dieser Anwendung
14 * schon zweimal eine Kennzahl gekostet hat.
15 */
16
17 import type { BrowserWindow } from 'electron';
18
19 import { fundstelleText } from '../shared/erklaerungen';
20 import {
21 fehlerprotokollBauen,
22 fehlerprotokollDateiname,
23 type Erklaerungstiefe,
24 } from '../shared/druck/fehlerprotokoll';
25 import { fragenlisteBauen, fragenlisteDateiname } from '../shared/druck/fragenliste';
26 import type { Papierfrage } from '../shared/druck/frageblock';
27 import type { Druckergebnis } from '../shared/ipc';
28 import type { Frage, Katalog } from '../shared/katalog';
29 import {
30 papierfragen,
31 quelleAusKatalog,
32 schriftgroessePruefen,
33 speichern,
34 type Zeitgeber,
35 } from './dokument-export';
36 import { erklaerungenLaden } from './erklaerungen';
37 import type { Lernstand } from './lernstand';
38
39 export interface Fragendruckumgebung {
40 readonly lernstand: Lernstand;
41 readonly katalog: Katalog;
42 readonly elternfenster: BrowserWindow | null;
43 readonly jetzt: Zeitgeber;
44 }
45
46 /** Alternativtexte je Bild-ID; sie liegen bereits im Katalog. */
47 function alttexteAus(katalog: Katalog): ReadonlyMap<string, string> {
48 return new Map(katalog.bilder.map((b) => [b.id, b.alt ?? '']));
49 }
50
51 /** Erklärungen in der Form, die der Papierbaustein erwartet. */
52 function erklaerungenFuer(
53 ids: ReadonlySet<string>,
54 ): ReadonlyMap<string, Papierfrage['erklaerung']> {
55 const bestand = erklaerungenLaden().zuFrage;
56 const karte = new Map<string, Papierfrage['erklaerung']>();
57 for (const id of ids) {
58 const e = bestand[id];
59 if (e === undefined) {
60 continue;
61 }
62 karte.set(id, {
63 kurz: e.kurz,
64 text: e.text,
65 ...(e.merksatz === undefined ? {} : { merksatz: e.merksatz }),
66 fundstellen: e.fundstellen.map(fundstelleText),
67 /* Ohne dieses Feld verlöre das Papier bei 84 der 575 Fragen die
68 Auskunft, dass es keine gesetzliche Grundlage gibt – und behauptete
69 im Vorspann trotzdem Fundstellen zu jeder Frage. */
70 ...(e.ohneFundstelleGrund === undefined
71 ? {}
72 : { ohneFundstelleGrund: e.ohneFundstelleGrund }),
73 });
74 }
75 return karte;
76 }
77
78 function tiefePruefen(wert: unknown): Erklaerungstiefe {
79 return wert === 'voll' ? 'voll' : 'kurz';
80 }
81
82 // ── Fehlerprotokoll ─────────────────────────────────────────────────────
83
84 export async function fehlerprotokollExportieren(
85 umgebung: Fragendruckumgebung,
86 profilIdRoh: unknown,
87 schriftgroesseRoh: unknown,
88 tiefeRoh: unknown,
89 ): Promise<Druckergebnis> {
90 const { lernstand, katalog, elternfenster, jetzt } = umgebung;
91
92 /* Derselbe Weg wie der Startbildschirm, nur ohne Deckel und ohne Mischen:
93 Das Dokument muss ALLE betroffenen Fragen enthalten, und zwar in
94 Katalogreihenfolge. `sitzung` prüft die Profil-ID mit denselben Regeln
95 wie überall sonst. */
96 const auswahl = lernstand.sitzung(profilIdRoh, {
97 nurFehler: true,
98 mischen: false,
99 anzahl: katalog.fragen.length,
100 });
101
102 const nachId = new Map(katalog.fragen.map((f) => [f.id, f]));
103 const fragen = auswahl
104 .map((s) => nachId.get(s.frageId))
105 .filter((f): f is Frage => f !== undefined);
106
107 const profilId = typeof profilIdRoh === 'number' ? profilIdRoh : -1;
108 const profilName = lernstand.profile().find((p) => p.id === profilId)?.name ?? 'Ohne Profil';
109 const erstelltAm = jetzt().toISOString();
110 const tiefe = tiefePruefen(tiefeRoh);
111
112 const html = fehlerprotokollBauen({
113 fragen: papierfragen(
114 fragen,
115 katalog,
116 alttexteAus(katalog),
117 erklaerungenFuer(new Set(fragen.map((f) => f.id))),
118 ),
119 profilName,
120 erstelltAm,
121 tiefe,
122 schriftgroesse: schriftgroessePruefen(schriftgroesseRoh),
123 quelle: quelleAusKatalog(katalog, {
124 art: 'wiedergabe',
125 fragen: fragen.length,
126 fragenGesamt: katalog.fragen.length,
127 loesungen: 'alle',
128 optionen: 'alle',
129 }),
130 });
131
132 return speichern({
133 html,
134 quelle: quelleAusKatalog(katalog, {
135 art: 'wiedergabe',
136 fragen: fragen.length,
137 fragenGesamt: katalog.fragen.length,
138 loesungen: 'alle',
139 optionen: 'alle',
140 }),
141 dialogtitel: 'Fehlerprotokoll speichern',
142 dateiname: fehlerprotokollDateiname(profilName, erstelltAm),
143 elternfenster,
144 });
145 }
146
147 // ── Fragenliste ─────────────────────────────────────────────────────────
148
149 /**
150 * Grenzt den Katalog auf die gewählten Bereiche ein.
151 *
152 * Eine leere Auswahl bedeutet **alles**. Das ist die einzige sinnvolle
153 * Lesart: Wer nichts einschränkt, will den ganzen Bogen – und der
154 * Quellenblock sagt dann „alle 575 Fragen“ statt „einen Teil“.
155 */
156 function eingegrenzt(katalog: Katalog, bereiche: readonly string[]): Frage[] {
157 if (bereiche.length === 0) {
158 return [...katalog.fragen];
159 }
160 const gewaehlt = new Set(bereiche);
161 return katalog.fragen.filter(
162 (f) => gewaehlt.has(f.kapitel) || (f.abschnitt !== null && gewaehlt.has(f.abschnitt)),
163 );
164 }
165
166 /** Überschrift der Auswahl – nennt Bereiche beim Titel, nicht bei der Kennung. */
167 function auswahlTitel(katalog: Katalog, bereiche: readonly string[]): string {
168 if (bereiche.length === 0) {
169 return 'gesamter Fragenkatalog';
170 }
171 const titel = new Map<string, string>();
172 for (const kapitel of katalog.kapitel) {
173 titel.set(kapitel.id, `${kapitel.id} – ${kapitel.titel}`);
174 for (const abschnitt of kapitel.abschnitte) {
175 titel.set(abschnitt.id, `${abschnitt.id} – ${abschnitt.titel}`);
176 }
177 }
178 return bereiche.map((b) => titel.get(b) ?? b).join(', ');
179 }
180
181 export async function fragenlisteExportieren(
182 umgebung: Fragendruckumgebung,
183 bereicheRoh: unknown,
184 schriftgroesseRoh: unknown,
185 mitLoesungenRoh: unknown,
186 ): Promise<Druckergebnis> {
187 const { katalog, elternfenster, jetzt } = umgebung;
188
189 const bereiche = Array.isArray(bereicheRoh)
190 ? bereicheRoh.filter((b): b is string => typeof b === 'string')
191 : [];
192 const fragen = eingegrenzt(katalog, bereiche);
193
194 /* Ohne ausdrückliche Angabe MIT Lösungsanhang: Der Fall „ganzer Katalog
195 ohne Lösungen“ ist der einzige, in dem gar keine Auszugserwägung mehr
196 trägt – 575 von 575 Fragen, 0 von 629 Markierungen. Solange die Anfrage
197 beim Bundesverwaltungsamt aussteht, ist die vorsichtige Vorgabe die
198 richtige. Siehe LICENSES/LicenseRef-Amtliches-Werk-BVA.txt Nummer 2a. */
199 const mitLoesungen = mitLoesungenRoh !== false;
200 const erstelltAm = jetzt().toISOString();
201 const auswahl = auswahlTitel(katalog, bereiche);
202
203 const umfang = {
204 art: 'wiedergabe',
205 fragen: fragen.length,
206 fragenGesamt: katalog.fragen.length,
207 loesungen: mitLoesungen ? ('alle' as const) : ('keine' as const),
208 optionen: 'alle' as const,
209 } as const;
210
211 const html = fragenlisteBauen({
212 fragen: papierfragen(fragen, katalog, alttexteAus(katalog), new Map()),
213 auswahl,
214 erstelltAm,
215 mitLoesungen,
216 kapitel: katalog.kapitel,
217 schriftgroesse: schriftgroessePruefen(schriftgroesseRoh),
218 quelle: quelleAusKatalog(katalog, umfang),
219 });
220
221 return speichern({
222 html,
223 quelle: quelleAusKatalog(katalog, umfang),
224 dialogtitel: 'Fragenliste speichern',
225 dateiname: fragenlisteDateiname(auswahl, erstelltAm),
226 elternfenster,
227 });
228 }