waffensachkunde

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

/ app src shared druck fehlerprotokoll.ts

4,0 KB Rohdatei
app/src/shared/druck/fehlerprotokoll.ts — 104 Zeilen
1 /**
2 * Das Fehlerprotokoll – die Fragen, die zuletzt falsch beantwortet wurden.
3 *
4 * Das Dokument, mit dem jemand ohne Rechner weiterarbeitet. Es enthält genau
5 * die Menge, die auf dem Startbildschirm hinter „Nur Fehler“ steht: Was dort
6 * als Fehler gilt, steht hier auf dem Papier. Eine zweite Zählweise daneben
7 * wäre die Art Widerspruch, die diese Anwendung schon zweimal abgeschafft hat.
8 *
9 * **Die Antwortmöglichkeiten stehen vollständig da.** Nachgemessen nennen
10 * 302 der 471 Erklärungen zu Auswahlfragen eine *falsche* Option beim
11 * Buchstaben; ein Dokument mit nur der richtigen Antwort machte seine eigenen
12 * Begründungen unlesbar.
13 *
14 * **Der Umfang ist erheblich und wird vorher gesagt.** Gemessen an wirklich
15 * gedruckten PDF: 60 Fragen mit Kurzerklärung ergeben 21 Seiten, 150 Fragen
16 * mit voller Begründung 161 Seiten, und der schlimmste Fall – 486 Fragen,
17 * volle Begründung, Großdruck – 982 Seiten. Die Oberfläche nennt die
18 * geschätzte Zahl, bevor der Speicherdialog aufgeht.
19 */
20
21 import { absatz, dokumentBauen, type Quellenangabe } from './dokument';
22 import { frageblock, type Papierfrage } from './frageblock';
23 import type { Schriftgroesse } from './stil';
24
25 /** Wie ausführlich die Begründungen ausfallen. */
26 export type Erklaerungstiefe = 'kurz' | 'voll';
27
28 export interface Fehlerprotokolldaten {
29 readonly fragen: readonly Papierfrage[];
30 readonly profilName: string;
31 readonly erstelltAm: string;
32 readonly tiefe: Erklaerungstiefe;
33 readonly quelle: Quellenangabe;
34 readonly schriftgroesse: Schriftgroesse;
35 }
36
37 /** Deutsches Datum – dieselbe Formatierung wie im Lernbericht. */
38 function datum(iso: string): string {
39 const wert = new Date(iso);
40 return Number.isNaN(wert.getTime())
41 ? 'ohne Datum'
42 : wert.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
43 }
44
45 /**
46 * Der Vorspann.
47 *
48 * Er sagt, worauf die Auswahl beruht, und er sagt es im Singular, wenn es
49 * eine einzige Frage ist – der Fall ist erreichbar und kommt nach einer
50 * guten Sitzung sogar häufig vor.
51 */
52 function vorspann(anzahl: number, tiefe: Erklaerungstiefe): string {
53 if (anzahl === 0) {
54 return absatz(
55 'In diesem Lernprofil ist derzeit keine Frage als zuletzt falsch beantwortet vermerkt. ' +
56 'Das Dokument bleibt deshalb leer.',
57 );
58 }
59
60 const menge =
61 anzahl === 1
62 ? 'Hier steht die eine Frage, die Sie zuletzt falsch beantwortet haben.'
63 : `Hier stehen die ${String(anzahl)} Fragen, die Sie zuletzt falsch beantwortet haben.`;
64
65 const nachtrag =
66 tiefe === 'voll'
67 ? 'Zu jeder Frage stehen die vollständige Begründung und die Fundstellen im Gesetz.'
68 : 'Zu jeder Frage steht die Kurzerklärung. Die ausführliche Begründung lässt sich beim Erzeugen zuschalten.';
69
70 return absatz(
71 `${menge} Sobald Sie eine davon wieder richtig beantworten, verschwindet sie aus dieser Liste. ${nachtrag}`,
72 );
73 }
74
75 export function fehlerprotokollBauen(daten: Fehlerprotokolldaten): string {
76 const form = { loesungZeigen: true, erklaerung: daten.tiefe } as const;
77
78 const bloecke = daten.fragen.map((papier) => frageblock(papier, form, 3));
79
80 return dokumentBauen({
81 titel: 'Fehlerprotokoll Waffensachkunde',
82 augenbraue: `${daten.profilName} · erstellt am ${datum(daten.erstelltAm)}`,
83 quelle: daten.quelle,
84 schriftgroesse: daten.schriftgroesse,
85 abschnitte: [
86 `<h2>Ihre offenen Fehler</h2>\n${vorspann(daten.fragen.length, daten.tiefe)}\n${bloecke.join('\n')}`,
87 ],
88 });
89 }
90
91 /** Vorschlag für den Dateinamen; enthält keine Zeichen, die Dateisysteme stören. */
92 export function fehlerprotokollDateiname(profilName: string, erstelltAm: string): string {
93 const tag = Number.isNaN(Date.parse(erstelltAm))
94 ? 'ohne-datum'
95 : new Date(erstelltAm).toISOString().slice(0, 10);
96 const name = profilName
97 .normalize('NFKD')
98 .replace(/[̀-ͯ]/gu, '')
99 .replace(/[^A-Za-z0-9]+/gu, '-')
100 .replace(/^-+|-+$/gu, '')
101 .slice(0, 40);
102
103 return `Fehlerprotokoll${name === '' ? '' : `-${name}`}-${tag}.pdf`;
104 }