fix: Portal-E-Mail-Feld konnte nur per Paste befüllt werden

Folge-Symptom des Pentest-29.4-Email-Validators: das Portal-Email-
Input feuerte bei jedem Keystroke einen PUT
/customers/:id/portal mit dem Zwischenstand ("p", "po", "por@") –
der Backend-Validator lehnte das mit 400 ab, der Server-State blieb
unverändert, das Input re-renderte mit dem alten Wert. Effekt: man
konnte nichts tippen, nur per Paste in einem Event eine
vollständige Adresse setzen.

Fix: lokaler emailDraft-State. Während getippt wird, bleibt der
Wert nur im Client. Commit erfolgt erst onBlur oder bei Enter –
oder wird mit Escape verworfen. Bei Mutations-Error gibt's jetzt
auch einen toast statt stiller Revert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-28 09:20:36 +02:00
parent c9f4fcf8de
commit 9385fc0f11
@@ -1902,6 +1902,14 @@ function PortalTab({
const [searchTerm, setSearchTerm] = useState('');
const [searchResults, setSearchResults] = useState<CustomerSummary[]>([]);
const [isSearching, setIsSearching] = useState(false);
// Lokaler Eingabezustand für die Portal-E-Mail. Wenn wir den Input
// direkt vom Server-State (`portal?.portalEmail`) speisen und auf
// jedes Keystroke einen PUT feuern, lehnt der Backend-Validator
// (Pentest 29.4) die Zwischenstände ("p", "po", "por") als
// ungültige E-Mail mit 400 ab → Server-State bleibt unverändert,
// Input rendert mit altem Wert → man konnte nur per Paste
// tippen. Lokaler State + Commit on Blur löst das.
const [emailDraft, setEmailDraft] = useState<string | null>(null);
// Lade Portal-Einstellungen
const { data: portalData, isLoading: portalLoading } = useQuery({
@@ -1921,6 +1929,11 @@ function PortalTab({
customerApi.updatePortalSettings(customerId, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['customer-portal', customerId] });
// Lokalen Draft verwerfen kommt jetzt frisch vom Server.
setEmailDraft(null);
},
onError: (err: Error) => {
toast.error(err.message || 'Speichern fehlgeschlagen');
},
});
@@ -2036,8 +2049,30 @@ function PortalTab({
<label className="block text-sm font-medium text-gray-700 mb-1">Portal E-Mail</label>
<div className="flex gap-2">
<Input
value={portal?.portalEmail || ''}
onChange={(e) => updatePortalMutation.mutate({ portalEmail: e.target.value || null })}
value={emailDraft ?? portal?.portalEmail ?? ''}
onChange={(e) => setEmailDraft(e.target.value)}
onBlur={() => {
if (emailDraft === null) return;
const trimmed = emailDraft.trim();
// Nur committen wenn sich was geändert hat sonst
// hauen wir bei jedem Tab-Wechsel einen sinnlosen
// PUT raus.
if (trimmed === (portal?.portalEmail || '')) {
setEmailDraft(null);
return;
}
updatePortalMutation.mutate({ portalEmail: trimmed || null });
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
(e.target as HTMLInputElement).blur();
}
if (e.key === 'Escape') {
setEmailDraft(null);
(e.target as HTMLInputElement).blur();
}
}}
placeholder="portal@example.com"
disabled={!canEdit || !portal?.portalEnabled}
className="flex-1"
@@ -2045,6 +2080,7 @@ function PortalTab({
</div>
<p className="text-xs text-gray-500 mt-1">
Diese E-Mail wird für den Login ins Kundenportal verwendet.
Speichern erfolgt automatisch beim Verlassen des Felds (oder Enter).
</p>
</div>