Vertrag: „Portaldaten nicht möglich / nicht benötigt"-Opt-out
Dritter Radio-Button in der Zugangsdaten-Card des Vertragsformulars. Wenn gesetzt, unterdrückt das Cockpit die Warnung „Portal-Zugangs- daten fehlen" für diesen Vertrag – für Anbieter ohne Portal oder Kunden, die bewusst keine Zugangsdaten pflegen. Verstopft das Cockpit sonst dauerhaft. Neues Feld Contract.portalCredentialsNotRequired (Boolean, default false) + idempotente Migration (ADD COLUMN IF NOT EXISTS). Bestand bleibt auf false, Warnung greift wie bisher. Beim Umschalten auf Opt-out werden portalUsername, portalPasswordEncrypted und stressfreiEmailId server-seitig explizit auf NULL gesetzt – Datenhygiene, damit keine verwaisten Anmeldedaten in der DB stehen bleiben. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -220,7 +220,7 @@ export default function ContractForm() {
|
||||
const [isContractLoaded, setIsContractLoaded] = useState(false);
|
||||
|
||||
// Portal-Benutzername: Manuell oder Stressfrei-Adresse
|
||||
const [usernameType, setUsernameType] = useState<'manual' | 'stressfrei'>('manual');
|
||||
const [usernameType, setUsernameType] = useState<'manual' | 'stressfrei' | 'not_required'>('manual');
|
||||
const [selectedStressfreiEmailId, setSelectedStressfreiEmailId] = useState<string>('');
|
||||
|
||||
// Passwort-Sichtbarkeit
|
||||
@@ -442,8 +442,14 @@ export default function ContractForm() {
|
||||
setPhoneNumbers([]);
|
||||
}
|
||||
|
||||
// Portal-Benutzername-Typ initialisieren
|
||||
if (c.stressfreiEmailId) {
|
||||
// Portal-Benutzername-Typ initialisieren.
|
||||
// Opt-out („nicht benötigt") gewinnt vor Stressfrei/Manual, damit ein
|
||||
// versehentlich noch alter portalUsername den Radio-Zustand nicht
|
||||
// überstimmt.
|
||||
if (c.portalCredentialsNotRequired) {
|
||||
setUsernameType('not_required');
|
||||
setSelectedStressfreiEmailId('');
|
||||
} else if (c.stressfreiEmailId) {
|
||||
setUsernameType('stressfrei');
|
||||
setSelectedStressfreiEmailId(c.stressfreiEmailId.toString());
|
||||
} else {
|
||||
@@ -576,10 +582,13 @@ export default function ContractForm() {
|
||||
cancellationPeriodId: safeParseInt(data.cancellationPeriodId) ?? null,
|
||||
contractDurationId: safeParseInt(data.contractDurationId) ?? null,
|
||||
commission: data.commission ? parseFloat(data.commission) : null,
|
||||
// Portal-Benutzername: entweder manuell oder Stressfrei-Adresse
|
||||
// Portal-Benutzername: manuell, Stressfrei-Adresse oder Opt-out.
|
||||
// Bei 'not_required' werden Username-Felder + Passwort explizit
|
||||
// geleert und das Flag gesetzt – so verschwindet die Cockpit-Warnung.
|
||||
portalUsername: usernameType === 'manual' ? emptyToNull(data.portalUsername) : null,
|
||||
stressfreiEmailId: usernameType === 'stressfrei' && selectedStressfreiEmailId ? parseInt(selectedStressfreiEmailId) : null,
|
||||
portalPassword: data.portalPassword || undefined, // Passwort: undefined = nicht ändern
|
||||
portalCredentialsNotRequired: usernameType === 'not_required',
|
||||
portalPassword: usernameType === 'not_required' ? undefined : (data.portalPassword || undefined), // Passwort: undefined = nicht ändern
|
||||
notes: emptyToNull(data.notes),
|
||||
cancellationConfirmationDate: data.cancellationConfirmationDate ? new Date(data.cancellationConfirmationDate) : null,
|
||||
cancellationConfirmationOptionsDate: data.cancellationConfirmationOptionsDate ? new Date(data.cancellationConfirmationOptionsDate) : null,
|
||||
@@ -1179,27 +1188,49 @@ export default function ContractForm() {
|
||||
Keine {customerEmailLabel} Adressen für diesen Kunden vorhanden. Bitte zuerst beim Kunden anlegen.
|
||||
</p>
|
||||
)}
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name="usernameType"
|
||||
checked={usernameType === 'not_required'}
|
||||
onChange={() => {
|
||||
setUsernameType('not_required');
|
||||
setValue('portalUsername', '');
|
||||
setValue('portalPassword', '');
|
||||
setSelectedStressfreiEmailId('');
|
||||
}}
|
||||
className="text-blue-600"
|
||||
/>
|
||||
<span className="text-sm">Nicht möglich zu bekommen / nicht benötigt</span>
|
||||
</label>
|
||||
{usernameType === 'not_required' && (
|
||||
<p className="text-xs text-gray-500">
|
||||
Anbieter hat kein Portal oder Kunde bekommt keine Zugangsdaten. Die Warnung „Portal-Zugangsdaten fehlen" im Cockpit wird für diesen Vertrag unterdrückt.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
{isEdit ? 'Neues Passwort (leer lassen = unverändert)' : 'Portal Passwort'}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showPortalPassword ? 'text' : 'password'}
|
||||
{...register('portalPassword')}
|
||||
className="block w-full px-3 py-2 pr-10 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPortalPassword(!showPortalPassword)}
|
||||
className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
{showPortalPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||
</button>
|
||||
{usernameType !== 'not_required' && (
|
||||
<div className="mt-8">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
{isEdit ? 'Neues Passwort (leer lassen = unverändert)' : 'Portal Passwort'}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showPortalPassword ? 'text' : 'password'}
|
||||
{...register('portalPassword')}
|
||||
className="block w-full px-3 py-2 pr-10 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPortalPassword(!showPortalPassword)}
|
||||
className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
{showPortalPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
|
||||
@@ -481,6 +481,9 @@ export interface Contract {
|
||||
* bleibt server-seitig (sanitizeContract strippt `portalPasswordEncrypted`).
|
||||
* Entschlüsselter Wert kommt über `GET /contracts/:id/password`. */
|
||||
hasPortalPassword?: boolean;
|
||||
/** Opt-out: Anbieter hat kein Portal / Kunde bekommt keine Zugangsdaten.
|
||||
* Wenn true, unterdrückt das Cockpit die „Portal-Zugangsdaten fehlen"-Warnung. */
|
||||
portalCredentialsNotRequired?: boolean;
|
||||
notes?: string;
|
||||
// Kündigungsdokumente
|
||||
cancellationLetterPath?: string;
|
||||
|
||||
Reference in New Issue
Block a user