shipping updates

This commit is contained in:
Cyril Joseph 2025-07-15 22:24:24 -03:00
parent d1bc0eb45a
commit 78f7b114fa
17 changed files with 281 additions and 94 deletions

View File

@ -34,6 +34,7 @@
}
],
"styles": [
"@angular/material/prebuilt-themes/azure-blue.css",
"src/styles.scss"
],
"server": "src/main.server.ts",
@ -110,6 +111,7 @@
}
],
"styles": [
"@angular/material/prebuilt-themes/azure-blue.css",
"src/styles.scss"
]
}

11
package-lock.json generated
View File

@ -20,6 +20,7 @@
"@angular/router": "^20.0.0",
"@angular/ssr": "^20.0.4",
"chart.js": "^4.3.0",
"date-fns": "^4.1.0",
"express": "^5.1.0",
"ng2-charts": "^8.0.0",
"ngx-cookie-service": "^20.0.1",
@ -4464,6 +4465,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/date-fns": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/date-format": {
"version": "4.0.14",
"resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz",

View File

@ -33,6 +33,7 @@
"@angular/router": "^20.0.0",
"@angular/ssr": "^20.0.4",
"chart.js": "^4.3.0",
"date-fns": "^4.1.0",
"express": "^5.1.0",
"ng2-charts": "^8.0.0",
"ngx-cookie-service": "^20.0.1",

View File

@ -39,7 +39,7 @@
<!-- Shipping & Payment Step -->
<mat-step [completed]="stepsCompleted.shipping" [editable]="stepsCompleted.applicationDetail">
<ng-template matStepLabel>Shipping & Payment</ng-template>
<app-shipping (completed)="onShippingSaved($event)" [headerid]="headerid">
<app-shipping (completed)="onShippingSaved($event)" [headerid]="headerid" [isEditMode]="isEditMode">
</app-shipping>
</mat-step>
</mat-stepper>

View File

@ -17,9 +17,9 @@
<div class="form-section">
<div class="checkbox-group">
<mat-checkbox labelPosition="before" formControlName="roadVehiclesUsed">Road Vehicles
<mat-checkbox formControlName="roadVehiclesUsed">Road Vehicles
used?</mat-checkbox>
<mat-checkbox labelPosition="before" formControlName="horseUsed">Horse used?</mat-checkbox>
<mat-checkbox formControlName="horseUsed">Horse used?</mat-checkbox>
</div>
</div>

View File

@ -4,11 +4,11 @@
<div class="section">
<h3>Insurance & Bond</h3>
<div class="checkbox-group">
<mat-checkbox labelPosition="before" formControlName="needsBond">Do you need Bond from
<mat-checkbox formControlName="needsBond">Do you need Bond from
us?</mat-checkbox>
<mat-checkbox labelPosition="before" formControlName="needsInsurance">Do you need insurance for your
<mat-checkbox formControlName="needsInsurance">Do you need insurance for your
goods?</mat-checkbox>
<mat-checkbox labelPosition="before" formControlName="needsLostDocProtection">Do you need Lost document
<mat-checkbox formControlName="needsLostDocProtection">Do you need Lost document
protection?</mat-checkbox>
</div>
</div>
@ -24,11 +24,18 @@
<mat-card appearance="outlined" *ngIf="!showAddressForm && !showContactForm">
<mat-card-content>
<p>{{getAddressLabel()}}</p>
<div class="presaved-address">
<div class="presaved-address-content">
<p>{{getAddressLabel()}}</p>
<p>{{getContactLabel()}}</p>
</div>
<div class="presaved-address-actions">
<button mat-icon-button color="primary" (click)="editAddressForm()" matTooltip="Edit">
<mat-icon>edit</mat-icon>
</button>
</div>
</div>
</mat-card-content>
<mat-card-actions>
<button mat-button (click)="editAddressForm()">Edit</button>
</mat-card-actions>
</mat-card>
<div *ngIf="showAddressForm" class="address-form" formGroupName="address">
<h4>Shipping Address</h4>
@ -229,12 +236,16 @@
<textarea matInput formControlName="notes" rows="2"></textarea>
</mat-form-field>
</div>
<div class="form-actions" *ngIf="showAddressForm && shippingForm.get('shipTo')?.value !== 'thirdParty'">
<button mat-button type="button" (click)="cancelEditAddressForm()">Cancel</button>
</div>
</div>
</div>
<!-- Delivery Section -->
<div class="section">
<h3>Delivery Method</h3>
<h3>Delivery</h3>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Delivery Type</mat-label>
@ -246,6 +257,9 @@
<mat-error *ngIf="shippingForm.get('deliveryType')?.errors?.['required']">
Delivery Type is required
</mat-error>
<mat-hint align="start" *ngIf="deliveryEstimate" class="delivery-estimate">
<span>{{ deliveryEstimate }}</span>
</mat-hint>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Delivery Method</mat-label>
@ -259,11 +273,14 @@
</mat-error>
</mat-form-field>
</div>
<div *ngIf="shippingForm.get('deliveryMethod')?.value === 'customerCourier'" class="form-row">
<div *ngIf="shippingForm.get('deliveryMethod')?.value === 'CLC'" class="form-row">
<mat-form-field appearance="outline" class="full-width">
<mat-label>Courier Account Number</mat-label>
<input matInput formControlName="courierAccount">
<mat-error>Required when using customer courier</mat-error>
<mat-error *ngIf="shippingForm.get('courierAccount')?.errors?.['required']">
Required when using customer courier
</mat-error>
</mat-form-field>
</div>
</div>

View File

@ -20,13 +20,13 @@
}
.mat-mdc-card-content:first-child {
padding: 0 16px;
padding: 0 16px;
}
}
.checkbox-group {
display: flex;
flex-direction: column;
flex-direction: row;
gap: 8px;
}
@ -60,6 +60,22 @@
gap: 16px;
margin-top: 16px;
}
.presaved-address {
padding: 10px;
display: flex;
flex-direction: row;
gap: 8px;
p {
margin: 0;
margin-bottom: 4px;
}
.presaved-address-actions {
margin-top: 8px;
}
}
}
@media (max-width: 768px) {

View File

@ -14,6 +14,7 @@ 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';
@Component({
selector: 'app-shipping',
@ -36,6 +37,7 @@ export class ShippingComponent {
isLoading = false;
showAddressForm = false;
showContactForm = false;
deliveryEstimate: string = '';
countriesHasStates = ['US', 'CA', 'MX'];
countries: Country[] = [];
@ -59,6 +61,7 @@ export class ShippingComponent {
}
preparerAddress = {
companyName: 'ABC Company',
address1: '123 Main St',
address2: 'Suite 100',
city: 'Anytown',
@ -68,6 +71,7 @@ export class ShippingComponent {
};
holderAddress = {
companyName: 'XYZ Company',
address1: '456 Holder St',
address2: 'Apt 200',
city: 'Othertown',
@ -94,22 +98,22 @@ export class ShippingComponent {
needsLostDocProtection: [false],
shipTo: ['preparer', Validators.required],
address: this.fb.group({
address1: ['', [Validators.required, Validators.maxLength(100)]],
address2: ['', [Validators.maxLength(100)]],
city: ['', [Validators.required, Validators.maxLength(50)]],
state: ['', Validators.required],
country: ['', Validators.required],
zip: ['', [Validators.required, ZipCodeValidator('country')]],
address1: [''],
address2: [''],
city: [''],
state: [''],
country: [''],
zip: [''],
}),
contact: this.fb.group({
firstName: ['', [Validators.required, Validators.maxLength(50)]],
lastName: ['', [Validators.required, Validators.maxLength(50)]],
middleInitial: ['', [Validators.maxLength(1)]],
title: ['', [Validators.required, Validators.maxLength(100)]],
phone: ['', [Validators.required, Validators.pattern(/^[0-9]{10,15}$/)]],
mobile: ['', [Validators.required, Validators.pattern(/^[0-9]{10,15}$/)]],
fax: ['', [Validators.pattern(/^[0-9]{10,15}$/)]],
email: ['', [Validators.required, Validators.email, Validators.maxLength(100)]],
firstName: [''],
lastName: [''],
middleInitial: [''],
title: [''],
phone: [''],
mobile: [''],
fax: [''],
email: [''],
refNumber: [''],
notes: ['']
}),
@ -117,17 +121,17 @@ export class ShippingComponent {
deliveryMethod: ['', Validators.required],
courierAccount: [''],
paymentMethod: ['', Validators.required],
notes: ['']
paymentNotes: ['']
});
// Update form validation based on selections
// this.shippingForm.get('shipTo')?.valueChanges.subscribe(value => {
// this.updateShippingValidation(value);
// });
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 === 'customerCourier') {
if (value === 'CLC') {
courierControl?.setValidators([Validators.required]);
} else {
courierControl?.clearValidators();
@ -135,9 +139,13 @@ export class ShippingComponent {
courierControl?.updateValueAndValidity();
});
this.shippingForm.valueChanges.subscribe(() => {
this.completed.emit(this.shippingForm.valid);
this.shippingForm.get('deliveryType')?.valueChanges.subscribe(() => {
this.calculateDeliveryEstimate();
});
// this.shippingForm.valueChanges.subscribe(() => {
// this.completed.emit(this.shippingForm.valid);
// });
}
ngOnInit(): void {
@ -267,41 +275,99 @@ export class ShippingComponent {
needsInsurance: shipping.needsInsurance,
needsLostDocProtection: shipping.needsLostDocProtection,
shipTo: shipping.shipTo,
address: shipping.address,
contact: shipping.contact,
deliveryType: shipping.deliveryType,
deliveryMethod: shipping.deliveryMethod,
courierAccount: shipping.courierAccount,
paymentMethod: shipping.paymentMethod,
notes: shipping.notes
paymentMethod: shipping.paymentMethod
});
if (shipping.address?.country) {
this.loadStates(shipping.address?.country);
}
if (shipping.shipTo === 'thirdParty') {
this.updateShippingValidation(shipping.shipTo);
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,
})
}
this.calculateDeliveryEstimate();
}
updateShippingValidation(shipTo: string): void {
// const addressGroup = this.shippingForm.get('address') as FormGroup;
// const contactGroup = this.shippingForm.get('contact') as FormGroup;
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 => {
// addressGroup.get(key)?.setValidators(Validators.required);
// });
// Object.keys(contactGroup.controls).forEach(key => {
// if (key !== 'middleInitial' && key !== 'fax' && key !== 'notes' && key !== 'refNumber') {
// contactGroup.get(key)?.setValidators(Validators.required);
// }
// });
// } else {
// Object.keys(addressGroup.controls).forEach(key => {
// addressGroup.get(key)?.clearValidators();
// });
// Object.keys(contactGroup.controls).forEach(key => {
// contactGroup.get(key)?.clearValidators();
// });
// }
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')]);
}
});
Object.keys(contactGroup.controls).forEach(key => {
if (key === 'firstName') {
addressGroup.get(key)?.setValidators([Validators.required, Validators.maxLength(50)]);
} else if (key === 'lastName') {
addressGroup.get(key)?.setValidators([Validators.required, Validators.maxLength(50)]);
} else if (key === 'middleInitial') {
addressGroup.get(key)?.setValidators([Validators.maxLength(1)]);
} else if (key === 'title') {
addressGroup.get(key)?.setValidators([Validators.required, Validators.maxLength(100)]);
} else if (key === 'phone') {
addressGroup.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}$/)]);
} else if (key === 'fax') {
addressGroup.get(key)?.setValidators([Validators.pattern(/^[0-9]{10,15}$/)]);
} else if (key === 'email') {
addressGroup.get(key)?.setValidators([Validators.required, Validators.email, Validators.maxLength(100)]);
}
});
} else {
Object.keys(addressGroup.controls).forEach(key => {
addressGroup.get(key)?.clearValidators();
});
Object.keys(contactGroup.controls).forEach(key => {
contactGroup.get(key)?.clearValidators();
});
}
// addressGroup.updateValueAndValidity();
// contactGroup.updateValueAndValidity();
@ -334,11 +400,31 @@ export class ShippingComponent {
let shipTo = this.shippingForm.get('shipTo')?.value;
if (shipTo === 'preparer') {
return `${this.preparerContact.firstName} ${this.preparerContact.lastName}, ${this.preparerAddress.address1}, ${this.preparerAddress.city}, ${this.preparerAddress.state}, ${this.preparerAddress.zip}, ${this.preparerAddress.country}`;
return `${this.preparerAddress.companyName}, ${this.preparerAddress.address1},
${this.preparerAddress.city}, ${this.preparerAddress.state}, ${this.preparerAddress.zip},
${this.preparerAddress.country}`;
}
if (shipTo === 'holder') {
return `${this.holderContact.firstName} ${this.holderContact.lastName}, ${this.holderAddress.address1}, ${this.holderAddress.city}, ${this.holderAddress.state}, ${this.holderAddress.zip}, ${this.holderAddress.country}`;
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') {
return `${this.preparerContact.firstName} ${this.preparerContact.middleInitial} ${this.preparerContact.lastName},
${this.preparerContact.email}, ${this.preparerContact.phone}`;
}
if (shipTo === 'holder') {
return `${this.holderContact.firstName} ${this.holderContact.middleInitial} ${this.holderContact.lastName},
${this.holderContact.email}, ${this.holderContact.phone}`;
}
return '';
}
@ -373,4 +459,56 @@ export class ShippingComponent {
this.showContactForm = true;
}
}
private 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;
}
}

View File

@ -1,4 +1,5 @@
export interface ShippingAddress {
addressid: number;
address1: string;
address2?: string | null;
city: string;

View File

@ -1,4 +1,5 @@
export interface ShippingContact {
contactid: number;
firstName: string;
lastName: string;
middleInitial?: string | null;

View File

@ -19,5 +19,5 @@ export interface Shipping {
// Payment details
paymentMethod: string;
notes?: string;
paymentNotes?: string;
}

View File

@ -2,4 +2,6 @@ export interface DeliveryType {
name: string;
id: string;
value: string;
daysToDelivery: string;
cutOffTime: string;
}

View File

@ -109,19 +109,9 @@ export class GoodsService {
deleteGoodsItem(headerid: number, item: GoodsItem): Observable<any> {
const goodsItem = {
ITEMNO: item.itemNumber,
ITEMDESCRIPTION: item.description,
NOOFPIECES: item.pieces,
ITEMWEIGHT: item.weight,
ITEMWEIGHTUOM: item.unitOfMeasure,
ITEMVALUE: item.value,
GOODSORIGINCOUNTRY: item.countryOfOrigin,
}
const goodsItemsObj = {
P_HEADERID: headerid,
P_GLTABLE: [goodsItem],
P_ITEMNO: item.itemNumber,
P_USERID: this.userService.getUser(),
};

View File

@ -30,7 +30,6 @@ export class ShippingService {
courierAccount: shippingDetails.CUSTCOURIERNO,
paymentMethod: shippingDetails.PAYMENTMETHOD,
notes: shippingDetails.NOTES,
needsBond: shippingDetails.INSPROTECTION === 'Y',
needsInsurance: shippingDetails.LDIPROTECTION === 'Y',
needsLostDocProtection: shippingDetails.LDIPROTECTION === 'Y'
@ -38,6 +37,7 @@ export class ShippingService {
if (shippingDetails.address) {
shippingData.address = {
addressid: shippingDetails.SHIPADDRESSID,
address1: shippingDetails.ADDRESS1,
address2: shippingDetails.ADDRESS2,
city: shippingDetails.CITY,
@ -49,6 +49,7 @@ export class ShippingService {
if (shippingDetails.contact) {
shippingData.contact = {
contactid: shippingDetails.SHIPCONTACTID,
firstName: shippingDetails.FIRSTNAME,
lastName: shippingDetails.LASTNAME,
middleInitial: shippingDetails.MIDDLEINITIAL,
@ -58,6 +59,7 @@ export class ShippingService {
fax: shippingDetails.FAXNO,
email: shippingDetails.EMAILADDRESS,
refNumber: shippingDetails.REFNO,
notes: shippingDetails.NOTES,
}
}
@ -73,28 +75,30 @@ export class ShippingService {
P_DELIVERYMETHOD: shippingData.deliveryMethod,
P_CUSTCOURIERNO: shippingData.courierAccount,
P_PAYMENTMETHOD: shippingData.paymentMethod,
P_NOTES: shippingData.notes,
P_FORMOFSECURITY: shippingData.needsBond ? 'Y' : 'N',
P_INSPROTECTION: shippingData.needsInsurance ? 'Y' : 'N',
P_LDIPROTECTION: shippingData.needsLostDocProtection ? 'Y' : 'N',
address1: shippingData.address?.address1 || '',
address2: shippingData.address?.address2 || null,
city: shippingData.address?.city || '',
state: shippingData.address?.state || '',
zip: shippingData.address?.zip || '',
country: shippingData.address?.country || '',
P_SHIPCONTACTID: shippingData.address?.addressid || 0,
P_ADDRESS1: shippingData.address?.address1 || null,
P_ADDRESS2: shippingData.address?.address2 || null,
P_CITY: shippingData.address?.city || null,
P_STATE: shippingData.address?.state || null,
P_ZIP: shippingData.address?.zip || null,
P_COUNTRY: shippingData.address?.country || null,
firstName: shippingData.contact?.firstName || '',
lastName: shippingData.contact?.lastName || '',
middleInitial: shippingData.contact?.middleInitial || null,
title: shippingData.contact?.title || '',
phone: shippingData.contact?.phone || '',
mobile: shippingData.contact?.mobile || '',
fax: shippingData.contact?.fax || null,
emailAddress: shippingData.contact?.email || '',
refNo: shippingData.contact?.refNumber || '',
P_FIRSTNAME: shippingData.contact?.firstName || '',
P_LASTNAME: shippingData.contact?.lastName || '',
P_MIDDLEINITIAL: shippingData.contact?.middleInitial || null,
P_TITLE: shippingData.contact?.title || '',
P_PHONENO: shippingData.contact?.phone || '',
P_MOBILENO: shippingData.contact?.mobile || '',
P_FAXNO: shippingData.contact?.fax || null,
P_EMAILADDRESS: shippingData.contact?.email || '',
P_REFNO: shippingData.contact?.refNumber || '',
P_NOTES: shippingData.contact?.notes,
P_USERID: this.userService.getUser()
}

View File

@ -72,7 +72,9 @@ export class CommonService {
response.map((item) => ({
name: item.PARAMDESC,
id: item.PARAMID,
value: item.PARAMVALUE
value: item.PARAMVALUE,
daysToDelivery: item.ADDLPARAMVALUE1,
cutOffTime: item.ADDLPARAMVALUE2,
}))
)
);

View File

@ -1,5 +1,6 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>ClientApp</title>
@ -9,7 +10,9 @@
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body class="mat-typography">
<app-root></app-root>
</body>
</html>
</html>

View File

@ -2,8 +2,7 @@
@use '@angular/material' as mat;
html {
@include mat.theme((density: -3,
));
@include mat.theme((density: -4));
}
html,