feat(stammdaten): Mitarbeiter, Abteilungen und Geraete
Prüfung / pruefung (pull_request) Canceled after 0s
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (pull_request) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s

Die Stammdaten, an denen die Historie haengt. Personen werden deaktiviert,
Geraete und Abteilungen archiviert — geloescht wird nichts, woran ein Lauf
haengen kann.

- Mitarbeiterliste mit Suche ueber Anzeigename, Mail, UPN und sAMAccountName,
  Filter nach Abteilung, standardmaessig nur aktive; die Suche ist ein eigenes
  Bauteil, weil #013 und #014 sie wieder brauchen
- Personenseite mit Stammdaten, Geraeten, Vorgesetztem; was aus dem Verzeichnis
  kommt, ist gesperrt — der naechste Abgleich wuerde es ohnehin zurueckdrehen
- Abteilungen unter Admin: Ziel-OU, Standardgruppen, Mail-Domain, dazu die
  Namen, die bei Personen vorkommen, aber nicht gepflegt sind
- Geraete mit Suche ueber Hostname, Bezeichnung, Seriennummer und Tag-Code;
  CSV-Import mit einem Ergebnis je Zeile
- Neues Recht mitarbeiterBearbeiten; die Seitenleiste fuehrt Personen und
  Geraete unter einem Eintrag, im Modus Dienstleistung anders beschriftet

Der unabhaengige Review fand zwei Blocker. Der schwerere meldete Erfolg: ein
nicht geschlossenes Anfuehrungszeichen verschluckte im CSV-Import alle
Folgezeilen bis zum naechsten `"`, und der Import sagte „3 von 3 Zeilen
angelegt". Ein Wert reicht jetzt nicht mehr ueber die Zeile hinaus — fuer
Hostnamen und Seriennummern ist ein Umbruch praktisch immer ein vergessenes
Anfuehrungszeichen, und so endet der Schaden an seiner Zeile. Der zweite:
ungepflegte Abteilungen wurden ueber alle Kunden hinweg gruppiert.

Dazu sieben weitere Befunde, alle behoben — darunter eine Besitzerauswahl, die
Personen fremder Kunden anbot, und ein Techniker, der ein Geraet endgueltig
loeschen konnte.

610 Tests gruen, 18 E2E-Ablaeufe gegen den gebauten Stand.

Schliesst #011

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-09-25 12:51:54 +02:00
co-authored by Claude Opus 5
parent e3d80e5982
commit 86d55917f9
81 changed files with 6784 additions and 59 deletions
+58 -2
View File
@@ -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.
@@ -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 `<select>` zu gießen; die
Zuweisung über die Geräteseite mit Suche kommt mit #014. Bis dahin sagt das Formular es
ausdrücklich, statt einen Weg zu versprechen, den es nicht gibt.
+2 -2
View File
@@ -15,7 +15,7 @@ Diese Dateien sind die Quelle; `pnpm issues:sync` spiegelt sie in den Gitea-Trac
| #008 | [M3 – Betriebsmodus und Kunden](008-betriebsmodus-kunden.md) | erledigt | v0.1.0 | 6 h | 1 h 20 min |
| #009 | [M4 – Vorlagen-Versionierung im Service](009-vorlagen-versionierung.md) | erledigt | v0.1.0 | 6 h | 1 h 20 min |
| #010 | [M5 – Vorlagen-Editor](010-vorlagen-editor.md) | erledigt | v0.1.0 | 10 h | 2 h 10 min |
| #011 | [M6 – Mitarbeiter, Abteilungen, Geräte](011-mitarbeiter-abteilungen-geraete.md) | offen | v0.1.0 | 9 h | – |
| #011 | [M6 – Mitarbeiter, Abteilungen, Geräte](011-mitarbeiter-abteilungen-geraete.md) | erledigt | v0.1.0 | 9 h | 2 h 15 min |
| #012 | [M7 – Platzhalter, Namensregeln, Escaping](012-platzhalter-namensregeln.md) | offen | v0.1.0 | 8 h | – |
| #013 | [M8 – Läufe: Service und Abarbeitung](013-laeufe-abarbeitung.md) | offen | v0.1.0 | 14 h | – |
| #014 | [M9 – QR-Tags, Geräteseite, Etiketten, Scanner](014-qr-tags-geraeteseite.md) | offen | v0.1.0 | 12 h | – |
@@ -24,4 +24,4 @@ Diese Dateien sind die Quelle; `pnpm issues:sync` spiegelt sie in den Gitea-Trac
| #017 | [M12 – Seed der sieben Vorlagen](017-seed-sieben-vorlagen.md) | offen | v0.1.0 | 8 h | – |
| #018 | [M13 – E2E-Tests, Anleitung, PDF](018-e2e-anleitung-pdf.md) | offen | v0.1.0 | 14 h | – |
| #019 | [M14 – Betriebswege und Release v0.1.0](019-betrieb-release.md) | offen | v0.1.0 | 10 h | – |
| | **Summe** | | | **136 h** | **9 h** |
| | **Summe** | | | **136 h** | **11 h 15 min** |
Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB