client-app/src/app/carnet/shipping/shipping.component.ts
2025-07-24 22:22:42 -03:00

613 lines
21 KiB
TypeScript

import { CommonModule } from '@angular/common';
import { Component, EventEmitter, inject, Input, Output, SimpleChanges } 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, addDays, 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';
@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() refreshShippingData = false;
@Input() enableSubmitButton = false;
@Input() applicationName: string = '';
@Output() completed = new EventEmitter<boolean>();
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);
shippingForm: FormGroup;
isLoading = false;
showAddressForm = false;
showContactForm = false;
deliveryEstimate: string = '';
holderid: number = 0;
countriesHasStates = ['US', 'CA', 'MX'];
countries: Country[] = [];
states: State[] = [];
deliveryTypes: DeliveryType[] = [];
deliveryMethods: DeliveryMethod[] = [];
paymentTypes: PaymentType[] = [];
private destroy$ = new Subject<void>();
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],
shipTo: ['PREPARER', Validators.required],
address: this.fb.group({
companyName: [''],
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('deliveryType')?.valueChanges.subscribe(() => {
this.calculateDeliveryEstimate();
});
}
ngOnInit(): void {
this.loadCountries();
this.loadDeliveryTypes();
this.loadDeliveryMethods();
this.loadPaymentTypes();
if (this.headerid) {
this.loadShippingData();
}
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
onSubmit(): void {
if (this.shippingForm.invalid) {
this.shippingForm.markAllAsTouched();
return;
}
this.isLoading = 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;
}
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();
}
ngOnChanges(changes: SimpleChanges) {
if (changes['refreshShippingData'] && this.headerid > 0) {
this.loadShippingData();
}
}
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();
}
onShipToChange(event: any): void {
const shipTo = event.value;
this.showAddressForm = false;
this.showContactForm = false;
this.updateShippingValidation(shipTo);
if (shipTo === '3RDPARTY') {
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.holderService.getHolder(this.headerid).pipe(
switchMap((holder: Holder) => {
if (!holder?.holderid) {
return of(undefined);
}
return forkJoin({
holderContacts: this.shippingService.getHolderContactsById(holder.holderid),
holderAddress: this.shippingService.getHolderAddressById(holder.holderid),
preparerAddress: this.shippingService.getPreparerAddress(),
preparerContacts: this.shippingService.getPreparerContacts(),
shippingData: this.shippingService.getShippingData(this.headerid)
}).pipe(
// Combine the forkJoin results with the original holder object
map((apiResults: any) => ({ ...apiResults, holder }))
);
}),
finalize(() => this.isLoading = false)
).subscribe({
next: (results) => {
if (!results) { // 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 (results.shippingData?.shipTo === 'HOLDER' && results.shippingData?.contact?.contactid) {
this.holderContact = this.holderContacts.find(hc => hc.contactid === results.shippingData?.contact?.contactid);
} else {
this.holderContact = this.holderContacts?.[0];
}
// Set preparer contact
if (results.shippingData?.shipTo === 'PREPARER' && results.shippingData?.contact?.contactid) {
this.preparerContact = this.preparerContacts.find(pc => pc.contactid === results.shippingData?.contact?.contactid);
} else {
this.preparerContact = this.preparerContacts.find(pc => pc.defaultContact);
}
this.patchShippingData(results.shippingData);
},
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(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 === '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: shipping.address?.addressid,
companyName: shipping.address?.companyName,
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,
})
}
this.completed.emit(this.shippingForm.valid);
}
updateShippingValidation(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 {
if (this.headerid) {
let currentApplicationDetails = { headerid: this.headerid, applicationName: this.applicationName }
this.storageService.set("currentapplication", currentApplicationDetails);
}
this.navigationService.navigate(["terms"])
}
}