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>
190 lines
6.8 KiB
TypeScript
190 lines
6.8 KiB
TypeScript
import type { Metadata } from 'next';
|
||
import Link from 'next/link';
|
||
import { notFound } from 'next/navigation';
|
||
import { erfordereBenutzer } from '@/lib/auth/rechte';
|
||
import { inhalte } from '@/lib/inhalte';
|
||
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';
|
||
|
||
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {
|
||
const { id } = await params;
|
||
const handout = inhalte().handoutNach.get(id.toUpperCase());
|
||
return { title: handout ? `${handout.id} – ${handout.titel}` : 'Handout' };
|
||
}
|
||
|
||
export default async function HandoutSeite({ params }: { params: Promise<{ id: string }> }) {
|
||
const benutzer = await erfordereBenutzer();
|
||
const { id } = await params;
|
||
const alle = inhalte();
|
||
const handout = alle.handoutNach.get(id.toUpperCase());
|
||
|
||
if (!handout) notFound();
|
||
|
||
const block = alle.blockNach.get(handout.block);
|
||
const lernfeld = alle.lernfeldNach.get(handout.lernfeld);
|
||
const positionen = handout.rahmenplan.map((p) => alle.positionNach.get(p)).filter((p) => p !== undefined);
|
||
const woche = alle.wocheNach.get(handout.woche);
|
||
const istAzubi = benutzer.rolle === 'azubi';
|
||
const gelesen = istAzubi ? gelesenAm(benutzer.id, handout.id) : null;
|
||
|
||
const desBlocks = alle.handoutsNachBlock.get(handout.block) ?? [];
|
||
const stelle = desBlocks.findIndex((h) => h.id === handout.id);
|
||
const vorher = stelle > 0 ? desBlocks[stelle - 1] : undefined;
|
||
const nachher = stelle < desBlocks.length - 1 ? desBlocks[stelle + 1] : undefined;
|
||
|
||
const wesentlichHtml = handout.wesentlich ? verlinkeGlossar(handout.wesentlich.html, alle.glossar) : '';
|
||
|
||
return (
|
||
<article className="stapel handout">
|
||
<nav className="leise nicht-drucken" aria-label="Wo du bist">
|
||
<Link href="/handouts">Handouts</Link>
|
||
{block ? (
|
||
<>
|
||
{' › '}
|
||
<Link href={`/handouts?block=${block.id}`}>{block.titel}</Link>
|
||
</>
|
||
) : null}
|
||
</nav>
|
||
|
||
<header className="stapel-eng">
|
||
<div className="reihe" style={{ gap: '0.6rem' }}>
|
||
<span className="handout-kuerzel handout-kuerzel-gross">{handout.id}</span>
|
||
{gelesen ? (
|
||
<span className="marke-status" data-ton="gut">
|
||
✓ gelesen am {alsZeitpunktText(gelesen)}
|
||
</span>
|
||
) : null}
|
||
</div>
|
||
<h1 style={{ marginBottom: '0.2rem' }}>{handout.titel}</h1>
|
||
<p className="handout-worum">{handout.worum}</p>
|
||
</header>
|
||
|
||
<div className="handout-zuordnung karte">
|
||
<dl>
|
||
<div>
|
||
<dt>Ausbildungsrahmenplan</dt>
|
||
<dd>
|
||
{positionen.length === 0
|
||
? '–'
|
||
: positionen.map((p) => (
|
||
<span key={p.id} title={`${p.titel} (${p.verordnung})`} className="zuordnung-marke">
|
||
<strong>{p.id}</strong> {p.kurz}
|
||
</span>
|
||
))}
|
||
</dd>
|
||
</div>
|
||
<div>
|
||
<dt>Lernfeld</dt>
|
||
<dd>
|
||
{lernfeld ? (
|
||
<span className="zuordnung-marke" title={`${lernfeld.stunden} Stunden im ${lernfeld.jahr}. Jahr`}>
|
||
<strong>{lernfeld.id}</strong> {lernfeld.titel}
|
||
</span>
|
||
) : (
|
||
'–'
|
||
)}
|
||
</dd>
|
||
</div>
|
||
<div>
|
||
<dt>Woche</dt>
|
||
<dd>
|
||
{woche ? (
|
||
<span className="zuordnung-marke">
|
||
<strong>{woche.nummer}</strong> {woche.titel}
|
||
</span>
|
||
) : (
|
||
'–'
|
||
)}
|
||
</dd>
|
||
</div>
|
||
</dl>
|
||
</div>
|
||
|
||
{handout.wesentlich ? (
|
||
<section aria-labelledby="wesentlich">
|
||
<h2 id="wesentlich">Das Wesentliche</h2>
|
||
<div className="fliesstext" dangerouslySetInnerHTML={{ __html: wesentlichHtml }} />
|
||
</section>
|
||
) : null}
|
||
|
||
{handout.uebersicht ? (
|
||
<section aria-labelledby="uebersicht">
|
||
<h2 id="uebersicht">Übersicht</h2>
|
||
<div className="tabelle-huelle fliesstext" dangerouslySetInnerHTML={{ __html: handout.uebersicht.html }} />
|
||
</section>
|
||
) : null}
|
||
|
||
<aside className="merksatz" aria-label="Merksatz">
|
||
<span className="merksatz-kennzeichnung">Merksatz</span>
|
||
<p>{handout.merksatz}</p>
|
||
</aside>
|
||
|
||
{handout.pruefungsfrage ? (
|
||
<details className="pruefungsfrage">
|
||
<summary>Prüfungsfrage anzeigen</summary>
|
||
<div className="fliesstext" dangerouslySetInnerHTML={{ __html: handout.pruefungsfrage.html }} />
|
||
</details>
|
||
) : null}
|
||
|
||
<section aria-labelledby="karten" className="karte stapel-eng">
|
||
<h2 id="karten" style={{ marginBottom: '0.3rem' }}>
|
||
Merkkarten zu diesem Handout
|
||
</h2>
|
||
<p className="leise" style={{ margin: 0 }}>
|
||
{handout.karten.length === 1 ? 'Eine Karte' : `${handout.karten.length} Karten`} – im Lernmodus tauchen sie
|
||
nach Fälligkeit auf.
|
||
</p>
|
||
<ol className="stapel-eng" style={{ margin: '0.5rem 0 0', paddingLeft: '1.2rem' }}>
|
||
{handout.karten.map((karte, index) => (
|
||
<li key={index}>
|
||
<strong>{karte.frage}</strong>
|
||
<br />
|
||
<span className="leise">{karte.antwort}</span>
|
||
</li>
|
||
))}
|
||
</ol>
|
||
</section>
|
||
|
||
{istAzubi ? (
|
||
<form action={gelesenUmschalten} className="nicht-drucken">
|
||
<input type="hidden" name="handoutId" value={handout.id} />
|
||
<input type="hidden" name="gelesen" value={gelesen ? '0' : '1'} />
|
||
<button type="submit" className={`knopf ${gelesen ? 'knopf-zweit' : 'knopf-akzent'}`}>
|
||
{gelesen ? 'Doch noch nicht gelesen' : 'Als gelesen markieren'}
|
||
</button>
|
||
</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">
|
||
← {vorher.id} {vorher.titel}
|
||
</Link>
|
||
) : (
|
||
<span />
|
||
)}
|
||
{nachher ? (
|
||
<Link href={`/handouts/${nachher.id}`} className="knopf knopf-still">
|
||
{nachher.id} {nachher.titel} →
|
||
</Link>
|
||
) : (
|
||
<span />
|
||
)}
|
||
</nav>
|
||
|
||
<p className="leise nicht-drucken" style={{ marginBottom: 0 }}>
|
||
Quelle: <code>{handout.datei}</code>
|
||
</p>
|
||
</article>
|
||
);
|
||
}
|