From 96a054aa1aa3a56f3744421a61d495565eac361a Mon Sep 17 00:00:00 2001 From: duffyduck Date: Thu, 18 Jun 2026 11:20:03 +0200 Subject: [PATCH] Stressfrei-Adressen: Zusatz-Weiterleitungen auch beim Anlegen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der "Weitere Weiterleitungen"-Button war bisher nur im Bearbeiten- Modus sichtbar (provider-vorhanden + ID nötig). Jetzt erscheint er auch im Anlegen-Modus, sobald "Beim E-Mail-Provider anlegen" angehakt ist. - Sub-Modal generalisiert: value/onChange-controlled. Mit email-Prop → API-Persist pro Änderung (Edit-Modus). Ohne email-Prop → reiner lokaler State (Create-Modus). - Haupt-Modal trackt additionalForwards als eigenen State und ruft nach erfolgreicher createEmail einmalig updateAdditionalForwards mit der vollen Liste auf – ein zweiter Provider-Sync mit set: setzt die finale Liste. - Counter-Badge am Button zeigt die Anzahl bereits eingegebener Adressen. --- docs/todo.md | 10 ++ .../src/pages/customers/CustomerDetail.tsx | 130 ++++++++++++------ 2 files changed, 96 insertions(+), 44 deletions(-) diff --git a/docs/todo.md b/docs/todo.md index 388c6017..a6b558f6 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -97,6 +97,16 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung ## ✅ Erledigt +- [x] **🆕 Stressfrei-Adressen: Zusatz-Weiterleitungen auch beim Anlegen** + - Im „Adresse hinzufügen"-Modal erscheint der „Weitere + Weiterleitungen"-Button jetzt auch, sobald „Beim E-Mail-Provider + anlegen" angehakt ist. Liste wird lokal gepflegt, Provider-Sync + läuft direkt nach `createEmail` mit der vollen Liste. + - Sub-Modal generalisiert: `value`/`onChange`-Pattern (controlled). + Mit `email`-Prop → API-Persist pro Änderung (Edit). Ohne `email` + → lokaler State (Create). Counter-Badge am Button zeigt die + Anzahl Adressen. + - [x] **🆕 Stressfrei-Wechseln-Adressen: zusätzliche Weiterleitungsziele** - Neues Feld `StressfreiEmail.additionalForwardingEmails` (Text/ JSON-Array), Migration `20260608100000_stressfrei_email_additional_forwards` diff --git a/frontend/src/pages/customers/CustomerDetail.tsx b/frontend/src/pages/customers/CustomerDetail.tsx index 7b0c76ef..0ba362d8 100644 --- a/frontend/src/pages/customers/CustomerDetail.tsx +++ b/frontend/src/pages/customers/CustomerDetail.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useMemo } from 'react'; +import { useState, useEffect } from 'react'; import { useParams, Link, useNavigate, useSearchParams, useLocation } from 'react-router-dom'; import { pushHistory, popHistory } from '../../utils/navigation'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; @@ -3745,6 +3745,7 @@ function StressfreiEmailModal({ const [isLoadingCredentials, setIsLoadingCredentials] = useState(false); const [isResettingPassword, setIsResettingPassword] = useState(false); const [showForwardsModal, setShowForwardsModal] = useState(false); + const [additionalForwards, setAdditionalForwards] = useState([]); const queryClient = useQueryClient(); const isEditing = !!email; @@ -3883,6 +3884,15 @@ function StressfreiEmailModal({ setNotes(email.notes || ''); setProviderStatus('idle'); setMailboxEnabled(email.hasMailbox || false); + // Aktuelle Zusatz-Weiterleitungen aus dem JSON-Feld parsen. + let parsed: string[] = []; + if (email.additionalForwardingEmails) { + try { + const x = JSON.parse(email.additionalForwardingEmails); + if (Array.isArray(x)) parsed = x.filter((s): s is string => typeof s === 'string'); + } catch {/* fällt auf [] zurück */} + } + setAdditionalForwards(parsed); // Status beim Provider prüfen wenn Provider vorhanden if (hasProvider) { checkProviderStatus(emailLocalPart); @@ -3896,6 +3906,7 @@ function StressfreiEmailModal({ setCreateMailbox(false); setProviderStatus('idle'); setMailboxEnabled(false); + setAdditionalForwards([]); } setProvisionError(null); // Zugangsdaten zurücksetzen @@ -3907,12 +3918,25 @@ function StressfreiEmailModal({ const createMutation = useMutation({ 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, { + const result = await stressfreiEmailApi.create(customerId, { email: data.email, notes: data.notes, provisionAtProvider: data.provision, createMailbox: data.createMailbox, }); + // Wenn der User zusätzliche Weiterleitungen im Sub-Modal gepflegt + // hat: nach der Erstellung gleich am Provider nachziehen. Das + // sorgt für einen `set:`-Sync mit der vollen Liste, kein + // Edit-Modus-Roundtrip nötig. + if (data.provision && additionalForwards.length > 0 && result.data?.id) { + try { + await stressfreiEmailApi.updateAdditionalForwards(result.data.id, additionalForwards); + } catch (e) { + console.error('Zusatz-Weiterleitungen konnten nicht gesetzt werden:', e); + // Adresse selbst wurde angelegt – nicht hart fehlschlagen. + } + } + return result; }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['customer', customerId] }); @@ -3921,6 +3945,7 @@ function StressfreiEmailModal({ setNotes(''); setProvisionAtProvider(false); setCreateMailbox(false); + setAdditionalForwards([]); onClose(); }, onError: (error) => { @@ -4178,7 +4203,8 @@ function StressfreiEmailModal({
- {isEditing && email && providerStatus === 'exists' && ( + {((isEditing && email && providerStatus === 'exists') || + (!isEditing && provisionAtProvider)) && ( )}
@@ -4202,67 +4233,71 @@ function StressfreiEmailModal({
- {isEditing && email && ( - setShowForwardsModal(false)} - email={email} - customerEmail={customerEmail} - /> - )} + setShowForwardsModal(false)} + email={email ?? undefined} + customerEmail={customerEmail} + value={additionalForwards} + onChange={setAdditionalForwards} + /> ); } -// Untermodal: zusätzliche Weiterleitungs-E-Mails verwalten +// Untermodal: zusätzliche Weiterleitungs-E-Mails verwalten. +// Edit-Modus: `email` prop gesetzt → jede Änderung wird sofort am +// Provider gesynct. +// Create-Modus: `email` undefined → reine lokale Verwaltung über +// `value`/`onChange`. Wird beim createEmail-Submit mitgegeben. function AdditionalForwardsModal({ isOpen, onClose, email, customerEmail, + value, + onChange, }: { isOpen: boolean; onClose: () => void; - email: StressfreiEmail; + email?: StressfreiEmail; customerEmail?: string; + /** Aktuelle Liste – im Create-Modus controlled, im Edit-Modus initialer Wert. */ + value: string[]; + onChange: (next: string[]) => void; }) { const queryClient = useQueryClient(); - const initial = useMemo(() => { - if (!email.additionalForwardingEmails) return []; - try { - const parsed = JSON.parse(email.additionalForwardingEmails); - return Array.isArray(parsed) ? parsed.filter((x): x is string => typeof x === 'string') : []; - } catch { - return []; - } - }, [email.additionalForwardingEmails]); - - const [forwards, setForwards] = useState(initial); const [newEmail, setNewEmail] = useState(''); const [error, setError] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); useEffect(() => { - setForwards(initial); setNewEmail(''); setError(null); - }, [initial, isOpen]); + }, [isOpen]); const EMAIL_REGEX = /^[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}$/; const persist = async (next: string[]) => { - setIsSubmitting(true); setError(null); - try { - await stressfreiEmailApi.updateAdditionalForwards(email.id, next); - setForwards(next); - queryClient.invalidateQueries({ queryKey: ['stressfrei-emails', email.customerId] }); - } catch (e) { - const msg = e instanceof Error ? e.message : 'Speichern fehlgeschlagen'; - setError(msg); - throw e; - } finally { - setIsSubmitting(false); + if (email) { + // Edit-Modus: sofort am Provider syncen. + setIsSubmitting(true); + try { + await stressfreiEmailApi.updateAdditionalForwards(email.id, next); + onChange(next); + queryClient.invalidateQueries({ queryKey: ['stressfrei-emails', email.customerId] }); + } catch (e) { + const msg = e instanceof Error ? e.message : 'Speichern fehlgeschlagen'; + setError(msg); + throw e; + } finally { + setIsSubmitting(false); + } + } else { + // Create-Modus: nur lokal updaten. Persistierung beim Submit + // des Haupt-Modals. + onChange(next); } }; @@ -4278,12 +4313,12 @@ function AdditionalForwardsModal({ setError('Die Stamm-E-Mail des Kunden ist bereits Weiterleitungsziel.'); return; } - if (forwards.some((f) => f.toLowerCase() === candidate)) { + if (value.some((f) => f.toLowerCase() === candidate)) { setError('Diese Adresse ist schon in der Liste.'); return; } try { - await persist([...forwards, candidate]); + await persist([...value, candidate]); setNewEmail(''); } catch { /* error wird oben gesetzt */ @@ -4292,32 +4327,39 @@ function AdditionalForwardsModal({ const handleRemove = async (target: string) => { try { - await persist(forwards.filter((f) => f !== target)); + await persist(value.filter((f) => f !== target)); } catch { /* error wird oben gesetzt */ } }; + const title = email + ? `Weiterleitungen für ${email.email}` + : 'Weitere Weiterleitungen'; + return ( - +

Posteingänge gehen immer an die Stamm-E-Mail des Kunden {customerEmail && ( <> ({customerEmail}) )} - . Hier kannst du zusätzliche Adressen hinterlegen, die ebenfalls eine Kopie bekommen. Änderungen werden sofort am E-Mail-Provider übernommen. + . Hier kannst du zusätzliche Adressen hinterlegen, die ebenfalls eine Kopie bekommen. + {email + ? ' Änderungen werden sofort am E-Mail-Provider übernommen.' + : ' Sie werden zusammen mit der Adresse angelegt, sobald du speicherst.'}

- {forwards.length === 0 ? ( + {value.length === 0 ? (

Noch keine zusätzlichen Adressen.

) : (
    - {forwards.map((f) => ( + {value.map((f) => (