diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cedf55..ff234bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,58 @@ Vor `npm version` wandert der Abschnitt „Unveröffentlicht" unter eine neue ## [Unveröffentlicht] +## [0.7.0] – 2026-09-10 + +Das Berichtsheft – der Teil, ohne den es keine Zulassung zur Abschlussprüfung +gibt. + +### Hinzugefügt + +- **Tägliche Stichworte**: je Werktag ein eigenes kleines Formular mit Text und + Stundenzahl. Bewusst je Tag getrennt absendbar, damit es am Handy in zwei + Minuten geht und nicht die ganze Woche neu geschrieben wird +- **Wochenblatt** mit Berichtszeitraum, betrieblichen Tätigkeiten und + Unterweisungen (je mit Stundenangabe), Berufsschule (Lernfeld, Themen, + Stunden) und Sonstiges; Zeilen lassen sich hinzufügen und entfernen +- **Vorschläge aus dem System**: aus den Tagesnotizen der Woche, den in dieser + Woche abgegebenen oder abgenommenen Praxisaufgaben, den abgeschlossenen + Checklisten-Durchläufen und den gelesenen Handouts. Ein Klick übernimmt den + Vorschlag ins Blatt, wo er bearbeitet wird +- **Prüfung vor dem Absenden**, mitlaufend beim Tippen: + - fehlende oder unglaubwürdige Stundenangaben, Wochensumme + - Floskeln wie „diverse Arbeiten" oder „wie in der Vorwoche" – aber nur, wenn + die Floskel den Eintrag ausmacht oder kein Tätigkeitsverb vorkommt + - IP- und MAC-Adressen, E-Mail-Adressen, Zugangsdaten, Telefonnummern und + Kundennamen (erkannt an der Rechtsform) + - **Warnung, keine Sperre.** Der Knopf zum Einreichen bleibt immer bedienbar +- **Formulierungshilfe**: Tätigkeitsverben nach Einsatzfeld gruppiert, sechs + Regeln und acht Gegenüberstellungen „so besser nicht / so ist es richtig" + samt Begründung +- **Abzeichnen**: Der Ausbilder sieht eingereichte Blätter unter „Was auf dich + wartet", liest sie in einer Nur-Lese-Ansicht mit derselben Prüfung und kann + abzeichnen oder zur Nacharbeit zurückgeben. Zurückgeben verlangt eine + Anmerkung – ohne sie weiß der Azubi nicht, was zu ändern ist +- Abgezeichnete Blätter sind **gesperrt**: keine Bearbeitung, keine Vorschläge, + kein Einreichen mehr +- **Fehlende Wochen** werden benannt: Für jede vergangene Woche ohne Blatt gibt + es einen Knopf zum Nachtragen. Die laufende Nummer richtet sich nach dem + Zeitraum, nicht nach dem Anlegen – wer nachträgt, bekommt die richtige Nummer +- **PDF im Layout eines üblichen IHK-Ausbildungsnachweises**: Kopffeld mit Name, + Beruf, Ausbildungsjahr und Berichtszeitraum, gerahmte Kästen mit + Stundenspalte und Zwischensummen, Anmerkung des Ausbilders, + Unterschriftenzeilen für Auszubildenden und Ausbilder. Einzeln oder als + ganzer Zeitraum mit einem Blatt je Seite +- Das Ausbildungsjahr wird aus dem Ausbildungsbeginn gerechnet +- 55 Tests für Prüfung, Vorschläge und PDF-Erzeugung + +### Behoben + +- Im Kopffeld des PDF liefen die beiden Spalten auseinander: `dokument.y` + wandert bei jedem Schreibvorgang weiter und taugte nicht als Bezugspunkt + innerhalb der Schleife +- Das Abzeichnen-Formular hatte keine `action` – der Knopf hätte nichts getan. + Aufgefallen ist das nur, weil ESLint die ungenutzte Serveraktion gemeldet hat + ## [0.6.0] – 2026-09-10 Checklisten mit Durchläufen – und der erste PDF-Export. @@ -267,6 +319,7 @@ anmelden und verwalten. - `npm run konto:anlegen` für das erste Ausbilderkonto [Unveröffentlicht]: #unveröffentlicht +[0.7.0]: #070--2026-09-10 [0.6.0]: #060--2026-09-10 [0.5.0]: #050--2026-09-10 [0.4.0]: #040--2026-09-10 diff --git a/DEVLOG.md b/DEVLOG.md index c78a95a..c3f24eb 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -7,6 +7,46 @@ Format: `HH:MM–HH:MM (Dauer) · was gemacht wurde` --- +## 2026-09-10 · Baustein 7 – Berichtsheft + +| Von | Bis | Dauer | Tätigkeit | +|---|---|---|---| +| 21:50 | 22:05 | 0:15 | Prüflogik: Stunden, Floskeln, schützenswerte Angaben – mit 31 Tests | +| 22:05 | 22:15 | 0:10 | Vorschläge aus dem System, Formulierungshilfe, 15 Tests | +| 22:15 | 22:25 | 0:10 | Datenmodell, Migration, Repository | +| 22:25 | 22:40 | 0:15 | PDF im IHK-Layout mit Kästen, Stundenspalten und Unterschriften | +| 22:40 | 23:00 | 0:20 | Seiten, Formulare, Abzeichnen, Tagesnotizen | +| 23:00 | 23:15 | 0:15 | Durchstich über den ganzen Weg, zwei Fehler behoben | + +**Summe Baustein 7: rund 1 h 25 min** + +### Was aufgefallen ist + +- Die Floskelprüfung war der kniffligste Teil. „Diverse Arbeiten" ist eine + Floskel, „Diverse Patchkabel konfektioniert und beschriftet" nicht. Gelöst + über zwei Bedingungen: Die Prüfung schlägt an, wenn die Floskel den Eintrag + im Wesentlichen ausmacht **oder** wenn im ganzen Eintrag kein einziges + Tätigkeitsverb vorkommt. +- Bei den Kundennamen hat ein Test einen echten Fehler gefunden: „Beispiel + e. V." wurde nicht erkannt. Ursache war ein `\b` am Ende des Musters – nach + einem Punkt gibt es vor einem Leerzeichen keine Wortgrenze. Ersetzt durch + einen negativen Vorausblick. +- ESLint hat wieder einen echten Bug gefunden: Das Abzeichnen-Formular hatte + gar keine `action`, der Knopf hätte nichts bewirkt. Sichtbar wurde das nur + über die Meldung „Serveraktion zugewiesen, aber nie benutzt". +- Im PDF-Kopffeld liefen die beiden Spalten auseinander, weil ich `dokument.y` + als Bezugspunkt in der Schleife benutzt habe – der Wert wandert aber bei + jedem Schreibvorgang weiter. Wieder etwas, das erst beim Ansehen auffiel. +- Beim Prüfen des PDF-Zugriffs ohne Anmeldung kam zunächst 200. Erst der + zweite Blick mit `maxRedirects: 0` zeigte: 307 auf /anmelden, 0 Byte, kein + PDF. Playwright war der Weiterleitung gefolgt. Merke: Bei + Berechtigungsprüfungen nie den Weiterleitungen folgen lassen. +- Die laufende Nummer eines Wochenblatts richtet sich nach dem Zeitraum, nicht + nach der Reihenfolge des Anlegens. Wer eine Woche nachträgt, soll Nr. 3 + bekommen und nicht Nr. 12 – die dahinterliegenden Blätter rücken auf. + +--- + ## 2026-09-10 · Baustein 6 – Checklisten und PDF | Von | Bis | Dauer | Tätigkeit | diff --git a/ENTSCHEIDUNGEN.md b/ENTSCHEIDUNGEN.md index 8632e14..ef96c38 100644 --- a/ENTSCHEIDUNGEN.md +++ b/ENTSCHEIDUNGEN.md @@ -257,3 +257,55 @@ Beim Checklisten-Durchlauf fallen die beiden Rechte auseinander: Der Ausbilder hakt also nichts für seinen Azubi ab. Im Nachweis soll keine Arbeit stehen, die der Azubi nicht selbst gemacht hat. Für die Abnahme gibt es die Praxisaufgaben – dort ist es genau andersherum. + +## E27 · Die Berichtsheftprüfung warnt, sie sperrt nicht + +`pruefeWochenblatt()` liefert eine Liste von Warnungen und nichts anderes. Es +gibt bewusst **kein** `darfEinreichen()`. + +Grund: Eine Anwendung, die das Einreichen verhindert, erzieht dazu, Unsinn +einzutragen, bis die Prüfung zufrieden ist. Der Azubi soll die Warnung sehen – +was er damit macht, entscheidet er. Der Knopf heißt dann eben +„Trotz 3 Warnungen einreichen". + +Die Prüfung läuft im Browser beim Tippen mit und benutzt dieselbe Funktion wie +der Server. Sie ist keine Sicherheitsmaßnahme, sondern eine Hilfe. + +## E28 · Floskeln werden am Zusammenhang erkannt, nicht am Wort + +„Diverse Arbeiten" ist eine Floskel. „Diverse Patchkabel konfektioniert, +gemessen und im Verteiler beschriftet" ist keine. Ein reiner Wortabgleich würde +beide gleich behandeln und wäre nach dem dritten Fehlalarm nur noch lästig. + +Die Prüfung schlägt deshalb nur an, wenn + +- die Floskel den Eintrag im Wesentlichen ausmacht (höchstens 20 Zeichen + Rest), **oder** +- im ganzen Eintrag kein einziges Tätigkeitsverb vorkommt. + +Dieselbe Verbliste trägt auch die Formulierungshilfe. + +## E29 · Die laufende Nummer richtet sich nach dem Zeitraum + +Wird eine vergessene Woche nachgetragen, bekommt sie die Nummer, die ihr +zeitlich zusteht; alle späteren Blätter rücken eine Nummer weiter. + +Der Grund ist das Papierformular: Ein Ausbildungsnachweis wird chronologisch +abgeheftet, und Nr. 12 zwischen Nr. 3 und Nr. 4 wäre gegenüber der IHK nicht +erklärbar. + +## E30 · Kundennamen werden an der Rechtsform erkannt + +Personennamen lassen sich nicht zuverlässig erkennen, ohne ständig Fehlalarme +zu erzeugen. Firmennamen dagegen schon: Sie enden fast immer auf GmbH, AG, KG, +mbH, e. V. und Ähnliches. + +Das Muster schließt bewusst mit einem negativen Vorausblick statt mit `\b` – +Rechtsformen wie „e. V." enden auf einen Punkt, und dort gibt es vor einem +Leerzeichen keine Wortgrenze. Genau dieser Fall war zuerst durchgerutscht. + +## E31 · Die Formulierungshilfe steht im Quelltext, nicht unter content/ + +Tätigkeitsverben und die Gegenüberstellungen sind Bedienhilfe der Anwendung, +kein Ausbildungsinhalt. Sie ändern sich nicht mit dem Lehrgang und gehören +deshalb nicht in dieselbe Ablage wie Handouts und Aufgaben. diff --git a/README.md b/README.md index 4e07093..d45d742 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 6 von 10 sind fertig – Fundament, Inhalte, -> Wochenplan, Merkkarten, Praxisaufgaben, Checklisten. Der -> [Fahrplan](ROADMAP.md) zeigt, was folgt, das +> **Stand:** Bausteine 1 bis 7 von 10 sind fertig – Fundament, Inhalte, +> Wochenplan, Merkkarten, Praxisaufgaben, Checklisten und das Berichtsheft. +> Der [Fahrplan](ROADMAP.md) zeigt, was folgt, das > [Änderungsprotokoll](CHANGELOG.md), was schon drin ist. --- diff --git a/ROADMAP.md b/ROADMAP.md index 31fe217..b885909 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -72,7 +72,15 @@ Legende: `[ ]` offen · `[~]` in Arbeit · `[x]` fertig - [x] Abgeschlossene Durchläufe bleiben als Nachweis und sind gesperrt - [x] PDF-Export mit Unterschriftenzeilen - [x] Ausbilder liest mit, hakt aber nicht für den Azubi ab -- [ ] **B7 · Berichtsheft: Tagesnotizen, Vorschläge, Prüfung, Abzeichnen, PDF** +- [x] **B7 · Berichtsheft: Tagesnotizen, Vorschläge, Prüfung, Abzeichnen, PDF** + - [x] Tägliche Stichworte, je Tag ein eigenes kleines Formular + - [x] Vorschläge aus Tagesnotizen, Aufgaben, Checklisten und Handouts + - [x] Formulierungshilfe: Tätigkeitsverben und „so besser nicht / so ist es richtig" + - [x] Prüfung auf fehlende Stunden, Floskeln und schützenswerte Angaben + - [x] Warnung, keine Sperre – Einreichen ist immer möglich + - [x] Einreichen, zurückgeben mit Anmerkung, abzeichnen; danach gesperrt + - [x] Fehlende Wochen werden benannt und lassen sich nachtragen + - [x] PDF im IHK-Layout, einzeln oder als ganzer Zeitraum ## 0.5 – Bewertung diff --git a/docs/beispiel-ausbildungsnachweis.pdf b/docs/beispiel-ausbildungsnachweis.pdf new file mode 100644 index 0000000..4d04716 Binary files /dev/null and b/docs/beispiel-ausbildungsnachweis.pdf differ diff --git a/docs/bilder/02-ausbilder-ueberblick.png b/docs/bilder/02-ausbilder-ueberblick.png index a16833d..50bf49a 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 486bc12..8e79fa5 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 be5dba8..a34b07b 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 c55bfd4..a2b1d72 100644 Binary files a/docs/bilder/05-aufgabe-abnahme.png and b/docs/bilder/05-aufgabe-abnahme.png differ diff --git a/docs/bilder/10-azubi-woche.png b/docs/bilder/10-azubi-woche.png index ac44d6b..c9a8685 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 f343dbd..bd20e8e 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 e0e3be6..0d42a06 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 d167a5b..d255485 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 f901b8f..0ec5b82 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 4c1d485..7edfe00 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 1a93c31..284a1cb 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 8261513..31ab39a 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 b9f07ae..2a07b28 100644 Binary files a/docs/bilder/18-checklisten.png and b/docs/bilder/18-checklisten.png differ diff --git a/docs/bilder/20-dunkel.png b/docs/bilder/20-dunkel.png index a0eabcd..8fcaf6c 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 new file mode 100644 index 0000000..132752f Binary files /dev/null and b/docs/bilder/21-berichtsheft.png differ diff --git a/docs/bilder/22-wochenblatt.png b/docs/bilder/22-wochenblatt.png new file mode 100644 index 0000000..4ade82b Binary files /dev/null and b/docs/bilder/22-wochenblatt.png differ diff --git a/scripts/screenshots.ts b/scripts/screenshots.ts index 014a8cb..f179891 100644 --- a/scripts/screenshots.ts +++ b/scripts/screenshots.ts @@ -122,6 +122,17 @@ async function main() { await seite.waitForSelector('h1'); await bild(seite, '18-checklisten'); + await seite.goto(`${BASIS}/berichtsheft`); + await seite.waitForSelector('h1'); + await bild(seite, '21-berichtsheft'); + + const blatt = await seite.$('.blattzeile-link'); + if (blatt) { + await blatt.click(); + await seite.waitForSelector('h1'); + await bild(seite, '22-wochenblatt'); + } + const lauf = await seite.$('.laufzeile-haupt'); if (lauf) { await lauf.click(); diff --git a/src/app/(angemeldet)/berichtsheft/BerichtsMarke.tsx b/src/app/(angemeldet)/berichtsheft/BerichtsMarke.tsx new file mode 100644 index 0000000..340d38b --- /dev/null +++ b/src/app/(angemeldet)/berichtsheft/BerichtsMarke.tsx @@ -0,0 +1,18 @@ +import type { BerichtStatus } from '@/lib/db/schema'; + +/** Zustand eines Wochenblatts – nie nur über die Farbe, immer mit Zeichen. */ +export const berichtsStatusText: Record = { + entwurf: { text: 'Entwurf', ton: 'neutral', zeichen: '○' }, + eingereicht: { text: 'eingereicht', ton: 'warnung', zeichen: '▲' }, + zurueckgegeben: { text: 'zurückgegeben', ton: 'fehler', zeichen: '↩' }, + abgezeichnet: { text: 'abgezeichnet', ton: 'gut', zeichen: '✓' }, +}; + +export function BerichtsMarke({ status }: { status: BerichtStatus }) { + const marke = berichtsStatusText[status]; + return ( + + {marke.zeichen} {marke.text} + + ); +} diff --git a/src/app/(angemeldet)/berichtsheft/Blattformular.tsx b/src/app/(angemeldet)/berichtsheft/Blattformular.tsx new file mode 100644 index 0000000..31e2a7f --- /dev/null +++ b/src/app/(angemeldet)/berichtsheft/Blattformular.tsx @@ -0,0 +1,380 @@ +'use client'; + +import { useActionState, useState } from 'react'; +import { Absenden, Meldung } from '@/komponenten/Formularmeldung'; +import { blattEinreichen, blattSpeichern, type Rueckmeldung } from './aktionen'; +import { pruefeWochenblatt, summeStunden, type Wochenblatt } from '@/lib/berichtsheft/pruefung'; +import type { Vorschlag } from '@/lib/berichtsheft/vorschlaege'; + +/** + * Das Wochenblatt zum Ausfüllen. + * + * Die Prüfung läuft im Browser mit, während getippt wird – dieselbe Funktion, + * die auch serverseitig verwendet wird. Sie warnt, sie sperrt nicht: Der Knopf + * zum Einreichen bleibt immer bedienbar. + */ +export function Blattformular({ + berichtId, + anfangsblatt, + vorschlaege, + status, +}: { + berichtId: string; + anfangsblatt: Wochenblatt; + vorschlaege: Vorschlag[]; + status: string; +}) { + const [blatt, setzeBlatt] = useState(() => ({ + betrieb: anfangsblatt.betrieb.length > 0 ? anfangsblatt.betrieb : [{ text: '', stunden: 0 }], + schule: anfangsblatt.schule.length > 0 ? anfangsblatt.schule : [{ lernfeld: '', themen: '', stunden: 0 }], + sonstiges: anfangsblatt.sonstiges, + })); + const [uebernommen, setzeUebernommen] = useState([]); + + const [speicherZustand, speichern] = useActionState(blattSpeichern, {}); + const [einreichZustand, einreichen] = useActionState(blattEinreichen, {}); + + const gesperrt = status === 'abgezeichnet'; + const warnungen = pruefeWochenblatt(blatt); + const ernste = warnungen.filter((w) => w.schwere === 'warnung'); + + function betriebAendern(stelle: number, teil: Partial<{ text: string; stunden: number }>) { + setzeBlatt((v) => ({ + ...v, + betrieb: v.betrieb.map((e, i) => (i === stelle ? { ...e, ...teil } : e)), + })); + } + + function schuleAendern(stelle: number, teil: Partial<{ lernfeld: string; themen: string; stunden: number }>) { + setzeBlatt((v) => ({ + ...v, + schule: v.schule.map((e, i) => (i === stelle ? { ...e, ...teil } : e)), + })); + } + + function uebernehmen(vorschlag: Vorschlag) { + setzeUebernommen((v) => [...v, vorschlag.id]); + + if (vorschlag.ziel === 'schule') { + setzeBlatt((v) => { + const leer = v.schule.findIndex((e) => e.lernfeld === '' && e.themen === ''); + const neu = { lernfeld: vorschlag.herkunft, themen: vorschlag.text, stunden: vorschlag.stunden }; + return { + ...v, + schule: leer >= 0 ? v.schule.map((e, i) => (i === leer ? neu : e)) : [...v.schule, neu], + }; + }); + return; + } + + setzeBlatt((v) => { + const leer = v.betrieb.findIndex((e) => e.text.trim() === ''); + const neu = { text: vorschlag.text, stunden: vorschlag.stunden }; + return { + ...v, + betrieb: leer >= 0 ? v.betrieb.map((e, i) => (i === leer ? neu : e)) : [...v.betrieb, neu], + }; + }); + } + + const offeneVorschlaege = vorschlaege.filter((v) => !uebernommen.includes(v.id)); + + return ( +
+ {!gesperrt && offeneVorschlaege.length > 0 ? ( +
+
+

+ Vorschläge aus dieser Woche +

+

+ Übernehmen und dann in eigene Worte bringen. Ein wörtlich übernommener Satz ist kein + Ausbildungsnachweis, und die Stundenangabe ist nur ein Anhaltspunkt. +

+
+ +
    + {offeneVorschlaege.map((vorschlag) => ( +
  • + +
  • + ))} +
+
+ ) : null} + +
+ + + {/* --- Betriebliche Tätigkeiten --- */} +
+
+

+ Betriebliche Tätigkeiten und Unterweisungen +

+ {zahlText(summeStunden({ ...blatt, schule: [] }))} h +
+ + {blatt.betrieb.map((eintrag, stelle) => ( +
+
+ +