contracts: VVL (Vertragsverlängerung) als Split-Button neben Folgevertrag
VVL = Vertragsverlängerung beim selben Anbieter (vs. Folgevertrag = i.d.R. Anbieterwechsel). Im Gegensatz zu createFollowUpContract wird ALLES kopiert: - Provider, Tarif, Portal-Username/Passwort (verschlüsselt) - Preise (basePrice/unitPrice/bonus etc.) - Notes, Commission, Internet-Zugangsdaten, SIP-Daten, SIM-PINs - ContractDocuments (1:1, gleiche Datei-Referenz) - Detail-Tabellen (Energy/Internet/Mobile/TV/CarInsurance) komplett Berechnet: - newStartDate = oldStartDate + Vertragslaufzeit (Monate aus ContractDuration.code/description geparsed: "24M" / "24 Monate" / "2J") - newEndDate = newStartDate + Laufzeit - status = DRAFT (User bestätigt manuell) NICHT kopiert: - documentType "Auftragsformular" (das wird neu unterschrieben) - cancellation*-Felder (alter Cancel-Flow nicht relevant) Frontend: - Split-Button: Hauptaktion "Folgevertrag anlegen" + ChevronDown-Pfeil - Dropdown: "VVL anlegen" mit Bestätigungs-Modal - Modal zeigt Vorhersage des neuen Startdatums (alter Start + Vertragslaufzeit als Hinweis) History-Einträge wie bei Folgevertrag, mit eigenem VVL-Wording. Doppel-Schutz: maximal 1 Folge-/VVL-Vertrag pro Vorgänger. Live-verifiziert: - Contract #17 (FIBER, 2026-05-01, 24M) → VVL mit Start 2028-05-01 ✓ - Provider/Tarif/Preise/Credentials 1:1 übernommen - 2 Dokumente kopiert (außer Auftragsformular) - History-Einträge in beiden Verträgen vorhanden Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1514,6 +1514,26 @@ export default function ContractDetail() {
|
||||
},
|
||||
});
|
||||
|
||||
// VVL = Vertragsverlängerung beim selben Anbieter (alle Daten 1:1 + Datum berechnet)
|
||||
const renewalMutation = useMutation({
|
||||
mutationFn: () => contractApi.createRenewal(contractId),
|
||||
onSuccess: (data) => {
|
||||
if (data.data) {
|
||||
navigate(`/contracts/${data.data.id}/edit`);
|
||||
} else {
|
||||
alert('VVL wurde erstellt, aber keine ID zurückgegeben');
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('VVL Fehler:', error);
|
||||
alert(`Fehler beim Erstellen der VVL: ${error instanceof Error ? error.message : 'Unbekannter Fehler'}`);
|
||||
},
|
||||
});
|
||||
|
||||
// Dropdown-Toggle für VVL
|
||||
const [showFollowUpMenu, setShowFollowUpMenu] = useState(false);
|
||||
const [showVvlConfirm, setShowVvlConfirm] = useState(false);
|
||||
|
||||
// Un-Snooze Mutation
|
||||
const unsnoozeMutation = useMutation({
|
||||
mutationFn: () => contractApi.snooze(contractId, {}),
|
||||
@@ -1756,14 +1776,50 @@ export default function ContractDetail() {
|
||||
</Link>
|
||||
)}
|
||||
{hasPermission('contracts:create') && !c.followUpContract && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => setShowFollowUpConfirm(true)}
|
||||
disabled={followUpMutation.isPending}
|
||||
>
|
||||
<Copy className="w-4 h-4 mr-2" />
|
||||
{followUpMutation.isPending ? 'Erstelle...' : 'Folgevertrag anlegen'}
|
||||
</Button>
|
||||
<div className="relative inline-flex">
|
||||
{/* Hauptaktion: Folgevertrag anlegen */}
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => setShowFollowUpConfirm(true)}
|
||||
disabled={followUpMutation.isPending || renewalMutation.isPending}
|
||||
className="!rounded-r-none !border-r-0"
|
||||
>
|
||||
<Copy className="w-4 h-4 mr-2" />
|
||||
{followUpMutation.isPending ? 'Erstelle...' : 'Folgevertrag anlegen'}
|
||||
</Button>
|
||||
{/* Dropdown-Pfeil für VVL */}
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => setShowFollowUpMenu(!showFollowUpMenu)}
|
||||
disabled={followUpMutation.isPending || renewalMutation.isPending}
|
||||
className="!rounded-l-none !px-2"
|
||||
title="Weitere Optionen"
|
||||
>
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
</Button>
|
||||
{showFollowUpMenu && (
|
||||
<>
|
||||
{/* Click-outside-Overlay */}
|
||||
<div
|
||||
className="fixed inset-0 z-10"
|
||||
onClick={() => setShowFollowUpMenu(false)}
|
||||
/>
|
||||
<div className="absolute top-full right-0 mt-1 z-20 w-56 bg-white border border-gray-200 rounded-lg shadow-lg py-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShowFollowUpMenu(false);
|
||||
setShowVvlConfirm(true);
|
||||
}}
|
||||
className="w-full text-left px-4 py-2 text-sm hover:bg-gray-100 flex items-center gap-2"
|
||||
>
|
||||
<Copy className="w-4 h-4 text-gray-500" />
|
||||
VVL anlegen
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{c.followUpContract && (
|
||||
<Link to={`/contracts/${c.followUpContract.id}`}>
|
||||
@@ -3077,6 +3133,53 @@ export default function ContractDetail() {
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* VVL Bestätigung */}
|
||||
<Modal
|
||||
isOpen={showVvlConfirm}
|
||||
onClose={() => setShowVvlConfirm(false)}
|
||||
title="Vertragsverlängerung (VVL) anlegen"
|
||||
size="sm"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<p className="text-gray-700">
|
||||
Möchten Sie eine Vertragsverlängerung für diesen Vertrag anlegen?
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
Alle Daten werden 1:1 übernommen (auch Provider, Tarif, Portal-
|
||||
Zugang, Preise und Vertragsdokumente). Das Startdatum wird auf
|
||||
den nächsten Laufzeit-Beginn berechnet (altes Startdatum +
|
||||
Vertragslaufzeit). Das <strong>Auftragsdokument</strong> wird
|
||||
<strong> nicht </strong> mitkopiert – das ist die neue,
|
||||
unterschriebene VVL, die Sie selbst hochladen.
|
||||
</p>
|
||||
{c.startDate && c.contractDuration?.description && (
|
||||
<div className="text-xs text-gray-500 bg-gray-50 p-2 rounded">
|
||||
Vorhersage: alter Beginn{' '}
|
||||
<strong>{new Date(c.startDate).toLocaleDateString('de-DE')}</strong> +{' '}
|
||||
<strong>{c.contractDuration.description}</strong>
|
||||
{' = '}neuer VVL-Beginn (siehe danach im Vertrag)
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-end gap-3 pt-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => setShowVvlConfirm(false)}
|
||||
>
|
||||
Nein
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowVvlConfirm(false);
|
||||
renewalMutation.mutate();
|
||||
}}
|
||||
disabled={renewalMutation.isPending}
|
||||
>
|
||||
{renewalMutation.isPending ? 'Erstelle...' : 'Ja, VVL anlegen'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* Status-Info Modal */}
|
||||
<StatusInfoModal isOpen={showStatusInfo} onClose={() => setShowStatusInfo(false)} />
|
||||
|
||||
|
||||
@@ -657,6 +657,10 @@ export const contractApi = {
|
||||
const res = await api.post<ApiResponse<Contract>>(`/contracts/${id}/follow-up`);
|
||||
return res.data;
|
||||
},
|
||||
createRenewal: async (id: number) => {
|
||||
const res = await api.post<ApiResponse<Contract>>(`/contracts/${id}/renewal`);
|
||||
return res.data;
|
||||
},
|
||||
getPassword: async (id: number) => {
|
||||
const res = await api.get<ApiResponse<{ password: string }>>(`/contracts/${id}/password`);
|
||||
return res.data;
|
||||
|
||||
Reference in New Issue
Block a user