Baustein 7: Berichtsheft mit Vorschlaegen, Pruefung, Abzeichnen und PDF
- Taegliche Stichworte, je Tag ein eigenes Formular - Wochenblatt mit Betrieb, Berufsschule und Sonstiges, je mit Stunden - Vorschlaege aus Tagesnotizen, Aufgaben, Checklisten und Handouts - Pruefung mitlaufend beim Tippen: fehlende Stunden, Floskeln, IP- und MAC-Adressen, E-Mail, Zugangsdaten, Kundennamen - Warnung, keine Sperre - Einreichen ist immer moeglich - Formulierungshilfe mit Taetigkeitsverben und Gegenueberstellungen - Einreichen, zurueckgeben mit Anmerkung, abzeichnen; danach gesperrt - Fehlende Wochen werden benannt und lassen sich nachtragen; die laufende Nummer richtet sich nach dem Zeitraum, nicht nach dem Anlegen - PDF im IHK-Layout, einzeln oder als ganzer Zeitraum Behoben: Im PDF-Kopffeld liefen die Spalten auseinander (dokument.y als Bezugspunkt in der Schleife). Das Abzeichnen-Formular hatte keine action - gefunden ueber die ESLint-Meldung zur ungenutzten Serveraktion. 55 neue Tests, insgesamt 241. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
b9d6041f3b
commit
7c4a051347
@@ -0,0 +1,296 @@
|
||||
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';
|
||||
|
||||
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 }> }) {
|
||||
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();
|
||||
|
||||
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 = [...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 = 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('.', ',');
|
||||
}
|
||||
Reference in New Issue
Block a user