Compare commits
2 Commits
c9f4fcf8de
...
ee4ca9df07
| Author | SHA1 | Date | |
|---|---|---|---|
| ee4ca9df07 | |||
| 9385fc0f11 |
@@ -2416,6 +2416,24 @@ export default function ContractDetail() {
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
{c.provider?.portalUrl && (
|
||||
<div>
|
||||
<dt className="text-sm text-gray-500">Portal-Link</dt>
|
||||
<dd className="flex items-center gap-1">
|
||||
<a
|
||||
href={c.provider.portalUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:underline truncate"
|
||||
title={c.provider.portalUrl}
|
||||
>
|
||||
{c.provider.portalUrl.replace(/^https?:\/\//, '').replace(/\/$/, '')}
|
||||
<ExternalLink className="w-3 h-3 inline ml-1 -mt-0.5" />
|
||||
</a>
|
||||
<CopyButton value={c.provider.portalUrl} />
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
{c.hasPortalPassword && (
|
||||
<div>
|
||||
<dt className="text-sm text-gray-500">Passwort</dt>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user