Baustein 10a: PWA, Offlinebetrieb, Druckansichten

Installierbar: Manifest, Symbole aus dem eigenen Zeichen erzeugt
(scripts/symbole.ts, kein Bilddienst), Verknuepfungen zu Lernmodus,
Berichtsheft und Handouts.

Offline gilt eine Regel, und die steht im Dienstarbeiter: Nur das
Lernmaterial liegt im Browserspeicher - Handouts, Merkkarten, Glossar,
die eigene Woche. Lernzielkontrollen, Berichtsheft, Bewertungen,
Feedbackgespraeche, Anhaenge und jedes PDF niemals. Das ist keine
Sparsamkeit: Was im Browsercache liegt, ist an der serverseitigen
Rechtepruefung vorbei.

Bewertungen ohne Netz gehen in eine Warteschlange und werden
nachgereicht. Die Schlange haengt an der Benutzerkennung, nicht am
Geraet - auf einem geteilten Werkstattgeraet bekaeme sonst der Zweite
die offenen Bewertungen des Ersten aufs Konto. Der Tag der Bewertung
kommt vom Geraet und wird auf heute gedeckelt (tagDerBewertung, 4
Tests): Eine vorgestellte Uhr darf das Wiederholen nicht wegstellen.

Druck: Wer die dunkle Fassung eingestellt hatte, druckte bisher schwarze
Flaechen. Im Druck wird jetzt die komplette helle Palette gesetzt, dazu
A4-Raender, keine Umbrueche mitten in Karten und Zeilen, aufgeklappte
details. Druckknoepfe bei Handout, Glossar und Auswertung.

Durchstich in scripts/pruefungen/baustein-10.mjs mit echtem Abschalten
des Netzes: 33 Pruefungen, darunter der Nachweis, dass nichts
Bewertungsrelevantes im Speicher landet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-09-11 15:47:06 +02:00
co-authored by Claude Opus 5
parent be2a674c7a
commit 2babef9293
24 changed files with 1126 additions and 6 deletions
@@ -3,6 +3,7 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import Link from 'next/link';
import { karteBewerten, type Bewertungsergebnis } from '../aktionen';
import { einreihen } from '@/lib/offline/warteschlange';
export type Lernkarte = {
id: string;
@@ -30,7 +31,15 @@ const knoepfe: { bewertung: Bewertung; text: string; taste: string; klasse: stri
* Abkürzung, kein Überfahren mit der Maus. Am Rechner tun es zusätzlich die
* Tasten 1, 2, 3 und die Leertaste.
*/
export function Lernmodus({ karten, block }: { karten: Lernkarte[]; block: string | null }) {
export function Lernmodus({
karten,
block,
benutzerId,
}: {
karten: Lernkarte[];
block: string | null;
benutzerId: string;
}) {
const [stapel, setzeStapel] = useState<Lernkarte[]>(karten);
const [stelle, setzeStelle] = useState(0);
const [umgedreht, setzeUmgedreht] = useState(false);
@@ -48,7 +57,7 @@ export function Lernmodus({ karten, block }: { karten: Lernkarte[]; block: strin
if (!karte || laeuft) return;
setzeLaeuft(true);
const ergebnis = await karteBewerten(karte.id, bewertung);
const ergebnis = await bewertungAbsetzen(benutzerId, karte.id, bewertung);
setzeRueckmeldung(ergebnis);
setzeZaehler((z) => ({ ...z, [bewertung]: z[bewertung] + 1 }));
setzeErledigt((e) => e + 1);
@@ -63,7 +72,7 @@ export function Lernmodus({ karten, block }: { karten: Lernkarte[]; block: strin
setzeStelle((s) => s + 1);
setzeLaeuft(false);
},
[karte, laeuft],
[karte, laeuft, benutzerId],
);
useEffect(() => {
@@ -239,3 +248,32 @@ export function Lernmodus({ karten, block }: { karten: Lernkarte[]; block: strin
</div>
);
}
/**
* Eine Bewertung absetzen – und wenn kein Netz da ist, sie merken.
*
* Ohne Netz scheitert die Serveraktion mit einem Netzfehler. Dann geht die
* Bewertung in die Warteschlange und wird nachgereicht, sobald die Verbindung
* wieder steht. Wann die Karte dann wiederkommt, sagt der Server; hier wird
* bewusst nichts ausgerechnet, was später anders ausfallen könnte.
*/
async function bewertungAbsetzen(
benutzerId: string,
karteId: string,
bewertung: Bewertung,
): Promise<Bewertungsergebnis> {
try {
return await karteBewerten(karteId, bewertung);
} catch {
einreihen(benutzerId, { karteId, bewertung, erfasstAm: heutigesDatum() });
window.dispatchEvent(new Event('lernbar-warteschlange'));
return { wiederAm: 'gemerkt – geht ab, sobald wieder Netz da ist' };
}
}
/** Der heutige Tag nach der Uhr des Geräts, als `JJJJ-MM-TT`. */
function heutigesDatum(): string {
const jetzt = new Date();
const zweistellig = (zahl: number) => String(zahl).padStart(2, '0');
return `${jetzt.getFullYear()}-${zweistellig(jetzt.getMonth() + 1)}-${zweistellig(jetzt.getDate())}`;
}
+1 -1
View File
@@ -73,5 +73,5 @@ export default async function LernSeite({ searchParams }: { searchParams: Promis
);
}
return <Lernmodus karten={karten} block={gewaehlterBlock ?? null} />;
return <Lernmodus karten={karten} block={gewaehlterBlock ?? null} benutzerId={benutzer.id} />;
}