Merkkarten, Praxisaufgaben, Checklisten, Berichtsheft, Lernzielkontrollen und Feedbackgespraech lassen sich unter Verwaltung -> Module fuer die ganze Lern-Bar ab- und einschalten. Wochenplan, Handouts, Glossar, Auswertung und Verwaltung bleiben immer an. Gesperrt wird an drei Stellen, nicht im Layout (Next rendert Layouts beim Seitenwechsel darunter nicht neu): Seiten mit erfordereModul, Routen fuer PDF, Anhaenge und Lernstand mit modulAn, alle 37 Serveraktionen der Module mit pruefeModul. Die Anmeldepruefung kommt jeweils zuerst, damit ein Unangemeldeter nicht am 404 abliest, was abgeschaltet ist. Ein abgeschaltetes Modul traegt nirgends bei: Wochenplan, Fortschritt, Was-wartet, Mein Stand, Ergebnisverlauf, Rahmenplanabdeckung, Vorschlaege fuers Berichtsheft, Karten im Handout, Kurzbefehle im Manifest. Die Lernstand-Schnittstelle antwortet mit 410, das Geraet verwirft daraufhin seine Offline-Warteschlange. Abschalten loescht nichts. Gespeichert in modul_einstellung (Migration 0010); fehlt die Zeile, ist das Modul an, eine bestehende Datenbank verhaelt sich also wie vorher. 12 Tests fuer die Regeln, Abschnitt A8 im Abnahmelauf, Anleitung Kapitel 9 mit Bildschirmfoto, README, E43. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
193 lines
7.0 KiB
TypeScript
193 lines
7.0 KiB
TypeScript
import type { Metadata } from 'next';
|
||
import Link from 'next/link';
|
||
import { erfordereBenutzer } from '@/lib/auth/rechte';
|
||
import { inhalte } from '@/lib/inhalte';
|
||
import { alsKartenstand, staendeVon, uebersicht } from '@/lib/repos/karten';
|
||
import { fach, fachNamen, heutigerStapel } from '@/lib/lernen/sm2';
|
||
import { alsDatumText, heute } from '@/lib/zeit';
|
||
import { erfordereModul } from '@/lib/module/pruefen';
|
||
|
||
export const metadata: Metadata = { title: 'Merkkarten' };
|
||
export const dynamic = 'force-dynamic';
|
||
|
||
const STAPELGROESSE = 20;
|
||
|
||
export default async function KartenSeite() {
|
||
const benutzer = await erfordereBenutzer();
|
||
erfordereModul('karten');
|
||
const alle = inhalte();
|
||
const heuteDatum = heute();
|
||
|
||
const istAzubi = benutzer.rolle === 'azubi';
|
||
const staende = istAzubi ? staendeVon(benutzer.id) : new Map();
|
||
const stand = istAzubi
|
||
? uebersicht(benutzer.id, alle.karten.map((k) => k.id), heuteDatum)
|
||
: { gesamt: alle.karten.length, gelernt: 0, faellig: 0, nieGelernt: alle.karten.length, jeFach: { 1: alle.karten.length, 2: 0, 3: 0, 4: 0, 5: 0 } };
|
||
|
||
const faelligeAlle = heutigerStapel(
|
||
alle.karten.map((karte) => ({
|
||
karteId: karte.id,
|
||
faelligAm: staende.get(karte.id)?.faelligAm ?? null,
|
||
block: karte.block,
|
||
})),
|
||
heuteDatum,
|
||
9999,
|
||
);
|
||
|
||
const jeBlock = alle.bloecke.map((block) => {
|
||
const desBlocks = alle.karten.filter((k) => k.block === block.id);
|
||
const faellig = faelligeAlle.filter((f) => f.block === block.id).length;
|
||
const gelernt = desBlocks.filter((k) => {
|
||
const zeile = staende.get(k.id);
|
||
return zeile ? fach(alsKartenstand(zeile)) >= 4 : false;
|
||
}).length;
|
||
return { block, gesamt: desBlocks.length, faellig, gelernt };
|
||
});
|
||
|
||
if (!istAzubi) {
|
||
return (
|
||
<div className="stapel">
|
||
<div>
|
||
<h1>Merkkarten</h1>
|
||
<p className="leise">
|
||
{alle.karten.length} Karten aus {alle.handouts.length} Handouts. Gelernt wird nur vom Azubi – du
|
||
siehst hier, was es gibt.
|
||
</p>
|
||
</div>
|
||
|
||
<div className="raster">
|
||
{jeBlock.map(({ block, gesamt }) => (
|
||
<section key={block.id} className="karte">
|
||
<h2 style={{ fontSize: '1rem', marginBottom: '0.2rem' }}>{block.titel}</h2>
|
||
<p className="leise" style={{ marginBottom: 0 }}>
|
||
{gesamt} {gesamt === 1 ? 'Karte' : 'Karten'}
|
||
</p>
|
||
</section>
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const heuteStapel = Math.min(stand.faellig, STAPELGROESSE);
|
||
|
||
return (
|
||
<div className="stapel">
|
||
<div>
|
||
<h1>Merkkarten</h1>
|
||
<p className="leise">
|
||
{stand.gesamt} Karten insgesamt · {stand.gelernt} sitzen · {stand.nieGelernt} noch nie gelernt
|
||
</p>
|
||
</div>
|
||
|
||
<section className="karte-grund stapel" aria-labelledby="heute">
|
||
<h2 id="heute" style={{ marginBottom: 0 }}>
|
||
Heute fällig
|
||
</h2>
|
||
|
||
{stand.faellig === 0 ? (
|
||
<p style={{ marginBottom: 0 }}>
|
||
Nichts. Alle Karten liegen weiter hinten – morgen ist wieder etwas dran.
|
||
</p>
|
||
) : (
|
||
<>
|
||
<p style={{ marginBottom: 0 }}>
|
||
<strong style={{ fontSize: '2rem' }}>{heuteStapel}</strong>{' '}
|
||
{heuteStapel === 1 ? 'Karte' : 'Karten'}
|
||
{stand.faellig > STAPELGROESSE ? (
|
||
<span className="leise"> von {stand.faellig} fälligen – der Rest kommt morgen</span>
|
||
) : null}
|
||
</p>
|
||
<div>
|
||
<Link href="/karten/lernen" className="knopf knopf-akzent">
|
||
Lernen starten
|
||
</Link>
|
||
</div>
|
||
</>
|
||
)}
|
||
</section>
|
||
|
||
<section className="stapel" aria-labelledby="bloecke">
|
||
<div>
|
||
<h2 id="bloecke" style={{ marginBottom: '0.2rem' }}>
|
||
Nach Themenblock
|
||
</h2>
|
||
<p className="leise" style={{ margin: 0 }}>
|
||
Vor einer Lernzielkontrolle gezielt üben – hier sind auch Karten dabei, die eigentlich erst später
|
||
dran wären.
|
||
</p>
|
||
</div>
|
||
|
||
<div className="raster">
|
||
{jeBlock.map(({ block, gesamt, faellig, gelernt }) => (
|
||
<section key={block.id} className="karte stapel-eng">
|
||
<h3 style={{ fontSize: '1rem', marginBottom: '0.2rem' }}>{block.titel}</h3>
|
||
<p className="leise" style={{ margin: 0 }}>
|
||
{gesamt} Karten · {gelernt} sitzen · {faellig} heute fällig
|
||
</p>
|
||
<div className="balken" aria-hidden="true">
|
||
<span style={{ width: `${gesamt === 0 ? 0 : Math.round((gelernt / gesamt) * 100)}%` }} />
|
||
</div>
|
||
<div>
|
||
<Link
|
||
href={`/karten/lernen?block=${block.id}`}
|
||
className={`knopf ${faellig > 0 ? '' : 'knopf-zweit'}`}
|
||
>
|
||
{faellig > 0 ? `${Math.min(faellig, STAPELGROESSE)} lernen` : 'Trotzdem üben'}
|
||
</Link>
|
||
</div>
|
||
</section>
|
||
))}
|
||
</div>
|
||
</section>
|
||
|
||
<section className="karte stapel-eng" aria-labelledby="faecher">
|
||
<h2 id="faecher" style={{ fontSize: '1rem', marginBottom: '0.2rem' }}>
|
||
Wie fest die Karten sitzen
|
||
</h2>
|
||
<ul className="faecher">
|
||
{[1, 2, 3, 4, 5].map((nummer) => {
|
||
const anzahl = stand.jeFach[nummer] ?? 0;
|
||
const anteil = stand.gesamt === 0 ? 0 : Math.round((anzahl / stand.gesamt) * 100);
|
||
return (
|
||
<li key={nummer}>
|
||
<span className="fach-nummer">{nummer}</span>
|
||
<span className="fach-name">{fachNamen[nummer]}</span>
|
||
<span className="fach-balken" aria-hidden="true">
|
||
<span style={{ width: `${anteil}%` }} />
|
||
</span>
|
||
<span className="fach-zahl">{anzahl}</span>
|
||
</li>
|
||
);
|
||
})}
|
||
</ul>
|
||
</section>
|
||
|
||
{stand.gesamt > stand.nieGelernt ? (
|
||
<section className="karte" aria-labelledby="naechste">
|
||
<h2 id="naechste" style={{ fontSize: '1rem', marginBottom: '0.4rem' }}>
|
||
Als Nächstes fällig
|
||
</h2>
|
||
<ul className="stapel-eng" style={{ listStyle: 'none', margin: 0, padding: 0 }}>
|
||
{[...staende.values()]
|
||
.filter((z) => z.faelligAm > heuteDatum)
|
||
.sort((a, b) => a.faelligAm.localeCompare(b.faelligAm))
|
||
.slice(0, 5)
|
||
.map((z) => {
|
||
const karte = alle.karteNach.get(z.karteId);
|
||
if (!karte) return null;
|
||
return (
|
||
<li key={z.karteId} className="reihe" style={{ gap: '0.5rem', flexWrap: 'nowrap' }}>
|
||
<span className="handout-kuerzel">{karte.handoutId}</span>
|
||
<span style={{ flex: 1 }}>{karte.frage}</span>
|
||
<span className="leise">{alsDatumText(z.faelligAm)}</span>
|
||
</li>
|
||
);
|
||
})}
|
||
</ul>
|
||
</section>
|
||
) : null}
|
||
</div>
|
||
);
|
||
}
|