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>
304 lines
11 KiB
TypeScript
304 lines
11 KiB
TypeScript
import type { Metadata } from 'next';
|
||
import Link from 'next/link';
|
||
import { notFound } from 'next/navigation';
|
||
import { benutzerNachId } from '@/lib/repos/benutzer';
|
||
import { ausbildungNachAzubi } from '@/lib/repos/ausbildung';
|
||
import { inhalte } from '@/lib/inhalte';
|
||
import { alsWochenblatt, notizenIm } from '@/lib/repos/berichtsheft';
|
||
import { staendeVon as aufgabenStaende } from '@/lib/repos/aufgaben';
|
||
import { laeufeVon } from '@/lib/repos/checklisten';
|
||
import { gelesenSet } from '@/lib/repos/fortschritt';
|
||
import { planVon } from '@/lib/repos/wochenplanung';
|
||
import { baueVorschlaege, type Rohdaten } from '@/lib/berichtsheft/vorschlaege';
|
||
import { pruefeWochenblatt, summeBetrieb, summeSchule, summeStunden } from '@/lib/berichtsheft/pruefung';
|
||
import { alsDatumText, alsZeitpunktText, kalenderwocheText } from '@/lib/zeit';
|
||
import { Blattformular } from '../Blattformular';
|
||
import { Formulierungshilfe } from '../Formulierungshilfe';
|
||
import { BerichtsMarke } from '../BerichtsMarke';
|
||
import { Abzeichnen } from './Abzeichnen';
|
||
import { blattZurueckziehen, blattZumLesen } from '../aktionen';
|
||
import { erfordereModul } from '@/lib/module/pruefen';
|
||
import { erfordereBenutzer } from '@/lib/auth/rechte';
|
||
import { modulStand } from '@/lib/repos/module';
|
||
|
||
export const dynamic = 'force-dynamic';
|
||
|
||
export async function generateMetadata({ params }: { params: Promise<{ berichtId: string }> }): Promise<Metadata> {
|
||
const { berichtId } = await params;
|
||
const gelesen = await blattZumLesen(berichtId);
|
||
return { title: gelesen ? `Wochenblatt Nr. ${gelesen.bericht.nr}` : 'Wochenblatt' };
|
||
}
|
||
|
||
export default async function BlattSeite({ params }: { params: Promise<{ berichtId: string }> }) {
|
||
await erfordereBenutzer();
|
||
erfordereModul('berichtsheft');
|
||
const { berichtId } = await params;
|
||
|
||
// Rechteprüfung steckt hier drin: kein Zugriff bedeutet null bedeutet 404.
|
||
const gelesen = await blattZumLesen(berichtId);
|
||
if (!gelesen) notFound();
|
||
|
||
const { benutzer, bericht } = gelesen;
|
||
const eigenes = bericht.benutzerId === benutzer.id;
|
||
const azubi = eigenes ? benutzer : benutzerNachId(bericht.benutzerId);
|
||
if (!azubi) notFound();
|
||
|
||
const blatt = alsWochenblatt(bericht);
|
||
const warnungen = pruefeWochenblatt(blatt);
|
||
const vorschlaege = eigenes ? sammleVorschlaege(azubi.id, bericht.von, bericht.bis) : [];
|
||
|
||
return (
|
||
<article className="stapel">
|
||
<nav className="leise nicht-drucken" aria-label="Wo du bist">
|
||
<Link href="/berichtsheft">Berichtsheft</Link>
|
||
{!eigenes ? ` › ${azubi.name}` : ''}
|
||
</nav>
|
||
|
||
<header className="stapel-eng">
|
||
<div className="reihe" style={{ gap: '0.6rem' }}>
|
||
<span className="blatt-nr blatt-nr-gross">Nr. {bericht.nr}</span>
|
||
<BerichtsMarke status={bericht.status} />
|
||
<span className="leise">{kalenderwocheText(bericht.von)}</span>
|
||
</div>
|
||
|
||
<h1 style={{ marginBottom: '0.2rem' }}>
|
||
{alsDatumText(bericht.von)} – {alsDatumText(bericht.bis)}
|
||
</h1>
|
||
<p className="leise">
|
||
{azubi.name}
|
||
{bericht.eingereichtAm ? ` · eingereicht ${alsZeitpunktText(bericht.eingereichtAm)}` : ''}
|
||
</p>
|
||
</header>
|
||
|
||
{bericht.status === 'abgezeichnet' ? (
|
||
<p className="hinweis hinweis-gut">
|
||
✓ Abgezeichnet am {alsZeitpunktText(bericht.abgezeichnetAm)}
|
||
{bericht.abgezeichnetVon ? ` von ${benutzerNachId(bericht.abgezeichnetVon)?.name ?? 'dem Ausbilder'}` : ''}.
|
||
Das Blatt ist gesperrt.
|
||
</p>
|
||
) : null}
|
||
|
||
{bericht.status === 'zurueckgegeben' && bericht.anmerkung ? (
|
||
<div className="hinweis hinweis-warnung">
|
||
<strong>Zur Nacharbeit zurückgegeben:</strong>
|
||
<p className="notiz-anzeige" style={{ marginTop: '0.4rem' }}>
|
||
{bericht.anmerkung}
|
||
</p>
|
||
</div>
|
||
) : null}
|
||
|
||
{bericht.status === 'abgezeichnet' && bericht.anmerkung ? (
|
||
<div className="karte">
|
||
<strong>Anmerkung des Ausbilders</strong>
|
||
<p className="notiz-anzeige" style={{ marginTop: '0.4rem' }}>
|
||
{bericht.anmerkung}
|
||
</p>
|
||
</div>
|
||
) : null}
|
||
|
||
{eigenes ? (
|
||
<Blattformular
|
||
berichtId={bericht.id}
|
||
anfangsblatt={blatt}
|
||
vorschlaege={vorschlaege}
|
||
status={bericht.status}
|
||
/>
|
||
) : (
|
||
<AnsichtFuerAusbilder blatt={blatt} warnungen={warnungen} />
|
||
)}
|
||
|
||
{/* --- Werkzeuge --- */}
|
||
|
||
<div className="reihe nicht-drucken">
|
||
<a href={`/berichtsheft/${bericht.id}/pdf`} className="knopf knopf-zweit" target="_blank" rel="noreferrer">
|
||
Dieses Blatt als PDF
|
||
</a>
|
||
|
||
{eigenes && bericht.status === 'eingereicht' ? (
|
||
<form action={blattZurueckziehen}>
|
||
<input type="hidden" name="berichtId" value={bericht.id} />
|
||
<button type="submit" className="knopf knopf-still">
|
||
Einreichung zurückziehen
|
||
</button>
|
||
</form>
|
||
) : null}
|
||
</div>
|
||
|
||
{!eigenes ? (
|
||
<section className="karte-grund stapel nicht-drucken" aria-labelledby="abzeichnen">
|
||
<h2 id="abzeichnen" style={{ marginBottom: 0 }}>
|
||
Abzeichnen
|
||
</h2>
|
||
<Abzeichnen berichtId={bericht.id} anmerkung={bericht.anmerkung ?? ''} status={bericht.status} />
|
||
</section>
|
||
) : null}
|
||
|
||
{eigenes ? <Formulierungshilfe /> : null}
|
||
</article>
|
||
);
|
||
}
|
||
|
||
/** Die Nur-Lese-Ansicht für den Ausbilder – dieselben Daten, ohne Formular. */
|
||
function AnsichtFuerAusbilder({
|
||
blatt,
|
||
warnungen,
|
||
}: {
|
||
blatt: ReturnType<typeof alsWochenblatt>;
|
||
warnungen: ReturnType<typeof pruefeWochenblatt>;
|
||
}) {
|
||
return (
|
||
<div className="stapel">
|
||
<section className="karte stapel-eng" aria-labelledby="betrieb-lesen">
|
||
<div className="reihe" style={{ justifyContent: 'space-between' }}>
|
||
<h2 id="betrieb-lesen" style={{ margin: 0, fontSize: '1rem' }}>
|
||
Betriebliche Tätigkeiten und Unterweisungen
|
||
</h2>
|
||
<span className="leise">{zahlText(summeBetrieb(blatt))} h</span>
|
||
</div>
|
||
|
||
{blatt.betrieb.length === 0 ? (
|
||
<p className="leise" style={{ marginBottom: 0 }}>
|
||
Keine Eintragung.
|
||
</p>
|
||
) : (
|
||
<ul className="leseliste">
|
||
{blatt.betrieb.map((eintrag, stelle) => (
|
||
<li key={stelle}>
|
||
<span>{eintrag.text}</span>
|
||
<span className="leise">{eintrag.stunden > 0 ? `${zahlText(eintrag.stunden)} h` : '–'}</span>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
)}
|
||
</section>
|
||
|
||
<section className="karte stapel-eng" aria-labelledby="schule-lesen">
|
||
<div className="reihe" style={{ justifyContent: 'space-between' }}>
|
||
<h2 id="schule-lesen" style={{ margin: 0, fontSize: '1rem' }}>
|
||
Berufsschule
|
||
</h2>
|
||
<span className="leise">{zahlText(summeSchule(blatt))} h</span>
|
||
</div>
|
||
|
||
{blatt.schule.length === 0 ? (
|
||
<p className="leise" style={{ marginBottom: 0 }}>
|
||
Keine Eintragung.
|
||
</p>
|
||
) : (
|
||
<ul className="leseliste">
|
||
{blatt.schule.map((eintrag, stelle) => (
|
||
<li key={stelle}>
|
||
<span>
|
||
{eintrag.lernfeld ? <strong>{eintrag.lernfeld} </strong> : null}
|
||
{eintrag.themen}
|
||
</span>
|
||
<span className="leise">{eintrag.stunden > 0 ? `${zahlText(eintrag.stunden)} h` : '–'}</span>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
)}
|
||
</section>
|
||
|
||
{blatt.sonstiges ? (
|
||
<section className="karte stapel-eng" aria-labelledby="sonstiges-lesen">
|
||
<h2 id="sonstiges-lesen" style={{ margin: 0, fontSize: '1rem' }}>
|
||
Sonstiges
|
||
</h2>
|
||
<p className="notiz-anzeige">{blatt.sonstiges}</p>
|
||
</section>
|
||
) : null}
|
||
|
||
<section className="karte stapel-eng" aria-labelledby="pruefung-lesen">
|
||
<div className="reihe" style={{ justifyContent: 'space-between' }}>
|
||
<h2 id="pruefung-lesen" style={{ margin: 0, fontSize: '1rem' }}>
|
||
Prüfung
|
||
</h2>
|
||
<strong>{zahlText(summeStunden(blatt))} Stunden gesamt</strong>
|
||
</div>
|
||
|
||
{warnungen.length === 0 ? (
|
||
<p className="hinweis hinweis-gut" style={{ marginBottom: 0 }}>
|
||
✓ Nichts zu beanstanden.
|
||
</p>
|
||
) : (
|
||
<ul className="warnliste">
|
||
{warnungen.map((warnung, stelle) => (
|
||
<li key={stelle} data-schwere={warnung.schwere}>
|
||
<span className="warnzeichen" aria-hidden="true">
|
||
{warnung.schwere === 'warnung' ? '!' : 'i'}
|
||
</span>
|
||
<span>
|
||
{warnung.text}
|
||
{warnung.auszug ? <em> – „{warnung.auszug}“</em> : null}
|
||
</span>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
)}
|
||
</section>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/** Stellt zusammen, was die Anwendung über diese Woche weiß. */
|
||
function sammleVorschlaege(azubiId: string, von: string, bis: string) {
|
||
const alle = inhalte();
|
||
// Aus abgeschalteten Modulen kommen keine Vorschläge.
|
||
const eingeschaltet = modulStand();
|
||
|
||
const notizen = notizenIm(azubiId, von, bis).map((n) => ({
|
||
datum: n.datum,
|
||
text: n.text,
|
||
stunden: n.stunden,
|
||
}));
|
||
|
||
// Praxisaufgaben, die in dieser Woche abgegeben oder abgenommen wurden.
|
||
const staende = aufgabenStaende(azubiId);
|
||
const aufgaben = [...(eingeschaltet.aufgaben ? staende.values() : [])]
|
||
.filter((s) => {
|
||
const zeitpunkt = (s.abgenommenAm ?? s.abgegebenAm ?? '').slice(0, 10);
|
||
return zeitpunkt >= von && zeitpunkt <= bis;
|
||
})
|
||
.map((s) => {
|
||
const aufgabe = alle.aufgabeNach.get(s.aufgabeId);
|
||
return {
|
||
id: s.aufgabeId,
|
||
titel: aufgabe?.titel ?? s.aufgabeId,
|
||
zeitrahmen: aufgabe?.zeitrahmen ?? '',
|
||
status: s.status,
|
||
};
|
||
});
|
||
|
||
const checklisten = (eingeschaltet.checklisten ? laeufeVon(azubiId) : [])
|
||
.filter((l) => l.abgeschlossenAm !== null && l.datum >= von && l.datum <= bis)
|
||
.map((l) => ({ id: l.id, titel: l.titel, ticket: l.ticket, kunde: l.kunde }));
|
||
|
||
// Handouts der Wochen, die in diesem Zeitraum lagen – nur die gelesenen.
|
||
const ausbildung = ausbildungNachAzubi(azubiId);
|
||
const gelesen = gelesenSet(azubiId);
|
||
const wochenImZeitraum = ausbildung
|
||
? planVon(ausbildung.id).filter((z) => z.geplantVon <= bis && z.geplantBis >= von)
|
||
: [];
|
||
|
||
const handouts = wochenImZeitraum
|
||
.flatMap((z) => alle.wocheNach.get(z.wochenNummer)?.handouts ?? [])
|
||
.filter((id) => gelesen.has(id))
|
||
.map((id) => ({ id, titel: alle.handoutNach.get(id)?.titel ?? id }));
|
||
|
||
const lernfelder = [
|
||
...new Set(
|
||
wochenImZeitraum
|
||
.flatMap((z) => alle.wocheNach.get(z.wochenNummer)?.handouts ?? [])
|
||
.map((id) => alle.handoutNach.get(id)?.lernfeld)
|
||
.filter((lf): lf is string => Boolean(lf)),
|
||
),
|
||
].map((id) => ({ id, titel: alle.lernfeldNach.get(id)?.titel ?? id }));
|
||
|
||
const rohdaten: Rohdaten = { tagesnotizen: notizen, aufgaben, checklisten, handouts, lernfelder };
|
||
return baueVorschlaege(rohdaten);
|
||
}
|
||
|
||
function zahlText(wert: number): string {
|
||
return (Math.round(wert * 100) / 100).toString().replace('.', ',');
|
||
}
|