Die Abdeckung des Ausbildungsrahmenplans ist die Ansicht, die gegenueber
der IHK zaehlt. Sie unterscheidet vier Lagen statt "erledigt/offen":
vermittelt eine abgenommene Aufgabe oder ein abgeschlossener
Checklisten-Durchlauf, oder zwei gelesene Handouts
angefangen etwas erledigt, aber noch nicht genug
geplant Inhalt vorhanden, nichts davon bearbeitet
ohne Inhalt zu dieser Position gibt es im Lehrgang gar nichts
Die vierte Lage ist die wichtigste: Sie ist keine Luecke beim Azubi,
sondern eine im Lehrgang, und wird getrennt ausgewiesen. Die Regel steht
als reine Funktion in src/lib/auswertung/abdeckung.ts, mit 17 Tests.
Das Feedbackgespraech laesst beide Seiten getrennt bewerten. Wo sie um
zwei Stufen oder mehr auseinanderliegen, wird das hervorgehoben - in der
Ansicht, in der Uebersicht und im PDF, alle drei ueber dieselbe Funktion
liegtWeitAuseinander(). Nach beidseitiger Bestaetigung ist das Gespraech
unveraenderlich und wird nur noch gelesen.
Durchstich in scripts/pruefungen/baustein-9.mjs: alle fuenf Luecken
erkannt, getrennte Spalten, Sperre serverseitig, PDF nur mit Anmeldung,
ein fremder Azubi bekommt 404 - auch auf das PDF.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
181 lines
6.5 KiB
TypeScript
181 lines
6.5 KiB
TypeScript
'use server';
|
||
|
||
import { revalidatePath } from 'next/cache';
|
||
import { redirect } from 'next/navigation';
|
||
import { erfordereBenutzer, erfordereBewertungsrecht } from '@/lib/auth/rechte';
|
||
import { darfAzubiDatenSehen } from '@/lib/auth/regeln';
|
||
import { ausbildungNachAzubi, ausbildungNachId } from '@/lib/repos/ausbildung';
|
||
import { inhalte } from '@/lib/inhalte';
|
||
import {
|
||
anlegen,
|
||
bestaetigen,
|
||
bestaetigungZuruecknehmen,
|
||
gespraechNachId,
|
||
inhaltSpeichern,
|
||
inhaltVon,
|
||
loeschen,
|
||
} from '@/lib/repos/feedback';
|
||
import { istAbgeschlossen, type Gespraechsinhalt } from '@/lib/feedback/inhalt';
|
||
|
||
export type Rueckmeldung = { fehler?: string; erfolg?: string };
|
||
|
||
const datumMuster = /^\d{4}-\d{2}-\d{2}$/;
|
||
|
||
/**
|
||
* Holt ein Gespräch samt Rechteprüfung.
|
||
*
|
||
* Beide Seiten dürfen lesen und – solange nicht beide bestätigt haben –
|
||
* schreiben. Das Gespräch gehört ihnen gemeinsam; eine Selbsteinschätzung,
|
||
* die der Ausbilder eintragen könnte, wäre keine.
|
||
*/
|
||
export async function gespraechZumLesen(id: string) {
|
||
const benutzer = await erfordereBenutzer();
|
||
const gespraech = gespraechNachId(id);
|
||
if (!gespraech) return null;
|
||
|
||
const ausbildung = ausbildungNachId(gespraech.ausbildungId);
|
||
if (!ausbildung) return null;
|
||
|
||
if (!darfAzubiDatenSehen(benutzer, ausbildung.azubiId, ausbildung)) return null;
|
||
|
||
return { benutzer, gespraech, ausbildung };
|
||
}
|
||
|
||
function aktualisiere(id?: string) {
|
||
revalidatePath('/feedback');
|
||
if (id) revalidatePath(`/feedback/${id}`);
|
||
}
|
||
|
||
export async function gespraechAnlegen(formular: FormData): Promise<void> {
|
||
const ausbilder = await erfordereBenutzer();
|
||
const azubiId = String(formular.get('azubiId') ?? '');
|
||
erfordereBewertungsrecht(ausbilder, azubiId);
|
||
|
||
const ausbildung = ausbildungNachAzubi(azubiId);
|
||
if (!ausbildung) return;
|
||
|
||
const datum = String(formular.get('datum') ?? '').trim();
|
||
const gespraech = anlegen(
|
||
ausbildung.id,
|
||
ausbilder.id,
|
||
inhalte().feedback,
|
||
datumMuster.test(datum) ? datum : undefined,
|
||
);
|
||
|
||
aktualisiere(gespraech.id);
|
||
redirect(`/feedback/${gespraech.id}`);
|
||
}
|
||
|
||
export async function gespraechSpeichern(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
||
const id = String(formular.get('gespraechId') ?? '');
|
||
const gelesen = await gespraechZumLesen(id);
|
||
if (!gelesen) return { fehler: 'Dieses Gespräch gibt es nicht.' };
|
||
|
||
const { benutzer, gespraech } = gelesen;
|
||
|
||
if (istAbgeschlossen({
|
||
bestaetigtAzubi: gespraech.bestaetigtAzubi === 1,
|
||
bestaetigtAusbilder: gespraech.bestaetigtAusbilder === 1,
|
||
})) {
|
||
return { fehler: 'Das Gespräch ist von beiden Seiten bestätigt und damit unveränderlich.' };
|
||
}
|
||
|
||
const vorlage = inhalte().feedback;
|
||
const bestand = inhaltVon(gespraech, vorlage);
|
||
const istAzubi = benutzer.rolle === 'azubi';
|
||
|
||
// Jede Seite trägt nur ihre eigene Spalte ein.
|
||
const bewertungen: Gespraechsinhalt['bewertungen'] = bestand.bewertungen.map((bewertung) => {
|
||
const roh = String(formular.get(`stufe-${bewertung.kriteriumId}`) ?? '').trim();
|
||
const gelesen = roh === '' ? null : Number(roh);
|
||
const gueltig = gelesen !== null && bestand.skala.some((s) => s.wert === gelesen) ? gelesen : null;
|
||
|
||
return {
|
||
...bewertung,
|
||
ausbilder: istAzubi ? bewertung.ausbilder : gueltig,
|
||
azubi: istAzubi ? gueltig : bewertung.azubi,
|
||
// Die Bemerkung je Kriterium schreibt der Ausbilder.
|
||
bemerkung: istAzubi
|
||
? bewertung.bemerkung
|
||
: String(formular.get(`bemerkung-${bewertung.kriteriumId}`) ?? '').slice(0, 500),
|
||
};
|
||
});
|
||
|
||
// Die Freitexte schreibt der Ausbilder; der Azubi liest sie.
|
||
const neu: Gespraechsinhalt = istAzubi
|
||
? { ...bestand, bewertungen }
|
||
: {
|
||
...bestand,
|
||
bewertungen,
|
||
leitfadennotizen: bestand.leitfadennotizen.map((notiz, stelle) => ({
|
||
...notiz,
|
||
notiz: String(formular.get(`leitfaden-${stelle}`) ?? '').slice(0, 4000),
|
||
})),
|
||
staerken: String(formular.get('staerken') ?? '').slice(0, 4000),
|
||
entwicklungsfelder: String(formular.get('entwicklungsfelder') ?? '').slice(0, 4000),
|
||
ziele: String(formular.get('ziele') ?? '').slice(0, 4000),
|
||
sonstiges: String(formular.get('sonstiges') ?? '').slice(0, 4000),
|
||
};
|
||
|
||
const datum = String(formular.get('datum') ?? '').trim();
|
||
inhaltSpeichern(id, neu, !istAzubi && datumMuster.test(datum) ? datum : undefined);
|
||
|
||
aktualisiere(id);
|
||
return { erfolg: istAzubi ? 'Deine Selbsteinschätzung ist gespeichert.' : 'Gespeichert.' };
|
||
}
|
||
|
||
export async function gespraechBestaetigen(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
||
const id = String(formular.get('gespraechId') ?? '');
|
||
const gelesen = await gespraechZumLesen(id);
|
||
if (!gelesen) return { fehler: 'Dieses Gespräch gibt es nicht.' };
|
||
|
||
const { benutzer, gespraech } = gelesen;
|
||
const rolle = benutzer.rolle === 'azubi' ? 'azubi' : 'ausbilder';
|
||
|
||
const schon = rolle === 'azubi' ? gespraech.bestaetigtAzubi === 1 : gespraech.bestaetigtAusbilder === 1;
|
||
if (schon) return { fehler: 'Du hast bereits bestätigt.' };
|
||
|
||
bestaetigen(id, rolle);
|
||
aktualisiere(id);
|
||
|
||
const andereHatSchon = rolle === 'azubi' ? gespraech.bestaetigtAusbilder === 1 : gespraech.bestaetigtAzubi === 1;
|
||
|
||
return {
|
||
erfolg: andereHatSchon
|
||
? 'Bestätigt. Beide Seiten haben zugestimmt – das Gespräch ist jetzt unveränderlich.'
|
||
: 'Bestätigt. Sobald die andere Seite ebenfalls bestätigt, ist das Gespräch unveränderlich.',
|
||
};
|
||
}
|
||
|
||
export async function bestaetigungZurueck(formular: FormData): Promise<void> {
|
||
const id = String(formular.get('gespraechId') ?? '');
|
||
const gelesen = await gespraechZumLesen(id);
|
||
if (!gelesen) return;
|
||
|
||
const { benutzer, gespraech } = gelesen;
|
||
const rolle = benutzer.rolle === 'azubi' ? 'azubi' : 'ausbilder';
|
||
|
||
// Hat die andere Seite schon bestätigt, ist das Gespräch abgeschlossen.
|
||
const andereHatSchon = rolle === 'azubi' ? gespraech.bestaetigtAusbilder === 1 : gespraech.bestaetigtAzubi === 1;
|
||
if (andereHatSchon) return;
|
||
|
||
bestaetigungZuruecknehmen(id, rolle);
|
||
aktualisiere(id);
|
||
}
|
||
|
||
export async function gespraechVerwerfen(formular: FormData): Promise<void> {
|
||
const id = String(formular.get('gespraechId') ?? '');
|
||
const gelesen = await gespraechZumLesen(id);
|
||
if (!gelesen) return;
|
||
|
||
const { benutzer, gespraech } = gelesen;
|
||
if (benutzer.rolle !== 'ausbilder') return;
|
||
|
||
// Bestätigtes bleibt. Verworfen wird nur, was noch niemand unterschrieben hat.
|
||
if (gespraech.bestaetigtAzubi === 1 || gespraech.bestaetigtAusbilder === 1) return;
|
||
|
||
loeschen(id);
|
||
revalidatePath('/feedback');
|
||
redirect('/feedback');
|
||
}
|