Mandantenfähigkeit: Domain + Kunden-E-Mail-Label dynamisch pro Provider
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 (@<domain>)
- 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) <noreply@anthropic.com>
This commit is contained in:
@@ -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())
|
||||
|
||||
@@ -337,3 +337,20 @@ export async function getProviderDomain(req: Request, res: Response): Promise<vo
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Öffentliche Provider-Einstellungen für die Frontend-UI:
|
||||
* Domain + Label für Kunden-E-Mail-Adressen.
|
||||
* Auch für Nicht-Admin-Mitarbeiter verfügbar, da nur UI-Labels.
|
||||
*/
|
||||
export async function getPublicSettings(req: Request, res: Response): Promise<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<string | null> {
|
||||
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';
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user