added backup and email client

This commit is contained in:
2026-02-01 00:02:35 +01:00
parent ff857be01a
commit e4fdfbc95f
210 changed files with 24211 additions and 742 deletions
+266 -27
View File
@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
import { useParams, Link, useNavigate, useSearchParams } from 'react-router-dom';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { customerApi, addressApi, bankCardApi, documentApi, meterApi, uploadApi, contractApi, stressfreiEmailApi, emailProviderApi, StressfreiEmail } from '../../services/api';
import { EmailClientTab } from '../../components/email';
import { useAuth } from '../../context/AuthContext';
import Card from '../../components/ui/Card';
import Button from '../../components/ui/Button';
@@ -132,6 +133,13 @@ export default function CustomerDetail() {
/>
),
},
{
id: 'emails',
label: 'E-Mail-Postfach',
content: (
<EmailClientTab customerId={customerId} />
),
},
{
id: 'contracts',
label: 'Verträge',
@@ -2793,9 +2801,21 @@ function StressfreiEmailModal({
const [localPart, setLocalPart] = useState('');
const [notes, setNotes] = useState('');
const [provisionAtProvider, setProvisionAtProvider] = useState(false);
const [createMailbox, setCreateMailbox] = useState(false);
const [provisionError, setProvisionError] = useState<string | null>(null);
const [providerStatus, setProviderStatus] = useState<'idle' | 'checking' | 'exists' | 'not_exists' | 'error'>('idle');
const [isProvisioning, setIsProvisioning] = useState(false);
const [isEnablingMailbox, setIsEnablingMailbox] = useState(false);
const [mailboxEnabled, setMailboxEnabled] = useState(false);
const [showCredentials, setShowCredentials] = useState(false);
const [credentials, setCredentials] = useState<{
email: string;
password: string;
imap: { server: string; port: number; encryption: string } | null;
smtp: { server: string; port: number; encryption: string } | null;
} | null>(null);
const [isLoadingCredentials, setIsLoadingCredentials] = useState(false);
const [isResettingPassword, setIsResettingPassword] = useState(false);
const queryClient = useQueryClient();
const isEditing = !!email;
@@ -2845,6 +2865,86 @@ function StressfreiEmailModal({
}
};
// Mailbox nachträglich aktivieren
const handleEnableMailbox = async () => {
if (!email) return;
setIsEnablingMailbox(true);
setProvisionError(null);
try {
const result = await stressfreiEmailApi.enableMailbox(email.id);
if (result.success) {
setMailboxEnabled(true);
queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] });
queryClient.invalidateQueries({ queryKey: ['mailbox-accounts', customerId] });
} else {
setProvisionError(result.error || 'Mailbox-Aktivierung fehlgeschlagen');
}
} catch (error) {
setProvisionError(error instanceof Error ? error.message : 'Fehler bei der Mailbox-Aktivierung');
} finally {
setIsEnablingMailbox(false);
}
};
// Mailbox-Status mit Provider synchronisieren
const syncMailboxStatusFromProvider = async () => {
if (!email) return;
try {
const result = await stressfreiEmailApi.syncMailboxStatus(email.id);
if (result.success && result.data) {
setMailboxEnabled(result.data.hasMailbox);
if (result.data.wasUpdated) {
// DB wurde aktualisiert, Query invalidieren
queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] });
}
}
} catch (error) {
console.error('Fehler beim Synchronisieren des Mailbox-Status:', error);
}
};
// Mailbox-Zugangsdaten laden
const loadCredentials = async () => {
if (!email) return;
setIsLoadingCredentials(true);
try {
const result = await stressfreiEmailApi.getMailboxCredentials(email.id);
if (result.success && result.data) {
setCredentials(result.data);
setShowCredentials(true);
}
} catch (error) {
console.error('Fehler beim Laden der Zugangsdaten:', error);
} finally {
setIsLoadingCredentials(false);
}
};
// Passwort zurücksetzen
const handleResetPassword = async () => {
if (!email) return;
if (!confirm('Neues Passwort generieren? Das alte Passwort wird ungültig.')) return;
setIsResettingPassword(true);
try {
const result = await stressfreiEmailApi.resetPassword(email.id);
if (result.success && result.data) {
// Credentials mit neuem Passwort aktualisieren
if (credentials) {
setCredentials({ ...credentials, password: result.data.password });
}
alert('Passwort wurde erfolgreich zurückgesetzt.');
} else {
alert(result.error || 'Fehler beim Zurücksetzen des Passworts');
}
} catch (error) {
console.error('Fehler beim Zurücksetzen des Passworts:', error);
alert(error instanceof Error ? error.message : 'Fehler beim Zurücksetzen des Passworts');
} finally {
setIsResettingPassword(false);
}
};
// Reset form when modal opens or email changes
useEffect(() => {
if (isOpen) {
@@ -2853,40 +2953,45 @@ function StressfreiEmailModal({
setLocalPart(emailLocalPart);
setNotes(email.notes || '');
setProviderStatus('idle');
setMailboxEnabled(email.hasMailbox || false);
// Status beim Provider prüfen wenn Provider vorhanden
if (hasProvider) {
checkProviderStatus(emailLocalPart);
// Mailbox-Status synchronisieren
syncMailboxStatusFromProvider();
}
} else {
setLocalPart('');
setNotes('');
setProvisionAtProvider(false);
setCreateMailbox(false);
setProviderStatus('idle');
setMailboxEnabled(false);
}
setProvisionError(null);
// Zugangsdaten zurücksetzen
setShowCredentials(false);
setCredentials(null);
}
}, [isOpen, email, hasProvider]);
const createMutation = useMutation({
mutationFn: async (data: { email: string; notes?: string; provision?: boolean }) => {
// Wenn Provisionierung aktiviert, erst beim Provider anlegen
if (data.provision && customerEmail) {
const provisionResult = await emailProviderApi.provisionEmail(localPart, customerEmail);
if (!provisionResult.data?.success) {
throw new Error(provisionResult.data?.error || provisionResult.data?.message || 'Provisionierung fehlgeschlagen');
}
}
// Dann in der Datenbank anlegen
mutationFn: async (data: { email: string; notes?: string; provision?: boolean; createMailbox?: boolean }) => {
// Verwendet die neue API-Funktion, die Provisioning und Mailbox-Erstellung unterstützt
return stressfreiEmailApi.create(customerId, {
email: data.email,
notes: data.notes,
provisionAtProvider: data.provision,
createMailbox: data.createMailbox,
});
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] });
queryClient.invalidateQueries({ queryKey: ['mailbox-accounts', customerId] });
setLocalPart('');
setNotes('');
setProvisionAtProvider(false);
setCreateMailbox(false);
onClose();
},
onError: (error) => {
@@ -2918,6 +3023,7 @@ function StressfreiEmailModal({
email: fullEmail,
notes: notes || undefined,
provision: provisionAtProvider,
createMailbox: provisionAtProvider && createMailbox,
});
}
};
@@ -2966,24 +3072,48 @@ function StressfreiEmailModal({
{hasProvider && customerEmail && (
<div className="bg-blue-50 p-3 rounded-lg">
{!isEditing ? (
// Erstellen-Modus: Checkbox
<label className="flex items-start gap-2 cursor-pointer">
<input
type="checkbox"
checked={provisionAtProvider}
onChange={(e) => setProvisionAtProvider(e.target.checked)}
className="mt-1 rounded border-gray-300"
/>
<div>
<span className="text-sm font-medium text-gray-700">
Beim E-Mail-Provider anlegen
</span>
<p className="text-xs text-gray-500 mt-1">
Die E-Mail-Weiterleitung wird automatisch auf dem konfigurierten Server erstellt.
Weiterleitungsziel: {customerEmail}
</p>
</div>
</label>
// Erstellen-Modus: Checkboxen
<div className="space-y-3">
<label className="flex items-start gap-2 cursor-pointer">
<input
type="checkbox"
checked={provisionAtProvider}
onChange={(e) => {
setProvisionAtProvider(e.target.checked);
if (!e.target.checked) setCreateMailbox(false);
}}
className="mt-1 rounded border-gray-300"
/>
<div>
<span className="text-sm font-medium text-gray-700">
Beim E-Mail-Provider anlegen
</span>
<p className="text-xs text-gray-500 mt-1">
Die E-Mail-Weiterleitung wird automatisch auf dem konfigurierten Server erstellt.
Weiterleitungsziel: {customerEmail}
</p>
</div>
</label>
{provisionAtProvider && (
<label className="flex items-start gap-2 cursor-pointer ml-6">
<input
type="checkbox"
checked={createMailbox}
onChange={(e) => setCreateMailbox(e.target.checked)}
className="mt-1 rounded border-gray-300"
/>
<div>
<span className="text-sm font-medium text-gray-700">
Echte Mailbox erstellen (IMAP/SMTP-Zugang)
</span>
<p className="text-xs text-gray-500 mt-1">
Ermöglicht E-Mails direkt im CRM zu empfangen und zu versenden.
</p>
</div>
</label>
)}
</div>
) : (
// Bearbeiten-Modus: Status anzeigen
<div className="space-y-2">
@@ -3039,6 +3169,115 @@ function StressfreiEmailModal({
Erneut prüfen
</Button>
)}
{/* Mailbox-Status anzeigen wenn Provider vorhanden */}
{providerStatus === 'exists' && (
<div className="pt-3 mt-3 border-t border-blue-100">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-700">
Mailbox (IMAP/SMTP)
</span>
{mailboxEnabled ? (
<span className="text-xs text-green-600 flex items-center gap-1">
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
Mailbox aktiv
</span>
) : (
<span className="text-xs text-orange-600">Keine Mailbox</span>
)}
</div>
{!mailboxEnabled && (
<div className="mt-2">
<p className="text-xs text-gray-500 mb-2">
Aktiviere eine echte Mailbox um E-Mails direkt im CRM zu empfangen und zu versenden.
</p>
<Button
type="button"
size="sm"
onClick={handleEnableMailbox}
disabled={isEnablingMailbox}
>
{isEnablingMailbox ? 'Wird aktiviert...' : 'Mailbox aktivieren'}
</Button>
</div>
)}
{/* Zugangsdaten anzeigen wenn Mailbox aktiv */}
{mailboxEnabled && (
<div className="mt-3">
{!showCredentials ? (
<Button
type="button"
size="sm"
variant="secondary"
onClick={loadCredentials}
disabled={isLoadingCredentials}
>
{isLoadingCredentials ? (
'Laden...'
) : (
<>
<Eye className="w-4 h-4 mr-1" />
Zugangsdaten anzeigen
</>
)}
</Button>
) : credentials && (
<div className="bg-white border border-gray-200 rounded-lg p-3 space-y-2">
<div className="flex justify-between items-center mb-2">
<span className="text-xs font-medium text-gray-500 uppercase">Zugangsdaten</span>
<button
type="button"
onClick={() => setShowCredentials(false)}
className="text-gray-400 hover:text-gray-600"
>
<EyeOff className="w-4 h-4" />
</button>
</div>
<div className="grid grid-cols-2 gap-2 text-xs">
<div>
<span className="text-gray-500">E-Mail:</span>
<p className="font-mono text-gray-900 break-all">{credentials.email}</p>
</div>
<div>
<span className="text-gray-500">Passwort:</span>
<div className="flex items-center gap-2">
<p className="font-mono text-gray-900 break-all select-all flex-1">{credentials.password}</p>
<button
type="button"
onClick={handleResetPassword}
disabled={isResettingPassword}
className="text-blue-600 hover:text-blue-800 text-xs whitespace-nowrap disabled:opacity-50"
title="Neues Kennwort generieren"
>
{isResettingPassword ? 'Generiere...' : 'Neu generieren'}
</button>
</div>
</div>
</div>
{credentials.imap && (
<div className="pt-2 border-t border-gray-100">
<span className="text-xs font-medium text-gray-500">IMAP (Empfang)</span>
<p className="font-mono text-xs text-gray-900">
{credentials.imap.server}:{credentials.imap.port} ({credentials.imap.encryption})
</p>
</div>
)}
{credentials.smtp && (
<div className="pt-2 border-t border-gray-100">
<span className="text-xs font-medium text-gray-500">SMTP (Versand)</span>
<p className="font-mono text-xs text-gray-900">
{credentials.smtp.server}:{credentials.smtp.port} ({credentials.smtp.encryption})
</p>
</div>
)}
</div>
)}
</div>
)}
</div>
)}
</div>
)}
</div>