Files
lernbar/src/app/(angemeldet)/berichtsheft/pdf/route.ts
T
Florian WildeandClaude Opus 5 7c4a051347 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>
2026-09-10 23:09:54 +02:00

78 lines
3.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { erfordereBenutzer, ausbildungVonAzubi } from '@/lib/auth/rechte';
import { darfAzubiDatenSehen } from '@/lib/auth/regeln';
import { benutzerNachId } from '@/lib/repos/benutzer';
import { alsWochenblatt, berichteVon } from '@/lib/repos/berichtsheft';
import { berichtsheftPdf, type Blattangaben } from '@/lib/pdf/berichtsheft';
import { ausbildungsjahr } from '@/lib/berichtsheft/jahr';
import { alsDatumText, alsZeitpunktText } from '@/lib/zeit';
export const dynamic = 'force-dynamic';
const datumMuster = /^\d{4}-\d{2}-\d{2}$/;
/**
* Ein ganzer Zeitraum als PDF, ein Blatt je Seite.
*
* /berichtsheft/pdf?von=2026-08-01&bis=2026-12-31[&azubi=…]
*
* Ohne `azubi` das eigene Berichtsheft. Der Ausbilder gibt die Kennung an –
* geprüft wird sie serverseitig gegen das Ausbildungsverhältnis.
*/
export async function GET(anfrage: Request) {
const benutzer = await erfordereBenutzer();
const adresse = new URL(anfrage.url);
const azubiId = benutzer.rolle === 'azubi' ? benutzer.id : (adresse.searchParams.get('azubi') ?? '');
if (!azubiId) return new Response('Nicht gefunden.', { status: 404 });
const bezug = ausbildungVonAzubi(azubiId);
if (!darfAzubiDatenSehen(benutzer, azubiId, bezug)) {
return new Response('Nicht gefunden.', { status: 404 });
}
const von = adresse.searchParams.get('von') ?? '';
const bis = adresse.searchParams.get('bis') ?? '';
if (!datumMuster.test(von) || !datumMuster.test(bis) || von > bis) {
return new Response('Ungültiger Zeitraum.', { status: 400 });
}
const azubi = benutzerNachId(azubiId);
const ausbilder = bezug ? benutzerNachId(bezug.ausbilderId) : null;
// Aufsteigend, damit das Heft in der richtigen Reihenfolge ausgedruckt wird.
const berichte = berichteVon(azubiId)
.filter((b) => b.bis >= von && b.von <= bis)
.sort((a, b) => a.nr - b.nr);
const blaetter: Blattangaben[] = berichte.map((bericht) => {
const abzeichner = bericht.abgezeichnetVon ? benutzerNachId(bericht.abgezeichnetVon) : null;
return {
nr: bericht.nr,
von: alsDatumText(bericht.von),
bis: alsDatumText(bericht.bis),
azubi: azubi?.name ?? 'unbekannt',
ausbilder: ausbilder?.name ?? '',
beruf: bezug?.beruf ?? 'Fachinformatiker/-in für Systemintegration',
ausbildungsjahr: bezug ? ausbildungsjahr(bezug.beginn, bericht.von) : null,
status: bericht.status,
anmerkung: bericht.anmerkung,
abgezeichnetVon: abzeichner?.name ?? null,
abgezeichnetAm: bericht.abgezeichnetAm ? alsZeitpunktText(bericht.abgezeichnetAm) : null,
blatt: alsWochenblatt(bericht),
};
});
const pdf = await berichtsheftPdf(blaetter, `${alsDatumText(von)} bis ${alsDatumText(bis)}`);
const dateiname = `Ausbildungsnachweis_${von}_bis_${bis}.pdf`;
return new Response(new Uint8Array(pdf), {
headers: {
'Content-Type': 'application/pdf',
'Content-Length': String(pdf.byteLength),
'Content-Disposition': `inline; filename="${dateiname}"`,
'Cache-Control': 'private, no-store',
'X-Content-Type-Options': 'nosniff',
},
});
}