Files
Laufzettel/docs/issues/011-mitarbeiter-abteilungen-geraete.md
florian.wilde f85bc87324
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s
feat(stammdaten): Mitarbeiter, Abteilungen und Geraete (#25)
2026-09-25 10:52:44 +00:00

80 lines
5.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# #011 M6 – Mitarbeiter, Abteilungen, Geräte
**Status:** erledigt
**Meilenstein:** v0.1.0
**Labels:** phase-1, backend, frontend
**Geschätzt:** 9 h **Tatsächlich:** 2 h 15 min
## Ziel
Die Stammdaten, an denen die Historie hängt. Mitarbeiter und Geräte werden nie gelöscht, damit archivierte Läufe für immer auf sie zeigen können.
## Aufgaben
- [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
- [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.