feat(quick-260909-ab3): verstaendlicher Sync-Bericht, Konten ohne Adresse in der UI

- admin/ldap/page.tsx: SyncResult um emailConflicts/skippedNoLogin/
  entryFailures erweitert; drei neue Berichtsabschnitte (Bernstein fuer
  Kollisionen, neutral fuer fehlenden Anmeldenamen, destruktiv fuer
  unerwartete Fehler) — kein englischer Techniktext, kein roher
  Datenbank-Wortlaut mehr im Bericht
- GroupMembersModal.tsx: TenantUser.email optional, Suchvergleich gegen
  leere Adresse abgesichert (WINDOWS #15) — die Mitgliedersuche stuerzt
  nicht mehr ab, sobald ein Konto ohne Adresse existiert
- users/page.tsx: User.email optional, Formular faellt auf leere
  Zeichenkette zurueck, Tabellenzelle zeigt einen Gedankenstrich;
  UserFormData.email bleibt bei Handanlage Pflicht
- Neue Sprachschluessel unter admin.ldap.sync in de.json/en.json,
  Umlaut- und Sprachschluessel-Gate bestaetigt gruen (kein neues
  Allowlist-Wort noetig)
- Neuer Testfall in groups-page.test.tsx vorab gegen den unveraenderten
  Bestand rot gelaufen; Abweichung von der Plan-Fixture dokumentiert
  (Kurzfassung: ein einzelnes Konto mit passendem Benutzernamen loest die
  Kollision wegen OR-Kurzschlussauswertung nie aus, ein zweites,
  nicht-treffendes Konto ohne Adresse schon — Vollfassung im SUMMARY)
- 651/651 API- und 233/233 Web-Tests gruen, beide Typpruefungen sauber

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
This commit is contained in:
2026-09-09 07:54:32 +02:00
parent 1222951af6
commit 21670462dd
6 changed files with 99 additions and 6 deletions
@@ -72,6 +72,13 @@ interface SyncResult {
groupsRenamed: number;
groupsDeleted: number;
defaultMarkerMoved: number;
// WINDOWS #15: Konten, die wegen einer bereits vergebenen Adresse ohne
// diese Adresse angelegt/aktualisiert wurden; Kennungen ohne
// Anmeldenamen (normaler Vorgang, kein Fehler); Kennungen mit einem
// unerwarteten Fehler (technischer Wortlaut bleibt im Serverprotokoll).
emailConflicts: { account: string; email: string }[];
skippedNoLogin: string[];
entryFailures: string[];
errors: string[];
}
@@ -1338,6 +1345,45 @@ export default function AdminLdapPage() {
})}
</p>
)}
{syncResult.emailConflicts.length > 0 && (
<div className="mt-2 space-y-1">
<p className="text-sm font-medium text-amber-700 dark:text-amber-400">
{t('sync.emailConflictsHeading')}
</p>
{syncResult.emailConflicts.map((conflict, i) => (
<p key={i} className="text-xs text-amber-700 dark:text-amber-400">
{t('sync.emailConflictLine', {
account: conflict.account,
email: conflict.email,
})}
</p>
))}
</div>
)}
{syncResult.skippedNoLogin.length > 0 && (
<div className="mt-2 space-y-1">
<p className="text-sm font-medium text-muted-foreground">
{t('sync.skippedNoLoginHeading')}
</p>
{syncResult.skippedNoLogin.map((entry, i) => (
<p key={i} className="text-xs text-muted-foreground">
{entry}
</p>
))}
</div>
)}
{syncResult.entryFailures.length > 0 && (
<div className="mt-2 space-y-1">
<p className="text-sm font-medium text-destructive">
{t('sync.entryFailuresHeading')}
</p>
{syncResult.entryFailures.map((entry, i) => (
<p key={i} className="text-xs text-destructive">
{entry}
</p>
))}
</div>
)}
{syncResult.errors.length > 0 && (
<div className="mt-2 space-y-1">
{syncResult.errors.map((err, i) => (