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>
283 lines
10 KiB
TypeScript
283 lines
10 KiB
TypeScript
'use client';
|
||
|
||
import { useActionState } from 'react';
|
||
import { Absenden, Meldung } from '@/komponenten/Formularmeldung';
|
||
import { gespraechBestaetigen, gespraechSpeichern, type Rueckmeldung } from './aktionen';
|
||
import { liegtWeitAuseinander, type Gespraechsinhalt } from '@/lib/feedback/inhalt';
|
||
|
||
/**
|
||
* Der Bogen für das Gespräch.
|
||
*
|
||
* Jede Seite trägt ihre eigene Spalte ein: der Ausbilder seine Einschätzung
|
||
* und die Freitexte, der Azubi seine Selbsteinschätzung. Wo beide deutlich
|
||
* auseinanderliegen, wird das hervorgehoben – genau darüber lohnt das
|
||
* Gespräch.
|
||
*/
|
||
export function Gespraechsformular({
|
||
gespraechId,
|
||
inhalt,
|
||
leitfaden,
|
||
istAzubi,
|
||
gesperrt,
|
||
datum,
|
||
bereitsBestaetigt,
|
||
}: {
|
||
gespraechId: string;
|
||
inhalt: Gespraechsinhalt;
|
||
leitfaden: { abschnitt: string; fragen: string[] }[];
|
||
istAzubi: boolean;
|
||
gesperrt: boolean;
|
||
datum: string;
|
||
bereitsBestaetigt: boolean;
|
||
}) {
|
||
const [speicherstand, speichern] = useActionState<Rueckmeldung, FormData>(gespraechSpeichern, {});
|
||
const [bestaetigungsstand, bestaetigen] = useActionState<Rueckmeldung, FormData>(gespraechBestaetigen, {});
|
||
|
||
const eigeneSpalte = istAzubi ? 'azubi' : 'ausbilder';
|
||
|
||
return (
|
||
<form action={speichern} className="stapel">
|
||
<input type="hidden" name="gespraechId" value={gespraechId} />
|
||
|
||
{/* Nach Abschluss steht das Datum schon groß im Kopf – ein zweites,
|
||
gesperrtes Feld daneben sagt nichts dazu. */}
|
||
{!istAzubi && !gesperrt ? (
|
||
<div className="feld" style={{ maxWidth: '14rem' }}>
|
||
<label htmlFor="datum">Datum des Gesprächs</label>
|
||
<input id="datum" name="datum" type="date" defaultValue={datum} />
|
||
</div>
|
||
) : null}
|
||
|
||
{/* --- Bewertungsbogen --- */}
|
||
|
||
<section className="stapel-eng" aria-labelledby="bogen">
|
||
<div>
|
||
<h2 id="bogen" style={{ marginBottom: '0.15rem' }}>
|
||
Bewertungsbogen
|
||
</h2>
|
||
<p className="leise" style={{ margin: 0 }}>
|
||
{inhalt.skala[0]?.wert} = {inhalt.skala[0]?.text} bis {inhalt.skala.at(-1)?.wert} ={' '}
|
||
{inhalt.skala.at(-1)?.text}.{' '}
|
||
{istAzubi
|
||
? 'Du trägst deine Selbsteinschätzung ein – die Spalte des Ausbilders ist für dich gesperrt.'
|
||
: 'Du trägst deine Einschätzung ein; die Selbsteinschätzung kommt vom Azubi.'}
|
||
</p>
|
||
</div>
|
||
|
||
<ul className="kriterienbogen">
|
||
{inhalt.kriterien.map((kriterium) => {
|
||
const bewertung = inhalt.bewertungen.find((b) => b.kriteriumId === kriterium.id);
|
||
const eigener = eigeneSpalte === 'azubi' ? bewertung?.azubi : bewertung?.ausbilder;
|
||
const anderer = eigeneSpalte === 'azubi' ? bewertung?.ausbilder : bewertung?.azubi;
|
||
const weitAuseinander = liegtWeitAuseinander(bewertung);
|
||
|
||
return (
|
||
<li key={kriterium.id} data-abweichung={weitAuseinander ? 'ja' : 'nein'}>
|
||
<div className="kriterium-kopf">
|
||
<strong>{kriterium.titel}</strong>
|
||
<span className="leise">{kriterium.erlaeuterung}</span>
|
||
</div>
|
||
|
||
<fieldset className="stufenwahl" disabled={gesperrt}>
|
||
<legend className="nur-lesehilfe">
|
||
{istAzubi ? 'Deine Selbsteinschätzung' : 'Deine Einschätzung'} für {kriterium.titel}
|
||
</legend>
|
||
|
||
{inhalt.skala.map((stufe) => (
|
||
<label key={stufe.wert} className="stufe" title={stufe.text}>
|
||
<input
|
||
type="radio"
|
||
name={`stufe-${kriterium.id}`}
|
||
value={stufe.wert}
|
||
defaultChecked={eigener === stufe.wert}
|
||
/>
|
||
<span>{stufe.wert}</span>
|
||
</label>
|
||
))}
|
||
</fieldset>
|
||
|
||
<div className="kriterium-fuss">
|
||
<span className="leise">
|
||
{anderer === null || anderer === undefined
|
||
? `${istAzubi ? 'Ausbilder' : 'Azubi'}: noch offen`
|
||
: `${istAzubi ? 'Ausbilder' : 'Selbsteinschätzung'}: ${anderer} (${
|
||
inhalt.skala.find((s) => s.wert === anderer)?.text ?? ''
|
||
})`}
|
||
</span>
|
||
|
||
{weitAuseinander ? (
|
||
<span className="marke-status" data-ton="warnung">
|
||
! unterschiedliche Sicht
|
||
</span>
|
||
) : null}
|
||
</div>
|
||
|
||
{/* Wer nicht schreiben darf oder kann, sieht die Bemerkung als
|
||
Text – und ein leeres Feld gar nicht. Ein abgeschlossenes
|
||
Blatt für die Akte soll nicht voller Platzhalter stehen. */}
|
||
{istAzubi || gesperrt ? (
|
||
bewertung?.bemerkung ? (
|
||
<p className="schritt-bemerkung">{bewertung.bemerkung}</p>
|
||
) : null
|
||
) : (
|
||
<input
|
||
type="text"
|
||
name={`bemerkung-${kriterium.id}`}
|
||
maxLength={500}
|
||
defaultValue={bewertung?.bemerkung ?? ''}
|
||
placeholder="Bemerkung – ein Beispiel sagt mehr als eine Zahl"
|
||
/>
|
||
)}
|
||
</li>
|
||
);
|
||
})}
|
||
</ul>
|
||
</section>
|
||
|
||
{/* --- Freitexte --- */}
|
||
|
||
{/* Nach Abschluss liest auch der Ausbilder nur noch – ein gesperrtes
|
||
Eingabefeld sieht aus, als könnte man es doch noch benutzen. */}
|
||
{istAzubi || gesperrt ? (
|
||
<section className="stapel-eng" aria-labelledby="texte">
|
||
<h2 id="texte">Stärken, Entwicklungsfelder, Ziele</h2>
|
||
<Lesefeld titel="Stärken" text={inhalt.staerken} />
|
||
<Lesefeld titel="Entwicklungsfelder" text={inhalt.entwicklungsfelder} />
|
||
<Lesefeld titel="Vereinbarte Ziele" text={inhalt.ziele} />
|
||
{inhalt.sonstiges ? <Lesefeld titel="Sonstiges" text={inhalt.sonstiges} /> : null}
|
||
</section>
|
||
) : (
|
||
<section className="stapel-eng" aria-labelledby="texte">
|
||
<h2 id="texte">Stärken, Entwicklungsfelder, Ziele</h2>
|
||
|
||
<div className="feld">
|
||
<label htmlFor="staerken">Stärken</label>
|
||
<textarea
|
||
id="staerken"
|
||
name="staerken"
|
||
rows={3}
|
||
maxLength={4000}
|
||
defaultValue={inhalt.staerken}
|
||
placeholder="Woran hast du gemerkt, dass er besser geworden ist? Nenne ein Beispiel."
|
||
/>
|
||
</div>
|
||
|
||
<div className="feld">
|
||
<label htmlFor="entwicklungsfelder">Entwicklungsfelder</label>
|
||
<textarea
|
||
id="entwicklungsfelder"
|
||
name="entwicklungsfelder"
|
||
rows={3}
|
||
maxLength={4000}
|
||
defaultValue={inhalt.entwicklungsfelder}
|
||
placeholder="Was soll besser werden – und woran würde man es merken?"
|
||
/>
|
||
</div>
|
||
|
||
<div className="feld">
|
||
<label htmlFor="ziele">Vereinbarte Ziele bis zum nächsten Gespräch</label>
|
||
<textarea
|
||
id="ziele"
|
||
name="ziele"
|
||
rows={3}
|
||
maxLength={4000}
|
||
defaultValue={inhalt.ziele}
|
||
placeholder="So formuliert, dass beim nächsten Mal feststellbar ist, ob sie erreicht wurden."
|
||
/>
|
||
</div>
|
||
|
||
<div className="feld">
|
||
<label htmlFor="sonstiges">Sonstiges</label>
|
||
<textarea
|
||
id="sonstiges"
|
||
name="sonstiges"
|
||
rows={2}
|
||
maxLength={4000}
|
||
defaultValue={inhalt.sonstiges}
|
||
/>
|
||
</div>
|
||
</section>
|
||
)}
|
||
|
||
{/* --- Leitfaden --- */}
|
||
|
||
<details className="karte formulierungshilfe" open={!istAzubi && !gesperrt}>
|
||
<summary>
|
||
<strong>Gesprächsleitfaden</strong>
|
||
<span className="leise"> – Fragen für das Gespräch</span>
|
||
</summary>
|
||
|
||
<div className="stapel" style={{ marginTop: '0.9rem' }}>
|
||
{leitfaden.map((teil, stelle) => (
|
||
<section key={teil.abschnitt} className="stapel-eng">
|
||
<h3 style={{ fontSize: '0.95rem', marginBottom: '0.2rem' }}>{teil.abschnitt}</h3>
|
||
<ul className="stapel-eng" style={{ margin: 0, paddingLeft: '1.1rem' }}>
|
||
{teil.fragen.map((frage) => (
|
||
<li key={frage}>{frage}</li>
|
||
))}
|
||
</ul>
|
||
|
||
{istAzubi || gesperrt ? (
|
||
inhalt.leitfadennotizen[stelle]?.notiz ? (
|
||
<p className="notiz-anzeige">{inhalt.leitfadennotizen[stelle]!.notiz}</p>
|
||
) : null
|
||
) : (
|
||
<>
|
||
<label htmlFor={`leitfaden-${stelle}`} className="nur-lesehilfe">
|
||
Notiz zu {teil.abschnitt}
|
||
</label>
|
||
<textarea
|
||
id={`leitfaden-${stelle}`}
|
||
name={`leitfaden-${stelle}`}
|
||
rows={2}
|
||
maxLength={4000}
|
||
defaultValue={inhalt.leitfadennotizen[stelle]?.notiz ?? ''}
|
||
placeholder="Notiz zum Gespräch"
|
||
/>
|
||
</>
|
||
)}
|
||
</section>
|
||
))}
|
||
</div>
|
||
</details>
|
||
|
||
<Meldung zustand={speicherstand} />
|
||
<Meldung zustand={bestaetigungsstand} />
|
||
|
||
{!gesperrt ? (
|
||
<div className="reihe nicht-drucken">
|
||
<Absenden className="knopf knopf-zweit">Speichern</Absenden>
|
||
|
||
{!bereitsBestaetigt ? (
|
||
<button type="submit" className="knopf knopf-akzent" formAction={bestaetigen}>
|
||
{istAzubi ? 'Gespräch bestätigen' : 'Als Ausbilder bestätigen'}
|
||
</button>
|
||
) : null}
|
||
</div>
|
||
) : null}
|
||
|
||
{!gesperrt && !bereitsBestaetigt ? (
|
||
<p className="leise" style={{ marginBottom: 0 }}>
|
||
Bestätigen heißt: Du bist mit dem Inhalt einverstanden. Sobald beide bestätigt haben, ist das
|
||
Gespräch unveränderlich und gehört in die Ausbildungsakte.
|
||
</p>
|
||
) : null}
|
||
</form>
|
||
);
|
||
}
|
||
|
||
function Lesefeld({ titel, text }: { titel: string; text: string }) {
|
||
return (
|
||
<div>
|
||
<span className="wochentag-marke">{titel}</span>
|
||
{text.trim() ? (
|
||
<p className="notiz-anzeige">{text}</p>
|
||
) : (
|
||
<p className="leise" style={{ margin: 0 }}>
|
||
Noch nichts eingetragen.
|
||
</p>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|