Compare commits

...

2 Commits

Author SHA1 Message Date
duffyduck ee4ca9df07 Zugangsdaten-Card: Portal-Link des Anbieters anzeigen
Im Vertragsdetail unter "Zugangsdaten" zwischen Benutzername und
Passwort jetzt eine zusätzliche Zeile "Portal-Link" mit klickbarem
Link zum Anbieter-Portal (öffnet in neuem Tab, mit Copy-Button).
Greift auf das bestehende c.provider.portalUrl-Feld zurück (wird
auch schon für den Auto-Login-Button verwendet).

Schema und Host werden im Anzeigetext gestrippt, die volle URL
bleibt im href und im title-Attribut sichtbar.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 11:27:02 +02:00
duffyduck 9385fc0f11 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>
2026-05-28 09:20:36 +02:00
2 changed files with 56 additions and 2 deletions
@@ -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>