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:
co-authored by
Claude Opus 5
parent
be2a674c7a
commit
2babef9293
@@ -6,6 +6,7 @@ import { benutzerNachId } from '@/lib/repos/benutzer';
|
||||
import { eigenerStand, rahmenplanabdeckung } from '@/lib/auswertung/sammeln';
|
||||
import { bilanz, nachGruppen, stufenText, type Abdeckungsstufe } from '@/lib/auswertung/abdeckung';
|
||||
import { verlauf } from '@/lib/kontrollen/bewertung';
|
||||
import { Druckknopf } from '@/komponenten/Druckknopf';
|
||||
|
||||
export const metadata: Metadata = { title: 'Auswertung' };
|
||||
export const dynamic = 'force-dynamic';
|
||||
@@ -72,6 +73,12 @@ export default async function AuswertungsSeite({ searchParams }: { searchParams:
|
||||
<Rahmenplan azubiId={azubiId} />
|
||||
<Kontrollverlauf azubiId={azubiId} />
|
||||
<Lernfelder azubiId={azubiId} />
|
||||
|
||||
{/* Die Abdeckung ist die Ansicht, die man zum Gespräch mitnimmt – und zwar
|
||||
auf Papier, weil dort niemand scrollt. */}
|
||||
<div className="reihe nicht-drucken">
|
||||
<Druckknopf beschriftung="Auswertung drucken" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { inhalte } from '@/lib/inhalte';
|
||||
import { glossarAnker } from '@/lib/inhalte/glossarverweise';
|
||||
import { normalisiere } from '@/lib/inhalte/laden';
|
||||
import { GlossarSuche } from './GlossarSuche';
|
||||
import { Druckknopf } from '@/komponenten/Druckknopf';
|
||||
|
||||
export const metadata: Metadata = { title: 'Glossar' };
|
||||
export const dynamic = 'force-dynamic';
|
||||
@@ -67,6 +68,10 @@ export default async function GlossarSeite({ searchParams }: { searchParams: Pro
|
||||
))}
|
||||
</dl>
|
||||
)}
|
||||
|
||||
<div className="reihe nicht-drucken">
|
||||
<Druckknopf beschriftung="Glossar drucken" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { verlinkeGlossar } from '@/lib/inhalte/glossarverweise';
|
||||
import { gelesenAm } from '@/lib/repos/fortschritt';
|
||||
import { alsZeitpunktText } from '@/lib/zeit';
|
||||
import { gelesenUmschalten } from './aktionen';
|
||||
import { Druckknopf } from '@/komponenten/Druckknopf';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
@@ -159,6 +160,10 @@ export default async function HandoutSeite({ params }: { params: Promise<{ id: s
|
||||
</form>
|
||||
) : null}
|
||||
|
||||
<div className="reihe nicht-drucken">
|
||||
<Druckknopf beschriftung="Handout drucken" />
|
||||
</div>
|
||||
|
||||
<nav className="reihe nicht-drucken" style={{ justifyContent: 'space-between' }} aria-label="Weitere Handouts">
|
||||
{vorher ? (
|
||||
<Link href={`/handouts/${vorher.id}`} className="knopf knopf-still">
|
||||
|
||||
@@ -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())}`;
|
||||
}
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { erfordereBenutzer } from '@/lib/auth/rechte';
|
||||
import { Kopfzeile, type Navigationseintrag } from '@/komponenten/Kopfzeile';
|
||||
import { Netzzustand } from '@/komponenten/Netzzustand';
|
||||
import { abmelden } from '../anmelden/aktionen';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
@@ -49,6 +50,7 @@ export default async function AngemeldetLayout({ children }: { children: React.R
|
||||
rolle={benutzer.rolle === 'ausbilder' ? 'Ausbilder' : 'Azubi'}
|
||||
abmeldeAktion={abmelden}
|
||||
/>
|
||||
<Netzzustand benutzerId={benutzer.id} />
|
||||
<main id="inhalt" className="huelle">
|
||||
{children}
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user