@@ -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,43 @@ 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-Zugangsdaten (Benutzername/Stressfrei-Verknüpfung) bewusst
// BEHALTEN – bei gleichem Anbieter oft identisch. Das Passwort liegt
// verschlüsselt vor und wird (wie beim Bearbeiten) nicht ins Feld
// geladen; es bleibt leer und kann bei Bedarf neu gesetzt werden.
} ) ;
}
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 +454,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,7 +497,8 @@ 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.
// überstimmt. Gilt auch im Kopier-Modus: Portal-Zugangsdaten werden
// mitkopiert (nur das Passwort bleibt leer, s.o.).
if ( c . portalCredentialsNotRequired ) {
setUsernameType ( 'not_required' ) ;
setSelectedStressfreiEmailId ( '' ) ;
@@ -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,17 @@ 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 - / V e r t r a g s n u m m e r n u n d D a t e n b i t t e p r ü f e n . P o r t a l - B e n u t z e r n a m e
ü bernommen , Passwort ggf . neu setzen .
< / p >
) }
{ customer && (
< p className = "text-gray-500 ml-12 flex items-center gap-1" >
Kunde : { ' ' }