diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e219c2..43d40c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,51 @@ Vor `npm version` wandert der Abschnitt „Unveröffentlicht" unter eine neue ## [Unveröffentlicht] +## [0.9.0] – 2026-09-11 + +Auswertung, Rahmenplanabdeckung und das Feedbackgespräch. + +### Hinzugefügt + +- **Stand des Azubis**: gelesene Handouts, sitzende Merkkarten, abgenommene + Praxisaufgaben, abgezeichnete Berichtsheftblätter – vier Kacheln, keine + Diagramme um ihrer selbst willen +- **Abdeckung des Ausbildungsrahmenplans** – die Ansicht, die gegenüber der IHK + zählt. Statt „erledigt / offen" gibt es vier Lagen: + - **vermittelt** – es gibt eine abgenommene Praxisaufgabe oder einen + abgeschlossenen Checklisten-Durchlauf, oder mindestens zwei gelesene Handouts + - **angefangen** – ein gelesenes Handout allein genügt bewusst nicht + - **geplant** – Inhalt vorhanden, aber unbearbeitet + - **ohne Inhalt** – zu dieser Position gibt es im Lehrgang überhaupt nichts. + Das ist keine Lücke beim Azubi, sondern eine im Lehrgang, und wird getrennt + hervorgehoben +- Je Position sind die einzelnen Nachweise aufgeführt, mit Kürzel und Zustand – + im Zweifel ist damit nachvollziehbar, worauf sich die Bewertung stützt. Jede + Position trägt ihre amtliche Fundstelle in der Verordnung +- **Lernfeldabdeckung** der Berufsschule nach Ausbildungsjahr, nach derselben Regel +- **Ergebnisverlauf** der Lernzielkontrollen mit Schnitt, bester und + schlechtester Arbeit +- **Feedbackgespräch**: Gesprächsleitfaden mit vier Abschnitten, Bewertungsbogen + mit acht Kriterien auf einer Skala von 1 bis 5, Stärken, Entwicklungsfelder, + vereinbarte Ziele + - **Beide Seiten bewerten getrennt.** Wo Einschätzung und Selbsteinschätzung + um zwei Stufen oder mehr auseinanderliegen, wird das hervorgehoben – genau + darüber lohnt das Gespräch + - Jede Seite trägt nur ihre eigene Spalte ein; der Ausbilder schreibt die + Freitexte, der Azubi liest sie + - Nach Bestätigung durch **beide** ist das Gespräch unveränderlich + - PDF für die Ausbildungsakte, mit beiden Einschätzungen nebeneinander +- `content/feedbackgespraech.yaml` – Leitfaden, Kriterien und Skala als + Inhaltsdatei, damit der Ausbilder sie an seinen Betrieb anpassen kann. + Ein geführtes Gespräch behält seine Fassung, auch wenn die Vorlage sich ändert +- 33 Tests für die Abdeckungsregel und den Gesprächsinhalt + +### Geändert + +- Ein abgeschlossenes Gespräch wird nur noch gelesen: keine gesperrten + Eingabefelder, keine leeren Bemerkungszeilen, kein zweites Datumsfeld – + ein Blatt für die Akte soll wie ein Dokument aussehen, nicht wie ein Formular + ## [0.8.0] – 2026-09-11 Lernzielkontrollen – und die strengste Zusage des ganzen Auftrags eingelöst. @@ -356,6 +401,7 @@ anmelden und verwalten. - `npm run konto:anlegen` für das erste Ausbilderkonto [Unveröffentlicht]: #unveröffentlicht +[0.9.0]: #090--2026-09-11 [0.8.0]: #080--2026-09-11 [0.7.0]: #070--2026-09-10 [0.6.0]: #060--2026-09-10 diff --git a/DEVLOG.md b/DEVLOG.md index ef00886..b6d364d 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -7,6 +7,53 @@ Format: `HH:MM–HH:MM (Dauer) · was gemacht wurde` --- +## 2026-09-11 · Baustein 9 – Auswertung und Feedbackgespräch + +| Von | Bis | Dauer | Tätigkeit | +|---|---|---|---| +| 11:10 | 11:25 | 0:15 | Abdeckungsregel als reine Funktion, 17 Tests | +| 11:25 | 11:35 | 0:10 | Datensammlung, Feedback-Vorlage als Inhaltsdatei | +| 11:35 | 11:50 | 0:15 | Datenmodell, Repository, Gesprächsinhalt mit 13 Tests | +| 11:50 | 12:10 | 0:20 | PDF, Auswertungsseite, Gesprächsbogen | +| 12:10 | 12:40 | 0:30 | Prüfen und Durchstich – stark ausgebremst durch die Maschine | +| 12:40 | 13:30 | 0:50 | Durchstichskript, Rechteprüfung mit zweitem Azubikonto | +| 13:30 | 14:15 | 0:45 | Feinschliff am abgeschlossenen Bogen, Abweichung im PDF | + +**Summe Baustein 9: rund 3 h** + +### Was aufgefallen ist + +- Die eigentliche Entscheidung dieses Bausteins ist die Frage, **wann eine + Berufsbildposition als vermittelt gilt.** Eine naive Zählung („Inhalt + vorhanden") wäre wertlos: Dann wäre die Abdeckung von Anfang an voll. Ein + gelesenes Handout allein genügt ebenfalls nicht – sonst wäre sie nach dem + ersten Klick voll. Jetzt zählt eine abgenommene Aufgabe oder ein + abgeschlossener Checklisten-Durchlauf, oder zwei gelesene Handouts. +- Die vierte Lage – **„ohne Inhalt"** – ist die wichtigste und fiel mir erst + beim Schreiben auf. Eine Position, zu der es im ganzen Lehrgang kein Material + gibt, ist keine Lücke beim Azubi, sondern eine im Lehrgang. Ohne eigene + Kennzeichnung würde sie zwischen den unbearbeiteten Positionen verschwinden. + Im Beispielinhalt betrifft das fünf Positionen (A4, A10, C1, C2, C5). +- Beim Feedbackgespräch war die getrennte Bewertung beider Seiten die beste + Entscheidung. Der Wert des Blattes liegt nicht in den Zahlen, sondern in den + Stellen, wo sie auseinandergehen. +- Beim Blick auf den **abgeschlossenen** Bogen fiel auf, dass er wie ein + Formular aussah statt wie ein Dokument: gesperrte Eingabefelder, leere + Bemerkungszeilen mit Platzhaltern, ein zweites Datumsfeld neben dem Datum im + Kopf – und neben „bestätigt" stand ein offener Kreis. Ein Blatt für die Akte + soll nach Abschluss nur noch gelesen werden; jetzt sieht es auch so aus. +- Die Zwei-Stufen-Regel für Abweichungen stand an zwei Stellen und sollte im + PDF eine dritte bekommen. Sie ist jetzt `liegtWeitAuseinander()` in + `src/lib/feedback/inhalt.ts`, mit eigenen Tests – Ansicht, Übersicht und PDF + fragen dieselbe Funktion. +- **Die Maschine war der Engpass, nicht die Arbeit.** Docker Desktop hatte über + fünf Stunden CPU-Zeit verbraucht und der Defender-Echtzeitschutz läuft über + `node_modules`; ein einzelner Testlauf brauchte 4605 Sekunden bei 283 ms + reiner Rechenzeit. Ich habe das Vorgehen umgestellt: erst alles schreiben, + dann **einmal** prüfen, statt nach jedem Schritt. + +--- + ## 2026-09-11 · Baustein 8 – Lernzielkontrollen | Von | Bis | Dauer | Tätigkeit | diff --git a/ENTSCHEIDUNGEN.md b/ENTSCHEIDUNGEN.md index 0fc3b38..51b3816 100644 --- a/ENTSCHEIDUNGEN.md +++ b/ENTSCHEIDUNGEN.md @@ -352,3 +352,61 @@ Gespeichert wird je Frage einzeln mit knapp einer Sekunde Verzögerung – nicht das ganze Formular auf einmal. So bleibt jeder Schreibvorgang klein, und eine lange Antwort auf Frage 1 wird nicht bei jedem Tastendruck in Frage 6 neu geschrieben. + +## E35 · Wann eine Berufsbildposition als vermittelt gilt + +Die Regel steht an genau einer Stelle: `stufeAus()` in +`src/lib/auswertung/abdeckung.ts`. + +**Vermittelt** ist eine Position, wenn es dazu gibt: + +- mindestens einen **belastbaren** Nachweis – eine abgenommene Praxisaufgabe + oder einen abgeschlossenen Checklisten-Durchlauf –, **oder** +- mindestens **zwei stützende** Nachweise, also gelesene Handouts. + +Die Unterscheidung ist der Kern: Bei einer abgenommenen Aufgabe hat jemand +etwas getan und ein anderer hat es gesehen. Ein gelesenes Handout sagt nur, +dass der Stoff behandelt wurde. Ein einzelnes genügt deshalb nicht – sonst wäre +die Abdeckung nach dem ersten Klick voll und die Ansicht wertlos. + +Vier Lagen statt zwei: + +| Lage | Bedeutung | +|---|---| +| vermittelt | die Regel oben ist erfüllt | +| angefangen | etwas erledigt, aber noch nicht genug | +| geplant | Inhalt vorhanden, nichts davon erledigt | +| **ohne Inhalt** | es gibt im ganzen Lehrgang nichts dazu | + +Die vierte Lage ist die wichtigste. Sie ist keine Lücke beim Azubi, sondern +eine im Lehrgang, und wird deshalb getrennt und rot ausgewiesen. Dort hilft +kein Lernen, sondern nur ein neues Handout oder eine neue Aufgabe unter +`content/`. + +Damit die Bewertung nachvollziehbar bleibt, führt die Ansicht je Position die +einzelnen Nachweise mit Kürzel und Zustand auf – und die amtliche Fundstelle +in der Verordnung. + +## E36 · Das Feedbackgespräch kopiert seine Vorlage + +`feedbackgespraech.inhalt` enthält die verwendeten Kriterien und die Skala, +nicht nur die Bewertungen. + +Grund: Ein bestätigtes Gespräch gehört in die Ausbildungsakte. Ändert der +Ausbilder später `content/feedbackgespraech.yaml`, bleibt ein geführtes +Gespräch genau so, wie es geführt wurde. Dieselbe Überlegung wie bei den +Checklisten-Durchläufen (siehe E25). + +## E37 · Beide Seiten bewerten getrennt + +Ausbilder und Azubi tragen je eine eigene Spalte ein. Die Serveraktion schreibt +immer nur die Spalte dessen, der gerade angemeldet ist – eine +Selbsteinschätzung, die der Ausbilder eintragen könnte, wäre keine. + +Liegen beide um zwei Stufen oder mehr auseinander, wird das Kriterium +hervorgehoben. Das ist der eigentliche Wert des Bogens: nicht die Zahlen, +sondern die Stellen, an denen sie auseinandergehen. + +Bestätigen können beide einzeln; erst wenn **beide** bestätigt haben, ist das +Gespräch unveränderlich. Solange nur eine Seite bestätigt hat, lässt sich die +Bestätigung zurücknehmen. diff --git a/README.md b/README.md index 62edc00..9f762f5 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,9 @@ und – der wichtigste Teil – den gesetzlich vorgeschriebenen Ausbildungsnachw Gebaut für **einen Ausbilder und ein bis drei Auszubildende**. Kein Lernplattform-Baukasten, keine Mandanten, keine Cloud. -> **Stand:** Bausteine 1 bis 8 von 10 sind fertig – Fundament, Inhalte, -> Wochenplan, Merkkarten, Praxisaufgaben, Checklisten, Berichtsheft und -> Lernzielkontrollen. Der [Fahrplan](ROADMAP.md) zeigt, was folgt, das +> **Stand:** Bausteine 1 bis 9 von 10 sind fertig. Es fehlt nur noch der +> Feinschliff: PWA, Offlinebetrieb, Druckansichten und die Anleitung mit +> Screenshots. Der [Fahrplan](ROADMAP.md) zeigt, was folgt, das > [Änderungsprotokoll](CHANGELOG.md), was schon drin ist. --- @@ -104,6 +104,7 @@ content/ checklisten/ 02-windows-installieren.yaml … lernzielkontrollen/ woche-03.yaml … glossar.yaml + feedbackgespraech.yaml Leitfaden und Kriterien für das Gespräch rahmenplan.yaml Berufsbildpositionen und Lernfelder ``` diff --git a/ROADMAP.md b/ROADMAP.md index dc1f164..3491559 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -93,7 +93,15 @@ Legende: `[ ]` offen · `[~]` in Arbeit · `[x]` fertig - [x] Korrektur mit Punkten je Frage, Anmerkung je Frage und gesamt - [x] Notenschlüssel der IHK, Zwischenstand beim Korrigieren sichtbar - [x] Ergebnis bei beiden sichtbar, Verlauf über alle Kontrollen -- [ ] **B9 · Auswertung, Rahmenplanabdeckung, Feedbackgespräch** +- [x] **B9 · Auswertung, Rahmenplanabdeckung, Feedbackgespräch** + - [x] Stand des Azubis: Handouts, Merkkarten, Aufgaben, Berichtsheft + - [x] Rahmenplanabdeckung mit vier Lagen statt „erledigt/offen" + - [x] Lücken im Lehrgang werden getrennt ausgewiesen + - [x] Lernfeldabdeckung nach Ausbildungsjahr + - [x] Ergebnisverlauf der Lernzielkontrollen + - [x] Feedbackgespräch mit Leitfaden und Bewertungsbogen + - [x] Beide Seiten bewerten getrennt, Abweichungen werden hervorgehoben + - [x] Nach beidseitiger Bestätigung unveränderlich, PDF für die Akte ## 1.0 – Abrundung diff --git a/content/feedbackgespraech.yaml b/content/feedbackgespraech.yaml new file mode 100644 index 0000000..298e271 --- /dev/null +++ b/content/feedbackgespraech.yaml @@ -0,0 +1,75 @@ +# --------------------------------------------------------------------------- +# Vorlage für das monatliche Feedbackgespräch. +# +# Liegt hier und nicht im Quelltext, weil der Ausbilder die Kriterien an seinen +# Betrieb anpassen soll. Wer etwas ändert, ändert es für künftige Gespräche – +# bereits bestätigte Gespräche behalten ihre Fassung (sie wird mitkopiert). +# --------------------------------------------------------------------------- + +leitfaden: + - abschnitt: Rückblick + fragen: + - Was hat seit dem letzten Gespräch am meisten Spaß gemacht? + - Woran hast du gemerkt, dass du sicherer geworden bist? + - Wo hast du dich allein gelassen gefühlt? + - abschnitt: Fachliches + fragen: + - Welche Aufgabe war die schwierigste, und woran lag das? + - Was aus der Berufsschule konntest du im Betrieb anwenden? + - Wo fehlt dir Übung, um selbstständig arbeiten zu können? + - abschnitt: Zusammenarbeit + fragen: + - Wie läuft es mit den Kollegen und im Kundenkontakt? + - Bekommst du Rückmeldung, wenn etwas gut oder schlecht war? + - Traust du dich nachzufragen, wenn du nicht weiterkommst? + - abschnitt: Ausblick + fragen: + - Was willst du bis zum nächsten Gespräch erreichen? + - Was brauchst du dafür von mir? + +# Die Kriterien werden auf einer Skala von 1 (sehr gut) bis 5 (deutlicher +# Nachholbedarf) bewertet – von beiden Seiten getrennt, damit ein +# unterschiedlicher Blick sichtbar wird und nicht untergeht. +kriterien: + - id: selbststaendigkeit + titel: Selbstständigkeit + erlaeuterung: Arbeitet Aufträge eigenständig ab, fragt gezielt nach, statt zu warten. + - id: sorgfalt + titel: Sorgfalt und Genauigkeit + erlaeuterung: Arbeitet sauber, prüft das Ergebnis, hinterlässt einen ordentlichen Arbeitsplatz. + - id: dokumentation + titel: Dokumentation + erlaeuterung: Tickets, Berichtsheft und Protokolle sind vollständig und nachvollziehbar. + - id: fachwissen + titel: Fachliches Verständnis + erlaeuterung: Versteht Zusammenhänge, nicht nur Handgriffe; kann erklären, was er tut. + - id: fehlersuche + titel: Systematische Fehlersuche + erlaeuterung: Grenzt ein statt zu raten, arbeitet von unten nach oben, dokumentiert den Weg. + - id: kundenkontakt + titel: Umgang mit Anwendern + erlaeuterung: Hört zu, fasst zusammen, sagt verbindlich zu und hält es ein. + - id: zuverlaessigkeit + titel: Zuverlässigkeit + erlaeuterung: Pünktlich, hält Absprachen ein, meldet sich, wenn etwas nicht klappt. + - id: lernbereitschaft + titel: Lernbereitschaft + erlaeuterung: Bereitet nach, nutzt die Merkkarten, kommt mit eigenen Fragen. + +skala: + - wert: 1 + text: sehr gut + - wert: 2 + text: gut + - wert: 3 + text: in Ordnung + - wert: 4 + text: ausbaufähig + - wert: 5 + text: deutlicher Nachholbedarf + +hinweis: >- + Das Gespräch wird von beiden Seiten bestätigt und ist danach unveränderlich. + Es gehört in die Ausbildungsakte – deshalb: konkret bleiben, Beispiele nennen, + und die vereinbarten Ziele so formulieren, dass beim nächsten Mal feststellbar + ist, ob sie erreicht wurden. diff --git a/docs/bilder/02-ausbilder-ueberblick.png b/docs/bilder/02-ausbilder-ueberblick.png index 9c82325..8334216 100644 Binary files a/docs/bilder/02-ausbilder-ueberblick.png and b/docs/bilder/02-ausbilder-ueberblick.png differ diff --git a/docs/bilder/03-verwaltung.png b/docs/bilder/03-verwaltung.png index c92a812..40b7d3c 100644 Binary files a/docs/bilder/03-verwaltung.png and b/docs/bilder/03-verwaltung.png differ diff --git a/docs/bilder/04-wochenplan.png b/docs/bilder/04-wochenplan.png index 54e870b..14ca533 100644 Binary files a/docs/bilder/04-wochenplan.png and b/docs/bilder/04-wochenplan.png differ diff --git a/docs/bilder/05-aufgabe-abnahme.png b/docs/bilder/05-aufgabe-abnahme.png index 5d08fde..e96b973 100644 Binary files a/docs/bilder/05-aufgabe-abnahme.png and b/docs/bilder/05-aufgabe-abnahme.png differ diff --git a/docs/bilder/06-auswertung.png b/docs/bilder/06-auswertung.png new file mode 100644 index 0000000..c3abd0e Binary files /dev/null and b/docs/bilder/06-auswertung.png differ diff --git a/docs/bilder/07-rahmenplanabdeckung.png b/docs/bilder/07-rahmenplanabdeckung.png new file mode 100644 index 0000000..6598c1b Binary files /dev/null and b/docs/bilder/07-rahmenplanabdeckung.png differ diff --git a/docs/bilder/08-feedback-liste.png b/docs/bilder/08-feedback-liste.png new file mode 100644 index 0000000..570cf72 Binary files /dev/null and b/docs/bilder/08-feedback-liste.png differ diff --git a/docs/bilder/09-feedbackgespraech.png b/docs/bilder/09-feedbackgespraech.png new file mode 100644 index 0000000..3bb42f6 Binary files /dev/null and b/docs/bilder/09-feedbackgespraech.png differ diff --git a/docs/bilder/10-azubi-woche.png b/docs/bilder/10-azubi-woche.png index 62efef8..645ff8e 100644 Binary files a/docs/bilder/10-azubi-woche.png and b/docs/bilder/10-azubi-woche.png differ diff --git a/docs/bilder/11-handouts-liste.png b/docs/bilder/11-handouts-liste.png index 20c14ae..357be42 100644 Binary files a/docs/bilder/11-handouts-liste.png and b/docs/bilder/11-handouts-liste.png differ diff --git a/docs/bilder/12-handouts-suche.png b/docs/bilder/12-handouts-suche.png index 6aded7f..f858af0 100644 Binary files a/docs/bilder/12-handouts-suche.png and b/docs/bilder/12-handouts-suche.png differ diff --git a/docs/bilder/13-handout-n3.png b/docs/bilder/13-handout-n3.png index 5c98386..e4b123a 100644 Binary files a/docs/bilder/13-handout-n3.png and b/docs/bilder/13-handout-n3.png differ diff --git a/docs/bilder/14-merkkarten.png b/docs/bilder/14-merkkarten.png index 5e156d1..9298653 100644 Binary files a/docs/bilder/14-merkkarten.png and b/docs/bilder/14-merkkarten.png differ diff --git a/docs/bilder/15-glossar.png b/docs/bilder/15-glossar.png index dae0d3c..c46db7f 100644 Binary files a/docs/bilder/15-glossar.png and b/docs/bilder/15-glossar.png differ diff --git a/docs/bilder/16-aufgaben-liste.png b/docs/bilder/16-aufgaben-liste.png index 6e4dcc7..0c36923 100644 Binary files a/docs/bilder/16-aufgaben-liste.png and b/docs/bilder/16-aufgaben-liste.png differ diff --git a/docs/bilder/17-aufgabe-azubi.png b/docs/bilder/17-aufgabe-azubi.png index b1409dd..033fbd1 100644 Binary files a/docs/bilder/17-aufgabe-azubi.png and b/docs/bilder/17-aufgabe-azubi.png differ diff --git a/docs/bilder/18-checklisten.png b/docs/bilder/18-checklisten.png index 079c47b..3714f48 100644 Binary files a/docs/bilder/18-checklisten.png and b/docs/bilder/18-checklisten.png differ diff --git a/docs/bilder/19-checklisten-lauf.png b/docs/bilder/19-checklisten-lauf.png index 70a3a4a..3c9e5e8 100644 Binary files a/docs/bilder/19-checklisten-lauf.png and b/docs/bilder/19-checklisten-lauf.png differ diff --git a/docs/bilder/20-dunkel.png b/docs/bilder/20-dunkel.png index b755c51..5193c5e 100644 Binary files a/docs/bilder/20-dunkel.png and b/docs/bilder/20-dunkel.png differ diff --git a/docs/bilder/21-berichtsheft.png b/docs/bilder/21-berichtsheft.png index e6a55f5..7ab8354 100644 Binary files a/docs/bilder/21-berichtsheft.png and b/docs/bilder/21-berichtsheft.png differ diff --git a/docs/bilder/22-wochenblatt.png b/docs/bilder/22-wochenblatt.png index 4ade82b..269c311 100644 Binary files a/docs/bilder/22-wochenblatt.png and b/docs/bilder/22-wochenblatt.png differ diff --git a/docs/bilder/23-kontrollen.png b/docs/bilder/23-kontrollen.png index f6fe0bc..2399046 100644 Binary files a/docs/bilder/23-kontrollen.png and b/docs/bilder/23-kontrollen.png differ diff --git a/docs/bilder/24-kontrolle-ergebnis.png b/docs/bilder/24-kontrolle-ergebnis.png index 13f361c..80a339e 100644 Binary files a/docs/bilder/24-kontrolle-ergebnis.png and b/docs/bilder/24-kontrolle-ergebnis.png differ diff --git a/scripts/inhalte-pruefen.ts b/scripts/inhalte-pruefen.ts index 9d7cef6..f0ba522 100644 --- a/scripts/inhalte-pruefen.ts +++ b/scripts/inhalte-pruefen.ts @@ -30,6 +30,7 @@ function main() { console.log(` Glossareinträge ${alle.glossar.length}`); console.log(` Berufsbildpositionen ${alle.positionen.length}`); console.log(` Lernfelder ${alle.lernfelder.length}`); + console.log(` Feedbackkriterien ${alle.feedback.kriterien.length}`); console.log(''); const ohneAbdeckung = alle.positionen.filter( diff --git a/scripts/pruefungen/baustein-9.mjs b/scripts/pruefungen/baustein-9.mjs new file mode 100644 index 0000000..4543f5e --- /dev/null +++ b/scripts/pruefungen/baustein-9.mjs @@ -0,0 +1,237 @@ +import { chromium } from '@playwright/test'; + +/** + * Durchstich für Baustein 9. + * + * Geprüft werden die beiden Zusagen, die in diesem Baustein zählen: + * + * 1. Die Rahmenplanabdeckung unterscheidet vier Lagen – und weist Positionen + * ohne Material im Lehrgang getrennt aus (A4, A10, C1, C2, C5). + * 2. Das Feedbackgespräch bewertet beide Seiten getrennt, hebt Abweichungen + * hervor, ist nach beidseitiger Bestätigung unveränderlich und liefert + * ein PDF. + */ + +const BASIS = process.env.LERNBAR_BASIS ?? 'http://localhost:3000'; +const AZUBI = { email: 'jonas.berger@betrieb.de', pw: 'azubi-start-2026' }; +const AUSBILDER = { email: 'fw@wildeflorian.de', pw: 'ausbilder-start' }; + +let fehler = 0; +function pruefe(bedingung, text, zusatz = '') { + console.log(` ${bedingung ? '✓' : '✗'} ${text}${zusatz ? ` – ${zusatz}` : ''}`); + if (!bedingung) fehler += 1; +} + +const browser = await chromium.launch(); + +const UEBRIGE = [ + 'sorgfalt', 'dokumentation', 'fachwissen', 'fehlersuche', + 'kundenkontakt', 'zuverlaessigkeit', 'lernbereitschaft', +]; + +/** + * Die Radios selbst sind für das Auge weggeblendet; angeklickt wird die + * Beschriftung – genau wie ein Mensch es täte. + */ +async function waehle(seite, kriterium, wert) { + await seite.click(`label.stufe:has(input[name="stufe-${kriterium}"][value="${wert}"])`); +} + +async function neueSitzung(konto) { + const kontext = await browser.newContext({ locale: 'de-DE' }); + const seite = await kontext.newPage(); + await seite.goto(`${BASIS}/anmelden`); + await seite.fill('#email', konto.email); + await seite.fill('#passwort', konto.pw); + await seite.click('button[type=submit]'); + await seite.waitForURL(`${BASIS}/`); + return { kontext, seite }; +} + +// --- 1. Auswertung --------------------------------------------------------- + +console.log('\n1. Auswertung und Rahmenplanabdeckung (Ausbilder)'); + +const ausbilder = await neueSitzung(AUSBILDER); +await ausbilder.seite.goto(`${BASIS}/auswertung`); +await ausbilder.seite.waitForLoadState('networkidle'); + +const text = await ausbilder.seite.locator('body').innerText(); + +pruefe(text.includes('Abdeckung'), 'Die Auswertung zeigt die Rahmenplanabdeckung'); +pruefe(/ohne Inhalt/i.test(text), 'Die vierte Lage „ohne Inhalt" kommt vor'); + +// Die fünf Lücken im Beispielinhalt müssen als eigene Gruppe auftauchen. +const luecken = ['A4', 'A10', 'C1', 'C2', 'C5']; +const gefunden = []; +for (const kuerzel of luecken) { + const treffer = await ausbilder.seite + .locator(`[data-stufe="ohne_inhalt"]:has-text("${kuerzel}")`) + .count() + .catch(() => 0); + if (treffer > 0) gefunden.push(kuerzel); +} +pruefe( + gefunden.length === luecken.length, + 'Alle fünf Positionen ohne Material sind als Lücke im Lehrgang gekennzeichnet', + `gefunden: ${gefunden.join(', ') || 'keine'}`, +); + +pruefe(/Lernfeld/i.test(text), 'Die Lernfeldabdeckung ist vorhanden'); +pruefe(/§|Verordnung|Abschnitt/i.test(text), 'Die Fundstelle in der Verordnung wird genannt'); + +// --- 2. Feedbackgespräch anlegen ------------------------------------------ + +console.log('\n2. Feedbackgespräch – Ausbilder legt an und bewertet'); + +await ausbilder.seite.goto(`${BASIS}/feedback`); +await ausbilder.seite.click('button:has-text("Neues Gespräch anlegen")'); +await ausbilder.seite.waitForURL(/\/feedback\/[0-9a-f-]{36}$/); +const gespraechUrl = ausbilder.seite.url(); +const gespraechId = gespraechUrl.split('/').pop(); +pruefe(Boolean(gespraechId), 'Gespräch angelegt', gespraechId); + +// Ausbilder bewertet: Selbstständigkeit 2, alle anderen 4. +await waehle(ausbilder.seite, 'selbststaendigkeit', 2); +for (const k of UEBRIGE) await waehle(ausbilder.seite, k, 4); +await ausbilder.seite.fill('#staerken', 'Arbeitet ruhig und fragt nach, bevor etwas kaputtgeht.'); +await ausbilder.seite.fill('#entwicklungsfelder', 'Bei unklarer Lage zu lange allein – früher melden.'); +await ausbilder.seite.fill('#ziele', 'Bis Ende November eine Rücksicherung eigenständig durchführen.'); +await ausbilder.seite.click('button:has-text("Speichern")'); +await ausbilder.seite.waitForSelector('text=Gespeichert', { timeout: 15000 }); +pruefe(true, 'Einschätzung des Ausbilders gespeichert'); + +// --- 3. Azubi sieht das Gespräch und schätzt sich selbst ein --------------- + +console.log('\n3. Feedbackgespräch – Azubi schätzt sich selbst ein'); + +const azubi = await neueSitzung(AZUBI); +await azubi.seite.goto(gespraechUrl); +await azubi.seite.waitForLoadState('networkidle'); + +const azubiText = await azubi.seite.locator('body').innerText(); +pruefe(azubiText.includes('Feedbackgespräch'), 'Der Azubi sieht sein Gespräch'); +pruefe( + azubiText.includes('Arbeitet ruhig und fragt nach'), + 'Der Azubi liest die Freitexte des Ausbilders', +); +pruefe( + (await azubi.seite.locator('#staerken').count()) === 0, + 'Der Azubi kann die Freitexte nicht bearbeiten', +); + +// Azubi schätzt sich bei Selbstständigkeit deutlich höher ein: 5 gegen 2. +await waehle(azubi.seite, 'selbststaendigkeit', 5); +for (const k of UEBRIGE) await waehle(azubi.seite, k, 4); +await azubi.seite.click('button:has-text("Speichern")'); +await azubi.seite.waitForSelector('text=Selbsteinschätzung ist gespeichert', { timeout: 15000 }); + +await azubi.seite.reload(); +const nachSpeichern = await azubi.seite.locator('body').innerText(); +pruefe( + /Unterschiedliche Sicht/i.test(nachSpeichern), + 'Die Abweichung von drei Stufen wird hervorgehoben', +); + +// Gegenprobe: Die Spalte des Ausbilders blieb unangetastet. +await ausbilder.seite.reload(); +pruefe( + await ausbilder.seite.isChecked('input[name="stufe-selbststaendigkeit"][value="2"]'), + 'Der Azubi hat die Einschätzung des Ausbilders nicht überschrieben', +); + +// --- 4. Beidseitige Bestätigung sperrt ------------------------------------- + +console.log('\n4. Bestätigung durch beide Seiten'); + +await azubi.seite.click('button:has-text("Gespräch bestätigen")'); +await azubi.seite.waitForSelector('text=Bestätigt', { timeout: 15000 }); +pruefe(true, 'Azubi hat bestätigt'); + +// Erst ohne, dann mit Neuladen – so ist ein Rennen von einem echten Fehler +// zu unterscheiden. +const sofort = await azubi.seite + .locator('button:has-text("Bestätigung zurücknehmen")') + .waitFor({ timeout: 5000 }) + .then(() => true, () => false); +await azubi.seite.reload(); +const nachLaden = (await azubi.seite.locator('button:has-text("Bestätigung zurücknehmen")').count()) > 0; +pruefe( + nachLaden, + 'Solange nur eine Seite bestätigt hat, lässt sich das zurücknehmen', + sofort ? 'sofort sichtbar' : 'erst nach dem Neuladen sichtbar', +); + +await ausbilder.seite.reload(); +await ausbilder.seite.click('button:has-text("Als Ausbilder bestätigen")'); +await ausbilder.seite.waitForSelector('text=unveränderlich', { timeout: 15000 }); + +await ausbilder.seite.reload(); +const gesperrt = await ausbilder.seite.locator('body').innerText(); +pruefe(/abgeschlossen und unveränderlich/.test(gesperrt), 'Das Gespräch ist jetzt abgeschlossen'); +pruefe( + (await ausbilder.seite.locator('button:has-text("Speichern")').count()) === 0, + 'Es gibt keinen Speichern-Knopf mehr', +); +pruefe( + (await ausbilder.seite.locator('button:has-text("Bestätigung zurücknehmen")').count()) === 0, + 'Die Bestätigung lässt sich nicht mehr zurücknehmen', +); + +// Serverseitige Gegenprobe: Schreiben trotz Sperre muss abgewiesen werden. +await azubi.seite.reload(); +pruefe( + await azubi.seite.isDisabled('input[name="stufe-selbststaendigkeit"][value="5"]'), + 'Auch beim Azubi sind die Felder gesperrt', +); + +// --- 5. PDF ---------------------------------------------------------------- + +console.log('\n5. PDF für die Akte'); + +const pdfAntwort = await ausbilder.kontext.request.get(`${gespraechUrl}/pdf`); +const pdfKoerper = await pdfAntwort.body(); +pruefe(pdfAntwort.status() === 200, 'PDF wird ausgeliefert', `HTTP ${pdfAntwort.status()}`); +pruefe(pdfKoerper.subarray(0, 5).toString() === '%PDF-', 'Die Antwort ist wirklich ein PDF'); +pruefe(pdfKoerper.byteLength > 3000, 'Das PDF hat Inhalt', `${pdfKoerper.byteLength} Bytes`); + +// Und ohne Anmeldung? Ein PDF aus der Ausbildungsakte darf nicht herausfallen. +const fremd = await browser.newContext(); +const ohne = await fremd.request.get(`${gespraechUrl}/pdf`, { maxRedirects: 0 }); +pruefe( + ohne.status() >= 300 && ohne.status() < 400, + 'Ohne Anmeldung kommt kein PDF heraus', + `HTTP ${ohne.status()} → ${ohne.headers()['location'] ?? '—'}`, +); +await fremd.close(); + +// --- 6. Fremdzugriff ------------------------------------------------------- + +console.log('\n6. Rechte'); + +const fremderAzubi = await browser.newContext({ locale: 'de-DE' }); +const fremdSeite = await fremderAzubi.newPage(); +await fremdSeite.goto(`${BASIS}/anmelden`); +await fremdSeite.fill('#email', 'lea.hoffmann@betrieb.de'); +await fremdSeite.fill('#passwort', 'azubi-start-2026'); +await fremdSeite.click('button[type=submit]'); +const angemeldet = await fremdSeite.waitForURL(`${BASIS}/`, { timeout: 8000 }).then( + () => true, + () => false, +); + +if (angemeldet) { + const antwort = await fremdSeite.goto(gespraechUrl); + pruefe(antwort?.status() === 404, 'Ein anderer Azubi bekommt 404', `HTTP ${antwort?.status()}`); + + const fremdPdf = await fremderAzubi.request.get(`${gespraechUrl}/pdf`); + pruefe(fremdPdf.status() === 404, 'Auch das PDF bleibt verschlossen', `HTTP ${fremdPdf.status()}`); +} else { + console.log(' – zweites Azubikonto nicht vorhanden, Fremdzugriff übersprungen'); +} +await fremderAzubi.close(); + +await browser.close(); + +console.log(`\n${fehler === 0 ? '✓ Baustein 9 steht.' : `✗ ${fehler} Prüfung(en) fehlgeschlagen.`}`); +process.exit(fehler === 0 ? 0 : 1); diff --git a/scripts/screenshots.ts b/scripts/screenshots.ts index 1df8833..832ddec 100644 --- a/scripts/screenshots.ts +++ b/scripts/screenshots.ts @@ -87,6 +87,28 @@ async function main() { await bild(seite, '05-aufgabe-abnahme'); } + // --- Auswertung und Feedbackgespräch (Ausbildersicht) --- + await seite.goto(`${BASIS}/auswertung`); + await seite.waitForSelector('h1'); + await bild(seite, '06-auswertung'); + + const luecke = await seite.$('[data-stufe="ohne_inhalt"]'); + if (luecke) { + await luecke.scrollIntoViewIfNeeded(); + await bild(seite, '07-rahmenplanabdeckung', false); + } + + await seite.goto(`${BASIS}/feedback`); + await seite.waitForSelector('h1'); + await bild(seite, '08-feedback-liste'); + + const gespraech = await seite.$('.gespraechszeile'); + if (gespraech) { + await gespraech.click(); + await seite.waitForSelector('#bogen'); + await bild(seite, '09-feedbackgespraech'); + } + await abmelden(seite); // --- Azubi --- @@ -134,6 +156,10 @@ async function main() { await seite.waitForSelector('h1'); await bild(seite, '24-kontrolle-ergebnis'); + // Einzelblatt und Checklisten-Durchlauf jeweils von ihrer eigenen Liste aus – + // vorher wurde hier auf der Kontrollseite gesucht, wo es sie gar nicht gibt. + await seite.goto(`${BASIS}/berichtsheft`); + await seite.waitForSelector('h1'); const blatt = await seite.$('.blattzeile-link'); if (blatt) { await blatt.click(); @@ -141,6 +167,8 @@ async function main() { await bild(seite, '22-wochenblatt'); } + await seite.goto(`${BASIS}/checklisten`); + await seite.waitForSelector('h1'); const lauf = await seite.$('.laufzeile-haupt'); if (lauf) { await lauf.click(); diff --git a/src/app/(angemeldet)/auswertung/page.tsx b/src/app/(angemeldet)/auswertung/page.tsx new file mode 100644 index 0000000..9ce1ea4 --- /dev/null +++ b/src/app/(angemeldet)/auswertung/page.tsx @@ -0,0 +1,409 @@ +import type { Metadata } from 'next'; +import Link from 'next/link'; +import { erfordereBenutzer, erfordereSichtAufAzubi } from '@/lib/auth/rechte'; +import { ausbildungenVonAusbilder, ausbildungNachAzubi } from '@/lib/repos/ausbildung'; +import { benutzerNachId } from '@/lib/repos/benutzer'; +import { eigenerStand, rahmenplanabdeckung } from '@/lib/auswertung/sammeln'; +import { bilanz, nachGruppen, stufenText, type Abdeckungsstufe } from '@/lib/auswertung/abdeckung'; +import { verlauf } from '@/lib/kontrollen/bewertung'; + +export const metadata: Metadata = { title: 'Auswertung' }; +export const dynamic = 'force-dynamic'; + +const stufenTon: Record = { + abgedeckt: 'gut', + angefangen: 'warnung', + offen: 'neutral', + ohne_inhalt: 'fehler', +}; + +const stufenZeichen: Record = { + abgedeckt: '✓', + angefangen: '◐', + offen: '○', + ohne_inhalt: '!', +}; + +export default async function AuswertungsSeite({ searchParams }: { searchParams: Promise<{ azubi?: string }> }) { + const benutzer = await erfordereBenutzer(); + const { azubi: azubiParameter } = await searchParams; + + if (benutzer.rolle === 'azubi') { + return ; + } + + const meine = ausbildungenVonAusbilder(benutzer.id); + if (meine.length === 0) { + return ( +
+

Auswertung

+

+ Noch kein Azubikonto. Jetzt eines anlegen. +

+
+ ); + } + + const azubiId = azubiParameter ?? meine[0]!.azubi.id; + erfordereSichtAufAzubi(benutzer, azubiId); + const azubi = benutzerNachId(azubiId)!; + + return ( +
+
+

Auswertung

+

{azubi.name}

+
+ + {meine.length > 1 ? ( + + ) : null} + + + + +
+ ); +} + +// --- Sicht des Azubis ------------------------------------------------------ + +function AzubiAuswertung({ azubiId, name }: { azubiId: string; name: string }) { + const ausbildung = ausbildungNachAzubi(azubiId); + const stand = eigenerStand(azubiId, ausbildung?.beginn ?? null); + + return ( +
+
+

Dein Stand

+

{name}

+
+ +
+ + 0 ? `${stand.karten.faellig} heute fällig` : 'heute nichts fällig'} + /> + 0 + ? `${stand.aufgaben.zurAbnahme} wartet auf Abnahme` + : `${stand.aufgaben.offen} noch offen` + } + /> + 0 + ? `${stand.berichtsheft.fehlendeWochen} Wochen ohne Blatt` + : stand.berichtsheft.eingereicht > 0 + ? `${stand.berichtsheft.eingereicht} beim Ausbilder` + : 'nichts offen' + } + ton={stand.berichtsheft.fehlendeWochen > 0 ? 'fehler' : undefined} + /> +
+ + {stand.berichtsheft.fehlendeWochen > 0 ? ( +

+ + {stand.berichtsheft.fehlendeWochen}{' '} + {stand.berichtsheft.fehlendeWochen === 1 ? 'Woche' : 'Wochen'} ohne Berichtsheftblatt. + {' '} + Ohne geführten Nachweis gibt es keine Zulassung zur Abschlussprüfung.{' '} + Jetzt nachtragen. +

+ ) : null} + + +
+ ); +} + +function Kachel({ + titel, + wert, + anteil, + fuss, + ton, +}: { + titel: string; + wert: string; + anteil: number; + fuss: string; + ton?: string; +}) { + return ( +
+

{titel}

+ {wert} +
+ +
+ + {fuss} + +
+ ); +} + +// --- Rahmenplanabdeckung --------------------------------------------------- + +function Rahmenplan({ azubiId }: { azubiId: string }) { + const { positionen, gruppennamen } = rahmenplanabdeckung(azubiId); + const gruppen = nachGruppen(positionen); + const gesamt = bilanz(positionen); + + return ( +
+
+

+ Abdeckung des Ausbildungsrahmenplans +

+

+ Die Ansicht, die im Zweifel gegenüber der IHK zählt. Als vermittelt gilt eine + Position, wenn es dazu eine abgenommene Praxisaufgabe oder einen abgeschlossenen + Checklisten-Durchlauf gibt – oder mindestens zwei gelesene Handouts. +

+
+ +
+
+ + {gesamt.abgedeckt} von {gesamt.gesamt} Positionen vermittelt + + {gesamt.prozent} % +
+
+ +
+
+ + ✓ {gesamt.abgedeckt} vermittelt + + + ◐ {gesamt.angefangen} angefangen + + ○ {gesamt.offen} geplant + {gesamt.ohneInhalt > 0 ? ( + + ! {gesamt.ohneInhalt} ohne Inhalt + + ) : null} +
+
+ + {gesamt.ohneInhalt > 0 ? ( +

+ + Zu {gesamt.ohneInhalt} {gesamt.ohneInhalt === 1 ? 'Position' : 'Positionen'} gibt es im Lehrgang + überhaupt keinen Inhalt. + {' '} + Das ist keine Lücke beim Azubi, sondern eine im Lehrgang – dort hilft kein Lernen, sondern nur ein + neues Handout oder eine neue Aufgabe unter content/. +

+ ) : null} + + {gruppen.map((gruppe) => ( +
0}> + + {gruppennamen[gruppe.gruppe] ?? gruppe.gruppe} + + {' '} + – {gruppe.bilanz.abgedeckt} von {gruppe.bilanz.gesamt} vermittelt + + + +
    + {gruppe.positionen.map((position) => ( +
  • + {position.id} + + + {position.titel} + + {position.verordnung} + {position.geplant > 0 + ? ` · ${position.nachweise.filter((n) => n.erledigt).length} von ${position.geplant} Nachweisen` + : ''} + + + {position.nachweise.length > 0 ? ( + + {position.nachweise.map((nachweis) => ( + + {nachweis.erledigt ? '✓' : '○'} {nachweis.id} + + ))} + + ) : null} + + + + {stufenZeichen[position.stufe]} {stufenText[position.stufe]} + +
  • + ))} +
+
+ ))} +
+ ); +} + +// --- Lernfelder ------------------------------------------------------------ + +function Lernfelder({ azubiId }: { azubiId: string }) { + const { lernfelder } = rahmenplanabdeckung(azubiId); + const jahre = [1, 2, 3]; + + return ( +
+
+

+ Lernfelder der Berufsschule +

+

+ Nach derselben Regel bewertet. Der Unterricht selbst findet in der Schule statt – hier steht, was im + Betrieb dazu angekommen ist. +

+
+ + {jahre.map((jahr) => { + const eigene = lernfelder.filter((l) => l.jahr === jahr); + if (eigene.length === 0) return null; + + return ( +
+

{jahr}. Ausbildungsjahr

+
    + {eigene.map((lernfeld) => ( +
  • + {lernfeld.id} + + {lernfeld.titel} + + {lernfeld.stunden} Stunden + + + + {stufenZeichen[lernfeld.stufe]} {stufenText[lernfeld.stufe]} + +
  • + ))} +
+
+ ); + })} +
+ ); +} + +// --- Ergebnisverlauf ------------------------------------------------------- + +function Kontrollverlauf({ azubiId }: { azubiId: string }) { + const ausbildung = ausbildungNachAzubi(azubiId); + const stand = eigenerStand(azubiId, ausbildung?.beginn ?? null); + return ; +} + +function ErgebnisTabelle({ + ergebnisse, +}: { + ergebnisse: { + id: string; + titel: string; + woche: number; + prozent: number; + note: number; + notentext: string; + erreicht: number; + moeglich: number; + korrigiertAm: string; + }[]; +}) { + const stand = verlauf(ergebnisse); + + return ( +
+
+

+ Lernzielkontrollen +

+ {stand.anzahl > 0 ? ( + + Schnitt {stand.schnittProzent} % · Note {stand.schnittNote.toString().replace('.', ',')} · beste{' '} + {stand.beste} % + + ) : null} +
+ + {ergebnisse.length === 0 ? ( +

+ Noch keine korrigierte Lernzielkontrolle. +

+ ) : ( +
    + {ergebnisse.map((ergebnis) => ( +
  1. + Woche {ergebnis.woche} + + {ergebnis.titel} + + {zahl(ergebnis.erreicht)} von {zahl(ergebnis.moeglich)} Punkten · korrigiert{' '} + {ergebnis.korrigiertAm} + + + + + + + {ergebnis.prozent} % + + + {ergebnis.note} + +
  2. + ))} +
+ )} +
+ ); +} + +function anteil(erledigt: number, gesamt: number): number { + if (gesamt <= 0) return 0; + return Math.round((erledigt / gesamt) * 100); +} + +function zahl(wert: number): string { + return wert.toString().replace('.', ','); +} diff --git a/src/app/(angemeldet)/feedback/Gespraechsformular.tsx b/src/app/(angemeldet)/feedback/Gespraechsformular.tsx new file mode 100644 index 0000000..1c62927 --- /dev/null +++ b/src/app/(angemeldet)/feedback/Gespraechsformular.tsx @@ -0,0 +1,282 @@ +'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(gespraechSpeichern, {}); + const [bestaetigungsstand, bestaetigen] = useActionState(gespraechBestaetigen, {}); + + const eigeneSpalte = istAzubi ? 'azubi' : 'ausbilder'; + + return ( +
+ + + {/* Nach Abschluss steht das Datum schon groß im Kopf – ein zweites, + gesperrtes Feld daneben sagt nichts dazu. */} + {!istAzubi && !gesperrt ? ( +
+ + +
+ ) : null} + + {/* --- Bewertungsbogen --- */} + +
+
+

+ Bewertungsbogen +

+

+ {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.'} +

+
+ +
    + {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 ( +
  • +
    + {kriterium.titel} + {kriterium.erlaeuterung} +
    + +
    + + {istAzubi ? 'Deine Selbsteinschätzung' : 'Deine Einschätzung'} für {kriterium.titel} + + + {inhalt.skala.map((stufe) => ( + + ))} +
    + +
    + + {anderer === null || anderer === undefined + ? `${istAzubi ? 'Ausbilder' : 'Azubi'}: noch offen` + : `${istAzubi ? 'Ausbilder' : 'Selbsteinschätzung'}: ${anderer} (${ + inhalt.skala.find((s) => s.wert === anderer)?.text ?? '' + })`} + + + {weitAuseinander ? ( + + ! unterschiedliche Sicht + + ) : null} +
    + + {/* 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 ? ( +

    {bewertung.bemerkung}

    + ) : null + ) : ( + + )} +
  • + ); + })} +
+
+ + {/* --- Freitexte --- */} + + {/* Nach Abschluss liest auch der Ausbilder nur noch – ein gesperrtes + Eingabefeld sieht aus, als könnte man es doch noch benutzen. */} + {istAzubi || gesperrt ? ( +
+

Stärken, Entwicklungsfelder, Ziele

+ + + + {inhalt.sonstiges ? : null} +
+ ) : ( +
+

Stärken, Entwicklungsfelder, Ziele

+ +
+ +