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 { Subject, takeUntil } 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, addDays, isAfter, isWeekend } 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'; @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; @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); shippingForm: FormGroup; isLoading = false; showAddressForm = false; showContactForm = false; deliveryEstimate: string = ''; countriesHasStates = ['US', 'CA', 'MX']; countries: Country[] = []; states: State[] = []; deliveryTypes: DeliveryType[] = []; deliveryMethods: DeliveryMethod[] = []; paymentTypes: PaymentType[] = []; private destroy$ = new Subject(); // preparer contact and address mock data preparerContact: ShippingContact | null | undefined = null; preparerContacts: ShippingContact[] = [ { contactid: 1, firstName: 'John', lastName: 'Doe', middleInitial: 'A', title: 'Mr.', phone: '1234567890', mobile: '0987654321', fax: '1234567890', email: 'j@doe.com', defaultContact: true }, { contactid: 2, firstName: 'Jane', lastName: 'Smith', middleInitial: 'B', title: 'Ms.', phone: '2345678901', mobile: '1098765432', fax: '2345678901', email: 'jan@sm.cm', defaultContact: false }]; preparerAddress = { companyName: 'ABC Company', address1: '123 Main St', address2: 'Suite 100', city: 'Anytown', state: 'CA', zip: '12345', country: 'US' }; holderAddress = { companyName: 'XYZ Company', address1: '456 Holder St', address2: 'Apt 200', city: 'Othertown', state: 'NY', zip: '67890', country: 'US' }; holderContact: ShippingContact | null | undefined = null; holderContacts: ShippingContact[] = [ { contactid: 1, firstName: 'John', lastName: 'Doe', middleInitial: 'A', title: 'Mr.', phone: '1234567890', mobile: '0987654321', fax: '1234567890', email: 'j@doe.com', defaultContact: false }, { contactid: 2, firstName: 'Jane', lastName: 'Smith', middleInitial: 'B', title: 'Ms.', phone: '2345678901', mobile: '1098765432', fax: '2345678901', email: 'jan@sm.cm', defaultContact: true }]; constructor() { this.shippingForm = this.fb.group({ needsBond: [false], needsInsurance: [false], needsLostDocProtection: [false], shipTo: ['preparer', Validators.required], address: this.fb.group({ address1: [''], address2: [''], city: [''], state: [''], country: [''], 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('deliveryMethod')?.valueChanges.subscribe(value => { }); this.shippingForm.get('deliveryType')?.valueChanges.subscribe(() => { this.calculateDeliveryEstimate(); }); // this.shippingForm.valueChanges.subscribe(() => { // this.completed.emit(this.shippingForm.valid); // }); } ngOnInit(): void { this.loadCountries(); this.loadDeliveryTypes(); this.loadDeliveryMethods(); this.loadPaymentTypes(); if (this.headerid && this.isEditMode) { this.loadShippingData(); // TODO this.preparerContact = this.preparerContacts.find(pc => pc.defaultContact); this.holderContact = this.holderContacts.find(hc => hc.defaultContact); } } ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); } onSubmit(): void { if (this.shippingForm.invalid) { this.shippingForm.markAllAsTouched(); return; } this.isLoading = true; const shippingData = this.shippingForm.value; this.shippingService.saveShippingDetails(this.headerid, shippingData).subscribe({ next: () => { this.notificationService.showSuccess('Shipping & payments information saved successfully'); this.completed.emit(true); this.isLoading = false; }, error: (error) => { let errorMessage = this.errorHandler.handleApiError(error, 'Failed to save shipping and payment information'); this.notificationService.showError(errorMessage); this.isLoading = false; } }); } 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(); } editAddressForm(): void { this.showAddressForm = true; let shipTo = this.shippingForm.get('shipTo')?.value; if (shipTo === 'preparer') { this.shippingForm.get('address')?.patchValue(this.preparerAddress); this.loadStates(this.preparerAddress.country); } else if (shipTo === 'holder') { this.shippingForm.get('address')?.patchValue(this.holderAddress); this.loadStates(this.preparerAddress.country); } } cancelEditAddressForm(): void { this.showAddressForm = false; this.shippingForm.get('address')?.reset(); } onShipToChange(event: any): void { const shipTo = event.value; this.showAddressForm = false; this.showContactForm = false; this.updateShippingValidation(shipTo); if (shipTo === 'thirdParty') { this.shippingForm.get('contact')?.reset(); this.shippingForm.get('address')?.reset(); this.showAddressForm = true; this.showContactForm = true; } } loadCountries(): void { this.commonService.getCountries(0) .pipe(takeUntil(this.destroy$)) .subscribe({ next: (countries) => { this.countries = countries; }, error: (error) => { console.error('Failed to load countries', error); this.isLoading = false; } }); } loadDeliveryTypes(): void { this.commonService.getDeliveryTypes(0) .pipe(takeUntil(this.destroy$)) .subscribe({ next: (deliveryTypes) => { this.deliveryTypes = deliveryTypes; }, error: (error) => { console.error('Failed to load delivery types', error); this.isLoading = false; } }); } loadDeliveryMethods(): void { this.commonService.getDeliveryMethods(0) .pipe(takeUntil(this.destroy$)) .subscribe({ next: (deliveryMethods) => { this.deliveryMethods = deliveryMethods; }, error: (error) => { console.error('Failed to load delivery methods', error); this.isLoading = false; } }); } loadPaymentTypes(): void { this.commonService.getPaymentTypes(0) .pipe(takeUntil(this.destroy$)) .subscribe({ next: (paymentTypes) => { this.paymentTypes = paymentTypes; }, error: (error) => { console.error('Failed to load payment types', error); this.isLoading = false; } }); } loadStates(country: string): void { this.isLoading = true; country = this.countriesHasStates.includes(country) ? country : 'FN'; this.commonService.getStates(country, 0) .pipe(takeUntil(this.destroy$)) .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'); } this.isLoading = false; }, error: (error) => { console.error('Failed to load states', error); this.isLoading = false; } }); } loadShippingData(): void { this.isLoading = true; this.shippingService.getShippingData(this.headerid).subscribe({ next: (data: Shipping) => { this.patchShippingData(data); this.isLoading = false; }, error: (error: any) => { let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load shipping and payments data'); this.notificationService.showError(errorMessage); this.isLoading = false; } }); } patchShippingData(shipping: Shipping): void { this.shippingForm.patchValue({ needsBond: shipping.needsBond, needsInsurance: shipping.needsInsurance, needsLostDocProtection: shipping.needsLostDocProtection, shipTo: shipping.shipTo, deliveryType: shipping.deliveryType, deliveryMethod: shipping.deliveryMethod, courierAccount: shipping.courierAccount, paymentMethod: shipping.paymentMethod }); if (shipping.address?.country) { this.loadStates(shipping.address?.country); } if (shipping.deliveryMethod === 'CLC') { this.onDeliveryMethodChange(shipping.deliveryMethod); } this.calculateDeliveryEstimate(); this.updateShippingValidation(shipping.shipTo); if (shipping.shipTo === 'thirdParty') { 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: shipping.address?.addressid, address1: shipping.address?.address1, address2: shipping.address?.address2, city: shipping.address?.city, state: shipping.address?.state, zip: shipping.address?.zip, country: shipping.address?.country }) contactGroup.patchValue({ contactid: shipping.contact?.contactid, firstName: shipping.contact?.firstName, lastName: shipping.contact?.lastName, middleInitial: shipping.contact?.middleInitial, title: shipping.contact?.title, phone: shipping.contact?.phone, mobile: shipping.contact?.mobile, fax: shipping.contact?.fax, email: shipping.contact?.email, refNumber: shipping.contact?.refNumber, notes: shipping.contact?.notes, }) } } loadPreparerContacts(): void { this.isLoading = true; this.shippingService.getPreparerContactsById().subscribe({ next: (data: ShippingContact) => { // this.preparerContacts = data; this.preparerContact = this.preparerContacts.find(pc => pc.defaultContact); }, error: (error: any) => { let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load preparer contacts data'); this.notificationService.showError(errorMessage); this.isLoading = false; } }); } loadHolderContacts(): void { this.isLoading = true; this.shippingService.getHolderContactsById(0).subscribe({ next: (data: ShippingContact) => { // this.holderContacts = data; this.holderContact = this.holderContacts.find(hc => hc.defaultContact); }, error: (error: any) => { let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load holder contacts data'); this.notificationService.showError(errorMessage); this.isLoading = false; } }); } loadPreparerAddress(): void { this.isLoading = true; this.shippingService.getPreparerAddress().subscribe({ next: (data: ShippingAddress) => { // this.preparerAddress = data; }, error: (error: any) => { let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load preparer address data'); this.notificationService.showError(errorMessage); this.isLoading = false; } }); } loadHolderAddress(): void { this.isLoading = true; this.shippingService.getHolderAddressById(0).subscribe({ next: (data: ShippingAddress) => { // this.holderAddress = data; }, error: (error: any) => { let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load holder address data'); this.notificationService.showError(errorMessage); this.isLoading = false; } }); } updateShippingValidation(shipTo: string): void { const addressGroup = this.shippingForm.get('address') as FormGroup; const contactGroup = this.shippingForm.get('contact') as FormGroup; if (shipTo === 'thirdParty') { Object.keys(addressGroup.controls).forEach(key => { 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') { return `${this.preparerAddress.companyName}, ${this.preparerAddress.address1}, ${this.preparerAddress.city}, ${this.preparerAddress.state}, ${this.preparerAddress.zip}, ${this.preparerAddress.country}`; } if (shipTo === 'holder') { return `${this.holderAddress.companyName}, ${this.holderAddress.address1}, ${this.holderAddress.city}, ${this.holderAddress.state}, ${this.holderAddress.zip}, ${this.holderAddress.country}`; } return ''; } getContactLabel(): string { let shipTo = this.shippingForm.get('shipTo')?.value; if (shipTo === 'preparer' && this.preparerContact) { return `${this.preparerContact.firstName} ${this.preparerContact.middleInitial} ${this.preparerContact.lastName}, ${this.preparerContact.email}, ${this.preparerContact.phone}`; } if (shipTo === 'holder' && this.holderContact) { return `${this.holderContact.firstName} ${this.holderContact.middleInitial} ${this.holderContact.lastName}, ${this.holderContact.email}, ${this.holderContact.phone}`; } return ''; } 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: '; switch (deliveryType) { case 'SAME': if (isAfter(now, cutoffTime)) { // After cutoff time, deliver next business day deliveryDate = addDays(now, 1); while (isWeekend(deliveryDate)) { deliveryDate = addDays(deliveryDate, 1); } message += format(deliveryDate, 'EEEE, MMMM do, yyyy'); } else { message += 'Today by end of day'; } break; case 'STD': // Standard is 3 business days deliveryDate = addDays(now, daysToDelivery); while (isWeekend(deliveryDate)) { deliveryDate = addDays(deliveryDate, 1); } message += format(deliveryDate, 'EEEE, MMMM do, yyyy'); break; case 'NBD': // Next business day for pickup deliveryDate = addDays(now, daysToDelivery); while (isWeekend(deliveryDate)) { deliveryDate = addDays(deliveryDate, 1); } message += format(deliveryDate, 'EEEE, MMMM do, yyyy'); break; default: message = ''; } this.deliveryEstimate = message; } selectContact(): void { let shipTo = this.shippingForm.get('shipTo')?.value; if (shipTo === 'preparer') { this.preparerContact = this.preparerContacts.find(pc => pc.defaultContact); } else if (shipTo === 'holder') { this.holderContact = this.holderContacts.find(hc => hc.defaultContact); } const contacts = shipTo === 'preparer' ? this.preparerContacts : this.holderContacts; const dialogRef = this.dialog.open(ContactDialogComponent, { width: '500px', data: { contacts } }); 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; } } }); } }