Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3009c53b5c | ||
|
|
44e6c8bb79 |
@@ -26,6 +26,42 @@ Vor `npm version` wandert der Abschnitt „Unveröffentlicht" unter eine neue
|
|||||||
|
|
||||||
## [Unveröffentlicht]
|
## [Unveröffentlicht]
|
||||||
|
|
||||||
|
## [1.1.0] – 2026-10-05
|
||||||
|
|
||||||
|
Module lassen sich abschalten.
|
||||||
|
|
||||||
|
### Hinzugefügt
|
||||||
|
|
||||||
|
- **Verwaltung → Module.** Merkkarten, Praxisaufgaben, Checklisten,
|
||||||
|
Berichtsheft, Lernzielkontrollen und Feedbackgespräch lassen sich für die
|
||||||
|
ganze Lern-Bar ab- und wieder einschalten, sofort und ohne Neustart. Zu jedem
|
||||||
|
Modul steht dort, was es tut, was sich beim Abschalten ändert und wer es
|
||||||
|
zuletzt umgeschaltet hat. Wochenplan, Handouts, Glossar, Auswertung und
|
||||||
|
Verwaltung bleiben immer an
|
||||||
|
- Ein abgeschaltetes Modul ist **überall weg**: Navigation, Wochenplan und
|
||||||
|
Wochenfortschritt (eine Woche erreicht ohne das Modul trotzdem 100 %), „Was
|
||||||
|
auf dich wartet", „Mein Stand", Ergebnisverlauf, Rahmenplanabdeckung,
|
||||||
|
Vorschläge fürs Berichtsheft, Merkkarten im Handout, Kurzbefehle im
|
||||||
|
App-Symbol
|
||||||
|
- Ein abgeschaltetes Modul ist **gesperrt, nicht nur ausgeblendet**: Seiten,
|
||||||
|
PDF-Ausgaben, Anhänge und alle 37 Serveraktionen der Module prüfen den
|
||||||
|
Schalter. Die Lernstand-Schnittstelle antwortet mit 410, und Geräte verwerfen
|
||||||
|
daraufhin offline gesammelte Bewertungen
|
||||||
|
- **Abschalten löscht nichts.** Nach dem Einschalten ist alles unverändert da
|
||||||
|
- Die Verwaltung hat zwei Bereiche: Konten und Module
|
||||||
|
- Migration `0010_module` – eine bestehende Datenbank hat danach alle Module an
|
||||||
|
und verhält sich wie vorher
|
||||||
|
- 12 Tests für die Modulregeln, Abschnitt A8 im Abnahmelauf (abschalten,
|
||||||
|
Sperre für beide Rollen prüfen, einschalten, Daten noch da)
|
||||||
|
- Anleitung Kapitel 9 „Module ein- und ausschalten" mit Bildschirmfoto,
|
||||||
|
README-Abschnitt, Entscheidung E43
|
||||||
|
|
||||||
|
### Geändert
|
||||||
|
|
||||||
|
- Das App-Manifest wird bei jedem Abruf erzeugt statt einmal beim Bau, damit
|
||||||
|
die Kurzbefehle zum Modulstand passen
|
||||||
|
- In der Anleitung haben sich die Kapitel ab 9 um eins verschoben
|
||||||
|
|
||||||
## [1.0.5] – 2026-10-05
|
## [1.0.5] – 2026-10-05
|
||||||
|
|
||||||
Das zweite Vierteljahr beginnt, mit Blick auf Teil 1 der Abschlussprüfung.
|
Das zweite Vierteljahr beginnt, mit Blick auf Teil 1 der Abschlussprüfung.
|
||||||
|
|||||||
@@ -493,3 +493,41 @@ oder eine erfolgreiche Anmeldung setzt den Zähler zurück.
|
|||||||
- **Gezählt wird vor der Prüfung.** Jeder Versuch gilt sofort als Fehlversuch
|
- **Gezählt wird vor der Prüfung.** Jeder Versuch gilt sofort als Fehlversuch
|
||||||
und wird bei Erfolg zurückgenommen. Zwischen Sperrprüfung und Zählen liegt
|
und wird bei Erfolg zurückgenommen. Zwischen Sperrprüfung und Zählen liegt
|
||||||
so kein `await`; sonst kämen gleichzeitige Anfragen alle an der Sperre vorbei.
|
so kein `await`; sonst kämen gleichzeitige Anfragen alle an der Sperre vorbei.
|
||||||
|
|
||||||
|
## E43 · Module schaltbar für die ganze Lern-Bar, gesperrt an drei Stellen, nie gelöscht
|
||||||
|
|
||||||
|
Merkkarten, Praxisaufgaben, Checklisten, Berichtsheft, Lernzielkontrollen und
|
||||||
|
Feedbackgespräch lassen sich unter **Verwaltung → Module** abschalten. Wochenplan,
|
||||||
|
Handouts, Glossar, Auswertung und Verwaltung bleiben immer an – an ihnen hängt
|
||||||
|
alles andere, und die Auswertung passt sich ohnehin an.
|
||||||
|
|
||||||
|
- **Für die ganze Lern-Bar, nicht je Azubi.** Ein Betrieb startet mit einer
|
||||||
|
Fachrichtung und entscheidet einmal, womit er arbeitet. Schalter je Azubi
|
||||||
|
hätten jede Ausbilderansicht je Person unterschiedlich gemacht. Ließe sich
|
||||||
|
später ergänzen, ohne das Bestehende umzubauen: Der Stand ist schon heute ein
|
||||||
|
Objekt, das an einer Stelle entsteht (`repos/module`).
|
||||||
|
- **In der Datenbank, nicht in einer Konfigurationsdatei.** Ein Ausbilder soll
|
||||||
|
ohne Zugang zum Server umschalten können, und die Änderung soll sofort
|
||||||
|
wirken, ohne Neustart. Gespeichert wird nur, was je umgeschaltet wurde; fehlt
|
||||||
|
die Zeile, ist das Modul an. Eine bestehende Datenbank verhält sich nach der
|
||||||
|
Migration also genau wie vorher.
|
||||||
|
- **Gesperrt an drei Stellen, nicht im Layout.** Seiten rufen `erfordereModul`
|
||||||
|
auf (Antwort „nicht gefunden", wie bei fehlender Berechtigung, E21), Routen
|
||||||
|
für PDF, Anhänge und Lernstand fragen `modulAn`, Serveraktionen rufen
|
||||||
|
`pruefeModul`. Ein Layout je Modul wäre kürzer gewesen, greift aber nicht
|
||||||
|
zuverlässig: Next.js rendert ein Layout beim Wechsel zwischen den Seiten
|
||||||
|
darunter nicht neu.
|
||||||
|
- **Abschalten löscht nichts.** Beim Berichtsheft ist das zwingend – es ist ein
|
||||||
|
Nachweis. Für alle anderen Module gilt dasselbe, damit niemand beim
|
||||||
|
Ausprobieren Daten verliert.
|
||||||
|
- **Abgeschaltet heißt: trägt nirgends bei.** Kein Eintrag im Wochenplan, kein
|
||||||
|
Anteil am Wochenfortschritt, kein Nachweis in der Rahmenplanabdeckung, kein
|
||||||
|
Vorschlag fürs Berichtsheft, kein Kurzbefehl im App-Symbol. Sonst stünden
|
||||||
|
Inhalte für immer als offen da, die niemand mehr bearbeiten kann.
|
||||||
|
- **Offline gesammelte Bewertungen werden verworfen**, wenn die Merkkarten aus
|
||||||
|
sind. Die Lernstand-Schnittstelle antwortet mit 410 statt 404, und das Gerät
|
||||||
|
leert daraufhin seine Warteschlange, statt sie bei jeder Verbindung erneut zu
|
||||||
|
schicken.
|
||||||
|
- **Die Prüfung der Anmeldung kommt zuerst**, die des Moduls danach. Sonst sähe
|
||||||
|
ein nicht angemeldeter Besucher an „nicht gefunden" statt der Anmeldeseite,
|
||||||
|
welche Module abgeschaltet sind.
|
||||||
|
|||||||
@@ -144,6 +144,21 @@ npm run inhalte:pruefen
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Module ein- und ausschalten
|
||||||
|
|
||||||
|
Unter **Verwaltung → Module** lassen sich Merkkarten, Praxisaufgaben,
|
||||||
|
Checklisten, Berichtsheft, Lernzielkontrollen und Feedbackgespräch abschalten –
|
||||||
|
für die ganze Lern-Bar, sofort und ohne Neustart. Wochenplan, Handouts, Glossar,
|
||||||
|
Auswertung und Verwaltung bleiben immer an.
|
||||||
|
|
||||||
|
Ein abgeschaltetes Modul verschwindet aus Navigation, Wochenplan, Fortschritt,
|
||||||
|
Auswertung und Rahmenplanabdeckung, und seine Seiten, PDFs und Serveraktionen
|
||||||
|
sind gesperrt. **Gelöscht wird nichts:** Nach dem Einschalten ist alles wieder
|
||||||
|
da. Einzelheiten in der [Anleitung](docs/ANLEITUNG.md#9-module-ein--und-ausschalten),
|
||||||
|
die Begründung in [E43](ENTSCHEIDUNGEN.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Datensicherung
|
## Datensicherung
|
||||||
|
|
||||||
Gesichert werden **Datenbank und Anhänge**. Die Inhalte stecken im Repository
|
Gesichert werden **Datenbank und Anhänge**. Die Inhalte stecken im Repository
|
||||||
@@ -244,7 +259,7 @@ Protokollzeilen.
|
|||||||
```bash
|
```bash
|
||||||
npm run typecheck # TypeScript
|
npm run typecheck # TypeScript
|
||||||
npm run lint # ESLint
|
npm run lint # ESLint
|
||||||
npm run test # Vitest – Logik, die wehtut (315 Tests)
|
npm run test # Vitest – Logik, die wehtut (341 Tests)
|
||||||
npm run abnahme # der Abnahmelauf, siehe unten
|
npm run abnahme # der Abnahmelauf, siehe unten
|
||||||
npm run abnahme:container # derselbe Weg gegen das Docker-Abbild
|
npm run abnahme:container # derselbe Weg gegen das Docker-Abbild
|
||||||
```
|
```
|
||||||
@@ -263,7 +278,10 @@ Datenbank** und spielt den ganzen Weg durch:
|
|||||||
4. Der Azubi liest ein Handout, lernt eine Karte, meldet eine Aufgabe zur Abnahme
|
4. Der Azubi liest ein Handout, lernt eine Karte, meldet eine Aufgabe zur Abnahme
|
||||||
5. Der Ausbilder nimmt sie ab; sie schlägt in der Rahmenplanabdeckung durch
|
5. Der Ausbilder nimmt sie ab; sie schlägt in der Rahmenplanabdeckung durch
|
||||||
6. Das Wochenblatt wird angelegt und als PDF ausgegeben
|
6. Das Wochenblatt wird angelegt und als PDF ausgegeben
|
||||||
7. **Gesichert, absichtlich beschädigt, zurückgespielt.** Eine Sicherung, die
|
7. Berichtsheft und Merkkarten werden über die Verwaltung abgeschaltet: Seite
|
||||||
|
und PDF sind für beide Rollen gesperrt, nach dem Einschalten ist das
|
||||||
|
Wochenblatt unverändert da
|
||||||
|
8. **Gesichert, absichtlich beschädigt, zurückgespielt.** Eine Sicherung, die
|
||||||
nie zurückgespielt wurde, ist keine Sicherung
|
nie zurückgespielt wurde, ist keine Sicherung
|
||||||
|
|
||||||
Das Protokoll landet in [`docs/ABNAHME.md`](docs/ABNAHME.md).
|
Das Protokoll landet in [`docs/ABNAHME.md`](docs/ABNAHME.md).
|
||||||
|
|||||||
+11
@@ -117,6 +117,17 @@ Legende: `[ ]` offen · `[~]` in Arbeit · `[x]` fertig
|
|||||||
- [x] Abnahmekriterien einzeln durchgespielt und protokolliert
|
- [x] Abnahmekriterien einzeln durchgespielt und protokolliert
|
||||||
(`docs/ABNAHME.md`)
|
(`docs/ABNAHME.md`)
|
||||||
|
|
||||||
|
## 1.1 – Module
|
||||||
|
|
||||||
|
- [x] **Module abschaltbar** unter Verwaltung → Module
|
||||||
|
- [x] Merkkarten, Praxisaufgaben, Checklisten, Berichtsheft,
|
||||||
|
Lernzielkontrollen, Feedbackgespräch; das Gerüst bleibt immer an
|
||||||
|
- [x] Gesperrt in Seiten, Routen und Serveraktionen, nicht nur ausgeblendet
|
||||||
|
- [x] Wochenplan, Fortschritt, Auswertung, Rahmenplanabdeckung, Vorschläge
|
||||||
|
und App-Kurzbefehle richten sich nach dem Stand
|
||||||
|
- [x] Abschalten löscht nichts
|
||||||
|
- [x] Im Abnahmelauf geprüft (A8)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Danach – bewusst zurückgestellt
|
## Danach – bewusst zurückgestellt
|
||||||
|
|||||||
+16
-6
@@ -1,6 +1,6 @@
|
|||||||
# Abnahmeprotokoll
|
# Abnahmeprotokoll
|
||||||
|
|
||||||
Erzeugt am 2026-10-04 16:24 Uhr
|
Erzeugt am 2026-10-05 14:48 Uhr
|
||||||
|
|
||||||
Erzeugt von `scripts/pruefungen/abnahme.mjs` gegen eine **frische, leere Datenbank**.
|
Erzeugt von `scripts/pruefungen/abnahme.mjs` gegen eine **frische, leere Datenbank**.
|
||||||
|
|
||||||
@@ -54,15 +54,25 @@ Erzeugt von `scripts/pruefungen/abnahme.mjs` gegen eine **frische, leere Datenba
|
|||||||
|
|
||||||
- ✓ Der Azubi legt das Wochenblatt der laufenden Woche an
|
- ✓ Der Azubi legt das Wochenblatt der laufenden Woche an
|
||||||
- ✓ Ein Wochenblatt lässt sich öffnen
|
- ✓ Ein Wochenblatt lässt sich öffnen
|
||||||
- ✓ Das Wochenblatt kommt als PDF heraus — HTTP 200, 2649 Bytes
|
- ✓ Das Wochenblatt kommt als PDF heraus — HTTP 200, 2646 Bytes
|
||||||
|
|
||||||
## A8 · Sicherung zurückspielen
|
## A8 · Module abschalten
|
||||||
|
|
||||||
- ✓ Die Sicherung läuft durch — lernbar-20261004-182433.tar.gz
|
- ✓ Der Ausbilder schaltet das Berichtsheft ab
|
||||||
|
- ✓ Das Berichtsheft verschwindet aus der Navigation
|
||||||
|
- ✓ Die Seite ist für beide Rollen gesperrt, nicht nur ausgeblendet — Azubi HTTP 404, Ausbilder HTTP 404
|
||||||
|
- ✓ Auch das PDF ist gesperrt — HTTP 404
|
||||||
|
- ✓ Ohne Merkkarten zeigt das Handout keine Karten
|
||||||
|
- ✓ Ein Gerät mit gesammelten Bewertungen erfährt, dass es sie verwerfen soll — HTTP 410
|
||||||
|
- ✓ Nach dem Einschalten ist das Wochenblatt unverändert wieder da — HTTP 200
|
||||||
|
|
||||||
|
## A9 · Sicherung zurückspielen
|
||||||
|
|
||||||
|
- ✓ Die Sicherung läuft durch — lernbar-20261005-164818.tar.gz
|
||||||
- ✓ Zum Beweis wird der Datenbestand beschädigt — 2 → 1 Konten
|
- ✓ Zum Beweis wird der Datenbestand beschädigt — 2 → 1 Konten
|
||||||
- ✓ Die Rückspielung läuft durch — ohne Fehler
|
- ✓ Die Rückspielung läuft durch — ohne Fehler
|
||||||
- ✓ Der gelöschte Azubi ist wieder da — Tim Vogt
|
- ✓ Der gelöschte Azubi ist wieder da — Tim Vogt
|
||||||
- ✓ Auch der Lernverlauf kam mit zurück — 1 Einträge
|
- ✓ Auch der Lernverlauf kam mit zurück — 1 Einträge
|
||||||
- ✓ Der überschriebene Stand wurde zur Seite gelegt, nicht gelöscht — verdraengt-20261004-182436
|
- ✓ Der überschriebene Stand wurde zur Seite gelegt, nicht gelöscht — verdraengt-20261005-164821
|
||||||
|
|
||||||
**36 von 36 Prüfungen erfüllt.**
|
**43 von 43 Prüfungen erfüllt.**
|
||||||
|
|||||||
+64
-24
@@ -22,24 +22,25 @@ gemalt, sondern gemessen.
|
|||||||
6. [Eine Lernzielkontrolle korrigieren](#6-eine-lernzielkontrolle-korrigieren)
|
6. [Eine Lernzielkontrolle korrigieren](#6-eine-lernzielkontrolle-korrigieren)
|
||||||
7. [Die Rahmenplanabdeckung lesen](#7-die-rahmenplanabdeckung-lesen)
|
7. [Die Rahmenplanabdeckung lesen](#7-die-rahmenplanabdeckung-lesen)
|
||||||
8. [Ein Feedbackgespräch führen](#8-ein-feedbackgespräch-führen)
|
8. [Ein Feedbackgespräch führen](#8-ein-feedbackgespräch-führen)
|
||||||
|
9. [Module ein- und ausschalten](#9-module-ein--und-ausschalten)
|
||||||
|
|
||||||
**Für den Azubi**
|
**Für den Azubi**
|
||||||
|
|
||||||
9. [Die Woche](#9-die-woche)
|
10. [Die Woche](#10-die-woche)
|
||||||
10. [Handouts lesen und suchen](#10-handouts-lesen-und-suchen)
|
11. [Handouts lesen und suchen](#11-handouts-lesen-und-suchen)
|
||||||
11. [Merkkarten lernen](#11-merkkarten-lernen)
|
12. [Merkkarten lernen](#12-merkkarten-lernen)
|
||||||
12. [Eine Praxisaufgabe abgeben](#12-eine-praxisaufgabe-abgeben)
|
13. [Eine Praxisaufgabe abgeben](#13-eine-praxisaufgabe-abgeben)
|
||||||
13. [Eine Checkliste abarbeiten](#13-eine-checkliste-abarbeiten)
|
14. [Eine Checkliste abarbeiten](#14-eine-checkliste-abarbeiten)
|
||||||
14. [Das Berichtsheft führen](#14-das-berichtsheft-führen)
|
15. [Das Berichtsheft führen](#15-das-berichtsheft-führen)
|
||||||
15. [Eine Lernzielkontrolle schreiben](#15-eine-lernzielkontrolle-schreiben)
|
16. [Eine Lernzielkontrolle schreiben](#16-eine-lernzielkontrolle-schreiben)
|
||||||
|
|
||||||
**Überall**
|
**Überall**
|
||||||
|
|
||||||
16. [Auf dem Handy](#16-auf-dem-handy)
|
17. [Auf dem Handy](#17-auf-dem-handy)
|
||||||
17. [Ohne Netz](#17-ohne-netz)
|
18. [Ohne Netz](#18-ohne-netz)
|
||||||
18. [Hell und dunkel](#18-hell-und-dunkel)
|
19. [Hell und dunkel](#19-hell-und-dunkel)
|
||||||
19. [Drucken](#19-drucken)
|
20. [Drucken](#20-drucken)
|
||||||
20. [Wenn etwas nicht geht](#20-wenn-etwas-nicht-geht)
|
21. [Wenn etwas nicht geht](#21-wenn-etwas-nicht-geht)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -218,11 +219,49 @@ Unterschriftenfeldern. Abweichungen sind auch dort markiert.
|
|||||||
> anpassen. Ein bereits geführtes Gespräch behält seine Fassung, auch wenn du
|
> anpassen. Ein bereits geführtes Gespräch behält seine Fassung, auch wenn du
|
||||||
> die Vorlage später änderst.
|
> die Vorlage später änderst.
|
||||||
|
|
||||||
|
### 9. Module ein- und ausschalten
|
||||||
|
|
||||||
|
**Verwaltung → Module**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Nicht jeder Betrieb braucht alles. Führt ihr das Berichtsheft zum Beispiel im
|
||||||
|
Portal der Kammer, schaltest du es hier ab – für die ganze Lern-Bar, für alle
|
||||||
|
Azubis und Ausbilder zugleich.
|
||||||
|
|
||||||
|
| Modul | Abschaltbar |
|
||||||
|
|---|---|
|
||||||
|
| Merkkarten, Praxisaufgaben, Checklisten, Berichtsheft, Lernzielkontrollen, Feedbackgespräch | ja |
|
||||||
|
| Wochenplan, Handouts, Glossar, Auswertung, Verwaltung | nein – das Gerüst |
|
||||||
|
|
||||||
|
Was beim Abschalten passiert:
|
||||||
|
|
||||||
|
- **Das Modul verschwindet** aus der Navigation, aus dem Wochenplan, aus „Was
|
||||||
|
auf dich wartet" und aus der Auswertung. Zu jedem Modul steht auf der Seite,
|
||||||
|
was sich genau ändert.
|
||||||
|
- **Seiten und PDFs sind gesperrt**, nicht nur ausgeblendet. Wer sich die
|
||||||
|
Adresse gemerkt hat, bekommt „Diese Seite gibt es nicht".
|
||||||
|
- **Der Wochenfortschritt** zählt das Modul nicht mehr mit. Eine Woche ohne
|
||||||
|
Merkkarten erreicht also trotzdem 100 %.
|
||||||
|
- **Die Rahmenplanabdeckung** zählt abgeschaltete Aufgaben und Checklisten
|
||||||
|
nicht mehr. Bleiben nur Handouts, gilt eine Position mit zwei gelesenen
|
||||||
|
Handouts als vermittelt.
|
||||||
|
- **Nichts wird gelöscht.** Was im Modul schon geschrieben wurde – Berichtshefte,
|
||||||
|
Bewertungen, Abnahmen –, bleibt gespeichert und ist nach dem Einschalten
|
||||||
|
unverändert wieder da.
|
||||||
|
|
||||||
|
Unter jedem Modul steht, wer es wann zuletzt umgeschaltet hat.
|
||||||
|
|
||||||
|
> **Berichtsheft abschalten heißt nicht, kein Berichtsheft zu führen.** Der
|
||||||
|
> Ausbildungsnachweis ist Pflicht und Voraussetzung für die Zulassung zur
|
||||||
|
> Abschlussprüfung. Er wird dann eben woanders geführt. In den Wochenplänen
|
||||||
|
> steht freitags weiterhin „Berichtsheft schreiben" – das gilt auch dann.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Für den Azubi
|
## Für den Azubi
|
||||||
|
|
||||||
### 9. Die Woche
|
### 10. Die Woche
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -232,7 +271,7 @@ Frage lautet „Was mache ich jetzt?", nicht „Wie ist meine Gesamtlage?".
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 10. Handouts lesen und suchen
|
### 11. Handouts lesen und suchen
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -254,7 +293,7 @@ mit Querverweisen.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 11. Merkkarten lernen
|
### 12. Merkkarten lernen
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -273,7 +312,7 @@ nachzuholen.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 12. Eine Praxisaufgabe abgeben
|
### 13. Eine Praxisaufgabe abgeben
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -287,7 +326,7 @@ Dann gibst du ab. Der Ausbilder nimmt ab oder gibt zurück, mit Bemerkung.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 13. Eine Checkliste abarbeiten
|
### 14. Eine Checkliste abarbeiten
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -304,7 +343,7 @@ begonnen hast. Am Ende gibt es ein PDF – ein Beispiel liegt unter
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 14. Das Berichtsheft führen
|
### 15. Das Berichtsheft führen
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -323,7 +362,7 @@ Nach dem Abzeichnen ist das Blatt unveränderlich.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 15. Eine Lernzielkontrolle schreiben
|
### 16. Eine Lernzielkontrolle schreiben
|
||||||
|
|
||||||
Der Ausbilder gibt die Kontrolle frei, du siehst sie unter *Kontrollen*. Du
|
Der Ausbilder gibt die Kontrolle frei, du siehst sie unter *Kontrollen*. Du
|
||||||
schreibst deine Antworten und sendest ab.
|
schreibst deine Antworten und sendest ab.
|
||||||
@@ -336,7 +375,7 @@ Kontrolle, deren Lösung vorher abrufbar wäre, misst nichts.
|
|||||||
|
|
||||||
## Überall
|
## Überall
|
||||||
|
|
||||||
### 16. Auf dem Handy
|
### 17. Auf dem Handy
|
||||||
|
|
||||||
Die Anwendung ist für das Handy gebaut, nicht dafür angepasst. Der Azubi hat es
|
Die Anwendung ist für das Handy gebaut, nicht dafür angepasst. Der Azubi hat es
|
||||||
in der Tasche, nicht den Laptop.
|
in der Tasche, nicht den Laptop.
|
||||||
@@ -355,7 +394,7 @@ Danach startet sie ohne Adresszeile und mit eigenem Symbol.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 17. Ohne Netz
|
### 18. Ohne Netz
|
||||||
|
|
||||||
Im Serverraum, im Keller, im Zug gibt es kein Netz. Deshalb:
|
Im Serverraum, im Keller, im Zug gibt es kein Netz. Deshalb:
|
||||||
|
|
||||||
@@ -376,7 +415,7 @@ weggeräumt.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 18. Hell und dunkel
|
### 19. Hell und dunkel
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -386,7 +425,7 @@ hast – sonst käme schwarzes Papier aus dem Drucker.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 19. Drucken
|
### 20. Drucken
|
||||||
|
|
||||||
Für die Dokumente, die eine Unterschrift tragen, gibt es **PDFs**:
|
Für die Dokumente, die eine Unterschrift tragen, gibt es **PDFs**:
|
||||||
Wochenblatt, Checklisten-Durchlauf, Feedbackgespräch.
|
Wochenblatt, Checklisten-Durchlauf, Feedbackgespräch.
|
||||||
@@ -397,11 +436,12 @@ mitten durchgeschnitten, aufklappbare Abschnitte drucken aufgeklappt.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 20. Wenn etwas nicht geht
|
### 21. Wenn etwas nicht geht
|
||||||
|
|
||||||
| Beobachtung | Woran es liegt |
|
| Beobachtung | Woran es liegt |
|
||||||
|---|---|
|
|---|---|
|
||||||
| „Zu viele Fehlversuche" bei der Anmeldung | Nach fünf falschen Passwörtern wird die Adresse für eine Minute gesperrt, bei weiteren Fehlversuchen länger, höchstens 15 Minuten. Abwarten – oder Passwort vom Ausbilder neu setzen lassen. Hat sich das einzige Ausbilderkonto ausgesperrt: `docker compose restart lernbar` setzt die Sperre zurück. |
|
| „Zu viele Fehlversuche" bei der Anmeldung | Nach fünf falschen Passwörtern wird die Adresse für eine Minute gesperrt, bei weiteren Fehlversuchen länger, höchstens 15 Minuten. Abwarten – oder Passwort vom Ausbilder neu setzen lassen. Hat sich das einzige Ausbilderkonto ausgesperrt: `docker compose restart lernbar` setzt die Sperre zurück. |
|
||||||
|
| Ein Bereich fehlt in der Navigation, oder seine Seite meldet „Diese Seite gibt es nicht" | Das Modul ist abgeschaltet. Ein Ausbilder schaltet es unter **Verwaltung → Module** wieder ein; die Daten sind noch da. |
|
||||||
| „Diese Seite gibt es nicht" bei fremden Daten | Kein Fehler, sondern die Rechteprüfung. Ob es zu einer Kennung überhaupt Daten gibt, geht Unbefugte nichts an – deshalb 404 und nicht „kein Zugriff". |
|
| „Diese Seite gibt es nicht" bei fremden Daten | Kein Fehler, sondern die Rechteprüfung. Ob es zu einer Kennung überhaupt Daten gibt, geht Unbefugte nichts an – deshalb 404 und nicht „kein Zugriff". |
|
||||||
| Die Anwendung startet nicht, nennt aber Datei und Zeile | Ein Inhaltsfehler unter `content/`. Die Meldung sagt genau, wo. `npm run inhalte:pruefen` prüft ohne Start. |
|
| Die Anwendung startet nicht, nennt aber Datei und Zeile | Ein Inhaltsfehler unter `content/`. Die Meldung sagt genau, wo. `npm run inhalte:pruefen` prüft ohne Start. |
|
||||||
| Ein Anhang lässt sich nicht hochladen | Über 25 MB. Bei einem Reverse Proxy zusätzlich `client_max_body_size 30m` setzen (siehe README). |
|
| Ein Anhang lässt sich nicht hochladen | Über 25 MB. Bei einem Reverse Proxy zusätzlich `client_max_body_size 30m` setzen (siehe README). |
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 218 KiB |
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "lernbar",
|
"name": "lernbar",
|
||||||
"version": "1.0.5",
|
"version": "1.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "lernbar",
|
"name": "lernbar",
|
||||||
"version": "1.0.5",
|
"version": "1.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@node-rs/argon2": "^2.0.2",
|
"@node-rs/argon2": "^2.0.2",
|
||||||
"better-sqlite3": "^12.11.1",
|
"better-sqlite3": "^12.11.1",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "lernbar",
|
"name": "lernbar",
|
||||||
"version": "1.0.5",
|
"version": "1.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Lern-Bar – betriebliche Ausbildung Fachinformatiker Systemintegration",
|
"description": "Lern-Bar – betriebliche Ausbildung Fachinformatiker Systemintegration",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -404,6 +404,7 @@ try {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const blatt = await z.$('.blattzeile-link');
|
const blatt = await z.$('.blattzeile-link');
|
||||||
|
let blattAdresse = null;
|
||||||
if (blatt) {
|
if (blatt) {
|
||||||
await blatt.click();
|
await blatt.click();
|
||||||
// Auf die Adresse warten, nicht auf eine Überschrift: Die steht auch auf
|
// Auf die Adresse warten, nicht auf eine Überschrift: Die steht auch auf
|
||||||
@@ -411,6 +412,7 @@ try {
|
|||||||
// die Anfrage landet auf der Sammelroute statt auf dem einzelnen Blatt.
|
// die Anfrage landet auf der Sammelroute statt auf dem einzelnen Blatt.
|
||||||
await z.waitForURL(/\/berichtsheft\/[0-9a-f-]{36}$/, { timeout: 30000 });
|
await z.waitForURL(/\/berichtsheft\/[0-9a-f-]{36}$/, { timeout: 30000 });
|
||||||
pruefe(true, 'Ein Wochenblatt lässt sich öffnen');
|
pruefe(true, 'Ein Wochenblatt lässt sich öffnen');
|
||||||
|
blattAdresse = z.url();
|
||||||
|
|
||||||
const pdf = await azubi.request.get(`${z.url()}/pdf`);
|
const pdf = await azubi.request.get(`${z.url()}/pdf`);
|
||||||
const puffer = await pdf.body();
|
const puffer = await pdf.body();
|
||||||
@@ -425,9 +427,59 @@ try {
|
|||||||
pruefe(false, 'Ein Wochenblatt lässt sich öffnen', 'keine Blattzeile gefunden');
|
pruefe(false, 'Ein Wochenblatt lässt sich öffnen', 'keine Blattzeile gefunden');
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- A8 Sicherung und Rückspielung ---------------------------------------
|
// --- A8 Module abschalten -------------------------------------------------
|
||||||
|
|
||||||
abschnitt('A8 · Sicherung zurückspielen');
|
abschnitt('A8 · Module abschalten');
|
||||||
|
|
||||||
|
// Über die Oberfläche, wie im Betrieb: Verwaltung → Module.
|
||||||
|
const umschalten = async (modul) => {
|
||||||
|
await a.goto(`${BASIS}/verwaltung/module`);
|
||||||
|
await a.click(`form:has(input[name="modul"][value="${modul}"]) button[type=submit]`);
|
||||||
|
await a.waitForLoadState('networkidle');
|
||||||
|
};
|
||||||
|
const zustandIn = async (modul) => {
|
||||||
|
await a.goto(`${BASIS}/verwaltung/module`);
|
||||||
|
// Die Statusmarke steht direkt neben der Überschrift des Moduls.
|
||||||
|
return (await a.locator(`#modul-${modul} + .marke-status`).innerText()).trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
await umschalten('berichtsheft');
|
||||||
|
await umschalten('karten');
|
||||||
|
pruefe((await zustandIn('berichtsheft')).includes('aus'), 'Der Ausbilder schaltet das Berichtsheft ab');
|
||||||
|
|
||||||
|
await z.goto(`${BASIS}/`);
|
||||||
|
pruefe((await z.locator('nav a[href="/berichtsheft"]').count()) === 0, 'Das Berichtsheft verschwindet aus der Navigation');
|
||||||
|
|
||||||
|
const seiteAus = await azubi.request.get(`${BASIS}/berichtsheft`);
|
||||||
|
const ausbilderSeiteAus = await ausbilder.request.get(`${BASIS}/berichtsheft`);
|
||||||
|
pruefe(
|
||||||
|
seiteAus.status() === 404 && ausbilderSeiteAus.status() === 404,
|
||||||
|
'Die Seite ist für beide Rollen gesperrt, nicht nur ausgeblendet',
|
||||||
|
`Azubi HTTP ${seiteAus.status()}, Ausbilder HTTP ${ausbilderSeiteAus.status()}`,
|
||||||
|
);
|
||||||
|
if (blattAdresse) {
|
||||||
|
const pdfAus = await azubi.request.get(`${blattAdresse}/pdf`);
|
||||||
|
pruefe(pdfAus.status() === 404, 'Auch das PDF ist gesperrt', `HTTP ${pdfAus.status()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await z.goto(`${BASIS}/handouts/N3`);
|
||||||
|
pruefe(
|
||||||
|
(await z.locator('text=Merkkarten zu diesem Handout').count()) === 0,
|
||||||
|
'Ohne Merkkarten zeigt das Handout keine Karten',
|
||||||
|
);
|
||||||
|
const lernstandAus = await azubi.request.post(`${BASIS}/api/lernstand`, { data: { eintraege: [] } });
|
||||||
|
pruefe(lernstandAus.status() === 410, 'Ein Gerät mit gesammelten Bewertungen erfährt, dass es sie verwerfen soll', `HTTP ${lernstandAus.status()}`);
|
||||||
|
|
||||||
|
await umschalten('berichtsheft');
|
||||||
|
await umschalten('karten');
|
||||||
|
if (blattAdresse) {
|
||||||
|
const wieder = await azubi.request.get(blattAdresse);
|
||||||
|
pruefe(wieder.status() === 200, 'Nach dem Einschalten ist das Wochenblatt unverändert wieder da', `HTTP ${wieder.status()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- A9 Sicherung und Rückspielung ---------------------------------------
|
||||||
|
|
||||||
|
abschnitt('A9 · Sicherung zurückspielen');
|
||||||
|
|
||||||
// Pfade für bash mit Schrägstrichen: Ein Backslash ist dort ein Fluchtzeichen.
|
// Pfade für bash mit Schrägstrichen: Ein Backslash ist dort ein Fluchtzeichen.
|
||||||
const alsPosix = (pfad) => pfad.split(path.sep).join('/');
|
const alsPosix = (pfad) => pfad.split(path.sep).join('/');
|
||||||
|
|||||||
@@ -74,6 +74,18 @@ async function main() {
|
|||||||
await seite.waitForSelector('#neu-name');
|
await seite.waitForSelector('#neu-name');
|
||||||
await bild(seite, '03-verwaltung');
|
await bild(seite, '03-verwaltung');
|
||||||
|
|
||||||
|
// Module: für das Bild ein Modul abschalten, danach den alten Stand herstellen.
|
||||||
|
const umschalten = async (modul: string) => {
|
||||||
|
await seite.goto(`${BASIS}/verwaltung/module`);
|
||||||
|
await seite.click(`form:has(input[name="modul"][value="${modul}"]) button[type=submit]`);
|
||||||
|
await seite.waitForLoadState('networkidle');
|
||||||
|
};
|
||||||
|
await umschalten('berichtsheft');
|
||||||
|
await seite.goto(`${BASIS}/verwaltung/module`);
|
||||||
|
await seite.waitForSelector('#modul-karten');
|
||||||
|
await bild(seite, '25-module');
|
||||||
|
await umschalten('berichtsheft');
|
||||||
|
|
||||||
await seite.goto(`${BASIS}/wochenplan`);
|
await seite.goto(`${BASIS}/wochenplan`);
|
||||||
await seite.waitForSelector('h1');
|
await seite.waitForSelector('h1');
|
||||||
await bild(seite, '04-wochenplan');
|
await bild(seite, '04-wochenplan');
|
||||||
|
|||||||
@@ -155,6 +155,7 @@ export function WochenAnsicht({
|
|||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{sicht.module.aufgaben ? (
|
||||||
<section className="karte" aria-labelledby="woche-aufgaben">
|
<section className="karte" aria-labelledby="woche-aufgaben">
|
||||||
<h2 id="woche-aufgaben" style={{ fontSize: '1rem' }}>
|
<h2 id="woche-aufgaben" style={{ fontSize: '1rem' }}>
|
||||||
Praxisaufgaben
|
Praxisaufgaben
|
||||||
@@ -173,7 +174,9 @@ export function WochenAnsicht({
|
|||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{sicht.module.checklisten ? (
|
||||||
<section className="karte" aria-labelledby="woche-checklisten">
|
<section className="karte" aria-labelledby="woche-checklisten">
|
||||||
<h2 id="woche-checklisten" style={{ fontSize: '1rem' }}>
|
<h2 id="woche-checklisten" style={{ fontSize: '1rem' }}>
|
||||||
Checklisten
|
Checklisten
|
||||||
@@ -188,7 +191,9 @@ export function WochenAnsicht({
|
|||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{sicht.module.kontrollen ? (
|
||||||
<section className="karte" aria-labelledby="woche-kontrolle">
|
<section className="karte" aria-labelledby="woche-kontrolle">
|
||||||
<h2 id="woche-kontrolle" style={{ fontSize: '1rem' }}>
|
<h2 id="woche-kontrolle" style={{ fontSize: '1rem' }}>
|
||||||
Lernzielkontrolle
|
Lernzielkontrolle
|
||||||
@@ -207,6 +212,7 @@ export function WochenAnsicht({
|
|||||||
<p className="leise" style={{ marginBottom: 0 }}>Diese Woche keine.</p>
|
<p className="leise" style={{ marginBottom: 0 }}>Diese Woche keine.</p>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav className="reihe nicht-drucken" style={{ justifyContent: 'space-between' }} aria-label="Andere Wochen">
|
<nav className="reihe nicht-drucken" style={{ justifyContent: 'space-between' }} aria-label="Andere Wochen">
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { alsZeitpunktText } from '@/lib/zeit';
|
|||||||
import { StatusMarke } from '../StatusMarke';
|
import { StatusMarke } from '../StatusMarke';
|
||||||
import { AnhangFormular, BewertungsFormular, NotizFormular, StatusWaehlen } from './Formulare';
|
import { AnhangFormular, BewertungsFormular, NotizFormular, StatusWaehlen } from './Formulare';
|
||||||
import { anhangEntfernen, aufgabeAbnehmen, aufgabeZurueckgeben } from '../aktionen';
|
import { anhangEntfernen, aufgabeAbnehmen, aufgabeZurueckgeben } from '../aktionen';
|
||||||
|
import { erfordereModul } from '@/lib/module/pruefen';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
@@ -29,6 +30,7 @@ export default async function AufgabenSeite({
|
|||||||
searchParams: Promise<{ azubi?: string }>;
|
searchParams: Promise<{ azubi?: string }>;
|
||||||
}) {
|
}) {
|
||||||
const benutzer = await erfordereBenutzer();
|
const benutzer = await erfordereBenutzer();
|
||||||
|
erfordereModul('aufgaben');
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
const { azubi: azubiParameter } = await searchParams;
|
const { azubi: azubiParameter } = await searchParams;
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
zurueckgeben,
|
zurueckgeben,
|
||||||
} from '@/lib/repos/aufgaben';
|
} from '@/lib/repos/aufgaben';
|
||||||
import { aufgabenStatus, type AufgabenStatus } from '@/lib/db/schema';
|
import { aufgabenStatus, type AufgabenStatus } from '@/lib/db/schema';
|
||||||
|
import { pruefeModul } from '@/lib/module/pruefen';
|
||||||
|
|
||||||
export type Rueckmeldung = { fehler?: string; erfolg?: string };
|
export type Rueckmeldung = { fehler?: string; erfolg?: string };
|
||||||
|
|
||||||
@@ -30,6 +31,7 @@ function aufgabeGibtEs(aufgabeId: string): boolean {
|
|||||||
|
|
||||||
export async function eigenenStatusSetzen(formular: FormData): Promise<void> {
|
export async function eigenenStatusSetzen(formular: FormData): Promise<void> {
|
||||||
const benutzer = await erfordereBenutzer();
|
const benutzer = await erfordereBenutzer();
|
||||||
|
pruefeModul('aufgaben');
|
||||||
if (benutzer.rolle !== 'azubi') return;
|
if (benutzer.rolle !== 'azubi') return;
|
||||||
|
|
||||||
const aufgabeId = String(formular.get('aufgabeId') ?? '');
|
const aufgabeId = String(formular.get('aufgabeId') ?? '');
|
||||||
@@ -46,6 +48,7 @@ export async function eigenenStatusSetzen(formular: FormData): Promise<void> {
|
|||||||
|
|
||||||
export async function eigeneNotizSpeichern(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
export async function eigeneNotizSpeichern(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
||||||
const benutzer = await erfordereBenutzer();
|
const benutzer = await erfordereBenutzer();
|
||||||
|
pruefeModul('aufgaben');
|
||||||
if (benutzer.rolle !== 'azubi') return { fehler: 'Nur der Azubi schreibt seine eigenen Notizen.' };
|
if (benutzer.rolle !== 'azubi') return { fehler: 'Nur der Azubi schreibt seine eigenen Notizen.' };
|
||||||
|
|
||||||
const aufgabeId = String(formular.get('aufgabeId') ?? '');
|
const aufgabeId = String(formular.get('aufgabeId') ?? '');
|
||||||
@@ -61,6 +64,7 @@ export async function eigeneNotizSpeichern(_vorher: Rueckmeldung, formular: Form
|
|||||||
|
|
||||||
export async function anhangHochladen(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
export async function anhangHochladen(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
||||||
const benutzer = await erfordereBenutzer();
|
const benutzer = await erfordereBenutzer();
|
||||||
|
pruefeModul('aufgaben');
|
||||||
if (benutzer.rolle !== 'azubi') return { fehler: 'Anhänge lädt der Azubi hoch.' };
|
if (benutzer.rolle !== 'azubi') return { fehler: 'Anhänge lädt der Azubi hoch.' };
|
||||||
|
|
||||||
const aufgabeId = String(formular.get('aufgabeId') ?? '');
|
const aufgabeId = String(formular.get('aufgabeId') ?? '');
|
||||||
@@ -107,6 +111,7 @@ export async function anhangHochladen(_vorher: Rueckmeldung, formular: FormData)
|
|||||||
|
|
||||||
export async function anhangEntfernen(formular: FormData): Promise<void> {
|
export async function anhangEntfernen(formular: FormData): Promise<void> {
|
||||||
const benutzer = await erfordereBenutzer();
|
const benutzer = await erfordereBenutzer();
|
||||||
|
pruefeModul('aufgaben');
|
||||||
const anhangId = String(formular.get('anhangId') ?? '');
|
const anhangId = String(formular.get('anhangId') ?? '');
|
||||||
const aufgabeId = String(formular.get('aufgabeId') ?? '');
|
const aufgabeId = String(formular.get('aufgabeId') ?? '');
|
||||||
|
|
||||||
@@ -126,6 +131,7 @@ export async function anhangEntfernen(formular: FormData): Promise<void> {
|
|||||||
|
|
||||||
export async function bewerten(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
export async function bewerten(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
||||||
const ausbilder = await erfordereBenutzer();
|
const ausbilder = await erfordereBenutzer();
|
||||||
|
pruefeModul('aufgaben');
|
||||||
const azubiId = String(formular.get('azubiId') ?? '');
|
const azubiId = String(formular.get('azubiId') ?? '');
|
||||||
const aufgabeId = String(formular.get('aufgabeId') ?? '');
|
const aufgabeId = String(formular.get('aufgabeId') ?? '');
|
||||||
|
|
||||||
@@ -157,6 +163,7 @@ export async function bewerten(_vorher: Rueckmeldung, formular: FormData): Promi
|
|||||||
|
|
||||||
export async function aufgabeAbnehmen(formular: FormData): Promise<void> {
|
export async function aufgabeAbnehmen(formular: FormData): Promise<void> {
|
||||||
const ausbilder = await erfordereBenutzer();
|
const ausbilder = await erfordereBenutzer();
|
||||||
|
pruefeModul('aufgaben');
|
||||||
const azubiId = String(formular.get('azubiId') ?? '');
|
const azubiId = String(formular.get('azubiId') ?? '');
|
||||||
const aufgabeId = String(formular.get('aufgabeId') ?? '');
|
const aufgabeId = String(formular.get('aufgabeId') ?? '');
|
||||||
|
|
||||||
@@ -169,6 +176,7 @@ export async function aufgabeAbnehmen(formular: FormData): Promise<void> {
|
|||||||
|
|
||||||
export async function aufgabeZurueckgeben(formular: FormData): Promise<void> {
|
export async function aufgabeZurueckgeben(formular: FormData): Promise<void> {
|
||||||
const ausbilder = await erfordereBenutzer();
|
const ausbilder = await erfordereBenutzer();
|
||||||
|
pruefeModul('aufgaben');
|
||||||
const azubiId = String(formular.get('azubiId') ?? '');
|
const azubiId = String(formular.get('azubiId') ?? '');
|
||||||
const aufgabeId = String(formular.get('aufgabeId') ?? '');
|
const aufgabeId = String(formular.get('aufgabeId') ?? '');
|
||||||
|
|
||||||
@@ -182,5 +190,6 @@ export async function aufgabeZurueckgeben(formular: FormData): Promise<void> {
|
|||||||
/** Wird von der Aufgabenseite gebraucht, um fremde Daten überhaupt lesen zu dürfen. */
|
/** Wird von der Aufgabenseite gebraucht, um fremde Daten überhaupt lesen zu dürfen. */
|
||||||
export async function pruefeLesezugriff(azubiId: string): Promise<void> {
|
export async function pruefeLesezugriff(azubiId: string): Promise<void> {
|
||||||
const benutzer = await erfordereBenutzer();
|
const benutzer = await erfordereBenutzer();
|
||||||
|
pruefeModul('aufgaben');
|
||||||
erfordereZugriffAufAzubi(benutzer, azubiId);
|
erfordereZugriffAufAzubi(benutzer, azubiId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ import { inhalte } from '@/lib/inhalte';
|
|||||||
import { staendeVon } from '@/lib/repos/aufgaben';
|
import { staendeVon } from '@/lib/repos/aufgaben';
|
||||||
import { alsZeitpunktText } from '@/lib/zeit';
|
import { alsZeitpunktText } from '@/lib/zeit';
|
||||||
import { StatusMarke } from './StatusMarke';
|
import { StatusMarke } from './StatusMarke';
|
||||||
|
import { erfordereModul } from '@/lib/module/pruefen';
|
||||||
|
|
||||||
export const metadata: Metadata = { title: 'Praxisaufgaben' };
|
export const metadata: Metadata = { title: 'Praxisaufgaben' };
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
export default async function AufgabenListe() {
|
export default async function AufgabenListe() {
|
||||||
const benutzer = await erfordereBenutzer();
|
const benutzer = await erfordereBenutzer();
|
||||||
|
erfordereModul('aufgaben');
|
||||||
const alle = inhalte();
|
const alle = inhalte();
|
||||||
|
|
||||||
if (benutzer.rolle === 'ausbilder') {
|
if (benutzer.rolle === 'ausbilder') {
|
||||||
|
|||||||
@@ -103,12 +103,15 @@ function AzubiAuswertung({ azubiId, name }: { azubiId: string; name: string }) {
|
|||||||
anteil={anteil(stand.handouts.erledigt, stand.handouts.gesamt)}
|
anteil={anteil(stand.handouts.erledigt, stand.handouts.gesamt)}
|
||||||
fuss={`${stand.handouts.gesamt - stand.handouts.erledigt} noch offen`}
|
fuss={`${stand.handouts.gesamt - stand.handouts.erledigt} noch offen`}
|
||||||
/>
|
/>
|
||||||
|
{stand.module.karten ? (
|
||||||
<Kachel
|
<Kachel
|
||||||
titel="Merkkarten, die sitzen"
|
titel="Merkkarten, die sitzen"
|
||||||
wert={`${stand.karten.erledigt} von ${stand.karten.gesamt}`}
|
wert={`${stand.karten.erledigt} von ${stand.karten.gesamt}`}
|
||||||
anteil={anteil(stand.karten.erledigt, stand.karten.gesamt)}
|
anteil={anteil(stand.karten.erledigt, stand.karten.gesamt)}
|
||||||
fuss={stand.karten.faellig > 0 ? `${stand.karten.faellig} heute fällig` : 'heute nichts fällig'}
|
fuss={stand.karten.faellig > 0 ? `${stand.karten.faellig} heute fällig` : 'heute nichts fällig'}
|
||||||
/>
|
/>
|
||||||
|
) : null}
|
||||||
|
{stand.module.aufgaben ? (
|
||||||
<Kachel
|
<Kachel
|
||||||
titel="Praxisaufgaben abgenommen"
|
titel="Praxisaufgaben abgenommen"
|
||||||
wert={`${stand.aufgaben.erledigt} von ${stand.aufgaben.gesamt}`}
|
wert={`${stand.aufgaben.erledigt} von ${stand.aufgaben.gesamt}`}
|
||||||
@@ -119,6 +122,8 @@ function AzubiAuswertung({ azubiId, name }: { azubiId: string; name: string }) {
|
|||||||
: `${stand.aufgaben.offen} noch offen`
|
: `${stand.aufgaben.offen} noch offen`
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
) : null}
|
||||||
|
{stand.module.berichtsheft ? (
|
||||||
<Kachel
|
<Kachel
|
||||||
titel="Berichtsheft abgezeichnet"
|
titel="Berichtsheft abgezeichnet"
|
||||||
wert={`${stand.berichtsheft.abgezeichnet} von ${stand.berichtsheft.gesamt}`}
|
wert={`${stand.berichtsheft.abgezeichnet} von ${stand.berichtsheft.gesamt}`}
|
||||||
@@ -132,9 +137,10 @@ function AzubiAuswertung({ azubiId, name }: { azubiId: string; name: string }) {
|
|||||||
}
|
}
|
||||||
ton={stand.berichtsheft.fehlendeWochen > 0 ? 'fehler' : undefined}
|
ton={stand.berichtsheft.fehlendeWochen > 0 ? 'fehler' : undefined}
|
||||||
/>
|
/>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{stand.berichtsheft.fehlendeWochen > 0 ? (
|
{stand.module.berichtsheft && stand.berichtsheft.fehlendeWochen > 0 ? (
|
||||||
<p className="hinweis hinweis-warnung">
|
<p className="hinweis hinweis-warnung">
|
||||||
<strong>
|
<strong>
|
||||||
{stand.berichtsheft.fehlendeWochen}{' '}
|
{stand.berichtsheft.fehlendeWochen}{' '}
|
||||||
@@ -145,7 +151,7 @@ function AzubiAuswertung({ azubiId, name }: { azubiId: string; name: string }) {
|
|||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<ErgebnisTabelle ergebnisse={stand.kontrollen.ergebnisse} />
|
{stand.module.kontrollen ? <ErgebnisTabelle ergebnisse={stand.kontrollen.ergebnisse} /> : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -180,9 +186,14 @@ function Kachel({
|
|||||||
// --- Rahmenplanabdeckung ---------------------------------------------------
|
// --- Rahmenplanabdeckung ---------------------------------------------------
|
||||||
|
|
||||||
function Rahmenplan({ azubiId }: { azubiId: string }) {
|
function Rahmenplan({ azubiId }: { azubiId: string }) {
|
||||||
const { positionen, gruppennamen } = rahmenplanabdeckung(azubiId);
|
const { positionen, gruppennamen, module: eingeschaltet } = rahmenplanabdeckung(azubiId);
|
||||||
const gruppen = nachGruppen(positionen);
|
const gruppen = nachGruppen(positionen);
|
||||||
const gesamt = bilanz(positionen);
|
const gesamt = bilanz(positionen);
|
||||||
|
// Die Regel steht in lib/auswertung/abdeckung – der Text nennt nur, was an ist.
|
||||||
|
const belastbar = [
|
||||||
|
eingeschaltet.aufgaben ? 'eine abgenommene Praxisaufgabe' : null,
|
||||||
|
eingeschaltet.checklisten ? 'einen abgeschlossenen Checklisten-Durchlauf' : null,
|
||||||
|
].filter((teil) => teil !== null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="stapel" aria-labelledby="rahmenplan">
|
<section className="stapel" aria-labelledby="rahmenplan">
|
||||||
@@ -192,8 +203,9 @@ function Rahmenplan({ azubiId }: { azubiId: string }) {
|
|||||||
</h2>
|
</h2>
|
||||||
<p className="leise" style={{ margin: 0 }}>
|
<p className="leise" style={{ margin: 0 }}>
|
||||||
Die Ansicht, die im Zweifel gegenüber der IHK zählt. Als <strong>vermittelt</strong> gilt eine
|
Die Ansicht, die im Zweifel gegenüber der IHK zählt. Als <strong>vermittelt</strong> gilt eine
|
||||||
Position, wenn es dazu eine abgenommene Praxisaufgabe oder einen abgeschlossenen
|
Position, wenn es dazu{' '}
|
||||||
Checklisten-Durchlauf gibt – oder mindestens zwei gelesene Handouts.
|
{belastbar.length > 0 ? `${belastbar.join(' oder ')} gibt – oder ` : ''}mindestens zwei gelesene
|
||||||
|
Handouts{belastbar.length > 0 ? '' : ' gibt'}.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -339,6 +351,7 @@ function Lernfelder({ azubiId }: { azubiId: string }) {
|
|||||||
function Kontrollverlauf({ azubiId }: { azubiId: string }) {
|
function Kontrollverlauf({ azubiId }: { azubiId: string }) {
|
||||||
const ausbildung = ausbildungNachAzubi(azubiId);
|
const ausbildung = ausbildungNachAzubi(azubiId);
|
||||||
const stand = eigenerStand(azubiId, ausbildung?.beginn ?? null);
|
const stand = eigenerStand(azubiId, ausbildung?.beginn ?? null);
|
||||||
|
if (!stand.module.kontrollen) return null;
|
||||||
return <ErgebnisTabelle ergebnisse={stand.kontrollen.ergebnisse} />;
|
return <ErgebnisTabelle ergebnisse={stand.kontrollen.ergebnisse} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ import { Formulierungshilfe } from '../Formulierungshilfe';
|
|||||||
import { BerichtsMarke } from '../BerichtsMarke';
|
import { BerichtsMarke } from '../BerichtsMarke';
|
||||||
import { Abzeichnen } from './Abzeichnen';
|
import { Abzeichnen } from './Abzeichnen';
|
||||||
import { blattZurueckziehen, blattZumLesen } from '../aktionen';
|
import { blattZurueckziehen, blattZumLesen } from '../aktionen';
|
||||||
|
import { erfordereModul } from '@/lib/module/pruefen';
|
||||||
|
import { erfordereBenutzer } from '@/lib/auth/rechte';
|
||||||
|
import { modulStand } from '@/lib/repos/module';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
@@ -27,6 +30,8 @@ export async function generateMetadata({ params }: { params: Promise<{ berichtId
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default async function BlattSeite({ params }: { params: Promise<{ berichtId: string }> }) {
|
export default async function BlattSeite({ params }: { params: Promise<{ berichtId: string }> }) {
|
||||||
|
await erfordereBenutzer();
|
||||||
|
erfordereModul('berichtsheft');
|
||||||
const { berichtId } = await params;
|
const { berichtId } = await params;
|
||||||
|
|
||||||
// Rechteprüfung steckt hier drin: kein Zugriff bedeutet null bedeutet 404.
|
// Rechteprüfung steckt hier drin: kein Zugriff bedeutet null bedeutet 404.
|
||||||
@@ -238,6 +243,8 @@ function AnsichtFuerAusbilder({
|
|||||||
/** Stellt zusammen, was die Anwendung über diese Woche weiß. */
|
/** Stellt zusammen, was die Anwendung über diese Woche weiß. */
|
||||||
function sammleVorschlaege(azubiId: string, von: string, bis: string) {
|
function sammleVorschlaege(azubiId: string, von: string, bis: string) {
|
||||||
const alle = inhalte();
|
const alle = inhalte();
|
||||||
|
// Aus abgeschalteten Modulen kommen keine Vorschläge.
|
||||||
|
const eingeschaltet = modulStand();
|
||||||
|
|
||||||
const notizen = notizenIm(azubiId, von, bis).map((n) => ({
|
const notizen = notizenIm(azubiId, von, bis).map((n) => ({
|
||||||
datum: n.datum,
|
datum: n.datum,
|
||||||
@@ -247,7 +254,7 @@ function sammleVorschlaege(azubiId: string, von: string, bis: string) {
|
|||||||
|
|
||||||
// Praxisaufgaben, die in dieser Woche abgegeben oder abgenommen wurden.
|
// Praxisaufgaben, die in dieser Woche abgegeben oder abgenommen wurden.
|
||||||
const staende = aufgabenStaende(azubiId);
|
const staende = aufgabenStaende(azubiId);
|
||||||
const aufgaben = [...staende.values()]
|
const aufgaben = [...(eingeschaltet.aufgaben ? staende.values() : [])]
|
||||||
.filter((s) => {
|
.filter((s) => {
|
||||||
const zeitpunkt = (s.abgenommenAm ?? s.abgegebenAm ?? '').slice(0, 10);
|
const zeitpunkt = (s.abgenommenAm ?? s.abgegebenAm ?? '').slice(0, 10);
|
||||||
return zeitpunkt >= von && zeitpunkt <= bis;
|
return zeitpunkt >= von && zeitpunkt <= bis;
|
||||||
@@ -262,7 +269,7 @@ function sammleVorschlaege(azubiId: string, von: string, bis: string) {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const checklisten = laeufeVon(azubiId)
|
const checklisten = (eingeschaltet.checklisten ? laeufeVon(azubiId) : [])
|
||||||
.filter((l) => l.abgeschlossenAm !== null && l.datum >= von && l.datum <= bis)
|
.filter((l) => l.abgeschlossenAm !== null && l.datum >= von && l.datum <= bis)
|
||||||
.map((l) => ({ id: l.id, titel: l.titel, ticket: l.ticket, kunde: l.kunde }));
|
.map((l) => ({ id: l.id, titel: l.titel, ticket: l.ticket, kunde: l.kunde }));
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,15 @@ import { berichtsheftPdf } from '@/lib/pdf/berichtsheft';
|
|||||||
import { alsDatumText, alsZeitpunktText } from '@/lib/zeit';
|
import { alsDatumText, alsZeitpunktText } from '@/lib/zeit';
|
||||||
import { ausbildungsjahr } from '@/lib/berichtsheft/jahr';
|
import { ausbildungsjahr } from '@/lib/berichtsheft/jahr';
|
||||||
import { blattZumLesen } from '../../aktionen';
|
import { blattZumLesen } from '../../aktionen';
|
||||||
|
import { erfordereBenutzer } from '@/lib/auth/rechte';
|
||||||
|
import { modulAn, modulGesperrt } from '@/lib/module/pruefen';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
/** Ein einzelnes Wochenblatt als PDF. Rechteprüfung über `blattZumLesen`. */
|
/** Ein einzelnes Wochenblatt als PDF. Rechteprüfung über `blattZumLesen`. */
|
||||||
export async function GET(_anfrage: Request, kontext: { params: Promise<{ berichtId: string }> }) {
|
export async function GET(_anfrage: Request, kontext: { params: Promise<{ berichtId: string }> }) {
|
||||||
|
await erfordereBenutzer();
|
||||||
|
if (!modulAn('berichtsheft')) return modulGesperrt();
|
||||||
const { berichtId } = await kontext.params;
|
const { berichtId } = await kontext.params;
|
||||||
|
|
||||||
const gelesen = await blattZumLesen(berichtId);
|
const gelesen = await blattZumLesen(berichtId);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
zurueckgeben,
|
zurueckgeben,
|
||||||
} from '@/lib/repos/berichtsheft';
|
} from '@/lib/repos/berichtsheft';
|
||||||
import type { Wochenblatt } from '@/lib/berichtsheft/pruefung';
|
import type { Wochenblatt } from '@/lib/berichtsheft/pruefung';
|
||||||
|
import { pruefeModul } from '@/lib/module/pruefen';
|
||||||
|
|
||||||
export type Rueckmeldung = { fehler?: string; erfolg?: string };
|
export type Rueckmeldung = { fehler?: string; erfolg?: string };
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ async function blattZumBearbeiten(berichtId: string) {
|
|||||||
/** Zum Lesen: das eigene Blatt oder das eines betreuten Azubis. */
|
/** Zum Lesen: das eigene Blatt oder das eines betreuten Azubis. */
|
||||||
export async function blattZumLesen(berichtId: string) {
|
export async function blattZumLesen(berichtId: string) {
|
||||||
const benutzer = await erfordereBenutzer();
|
const benutzer = await erfordereBenutzer();
|
||||||
|
pruefeModul('berichtsheft');
|
||||||
const bericht = berichtNachId(berichtId);
|
const bericht = berichtNachId(berichtId);
|
||||||
if (!bericht) return null;
|
if (!bericht) return null;
|
||||||
|
|
||||||
@@ -50,6 +52,7 @@ function aktualisiere(berichtId?: string) {
|
|||||||
|
|
||||||
export async function tagesnotizSpeichern(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
export async function tagesnotizSpeichern(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
||||||
const benutzer = await erfordereBenutzer();
|
const benutzer = await erfordereBenutzer();
|
||||||
|
pruefeModul('berichtsheft');
|
||||||
if (benutzer.rolle !== 'azubi') return { fehler: 'Tagesnotizen schreibt der Azubi.' };
|
if (benutzer.rolle !== 'azubi') return { fehler: 'Tagesnotizen schreibt der Azubi.' };
|
||||||
|
|
||||||
const datum = String(formular.get('datum') ?? '').trim();
|
const datum = String(formular.get('datum') ?? '').trim();
|
||||||
@@ -70,6 +73,7 @@ export async function tagesnotizSpeichern(_vorher: Rueckmeldung, formular: FormD
|
|||||||
|
|
||||||
export async function blattAnlegen(formular: FormData): Promise<void> {
|
export async function blattAnlegen(formular: FormData): Promise<void> {
|
||||||
const benutzer = await erfordereBenutzer();
|
const benutzer = await erfordereBenutzer();
|
||||||
|
pruefeModul('berichtsheft');
|
||||||
if (benutzer.rolle !== 'azubi') return;
|
if (benutzer.rolle !== 'azubi') return;
|
||||||
|
|
||||||
const datum = String(formular.get('datum') ?? '').trim();
|
const datum = String(formular.get('datum') ?? '').trim();
|
||||||
@@ -104,6 +108,7 @@ function leseBlatt(formular: FormData): Wochenblatt {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function blattSpeichern(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
export async function blattSpeichern(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
||||||
|
pruefeModul('berichtsheft');
|
||||||
const berichtId = String(formular.get('berichtId') ?? '');
|
const berichtId = String(formular.get('berichtId') ?? '');
|
||||||
const { bericht } = await blattZumBearbeiten(berichtId);
|
const { bericht } = await blattZumBearbeiten(berichtId);
|
||||||
|
|
||||||
@@ -128,6 +133,7 @@ export async function blattSpeichern(_vorher: Rueckmeldung, formular: FormData):
|
|||||||
* auf der Seite gesehen; ob er sie beherzigt, entscheidet er.
|
* auf der Seite gesehen; ob er sie beherzigt, entscheidet er.
|
||||||
*/
|
*/
|
||||||
export async function blattEinreichen(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
export async function blattEinreichen(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
||||||
|
pruefeModul('berichtsheft');
|
||||||
const berichtId = String(formular.get('berichtId') ?? '');
|
const berichtId = String(formular.get('berichtId') ?? '');
|
||||||
const { bericht } = await blattZumBearbeiten(berichtId);
|
const { bericht } = await blattZumBearbeiten(berichtId);
|
||||||
|
|
||||||
@@ -143,6 +149,7 @@ export async function blattEinreichen(_vorher: Rueckmeldung, formular: FormData)
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function blattZurueckziehen(formular: FormData): Promise<void> {
|
export async function blattZurueckziehen(formular: FormData): Promise<void> {
|
||||||
|
pruefeModul('berichtsheft');
|
||||||
const berichtId = String(formular.get('berichtId') ?? '');
|
const berichtId = String(formular.get('berichtId') ?? '');
|
||||||
const { bericht } = await blattZumBearbeiten(berichtId);
|
const { bericht } = await blattZumBearbeiten(berichtId);
|
||||||
if (bericht.status !== 'eingereicht') return;
|
if (bericht.status !== 'eingereicht') return;
|
||||||
@@ -155,6 +162,7 @@ export async function blattZurueckziehen(formular: FormData): Promise<void> {
|
|||||||
|
|
||||||
export async function blattAbzeichnen(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
export async function blattAbzeichnen(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
||||||
const ausbilder = await erfordereBenutzer();
|
const ausbilder = await erfordereBenutzer();
|
||||||
|
pruefeModul('berichtsheft');
|
||||||
const berichtId = String(formular.get('berichtId') ?? '');
|
const berichtId = String(formular.get('berichtId') ?? '');
|
||||||
|
|
||||||
const bericht = berichtNachId(berichtId);
|
const bericht = berichtNachId(berichtId);
|
||||||
@@ -173,6 +181,7 @@ export async function blattAbzeichnen(_vorher: Rueckmeldung, formular: FormData)
|
|||||||
|
|
||||||
export async function blattZurueckgeben(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
export async function blattZurueckgeben(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
||||||
const ausbilder = await erfordereBenutzer();
|
const ausbilder = await erfordereBenutzer();
|
||||||
|
pruefeModul('berichtsheft');
|
||||||
const berichtId = String(formular.get('berichtId') ?? '');
|
const berichtId = String(formular.get('berichtId') ?? '');
|
||||||
|
|
||||||
const bericht = berichtNachId(berichtId);
|
const bericht = berichtNachId(berichtId);
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ import { Tagesnotizen, type Tagesfeld } from './Tagesnotizen';
|
|||||||
import { Formulierungshilfe } from './Formulierungshilfe';
|
import { Formulierungshilfe } from './Formulierungshilfe';
|
||||||
import { blattAnlegen } from './aktionen';
|
import { blattAnlegen } from './aktionen';
|
||||||
import { BerichtsMarke } from './BerichtsMarke';
|
import { BerichtsMarke } from './BerichtsMarke';
|
||||||
|
import { erfordereModul } from '@/lib/module/pruefen';
|
||||||
|
|
||||||
export const metadata: Metadata = { title: 'Berichtsheft' };
|
export const metadata: Metadata = { title: 'Berichtsheft' };
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
export default async function BerichtsheftSeite() {
|
export default async function BerichtsheftSeite() {
|
||||||
const benutzer = await erfordereBenutzer();
|
const benutzer = await erfordereBenutzer();
|
||||||
|
erfordereModul('berichtsheft');
|
||||||
|
|
||||||
return benutzer.rolle === 'ausbilder' ? (
|
return benutzer.rolle === 'ausbilder' ? (
|
||||||
<AusbilderSicht ausbilderId={benutzer.id} />
|
<AusbilderSicht ausbilderId={benutzer.id} />
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { alsWochenblatt, berichteVon } from '@/lib/repos/berichtsheft';
|
|||||||
import { berichtsheftPdf, type Blattangaben } from '@/lib/pdf/berichtsheft';
|
import { berichtsheftPdf, type Blattangaben } from '@/lib/pdf/berichtsheft';
|
||||||
import { ausbildungsjahr } from '@/lib/berichtsheft/jahr';
|
import { ausbildungsjahr } from '@/lib/berichtsheft/jahr';
|
||||||
import { alsDatumText, alsZeitpunktText } from '@/lib/zeit';
|
import { alsDatumText, alsZeitpunktText } from '@/lib/zeit';
|
||||||
|
import { modulAn, modulGesperrt } from '@/lib/module/pruefen';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ const datumMuster = /^\d{4}-\d{2}-\d{2}$/;
|
|||||||
*/
|
*/
|
||||||
export async function GET(anfrage: Request) {
|
export async function GET(anfrage: Request) {
|
||||||
const benutzer = await erfordereBenutzer();
|
const benutzer = await erfordereBenutzer();
|
||||||
|
if (!modulAn('berichtsheft')) return modulGesperrt();
|
||||||
const adresse = new URL(anfrage.url);
|
const adresse = new URL(anfrage.url);
|
||||||
|
|
||||||
const azubiId = benutzer.rolle === 'azubi' ? benutzer.id : (adresse.searchParams.get('azubi') ?? '');
|
const azubiId = benutzer.rolle === 'azubi' ? benutzer.id : (adresse.searchParams.get('azubi') ?? '');
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
standVon,
|
standVon,
|
||||||
} from '@/lib/repos/checklisten';
|
} from '@/lib/repos/checklisten';
|
||||||
import { darfAbschliessen } from '@/lib/checklisten/fortschritt';
|
import { darfAbschliessen } from '@/lib/checklisten/fortschritt';
|
||||||
|
import { pruefeModul } from '@/lib/module/pruefen';
|
||||||
|
|
||||||
export type Rueckmeldung = { fehler?: string; erfolg?: string };
|
export type Rueckmeldung = { fehler?: string; erfolg?: string };
|
||||||
|
|
||||||
@@ -42,6 +43,7 @@ async function laufZumBearbeiten(laufId: string) {
|
|||||||
/** Zum Lesen: der eigene Durchlauf oder der eines betreuten Azubis. */
|
/** Zum Lesen: der eigene Durchlauf oder der eines betreuten Azubis. */
|
||||||
export async function laufZumLesen(laufId: string) {
|
export async function laufZumLesen(laufId: string) {
|
||||||
const benutzer = await erfordereBenutzer();
|
const benutzer = await erfordereBenutzer();
|
||||||
|
pruefeModul('checklisten');
|
||||||
const lauf = laufNachId(laufId);
|
const lauf = laufNachId(laufId);
|
||||||
if (!lauf) return null;
|
if (!lauf) return null;
|
||||||
|
|
||||||
@@ -59,6 +61,7 @@ function aktualisiere(laufId: string) {
|
|||||||
|
|
||||||
export async function durchlaufStarten(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
export async function durchlaufStarten(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
||||||
const benutzer = await erfordereBenutzer();
|
const benutzer = await erfordereBenutzer();
|
||||||
|
pruefeModul('checklisten');
|
||||||
if (benutzer.rolle !== 'azubi') {
|
if (benutzer.rolle !== 'azubi') {
|
||||||
return { fehler: 'Durchläufe führt der Azubi – der Ausbilder liest mit.' };
|
return { fehler: 'Durchläufe führt der Azubi – der Ausbilder liest mit.' };
|
||||||
}
|
}
|
||||||
@@ -82,6 +85,7 @@ export async function durchlaufStarten(_vorher: Rueckmeldung, formular: FormData
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function schrittUmschalten(formular: FormData): Promise<void> {
|
export async function schrittUmschalten(formular: FormData): Promise<void> {
|
||||||
|
pruefeModul('checklisten');
|
||||||
const laufId = String(formular.get('laufId') ?? '');
|
const laufId = String(formular.get('laufId') ?? '');
|
||||||
const schrittId = String(formular.get('schrittId') ?? '');
|
const schrittId = String(formular.get('schrittId') ?? '');
|
||||||
const erledigt = String(formular.get('erledigt') ?? '') === '1';
|
const erledigt = String(formular.get('erledigt') ?? '') === '1';
|
||||||
@@ -94,6 +98,7 @@ export async function schrittUmschalten(formular: FormData): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function schrittBemerkung(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
export async function schrittBemerkung(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
||||||
|
pruefeModul('checklisten');
|
||||||
const laufId = String(formular.get('laufId') ?? '');
|
const laufId = String(formular.get('laufId') ?? '');
|
||||||
const schrittId = String(formular.get('schrittId') ?? '');
|
const schrittId = String(formular.get('schrittId') ?? '');
|
||||||
const bemerkung = String(formular.get('bemerkung') ?? '');
|
const bemerkung = String(formular.get('bemerkung') ?? '');
|
||||||
@@ -108,6 +113,7 @@ export async function schrittBemerkung(_vorher: Rueckmeldung, formular: FormData
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function kopfdatenSpeichern(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
export async function kopfdatenSpeichern(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
||||||
|
pruefeModul('checklisten');
|
||||||
const laufId = String(formular.get('laufId') ?? '');
|
const laufId = String(formular.get('laufId') ?? '');
|
||||||
const { lauf } = await laufZumBearbeiten(laufId);
|
const { lauf } = await laufZumBearbeiten(laufId);
|
||||||
if (lauf.abgeschlossenAm) return { fehler: 'Der Durchlauf ist abgeschlossen.' };
|
if (lauf.abgeschlossenAm) return { fehler: 'Der Durchlauf ist abgeschlossen.' };
|
||||||
@@ -130,6 +136,7 @@ export async function kopfdatenSpeichern(_vorher: Rueckmeldung, formular: FormDa
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function durchlaufAbschliessen(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
export async function durchlaufAbschliessen(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
||||||
|
pruefeModul('checklisten');
|
||||||
const laufId = String(formular.get('laufId') ?? '');
|
const laufId = String(formular.get('laufId') ?? '');
|
||||||
const { lauf } = await laufZumBearbeiten(laufId);
|
const { lauf } = await laufZumBearbeiten(laufId);
|
||||||
if (lauf.abgeschlossenAm) return { fehler: 'Der Durchlauf ist schon abgeschlossen.' };
|
if (lauf.abgeschlossenAm) return { fehler: 'Der Durchlauf ist schon abgeschlossen.' };
|
||||||
@@ -148,6 +155,7 @@ export async function durchlaufAbschliessen(_vorher: Rueckmeldung, formular: For
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function durchlaufWiederOeffnen(formular: FormData): Promise<void> {
|
export async function durchlaufWiederOeffnen(formular: FormData): Promise<void> {
|
||||||
|
pruefeModul('checklisten');
|
||||||
const laufId = String(formular.get('laufId') ?? '');
|
const laufId = String(formular.get('laufId') ?? '');
|
||||||
await laufZumBearbeiten(laufId);
|
await laufZumBearbeiten(laufId);
|
||||||
laufWiederOeffnen(laufId);
|
laufWiederOeffnen(laufId);
|
||||||
@@ -155,6 +163,7 @@ export async function durchlaufWiederOeffnen(formular: FormData): Promise<void>
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function durchlaufVerwerfen(formular: FormData): Promise<void> {
|
export async function durchlaufVerwerfen(formular: FormData): Promise<void> {
|
||||||
|
pruefeModul('checklisten');
|
||||||
const laufId = String(formular.get('laufId') ?? '');
|
const laufId = String(formular.get('laufId') ?? '');
|
||||||
const { lauf } = await laufZumBearbeiten(laufId);
|
const { lauf } = await laufZumBearbeiten(laufId);
|
||||||
// Ein abgeschlossener Durchlauf ist ein Nachweis und wird nicht gelöscht.
|
// Ein abgeschlossener Durchlauf ist ein Nachweis und wird nicht gelöscht.
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { standVon } from '@/lib/repos/checklisten';
|
|||||||
import { alsDatumText, alsZeitpunktText } from '@/lib/zeit';
|
import { alsDatumText, alsZeitpunktText } from '@/lib/zeit';
|
||||||
import { AbschlussFormular, BemerkungFormular, KopfdatenFormular } from '../../Formulare';
|
import { AbschlussFormular, BemerkungFormular, KopfdatenFormular } from '../../Formulare';
|
||||||
import { durchlaufVerwerfen, durchlaufWiederOeffnen, laufZumLesen, schrittUmschalten } from '../../aktionen';
|
import { durchlaufVerwerfen, durchlaufWiederOeffnen, laufZumLesen, schrittUmschalten } from '../../aktionen';
|
||||||
|
import { erfordereModul } from '@/lib/module/pruefen';
|
||||||
|
import { erfordereBenutzer } from '@/lib/auth/rechte';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
@@ -17,6 +19,8 @@ export async function generateMetadata({ params }: { params: Promise<{ laufId: s
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default async function LaufSeite({ params }: { params: Promise<{ laufId: string }> }) {
|
export default async function LaufSeite({ params }: { params: Promise<{ laufId: string }> }) {
|
||||||
|
await erfordereBenutzer();
|
||||||
|
erfordereModul('checklisten');
|
||||||
const { laufId } = await params;
|
const { laufId } = await params;
|
||||||
|
|
||||||
// Enthält die Rechteprüfung: Wer nicht darf, bekommt null – und damit 404.
|
// Enthält die Rechteprüfung: Wer nicht darf, bekommt null – und damit 404.
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { standVon } from '@/lib/repos/checklisten';
|
|||||||
import { checklistenPdf } from '@/lib/pdf/checkliste';
|
import { checklistenPdf } from '@/lib/pdf/checkliste';
|
||||||
import { alsDatumText, alsZeitpunktText } from '@/lib/zeit';
|
import { alsDatumText, alsZeitpunktText } from '@/lib/zeit';
|
||||||
import { laufZumLesen } from '../../../aktionen';
|
import { laufZumLesen } from '../../../aktionen';
|
||||||
|
import { erfordereBenutzer } from '@/lib/auth/rechte';
|
||||||
|
import { modulAn, modulGesperrt } from '@/lib/module/pruefen';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
@@ -15,6 +17,8 @@ export const dynamic = 'force-dynamic';
|
|||||||
* gibt, geht einen Unbefugten nichts an.
|
* gibt, geht einen Unbefugten nichts an.
|
||||||
*/
|
*/
|
||||||
export async function GET(_anfrage: Request, kontext: { params: Promise<{ laufId: string }> }) {
|
export async function GET(_anfrage: Request, kontext: { params: Promise<{ laufId: string }> }) {
|
||||||
|
await erfordereBenutzer();
|
||||||
|
if (!modulAn('checklisten')) return modulGesperrt();
|
||||||
const { laufId } = await kontext.params;
|
const { laufId } = await kontext.params;
|
||||||
|
|
||||||
const gelesen = await laufZumLesen(laufId);
|
const gelesen = await laufZumLesen(laufId);
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ import { inhalte } from '@/lib/inhalte';
|
|||||||
import { laeufeVon, standVon } from '@/lib/repos/checklisten';
|
import { laeufeVon, standVon } from '@/lib/repos/checklisten';
|
||||||
import { alsDatumText, alsZeitpunktText, heute } from '@/lib/zeit';
|
import { alsDatumText, alsZeitpunktText, heute } from '@/lib/zeit';
|
||||||
import { StartFormular } from './Formulare';
|
import { StartFormular } from './Formulare';
|
||||||
|
import { erfordereModul } from '@/lib/module/pruefen';
|
||||||
|
|
||||||
export const metadata: Metadata = { title: 'Checklisten' };
|
export const metadata: Metadata = { title: 'Checklisten' };
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
export default async function ChecklistenSeite() {
|
export default async function ChecklistenSeite() {
|
||||||
const benutzer = await erfordereBenutzer();
|
const benutzer = await erfordereBenutzer();
|
||||||
|
erfordereModul('checklisten');
|
||||||
const alle = inhalte();
|
const alle = inhalte();
|
||||||
const istAzubi = benutzer.rolle === 'azubi';
|
const istAzubi = benutzer.rolle === 'azubi';
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { istAbgeschlossen, zusammenfassung } from '@/lib/feedback/inhalt';
|
|||||||
import { alsDatumText, alsZeitpunktText } from '@/lib/zeit';
|
import { alsDatumText, alsZeitpunktText } from '@/lib/zeit';
|
||||||
import { Gespraechsformular } from '../Gespraechsformular';
|
import { Gespraechsformular } from '../Gespraechsformular';
|
||||||
import { bestaetigungZurueck, gespraechVerwerfen, gespraechZumLesen } from '../aktionen';
|
import { bestaetigungZurueck, gespraechVerwerfen, gespraechZumLesen } from '../aktionen';
|
||||||
|
import { erfordereModul } from '@/lib/module/pruefen';
|
||||||
|
import { erfordereBenutzer } from '@/lib/auth/rechte';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
@@ -18,6 +20,8 @@ export async function generateMetadata({ params }: { params: Promise<{ gespraech
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default async function GespraechsSeite({ params }: { params: Promise<{ gespraechId: string }> }) {
|
export default async function GespraechsSeite({ params }: { params: Promise<{ gespraechId: string }> }) {
|
||||||
|
await erfordereBenutzer();
|
||||||
|
erfordereModul('feedback');
|
||||||
const { gespraechId } = await params;
|
const { gespraechId } = await params;
|
||||||
|
|
||||||
const gelesen = await gespraechZumLesen(gespraechId);
|
const gelesen = await gespraechZumLesen(gespraechId);
|
||||||
|
|||||||
@@ -4,11 +4,15 @@ import { inhaltVon } from '@/lib/repos/feedback';
|
|||||||
import { feedbackPdf } from '@/lib/pdf/feedback';
|
import { feedbackPdf } from '@/lib/pdf/feedback';
|
||||||
import { alsDatumText, alsZeitpunktText } from '@/lib/zeit';
|
import { alsDatumText, alsZeitpunktText } from '@/lib/zeit';
|
||||||
import { gespraechZumLesen } from '../../aktionen';
|
import { gespraechZumLesen } from '../../aktionen';
|
||||||
|
import { erfordereBenutzer } from '@/lib/auth/rechte';
|
||||||
|
import { modulAn, modulGesperrt } from '@/lib/module/pruefen';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
/** Das Feedbackgespräch als PDF. Rechteprüfung über `gespraechZumLesen`. */
|
/** Das Feedbackgespräch als PDF. Rechteprüfung über `gespraechZumLesen`. */
|
||||||
export async function GET(_anfrage: Request, kontext: { params: Promise<{ gespraechId: string }> }) {
|
export async function GET(_anfrage: Request, kontext: { params: Promise<{ gespraechId: string }> }) {
|
||||||
|
await erfordereBenutzer();
|
||||||
|
if (!modulAn('feedback')) return modulGesperrt();
|
||||||
const { gespraechId } = await kontext.params;
|
const { gespraechId } = await kontext.params;
|
||||||
|
|
||||||
const gelesen = await gespraechZumLesen(gespraechId);
|
const gelesen = await gespraechZumLesen(gespraechId);
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
loeschen,
|
loeschen,
|
||||||
} from '@/lib/repos/feedback';
|
} from '@/lib/repos/feedback';
|
||||||
import { istAbgeschlossen, type Gespraechsinhalt } from '@/lib/feedback/inhalt';
|
import { istAbgeschlossen, type Gespraechsinhalt } from '@/lib/feedback/inhalt';
|
||||||
|
import { pruefeModul } from '@/lib/module/pruefen';
|
||||||
|
|
||||||
export type Rueckmeldung = { fehler?: string; erfolg?: string };
|
export type Rueckmeldung = { fehler?: string; erfolg?: string };
|
||||||
|
|
||||||
@@ -30,6 +31,7 @@ const datumMuster = /^\d{4}-\d{2}-\d{2}$/;
|
|||||||
*/
|
*/
|
||||||
export async function gespraechZumLesen(id: string) {
|
export async function gespraechZumLesen(id: string) {
|
||||||
const benutzer = await erfordereBenutzer();
|
const benutzer = await erfordereBenutzer();
|
||||||
|
pruefeModul('feedback');
|
||||||
const gespraech = gespraechNachId(id);
|
const gespraech = gespraechNachId(id);
|
||||||
if (!gespraech) return null;
|
if (!gespraech) return null;
|
||||||
|
|
||||||
@@ -48,6 +50,7 @@ function aktualisiere(id?: string) {
|
|||||||
|
|
||||||
export async function gespraechAnlegen(formular: FormData): Promise<void> {
|
export async function gespraechAnlegen(formular: FormData): Promise<void> {
|
||||||
const ausbilder = await erfordereBenutzer();
|
const ausbilder = await erfordereBenutzer();
|
||||||
|
pruefeModul('feedback');
|
||||||
const azubiId = String(formular.get('azubiId') ?? '');
|
const azubiId = String(formular.get('azubiId') ?? '');
|
||||||
erfordereBewertungsrecht(ausbilder, azubiId);
|
erfordereBewertungsrecht(ausbilder, azubiId);
|
||||||
|
|
||||||
@@ -67,6 +70,7 @@ export async function gespraechAnlegen(formular: FormData): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function gespraechSpeichern(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
export async function gespraechSpeichern(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
||||||
|
pruefeModul('feedback');
|
||||||
const id = String(formular.get('gespraechId') ?? '');
|
const id = String(formular.get('gespraechId') ?? '');
|
||||||
const gelesen = await gespraechZumLesen(id);
|
const gelesen = await gespraechZumLesen(id);
|
||||||
if (!gelesen) return { fehler: 'Dieses Gespräch gibt es nicht.' };
|
if (!gelesen) return { fehler: 'Dieses Gespräch gibt es nicht.' };
|
||||||
@@ -125,6 +129,7 @@ export async function gespraechSpeichern(_vorher: Rueckmeldung, formular: FormDa
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function gespraechBestaetigen(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
export async function gespraechBestaetigen(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
||||||
|
pruefeModul('feedback');
|
||||||
const id = String(formular.get('gespraechId') ?? '');
|
const id = String(formular.get('gespraechId') ?? '');
|
||||||
const gelesen = await gespraechZumLesen(id);
|
const gelesen = await gespraechZumLesen(id);
|
||||||
if (!gelesen) return { fehler: 'Dieses Gespräch gibt es nicht.' };
|
if (!gelesen) return { fehler: 'Dieses Gespräch gibt es nicht.' };
|
||||||
@@ -148,6 +153,7 @@ export async function gespraechBestaetigen(_vorher: Rueckmeldung, formular: Form
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function bestaetigungZurueck(formular: FormData): Promise<void> {
|
export async function bestaetigungZurueck(formular: FormData): Promise<void> {
|
||||||
|
pruefeModul('feedback');
|
||||||
const id = String(formular.get('gespraechId') ?? '');
|
const id = String(formular.get('gespraechId') ?? '');
|
||||||
const gelesen = await gespraechZumLesen(id);
|
const gelesen = await gespraechZumLesen(id);
|
||||||
if (!gelesen) return;
|
if (!gelesen) return;
|
||||||
@@ -164,6 +170,7 @@ export async function bestaetigungZurueck(formular: FormData): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function gespraechVerwerfen(formular: FormData): Promise<void> {
|
export async function gespraechVerwerfen(formular: FormData): Promise<void> {
|
||||||
|
pruefeModul('feedback');
|
||||||
const id = String(formular.get('gespraechId') ?? '');
|
const id = String(formular.get('gespraechId') ?? '');
|
||||||
const gelesen = await gespraechZumLesen(id);
|
const gelesen = await gespraechZumLesen(id);
|
||||||
if (!gelesen) return;
|
if (!gelesen) return;
|
||||||
|
|||||||
@@ -7,12 +7,14 @@ import { gespraecheVon, inhaltVon } from '@/lib/repos/feedback';
|
|||||||
import { istAbgeschlossen, zusammenfassung } from '@/lib/feedback/inhalt';
|
import { istAbgeschlossen, zusammenfassung } from '@/lib/feedback/inhalt';
|
||||||
import { alsDatumText, heute, montagDerWoche } from '@/lib/zeit';
|
import { alsDatumText, heute, montagDerWoche } from '@/lib/zeit';
|
||||||
import { gespraechAnlegen } from './aktionen';
|
import { gespraechAnlegen } from './aktionen';
|
||||||
|
import { erfordereModul } from '@/lib/module/pruefen';
|
||||||
|
|
||||||
export const metadata: Metadata = { title: 'Feedbackgespräche' };
|
export const metadata: Metadata = { title: 'Feedbackgespräche' };
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
export default async function FeedbackSeite() {
|
export default async function FeedbackSeite() {
|
||||||
const benutzer = await erfordereBenutzer();
|
const benutzer = await erfordereBenutzer();
|
||||||
|
erfordereModul('feedback');
|
||||||
const vorlage = inhalte().feedback;
|
const vorlage = inhalte().feedback;
|
||||||
|
|
||||||
const beteiligte =
|
const beteiligte =
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { gelesenAm } from '@/lib/repos/fortschritt';
|
|||||||
import { alsZeitpunktText } from '@/lib/zeit';
|
import { alsZeitpunktText } from '@/lib/zeit';
|
||||||
import { gelesenUmschalten } from './aktionen';
|
import { gelesenUmschalten } from './aktionen';
|
||||||
import { Druckknopf } from '@/komponenten/Druckknopf';
|
import { Druckknopf } from '@/komponenten/Druckknopf';
|
||||||
|
import { modulStand } from '@/lib/repos/module';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
@@ -131,6 +132,7 @@ export default async function HandoutSeite({ params }: { params: Promise<{ id: s
|
|||||||
</details>
|
</details>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{modulStand().karten ? (
|
||||||
<section aria-labelledby="karten" className="karte stapel-eng">
|
<section aria-labelledby="karten" className="karte stapel-eng">
|
||||||
<h2 id="karten" style={{ marginBottom: '0.3rem' }}>
|
<h2 id="karten" style={{ marginBottom: '0.3rem' }}>
|
||||||
Merkkarten zu diesem Handout
|
Merkkarten zu diesem Handout
|
||||||
@@ -149,6 +151,7 @@ export default async function HandoutSeite({ params }: { params: Promise<{ id: s
|
|||||||
))}
|
))}
|
||||||
</ol>
|
</ol>
|
||||||
</section>
|
</section>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{istAzubi ? (
|
{istAzubi ? (
|
||||||
<form action={gelesenUmschalten} className="nicht-drucken">
|
<form action={gelesenUmschalten} className="nicht-drucken">
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { inhalte } from '@/lib/inhalte';
|
|||||||
import { bewerte } from '@/lib/repos/karten';
|
import { bewerte } from '@/lib/repos/karten';
|
||||||
import { bewertungen, fachNamen, type Bewertung } from '@/lib/lernen/sm2';
|
import { bewertungen, fachNamen, type Bewertung } from '@/lib/lernen/sm2';
|
||||||
import { alsDatumText, heute } from '@/lib/zeit';
|
import { alsDatumText, heute } from '@/lib/zeit';
|
||||||
|
import { pruefeModul } from '@/lib/module/pruefen';
|
||||||
|
|
||||||
export type Bewertungsergebnis = {
|
export type Bewertungsergebnis = {
|
||||||
fehler?: string;
|
fehler?: string;
|
||||||
@@ -25,6 +26,7 @@ export type Bewertungsergebnis = {
|
|||||||
*/
|
*/
|
||||||
export async function karteBewerten(karteId: string, bewertung: string): Promise<Bewertungsergebnis> {
|
export async function karteBewerten(karteId: string, bewertung: string): Promise<Bewertungsergebnis> {
|
||||||
const benutzer = await erfordereBenutzer();
|
const benutzer = await erfordereBenutzer();
|
||||||
|
pruefeModul('karten');
|
||||||
if (benutzer.rolle !== 'azubi') {
|
if (benutzer.rolle !== 'azubi') {
|
||||||
return { fehler: 'Nur der Azubi führt seinen eigenen Lernstand.' };
|
return { fehler: 'Nur der Azubi führt seinen eigenen Lernstand.' };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { alsKartenstand, staendeVon } from '@/lib/repos/karten';
|
|||||||
import { fach, fachNamen, heutigerStapel } from '@/lib/lernen/sm2';
|
import { fach, fachNamen, heutigerStapel } from '@/lib/lernen/sm2';
|
||||||
import { heute } from '@/lib/zeit';
|
import { heute } from '@/lib/zeit';
|
||||||
import { Lernmodus, type Lernkarte } from './Lernmodus';
|
import { Lernmodus, type Lernkarte } from './Lernmodus';
|
||||||
|
import { erfordereModul } from '@/lib/module/pruefen';
|
||||||
|
|
||||||
export const metadata: Metadata = { title: 'Lernmodus' };
|
export const metadata: Metadata = { title: 'Lernmodus' };
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
@@ -15,6 +16,7 @@ const STAPELGROESSE = 20;
|
|||||||
|
|
||||||
export default async function LernSeite({ searchParams }: { searchParams: Promise<{ block?: string }> }) {
|
export default async function LernSeite({ searchParams }: { searchParams: Promise<{ block?: string }> }) {
|
||||||
const benutzer = await erfordereBenutzer();
|
const benutzer = await erfordereBenutzer();
|
||||||
|
erfordereModul('karten');
|
||||||
if (benutzer.rolle !== 'azubi') redirect('/karten');
|
if (benutzer.rolle !== 'azubi') redirect('/karten');
|
||||||
|
|
||||||
const { block } = await searchParams;
|
const { block } = await searchParams;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { inhalte } from '@/lib/inhalte';
|
|||||||
import { alsKartenstand, staendeVon, uebersicht } from '@/lib/repos/karten';
|
import { alsKartenstand, staendeVon, uebersicht } from '@/lib/repos/karten';
|
||||||
import { fach, fachNamen, heutigerStapel } from '@/lib/lernen/sm2';
|
import { fach, fachNamen, heutigerStapel } from '@/lib/lernen/sm2';
|
||||||
import { alsDatumText, heute } from '@/lib/zeit';
|
import { alsDatumText, heute } from '@/lib/zeit';
|
||||||
|
import { erfordereModul } from '@/lib/module/pruefen';
|
||||||
|
|
||||||
export const metadata: Metadata = { title: 'Merkkarten' };
|
export const metadata: Metadata = { title: 'Merkkarten' };
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
@@ -13,6 +14,7 @@ const STAPELGROESSE = 20;
|
|||||||
|
|
||||||
export default async function KartenSeite() {
|
export default async function KartenSeite() {
|
||||||
const benutzer = await erfordereBenutzer();
|
const benutzer = await erfordereBenutzer();
|
||||||
|
erfordereModul('karten');
|
||||||
const alle = inhalte();
|
const alle = inhalte();
|
||||||
const heuteDatum = heute();
|
const heuteDatum = heute();
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { alsZeitpunktText } from '@/lib/zeit';
|
|||||||
import { Fragebogen } from '../Fragebogen';
|
import { Fragebogen } from '../Fragebogen';
|
||||||
import { Korrektur, type Korrekturfrage } from '../Korrektur';
|
import { Korrektur, type Korrekturfrage } from '../Korrektur';
|
||||||
import { standZumLesen } from '../aktionen';
|
import { standZumLesen } from '../aktionen';
|
||||||
|
import { erfordereModul } from '@/lib/module/pruefen';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
@@ -27,6 +28,7 @@ export default async function KontrollSeite({
|
|||||||
searchParams: Promise<{ azubi?: string }>;
|
searchParams: Promise<{ azubi?: string }>;
|
||||||
}) {
|
}) {
|
||||||
const benutzer = await erfordereBenutzer();
|
const benutzer = await erfordereBenutzer();
|
||||||
|
erfordereModul('kontrollen');
|
||||||
const { kontrolleId } = await params;
|
const { kontrolleId } = await params;
|
||||||
const { azubi: azubiParameter } = await searchParams;
|
const { azubi: azubiParameter } = await searchParams;
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
standVon,
|
standVon,
|
||||||
} from '@/lib/repos/kontrollen';
|
} from '@/lib/repos/kontrollen';
|
||||||
import { darfBearbeiten } from '@/lib/kontrollen/sicht';
|
import { darfBearbeiten } from '@/lib/kontrollen/sicht';
|
||||||
|
import { pruefeModul } from '@/lib/module/pruefen';
|
||||||
|
|
||||||
export type Rueckmeldung = { fehler?: string; erfolg?: string };
|
export type Rueckmeldung = { fehler?: string; erfolg?: string };
|
||||||
|
|
||||||
@@ -36,6 +37,7 @@ export async function antwortZwischenspeichern(
|
|||||||
antwort: string,
|
antwort: string,
|
||||||
): Promise<{ gespeichertUm: string } | { fehler: string }> {
|
): Promise<{ gespeichertUm: string } | { fehler: string }> {
|
||||||
const benutzer = await erfordereBenutzer();
|
const benutzer = await erfordereBenutzer();
|
||||||
|
pruefeModul('kontrollen');
|
||||||
|
|
||||||
const inhalt = inhalte().kontrolleNach.get(kontrolleId);
|
const inhalt = inhalte().kontrolleNach.get(kontrolleId);
|
||||||
if (!inhalt) return { fehler: 'Diese Lernzielkontrolle gibt es nicht.' };
|
if (!inhalt) return { fehler: 'Diese Lernzielkontrolle gibt es nicht.' };
|
||||||
@@ -57,6 +59,7 @@ export async function antwortZwischenspeichern(
|
|||||||
|
|
||||||
export async function kontrolleAbgeben(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
export async function kontrolleAbgeben(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
||||||
const benutzer = await erfordereBenutzer();
|
const benutzer = await erfordereBenutzer();
|
||||||
|
pruefeModul('kontrollen');
|
||||||
const kontrolleId = String(formular.get('kontrolleId') ?? '');
|
const kontrolleId = String(formular.get('kontrolleId') ?? '');
|
||||||
|
|
||||||
const inhalt = inhalte().kontrolleNach.get(kontrolleId);
|
const inhalt = inhalte().kontrolleNach.get(kontrolleId);
|
||||||
@@ -84,6 +87,7 @@ export async function kontrolleAbgeben(_vorher: Rueckmeldung, formular: FormData
|
|||||||
|
|
||||||
export async function kontrolleFreigeben(formular: FormData): Promise<void> {
|
export async function kontrolleFreigeben(formular: FormData): Promise<void> {
|
||||||
const ausbilder = await erfordereBenutzer();
|
const ausbilder = await erfordereBenutzer();
|
||||||
|
pruefeModul('kontrollen');
|
||||||
const azubiId = String(formular.get('azubiId') ?? '');
|
const azubiId = String(formular.get('azubiId') ?? '');
|
||||||
const kontrolleId = String(formular.get('kontrolleId') ?? '');
|
const kontrolleId = String(formular.get('kontrolleId') ?? '');
|
||||||
|
|
||||||
@@ -96,6 +100,7 @@ export async function kontrolleFreigeben(formular: FormData): Promise<void> {
|
|||||||
|
|
||||||
export async function freigabeZurueck(formular: FormData): Promise<void> {
|
export async function freigabeZurueck(formular: FormData): Promise<void> {
|
||||||
const ausbilder = await erfordereBenutzer();
|
const ausbilder = await erfordereBenutzer();
|
||||||
|
pruefeModul('kontrollen');
|
||||||
const azubiId = String(formular.get('azubiId') ?? '');
|
const azubiId = String(formular.get('azubiId') ?? '');
|
||||||
const kontrolleId = String(formular.get('kontrolleId') ?? '');
|
const kontrolleId = String(formular.get('kontrolleId') ?? '');
|
||||||
|
|
||||||
@@ -111,6 +116,7 @@ export async function freigabeZurueck(formular: FormData): Promise<void> {
|
|||||||
|
|
||||||
export async function kontrolleKorrigieren(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
export async function kontrolleKorrigieren(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
||||||
const ausbilder = await erfordereBenutzer();
|
const ausbilder = await erfordereBenutzer();
|
||||||
|
pruefeModul('kontrollen');
|
||||||
const azubiId = String(formular.get('azubiId') ?? '');
|
const azubiId = String(formular.get('azubiId') ?? '');
|
||||||
const kontrolleId = String(formular.get('kontrolleId') ?? '');
|
const kontrolleId = String(formular.get('kontrolleId') ?? '');
|
||||||
|
|
||||||
@@ -153,6 +159,7 @@ export async function kontrolleKorrigieren(_vorher: Rueckmeldung, formular: Form
|
|||||||
/** Prüft den Lesezugriff und liefert den Stand. Wird von Seiten benutzt. */
|
/** Prüft den Lesezugriff und liefert den Stand. Wird von Seiten benutzt. */
|
||||||
export async function standZumLesen(azubiId: string, kontrolleId: string) {
|
export async function standZumLesen(azubiId: string, kontrolleId: string) {
|
||||||
const benutzer = await erfordereBenutzer();
|
const benutzer = await erfordereBenutzer();
|
||||||
|
pruefeModul('kontrollen');
|
||||||
|
|
||||||
const bezug = ausbildungVonAzubi(azubiId);
|
const bezug = ausbildungVonAzubi(azubiId);
|
||||||
if (!darfAzubiDatenSehen(benutzer, azubiId, bezug)) return null;
|
if (!darfAzubiDatenSehen(benutzer, azubiId, bezug)) return null;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { gesamtpunkte, zustandVon, type Zustand } from '@/lib/kontrollen/sicht';
|
|||||||
import { verlauf } from '@/lib/kontrollen/bewertung';
|
import { verlauf } from '@/lib/kontrollen/bewertung';
|
||||||
import { alsZeitpunktText } from '@/lib/zeit';
|
import { alsZeitpunktText } from '@/lib/zeit';
|
||||||
import { kontrolleFreigeben, freigabeZurueck } from './aktionen';
|
import { kontrolleFreigeben, freigabeZurueck } from './aktionen';
|
||||||
|
import { erfordereModul } from '@/lib/module/pruefen';
|
||||||
|
|
||||||
export const metadata: Metadata = { title: 'Lernzielkontrollen' };
|
export const metadata: Metadata = { title: 'Lernzielkontrollen' };
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
@@ -21,6 +22,7 @@ const zustandsmarke: Record<Zustand, { text: string; ton: string; zeichen: strin
|
|||||||
|
|
||||||
export default async function KontrollenSeite() {
|
export default async function KontrollenSeite() {
|
||||||
const benutzer = await erfordereBenutzer();
|
const benutzer = await erfordereBenutzer();
|
||||||
|
erfordereModul('kontrollen');
|
||||||
const alle = inhalte();
|
const alle = inhalte();
|
||||||
|
|
||||||
if (benutzer.rolle === 'azubi') {
|
if (benutzer.rolle === 'azubi') {
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { erfordereBenutzer } from '@/lib/auth/rechte';
|
|||||||
import { Kopfzeile, type Navigationseintrag } from '@/komponenten/Kopfzeile';
|
import { Kopfzeile, type Navigationseintrag } from '@/komponenten/Kopfzeile';
|
||||||
import { Netzzustand } from '@/komponenten/Netzzustand';
|
import { Netzzustand } from '@/komponenten/Netzzustand';
|
||||||
import { abmelden } from '../anmelden/aktionen';
|
import { abmelden } from '../anmelden/aktionen';
|
||||||
|
import { modulStand } from '@/lib/repos/module';
|
||||||
|
import { nurOffene } from '@/lib/module/module';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
@@ -37,7 +39,8 @@ const fuerAusbilder: Navigationseintrag[] = [
|
|||||||
|
|
||||||
export default async function AngemeldetLayout({ children }: { children: React.ReactNode }) {
|
export default async function AngemeldetLayout({ children }: { children: React.ReactNode }) {
|
||||||
const benutzer = await erfordereBenutzer();
|
const benutzer = await erfordereBenutzer();
|
||||||
const eintraege = benutzer.rolle === 'ausbilder' ? fuerAusbilder : fuerAzubi;
|
// Abgeschaltete Module verschwinden aus der Navigation (lib/module).
|
||||||
|
const eintraege = nurOffene(benutzer.rolle === 'ausbilder' ? fuerAusbilder : fuerAzubi, modulStand());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { wartetAufAbzeichnung } from '@/lib/repos/berichtsheft';
|
|||||||
import { wartetAufKorrektur } from '@/lib/repos/kontrollen';
|
import { wartetAufKorrektur } from '@/lib/repos/kontrollen';
|
||||||
import { benutzerNachId } from '@/lib/repos/benutzer';
|
import { benutzerNachId } from '@/lib/repos/benutzer';
|
||||||
import { WochenAnsicht } from './WochenAnsicht';
|
import { WochenAnsicht } from './WochenAnsicht';
|
||||||
|
import { modulStand } from '@/lib/repos/module';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
@@ -151,10 +152,18 @@ function WartetAufMich({ ausbilderId }: { ausbilderId: string }) {
|
|||||||
const meine = ausbildungenVonAusbilder(ausbilderId);
|
const meine = ausbildungenVonAusbilder(ausbilderId);
|
||||||
const alle = inhalte();
|
const alle = inhalte();
|
||||||
const azubiIds = meine.map(({ azubi }) => azubi.id);
|
const azubiIds = meine.map(({ azubi }) => azubi.id);
|
||||||
const abnahmen = wartetAufAbnahme(azubiIds);
|
// Nur was zu eingeschalteten Modulen gehört – eine Abnahme, die man nicht
|
||||||
const berichte = wartetAufAbzeichnung(azubiIds);
|
// öffnen kann, wäre ein toter Verweis.
|
||||||
const kontrollen = wartetAufKorrektur(azubiIds);
|
const eingeschaltet = modulStand();
|
||||||
|
const abnahmen = eingeschaltet.aufgaben ? wartetAufAbnahme(azubiIds) : [];
|
||||||
|
const berichte = eingeschaltet.berichtsheft ? wartetAufAbzeichnung(azubiIds) : [];
|
||||||
|
const kontrollen = eingeschaltet.kontrollen ? wartetAufKorrektur(azubiIds) : [];
|
||||||
const gesamt = abnahmen.length + berichte.length + kontrollen.length;
|
const gesamt = abnahmen.length + berichte.length + kontrollen.length;
|
||||||
|
const art = [
|
||||||
|
eingeschaltet.aufgaben ? 'keine Abnahme' : null,
|
||||||
|
eingeschaltet.kontrollen ? 'keine Korrektur' : null,
|
||||||
|
eingeschaltet.berichtsheft ? 'kein Berichtsheft' : null,
|
||||||
|
].filter((teil) => teil !== null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="karte stapel-eng" aria-labelledby="wartet">
|
<section className="karte stapel-eng" aria-labelledby="wartet">
|
||||||
@@ -164,7 +173,7 @@ function WartetAufMich({ ausbilderId }: { ausbilderId: string }) {
|
|||||||
|
|
||||||
{gesamt === 0 ? (
|
{gesamt === 0 ? (
|
||||||
<p className="leise" style={{ marginBottom: 0 }}>
|
<p className="leise" style={{ marginBottom: 0 }}>
|
||||||
Nichts offen – keine Abnahme, keine Korrektur, kein Berichtsheft.
|
{art.length > 0 ? `Nichts offen – ${art.join(', ')}.` : 'Nichts offen.'}
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<ul className="offen-liste">
|
<ul className="offen-liste">
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
/** Die beiden Bereiche der Verwaltung. */
|
||||||
|
export function Unternavigation({ aktiv }: { aktiv: 'konten' | 'module' }) {
|
||||||
|
return (
|
||||||
|
<nav className="reihe nicht-drucken" aria-label="Bereiche der Verwaltung">
|
||||||
|
<Link
|
||||||
|
href="/verwaltung"
|
||||||
|
className={`knopf ${aktiv === 'konten' ? '' : 'knopf-zweit'}`}
|
||||||
|
aria-current={aktiv === 'konten' ? 'page' : undefined}
|
||||||
|
>
|
||||||
|
Konten
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/verwaltung/module"
|
||||||
|
className={`knopf ${aktiv === 'module' ? '' : 'knopf-zweit'}`}
|
||||||
|
aria-current={aktiv === 'module' ? 'page' : undefined}
|
||||||
|
>
|
||||||
|
Module
|
||||||
|
</Link>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import { revalidatePath } from 'next/cache';
|
||||||
|
import { erfordereAusbilder } from '@/lib/auth/rechte';
|
||||||
|
import { istModulId } from '@/lib/module/module';
|
||||||
|
import { modulSetzen } from '@/lib/repos/module';
|
||||||
|
|
||||||
|
/** Schaltet ein Modul für die ganze Lern-Bar ein oder aus. Nur Ausbilder. */
|
||||||
|
export async function modulUmschalten(formular: FormData): Promise<void> {
|
||||||
|
const ausbilder = await erfordereAusbilder();
|
||||||
|
const modul = String(formular.get('modul') ?? '');
|
||||||
|
const aktiv = String(formular.get('aktiv') ?? '') === '1';
|
||||||
|
|
||||||
|
if (!istModulId(modul)) return;
|
||||||
|
modulSetzen(modul, aktiv, ausbilder.id);
|
||||||
|
|
||||||
|
// Navigation, Startseite, Auswertung – alles hängt am Modulstand.
|
||||||
|
revalidatePath('/', 'layout');
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import type { Metadata } from 'next';
|
||||||
|
import { erfordereAusbilder } from '@/lib/auth/rechte';
|
||||||
|
import { modulIds, moduleintraege } from '@/lib/module/module';
|
||||||
|
import { modulAenderungen, modulStand } from '@/lib/repos/module';
|
||||||
|
import { benutzerNachId } from '@/lib/repos/benutzer';
|
||||||
|
import { alsZeitpunktText } from '@/lib/zeit';
|
||||||
|
import { Unternavigation } from '../Unternavigation';
|
||||||
|
import { modulUmschalten } from './aktionen';
|
||||||
|
|
||||||
|
export const metadata: Metadata = { title: 'Module' };
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
export default async function ModulSeite() {
|
||||||
|
await erfordereAusbilder();
|
||||||
|
const stand = modulStand();
|
||||||
|
const aenderungen = new Map(modulAenderungen().map((zeile) => [zeile.modul, zeile]));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="stapel">
|
||||||
|
<div>
|
||||||
|
<h1>Verwaltung</h1>
|
||||||
|
<p className="leise">
|
||||||
|
Module, die ihr nicht braucht, lassen sich abschalten – für die ganze Lern-Bar, für alle Azubis und
|
||||||
|
Ausbilder zugleich.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Unternavigation aktiv="module" />
|
||||||
|
|
||||||
|
<p className="hinweis">
|
||||||
|
<strong>Abschalten löscht nichts.</strong> Ein abgeschaltetes Modul verschwindet aus Navigation, Wochenplan
|
||||||
|
und Auswertung, und seine Seiten sind gesperrt. Was darin schon geschrieben wurde, bleibt gespeichert und ist
|
||||||
|
nach dem Einschalten unverändert wieder da. Wochenplan, Handouts, Glossar, Auswertung und Verwaltung sind
|
||||||
|
immer an.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="stapel">
|
||||||
|
{modulIds.map((id) => {
|
||||||
|
const eintrag = moduleintraege[id];
|
||||||
|
const an = stand[id];
|
||||||
|
const aenderung = aenderungen.get(id);
|
||||||
|
const wer = aenderung?.geaendertVon ? benutzerNachId(aenderung.geaendertVon)?.name : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section key={id} className="karte stapel-eng" aria-labelledby={`modul-${id}`}>
|
||||||
|
<div className="reihe" style={{ justifyContent: 'space-between' }}>
|
||||||
|
<div className="reihe" style={{ gap: '0.6rem' }}>
|
||||||
|
<h2 id={`modul-${id}`} style={{ margin: 0, fontSize: '1.05rem' }}>
|
||||||
|
{eintrag.titel}
|
||||||
|
</h2>
|
||||||
|
<span className="marke-status" data-ton={an ? 'gut' : 'warnung'}>
|
||||||
|
{an ? '● an' : '○ aus'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<form action={modulUmschalten}>
|
||||||
|
<input type="hidden" name="modul" value={id} />
|
||||||
|
<input type="hidden" name="aktiv" value={an ? '0' : '1'} />
|
||||||
|
<button type="submit" className={`knopf ${an ? 'knopf-zweit' : ''}`}>
|
||||||
|
{an ? 'Abschalten' : 'Einschalten'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style={{ margin: 0 }}>{eintrag.beschreibung}</p>
|
||||||
|
<p className="leise" style={{ margin: 0 }}>
|
||||||
|
<strong>Wenn aus:</strong> {eintrag.folgen}
|
||||||
|
</p>
|
||||||
|
{aenderung ? (
|
||||||
|
<p className="leise" style={{ margin: 0 }}>
|
||||||
|
Zuletzt {aenderung.aktiv === 1 ? 'eingeschaltet' : 'abgeschaltet'} am{' '}
|
||||||
|
{alsZeitpunktText(aenderung.geaendertAm)}
|
||||||
|
{wer ? ` von ${wer}` : ''}.
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import { ausbildungenVonAusbilder } from '@/lib/repos/ausbildung';
|
|||||||
import { alsDatumText, alsZeitpunktText, heute } from '@/lib/zeit';
|
import { alsDatumText, alsZeitpunktText, heute } from '@/lib/zeit';
|
||||||
import { AusbildungFormular, AzubiAnlegenFormular, PasswortFormular } from './Formulare';
|
import { AusbildungFormular, AzubiAnlegenFormular, PasswortFormular } from './Formulare';
|
||||||
import { kontoAktivSetzen } from './aktionen';
|
import { kontoAktivSetzen } from './aktionen';
|
||||||
|
import { Unternavigation } from './Unternavigation';
|
||||||
|
|
||||||
export const metadata: Metadata = { title: 'Verwaltung' };
|
export const metadata: Metadata = { title: 'Verwaltung' };
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
@@ -17,10 +18,13 @@ export default async function VerwaltungsSeite() {
|
|||||||
<div>
|
<div>
|
||||||
<h1>Verwaltung</h1>
|
<h1>Verwaltung</h1>
|
||||||
<p className="leise">
|
<p className="leise">
|
||||||
Hier legst du Azubikonten an und pflegst die Eckdaten der Ausbildung. Mehr Verwaltung gibt es bewusst nicht.
|
Hier legst du Azubikonten an und pflegst die Eckdaten der Ausbildung. Unter „Module“ schaltest du ab, was
|
||||||
|
ihr nicht braucht.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Unternavigation aktiv="konten" />
|
||||||
|
|
||||||
<section className="karte stapel" aria-labelledby="ueberschrift-azubis">
|
<section className="karte stapel" aria-labelledby="ueberschrift-azubis">
|
||||||
<h2 id="ueberschrift-azubis" style={{ marginBottom: 0 }}>
|
<h2 id="ueberschrift-azubis" style={{ marginBottom: 0 }}>
|
||||||
Deine Auszubildenden
|
Deine Auszubildenden
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { darfAzubiDatenSehen } from '@/lib/auth/regeln';
|
|||||||
import { ausbildungVonAzubi } from '@/lib/auth/rechte';
|
import { ausbildungVonAzubi } from '@/lib/auth/rechte';
|
||||||
import { anhangLesen, anhangNachId } from '@/lib/repos/anhaenge';
|
import { anhangLesen, anhangNachId } from '@/lib/repos/anhaenge';
|
||||||
import { istBild } from '@/lib/anhaenge/dateien';
|
import { istBild } from '@/lib/anhaenge/dateien';
|
||||||
|
import { modulAn, modulGesperrt } from '@/lib/module/pruefen';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@ export const dynamic = 'force-dynamic';
|
|||||||
export async function GET(_anfrage: Request, kontext: { params: Promise<{ id: string }> }) {
|
export async function GET(_anfrage: Request, kontext: { params: Promise<{ id: string }> }) {
|
||||||
const benutzer = await angemeldeterBenutzer();
|
const benutzer = await angemeldeterBenutzer();
|
||||||
if (!benutzer) return new Response('Nicht angemeldet.', { status: 401 });
|
if (!benutzer) return new Response('Nicht angemeldet.', { status: 401 });
|
||||||
|
if (!modulAn('aufgaben')) return modulGesperrt();
|
||||||
|
|
||||||
const { id } = await kontext.params;
|
const { id } = await kontext.params;
|
||||||
const eintrag = anhangNachId(id);
|
const eintrag = anhangNachId(id);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { bewerte } from '@/lib/repos/karten';
|
|||||||
import { bewertungen, type Bewertung } from '@/lib/lernen/sm2';
|
import { bewertungen, type Bewertung } from '@/lib/lernen/sm2';
|
||||||
import { heute } from '@/lib/zeit';
|
import { heute } from '@/lib/zeit';
|
||||||
import { tagDerBewertung } from '@/lib/offline/nachtrag';
|
import { tagDerBewertung } from '@/lib/offline/nachtrag';
|
||||||
|
import { modulAn } from '@/lib/module/pruefen';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
@@ -28,6 +29,9 @@ type Eingang = { karteId?: unknown; bewertung?: unknown; erfasstAm?: unknown };
|
|||||||
export async function POST(anfrage: Request): Promise<Response> {
|
export async function POST(anfrage: Request): Promise<Response> {
|
||||||
const benutzer = await angemeldeterBenutzer();
|
const benutzer = await angemeldeterBenutzer();
|
||||||
if (!benutzer) return Response.json({ fehler: 'Nicht angemeldet.' }, { status: 401 });
|
if (!benutzer) return Response.json({ fehler: 'Nicht angemeldet.' }, { status: 401 });
|
||||||
|
// 410 statt 404: Das Gerät soll seine gesammelten Bewertungen verwerfen,
|
||||||
|
// statt sie bei jeder Verbindung erneut zu schicken.
|
||||||
|
if (!modulAn('karten')) return Response.json({ fehler: 'Die Merkkarten sind abgeschaltet.' }, { status: 410 });
|
||||||
if (benutzer.rolle !== 'azubi') {
|
if (benutzer.rolle !== 'azubi') {
|
||||||
return Response.json({ fehler: 'Nur der Azubi führt seinen eigenen Lernstand.' }, { status: 403 });
|
return Response.json({ fehler: 'Nur der Azubi führt seinen eigenen Lernstand.' }, { status: 403 });
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-6
@@ -1,4 +1,16 @@
|
|||||||
import type { MetadataRoute } from 'next';
|
import type { MetadataRoute } from 'next';
|
||||||
|
import { modulStand } from '@/lib/repos/module';
|
||||||
|
import { pfadOffen } from '@/lib/module/module';
|
||||||
|
|
||||||
|
// Die Kurzbefehle richten sich nach den eingeschalteten Modulen – also bei
|
||||||
|
// jedem Abruf neu erzeugen, nicht einmal beim Bau.
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
const kurzbefehle = [
|
||||||
|
{ name: 'Lernmodus', short_name: 'Lernen', url: '/karten/lernen' },
|
||||||
|
{ name: 'Berichtsheft', short_name: 'Bericht', url: '/berichtsheft' },
|
||||||
|
{ name: 'Handouts', short_name: 'Handouts', url: '/handouts' },
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Die Angaben für die Installation auf dem Startbildschirm.
|
* Die Angaben für die Installation auf dem Startbildschirm.
|
||||||
@@ -8,11 +20,12 @@ import type { MetadataRoute } from 'next';
|
|||||||
* abgelaufen, leitet der Server ohnehin zur Anmeldung.
|
* abgelaufen, leitet der Server ohnehin zur Anmeldung.
|
||||||
*/
|
*/
|
||||||
export default function manifest(): MetadataRoute.Manifest {
|
export default function manifest(): MetadataRoute.Manifest {
|
||||||
|
const stand = modulStand();
|
||||||
return {
|
return {
|
||||||
name: 'Lern-Bar – Betriebliche Ausbildung',
|
name: 'Lern-Bar – Betriebliche Ausbildung',
|
||||||
short_name: 'Lern-Bar',
|
short_name: 'Lern-Bar',
|
||||||
description:
|
description:
|
||||||
'Wochenplan, Handouts, Merkkarten, Aufgaben, Checklisten und Berichtsheft für die Ausbildung zum Fachinformatiker für Systemintegration.',
|
'Wochenplan, Lehrgang und Nachweise für die Ausbildung zum Fachinformatiker für Systemintegration.',
|
||||||
lang: 'de',
|
lang: 'de',
|
||||||
dir: 'ltr',
|
dir: 'ltr',
|
||||||
start_url: '/',
|
start_url: '/',
|
||||||
@@ -27,10 +40,6 @@ export default function manifest(): MetadataRoute.Manifest {
|
|||||||
{ src: '/symbol-512.png', sizes: '512x512', type: 'image/png', purpose: 'any' },
|
{ src: '/symbol-512.png', sizes: '512x512', type: 'image/png', purpose: 'any' },
|
||||||
{ src: '/symbol-maskabel-512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' },
|
{ src: '/symbol-maskabel-512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' },
|
||||||
],
|
],
|
||||||
shortcuts: [
|
shortcuts: kurzbefehle.filter((kurz) => pfadOffen(kurz.url, stand)),
|
||||||
{ name: 'Lernmodus', short_name: 'Lernen', url: '/karten/lernen' },
|
|
||||||
{ name: 'Berichtsheft', short_name: 'Bericht', url: '/berichtsheft' },
|
|
||||||
{ name: 'Handouts', short_name: 'Handouts', url: '/handouts' },
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { berichteVon, fehlendeWochen } from '../repos/berichtsheft';
|
|||||||
import { ergebnisVon, staendeVon as kontrollStaende } from '../repos/kontrollen';
|
import { ergebnisVon, staendeVon as kontrollStaende } from '../repos/kontrollen';
|
||||||
import { alsDatumText, alsZeitpunktText } from '../zeit';
|
import { alsDatumText, alsZeitpunktText } from '../zeit';
|
||||||
import { berechneAbdeckung, berechneLernfelder, type Nachweis } from './abdeckung';
|
import { berechneAbdeckung, berechneLernfelder, type Nachweis } from './abdeckung';
|
||||||
|
import { modulStand } from '../repos/module';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sammelt aus Inhalten und Fortschritt alles, was die Auswertung braucht.
|
* Sammelt aus Inhalten und Fortschritt alles, was die Auswertung braucht.
|
||||||
@@ -17,6 +18,9 @@ import { berechneAbdeckung, berechneLernfelder, type Nachweis } from './abdeckun
|
|||||||
|
|
||||||
export function rahmenplanabdeckung(azubiId: string) {
|
export function rahmenplanabdeckung(azubiId: string) {
|
||||||
const alle = inhalte();
|
const alle = inhalte();
|
||||||
|
// Abgeschaltete Module liefern keine Nachweise. Sonst stünden ihre Inhalte
|
||||||
|
// für immer als „geplant, noch nicht bearbeitet" da.
|
||||||
|
const eingeschaltet = modulStand();
|
||||||
const gelesen = gelesenSet(azubiId);
|
const gelesen = gelesenSet(azubiId);
|
||||||
const aufgaben = aufgabenStaende(azubiId);
|
const aufgaben = aufgabenStaende(azubiId);
|
||||||
const laeufe = laeufeVon(azubiId);
|
const laeufe = laeufeVon(azubiId);
|
||||||
@@ -47,7 +51,7 @@ export function rahmenplanabdeckung(azubiId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Praxisaufgaben ---
|
// --- Praxisaufgaben ---
|
||||||
for (const aufgabe of alle.aufgaben) {
|
for (const aufgabe of eingeschaltet.aufgaben ? alle.aufgaben : []) {
|
||||||
const stand = aufgaben.get(aufgabe.id);
|
const stand = aufgaben.get(aufgabe.id);
|
||||||
const nachweis: Nachweis = {
|
const nachweis: Nachweis = {
|
||||||
art: 'aufgabe',
|
art: 'aufgabe',
|
||||||
@@ -61,7 +65,7 @@ export function rahmenplanabdeckung(azubiId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Checklisten ---
|
// --- Checklisten ---
|
||||||
for (const liste of alle.checklisten) {
|
for (const liste of eingeschaltet.checklisten ? alle.checklisten : []) {
|
||||||
const abgeschlossenAm = abgeschlosseneListen.get(liste.id);
|
const abgeschlossenAm = abgeschlosseneListen.get(liste.id);
|
||||||
const nachweis: Nachweis = {
|
const nachweis: Nachweis = {
|
||||||
art: 'checkliste',
|
art: 'checkliste',
|
||||||
@@ -75,6 +79,7 @@ export function rahmenplanabdeckung(azubiId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
module: eingeschaltet,
|
||||||
positionen: berechneAbdeckung(alle.positionen, jePosition),
|
positionen: berechneAbdeckung(alle.positionen, jePosition),
|
||||||
lernfelder: berechneLernfelder(alle.lernfelder, jeLernfeld),
|
lernfelder: berechneLernfelder(alle.lernfelder, jeLernfeld),
|
||||||
gruppennamen: alle.rahmenplan.gruppen,
|
gruppennamen: alle.rahmenplan.gruppen,
|
||||||
@@ -84,6 +89,7 @@ export function rahmenplanabdeckung(azubiId: string) {
|
|||||||
/** Der persönliche Stand des Azubis – was er selbst sehen will. */
|
/** Der persönliche Stand des Azubis – was er selbst sehen will. */
|
||||||
export function eigenerStand(azubiId: string, beginn: string | null) {
|
export function eigenerStand(azubiId: string, beginn: string | null) {
|
||||||
const alle = inhalte();
|
const alle = inhalte();
|
||||||
|
const eingeschaltet = modulStand();
|
||||||
const gelesen = gelesenSet(azubiId);
|
const gelesen = gelesenSet(azubiId);
|
||||||
const aufgaben = aufgabenStaende(azubiId);
|
const aufgaben = aufgabenStaende(azubiId);
|
||||||
const karten = kartenUebersicht(azubiId, alle.karten.map((k) => k.id));
|
const karten = kartenUebersicht(azubiId, alle.karten.map((k) => k.id));
|
||||||
@@ -111,6 +117,7 @@ export function eigenerStand(azubiId: string, beginn: string | null) {
|
|||||||
.sort((a, b) => a.woche - b.woche);
|
.sort((a, b) => a.woche - b.woche);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
module: eingeschaltet,
|
||||||
handouts: { erledigt: gelesen.size, gesamt: alle.handouts.length },
|
handouts: { erledigt: gelesen.size, gesamt: alle.handouts.length },
|
||||||
karten: { erledigt: karten.gelernt, gesamt: karten.gesamt, faellig: karten.faellig },
|
karten: { erledigt: karten.gelernt, gesamt: karten.gesamt, faellig: karten.faellig },
|
||||||
aufgaben: {
|
aufgaben: {
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
-- Fassung 1.1: Module ein- und ausschalten.
|
||||||
|
--
|
||||||
|
-- Eine Zeile je Modul, das schon einmal umgeschaltet wurde. Fehlt die Zeile,
|
||||||
|
-- ist das Modul eingeschaltet – eine frische Datenbank hat alles an, und eine
|
||||||
|
-- bestehende verhält sich nach dem Einspielen genau wie vorher.
|
||||||
|
--
|
||||||
|
-- Abschalten löscht nichts. Die Daten eines Moduls bleiben in ihren Tabellen
|
||||||
|
-- und sind nach dem Wiedereinschalten unverändert da.
|
||||||
|
|
||||||
|
CREATE TABLE modul_einstellung (
|
||||||
|
modul TEXT PRIMARY KEY,
|
||||||
|
aktiv INTEGER NOT NULL CHECK (aktiv IN (0, 1)),
|
||||||
|
geaendert_von TEXT REFERENCES benutzer (id) ON DELETE SET NULL,
|
||||||
|
geaendert_am TEXT NOT NULL
|
||||||
|
);
|
||||||
@@ -323,6 +323,14 @@ export const feedbackgespraech = sqliteTable(
|
|||||||
(t) => [index('feedbackgespraech_ausbildung_idx').on(t.ausbildungId, t.datum)],
|
(t) => [index('feedbackgespraech_ausbildung_idx').on(t.ausbildungId, t.datum)],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** Abschaltbare Module – eine Zeile je Modul, das je umgeschaltet wurde. Siehe lib/module. */
|
||||||
|
export const modulEinstellung = sqliteTable('modul_einstellung', {
|
||||||
|
modul: text('modul').primaryKey(),
|
||||||
|
aktiv: integer('aktiv').notNull(),
|
||||||
|
geaendertVon: text('geaendert_von').references(() => benutzer.id, { onDelete: 'set null' }),
|
||||||
|
geaendertAm: text('geaendert_am').notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
export type Benutzer = typeof benutzer.$inferSelect;
|
export type Benutzer = typeof benutzer.$inferSelect;
|
||||||
export type Sitzung = typeof sitzung.$inferSelect;
|
export type Sitzung = typeof sitzung.$inferSelect;
|
||||||
export type Ausbildung = typeof ausbildung.$inferSelect;
|
export type Ausbildung = typeof ausbildung.$inferSelect;
|
||||||
@@ -339,6 +347,7 @@ export type Wochenbericht = typeof wochenbericht.$inferSelect;
|
|||||||
export type Kontrolle = typeof kontrolle.$inferSelect;
|
export type Kontrolle = typeof kontrolle.$inferSelect;
|
||||||
export type KontrollAntwort = typeof kontrollAntwort.$inferSelect;
|
export type KontrollAntwort = typeof kontrollAntwort.$inferSelect;
|
||||||
export type Feedbackgespraech = typeof feedbackgespraech.$inferSelect;
|
export type Feedbackgespraech = typeof feedbackgespraech.$inferSelect;
|
||||||
|
export type ModulEinstellung = typeof modulEinstellung.$inferSelect;
|
||||||
|
|
||||||
/** Wird von der Schema-Abgleichprüfung im Test verwendet. */
|
/** Wird von der Schema-Abgleichprüfung im Test verwendet. */
|
||||||
export const alleTabellen = {
|
export const alleTabellen = {
|
||||||
@@ -358,4 +367,5 @@ export const alleTabellen = {
|
|||||||
kontrolle,
|
kontrolle,
|
||||||
kontrollAntwort,
|
kontrollAntwort,
|
||||||
feedbackgespraech,
|
feedbackgespraech,
|
||||||
|
modulEinstellung,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import {
|
||||||
|
abgeschaltet,
|
||||||
|
alleAn,
|
||||||
|
istModulId,
|
||||||
|
modulIds,
|
||||||
|
moduleintraege,
|
||||||
|
modulVonPfad,
|
||||||
|
nurOffene,
|
||||||
|
pfadOffen,
|
||||||
|
standAus,
|
||||||
|
} from './module';
|
||||||
|
|
||||||
|
describe('standAus', () => {
|
||||||
|
it('hat in einer frischen Datenbank alles an', () => {
|
||||||
|
expect(standAus([])).toEqual(alleAn);
|
||||||
|
expect(Object.values(standAus([])).every(Boolean)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('übernimmt gespeicherte Schalter', () => {
|
||||||
|
const stand = standAus([
|
||||||
|
{ modul: 'berichtsheft', aktiv: 0 },
|
||||||
|
{ modul: 'karten', aktiv: 1 },
|
||||||
|
]);
|
||||||
|
expect(stand.berichtsheft).toBe(false);
|
||||||
|
expect(stand.karten).toBe(true);
|
||||||
|
expect(stand.aufgaben).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('übergeht unbekannte Kennungen, etwa aus einer späteren Fassung', () => {
|
||||||
|
const stand = standAus([{ modul: 'gibtsnicht', aktiv: 0 }]);
|
||||||
|
expect(stand).toEqual(alleAn);
|
||||||
|
expect('gibtsnicht' in stand).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('modulVonPfad', () => {
|
||||||
|
it('ordnet Seiten und Unterseiten ihrem Modul zu', () => {
|
||||||
|
expect(modulVonPfad('/berichtsheft')).toBe('berichtsheft');
|
||||||
|
expect(modulVonPfad('/berichtsheft/abc/pdf')).toBe('berichtsheft');
|
||||||
|
expect(modulVonPfad('/karten/lernen')).toBe('karten');
|
||||||
|
expect(modulVonPfad('/checklisten/lauf/123')).toBe('checklisten');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ordnet Routen außerhalb der Seiten ihrem Modul zu', () => {
|
||||||
|
expect(modulVonPfad('/anhaenge/abc')).toBe('aufgaben');
|
||||||
|
expect(modulVonPfad('/api/lernstand')).toBe('karten');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lässt das Gerüst unangetastet', () => {
|
||||||
|
for (const pfad of ['/', '/wochenplan', '/woche/3', '/handouts', '/handouts/N3', '/glossar', '/auswertung', '/verwaltung', '/verwaltung/module']) {
|
||||||
|
expect(modulVonPfad(pfad), pfad).toBeNull();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('trifft nur ganze Pfadteile', () => {
|
||||||
|
// „/kartenspiel" gehört nicht zu den Merkkarten.
|
||||||
|
expect(modulVonPfad('/kartenspiel')).toBeNull();
|
||||||
|
expect(modulVonPfad('/feedbackbogen')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('pfadOffen und nurOffene', () => {
|
||||||
|
const navigation = [
|
||||||
|
{ pfad: '/', titel: 'Woche' },
|
||||||
|
{ pfad: '/karten', titel: 'Merkkarten' },
|
||||||
|
{ pfad: '/berichtsheft', titel: 'Berichtsheft' },
|
||||||
|
{ pfad: '/glossar', titel: 'Glossar' },
|
||||||
|
];
|
||||||
|
|
||||||
|
it('lässt bei allem an die Navigation unverändert', () => {
|
||||||
|
expect(nurOffene(navigation, alleAn)).toEqual(navigation);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('entfernt genau die abgeschalteten Module', () => {
|
||||||
|
const stand = { ...alleAn, berichtsheft: false };
|
||||||
|
expect(nurOffene(navigation, stand).map((e) => e.titel)).toEqual(['Woche', 'Merkkarten', 'Glossar']);
|
||||||
|
expect(pfadOffen('/berichtsheft/pdf', stand)).toBe(false);
|
||||||
|
expect(pfadOffen('/karten/lernen', stand)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('behält das Gerüst auch, wenn alles aus ist', () => {
|
||||||
|
const alleAus = Object.fromEntries(modulIds.map((id) => [id, false])) as typeof alleAn;
|
||||||
|
expect(nurOffene(navigation, alleAus).map((e) => e.titel)).toEqual(['Woche', 'Glossar']);
|
||||||
|
expect(abgeschaltet(alleAus)).toEqual([...modulIds]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Moduleinträge', () => {
|
||||||
|
it('beschreibt jedes Modul vollständig', () => {
|
||||||
|
for (const id of modulIds) {
|
||||||
|
const eintrag = moduleintraege[id];
|
||||||
|
expect(eintrag.id).toBe(id);
|
||||||
|
expect(eintrag.titel.length, id).toBeGreaterThan(0);
|
||||||
|
expect(eintrag.beschreibung.length, id).toBeGreaterThan(10);
|
||||||
|
expect(eintrag.folgen.length, id).toBeGreaterThan(10);
|
||||||
|
expect(eintrag.pfade.length, id).toBeGreaterThan(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('erkennt gültige Kennungen', () => {
|
||||||
|
expect(istModulId('karten')).toBe(true);
|
||||||
|
expect(istModulId('verwaltung')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
/**
|
||||||
|
* Die abschaltbaren Module der Lern-Bar.
|
||||||
|
*
|
||||||
|
* Bewusst ohne Datenbank und ohne Next.js – die Liste und die Regeln stehen
|
||||||
|
* hier, der gespeicherte Zustand kommt aus `repos/module`, die Durchsetzung aus
|
||||||
|
* `module/pruefen`. So lassen sich die Regeln vollständig testen.
|
||||||
|
*
|
||||||
|
* Immer an bleiben Wochenplan, Handouts, Glossar, Auswertung und Verwaltung:
|
||||||
|
* Sie sind das Gerüst, an dem alles andere hängt. Die Auswertung passt sich an
|
||||||
|
* und zeigt nur, was eingeschaltet ist.
|
||||||
|
*
|
||||||
|
* Abschalten heißt ausblenden und sperren, **nicht löschen**. Was ein Azubi in
|
||||||
|
* einem Modul geschrieben hat, bleibt in der Datenbank und ist nach dem
|
||||||
|
* Wiedereinschalten unverändert da. Siehe E43.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const modulIds = ['karten', 'aufgaben', 'checklisten', 'berichtsheft', 'kontrollen', 'feedback'] as const;
|
||||||
|
export type ModulId = (typeof modulIds)[number];
|
||||||
|
|
||||||
|
/** Für jedes Modul: an oder aus. */
|
||||||
|
export type ModulStand = Record<ModulId, boolean>;
|
||||||
|
|
||||||
|
export type Moduleintrag = {
|
||||||
|
id: ModulId;
|
||||||
|
titel: string;
|
||||||
|
/** Was das Modul tut – für die Verwaltung. */
|
||||||
|
beschreibung: string;
|
||||||
|
/** Was sich ändert, wenn es aus ist – damit niemand überrascht wird. */
|
||||||
|
folgen: string;
|
||||||
|
/** Die Pfade, die zu diesem Modul gehören und mit ihm gesperrt werden. */
|
||||||
|
pfade: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const moduleintraege: Record<ModulId, Moduleintrag> = {
|
||||||
|
karten: {
|
||||||
|
id: 'karten',
|
||||||
|
titel: 'Merkkarten',
|
||||||
|
beschreibung: 'Wiederholung nach Zeitabstand, täglicher Stapel, Lernen auch ohne Netz.',
|
||||||
|
folgen:
|
||||||
|
'Merkkarten verschwinden aus Handouts, Wochenfortschritt und „Mein Stand". Bewertungen, die ein Gerät noch ohne Netz gesammelt hat, werden verworfen.',
|
||||||
|
pfade: ['/karten', '/api/lernstand'],
|
||||||
|
},
|
||||||
|
aufgaben: {
|
||||||
|
id: 'aufgaben',
|
||||||
|
titel: 'Praxisaufgaben',
|
||||||
|
beschreibung: 'Aufträge mit Bewertungskriterien, Notizen und Anhängen, Abnahme mit Note.',
|
||||||
|
folgen:
|
||||||
|
'Aufgaben verschwinden aus Wochenplan, „Was auf dich wartet" und Auswertung. In der Rahmenplanabdeckung zählen sie nicht mehr; Anhänge sind nicht mehr abrufbar.',
|
||||||
|
pfade: ['/aufgaben', '/anhaenge'],
|
||||||
|
},
|
||||||
|
checklisten: {
|
||||||
|
id: 'checklisten',
|
||||||
|
titel: 'Checklisten',
|
||||||
|
beschreibung: 'Abläufe Schritt für Schritt abhaken, Durchläufe als Nachweis mit PDF.',
|
||||||
|
folgen:
|
||||||
|
'Checklisten verschwinden aus Wochenplan und Wochenfortschritt. In der Rahmenplanabdeckung und bei den Vorschlägen fürs Berichtsheft zählen sie nicht mehr.',
|
||||||
|
pfade: ['/checklisten'],
|
||||||
|
},
|
||||||
|
berichtsheft: {
|
||||||
|
id: 'berichtsheft',
|
||||||
|
titel: 'Berichtsheft',
|
||||||
|
beschreibung: 'Tagesnotizen, Wochenblätter mit Vorschlägen, Einreichen, Abzeichnen und PDF im IHK-Layout.',
|
||||||
|
folgen:
|
||||||
|
'Das Berichtsheft verschwindet aus Navigation, „Was auf dich wartet" und „Mein Stand". Den Ausbildungsnachweis muss der Azubi dann anderswo führen – auf Papier oder im Portal der Kammer.',
|
||||||
|
pfade: ['/berichtsheft'],
|
||||||
|
},
|
||||||
|
kontrollen: {
|
||||||
|
id: 'kontrollen',
|
||||||
|
titel: 'Lernzielkontrollen',
|
||||||
|
beschreibung: 'Freigabe durch den Ausbilder, Abgabe, Korrektur mit Punkten und IHK-Notenschlüssel.',
|
||||||
|
folgen:
|
||||||
|
'Kontrollen verschwinden aus Wochenplan, Wochenfortschritt, „Was auf dich wartet" und dem Ergebnisverlauf der Auswertung.',
|
||||||
|
pfade: ['/kontrollen'],
|
||||||
|
},
|
||||||
|
feedback: {
|
||||||
|
id: 'feedback',
|
||||||
|
titel: 'Feedbackgespräch',
|
||||||
|
beschreibung: 'Leitfaden und Bewertungsbogen, getrennte Einschätzung beider Seiten, PDF für die Akte.',
|
||||||
|
folgen: 'Der Bereich „Feedback" verschwindet aus der Navigation. Bereits geführte Gespräche bleiben gespeichert.',
|
||||||
|
pfade: ['/feedback'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Werkszustand: alles an. */
|
||||||
|
export const alleAn: ModulStand = Object.fromEntries(modulIds.map((id) => [id, true])) as ModulStand;
|
||||||
|
|
||||||
|
export function istModulId(wert: string): wert is ModulId {
|
||||||
|
return (modulIds as readonly string[]).includes(wert);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baut den Stand aus gespeicherten Zeilen. Fehlt ein Modul, gilt es als an –
|
||||||
|
* eine frische Datenbank hat alles eingeschaltet. Unbekannte Kennungen (etwa
|
||||||
|
* aus einer späteren Fassung) werden übergangen.
|
||||||
|
*/
|
||||||
|
export function standAus(zeilen: { modul: string; aktiv: number }[]): ModulStand {
|
||||||
|
const stand: ModulStand = { ...alleAn };
|
||||||
|
for (const zeile of zeilen) {
|
||||||
|
if (istModulId(zeile.modul)) stand[zeile.modul] = zeile.aktiv === 1;
|
||||||
|
}
|
||||||
|
return stand;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Zu welchem Modul gehört ein Pfad? `null`, wenn zu keinem abschaltbaren. */
|
||||||
|
export function modulVonPfad(pfad: string): ModulId | null {
|
||||||
|
for (const id of modulIds) {
|
||||||
|
for (const wurzel of moduleintraege[id].pfade) {
|
||||||
|
if (pfad === wurzel || pfad.startsWith(`${wurzel}/`)) return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ist ein Pfad bei diesem Stand erreichbar? */
|
||||||
|
export function pfadOffen(pfad: string, stand: ModulStand): boolean {
|
||||||
|
const modul = modulVonPfad(pfad);
|
||||||
|
return modul === null || stand[modul];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Lässt aus einer Liste von Zielen nur die erreichbaren übrig – für Navigation und Kurzbefehle. */
|
||||||
|
export function nurOffene<T extends { pfad: string }>(eintraege: T[], stand: ModulStand): T[] {
|
||||||
|
return eintraege.filter((eintrag) => pfadOffen(eintrag.pfad, stand));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Die abgeschalteten Module, in fester Reihenfolge. */
|
||||||
|
export function abgeschaltet(stand: ModulStand): ModulId[] {
|
||||||
|
return modulIds.filter((id) => !stand[id]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import 'server-only';
|
||||||
|
import { notFound } from 'next/navigation';
|
||||||
|
import { modulStand } from '../repos/module';
|
||||||
|
import { moduleintraege, type ModulId } from './module';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Die Durchsetzung der Modulschalter. Ein abgeschaltetes Modul ist nicht nur
|
||||||
|
* unsichtbar, sondern zu – an drei Stellen:
|
||||||
|
*
|
||||||
|
* - **Seiten** rufen `erfordereModul` auf und enden mit „nicht gefunden",
|
||||||
|
* genau wie bei fehlender Berechtigung (E21).
|
||||||
|
* - **Routen** (PDF, Anhänge, Lernstand) fragen `modulAn` und antworten selbst.
|
||||||
|
* - **Serveraktionen** rufen `pruefeModul` auf. Ein Formular, das noch in einem
|
||||||
|
* offenen Fenster steht, darf nach dem Abschalten nichts mehr schreiben.
|
||||||
|
*
|
||||||
|
* Bewusst nicht im Layout des Moduls: Next.js rendert Layouts beim Wechsel
|
||||||
|
* zwischen Seiten darunter nicht neu, eine Prüfung dort würde also nicht bei
|
||||||
|
* jedem Aufruf greifen.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function modulAn(modul: ModulId): boolean {
|
||||||
|
return modulStand()[modul];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Für Seiten: abgeschaltet heißt „nicht gefunden". */
|
||||||
|
export function erfordereModul(modul: ModulId): void {
|
||||||
|
if (!modulAn(modul)) notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Für Serveraktionen: abgeschaltet heißt abbrechen, bevor etwas geschrieben wird. */
|
||||||
|
export function pruefeModul(modul: ModulId): void {
|
||||||
|
if (!modulAn(modul)) {
|
||||||
|
throw new Error(`Das Modul „${moduleintraege[modul].titel}" ist abgeschaltet.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Für Routen: die Antwort, wenn das Modul aus ist. */
|
||||||
|
export function modulGesperrt(): Response {
|
||||||
|
return new Response('Nicht gefunden.', { status: 404 });
|
||||||
|
}
|
||||||
@@ -100,6 +100,12 @@ export async function warteschlangeAbschicken(benutzerId: string): Promise<numbe
|
|||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Das Modul Merkkarten ist abgeschaltet: Die Bewertungen werden nie mehr
|
||||||
|
// angenommen, also nicht ewig weiter mitschleppen.
|
||||||
|
if (antwort.status === 410) {
|
||||||
|
warteschlangeLeeren(benutzerId);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
if (!antwort.ok) return null;
|
if (!antwort.ok) return null;
|
||||||
|
|
||||||
const ergebnis: unknown = await antwort.json();
|
const ergebnis: unknown = await antwort.json();
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { anzahlAbgenommen } from '../repos/aufgaben';
|
|||||||
import { anzahlAbgeschlossen } from '../repos/checklisten';
|
import { anzahlAbgeschlossen } from '../repos/checklisten';
|
||||||
import { anzahlAbgegeben } from '../repos/kontrollen';
|
import { anzahlAbgegeben } from '../repos/kontrollen';
|
||||||
import { heute } from '../zeit';
|
import { heute } from '../zeit';
|
||||||
|
import { modulStand } from '../repos/module';
|
||||||
|
import type { ModulStand } from '../module/module';
|
||||||
import { aktuelleWoche } from './wochen';
|
import { aktuelleWoche } from './wochen';
|
||||||
import { wochenFortschritt, type Fortschritt } from './fortschritt';
|
import { wochenFortschritt, type Fortschritt } from './fortschritt';
|
||||||
import type { Wochenplanung } from '../db/schema';
|
import type { Wochenplanung } from '../db/schema';
|
||||||
@@ -34,6 +36,8 @@ export type Wochensicht = {
|
|||||||
aufgaben: Aufgabe[];
|
aufgaben: Aufgabe[];
|
||||||
checklisten: Checkliste[];
|
checklisten: Checkliste[];
|
||||||
kontrolle: Kontrolle | null;
|
kontrolle: Kontrolle | null;
|
||||||
|
/** Welche Module an sind – die Ansicht blendet die übrigen aus. */
|
||||||
|
module: ModulStand;
|
||||||
vorherige: number | null;
|
vorherige: number | null;
|
||||||
naechste: number | null;
|
naechste: number | null;
|
||||||
};
|
};
|
||||||
@@ -66,14 +70,22 @@ export function wochensicht(ausbildungId: string, azubiId: string, wochenNummer:
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const aufgaben = woche.aufgaben.map((id) => alle.aufgabeNach.get(id)).filter((a) => a !== undefined);
|
// Ein abgeschaltetes Modul trägt nichts zur Woche bei – weder in der Ansicht
|
||||||
const checklisten = woche.checklisten.map((id) => alle.checklisteNach.get(id)).filter((c) => c !== undefined);
|
// noch im Fortschritt. So erreicht eine Woche auch ohne Merkkarten 100 %.
|
||||||
const kontrolle = woche.lernzielkontrolle ? (alle.kontrolleNach.get(woche.lernzielkontrolle) ?? null) : null;
|
const eingeschaltet = modulStand();
|
||||||
|
const aufgaben = eingeschaltet.aufgaben
|
||||||
|
? woche.aufgaben.map((id) => alle.aufgabeNach.get(id)).filter((a) => a !== undefined)
|
||||||
|
: [];
|
||||||
|
const checklisten = eingeschaltet.checklisten
|
||||||
|
? woche.checklisten.map((id) => alle.checklisteNach.get(id)).filter((c) => c !== undefined)
|
||||||
|
: [];
|
||||||
|
const kontrolle =
|
||||||
|
eingeschaltet.kontrollen && woche.lernzielkontrolle ? (alle.kontrolleNach.get(woche.lernzielkontrolle) ?? null) : null;
|
||||||
|
|
||||||
// Die Merkkarten dieser Woche sind die Karten ihrer Handouts.
|
// Die Merkkarten dieser Woche sind die Karten ihrer Handouts.
|
||||||
const karteIds = handouts.flatMap(({ handout }) =>
|
const karteIds = eingeschaltet.karten
|
||||||
alle.karten.filter((k) => k.handoutId === handout.id).map((k) => k.id),
|
? handouts.flatMap(({ handout }) => alle.karten.filter((k) => k.handoutId === handout.id).map((k) => k.id))
|
||||||
);
|
: [];
|
||||||
const kartenStand = kartenUebersicht(azubiId, karteIds, heuteDatum);
|
const kartenStand = kartenUebersicht(azubiId, karteIds, heuteDatum);
|
||||||
|
|
||||||
const fortschritt = wochenFortschritt({
|
const fortschritt = wochenFortschritt({
|
||||||
@@ -102,6 +114,7 @@ export function wochensicht(ausbildungId: string, azubiId: string, wochenNummer:
|
|||||||
aufgaben,
|
aufgaben,
|
||||||
checklisten,
|
checklisten,
|
||||||
kontrolle,
|
kontrolle,
|
||||||
|
module: eingeschaltet,
|
||||||
vorherige: stelle > 0 ? (nummern[stelle - 1] ?? null) : null,
|
vorherige: stelle > 0 ? (nummern[stelle - 1] ?? null) : null,
|
||||||
naechste: stelle < nummern.length - 1 ? (nummern[stelle + 1] ?? null) : null,
|
naechste: stelle < nummern.length - 1 ? (nummern[stelle + 1] ?? null) : null,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { datenbank, schema } from '../db';
|
||||||
|
import { jetzt } from '../zeit';
|
||||||
|
import { standAus, type ModulId, type ModulStand } from '../module/module';
|
||||||
|
|
||||||
|
/** Sämtlicher Zugriff auf die Tabelle `modul_einstellung` läuft über dieses Modul. */
|
||||||
|
|
||||||
|
/** Der aktuelle Stand aller Module. Ohne gespeicherte Zeile ist ein Modul an. */
|
||||||
|
export function modulStand(): ModulStand {
|
||||||
|
const zeilen = datenbank()
|
||||||
|
.select({ modul: schema.modulEinstellung.modul, aktiv: schema.modulEinstellung.aktiv })
|
||||||
|
.from(schema.modulEinstellung)
|
||||||
|
.all();
|
||||||
|
return standAus(zeilen);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Wer wann zuletzt umgeschaltet hat – für die Anzeige in der Verwaltung. */
|
||||||
|
export function modulAenderungen() {
|
||||||
|
return datenbank().select().from(schema.modulEinstellung).all();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function modulSetzen(modul: ModulId, aktiv: boolean, benutzerId: string): void {
|
||||||
|
const werte = { aktiv: aktiv ? 1 : 0, geaendertVon: benutzerId, geaendertAm: jetzt() };
|
||||||
|
datenbank()
|
||||||
|
.insert(schema.modulEinstellung)
|
||||||
|
.values({ modul, ...werte })
|
||||||
|
.onConflictDoUpdate({ target: schema.modulEinstellung.modul, set: werte })
|
||||||
|
.run();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user