import { CommonModule } from '@angular/common'; import { Component, EventEmitter, inject, Input, Output } from '@angular/core'; import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms'; import { NotificationService } from '../../core/services/common/notification.service'; import { AngularMaterialModule } from '../../shared/module/angular-material.module'; import { ShippingService } from '../../core/services/carnet/shipping.service'; import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service'; import { CommonService } from '../../core/services/common/common.service'; import { Shipping } from '../../core/models/carnet/shipping'; import { Country } from '../../core/models/country'; import { State } from '../../core/models/state'; import { ZipCodeValidator } from '../../shared/validators/zipcode-validator'; import { finalize, forkJoin, map, of, Subject, switchMap, takeUntil, throwError } from 'rxjs'; import { DeliveryType } from '../../core/models/delivery-type'; import { DeliveryMethod } from '../../core/models/delivery-method'; import { PaymentType } from '../../core/models/payment-type'; import { format, isAfter, isWeekend, addBusinessDays } from 'date-fns'; import { MatDialog } from '@angular/material/dialog'; import { ShippingContact } from '../../core/models/carnet/shipping-contact'; import { ShippingAddress } from '../../core/models/carnet/shipping-address'; import { ContactDialogComponent } from './contact-dialog.component'; import { HolderService } from '../../core/services/carnet/holder.service'; import { Holder } from '../../core/models/carnet/holder'; import { NavigationService } from '../../core/services/common/navigation.service'; import { StorageService } from '../../core/services/common/storage.service'; import { FormOfSecurity } from '../../core/models/formofsecurity'; import { CarnetService } from '../../core/services/carnet/carnet.service'; import { environment } from '../../../environments/environment'; @Component({ selector: 'app-shipping', imports: [AngularMaterialModule, CommonModule, ReactiveFormsModule], templateUrl: './shipping.component.html', styleUrl: './shipping.component.scss' }) export class ShippingComponent { @Input() headerid: number = 0; @Input() isEditMode = false; @Input() isViewMode = false; @Input() enableSubmitButton = false; @Input() applicationName: string = ''; @Output() completed = new EventEmitter(); private fb = inject(FormBuilder); private dialog = inject(MatDialog); private shippingService = inject(ShippingService); private notificationService = inject(NotificationService); private errorHandler = inject(ApiErrorHandlerService); private commonService = inject(CommonService); private holderService = inject(HolderService); private navigationService = inject(NavigationService); private storageService = inject(StorageService); private carnetService = inject(CarnetService); shippingForm: FormGroup; isLoading = false; changeInProgress = false; showAddressForm = false; showContactForm = false; deliveryEstimate: string = ''; holderid: number = 0; holder: Holder | undefined | null = null; showSubmitButton: boolean = environment.appType === 'client'; showProcessButton: boolean = environment.appType === 'service-provider'; countriesHasStates = ['US', 'CA', 'MX']; countries: Country[] = []; states: State[] = []; deliveryTypes: DeliveryType[] = []; deliveryMethods: DeliveryMethod[] = []; paymentTypes: PaymentType[] = []; formOfSecurities: FormOfSecurity[] = []; govFormOfSecurities: FormOfSecurity[] = []; nonGovFormOfSecurities: FormOfSecurity[] = []; shippingFromDB: Shipping | null = null; private destroy$ = new Subject(); preparerAddress: ShippingAddress | null | undefined = null; holderAddress: ShippingAddress | null | undefined = null; preparerContact: ShippingContact | null | undefined = null; preparerContacts: ShippingContact[] = []; holderContact: ShippingContact | null | undefined = null; holderContacts: ShippingContact[] = []; constructor() { this.shippingForm = this.fb.group({ // needsBond: [false], needsInsurance: [false], needsLostDocProtection: [false], formOfSecurity: ['', Validators.required], shipTo: ['PREPARER', Validators.required], address: this.fb.group({ companyName: [''], address1: [''], address2: [''], city: [''], state: [''], country: ['US'], zip: [''], }), contact: this.fb.group({ firstName: [''], lastName: [''], middleInitial: [''], title: [''], phone: [''], mobile: [''], fax: [''], email: [''], refNumber: [''], notes: [''] }), deliveryType: ['', Validators.required], deliveryMethod: ['', Validators.required], courierAccount: [''], paymentMethod: ['', Validators.required], paymentNotes: [''] }); this.shippingForm.get('deliveryType')?.valueChanges.subscribe(() => { this.calculateDeliveryEstimate(); }); } ngOnInit(): void { this.loadCountries(); this.loadDeliveryTypes(); this.loadDeliveryMethods(); this.loadPaymentTypes(); this.loadFormOfSecurities(); if (this.headerid && this.headerid > 0) { this.loadShippingData(); } } ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); } onSubmit(submitApp: boolean): void { if (this.shippingForm.invalid) { this.shippingForm.markAllAsTouched(); return; } this.changeInProgress = true; const shippingData: Shipping = this.shippingForm.value; if (shippingData.shipTo !== '3RDPARTY') { shippingData.address = shippingData.shipTo === 'PREPARER' ? this.preparerAddress ?? undefined : this.holderAddress ?? undefined; shippingData.contact = shippingData.shipTo === 'PREPARER' ? this.preparerContact ?? undefined : this.holderContact ?? undefined; } if (this.holder?.holderType.trim() === 'GOV') { shippingData.formOfSecurity = this.govFormOfSecurities?.[0].value; } this.shippingService.saveShippingDetails(this.headerid, shippingData).pipe(finalize(() => { this.changeInProgress = false; })).subscribe({ next: () => { this.notificationService.showSuccess('Shipping & payments information saved successfully'); this.completed.emit(true); if (this.enableSubmitButton && this.headerid && submitApp) { let currentApplicationDetails = { headerid: this.headerid, applicationName: this.applicationName } this.storageService.set("currentapplication", currentApplicationDetails); this.navigationService.navigate(["terms"]) } }, error: (error) => { let errorMessage = this.errorHandler.handleApiError(error, 'Failed to save shipping and payment information'); this.notificationService.showError(errorMessage); } }); } onDeliveryTypeChange(): void { this.calculateDeliveryEstimate(); } onDeliveryMethodChange(deliveryMethod: string): void { const courierControl = this.shippingForm.get('courierAccount'); if (deliveryMethod === 'CLC') { courierControl?.setValidators([Validators.required]); } else { courierControl?.clearValidators(); } courierControl?.updateValueAndValidity(); } onCountryChange(country: string): void { this.shippingForm.get('address.state')?.reset(); if (country) { this.loadStates(country); } this.shippingForm.get('address.zip')?.updateValueAndValidity(); } public refreshShippingData(): void { if (this.headerid > 0) { this.loadAddressContactData(); } } editAddressForm(): void { this.showAddressForm = true; let shipTo = this.shippingForm.get('shipTo')?.value; if (shipTo === 'PREPARER' && this.preparerAddress) { this.shippingForm.get('address')?.patchValue(this.preparerAddress); this.loadStates(this.preparerAddress.country); } else if (shipTo === 'HOLDER' && this.holderAddress) { this.shippingForm.get('address')?.patchValue(this.holderAddress); this.loadStates(this.holderAddress.country); } } cancelEditAddressForm(): void { this.showAddressForm = false; this.shippingForm.get('address')?.reset({ country: 'US' }); this.loadStates('US'); } onShipToChange(event: any): void { const shipTo = event.value; this.showAddressForm = false; this.showContactForm = false; this.updateAddressContactValidation(shipTo); if (shipTo === '3RDPARTY') { this.shippingForm.get('contact')?.reset(); this.shippingForm.get('address')?.reset({ country: 'US' }); this.loadStates('US'); this.showAddressForm = true; this.showContactForm = true; } } loadCountries(): void { this.commonService.getCountries() .pipe(takeUntil(this.destroy$)) .subscribe({ next: (countries) => { this.countries = countries; }, error: (error) => { console.error('Failed to load countries', error); } }); } loadDeliveryTypes(): void { this.commonService.getDeliveryTypes() .pipe(takeUntil(this.destroy$)) .subscribe({ next: (deliveryTypes) => { this.deliveryTypes = deliveryTypes; }, error: (error) => { console.error('Failed to load delivery types', error); } }); } loadDeliveryMethods(): void { this.commonService.getDeliveryMethods() .pipe(takeUntil(this.destroy$)) .subscribe({ next: (deliveryMethods) => { this.deliveryMethods = deliveryMethods; }, error: (error) => { console.error('Failed to load delivery methods', error); } }); } loadPaymentTypes(): void { this.commonService.getPaymentTypes() .pipe(takeUntil(this.destroy$)) .subscribe({ next: (paymentTypes) => { this.paymentTypes = paymentTypes; }, error: (error) => { console.error('Failed to load payment types', error); } }); } loadStates(country: string): void { this.isLoading = true; country = this.countriesHasStates.includes(country) ? country : 'FN'; this.commonService.getStates(country) .pipe(takeUntil(this.destroy$), finalize(() => { this.isLoading = false; })).subscribe({ next: (states) => { this.states = states; const stateControl = this.shippingForm.get('contact.state'); if (this.countriesHasStates.includes(country)) { stateControl?.enable(); } else { stateControl?.disable(); stateControl?.setValue('FN'); } }, error: (error) => { console.error('Failed to load states', error); } }); } loadFormOfSecurities(): void { this.commonService.getFormOfSecurities() .pipe(takeUntil(this.destroy$)) .subscribe({ next: (fos) => { this.govFormOfSecurities = fos.filter(f => f.isGov); this.nonGovFormOfSecurities = fos.filter(f => !f.isGov); }, error: (error) => { console.error('Failed to load form of securities', error); } }); } loadShippingData(): void { this.isLoading = true; this.shippingService.getShippingData(this.headerid).pipe( finalize(() => { this.isLoading = false; }) ).subscribe({ next: (results: Shipping) => { if (!results) { // do nothing if empty return; } this.shippingFromDB = results; this.patchShippingData(); this.loadAddressContactData(); }, error: (error: any) => { console.error('Error loading shipping data', error); const errorMessage = this.errorHandler.handleApiError(error, 'Failed to load shipping data'); this.notificationService.showError(errorMessage); } }); } loadAddressContactData(): void { this.isLoading = true; this.holderService.getHolder(this.headerid).pipe( switchMap((holder: Holder) => { if (!holder?.holderid) { return of(undefined); } this.holder = holder; this.formOfSecurities = holder.holderType?.trim() === 'GOV' ? this.govFormOfSecurities : this.nonGovFormOfSecurities; return forkJoin({ holderContacts: this.shippingService.getHolderContactsById(holder.holderid), holderAddress: this.shippingService.getHolderAddress(holder), preparerAddress: this.shippingService.getPreparerAddress(), preparerContacts: this.shippingService.getPreparerContacts() }).pipe( // Combine the forkJoin results with the original holder object map((apiResults: any) => ({ ...apiResults, holder })) ); }), finalize(() => this.isLoading = false) ).subscribe({ next: (results) => { if (!results && !this.shippingFromDB) { // do nothing if empty return; } this.holderid = results.holder.holderid; this.holderContacts = results.holderContacts; this.holderAddress = results.holderAddress; this.preparerContacts = results.preparerContacts; this.preparerAddress = results.preparerAddress; // Set holder contact if (this.shippingFromDB?.shipTo === 'HOLDER' && this.shippingFromDB?.contact?.contactid) { this.holderContact = this.holderContacts.find(hc => hc.contactid === this.shippingFromDB?.contact?.contactid); } else { this.holderContact = this.holderContacts?.[0]; } // Set preparer contact if (this.shippingFromDB?.shipTo === 'PREPARER' && this.shippingFromDB?.contact?.contactid) { this.preparerContact = this.preparerContacts.find(pc => pc.contactid === this.shippingFromDB?.contact?.contactid); } else { this.preparerContact = this.preparerContacts.find(pc => pc.defaultContact); } this.patchAddressContactData(); }, error: (error: any) => { console.error('Error loading shipping data', error); const errorMessage = this.errorHandler.handleApiError(error, 'Failed to load shipping data'); this.notificationService.showError(errorMessage); } }); } patchShippingData(): void { if (!this.shippingFromDB) { return; } this.shippingForm.patchValue({ // needsBond: shipping.needsBond, needsInsurance: this.shippingFromDB.needsInsurance, needsLostDocProtection: this.shippingFromDB.needsLostDocProtection, formOfSecurity: this.shippingFromDB.formOfSecurity, shipTo: this.shippingFromDB.shipTo, deliveryType: this.shippingFromDB.deliveryType, deliveryMethod: this.shippingFromDB.deliveryMethod, courierAccount: this.shippingFromDB.courierAccount, paymentMethod: this.shippingFromDB.paymentMethod }); if (this.shippingFromDB.deliveryMethod === 'CLC') { this.onDeliveryMethodChange(this.shippingFromDB.deliveryMethod); } this.calculateDeliveryEstimate(); this.shippingForm.markAsUntouched(); this.completed.emit(this.shippingForm.valid); } patchAddressContactData(): void { if (!this.shippingFromDB) { return } if (this.shippingFromDB.address?.country) { this.loadStates(this.shippingFromDB.address?.country); } let formOfSecurityControl = this.shippingForm.get('formOfSecurity'); if (this.holder?.holderType.trim() === 'GOV') { formOfSecurityControl?.setValue(this.govFormOfSecurities?.[0].value); formOfSecurityControl?.disable(); } else { formOfSecurityControl?.enable(); } this.calculateDeliveryEstimate(); this.updateAddressContactValidation(this.shippingFromDB.shipTo); if (this.shippingFromDB.shipTo === '3RDPARTY') { this.showAddressForm = true; this.showContactForm = true; const addressGroup = this.shippingForm.get('address') as FormGroup; const contactGroup = this.shippingForm.get('contact') as FormGroup; addressGroup.patchValue({ addressid: this.shippingFromDB.address?.addressid, companyName: this.shippingFromDB.address?.companyName, address1: this.shippingFromDB.address?.address1, address2: this.shippingFromDB.address?.address2, city: this.shippingFromDB.address?.city, state: this.shippingFromDB.address?.state, zip: this.shippingFromDB.address?.zip, country: this.shippingFromDB.address?.country }) contactGroup.patchValue({ contactid: this.shippingFromDB.contact?.contactid, firstName: this.shippingFromDB.contact?.firstName, lastName: this.shippingFromDB.contact?.lastName, middleInitial: this.shippingFromDB.contact?.middleInitial, title: this.shippingFromDB.contact?.title, phone: this.shippingFromDB.contact?.phone, mobile: this.shippingFromDB.contact?.mobile, fax: this.shippingFromDB.contact?.fax, email: this.shippingFromDB.contact?.email, refNumber: this.shippingFromDB.contact?.refNumber, notes: this.shippingFromDB.contact?.notes, }) } this.shippingForm.markAsUntouched(); this.completed.emit(this.shippingForm.valid); } updateAddressContactValidation(shipTo: string): void { const addressGroup = this.shippingForm.get('address') as FormGroup; const contactGroup = this.shippingForm.get('contact') as FormGroup; if (shipTo === '3RDPARTY') { Object.keys(addressGroup.controls).forEach(key => { if (key === 'companyName') { addressGroup.get(key)?.setValidators([Validators.required, Validators.maxLength(100)]); } else if (key === 'address1') { addressGroup.get(key)?.setValidators([Validators.required, Validators.maxLength(100)]); } else if (key === 'address2') { addressGroup.get(key)?.setValidators([Validators.maxLength(100)]); } else if (key === 'city') { addressGroup.get(key)?.setValidators([Validators.required, Validators.maxLength(50)]); } else if (key === 'state') { addressGroup.get(key)?.setValidators(Validators.required); } else if (key === 'country') { addressGroup.get(key)?.setValidators(Validators.required); } else if (key === 'zip') { addressGroup.get(key)?.setValidators([Validators.required, ZipCodeValidator('country')]); } addressGroup.get(key)?.updateValueAndValidity(); }); Object.keys(contactGroup.controls).forEach(key => { if (key === 'firstName') { contactGroup.get(key)?.setValidators([Validators.required, Validators.maxLength(50)]); } else if (key === 'lastName') { contactGroup.get(key)?.setValidators([Validators.required, Validators.maxLength(50)]); } else if (key === 'middleInitial') { contactGroup.get(key)?.setValidators([Validators.maxLength(1)]); } else if (key === 'title') { contactGroup.get(key)?.setValidators([Validators.required, Validators.maxLength(100)]); } else if (key === 'phone') { contactGroup.get(key)?.setValidators([Validators.required, Validators.pattern(/^[0-9\-\(\)]{10,15}$/)]); } else if (key === 'mobile') { contactGroup.get(key)?.setValidators([Validators.required, Validators.pattern(/^[0-9\-\(\)]{10,15}$/)]); } else if (key === 'fax') { contactGroup.get(key)?.setValidators([Validators.pattern(/^[0-9\-\(\)]{10,15}$/)]); } else if (key === 'email') { contactGroup.get(key)?.setValidators([Validators.required, Validators.email, Validators.maxLength(100)]); } contactGroup.get(key)?.updateValueAndValidity(); }); } else { Object.keys(addressGroup.controls).forEach(key => { addressGroup.get(key)?.clearValidators(); addressGroup.get(key)?.updateValueAndValidity(); }); Object.keys(contactGroup.controls).forEach(key => { contactGroup.get(key)?.clearValidators(); contactGroup.get(key)?.updateValueAndValidity(); }); } } getAddressLabel(): string { let shipTo = this.shippingForm.get('shipTo')?.value; if (shipTo === 'PREPARER' && this.preparerAddress) { return `${this.preparerAddress.companyName}, ${this.preparerAddress.address1}, ${this.preparerAddress.city}, ${this.preparerAddress.state}, ${this.preparerAddress.zip}, ${this.preparerAddress.country}`; } if (shipTo === 'HOLDER' && this.holderAddress) { return `${this.holderAddress.companyName}, ${this.holderAddress.address1}, ${this.holderAddress.city}, ${this.holderAddress.state}, ${this.holderAddress.zip}, ${this.holderAddress.country}`; } return ''; } getContactLabel(): string { const shipTo = this.shippingForm.get('shipTo')?.value; const contact = shipTo === 'PREPARER' ? this.preparerContact : shipTo === 'HOLDER' ? this.holderContact : null; if (!contact) { return 'No contact information available'; } // Build name parts, filtering out null/undefined/empty strings const nameParts = [ contact.firstName, contact.middleInitial, contact.lastName ].filter(part => part != null && part.trim() !== ''); // Build contact info parts const contactInfoParts = [ contact.email, contact.phone ].filter(part => part != null && part.trim() !== ''); // Combine all non-empty parts const allParts = [ nameParts.join(' '), // Join name parts with single spaces ...contactInfoParts // Add email and phone if they exist ].filter(part => part.trim() !== ''); return allParts.join(', ') || ''; } calculateDeliveryEstimate(): void { const deliveryType = this.shippingForm.get('deliveryType')?.value; const deliveryTypeObj = this.deliveryTypes.find(dt => dt.value === deliveryType); const daysToDelivery: number = Number(deliveryTypeObj?.daysToDelivery) !== 0 ? Number(deliveryTypeObj?.daysToDelivery) : 3; const cutOffTime: number = Number(deliveryTypeObj?.cutOffTime) !== 0 ? Number(deliveryTypeObj?.cutOffTime) : 16; const now = new Date(); const cutoffTime = new Date(); cutoffTime.setHours(cutOffTime, 0, 0, 0); let deliveryDate: Date; let message = 'Estimated delivery: '; const isAfterCutoff = isAfter(now, cutoffTime); const isWeekendDay = isWeekend(now); switch (deliveryType) { case 'SAME': if (isAfterCutoff || isWeekendDay) { deliveryDate = addBusinessDays(now, 1); message += format(deliveryDate, 'EEEE, MMMM do, yyyy'); } else { message += format(now, 'EEEE, MMMM do, yyyy'); } break; case 'STD': // Standard is 3 business days if (isAfterCutoff || isWeekendDay) { // If after cutoff or weekend, add 4 business days deliveryDate = addBusinessDays(now, daysToDelivery); } else { // Before cutoff on weekday, add 3 business days deliveryDate = addBusinessDays(now, daysToDelivery - 1); } message += format(deliveryDate, 'EEEE, MMMM do, yyyy'); break; case 'NBD': // Next business day for pickup if (isAfterCutoff || isWeekendDay) { // If after cutoff or weekend, add 2 business days deliveryDate = addBusinessDays(now, daysToDelivery); } else { // Before cutoff on weekday, add 1 business day deliveryDate = addBusinessDays(now, daysToDelivery); } message += format(deliveryDate, 'EEEE, MMMM do, yyyy'); break; default: message = ''; } this.deliveryEstimate = message; } selectContact(): void { let shipTo = this.shippingForm.get('shipTo')?.value; const contacts = shipTo === 'PREPARER' ? this.preparerContacts : this.holderContacts; const currentContact = shipTo === 'PREPARER' ? this.preparerContact : this.holderContact; const dialogRef = this.dialog.open(ContactDialogComponent, { width: '500px', data: { contacts: contacts, currentContact: currentContact } }); dialogRef.afterClosed().subscribe(selectedItem => { if (selectedItem) { const selectedContact = contacts.find(c => c.contactid === selectedItem.contactid); if (shipTo === 'PREPARER') { this.preparerContact = selectedContact; } else { this.holderContact = selectedContact; } } }); } submitApplication(): void { this.onSubmit(true); } returnToHome(): void { this.navigationService.navigate(["home"]) } processApplication(): void { this.changeInProgress = true; this.carnetService.processApplication(this.headerid).pipe(finalize(() => { this.changeInProgress = false; })).subscribe({ next: () => { this.notificationService.showSuccess('Application processed successfully'); this.navigationService.navigate(["home"]); }, error: (error) => { let errorMessage = this.errorHandler.handleApiError(error, 'Failed to submit application'); this.notificationService.showError(errorMessage); console.error('Error submitting the application', error); } }); } }