From cdde7b4ab7de4ab5ae4ca33ce22b17391b8f0951 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Thu, 23 Apr 2026 15:43:19 +0200 Subject: [PATCH] =?UTF-8?q?Mandantenf=C3=A4higkeit:=20Domain=20+=20Kunden-?= =?UTF-8?q?E-Mail-Label=20dynamisch=20pro=20Provider?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Alle hardcoded Referenzen auf 'stressfrei-wechseln.de' und 'Stressfrei-Wechseln' durch dynamische Werte aus der EmailProviderConfig ersetzt. Notwendig für Multi-Mandanten-Betrieb, wenn das CRM an Dritte vermietet wird. Schema: - Neues Feld EmailProviderConfig.customerEmailLabel (String?) - Wenn leer, wird Label aus Domain abgeleitet ('stressfrei-wechseln.de' → 'Stressfrei-Wechseln') Backend: - Neuer Endpoint GET /api/email-providers/public-settings liefert { domain, customerEmailLabel } - Neue Service-Funktionen: getProviderPublicSettings(), deriveLabelFromDomain() - create/updateProviderConfig erweitert um customerEmailLabel Frontend: - Neuer Hook useProviderSettings() mit Auto-Caching - Neues Eingabefeld 'Bezeichnung für Kunden-E-Mails' im Provider-Modal - Dynamische Domain-Suffix im Adress-Hinzufügen-Dialog (@) - Tab-Label 'Stressfrei-Wechseln' im Kunden-Detail → dynamisch - 'Stressfrei-Wechseln Adresse' in ContractForm → dynamisch - '(Stressfrei-Wechseln)' Badge in ContractDetail → dynamisch - 'Stressfrei-Wechseln E-Mail' im Generate-Modal → dynamisch - Leere-Zustand-Meldungen in Tab und E-Mail-Client → dynamisch Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/prisma/schema.prisma | 4 ++ .../controllers/emailProvider.controller.ts | 17 +++++++++ backend/src/routes/emailProvider.routes.ts | 1 + .../emailProvider/emailProviderService.ts | 37 +++++++++++++++++++ backend/todo.md | 9 +++++ .../src/components/email/EmailClientTab.tsx | 4 +- frontend/src/hooks/useProviderSettings.ts | 26 +++++++++++++ frontend/src/pages/Settings.tsx | 2 +- .../src/pages/contracts/ContractDetail.tsx | 7 +++- frontend/src/pages/contracts/ContractForm.tsx | 6 ++- .../src/pages/customers/CustomerDetail.tsx | 21 +++++++---- .../src/pages/settings/EmailProviders.tsx | 31 +++++++++++++--- frontend/src/services/api.ts | 10 +++++ 13 files changed, 156 insertions(+), 19 deletions(-) create mode 100644 frontend/src/hooks/useProviderSettings.ts diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 69051378..084d96a5 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -358,6 +358,10 @@ model EmailProviderConfig { systemEmailAddress String? // z.B. "info@stressfrei-wechseln.de" systemEmailPasswordEncrypted String? // Passwort (verschlüsselt) + // Label für Kunden-E-Mail-Adressen in der UI (z.B. "Stressfrei-Wechseln") + // Wenn leer, wird automatisch aus der Domain abgeleitet (z.B. "stressfrei-wechseln.de" → "Stressfrei-Wechseln") + customerEmailLabel String? + isActive Boolean @default(true) isDefault Boolean @default(false) // Standard-Provider createdAt DateTime @default(now()) diff --git a/backend/src/controllers/emailProvider.controller.ts b/backend/src/controllers/emailProvider.controller.ts index 72180be3..311609bc 100644 --- a/backend/src/controllers/emailProvider.controller.ts +++ b/backend/src/controllers/emailProvider.controller.ts @@ -337,3 +337,20 @@ export async function getProviderDomain(req: Request, res: Response): Promise { + try { + const settings = await emailProviderService.getProviderPublicSettings(); + res.json({ success: true, data: settings } as ApiResponse); + } catch (error) { + res.status(500).json({ + success: false, + error: 'Fehler beim Laden der Einstellungen', + } as ApiResponse); + } +} diff --git a/backend/src/routes/emailProvider.routes.ts b/backend/src/routes/emailProvider.routes.ts index 62db111d..6ccb1ac9 100644 --- a/backend/src/routes/emailProvider.routes.ts +++ b/backend/src/routes/emailProvider.routes.ts @@ -17,6 +17,7 @@ router.delete('/configs/:id', authenticate, requirePermission('settings:update') router.post('/test-connection', authenticate, requirePermission('settings:update'), emailProviderController.testConnection); router.post('/test-mail-access', authenticate, requirePermission('settings:update'), emailProviderController.testMailAccess); router.get('/domain', authenticate, emailProviderController.getProviderDomain); +router.get('/public-settings', authenticate, emailProviderController.getPublicSettings); router.get('/check/:localPart', authenticate, requirePermission('customers:read'), emailProviderController.checkEmailExists); router.post('/provision', authenticate, requirePermission('customers:update'), emailProviderController.provisionEmail); router.delete('/deprovision/:localPart', authenticate, requirePermission('customers:update'), emailProviderController.deprovisionEmail); diff --git a/backend/src/services/emailProvider/emailProviderService.ts b/backend/src/services/emailProvider/emailProviderService.ts index 05b6eaa0..3381801a 100644 --- a/backend/src/services/emailProvider/emailProviderService.ts +++ b/backend/src/services/emailProvider/emailProviderService.ts @@ -74,6 +74,8 @@ export interface CreateProviderConfigData { // System-E-Mail systemEmailAddress?: string; systemEmailPassword?: string; + // UI-Label für Kunden-E-Mail-Adressen (z.B. "Stressfrei-Wechseln", "Meine-Firma") + customerEmailLabel?: string; isActive?: boolean; isDefault?: boolean; } @@ -107,6 +109,7 @@ export async function createProviderConfig(data: CreateProviderConfigData) { allowSelfSignedCerts: data.allowSelfSignedCerts ?? false, systemEmailAddress: data.systemEmailAddress || null, systemEmailPasswordEncrypted, + customerEmailLabel: data.customerEmailLabel || null, isActive: data.isActive ?? true, isDefault: data.isDefault ?? false, }, @@ -139,6 +142,7 @@ export async function updateProviderConfig( if (data.smtpEncryption !== undefined) updateData.smtpEncryption = data.smtpEncryption; if (data.allowSelfSignedCerts !== undefined) updateData.allowSelfSignedCerts = data.allowSelfSignedCerts; if (data.systemEmailAddress !== undefined) updateData.systemEmailAddress = data.systemEmailAddress || null; + if (data.customerEmailLabel !== undefined) updateData.customerEmailLabel = data.customerEmailLabel?.trim() || null; if (data.isActive !== undefined) updateData.isActive = data.isActive; if (data.isDefault !== undefined) updateData.isDefault = data.isDefault; @@ -471,6 +475,39 @@ export async function getProviderDomain(): Promise { return config?.domain || null; } +/** + * Label aus der Domain ableiten, z.B. "stressfrei-wechseln.de" → "Stressfrei-Wechseln". + * Nimmt den Hauptteil bis zum ersten Punkt, trennt an "-" und kapitalisiert jeden Teil. + */ +export function deriveLabelFromDomain(domain: string | null | undefined): string { + if (!domain) return 'Kunden-E-Mail'; + const mainPart = domain.split('.')[0] || domain; + return mainPart + .split('-') + .map((s) => (s.length === 0 ? '' : s.charAt(0).toUpperCase() + s.slice(1))) + .join('-'); +} + +/** + * Öffentliche Provider-Einstellungen (Domain + Label) für UI. + * Kein auth-geschütztes Geheimnis, nur damit die Frontend-Labels stimmen. + */ +export async function getProviderPublicSettings(): Promise<{ + domain: string | null; + customerEmailLabel: string; + customerEmailLabelIsCustom: boolean; +}> { + const config = await getActiveProviderConfig(); + const domain = config?.domain ?? null; + const customLabel = config?.customerEmailLabel?.trim(); + + return { + domain, + customerEmailLabel: customLabel && customLabel.length > 0 ? customLabel : deriveLabelFromDomain(domain), + customerEmailLabelIsCustom: !!(customLabel && customLabel.length > 0), + }; +} + // Provider-Instanz aus übergebener Config erstellen (für Tests mit ungespeicherten Daten) function createProviderFromFormData(data: { type: 'PLESK' | 'CPANEL' | 'DIRECTADMIN'; diff --git a/backend/todo.md b/backend/todo.md index 16a0c1e9..b12179ee 100644 --- a/backend/todo.md +++ b/backend/todo.md @@ -14,6 +14,15 @@ ## ✅ Erledigt +- [x] **Mandantenfähigkeit: Domain + Kunden-E-Mail-Label dynamisch pro Provider** + - Neues Feld `customerEmailLabel` am EmailProviderConfig (z.B. "Stressfrei-Wechseln", "Meine-Firma") + - Wenn leer, wird das Label automatisch aus der Domain abgeleitet ("stressfrei-wechseln.de" → "Stressfrei-Wechseln") + - Neuer Frontend-Hook `useProviderSettings()` liefert Domain + Label + - Alle hardcoded "Stressfrei-Wechseln" und `@stressfrei-wechseln.de` Strings durch dynamische Werte ersetzt + (CustomerDetail, ContractForm, ContractDetail, EmailClientTab, Settings) + - Modal-Eingabefeld "Bezeichnung für Kunden-E-Mails" in Provider-Einstellungen + - Notwendig für Multi-Mandanten-Betrieb wenn das CRM an Dritte vermietet wird + - [x] **Factory-Defaults: Export + Import von Stammdaten-Katalogen** - Enthält: Anbieter, Tarife, Kündigungsfristen, Laufzeiten, Vertragskategorien, PDF-Auftragsvorlagen (+ PDF-Dateien) - Enthält NICHT: Kundendaten, Verträge, Dokumente, Emails, Einstellungen (dafür gibt es den Datenbank-Backup) diff --git a/frontend/src/components/email/EmailClientTab.tsx b/frontend/src/components/email/EmailClientTab.tsx index 0a048d93..e71d95c1 100644 --- a/frontend/src/components/email/EmailClientTab.tsx +++ b/frontend/src/components/email/EmailClientTab.tsx @@ -4,6 +4,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import toast from 'react-hot-toast'; import { cachedEmailApi, stressfreiEmailApi, CachedEmail } from '../../services/api'; import { useAuth } from '../../context/AuthContext'; +import { useProviderSettings } from '../../hooks/useProviderSettings'; import Button from '../ui/Button'; import EmailList from './EmailList'; import EmailDetail from './EmailDetail'; @@ -18,6 +19,7 @@ interface EmailClientTabProps { } export default function EmailClientTab({ customerId }: EmailClientTabProps) { + const { customerEmailLabel } = useProviderSettings(); const [selectedAccountId, setSelectedAccountId] = useState(null); const [selectedFolder, setSelectedFolder] = useState('INBOX'); const [selectedEmail, setSelectedEmail] = useState(null); @@ -150,7 +152,7 @@ export default function EmailClientTab({ customerId }: EmailClientTabProps) { Keine E-Mail-Konten vorhanden

- Erstellen Sie eine Stressfrei-Wechseln E-Mail-Adresse mit aktivierter Mailbox, + Erstellen Sie eine {customerEmailLabel} E-Mail-Adresse mit aktivierter Mailbox, um E-Mails hier empfangen und versenden zu können.

diff --git a/frontend/src/hooks/useProviderSettings.ts b/frontend/src/hooks/useProviderSettings.ts new file mode 100644 index 00000000..a83f8ac1 --- /dev/null +++ b/frontend/src/hooks/useProviderSettings.ts @@ -0,0 +1,26 @@ +import { useQuery } from '@tanstack/react-query'; +import { emailProviderApi } from '../services/api'; + +export interface ProviderSettings { + domain: string | null; + customerEmailLabel: string; // z.B. "Stressfrei-Wechseln" (aus Config oder aus Domain abgeleitet) + customerEmailLabelIsCustom: boolean; +} + +/** + * Holt die öffentlichen Provider-Einstellungen (Domain + Label für Kunden-E-Mail-Adressen). + * Mit Default-Fallback bei Ladefehler – UI-Labels werden dann generisch angezeigt. + */ +export function useProviderSettings(): ProviderSettings { + const { data } = useQuery({ + queryKey: ['email-provider-public-settings'], + queryFn: () => emailProviderApi.getPublicSettings(), + staleTime: 5 * 60_000, + }); + + return { + domain: data?.data?.domain ?? null, + customerEmailLabel: data?.data?.customerEmailLabel || 'Kunden-E-Mail', + customerEmailLabelIsCustom: data?.data?.customerEmailLabelIsCustom ?? false, + }; +} diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index faa678ae..2979c94c 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -156,7 +156,7 @@ export default function Settings() { Email-Provisionierung -

Konfigurieren Sie die automatische E-Mail-Erstellung für Stressfrei-Wechseln Adressen.

+

Konfigurieren Sie die automatische E-Mail-Erstellung für Kunden-E-Mail-Adressen.

diff --git a/frontend/src/pages/contracts/ContractDetail.tsx b/frontend/src/pages/contracts/ContractDetail.tsx index 08f2fb1c..cb1e9e6e 100644 --- a/frontend/src/pages/contracts/ContractDetail.tsx +++ b/frontend/src/pages/contracts/ContractDetail.tsx @@ -17,6 +17,7 @@ import { Edit, Trash2, Copy, Eye, EyeOff, ArrowLeft, ArrowRight, Download, Exter import { calculateConsumption, calculateCosts, calculateMultiMeterConsumption } from '../../utils/energyCalculations'; import CopyButton, { CopyableBlock } from '../../components/ui/CopyButton'; import { formatDate } from '../../utils/dateFormat'; +import { useProviderSettings } from '../../hooks/useProviderSettings'; import type { ContractType, ContractStatus, SimCard, MeterReading, ContractTask, ContractTaskSubtask, ContractMeter, ContractDocument } from '../../types'; const typeLabels: Record = { @@ -1444,6 +1445,7 @@ export default function ContractDetail() { const currentPath = `/contracts/${id}`; const { hasPermission, isCustomer, isCustomerPortal } = useAuth(); const contractId = parseInt(id!); + const { customerEmailLabel } = useProviderSettings(); const [showPassword, setShowPassword] = useState(false); const [decryptedPassword, setDecryptedPassword] = useState(null); @@ -2310,7 +2312,7 @@ export default function ContractDetail() {
Benutzername {c.stressfreiEmail && ( - (Stressfrei-Wechseln) + ({customerEmailLabel}) )}
@@ -3363,6 +3365,7 @@ function GenerateInputModal({ templateId, templateName, contractId, onClose }: { const [stressfreiEmailId, setStressfreiEmailId] = useState(''); const [manualValues, setManualValues] = useState>({}); const [generating] = useState(false); + const { customerEmailLabel } = useProviderSettings(); const { data: inputsData, isLoading } = useQuery({ queryKey: ['pdf-inputs', templateId, contractId], @@ -3390,7 +3393,7 @@ function GenerateInputModal({ templateId, templateName, contractId, onClose }: {
{inputs?.needsStressfreiEmail && (
- + - Keine Stressfrei-Wechseln Adressen für diesen Kunden vorhanden. Bitte zuerst beim Kunden anlegen. + Keine {customerEmailLabel} Adressen für diesen Kunden vorhanden. Bitte zuerst beim Kunden anlegen.

)}
diff --git a/frontend/src/pages/customers/CustomerDetail.tsx b/frontend/src/pages/customers/CustomerDetail.tsx index 2c9cdeff..e7746b2d 100644 --- a/frontend/src/pages/customers/CustomerDetail.tsx +++ b/frontend/src/pages/customers/CustomerDetail.tsx @@ -18,6 +18,7 @@ import CopyButton, { CopyableBlock } from '../../components/ui/CopyButton'; import BirthdayManagementModal from '../../components/BirthdayManagementModal'; import { formatDate } from '../../utils/dateFormat'; import { getContractTypeInfo } from '../../utils/contractInfo'; +import { useProviderSettings } from '../../hooks/useProviderSettings'; import type { Address, BankCard, IdentityDocument, Meter, Customer, CustomerRepresentative, CustomerSummary, CustomerConsent, ConsentType, ConsentStatus, RepresentativeAuthorization } from '../../types'; export default function CustomerDetail({ portalCustomerId }: { portalCustomerId?: number } = {}) { @@ -31,6 +32,7 @@ export default function CustomerDetail({ portalCustomerId }: { portalCustomerId? const customerId = portalCustomerId || parseInt(id!); const defaultTab = searchParams.get('tab') || 'addresses'; const [activeTab, setActiveTab] = useState(defaultTab); + const { customerEmailLabel } = useProviderSettings(); // Tab-Wechsel in URL synchronisieren (für Browser-History) const handleTabChange = (tabId: string) => { @@ -148,7 +150,7 @@ export default function CustomerDetail({ portalCustomerId }: { portalCustomerId? }, ...(!isCustomerPortal ? [{ id: 'stressfrei', - label: 'Stressfrei-Wechseln', + label: customerEmailLabel, content: ( void; }) { const queryClient = useQueryClient(); + const { customerEmailLabel } = useProviderSettings(); const updateMutation = useMutation({ mutationFn: ({ id, data }: { id: number; data: Partial }) => @@ -3067,7 +3068,7 @@ function StressfreiEmailsTab({ ))}
) : ( -

Keine Stressfrei-Wechseln Adressen vorhanden.

+

Keine {customerEmailLabel} Adressen vorhanden.

)} ); @@ -3240,6 +3241,10 @@ function StressfreiEmailModal({ const [provisionError, setProvisionError] = useState(null); const [providerStatus, setProviderStatus] = useState<'idle' | 'checking' | 'exists' | 'not_exists' | 'error'>('idle'); const [isProvisioning, setIsProvisioning] = useState(false); + + // Domain dynamisch vom Provider (mit Fallback) + const { domain: providerDomain } = useProviderSettings(); + const domainSuffix = `@${providerDomain || 'stressfrei-wechseln.de'}`; const [isEnablingMailbox, setIsEnablingMailbox] = useState(false); const [mailboxEnabled, setMailboxEnabled] = useState(false); const [showCredentials, setShowCredentials] = useState(false); @@ -3446,7 +3451,7 @@ function StressfreiEmailModal({ const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); setProvisionError(null); - const fullEmail = localPart + STRESSFREI_DOMAIN; + const fullEmail = localPart + domainSuffix; if (isEditing) { updateMutation.mutate({ @@ -3482,11 +3487,11 @@ function StressfreiEmailModal({ className="block w-full px-3 py-2 border border-gray-300 rounded-l-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" /> - {STRESSFREI_DOMAIN} + {domainSuffix}

- Vollständige Adresse: {localPart || '...'}{STRESSFREI_DOMAIN} + Vollständige Adresse: {localPart || '...'}{domainSuffix}

diff --git a/frontend/src/pages/settings/EmailProviders.tsx b/frontend/src/pages/settings/EmailProviders.tsx index b5284461..bec12819 100644 --- a/frontend/src/pages/settings/EmailProviders.tsx +++ b/frontend/src/pages/settings/EmailProviders.tsx @@ -39,6 +39,8 @@ interface ProviderFormData { // System-E-Mail systemEmailAddress: string; systemEmailPassword: string; + // UI-Label für Kunden-E-Mail-Adressen + customerEmailLabel: string; isActive: boolean; isDefault: boolean; } @@ -57,6 +59,7 @@ const emptyForm: ProviderFormData = { allowSelfSignedCerts: false, systemEmailAddress: '', systemEmailPassword: '', + customerEmailLabel: '', isActive: true, isDefault: false, }; @@ -143,6 +146,7 @@ export default function EmailProviders() { allowSelfSignedCerts: config.allowSelfSignedCerts ?? false, systemEmailAddress: config.systemEmailAddress || '', systemEmailPassword: '', // Passwort wird nicht geladen + customerEmailLabel: config.customerEmailLabel || '', isActive: config.isActive, isDefault: config.isDefault, }); @@ -290,6 +294,7 @@ export default function EmailProviders() { smtpEncryption: formData.smtpEncryption, allowSelfSignedCerts: formData.allowSelfSignedCerts, systemEmailAddress: formData.systemEmailAddress, + customerEmailLabel: formData.customerEmailLabel?.trim() || null, isActive: formData.isActive, isDefault: formData.isDefault, }; @@ -336,9 +341,9 @@ export default function EmailProviders() {

- Hier konfigurieren Sie die automatische Erstellung von Stressfrei-Wechseln E-Mail-Adressen. - Wenn beim Anlegen einer Stressfrei-Adresse die Option "Bei Provider anlegen" aktiviert ist, - wird die E-Mail-Weiterleitung automatisch erstellt. + Hier konfigurieren Sie die automatische Erstellung von Kunden-E-Mail-Adressen auf Ihrer + eigenen Domain. Wenn beim Anlegen einer Adresse die Option "Bei Provider anlegen" + aktiviert ist, wird die E-Mail-Weiterleitung automatisch erstellt.