shipping contact updates

This commit is contained in:
Cyril Joseph 2025-07-16 20:18:31 -03:00
parent 78f7b114fa
commit 1b6ceb6be7
5 changed files with 382 additions and 128 deletions

View File

@ -0,0 +1,61 @@
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-contact-dialog',
imports: [AngularMaterialModule, CommonModule, FormsModule],
template: `
<h2 mat-dialog-title>Select Contact</h2>
<mat-dialog-content>
<mat-radio-group [(ngModel)]="selectedContact">
<mat-radio-button *ngFor="let contact of contacts" [value]="contact">
{{getContactLabel(contact)}}
</mat-radio-button>
</mat-radio-group>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button (click)="onCancel()">Cancel</button>
<button mat-raised-button color="primary" (click)="onSelect()" [disabled]="!selectedContact">
Select
</button>
</mat-dialog-actions>
`,
styles: [`
mat-radio-group {
display: flex;
flex-direction: column;
gap: 12px;
}
mat-radio-button {
margin: 4px 0;
}
`]
})
export class ContactDialogComponent {
selectedContact: any | null = null;
constructor(
public dialogRef: MatDialogRef<ContactDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: { contacts: any[] }
) { }
get contacts() {
return this.data.contacts;
}
onSelect(): void {
this.dialogRef.close(this.selectedContact);
}
onCancel(): void {
this.dialogRef.close();
}
getContactLabel(contact: any): string {
return `${contact.firstName} ${contact.middleInitial} ${contact.lastName},
${contact.email}, ${contact.phone}`;
}
}

View File

@ -33,6 +33,10 @@
<button mat-icon-button color="primary" (click)="editAddressForm()" matTooltip="Edit">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button color="primary" (click)="selectContact()"
matTooltip="Change contact">
<mat-icon>compare_arrows</mat-icon>
</button>
</div>
</div>
</mat-card-content>
@ -249,7 +253,7 @@
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Delivery Type</mat-label>
<mat-select formControlName="deliveryType" required>
<mat-select formControlName="deliveryType" required (change)="onDeliveryTypeChange()">
<mat-option *ngFor="let deliveryType of deliveryTypes" [value]="deliveryType.value">
{{ deliveryType.name }}
</mat-option>
@ -263,7 +267,8 @@
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Delivery Method</mat-label>
<mat-select formControlName="deliveryMethod" required>
<mat-select formControlName="deliveryMethod" required
(selectionChange)="onDeliveryMethodChange($event.value)">
<mat-option *ngFor="let deliveryMethod of deliveryMethods" [value]="deliveryMethod.value">
{{ deliveryMethod.name }}
</mat-option>

View File

@ -15,6 +15,10 @@ 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',
@ -28,6 +32,8 @@ export class ShippingComponent {
@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);
@ -49,16 +55,33 @@ export class ShippingComponent {
private destroy$ = new Subject<void>();
// preparer contact and address mock data
preparerContact = {
firstName: 'John',
lastName: 'Doe',
middleInitial: 'A',
title: 'Mr.',
phone: '1234567890',
mobile: '0987654321',
fax: '1234567890',
email: 'j@doe.com',
}
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',
@ -80,16 +103,33 @@ export class ShippingComponent {
country: 'US'
};
holderContact = {
firstName: 'Jane',
lastName: 'Smith',
middleInitial: 'B',
title: 'Ms.',
phone: '2345678901',
mobile: '1098765432',
fax: '2345678901',
email: 'jan@sm.cm'
};
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({
@ -124,19 +164,8 @@ export class ShippingComponent {
paymentNotes: ['']
});
// Update form validation based on selections
this.shippingForm.get('shipTo')?.valueChanges.subscribe(value => {
this.updateShippingValidation(value);
});
this.shippingForm.get('deliveryMethod')?.valueChanges.subscribe(value => {
const courierControl = this.shippingForm.get('courierAccount');
if (value === 'CLC') {
courierControl?.setValidators([Validators.required]);
} else {
courierControl?.clearValidators();
}
courierControl?.updateValueAndValidity();
});
this.shippingForm.get('deliveryType')?.valueChanges.subscribe(() => {
@ -156,6 +185,10 @@ export class ShippingComponent {
if (this.headerid && this.isEditMode) {
this.loadShippingData();
// TODO
this.preparerContact = this.preparerContacts.find(pc => pc.defaultContact);
this.holderContact = this.holderContacts.find(hc => hc.defaultContact);
}
}
@ -164,6 +197,43 @@ export class ShippingComponent {
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();
@ -174,6 +244,39 @@ export class ShippingComponent {
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$))
@ -285,8 +388,14 @@ export class ShippingComponent {
this.loadStates(shipping.address?.country);
}
if (shipping.deliveryMethod === 'CLC') {
this.onDeliveryMethodChange(shipping.deliveryMethod);
}
this.calculateDeliveryEstimate();
this.updateShippingValidation(shipping.shipTo);
if (shipping.shipTo === 'thirdParty') {
this.updateShippingValidation(shipping.shipTo);
this.showAddressForm = true;
this.showContactForm = true;
@ -317,8 +426,64 @@ export class ShippingComponent {
notes: shipping.contact?.notes,
})
}
}
this.calculateDeliveryEstimate();
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 {
@ -340,60 +505,38 @@ export class ShippingComponent {
} 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') {
addressGroup.get(key)?.setValidators([Validators.required, Validators.maxLength(50)]);
contactGroup.get(key)?.setValidators([Validators.required, Validators.maxLength(50)]);
} else if (key === 'lastName') {
addressGroup.get(key)?.setValidators([Validators.required, Validators.maxLength(50)]);
contactGroup.get(key)?.setValidators([Validators.required, Validators.maxLength(50)]);
} else if (key === 'middleInitial') {
addressGroup.get(key)?.setValidators([Validators.maxLength(1)]);
contactGroup.get(key)?.setValidators([Validators.maxLength(1)]);
} else if (key === 'title') {
addressGroup.get(key)?.setValidators([Validators.required, Validators.maxLength(100)]);
contactGroup.get(key)?.setValidators([Validators.required, Validators.maxLength(100)]);
} else if (key === 'phone') {
addressGroup.get(key)?.setValidators([Validators.required, Validators.pattern(/^[0-9]{10,15}$/)]);
contactGroup.get(key)?.setValidators([Validators.required, Validators.pattern(/^[0-9]{10,15}$/)]);
} else if (key === 'mobile') {
addressGroup.get(key)?.setValidators([Validators.required, Validators.pattern(/^[0-9]{10,15}$/)]);
contactGroup.get(key)?.setValidators([Validators.required, Validators.pattern(/^[0-9]{10,15}$/)]);
} else if (key === 'fax') {
addressGroup.get(key)?.setValidators([Validators.pattern(/^[0-9]{10,15}$/)]);
contactGroup.get(key)?.setValidators([Validators.pattern(/^[0-9]{10,15}$/)]);
} else if (key === 'email') {
addressGroup.get(key)?.setValidators([Validators.required, Validators.email, Validators.maxLength(100)]);
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();
});
}
// addressGroup.updateValueAndValidity();
// contactGroup.updateValueAndValidity();
}
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;
}
});
}
getAddressLabel(): string {
@ -417,50 +560,19 @@ export class ShippingComponent {
getContactLabel(): string {
let shipTo = this.shippingForm.get('shipTo')?.value;
if (shipTo === 'preparer') {
if (shipTo === 'preparer' && this.preparerContact) {
return `${this.preparerContact.firstName} ${this.preparerContact.middleInitial} ${this.preparerContact.lastName},
${this.preparerContact.email}, ${this.preparerContact.phone}`;
}
if (shipTo === 'holder') {
if (shipTo === 'holder' && this.holderContact) {
return `${this.holderContact.firstName} ${this.holderContact.middleInitial} ${this.holderContact.lastName},
${this.holderContact.email}, ${this.holderContact.phone}`;
}
return '';
}
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;
if (shipTo === 'thirdParty') {
this.shippingForm.get('contact')?.reset();
this.shippingForm.get('address')?.reset();
this.showAddressForm = true;
this.showContactForm = true;
}
}
private calculateDeliveryEstimate(): void {
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;
@ -511,4 +623,32 @@ export class ShippingComponent {
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;
}
}
});
}
}

View File

@ -10,4 +10,6 @@ export interface ShippingContact {
email: string;
refNumber?: string | null;
notes?: string | null;
defaultContact?: boolean;
isInactive?: boolean;
}

View File

@ -4,6 +4,8 @@ import { HttpClient } from '@angular/common/http';
import { filter, map, Observable } from 'rxjs';
import { UserService } from '../common/user.service';
import { Shipping } from '../../models/carnet/shipping';
import { ShippingContact } from '../../models/carnet/shipping-contact';
import { ShippingAddress } from '../../models/carnet/shipping-address';
@Injectable({
providedIn: 'root'
@ -35,32 +37,28 @@ export class ShippingService {
needsLostDocProtection: shippingDetails.LDIPROTECTION === 'Y'
};
if (shippingDetails.address) {
shippingData.address = {
addressid: shippingDetails.SHIPADDRESSID,
address1: shippingDetails.ADDRESS1,
address2: shippingDetails.ADDRESS2,
city: shippingDetails.CITY,
state: shippingDetails.STATE,
zip: shippingDetails.ZIP,
country: shippingDetails.COUNTRY
}
shippingData.address = {
addressid: shippingDetails.SHIPADDRESSID,
address1: shippingDetails.ADDRESS1,
address2: shippingDetails.ADDRESS2,
city: shippingDetails.CITY,
state: shippingDetails.STATE,
zip: shippingDetails.ZIP,
country: shippingDetails.COUNTRY
}
if (shippingDetails.contact) {
shippingData.contact = {
contactid: shippingDetails.SHIPCONTACTID,
firstName: shippingDetails.FIRSTNAME,
lastName: shippingDetails.LASTNAME,
middleInitial: shippingDetails.MIDDLEINITIAL,
title: shippingDetails.TITLE,
phone: shippingDetails.PHONENO,
mobile: shippingDetails.MOBILENO,
fax: shippingDetails.FAXNO,
email: shippingDetails.EMAILADDRESS,
refNumber: shippingDetails.REFNO,
notes: shippingDetails.NOTES,
}
shippingData.contact = {
contactid: shippingDetails.SHIPCONTACTID,
firstName: shippingDetails.FIRSTNAME,
lastName: shippingDetails.LASTNAME,
middleInitial: shippingDetails.MIDDLEINITIAL,
title: shippingDetails.TITLE,
phone: shippingDetails.PHONENO,
mobile: shippingDetails.MOBILENO,
fax: shippingDetails.FAXNO,
email: shippingDetails.EMAILADDRESS,
refNumber: shippingDetails.REFNO,
notes: shippingDetails.NOTES,
}
return shippingData;
@ -104,4 +102,52 @@ export class ShippingService {
return this.http.patch(`${this.apiUrl}/${this.apiDb}/UpdateShippingDetails`, shippingDetails);
}
getPreparerContactsById(): ShippingContact[] | any {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetPreparerContactsByClientid/${this.userService.getUserSpid()}/${this.userService.getUserClientid()}`).pipe(
map(response => this.mapToContacts(response)));
}
getHolderContactsById(id: number): ShippingContact[] | any {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetHolderContacts/${this.userService.getUserSpid()}/${id}`).pipe(
map(response => this.mapToContacts(response)));
}
private mapToContacts(data: any[]): ShippingContact[] {
return data.map(contact => ({
contactid: contact.CLIENTCONTACTID,
defaultContact: contact.DEFCONTACTFLAG === 'Y',
firstName: contact.FIRSTNAME,
lastName: contact.LASTNAME,
title: contact.TITLE,
phone: contact.PHONENO,
mobile: contact.MOBILENO,
fax: contact.FAXNO || null,
email: contact.EMAILADDRESS,
middleInitial: contact.MIDDLEINITIAL || null,
isInactive: contact.INACTIVEFLAG === 'Y' || false
}));
}
getPreparerAddress(): ShippingAddress[] | any {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetPreparerLocByClientid/${this.userService.getUserSpid()}/${this.userService.getUserClientid()}`).pipe(
map(response => this.mapToAddress(response)));
}
getHolderAddressById(id: number): ShippingContact[] | any {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetHolderRecord/${this.userService.getUserSpid()}/${id}`).pipe(
map(response => this.mapToAddress(response)));
}
private mapToAddress(data: any[]): ShippingAddress[] {
return data.map(address => ({
addressid: address.SHIPADDRESSID,
address1: address.ADDRESS1,
address2: address.ADDRESS2,
city: address.CITY,
state: address.STATE,
zip: address.ZIP,
country: address.COUNTRY
}));
}
}