From 6e77bf43b0a412e05436776f7d42b6eef906347e Mon Sep 17 00:00:00 2001 From: Thomas Reitz Date: Wed, 11 Mar 2026 10:48:32 +0100 Subject: [PATCH] =?UTF-8?q?feat(crm):=20prevent=20duplicate=20Lexware=20im?= =?UTF-8?q?ports=20=E2=80=94=20show=20linked=20status=20in=20import=20list?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Import tab now loads all CRM companies/contacts and cross-references lexwareContactId to detect already-imported entries. Linked contacts show a green badge and "Öffnen" link instead of import buttons. Co-Authored-By: Claude Opus 4.6 --- docs/INSIGHT-CRM.md | 39 +++++ .../crm/lexware/LexwareSyncPage.module.css | 79 ++++++++++ .../src/crm/lexware/LexwareSyncPage.tsx | 135 ++++++++++++------ 3 files changed, 210 insertions(+), 43 deletions(-) diff --git a/docs/INSIGHT-CRM.md b/docs/INSIGHT-CRM.md index 4be9351..7312a34 100644 --- a/docs/INSIGHT-CRM.md +++ b/docs/INSIGHT-CRM.md @@ -1013,4 +1013,43 @@ In `lexware-contacts.service.ts` Zeile 229 wird `tenantId` als **TypeScript-Typ --- +## 2026-03-11 | Backend: Fix — Lexware Import 500 (fehlende tenantId) + +### Ursache + +Der `TenantGuard` liess `PLATFORM_ADMIN`-User ohne `tenantId`-Pruefung durch: + +```typescript +// ALT (fehlerhaft): +if (user?.role === 'PLATFORM_ADMIN') { + return true; // ← Kein tenantId-Check! +} +``` + +Wenn ein User mit Rolle `PLATFORM_ADMIN` keiner Tenant-Membership zugeordnet war (oder die Membership inaktiv), fehlte `tenantId` im JWT. Der Controller uebergab dann `user.tenantId!` = `undefined` an den Service, was zum Prisma-Validierungsfehler fuehrte. + +### Fixes + +**1. TenantGuard (`src/auth/guards/tenant.guard.ts`):** +- ALLE User (auch PLATFORM_ADMIN) muessen jetzt eine `tenantId` haben, um auf CRM-Ressourcen zuzugreifen +- Klare Fehlermeldung: "Kein Mandant zugeordnet. Bitte mit einem mandanten-gebundenen Account anmelden." + +**2. Defensive Pruefung in Lexware-Service (`src/lexware/lexware-contacts.service.ts`):** +- `importAsCompany()` und `importAsContact()` pruefen zusaetzlich `if (!tenantId)` und werfen `BadRequestException` mit klarer Meldung + +### Betroffene Dateien + +| Datei | Aenderung | +|-------|-----------| +| `src/auth/guards/tenant.guard.ts` | PLATFORM_ADMIN Bypass entfernt, tenantId immer required | +| `src/lexware/lexware-contacts.service.ts` | Defensive tenantId-Pruefung in Import-Methoden | + +### Auswirkung + +- PLATFORM_ADMIN ohne Tenant-Zuordnung bekommt jetzt **403 Forbidden** statt **500 Internal Server Error** +- Alle anderen User sind nicht betroffen (hatten vorher schon den tenantId-Check) +- TypeScript-Check: 0 Fehler + +--- + *Bitte neue Eintraege unten anfuegen. Format: `## YYYY-MM-DD | Absender: Betreff`* diff --git a/packages/frontend/src/crm/lexware/LexwareSyncPage.module.css b/packages/frontend/src/crm/lexware/LexwareSyncPage.module.css index 9882fd3..e93df44 100644 --- a/packages/frontend/src/crm/lexware/LexwareSyncPage.module.css +++ b/packages/frontend/src/crm/lexware/LexwareSyncPage.module.css @@ -579,3 +579,82 @@ font-size: 0.875rem; color: var(--color-text-muted); } + +/* Linked (already imported) indicator */ +.resultCardLinked { + background: rgba(5, 150, 105, 0.04); + border-radius: var(--radius-sm); + padding-left: 0.75rem; + padding-right: 0.75rem; +} + +:global([data-theme='dark']) .resultCardLinked { + background: rgba(5, 150, 105, 0.08); +} + +.linkedBadge { + display: inline-flex; + align-items: center; + gap: 0.25rem; + margin-left: 0.5rem; + padding: 0.0625rem 0.5rem; + border-radius: 9999px; + background: #d1fae5; + color: #065f46; + font-size: 0.6875rem; + font-weight: 600; + text-decoration: none; + white-space: nowrap; + vertical-align: middle; + transition: background 0.15s; +} + +.linkedBadge:hover { + background: #a7f3d0; + color: #064e3b; +} + +:global([data-theme='dark']) .linkedBadge { + background: #064e3b; + color: #a7f3d0; +} + +:global([data-theme='dark']) .linkedBadge:hover { + background: #065f46; + color: #d1fae5; +} + +.importBtnLinked { + display: inline-flex; + align-items: center; + gap: 0.375rem; + padding: 0.375rem 0.75rem; + border: 1px solid #a7f3d0; + border-radius: var(--radius-sm); + background: #ecfdf5; + color: #059669; + font-size: 0.8125rem; + font-weight: 500; + text-decoration: none; + cursor: pointer; + transition: all 0.15s; + white-space: nowrap; +} + +.importBtnLinked:hover { + background: #d1fae5; + border-color: #059669; + color: #047857; +} + +:global([data-theme='dark']) .importBtnLinked { + background: #064e3b; + border-color: #065f46; + color: #6ee7b7; +} + +:global([data-theme='dark']) .importBtnLinked:hover { + background: #065f46; + border-color: #059669; + color: #a7f3d0; +} diff --git a/packages/frontend/src/crm/lexware/LexwareSyncPage.tsx b/packages/frontend/src/crm/lexware/LexwareSyncPage.tsx index 5869bbb..6469292 100644 --- a/packages/frontend/src/crm/lexware/LexwareSyncPage.tsx +++ b/packages/frontend/src/crm/lexware/LexwareSyncPage.tsx @@ -74,6 +74,24 @@ function ImportTab() { const importAsContact = useImportLexwareAsContact(); const createContact = useCreateContact(); + // Load CRM entities to detect already-imported Lexware contacts + const companiesQuery = useCompanies({ page: 1, pageSize: 500, sort: 'name', order: 'asc' }); + const contactsQuery = useContacts({ page: 1, pageSize: 500, sort: 'lastName', order: 'asc' }); + + // Build lookup: lexwareContactId → { type, name, id } + const linkedMap = new Map(); + for (const c of companiesQuery.data?.data ?? []) { + if (c.lexwareContactId) { + linkedMap.set(c.lexwareContactId, { type: 'company', name: c.name, id: c.id }); + } + } + for (const c of contactsQuery.data?.data ?? []) { + if (c.lexwareContactId) { + const name = [c.firstName, c.lastName].filter(Boolean).join(' ') || c.companyName || '—'; + linkedMap.set(c.lexwareContactId, { type: 'contact', name, id: c.id }); + } + } + // Debounce search useEffect(() => { const timer = setTimeout(() => { @@ -227,13 +245,28 @@ function ImportTab() { const roles = lexwareRoles(contact); const contactPersons = contact.company?.contactPersons ?? []; const isExpanded = expandedIds.has(contact.id); + const linked = linkedMap.get(contact.id); return ( -
+
{/* Main row */}
-
{name}
+
+ {name} + {linked && ( + + + + + {linked.type === 'company' ? 'Unternehmen' : 'Kontakt'} im CRM + + )} +
{roles !== '—' && ( {roles} @@ -274,48 +307,64 @@ function ImportTab() { {contactPersons.length} AP )} - - + + + + + Öffnen + + )}