From f85bc873241e60c65880e3829205bd730516cbd5 Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Fri, 25 Sep 2026 10:52:44 +0000 Subject: [PATCH] feat(stammdaten): Mitarbeiter, Abteilungen und Geraete (#25) --- CHANGELOG.md | 22 + CLAUDE.md | 5 +- docs/devlog.md | 60 +- .../011-mitarbeiter-abteilungen-geraete.md | 84 ++- docs/issues/README.md | 4 +- docs/screenshots/m6/abteilungen-desktop.png | Bin 0 -> 317379 bytes docs/screenshots/m6/abteilungen-handy.png | Bin 0 -> 146112 bytes .../screenshots/m6/admin-benutzer-desktop.png | Bin 0 -> 181876 bytes docs/screenshots/m6/admin-benutzer-handy.png | Bin 0 -> 126557 bytes .../m6/admin-einstellungen-desktop.png | Bin 0 -> 286083 bytes .../m6/admin-einstellungen-handy.png | Bin 0 -> 139595 bytes .../m6/admin-moduswechsel-desktop.png | Bin 0 -> 351361 bytes .../m6/admin-moduswechsel-handy.png | Bin 0 -> 130259 bytes docs/screenshots/m6/anmelden-desktop.png | Bin 0 -> 44122 bytes docs/screenshots/m6/anmelden-handy.png | Bin 0 -> 31589 bytes docs/screenshots/m6/geraete-liste-desktop.png | Bin 0 -> 403859 bytes docs/screenshots/m6/geraete-liste-handy.png | Bin 0 -> 117523 bytes .../m6/mitarbeiter-liste-desktop.png | Bin 0 -> 204404 bytes .../m6/mitarbeiter-liste-handy.png | Bin 0 -> 104803 bytes .../m6/mitarbeiter-seite-desktop.png | Bin 0 -> 175897 bytes .../m6/mitarbeiter-seite-handy.png | Bin 0 -> 80301 bytes .../m6/mitarbeiter-verzeichnis-desktop.png | Bin 0 -> 195997 bytes .../m6/mitarbeiter-verzeichnis-handy.png | Bin 0 -> 101064 bytes docs/screenshots/m6/uebersicht-desktop.png | Bin 0 -> 106813 bytes docs/screenshots/m6/uebersicht-handy.png | Bin 0 -> 84794 bytes .../m6/vorlagen-editor-desktop.png | Bin 0 -> 522603 bytes docs/screenshots/m6/vorlagen-editor-handy.png | Bin 0 -> 122809 bytes .../screenshots/m6/vorlagen-liste-desktop.png | Bin 0 -> 216303 bytes docs/screenshots/m6/vorlagen-liste-handy.png | Bin 0 -> 121020 bytes .../screenshots/m6/vorlagen-seite-desktop.png | Bin 0 -> 260121 bytes docs/screenshots/m6/vorlagen-seite-handy.png | Bin 0 -> 121148 bytes package.json | 1 + scripts/beispiel-stammdaten.ts | 157 +++++ scripts/screenshots.ts | 27 + src/app/(app)/admin/abteilungen/page.tsx | 126 ++++ src/app/(app)/admin/layout.tsx | 1 + src/app/(app)/geraete/page.tsx | 282 ++++++++ src/app/(app)/mitarbeiter/[id]/page.tsx | 123 ++++ src/app/(app)/mitarbeiter/page.tsx | 143 ++++ src/app/(app)/vorlagen/page.tsx | 4 +- src/components/abteilung-archivieren.tsx | 43 ++ src/components/abteilung-formular.tsx | 125 ++++ src/components/benutzer-anlegen.tsx | 4 +- src/components/einstellungen-formular.tsx | 4 +- src/components/geraet-aktionen.tsx | 108 +++ src/components/geraet-anlegen.tsx | 160 +++++ src/components/geraet-bearbeiten.tsx | 164 +++++ src/components/geraet-importieren.tsx | 128 ++++ src/components/kunde-anlegen.tsx | 4 +- src/components/kunde-bearbeiten.tsx | 4 +- src/components/mitarbeiter-aktiv.tsx | 42 ++ src/components/mitarbeiter-anlegen.tsx | 108 +++ src/components/mitarbeiter-stammdaten.tsx | 153 +++++ src/components/mitarbeiter-suche.tsx | 98 +++ src/components/modus-wechsel.tsx | 4 +- src/components/stammdaten-reiter.tsx | 41 ++ src/components/vorlage-aktionen.tsx | 4 +- src/components/vorlage-anlegen.tsx | 4 +- src/components/vorlage-kopfdaten.tsx | 4 +- src/lib/csv.ts | 444 ++++++++++++ src/lib/formular.ts | 21 + src/lib/geraete-text.ts | 23 + src/lib/navigation.ts | 17 +- src/server/actions/abteilungen.ts | 150 ++++ src/server/actions/geraete.ts | 289 ++++++++ src/server/actions/mitarbeiter.ts | 155 +++++ src/server/auth/rollen.ts | 10 + src/server/queries/abteilungen.ts | 110 +++ src/server/queries/geraete.ts | 220 ++++++ src/server/queries/mitarbeiter.ts | 216 ++++++ src/server/services/abteilungen.ts | 214 ++++++ src/server/services/geraete.ts | 640 ++++++++++++++++++ src/server/services/mitarbeiter.ts | 252 +++++++ tests/abteilungen.test.ts | 268 ++++++++ tests/csv.test.ts | 254 +++++++ tests/e2e/geraete.spec.ts | 198 ++++++ tests/e2e/stammdaten.spec.ts | 133 ++++ tests/geraete.test.ts | 559 +++++++++++++++ tests/mitarbeiter.test.ts | 409 +++++++++++ tests/navigation.test.ts | 13 +- tests/rollen.test.ts | 7 +- 81 files changed, 6784 insertions(+), 59 deletions(-) create mode 100644 docs/screenshots/m6/abteilungen-desktop.png create mode 100644 docs/screenshots/m6/abteilungen-handy.png create mode 100644 docs/screenshots/m6/admin-benutzer-desktop.png create mode 100644 docs/screenshots/m6/admin-benutzer-handy.png create mode 100644 docs/screenshots/m6/admin-einstellungen-desktop.png create mode 100644 docs/screenshots/m6/admin-einstellungen-handy.png create mode 100644 docs/screenshots/m6/admin-moduswechsel-desktop.png create mode 100644 docs/screenshots/m6/admin-moduswechsel-handy.png create mode 100644 docs/screenshots/m6/anmelden-desktop.png create mode 100644 docs/screenshots/m6/anmelden-handy.png create mode 100644 docs/screenshots/m6/geraete-liste-desktop.png create mode 100644 docs/screenshots/m6/geraete-liste-handy.png create mode 100644 docs/screenshots/m6/mitarbeiter-liste-desktop.png create mode 100644 docs/screenshots/m6/mitarbeiter-liste-handy.png create mode 100644 docs/screenshots/m6/mitarbeiter-seite-desktop.png create mode 100644 docs/screenshots/m6/mitarbeiter-seite-handy.png create mode 100644 docs/screenshots/m6/mitarbeiter-verzeichnis-desktop.png create mode 100644 docs/screenshots/m6/mitarbeiter-verzeichnis-handy.png create mode 100644 docs/screenshots/m6/uebersicht-desktop.png create mode 100644 docs/screenshots/m6/uebersicht-handy.png create mode 100644 docs/screenshots/m6/vorlagen-editor-desktop.png create mode 100644 docs/screenshots/m6/vorlagen-editor-handy.png create mode 100644 docs/screenshots/m6/vorlagen-liste-desktop.png create mode 100644 docs/screenshots/m6/vorlagen-liste-handy.png create mode 100644 docs/screenshots/m6/vorlagen-seite-desktop.png create mode 100644 docs/screenshots/m6/vorlagen-seite-handy.png create mode 100644 scripts/beispiel-stammdaten.ts create mode 100644 src/app/(app)/admin/abteilungen/page.tsx create mode 100644 src/app/(app)/geraete/page.tsx create mode 100644 src/app/(app)/mitarbeiter/[id]/page.tsx create mode 100644 src/app/(app)/mitarbeiter/page.tsx create mode 100644 src/components/abteilung-archivieren.tsx create mode 100644 src/components/abteilung-formular.tsx create mode 100644 src/components/geraet-aktionen.tsx create mode 100644 src/components/geraet-anlegen.tsx create mode 100644 src/components/geraet-bearbeiten.tsx create mode 100644 src/components/geraet-importieren.tsx create mode 100644 src/components/mitarbeiter-aktiv.tsx create mode 100644 src/components/mitarbeiter-anlegen.tsx create mode 100644 src/components/mitarbeiter-stammdaten.tsx create mode 100644 src/components/mitarbeiter-suche.tsx create mode 100644 src/components/stammdaten-reiter.tsx create mode 100644 src/lib/csv.ts create mode 100644 src/lib/formular.ts create mode 100644 src/lib/geraete-text.ts create mode 100644 src/server/actions/abteilungen.ts create mode 100644 src/server/actions/geraete.ts create mode 100644 src/server/actions/mitarbeiter.ts create mode 100644 src/server/queries/abteilungen.ts create mode 100644 src/server/queries/geraete.ts create mode 100644 src/server/queries/mitarbeiter.ts create mode 100644 src/server/services/abteilungen.ts create mode 100644 src/server/services/geraete.ts create mode 100644 src/server/services/mitarbeiter.ts create mode 100644 tests/abteilungen.test.ts create mode 100644 tests/csv.test.ts create mode 100644 tests/e2e/geraete.spec.ts create mode 100644 tests/e2e/stammdaten.spec.ts create mode 100644 tests/geraete.test.ts create mode 100644 tests/mitarbeiter.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 8dd4a8b..bd3a21c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,20 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/). - Vorschau der Vorlage mit Beispielwerten, so wie ein Lauf sie zeigen wird (#010) - Versionsgeschichte je Vorlage: wer wann mit welcher Notiz veröffentlicht hat und wie viele Läufe an einer Version hängen (#010) +- Mitarbeiterliste mit Suche über Anzeigename, Mail, UPN und sAMAccountName, Filter nach Abteilung; + standardmäßig nur aktive Personen, mit Umschalter auch deaktivierte (#011) +- Mitarbeiterseite mit Stammdaten, zugeordneten Geräten und Platz für die Laufhistorie; Personen + werden deaktiviert statt gelöscht (#011) +- Abteilungen unter Admin: Ziel-OU, Standardgruppen und Mail-Domain — daraus entstehen später die + Befehle, die ein Konto anlegen. Abteilungsnamen, die bei Personen vorkommen, aber nicht gepflegt + sind, werden eigens ausgewiesen (#011) +- Geräteliste mit Suche über Hostname, Bezeichnung, Seriennummer und Tag-Code, Filter nach Typ; + Geräte anlegen, bearbeiten, archivieren (#011) +- CSV-Import für Geräte: je Zeile ein Ergebnis, eine fehlerhafte Zeile bricht den Import nicht ab, + sondern wird mit Zeilennummer und Grund gemeldet. Der Zerleger kommt mit Semikolon, BOM, CRLF, + Anführungszeichen und Zeilenumbrüchen im Wert zurecht (#011) +- Die Seitenleiste führt Personen und Geräte unter einem Eintrag; ein Umschalter wechselt zwischen + beiden Listen (#011) ### Sicherheit @@ -118,3 +132,11 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/). im Editor wäre sonst jede ungespeicherte Arbeit verloren gewesen (#010) - Eine archivierte Kundenzuordnung bleibt beim Speichern der Kopfdaten erhalten, statt sich stillschweigend in „für alle Kunden“ zu verwandeln (#010) +- Die Stammdaten einer Person aus dem Verzeichnis lassen sich nicht von Hand überschreiben — der + nächste Abgleich würde die Änderung ohnehin zurückdrehen, und ein Wert, der zwischen zwei Läufen + hin- und herspringt, ist schlimmer als ein falscher. Deaktivieren bleibt möglich (#011) +- Ein Gerät kann nur einer Person desselben Kunden gehören; der Dienst sagt es verständlich, bevor + der zusammengesetzte Fremdschlüssel zuschlägt (#011) +- Ein Gerät, an dem Läufe hängen, lässt sich nicht löschen, nur archivieren (#011) +- Der Tag-Code entsteht beim Anlegen, steht sofort im Vergabebuch und lässt sich nie ändern — beides + in einer Transaktion, damit kein Code ohne Eintrag entsteht (#011) diff --git a/CLAUDE.md b/CLAUDE.md index d6cd838..9cf6200 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -87,7 +87,10 @@ Dreizeiler: Recht prüfen, Zod-Schema anwenden, Service rufen. bleiben. - **`tagCode`:** wird nie geändert und nie wiederverwendet, auch nicht nach Archivierung. - **Löschen:** Geräte, Abteilungen und Vorlagen werden archiviert. Mitarbeiter werden nur - `aktiv = false`. Nichts wird gelöscht, woran ein Lauf hängt. + `aktiv = false`. Nichts wird gelöscht, woran ein Lauf hängt. Genau **eine** Ausnahme gibt es: ein + Gerät ohne Läufe lässt sich endgültig löschen, weil der Vertipper von eben sonst für immer in der + Liste stünde. Das verlangt `verwaltung` — der Tag-Code ist danach für immer verbrannt, und der + gedruckte Aufkleber wertlos. - **`{{generiertes_passwort}}`:** wird je Anzeige neu erzeugt und **nie** gespeichert — nicht in `Schrittergebnis`, `Kommentar`, `Protokoll` oder der Druckansicht. - **Erweiterbarkeit:** Kein Codepfad nimmt an, dass ein Schritt nur von einem Menschen erledigt diff --git a/docs/devlog.md b/docs/devlog.md index 707f005..d7d86c6 100644 --- a/docs/devlog.md +++ b/docs/devlog.md @@ -13,11 +13,12 @@ als Unterpunkte mit Aufgabe und Dauer. | #008 | M3 – Betriebsmodus und Kunden | 1 h 20 min | | #009 | M4 – Vorlagen-Versionierung im Service | 1 h 20 min | | #010 | M5 – Vorlagen-Editor | 2 h 10 min | -| | **Summe** | **9 h** | +| #011 | M6 – Mitarbeiter, Abteilungen, Geräte | 2 h 15 min | +| | **Summe** | **11 h 15 min** | | Meilenstein | Tatsächlich | Status | |---|---|---| -| v0.1.0 | 9 h | in Arbeit | +| v0.1.0 | 11 h 15 min | in Arbeit | --- @@ -396,3 +397,58 @@ als Unterpunkte mit Aufgabe und Dauer. - #011 Mitarbeiter, Abteilungen und Geräte: Listen mit Suche, Mitarbeiterseite mit Historie, Abteilungen mit Ziel-OU und Standardgruppen, Geräte mit CSV-Import. + +--- + +## 2026-09-25 · Sitzung 7 · 10:30–12:55 + +**Issue:** #011 (M6) +**Version:** 0.1.0 + +### Erledigt + +- Mitarbeiter: Dienst mit Sperre für abgeglichene Stammdaten, Liste mit Suche über vier Kennungen, + Personenseite mit Geräten, Vorgesetztem und Platz für die Laufhistorie +- Abteilungen unter Admin: Ziel-OU, Standardgruppen, Mail-Domain, dazu die Liste der + Abteilungsnamen, die bei Personen vorkommen, aber nicht gepflegt sind +- Geräte samt CSV-Import (Sub-Agent): eigener Zerleger in `src/lib/csv.ts`, Tag-Code-Vergabe in + einer Transaktion, Archivieren statt Löschen +- Neues Recht `mitarbeiterBearbeiten`; die Seitenleiste führt Personen und Geräte unter einem + Eintrag, im Modus Dienstleistung anders beschriftet +- Unabhängiger Review: zwei Blocker, sieben weitere Befunde, alle abgearbeitet +- **610 Tests** grün (vorher 601 vor dem Review), dazu 18 E2E-Abläufe + +### Entscheidungen + +- **Ein CSV-Wert reicht nicht über die Zeile hinaus.** Das Format erlaubt es, und `csvErzeugen` + schreibt es auch so — beim **Lesen** einer fremden Datei ist es trotzdem die schlechtere Annahme: + ein Umbruch in einem Hostnamen kommt nicht vor, ein vergessenes schließendes Anführungszeichen + ständig. Mit der Grenze endet der Schaden an der Zeile, in der er beginnt; ohne sie verschluckt + eine kaputte Zelle alle Folgezeilen, und der Import meldet dafür Erfolg. +- **Eine abgeglichene Person ist vollständig gesperrt**, nicht nur teilweise. Alle Stammdaten + kommen aus dem Verzeichnis, auch der Titel. Übrig bleibt genau ein Weg: aktivieren und + deaktivieren — und der muss bleiben, weil das Offboarding nicht auf den nächsten Abgleich wartet. +- **Löschen eines Geräts verlangt `verwaltung`.** Archivieren ist der Normalfall. Löschen ist nicht + rücknehmbar und verbrennt den Tag-Code für immer — der gedruckte Aufkleber ist danach wertlos. +- **Die Lesemodelle liefern den Kundennamen immer, die Seite entscheidet über die Anzeige.** + Vorher entschied das eine Lesemodell nach `istEigeneOrganisation` und das andere gar nicht. +- **Die Mitarbeitersuche ist ein eigenes Bauteil**, bevor sie zum zweiten Mal gebraucht wird: #013 + und #014 brauchen sie wieder, und dreimal abgeschrieben vergisst man den Umschalter „auch + deaktivierte“ zweimal. + +### Probleme + +- **Der schwerste Fehler war wieder einer, der Erfolg meldet.** Der CSV-Import hat Zeilen + verschluckt und „3 von 3 angelegt“ gesagt. Gefunden hat ihn der Review mit einer Datei, wie sie + aus einem halb ausgefüllten Excel kommt — meine eigenen Tests prüften nur saubere Dateien und die + eine Ausnahme, die ohnehin geworfen wurde. +- **Zwei Funktionen im selben Modul widersprachen sich**: `abteilungsListe` schlüsselte nach Kunde + und Name, `ungepflegteAbteilungen` nach Namen allein. Nebeneinander geschrieben, nicht + nebeneinander gelesen. +- **Vierter Meilenstein in Folge mit einem Kommentar, der eine Zusage macht, die der Code nicht + hält.** Diesmal versprach er zwei verschiedene Meldungen, gerendert wurde eine. + +### Nächster Schritt + +- #012 Platzhalter, Namensregeln und Escaping: die reine Service-Schicht, aus der die fertigen + PowerShell-Befehle entstehen — mit dichter Testabdeckung, denn hier darf nichts schiefgehen. diff --git a/docs/issues/011-mitarbeiter-abteilungen-geraete.md b/docs/issues/011-mitarbeiter-abteilungen-geraete.md index 5f943b7..2bf951b 100644 --- a/docs/issues/011-mitarbeiter-abteilungen-geraete.md +++ b/docs/issues/011-mitarbeiter-abteilungen-geraete.md @@ -1,9 +1,9 @@ # #011 M6 – Mitarbeiter, Abteilungen, Geräte -**Status:** offen +**Status:** erledigt **Meilenstein:** v0.1.0 **Labels:** phase-1, backend, frontend -**Geschätzt:** 9 h **Tatsächlich:** – +**Geschätzt:** 9 h **Tatsächlich:** 2 h 15 min ## Ziel @@ -11,23 +11,69 @@ Die Stammdaten, an denen die Historie hängt. Mitarbeiter und Geräte werden nie ## Aufgaben -- [ ] Mitarbeiterliste: Suche über Anzeigename, Mail, UPN und `sam`, Filter nach Abteilung und Aktiv-Status -- [ ] Mitarbeitersuche als wiederverwendbare Komponente: standardmäßig nur aktive, Umschalter „auch deaktivierte“ fürs Offboarding -- [ ] Mitarbeiterseite: Stammdaten, Abteilung, Vorgesetzter, seine Geräte, alle Läufe chronologisch (Onboarding, Übergaben, Offboarding) -- [ ] Mitarbeiter manuell anlegen und bearbeiten (`herkunft = MANUELL`); synchronisierte Felder sind bei `herkunft = ABGLEICH` gesperrt -- [ ] Deaktivieren statt löschen (`aktiv = false`) -- [ ] Abteilungen: Name (eindeutig je Kunde), Ziel-OU, Standardgruppen (`String[]`), Mail-Domain, Notizen -- [ ] Geräte: Stammdaten nach Vorgabe, Pflichtregel „`hostname` oder `bezeichnung` muss gesetzt sein“, Besitzer optional -- [ ] Geräteliste mit Suche über Hostname, Bezeichnung, Seriennummer und `tagCode`, Filter nach Typ -- [ ] CSV-Import für Geräte (`hostname`, `label`, `serialNumber`, `inventoryNumber`, `type`, `model`), `tagCode` wird beim Import erzeugt; eigener `src/lib/csv.ts` -- [ ] Archivieren statt Löschen, sobald Läufe an einem Gerät oder einer Abteilung hängen -- [ ] Screenshots Mitarbeiterliste, Mitarbeiterseite, Geräteliste, Abteilungen in `docs/screenshots/m6/` +- [x] Mitarbeiterliste: Suche über Anzeigename, Mail, UPN und `sam`, Filter nach Abteilung und Aktiv-Status +- [x] Mitarbeitersuche als wiederverwendbare Komponente: standardmäßig nur aktive, Umschalter „auch deaktivierte“ fürs Offboarding +- [x] Mitarbeiterseite: Stammdaten, Abteilung, Vorgesetzter, seine Geräte, alle Läufe chronologisch (Onboarding, Übergaben, Offboarding) +- [x] Mitarbeiter manuell anlegen und bearbeiten (`herkunft = MANUELL`); synchronisierte Felder sind bei `herkunft = ABGLEICH` gesperrt +- [x] Deaktivieren statt löschen (`aktiv = false`) +- [x] Abteilungen: Name (eindeutig je Kunde), Ziel-OU, Standardgruppen (`String[]`), Mail-Domain, Notizen +- [x] Geräte: Stammdaten nach Vorgabe, Pflichtregel „`hostname` oder `bezeichnung` muss gesetzt sein“, Besitzer optional +- [x] Geräteliste mit Suche über Hostname, Bezeichnung, Seriennummer und `tagCode`, Filter nach Typ +- [x] CSV-Import für Geräte (`hostname`, `label`, `serialNumber`, `inventoryNumber`, `type`, `model`), `tagCode` wird beim Import erzeugt; eigener `src/lib/csv.ts` +- [x] Archivieren statt Löschen, sobald Läufe an einem Gerät oder einer Abteilung hängen +- [x] Screenshots Mitarbeiterliste, Mitarbeiterseite, Geräteliste, Abteilungen in `docs/screenshots/m6/` ## Akzeptanzkriterien -- [ ] Test: Mitarbeitersuche liefert standardmäßig nur `aktiv = true`; mit Umschalter auch deaktivierte -- [ ] Test: ein Gerät ohne `hostname` und ohne `bezeichnung` wird abgelehnt -- [ ] Test: ein Gerät mit Läufen lässt sich nicht löschen, nur archivieren -- [ ] Test: `tagCode` eines bestehenden Geräts lässt sich nicht ändern -- [ ] Test: CSV-Import erzeugt je Zeile einen `tagCode`, meldet fehlerhafte Zeilen einzeln und bricht nicht ab -- [ ] Test: bei `herkunft = ABGLEICH` lehnt der Service Änderungen an synchronisierten Feldern ab +- [x] Test: Mitarbeitersuche liefert standardmäßig nur `aktiv = true`; mit Umschalter auch deaktivierte +- [x] Test: ein Gerät ohne `hostname` und ohne `bezeichnung` wird abgelehnt +- [x] Test: ein Gerät mit Läufen lässt sich nicht löschen, nur archivieren +- [x] Test: `tagCode` eines bestehenden Geräts lässt sich nicht ändern +- [x] Test: CSV-Import erzeugt je Zeile einen `tagCode`, meldet fehlerhafte Zeilen einzeln und bricht nicht ab +- [x] Test: bei `herkunft = ABGLEICH` lehnt der Service Änderungen an synchronisierten Feldern ab + +## Beim Bauen aufgefallen + +- **Der CSV-Import hat Zeilen still verloren und Erfolg gemeldet.** Ein Anführungszeichen, das in + einer Zelle beginnt und nicht schließt, verschluckte alle Folgezeilen bis zum nächsten `"` — der + Import sagte „3 von 3 Zeilen angelegt“, zwei Geräte fehlten, und in einer Bezeichnung stand der + Text der verschluckten Zeilen. Umgekehrt starb der ganze Import an einer Datei, in der nirgends + mehr ein `"` folgte. Beides ist genau das Gegenteil des Akzeptanzkriteriums. Der Zerleger setzt + jetzt am Zeilenende neu auf: **ein Wert reicht standardmäßig nicht über die Zeile hinaus**. Das + Format erlaubt es, für Hostnamen und Seriennummern ist ein Umbruch aber praktisch immer ein + vergessenes Anführungszeichen — und mit dieser Grenze endet der Schaden an der Zeile, in der er + beginnt. Wer eine selbst erzeugte Datei zurückliest, schaltet `werteUeberZeilen` ein. +- **`ungepflegteAbteilungen` kannte die Kundengrenze nicht.** Gruppiert wurde nach dem Namen + allein, also galt „Vertrieb“ als gepflegt, sobald **irgendein** Kunde eine Abteilung dieses + Namens hatte. Beim Kunden ohne sie blieben `{{ou}}` und `{{gruppen}}` im Lauf leer, ohne dass die + Seite es je sagte. Die Funktion direkt darüber machte es seit jeher richtig — zwei Funktionen im + selben Modul, die sich widersprachen. +- **Die Besitzerauswahl bot Personen fremder Kunden an.** Die Liste wurde einmal für die ganze + Seite geholt und in jede Karte gegeben. Der Dienst wies die Wahl ab — aber erst nach dem Klick, + und die Oberfläche hatte etwas angeboten, das sie nicht einlösen konnte. +- **Ein Techniker konnte ein Gerät endgültig löschen.** Abteilungen und Mitarbeiter haben gar + keinen Löschweg, Geräte schon — und der Tag-Code ist danach für immer verbrannt. Löschen verlangt + jetzt `verwaltung`; die Ausnahme steht ausdrücklich in `CLAUDE.md`. +- **Derselbe Kunde wurde zweimal verschieden dargestellt.** Die Mitarbeiterliste blendete die + eigene Organisation immer aus, die Geräteliste zeigte sie immer an. Im Modus Dienstleistung hieß + „keine Kundenangabe“ damit nicht „kein Kunde“, sondern „ausgerechnet dieser eine“. Jetzt liefern + beide Lesemodelle den Namen, und **die Seite** entscheidet anhand des Betriebsmodus, ob sie ihn + zeigt. +- **Ein Kommentar hat wieder eine Zusage gemacht, die der Code nicht hielt** („zwei verschiedene + Gründe, aber nicht dieselbe Meldung“) — gerendert wurde nur eine, und wer die Rolle nicht hatte, + sah gesperrte Felder ohne jede Erklärung. Vierter Meilenstein in Folge mit genau dieser Sorte + Fehler. +- **`FELD` stand neunmal wortgleich in neun Dateien.** Jetzt einmal in `src/lib/formular.ts`. +- **Die E2E-Tests haben ihre Daten liegen gelassen.** Sie können nichts löschen — die Anwendung + kennt bewusst keinen Löschweg für Personen und Abteilungen —, also räumen sie am Ende über die + Datenbank auf. Ohne das sähe jede Datenbank, gegen die sie laufen, nach ein paar Durchläufen aus + wie ein Messie-Keller. +- **Die Mitarbeitersuche ist ein eigenes Bauteil** (`src/components/mitarbeiter-suche.tsx`), weil + #013 sie beim Starten eines Laufs und #014 beim Zuweisen eines Geräts wieder braucht. + +## Offen geblieben + +- **Ab 501 aktiven Personen** lässt sich ein Besitzer nicht mehr über das Auswahlfeld zuordnen. Die + Grenze schützt davor, ein Verzeichnis mit zehntausend Personen in ein ` + + + + + {filter.suche || typ || filter.auchArchivierte ? ( + + zurücksetzen + + ) : null} + + + {zeilen.length === 0 ? ( +

+ Kein Gerät passt zu diesen Filtern. +

+ ) : ( + + )} + + {darfBearbeiten ? ( +
+
+

+

+

+ Der Tag-Code entsteht beim Anlegen und steht danach in der Meldung — er kommt auf den Aufkleber. +

+ +
+ +
+ + Geräte aus einer CSV-Datei einlesen + +
+ namen[0])} + kunden={kundenWahl} + /> +
+
+
+ ) : null} + + ); +} diff --git a/src/app/(app)/mitarbeiter/[id]/page.tsx b/src/app/(app)/mitarbeiter/[id]/page.tsx new file mode 100644 index 0000000..a5129d1 --- /dev/null +++ b/src/app/(app)/mitarbeiter/[id]/page.tsx @@ -0,0 +1,123 @@ +import Link from "next/link"; +import { notFound } from "next/navigation"; +import { ArrowLeft, Laptop } from "lucide-react"; +import { MitarbeiterAktiv } from "@/components/mitarbeiter-aktiv"; +import { MitarbeiterStammdaten } from "@/components/mitarbeiter-stammdaten"; +import { Herkunft } from "@/generated/prisma/enums"; +import { darf } from "@/server/auth/rollen"; +import { sitzungErforderlich } from "@/server/auth/sitzungspflicht"; +import { kundenEbeneSichtbar } from "@/server/mandant"; +import { geraeteDesMitarbeiters, mitarbeiterHolen, vorgesetzterVon } from "@/server/queries/mitarbeiter"; + +/** + * Die Seite einer Person (#011). + * + * Sie beantwortet drei Fragen: wer ist das, welche Geraete gehoeren ihr, und was + * ist mit ihr passiert. Die Laufhistorie kommt mit #013 dazu — bis dahin steht + * hier, dass sie kommt, statt eine leere Ueberschrift zu zeigen. + */ + +export default async function MitarbeiterSeite({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params; + const { benutzer } = await sitzungErforderlich("lesen", `/mitarbeiter/${id}`); + const person = await mitarbeiterHolen(id, benutzer); + // Nicht vorhanden und nicht sichtbar sind dieselbe Antwort – die + // Unterscheidung waere schon eine Auskunft ueber fremde Kunden (#009). + if (!person) notFound(); + + const darfBearbeiten = darf(benutzer.rolle, "mitarbeiterBearbeiten"); + const [geraete, kundenEbene, vorgesetzter] = await Promise.all([ + geraeteDesMitarbeiters(person.id, benutzer), + kundenEbeneSichtbar(), + vorgesetzterVon(person.id, benutzer), + ]); + + return ( + <> + +