- 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>
78 lines
3.0 KiB
TypeScript
78 lines
3.0 KiB
TypeScript
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',
|
||
},
|
||
});
|
||
}
|