Vertrag kopieren: neuen eigenstaendigen Vertrag aus Vorlage anlegen
Kopieren-Button in der Vertragsansicht oeffnet das Neu-Formular unter /contracts/new?copyFrom=<id> mit allen Daten des Quellvertrags vorbefuellt. Beim Speichern entsteht ein frischer, unabhaengiger Vertrag (kein previousContractId-Link, kein VVL). Use-Case: neuer Mobilfunk-/o.ae. Vertrag ist meist fast identisch; nur Preis, Laufzeit, Kunden-/Vertragsnummer, Anbieter/Tarif aendern sich - der Rest bleibt gleich. Im Kopier-Modus geleert (Integritaet/Eindeutigkeit): Status->DRAFT, Vorgaenger-Link, alle Datumsfelder, Kunden-/Vertragsnummer beim Anbieter + Plattform-Nummern, Portal-Zugang. SIM-Karten & Rufnummern werden nicht uebernommen. Rest (Anbieter/Tarif/Preise/Detailfelder/ Notizen) bleibt als Vorlage. Rein Frontend (ContractForm copy-Mode + Button in ContractDetail), kein neuer Endpoint - nutzt bestehendes Create. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,7 @@ import { buildContractLabelParts } from '../../utils/contractLabel';
|
||||
import type { ContractType } from '../../types';
|
||||
import { formatDate } from '../../utils/dateFormat';
|
||||
import { useProviderSettings } from '../../hooks/useProviderSettings';
|
||||
import { Plus, Trash2, Eye, EyeOff, Info, X, ArrowLeft, ExternalLink, Mail } from 'lucide-react';
|
||||
import { Plus, Trash2, Eye, EyeOff, Info, X, ArrowLeft, ExternalLink, Mail, Copy } from 'lucide-react';
|
||||
|
||||
// Kleine Helper-Komponente: Label + Mini-Link im neuen Tab. Wird neben
|
||||
// Select-Feldern eingesetzt, deren Stammdaten an anderer Stelle gepflegt
|
||||
@@ -100,6 +100,12 @@ export default function ContractForm() {
|
||||
const { customerEmailLabel } = useProviderSettings();
|
||||
|
||||
const preselectedCustomerId = searchParams.get('customerId');
|
||||
// Kopier-Modus: `?copyFrom=<id>` legt einen NEUEN, eigenständigen Vertrag
|
||||
// an, aber mit allen Daten eines Bestandsvertrags vorbefüllt. Kein
|
||||
// Bearbeiten (isEdit bleibt false) → beim Speichern entsteht ein frischer
|
||||
// Vertrag. Unique-/instanzspezifische Felder werden beim Prefill geleert.
|
||||
const copyFromId = searchParams.get('copyFrom');
|
||||
const isCopy = !isEdit && !!copyFromId;
|
||||
|
||||
const { register, handleSubmit, reset, watch, setValue, formState: { errors } } = useForm<any>({
|
||||
defaultValues: {
|
||||
@@ -124,6 +130,13 @@ export default function ContractForm() {
|
||||
enabled: isEdit,
|
||||
});
|
||||
|
||||
// Fetch source contract for copy mode
|
||||
const { data: copySource } = useQuery({
|
||||
queryKey: ['contract', 'copy', copyFromId],
|
||||
queryFn: () => contractApi.getById(parseInt(copyFromId!)),
|
||||
enabled: isCopy,
|
||||
});
|
||||
|
||||
// Fetch customers for dropdown
|
||||
const { data: customersData } = useQuery({
|
||||
queryKey: ['customers-all'],
|
||||
@@ -286,16 +299,19 @@ export default function ContractForm() {
|
||||
useEffect(() => {
|
||||
if (isEdit && contract?.data && !customerId) {
|
||||
setValue('customerId', contract.data.customerId.toString());
|
||||
} else if (isCopy && copySource?.data && !customerId) {
|
||||
setValue('customerId', copySource.data.customerId.toString());
|
||||
}
|
||||
}, [isEdit, contract, customerId, setValue]);
|
||||
}, [isEdit, contract, isCopy, copySource, customerId, setValue]);
|
||||
|
||||
// Reset form when contract data AND ALL dropdown data are loaded
|
||||
// This ensures dropdowns can properly display the selected values
|
||||
// We need customerDetail for addresses/bankCards/documents and providersData for tariffs
|
||||
useEffect(() => {
|
||||
if (contract?.data && platformsData?.data && contractCategoriesData?.data && providersData?.data && customerDetail?.data) {
|
||||
const c = contract.data;
|
||||
reset({
|
||||
const src = isEdit ? contract?.data : (isCopy ? copySource?.data : null);
|
||||
if (src && platformsData?.data && contractCategoriesData?.data && providersData?.data && customerDetail?.data) {
|
||||
const c = src;
|
||||
const values: any = {
|
||||
customerId: c.customerId.toString(),
|
||||
type: c.type,
|
||||
status: c.status,
|
||||
@@ -383,10 +399,41 @@ export default function ContractForm() {
|
||||
previousProviderId: c.previousProviderId?.toString() || '',
|
||||
previousCustomerNumber: c.previousCustomerNumber || '',
|
||||
previousContractNumber: c.previousContractNumber || '',
|
||||
});
|
||||
};
|
||||
|
||||
// Kopier-Modus: instanz-spezifische / eindeutige Felder leeren, damit
|
||||
// ein echter neuer eigenständiger Vertrag entsteht und keine Nummern
|
||||
// versehentlich doppelt vergeben werden. Der Rest (Tarif, Anbieter,
|
||||
// Preise, Detailfelder …) bleibt als Vorlage erhalten und wird vom
|
||||
// User bei Bedarf angepasst.
|
||||
if (isCopy) {
|
||||
Object.assign(values, {
|
||||
status: 'DRAFT',
|
||||
previousContractId: '',
|
||||
// Neuer Vertrag = neuer Zeitraum
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
installationDate: '',
|
||||
cancellationConfirmationDate: '',
|
||||
cancellationConfirmationOptionsDate: '',
|
||||
wasSpecialCancellation: false,
|
||||
// Eindeutige Kunden-/Vertragsnummern beim Anbieter & Plattform
|
||||
customerNumberAtProvider: '',
|
||||
contractNumberAtProvider: '',
|
||||
orderNumberAtSalesPlatform: '',
|
||||
customerNumberAtSalesPlatform: '',
|
||||
contractNumberAtSalesPlatform: '',
|
||||
// Portal-Zugang gehört zum konkreten Vertrag → frisch
|
||||
portalUsername: '',
|
||||
});
|
||||
}
|
||||
|
||||
reset(values);
|
||||
|
||||
// SIM-Karten & Rufnummern sind hardware-/vertragsspezifisch (eindeutige
|
||||
// Karten-/Telefonnummern) → im Kopier-Modus NICHT übernehmen.
|
||||
// Load simCards if available
|
||||
if (c.mobileDetails?.simCards && c.mobileDetails.simCards.length > 0) {
|
||||
if (!isCopy && c.mobileDetails?.simCards && c.mobileDetails.simCards.length > 0) {
|
||||
setSimCards(c.mobileDetails.simCards.map(sc => ({
|
||||
id: sc.id,
|
||||
phoneNumber: sc.phoneNumber || '',
|
||||
@@ -405,7 +452,7 @@ export default function ContractForm() {
|
||||
}
|
||||
|
||||
// Load phoneNumbers if available (Internet contracts)
|
||||
if (c.internetDetails?.phoneNumbers && c.internetDetails.phoneNumbers.length > 0) {
|
||||
if (!isCopy && c.internetDetails?.phoneNumbers && c.internetDetails.phoneNumbers.length > 0) {
|
||||
setPhoneNumbers(c.internetDetails.phoneNumbers.map(pn => {
|
||||
// Wenn areaCode gepflegt ist → 1:1 nutzen, sonst aus phoneNumber
|
||||
// heuristisch splitten (dann sollte der User das Ergebnis prüfen
|
||||
@@ -448,8 +495,11 @@ export default function ContractForm() {
|
||||
// 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) {
|
||||
// überstimmt. Im Kopier-Modus immer frisch (manuell, leer).
|
||||
if (isCopy) {
|
||||
setUsernameType('manual');
|
||||
setSelectedStressfreiEmailId('');
|
||||
} else if (c.portalCredentialsNotRequired) {
|
||||
setUsernameType('not_required');
|
||||
setSelectedStressfreiEmailId('');
|
||||
} else if (c.stressfreiEmailId) {
|
||||
@@ -463,7 +513,7 @@ export default function ContractForm() {
|
||||
// Mark contract as loaded so provider change detection works correctly
|
||||
setIsContractLoaded(true);
|
||||
}
|
||||
}, [contract, reset, platformsData, contractCategoriesData, providersData, customerDetail]);
|
||||
}, [contract, copySource, isEdit, isCopy, reset, platformsData, contractCategoriesData, providersData, customerDetail]);
|
||||
|
||||
// Watch für Enddatum-Berechnung
|
||||
const startDate = watch('startDate');
|
||||
@@ -806,9 +856,16 @@ export default function ContractForm() {
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
</Button>
|
||||
<h1 className="text-2xl font-bold">
|
||||
{isEdit ? 'Vertrag bearbeiten' : 'Neuer Vertrag'}
|
||||
{isEdit ? 'Vertrag bearbeiten' : isCopy ? 'Neuer Vertrag (Kopie)' : 'Neuer Vertrag'}
|
||||
</h1>
|
||||
</div>
|
||||
{isCopy && (
|
||||
<p className="ml-12 mt-1 text-sm text-blue-700 bg-blue-50 border border-blue-200 rounded px-2 py-1 inline-flex items-center gap-1">
|
||||
<Copy className="w-4 h-4" />
|
||||
Kopie eines bestehenden Vertrags – wird als <strong>neuer eigenständiger Vertrag</strong> gespeichert.
|
||||
Anbieter/Tarif, Preise, Kunden-/Vertragsnummern und Daten bitte prüfen.
|
||||
</p>
|
||||
)}
|
||||
{customer && (
|
||||
<p className="text-gray-500 ml-12 flex items-center gap-1">
|
||||
Kunde:{' '}
|
||||
|
||||
Reference in New Issue
Block a user