Baustein 2: Inhalte einlesen und pruefen, Handouts, Suche, Glossar
Die fachlichen Inhalte liegen als Dateien unter content/ und werden beim Start eingelesen, geprueft und in den Speicher geladen. - Inhaltslader mit Zod-Schemata und Querverweispruefung - Fehlermeldungen nennen Datei und Zeile, gesammelt statt beim ersten Fehler geworfen; die Anwendung startet bei Fehlern nicht - npm run inhalte:pruefen prueft ohne Start - Handout-Ansicht mit festem Aufbau, Uebersicht nach Themenbloecken - Volltextsuche mit Gewichtung und Trefferausschnitt - Gelesen-Markierung je Handout - Glossar mit Suche und automatischer Verlinkung in den Handouts - Beispielinhalte: 4 Wochen, 12 Handouts, 24 Merkkarten, 4 Aufgaben, 4 Checklisten, 2 Lernzielkontrollen, 24 Glossareintraege - rahmenplan.yaml aus FIAusbV 2020 und KMK-Rahmenlehrplan, mit Fundstellen - gray-matter entfernt, YAML-Kopf wird selbst herausgeloest - npm run screenshots nimmt die Bilder fuer die Anleitung auf - 39 neue Tests, insgesamt 68 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e856349cd4
commit
1af5613e9b
@@ -0,0 +1,38 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
export function GlossarSuche({ anfangswert }: { anfangswert: string }) {
|
||||
const [wert, setzeWert] = useState(anfangswert);
|
||||
const router = useRouter();
|
||||
const erstesMal = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (erstesMal.current) {
|
||||
erstesMal.current = false;
|
||||
return;
|
||||
}
|
||||
const zeitgeber = setTimeout(() => {
|
||||
router.replace(wert.trim() ? `/glossar?suche=${encodeURIComponent(wert.trim())}` : '/glossar', { scroll: false });
|
||||
}, 200);
|
||||
return () => clearTimeout(zeitgeber);
|
||||
}, [wert, router]);
|
||||
|
||||
return (
|
||||
<form action="/glossar" method="get" role="search" className="nicht-drucken">
|
||||
<label htmlFor="glossar-suche" className="nur-lesehilfe">
|
||||
Glossar durchsuchen
|
||||
</label>
|
||||
<input
|
||||
id="glossar-suche"
|
||||
name="suche"
|
||||
type="search"
|
||||
placeholder="Begriff suchen – z. B. Subnetzmaske"
|
||||
value={wert}
|
||||
onChange={(e) => setzeWert(e.target.value)}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import type { Metadata } from 'next';
|
||||
import Link from 'next/link';
|
||||
import { erfordereBenutzer } from '@/lib/auth/rechte';
|
||||
import { inhalte } from '@/lib/inhalte';
|
||||
import { glossarAnker } from '@/lib/inhalte/glossarverweise';
|
||||
import { normalisiere } from '@/lib/inhalte/laden';
|
||||
import { GlossarSuche } from './GlossarSuche';
|
||||
|
||||
export const metadata: Metadata = { title: 'Glossar' };
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export default async function GlossarSeite({ searchParams }: { searchParams: Promise<{ suche?: string }> }) {
|
||||
await erfordereBenutzer();
|
||||
const parameter = await searchParams;
|
||||
const alle = inhalte();
|
||||
|
||||
const suchbegriff = (parameter.suche ?? '').trim();
|
||||
const gesucht = normalisiere(suchbegriff);
|
||||
|
||||
const eintraege = [...alle.glossar].sort((a, b) => a.begriff.localeCompare(b.begriff, 'de-DE'));
|
||||
const gefiltert = gesucht
|
||||
? eintraege.filter(
|
||||
(e) =>
|
||||
normalisiere(e.begriff).includes(gesucht) ||
|
||||
normalisiere(e.steht_fuer ?? '').includes(gesucht) ||
|
||||
normalisiere(e.erklaerung).includes(gesucht) ||
|
||||
e.formen.some((f) => normalisiere(f).includes(gesucht)),
|
||||
)
|
||||
: eintraege;
|
||||
|
||||
return (
|
||||
<div className="stapel">
|
||||
<div>
|
||||
<h1>Glossar</h1>
|
||||
<p className="leise">
|
||||
{alle.glossar.length} Begriffe. In den Handouts sind sie verlinkt – ein Klick führt hierher.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<GlossarSuche anfangswert={suchbegriff} />
|
||||
|
||||
{gefiltert.length === 0 ? (
|
||||
<p className="karte">Kein Begriff passt zu „{suchbegriff}“.</p>
|
||||
) : (
|
||||
<dl className="glossar">
|
||||
{gefiltert.map((eintrag) => (
|
||||
<div key={eintrag.begriff} id={glossarAnker(eintrag.begriff)} className="glossar-eintrag karte">
|
||||
<dt>
|
||||
{eintrag.begriff}
|
||||
{eintrag.steht_fuer ? <span className="glossar-langform">{eintrag.steht_fuer}</span> : null}
|
||||
</dt>
|
||||
<dd>
|
||||
<p style={{ marginBottom: eintrag.siehe_auch.length > 0 ? '0.5rem' : 0 }}>{eintrag.erklaerung}</p>
|
||||
{eintrag.siehe_auch.length > 0 ? (
|
||||
<p className="leise" style={{ margin: 0 }}>
|
||||
Siehe auch:{' '}
|
||||
{eintrag.siehe_auch.map((verweis, index) => (
|
||||
<span key={verweis}>
|
||||
{index > 0 ? ', ' : ''}
|
||||
<Link href={`#${glossarAnker(verweis)}`}>{verweis}</Link>
|
||||
</span>
|
||||
))}
|
||||
</p>
|
||||
) : null}
|
||||
</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
/**
|
||||
* Suchfeld für die Handouts. Sucht beim Tippen mit kurzer Verzögerung, damit
|
||||
* nicht bei jedem Buchstaben eine Anfrage losgeht, und funktioniert auch ohne
|
||||
* JavaScript – dann eben erst beim Absenden.
|
||||
*/
|
||||
export function Suchfeld({ anfangswert }: { anfangswert: string }) {
|
||||
const [wert, setzeWert] = useState(anfangswert);
|
||||
const router = useRouter();
|
||||
const erstesMal = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (erstesMal.current) {
|
||||
erstesMal.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const zeitgeber = setTimeout(() => {
|
||||
const ziel = wert.trim() ? `/handouts?suche=${encodeURIComponent(wert.trim())}` : '/handouts';
|
||||
router.replace(ziel, { scroll: false });
|
||||
}, 250);
|
||||
|
||||
return () => clearTimeout(zeitgeber);
|
||||
}, [wert, router]);
|
||||
|
||||
return (
|
||||
<form action="/handouts" method="get" role="search" className="nicht-drucken">
|
||||
<label htmlFor="suche" className="nur-lesehilfe">
|
||||
Handouts durchsuchen
|
||||
</label>
|
||||
<div className="reihe" style={{ gap: '0.5rem', flexWrap: 'nowrap' }}>
|
||||
<input
|
||||
id="suche"
|
||||
name="suche"
|
||||
type="search"
|
||||
placeholder="Suchen – z. B. Subnetting, SMART, Vererbung"
|
||||
value={wert}
|
||||
onChange={(e) => setzeWert(e.target.value)}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<button type="submit" className="knopf knopf-zweit" style={{ flexShrink: 0 }}>
|
||||
Suchen
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
'use server';
|
||||
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { erfordereBenutzer } from '@/lib/auth/rechte';
|
||||
import { inhalte } from '@/lib/inhalte';
|
||||
import { alsGelesenMerken, alsUngelesenMerken } from '@/lib/repos/fortschritt';
|
||||
|
||||
/**
|
||||
* Gelesen-Markierung umschalten. Nur der Azubi führt seinen eigenen Fortschritt –
|
||||
* der Ausbilder liest mit, hakt aber nichts für ihn ab.
|
||||
*/
|
||||
export async function gelesenUmschalten(formular: FormData): Promise<void> {
|
||||
const benutzer = await erfordereBenutzer();
|
||||
if (benutzer.rolle !== 'azubi') return;
|
||||
|
||||
const handoutId = String(formular.get('handoutId') ?? '');
|
||||
if (!inhalte().handoutNach.has(handoutId)) return;
|
||||
|
||||
const gelesen = String(formular.get('gelesen') ?? '') === '1';
|
||||
if (gelesen) {
|
||||
alsGelesenMerken(benutzer.id, handoutId);
|
||||
} else {
|
||||
alsUngelesenMerken(benutzer.id, handoutId);
|
||||
}
|
||||
|
||||
revalidatePath(`/handouts/${handoutId}`);
|
||||
revalidatePath('/handouts');
|
||||
revalidatePath('/');
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
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';
|
||||
|
||||
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}
|
||||
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import type { Metadata } from 'next';
|
||||
import Link from 'next/link';
|
||||
import { erfordereBenutzer } from '@/lib/auth/rechte';
|
||||
import { inhalte } from '@/lib/inhalte';
|
||||
import { sucheHandouts } from '@/lib/inhalte/suche';
|
||||
import { gelesenSet } from '@/lib/repos/fortschritt';
|
||||
import { Suchfeld } from './Suchfeld';
|
||||
|
||||
export const metadata: Metadata = { title: 'Handouts' };
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export default async function HandoutListe({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ suche?: string; block?: string }>;
|
||||
}) {
|
||||
const benutzer = await erfordereBenutzer();
|
||||
const parameter = await searchParams;
|
||||
const alle = inhalte();
|
||||
const gelesen = benutzer.rolle === 'azubi' ? gelesenSet(benutzer.id) : new Set<string>();
|
||||
|
||||
const suchbegriff = (parameter.suche ?? '').trim();
|
||||
const treffer = suchbegriff ? sucheHandouts(alle, suchbegriff) : null;
|
||||
|
||||
const gefilterterBlock = parameter.block && alle.blockNach.has(parameter.block) ? parameter.block : null;
|
||||
|
||||
return (
|
||||
<div className="stapel">
|
||||
<div>
|
||||
<h1>Handouts</h1>
|
||||
<p className="leise">
|
||||
{alle.handouts.length} Lerneinheiten in {alle.bloecke.length} Themenblöcken
|
||||
{benutzer.rolle === 'azubi' ? ` · ${gelesen.size} gelesen` : ''}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Suchfeld anfangswert={suchbegriff} />
|
||||
|
||||
{treffer ? (
|
||||
<section className="stapel" aria-labelledby="treffer-ueberschrift">
|
||||
<h2 id="treffer-ueberschrift">
|
||||
{treffer.length === 0
|
||||
? `Nichts gefunden für „${suchbegriff}"`
|
||||
: `${treffer.length} ${treffer.length === 1 ? 'Treffer' : 'Treffer'} für „${suchbegriff}"`}
|
||||
</h2>
|
||||
|
||||
{treffer.length === 0 ? (
|
||||
<p className="karte" style={{ marginBottom: 0 }}>
|
||||
Versuch es mit einem einzelnen Wort. Gesucht wird in Titel, Merksatz, Merkkarten und im gesamten Text.
|
||||
</p>
|
||||
) : (
|
||||
<ul className="stapel-eng" style={{ listStyle: 'none', margin: 0, padding: 0 }}>
|
||||
{treffer.map(({ handout, ausschnitt }) => (
|
||||
<li key={handout.id}>
|
||||
<Link href={`/handouts/${handout.id}`} className="handout-treffer">
|
||||
<span className="handout-kuerzel">{handout.id}</span>
|
||||
<span>
|
||||
<strong>{handout.titel}</strong>
|
||||
<br />
|
||||
<span className="leise">{ausschnitt}</span>
|
||||
</span>
|
||||
{gelesen.has(handout.id) ? (
|
||||
<span className="marke-status" data-ton="gut">
|
||||
✓ gelesen
|
||||
</span>
|
||||
) : null}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
) : (
|
||||
<>
|
||||
<nav className="reihe nicht-drucken" aria-label="Themenblöcke">
|
||||
<Link href="/handouts" className={`knopf ${gefilterterBlock ? 'knopf-zweit' : ''}`} scroll={false}>
|
||||
Alle
|
||||
</Link>
|
||||
{alle.bloecke.map((block) => (
|
||||
<Link
|
||||
key={block.id}
|
||||
href={`/handouts?block=${block.id}`}
|
||||
className={`knopf ${gefilterterBlock === block.id ? '' : 'knopf-zweit'}`}
|
||||
scroll={false}
|
||||
>
|
||||
{block.titel}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{alle.bloecke
|
||||
.filter((block) => !gefilterterBlock || block.id === gefilterterBlock)
|
||||
.map((block) => {
|
||||
const desBlocks = alle.handoutsNachBlock.get(block.id) ?? [];
|
||||
if (desBlocks.length === 0) return null;
|
||||
const gelesenImBlock = desBlocks.filter((h) => gelesen.has(h.id)).length;
|
||||
|
||||
return (
|
||||
<section key={block.id} className="stapel" aria-labelledby={`block-${block.id}`}>
|
||||
<div className="reihe" style={{ justifyContent: 'space-between' }}>
|
||||
<h2 id={`block-${block.id}`} style={{ margin: 0 }}>
|
||||
{block.titel}
|
||||
</h2>
|
||||
{benutzer.rolle === 'azubi' ? (
|
||||
<span className="leise">
|
||||
{gelesenImBlock} von {desBlocks.length} gelesen
|
||||
</span>
|
||||
) : (
|
||||
<span className="leise">{desBlocks.length} Handouts</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ul className="raster" style={{ listStyle: 'none', margin: 0, padding: 0 }}>
|
||||
{desBlocks.map((handout) => (
|
||||
<li key={handout.id}>
|
||||
<Link href={`/handouts/${handout.id}`} className="handout-kachel">
|
||||
<span className="reihe" style={{ justifyContent: 'space-between', gap: '0.5rem' }}>
|
||||
<span className="handout-kuerzel">{handout.id}</span>
|
||||
{gelesen.has(handout.id) ? (
|
||||
<span className="marke-status" data-ton="gut">
|
||||
✓ gelesen
|
||||
</span>
|
||||
) : (
|
||||
<span className="leise" style={{ fontSize: '0.8rem' }}>
|
||||
Woche {handout.woche}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<strong style={{ display: 'block', marginTop: '0.4rem' }}>{handout.titel}</strong>
|
||||
<span className="leise" style={{ display: 'block', marginTop: '0.2rem' }}>
|
||||
{handout.worum}
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -6,10 +6,16 @@ export const dynamic = 'force-dynamic';
|
||||
|
||||
// Die Navigation wächst mit den Bausteinen mit – hier stehen nur Ziele,
|
||||
// die es auch wirklich gibt.
|
||||
const fuerAzubi: Navigationseintrag[] = [{ pfad: '/', titel: 'Woche' }];
|
||||
const fuerAzubi: Navigationseintrag[] = [
|
||||
{ pfad: '/', titel: 'Woche' },
|
||||
{ pfad: '/handouts', titel: 'Handouts' },
|
||||
{ pfad: '/glossar', titel: 'Glossar' },
|
||||
];
|
||||
|
||||
const fuerAusbilder: Navigationseintrag[] = [
|
||||
{ pfad: '/', titel: 'Überblick' },
|
||||
{ pfad: '/handouts', titel: 'Handouts' },
|
||||
{ pfad: '/glossar', titel: 'Glossar' },
|
||||
{ pfad: '/verwaltung', titel: 'Verwaltung' },
|
||||
];
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { erfordereAusbilder } from '@/lib/auth/rechte';
|
||||
import { passwortMangel } from '@/lib/auth/passwort';
|
||||
import { aktivSetzen, benutzerAnlegen, benutzerNachEmail, benutzerNachId, passwortSetzen } from '@/lib/repos/benutzer';
|
||||
import { ausbildungAendern, ausbildungAnlegen, ausbildungNachAzubi, ausbildungNachId } from '@/lib/repos/ausbildung';
|
||||
import { alsDatumText } from '@/lib/zeit';
|
||||
|
||||
export type Rueckmeldung = { fehler?: string; erfolg?: string };
|
||||
|
||||
@@ -39,7 +40,7 @@ export async function azubiAnlegen(_vorher: Rueckmeldung, formular: FormData): P
|
||||
});
|
||||
|
||||
revalidatePath('/verwaltung');
|
||||
return { erfolg: `Konto für ${name} angelegt und Ausbildung ab ${beginn} eingerichtet.` };
|
||||
return { erfolg: `Konto für ${name} angelegt und Ausbildung ab ${alsDatumText(beginn)} eingerichtet.` };
|
||||
}
|
||||
|
||||
export async function ausbildungBearbeiten(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
||||
|
||||
Reference in New Issue
Block a user