Compare commits
6 Commits
0c12e3fcbd
...
040859aedc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
040859aedc | ||
| d1bc0eb45a | |||
| 5185cb375d | |||
| f3864cb5ae | |||
| 3101a46c28 | |||
| 404adc0d3d |
104
package-lock.json
generated
104
package-lock.json
generated
@ -25,6 +25,7 @@
|
||||
"ngx-cookie-service": "^20.0.1",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"xlsx": "^0.18.5",
|
||||
"zone.js": "~0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -3509,6 +3510,15 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/adler-32": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
|
||||
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
|
||||
@ -3955,6 +3965,19 @@
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/cfb": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
|
||||
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"adler-32": "~1.3.0",
|
||||
"crc-32": "~1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "5.4.1",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
|
||||
@ -4173,6 +4196,15 @@
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/codepage": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
|
||||
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@ -4352,6 +4384,18 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/crc-32": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"crc32": "bin/crc32.njs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@ -5083,6 +5127,15 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/frac": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
|
||||
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/fresh": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
|
||||
@ -8539,6 +8592,18 @@
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/ssf": {
|
||||
"version": "0.11.2",
|
||||
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
|
||||
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"frac": "~1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/ssri": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz",
|
||||
@ -9214,6 +9279,24 @@
|
||||
"which": "bin/which"
|
||||
}
|
||||
},
|
||||
"node_modules/wmf": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
|
||||
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/word": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
|
||||
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||
@ -9427,6 +9510,27 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/xlsx": {
|
||||
"version": "0.18.5",
|
||||
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
|
||||
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"adler-32": "~1.3.0",
|
||||
"cfb": "~1.2.1",
|
||||
"codepage": "~1.15.0",
|
||||
"crc-32": "~1.2.1",
|
||||
"ssf": "~0.11.2",
|
||||
"wmf": "~1.0.1",
|
||||
"word": "~0.3.0"
|
||||
},
|
||||
"bin": {
|
||||
"xlsx": "bin/xlsx.njs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
|
||||
@ -38,6 +38,7 @@
|
||||
"ngx-cookie-service": "^20.0.1",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"xlsx": "^0.18.5",
|
||||
"zone.js": "~0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
import { RenderMode, ServerRoute } from '@angular/ssr';
|
||||
|
||||
export const serverRoutes: ServerRoute[] = [
|
||||
{ path: 'forgot-password/:token', renderMode: RenderMode.Client },
|
||||
{ path: ':appId', renderMode: RenderMode.Client },
|
||||
{ path: ':appId/home', renderMode: RenderMode.Client },
|
||||
{ path: ':appId/usersettings', renderMode: RenderMode.Client },
|
||||
{ path: ':appId/add-carnet', renderMode: RenderMode.Client },
|
||||
{ path: ':appId/edit-carnet/:headerid', renderMode: RenderMode.Client },
|
||||
{
|
||||
path: '**',
|
||||
renderMode: RenderMode.Prerender
|
||||
|
||||
@ -4,8 +4,9 @@ import { AuthGuard } from './guards/auth.guard';
|
||||
|
||||
export const routes: Routes = [
|
||||
{ path: 'login', loadComponent: () => import('./login/login.component').then(m => m.LoginComponent) },
|
||||
{ path: 'forgot-password/:token', loadComponent: () => import('./forgot-password/forgot-password.component').then(m => m.ForgotPasswordComponent) },
|
||||
{ path: 'forgot-password', loadComponent: () => import('./forgot-password/forgot-password.component').then(m => m.ForgotPasswordComponent) },
|
||||
{ path: 'register/:token', loadComponent: () => import('./register/register.component').then(m => m.RegisterComponent) },
|
||||
{ path: 'register', loadComponent: () => import('./register/register.component').then(m => m.RegisterComponent) },
|
||||
{ path: '404', loadComponent: () => import('./shared/components/not-found/not-found.component').then(m => m.NotFoundComponent) },
|
||||
{
|
||||
path: ':appId',
|
||||
@ -14,6 +15,8 @@ export const routes: Routes = [
|
||||
{ path: 'usersettings', loadComponent: () => import('./user-settings/user-settings.component').then(m => m.UserSettingsComponent) },
|
||||
{ path: 'add-carnet', loadComponent: () => import('./carnet/add/add-carnet.component').then(m => m.AddCarnetComponent) },
|
||||
{ path: 'edit-carnet/:headerid', loadComponent: () => import('./carnet/edit/edit-carnet.component').then(m => m.EditCarnetComponent) },
|
||||
{ path: 'add-holder', loadComponent: () => import('./holder/add/add.component').then(m => m.AddComponent) },
|
||||
{ path: 'edit-holder/:holderid', loadComponent: () => import('./holder/edit/edit.component').then(m => m.EditComponent) },
|
||||
{ path: '', redirectTo: 'home', pathMatch: 'full' }
|
||||
],
|
||||
canActivate: [AuthGuard, AppIdGuard]
|
||||
|
||||
@ -15,15 +15,25 @@
|
||||
<mat-step *ngIf="applicationType === 'new'" [completed]="stepsCompleted.holderSelection"
|
||||
[editable]="stepsCompleted.applicationDetail">
|
||||
<ng-template matStepLabel>Holder Selection</ng-template>
|
||||
<app-holder (completed)="onHolderSelectionSaved($event)" [headerid]="headerid">
|
||||
</app-holder>
|
||||
|
||||
<!-- <app-holder (completed)="onHolderSelectionSaved($event)" [headerid]="headerid">
|
||||
</app-holder> -->
|
||||
|
||||
<!-- <app-search [searchedHolder]="searchedHolder" (searchedHolderEmit)="onSearchedHolder($event)"
|
||||
(selectHolderCompleted)="onHolderSelectionSaved($event)" [HOLDERID]="holderid"
|
||||
[headerid]="headerid"></app-search> -->
|
||||
|
||||
<app-search
|
||||
(selectHolderCompleted)="onHolderSelectionSaved($event)"
|
||||
[headerid]="headerid"></app-search>
|
||||
</mat-step>
|
||||
|
||||
<!-- Goods Section Step -->
|
||||
<mat-step *ngIf="applicationType === 'new'" [completed]="stepsCompleted.goodsSection"
|
||||
[editable]="stepsCompleted.applicationDetail">
|
||||
<ng-template matStepLabel>Goods Section</ng-template>
|
||||
<app-goods (completed)="onGoodsSectionSaved($event)" [headerid]="headerid">
|
||||
<app-goods (completed)="onGoodsSectionSaved($event)" [headerid]="headerid"
|
||||
[userPreferences]="userPreferences">
|
||||
</app-goods>
|
||||
</mat-step>
|
||||
|
||||
@ -35,33 +45,11 @@
|
||||
</app-travel-plan>
|
||||
</mat-step>
|
||||
|
||||
<!-- Insurance Step -->
|
||||
<mat-step *ngIf="applicationType === 'new' || applicationType === 'extend' || applicationType === 'additional'"
|
||||
[completed]="stepsCompleted.insurance" [editable]="stepsCompleted.applicationDetail">
|
||||
<ng-template matStepLabel>Insurance</ng-template>
|
||||
<app-insurance (completed)="onInsuranceSaved($event)" [headerid]="headerid">
|
||||
</app-insurance>
|
||||
</mat-step>
|
||||
|
||||
<!-- Shipping Step -->
|
||||
<!-- Shipping & Payment Step -->
|
||||
<mat-step [completed]="stepsCompleted.shipping" [editable]="stepsCompleted.applicationDetail">
|
||||
<ng-template matStepLabel>Shipping</ng-template>
|
||||
<ng-template matStepLabel>Shipping & Payment</ng-template>
|
||||
<app-shipping (completed)="onShippingSaved($event)" [headerid]="headerid">
|
||||
</app-shipping>
|
||||
</mat-step>
|
||||
|
||||
<!-- Delivery Method Step -->
|
||||
<mat-step [completed]="stepsCompleted.deliveryMethod" [editable]="stepsCompleted.applicationDetail">
|
||||
<ng-template matStepLabel>Delivery Method</ng-template>
|
||||
<app-delivery (completed)="onDeliveryMethodSaved($event)" [headerid]="headerid">
|
||||
</app-delivery>
|
||||
</mat-step>
|
||||
|
||||
<!-- Payment Step -->
|
||||
<mat-step [completed]="stepsCompleted.payment" [editable]="stepsCompleted.applicationDetail">
|
||||
<ng-template matStepLabel>Payment</ng-template>
|
||||
<app-payment (completed)="onPaymentSaved($event)" [headerid]="headerid">
|
||||
</app-payment>
|
||||
</mat-step>
|
||||
</mat-stepper>
|
||||
</div>
|
||||
@ -7,25 +7,28 @@ import { ApplicationComponent } from "../application/application.component";
|
||||
import { HolderComponent } from '../holder/holder.component';
|
||||
import { GoodsComponent } from '../goods/goods.component';
|
||||
import { TravelPlanComponent } from '../travel-plan/travel-plan.component';
|
||||
import { InsuranceComponent } from '../insurance/insurance.component';
|
||||
import { PaymentComponent } from '../payment/payment.component';
|
||||
import { ShippingComponent } from '../shipping/shipping.component';
|
||||
import { DeliveryComponent } from '../delivery/delivery.component';
|
||||
import { UserPreferencesService } from '../../core/services/user-preference.service';
|
||||
import { UserPreferences } from '../../core/models/user-preference';
|
||||
import { SearchComponent } from '../../holder/search/search.component';
|
||||
import { CarnetStateStore } from '../../core/services/carnet/carnet-application.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-add-carnet',
|
||||
imports: [AngularMaterialModule, CommonModule, ReactiveFormsModule,
|
||||
ApplicationComponent, HolderComponent, GoodsComponent, TravelPlanComponent, InsuranceComponent,
|
||||
ShippingComponent, DeliveryComponent, PaymentComponent],
|
||||
ApplicationComponent, SearchComponent, GoodsComponent, TravelPlanComponent,
|
||||
ShippingComponent],
|
||||
templateUrl: './add-carnet.component.html',
|
||||
styleUrl: './add-carnet.component.scss'
|
||||
})
|
||||
export class AddCarnetComponent {
|
||||
store = inject(CarnetStateStore);
|
||||
currentStep = 0;
|
||||
isLinear = true;
|
||||
applicationType: 'new' | 'additional' | 'duplicate' | 'extend' | null = 'new';
|
||||
isEditMode = false;
|
||||
headerid: number = 0;
|
||||
userPreferences: UserPreferences;
|
||||
|
||||
// Track completion of each step
|
||||
stepsCompleted = {
|
||||
@ -33,18 +36,21 @@ export class AddCarnetComponent {
|
||||
holderSelection: false,
|
||||
goodsSection: false,
|
||||
travelPlan: false,
|
||||
insurance: false,
|
||||
shipping: false,
|
||||
deliveryMethod: false,
|
||||
payment: false
|
||||
shipping: false
|
||||
};
|
||||
|
||||
constructor(userPrefenceService: UserPreferencesService) {
|
||||
this.userPreferences = userPrefenceService.getPreferences();
|
||||
}
|
||||
|
||||
onStepChange(event: StepperSelectionEvent): void {
|
||||
this.currentStep = event.selectedIndex;
|
||||
}
|
||||
|
||||
onApplicationDetailCreated(headerid: number): void {
|
||||
this.headerid = this.headerid;
|
||||
onApplicationDetailCreated(data: { HEADERID: number, APPLICATION_NAME: string }): void {
|
||||
this.store.headerid.set(data.HEADERID);
|
||||
this.store.applicationName.set(data.APPLICATION_NAME);
|
||||
this.headerid = data.HEADERID;
|
||||
this.stepsCompleted.applicationDetail = true;
|
||||
this.isLinear = false; // Disable linear mode after application detail is created
|
||||
}
|
||||
@ -61,19 +67,7 @@ export class AddCarnetComponent {
|
||||
this.stepsCompleted.travelPlan = completed;
|
||||
}
|
||||
|
||||
onInsuranceSaved(completed: boolean): void {
|
||||
this.stepsCompleted.insurance = completed;
|
||||
}
|
||||
|
||||
onShippingSaved(completed: boolean): void {
|
||||
this.stepsCompleted.shipping = completed;
|
||||
}
|
||||
|
||||
onDeliveryMethodSaved(completed: boolean): void {
|
||||
this.stepsCompleted.deliveryMethod = completed;
|
||||
}
|
||||
|
||||
onPaymentSaved(completed: boolean): void {
|
||||
this.stepsCompleted.payment = completed;
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,7 +20,8 @@ export class ApplicationComponent {
|
||||
@Input() headerid: number = 0;
|
||||
@Input() applicationName: string = '';
|
||||
|
||||
@Output() headerIdCreated = new EventEmitter<number>();
|
||||
// @Output() headerIdCreated = new EventEmitter<number>();
|
||||
@Output() headerIdCreated = new EventEmitter<{ HEADERID: number, APPLICATION_NAME: string }>();
|
||||
|
||||
applicationDetailsForm: FormGroup;
|
||||
isLoading = false;
|
||||
@ -39,7 +40,7 @@ export class ApplicationComponent {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.applicationName = this.route.snapshot.queryParamMap.get('applicationName') || '';
|
||||
this.applicationName = this.route.snapshot.queryParamMap.get('applicationname') || '';
|
||||
|
||||
// Patch edit form data
|
||||
if (this.applicationName) {
|
||||
@ -102,7 +103,7 @@ export class ApplicationComponent {
|
||||
this.applicationDetailService.createApplicationDetails(applicationDetailData).subscribe({
|
||||
next: (applicationData: any) => {
|
||||
this.notificationService.showSuccess(`Application details added successfully`);
|
||||
this.headerIdCreated.emit(applicationData.HEADERID);
|
||||
this.headerIdCreated.emit({ HEADERID: +applicationData.HEADERID, APPLICATION_NAME: applicationDetailData.name });
|
||||
this.applicationDetailsForm.get('name')?.disable();
|
||||
this.disableSaveButton = true;
|
||||
this.isLoading = false;
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-delivery',
|
||||
imports: [],
|
||||
templateUrl: './delivery.component.html',
|
||||
styleUrl: './delivery.component.scss'
|
||||
})
|
||||
export class DeliveryComponent {
|
||||
@Input() headerid: number = 0;
|
||||
@Output() completed = new EventEmitter<boolean>();
|
||||
|
||||
}
|
||||
@ -15,15 +15,17 @@
|
||||
<mat-step *ngIf="applicationType === 'new'" [completed]="stepsCompleted.holderSelection"
|
||||
[editable]="stepsCompleted.applicationDetail">
|
||||
<ng-template matStepLabel>Holder Selection</ng-template>
|
||||
<app-holder (completed)="onHolderSelectionSaved($event)" [headerid]="headerid">
|
||||
</app-holder>
|
||||
<!-- <app-holder (completed)="onHolderSelectionSaved($event)" [headerid]="headerid">
|
||||
</app-holder> -->
|
||||
<app-search (selectHolderCompleted)="onHolderSelectionSaved($event)" [headerid]="headerid"></app-search>
|
||||
</mat-step>
|
||||
|
||||
<!-- Goods Section Step -->
|
||||
<mat-step *ngIf="applicationType === 'new'" [completed]="stepsCompleted.goodsSection"
|
||||
[editable]="stepsCompleted.applicationDetail">
|
||||
<ng-template matStepLabel>Goods Section</ng-template>
|
||||
<app-goods (completed)="onGoodsSectionSaved($event)" [headerid]="headerid">
|
||||
<app-goods (completed)="onGoodsSectionSaved($event)" [headerid]="headerid" [isEditMode]="isEditMode"
|
||||
[userPreferences]="userPreferences">
|
||||
</app-goods>
|
||||
</mat-step>
|
||||
|
||||
@ -35,33 +37,11 @@
|
||||
</app-travel-plan>
|
||||
</mat-step>
|
||||
|
||||
<!-- Insurance Step -->
|
||||
<mat-step *ngIf="applicationType === 'new' || applicationType === 'extend' || applicationType === 'additional'"
|
||||
[completed]="stepsCompleted.insurance" [editable]="stepsCompleted.applicationDetail">
|
||||
<ng-template matStepLabel>Insurance</ng-template>
|
||||
<app-insurance (completed)="onInsuranceSaved($event)" [headerid]="headerid">
|
||||
</app-insurance>
|
||||
</mat-step>
|
||||
|
||||
<!-- Shipping Step -->
|
||||
<!-- Shipping & Payment Step -->
|
||||
<mat-step [completed]="stepsCompleted.shipping" [editable]="stepsCompleted.applicationDetail">
|
||||
<ng-template matStepLabel>Shipping</ng-template>
|
||||
<ng-template matStepLabel>Shipping & Payment</ng-template>
|
||||
<app-shipping (completed)="onShippingSaved($event)" [headerid]="headerid">
|
||||
</app-shipping>
|
||||
</mat-step>
|
||||
|
||||
<!-- Delivery Method Step -->
|
||||
<mat-step [completed]="stepsCompleted.deliveryMethod" [editable]="stepsCompleted.applicationDetail">
|
||||
<ng-template matStepLabel>Delivery Method</ng-template>
|
||||
<app-delivery (completed)="onDeliveryMethodSaved($event)" [headerid]="headerid">
|
||||
</app-delivery>
|
||||
</mat-step>
|
||||
|
||||
<!-- Payment Step -->
|
||||
<mat-step [completed]="stepsCompleted.payment" [editable]="stepsCompleted.applicationDetail">
|
||||
<ng-template matStepLabel>Payment</ng-template>
|
||||
<app-payment (completed)="onPaymentSaved($event)" [headerid]="headerid">
|
||||
</app-payment>
|
||||
</mat-step>
|
||||
</mat-stepper>
|
||||
</div>
|
||||
@ -7,17 +7,16 @@ import { StepperSelectionEvent } from '@angular/cdk/stepper';
|
||||
import { GoodsComponent } from '../goods/goods.component';
|
||||
import { HolderComponent } from '../holder/holder.component';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { DeliveryComponent } from '../delivery/delivery.component';
|
||||
import { InsuranceComponent } from '../insurance/insurance.component';
|
||||
import { PaymentComponent } from '../payment/payment.component';
|
||||
import { ShippingComponent } from '../shipping/shipping.component';
|
||||
import { TravelPlanComponent } from '../travel-plan/travel-plan.component';
|
||||
import { UserPreferences } from '../../core/models/user-preference';
|
||||
import { UserPreferencesService } from '../../core/services/user-preference.service';
|
||||
import { SearchComponent } from '../../holder/search/search.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-edit-carnet',
|
||||
imports: [AngularMaterialModule, CommonModule, ReactiveFormsModule,
|
||||
ApplicationComponent, HolderComponent, GoodsComponent, TravelPlanComponent, InsuranceComponent,
|
||||
ShippingComponent, DeliveryComponent, PaymentComponent],
|
||||
ApplicationComponent, SearchComponent, GoodsComponent, TravelPlanComponent, ShippingComponent],
|
||||
templateUrl: './edit-carnet.component.html',
|
||||
styleUrl: './edit-carnet.component.scss'
|
||||
})
|
||||
@ -27,6 +26,7 @@ export class EditCarnetComponent {
|
||||
applicationType: 'new' | 'additional' | 'duplicate' | 'extend' | null = 'new';
|
||||
isEditMode = true; // Set to true for edit mode
|
||||
headerid: number = 0;
|
||||
userPreferences: UserPreferences;
|
||||
|
||||
// Track completion of each step
|
||||
stepsCompleted = {
|
||||
@ -34,14 +34,15 @@ export class EditCarnetComponent {
|
||||
holderSelection: false,
|
||||
goodsSection: false,
|
||||
travelPlan: false,
|
||||
insurance: false,
|
||||
shipping: false,
|
||||
deliveryMethod: false,
|
||||
payment: false
|
||||
shipping: false
|
||||
};
|
||||
|
||||
private route = inject(ActivatedRoute);
|
||||
|
||||
constructor(userPrefenceService: UserPreferencesService) {
|
||||
this.userPreferences = userPrefenceService.getPreferences();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
const idParam = this.route.snapshot.paramMap.get('headerid');
|
||||
this.headerid = idParam ? parseInt(idParam, 10) : 0;
|
||||
@ -63,19 +64,7 @@ export class EditCarnetComponent {
|
||||
this.stepsCompleted.travelPlan = completed;
|
||||
}
|
||||
|
||||
onInsuranceSaved(completed: boolean): void {
|
||||
this.stepsCompleted.insurance = completed;
|
||||
}
|
||||
|
||||
onShippingSaved(completed: boolean): void {
|
||||
this.stepsCompleted.shipping = completed;
|
||||
}
|
||||
|
||||
onDeliveryMethodSaved(completed: boolean): void {
|
||||
this.stepsCompleted.deliveryMethod = completed;
|
||||
}
|
||||
|
||||
onPaymentSaved(completed: boolean): void {
|
||||
this.stepsCompleted.payment = completed;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1 +1,235 @@
|
||||
<p>goods works!</p>
|
||||
<div class="goods-container">
|
||||
|
||||
<form [formGroup]="goodsForm">
|
||||
<!-- Purpose Section -->
|
||||
<div class="form-section">
|
||||
<div class="checkbox-group">
|
||||
<mat-label>Goods to be imported as </mat-label>
|
||||
<mat-checkbox formControlName="commercialSample">Commercial Sample</mat-checkbox>
|
||||
<mat-checkbox formControlName="professionalEquipment">Professional Equipment</mat-checkbox>
|
||||
<mat-checkbox formControlName="exhibitionsFair">Exhibitions and Fair</mat-checkbox>
|
||||
|
||||
<mat-error *ngIf="goodsForm.errors?.['atLeastOneRequired']">
|
||||
At least one must be selected
|
||||
</mat-error>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<div class="checkbox-group">
|
||||
<mat-checkbox labelPosition="before" formControlName="roadVehiclesUsed">Road Vehicles
|
||||
used?</mat-checkbox>
|
||||
<mat-checkbox labelPosition="before" formControlName="horseUsed">Horse used?</mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Authorized Representatives Section -->
|
||||
<div class="form-section">
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>Authorized Representative(s)</mat-label>
|
||||
<textarea matInput formControlName="authorizedRepresentatives" rows="3"></textarea>
|
||||
<mat-error *ngIf="goodsForm.get('authorizedRepresentatives')?.errors?.['required']">
|
||||
Authorized Representative(s) is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="table-actions">
|
||||
<h3>Goods Items</h3>
|
||||
<div class="actions">
|
||||
<button mat-raised-button color="primary" type="button" (click)="addNewItem()">
|
||||
<mat-icon>add</mat-icon> Add Item
|
||||
</button>
|
||||
<div class="upload-section">
|
||||
<input type="file" accept=".xlsx,.xls,.csv" (change)="fileUpload($event)" hidden #fileInput>
|
||||
<button mat-raised-button type="button" (click)="fileInput.click()" [disabled]="isProcessing">
|
||||
<mat-icon>upload</mat-icon> Upload
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-container mat-elevation-z8">
|
||||
<div class="loading-shade" *ngIf="isLoading">
|
||||
<mat-spinner diameter="50"></mat-spinner>
|
||||
</div>
|
||||
|
||||
<table mat-table [dataSource]="dataSource" matSort>
|
||||
<!-- Item Number Column -->
|
||||
<ng-container matColumnDef="itemNumber">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Item Number</th>
|
||||
<td mat-cell *matCellDef="let item">{{ item.itemNumber }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Description Column -->
|
||||
<ng-container matColumnDef="description">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Description</th>
|
||||
<td mat-cell *matCellDef="let item">{{ item.description }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Pieces Column -->
|
||||
<ng-container matColumnDef="pieces">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Pieces</th>
|
||||
<td mat-cell *matCellDef="let item">{{ item.pieces }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Weight Column -->
|
||||
<ng-container matColumnDef="weight">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Weight</th>
|
||||
<td mat-cell *matCellDef="let item">{{ formatDecimalDisplay(item.weight, 4) }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Unit of Measure Column -->
|
||||
<ng-container matColumnDef="unitOfMeasure">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Unit Of Measure</th>
|
||||
<td mat-cell *matCellDef="let item">{{ getUnitOfMeasureLabel(item.unitOfMeasure) }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Value Column -->
|
||||
<ng-container matColumnDef="value">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Value</th>
|
||||
<td mat-cell *matCellDef="let item">{{ formatDecimalDisplay(item.value, 2) | currency}}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Country of Origin Column -->
|
||||
<ng-container matColumnDef="countryOfOrigin">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Country of Origin</th>
|
||||
<td mat-cell *matCellDef="let item">{{ getCountryLabel(item.countryOfOrigin) }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Actions Column -->
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef>Actions</th>
|
||||
<td mat-cell *matCellDef="let item; let i = index">
|
||||
<button mat-icon-button color="primary" (click)="editItem(item)" matTooltip="Edit">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="warn" (click)="deleteItem(item)" matTooltip="Delete">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
|
||||
<tr matNoDataRow *matNoDataRow>
|
||||
<td [colSpan]="displayedColumns.length" class="no-data-message">
|
||||
<mat-icon>info</mat-icon>
|
||||
<span>No items added</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<mat-paginator [length]="dataSource.data.length" [pageSizeOptions]="[userPreferences.pageSize!]"
|
||||
[hidePageSize]="true" showFirstLastButtons></mat-paginator>
|
||||
</div>
|
||||
|
||||
<!-- Item Form -->
|
||||
<div class="form-container" *ngIf="showItemForm">
|
||||
<form [formGroup]="itemForm" (ngSubmit)="saveItem()">
|
||||
<div class="form-header">
|
||||
<h3>{{ isEditing ? 'Edit Item' : 'Add New Item' }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Item Number</mat-label>
|
||||
<input matInput formControlName="itemNumber" required>
|
||||
<mat-error *ngIf="itemForm.get('itemNumber')?.errors?.['required']">
|
||||
Item Number is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="description">
|
||||
<mat-label>Description</mat-label>
|
||||
<input matInput formControlName="description" required>
|
||||
<mat-error *ngIf="itemForm.get('description')?.errors?.['required']">
|
||||
Description is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Pieces</mat-label>
|
||||
<input matInput type="number" formControlName="pieces" required>
|
||||
<mat-error *ngIf="itemForm.get('pieces')?.errors?.['required']">
|
||||
Pieces is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="itemForm.get('pieces')?.errors?.['min']">
|
||||
Must be at least 1
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Weight</mat-label>
|
||||
<input matInput type="number" formControlName="weight" required>
|
||||
<mat-error *ngIf="itemForm.get('weight')?.errors?.['required']">
|
||||
Weight is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="itemForm.get('weight')?.errors?.['min']">
|
||||
Must be positive
|
||||
</mat-error>
|
||||
<mat-error *ngIf="itemForm.get('weight')?.errors?.['pattern']">
|
||||
Maximum 4 decimal places allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Unit of Measure</mat-label>
|
||||
<mat-select formControlName="unitOfMeasure" required>
|
||||
<mat-option *ngFor="let unit of unitsOfMeasures" [value]="unit.value">
|
||||
{{ unit.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="itemForm.get('unitOfMeasure')?.errors?.['required']">
|
||||
Unit of measure is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Value</mat-label>
|
||||
<input matInput type="number" formControlName="value" required>
|
||||
<mat-error *ngIf="itemForm.get('value')?.errors?.['required']">
|
||||
Value is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="itemForm.get('value')?.errors?.['min']">
|
||||
Must be positive
|
||||
</mat-error>
|
||||
<mat-error *ngIf="itemForm.get('value')?.errors?.['pattern']">
|
||||
Maximum 2 decimal places allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="country">
|
||||
<mat-label>Country of Origin</mat-label>
|
||||
<mat-select formControlName="countryOfOrigin" required>
|
||||
<mat-option *ngFor="let country of countries" [value]="country.value">
|
||||
{{ country.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="itemForm.get('countryOfOrigin')?.errors?.['required']">
|
||||
Country of origin is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button mat-button type="button" (click)="cancelEdit()">Cancel</button>
|
||||
<button mat-raised-button color="primary" type="submit" [disabled]="itemForm.invalid">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!showItemForm" class="form-actions">
|
||||
<button mat-raised-button color="primary" (click)="onSubmit()" [disabled]="goodsForm.invalid">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -0,0 +1,169 @@
|
||||
.goods-container {
|
||||
padding: 16px;
|
||||
|
||||
.form-section {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
|
||||
mat-label {
|
||||
margin-top: 5px;
|
||||
color: var(--mat-checkbox-label-text-color, var(--mat-sys-on-surface));
|
||||
font-family: var(--mat-checkbox-label-text-font, var(--mat-sys-body-medium-font));
|
||||
line-height: var(--mat-checkbox-label-text-line-height, var(--mat-sys-body-medium-line-height));
|
||||
font-size: var(--mat-checkbox-label-text-size, var(--mat-sys-body-medium-size));
|
||||
letter-spacing: var(--mat-checkbox-label-text-tracking, var(--mat-sys-body-medium-tracking));
|
||||
font-weight: var(--mat-checkbox-label-text-weight, var(--mat-sys-body-medium-weight));
|
||||
}
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.table-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
color: var(--mat-sys-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
span {
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.table-container {
|
||||
position: relative;
|
||||
overflow-x: auto;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.loading-shade {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
mat-table {
|
||||
width: 100%;
|
||||
|
||||
mat-icon {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
.mat-column-actions {
|
||||
width: 180px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.no-data-message {
|
||||
text-align: center;
|
||||
padding: 0.9rem;
|
||||
color: rgba(0, 0, 0, 0.54);
|
||||
|
||||
mat-icon {
|
||||
font-size: 1rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-bottom: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
mat-paginator {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.12);
|
||||
border-radius: 0 0 8px 8px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-container {
|
||||
// position: fixed;
|
||||
// top: 0;
|
||||
// right: 0;
|
||||
// bottom: 0;
|
||||
// width: 400px;
|
||||
// background: white;
|
||||
// box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1);
|
||||
// padding: 24px;
|
||||
// overflow-y: auto;
|
||||
// z-index: 100;
|
||||
|
||||
.form-header {
|
||||
margin-bottom: 24px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
color: var(--mat-sys-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
mat-form-field {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.description {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.country {
|
||||
flex: 2;
|
||||
}
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 16px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 24px;
|
||||
}
|
||||
}
|
||||
@ -1,14 +1,451 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { Component, EventEmitter, inject, Input, Output, ViewChild } from '@angular/core';
|
||||
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
|
||||
import { GoodsService } from '../../core/services/carnet/goods.service';
|
||||
import { NotificationService } from '../../core/services/common/notification.service';
|
||||
import * as XLSX from 'xlsx';
|
||||
import { CustomPaginator } from '../../shared/custom-paginator';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatPaginatorIntl, MatPaginator } from '@angular/material/paginator';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { ConfirmDialogComponent } from '../../shared/components/confirm-dialog/confirm-dialog.component';
|
||||
import { UserPreferences } from '../../core/models/user-preference';
|
||||
import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service';
|
||||
import { Country } from '../../core/models/country';
|
||||
import { UnitOfMeasure } from '../../core/models/unitofmeasure';
|
||||
import { CommonService } from '../../core/services/common/common.service';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
import { Goods, GoodsItem } from '../../core/models/carnet/goods';
|
||||
|
||||
@Component({
|
||||
selector: 'app-goods',
|
||||
imports: [AngularMaterialModule, CommonModule],
|
||||
imports: [AngularMaterialModule, CommonModule, ReactiveFormsModule],
|
||||
templateUrl: './goods.component.html',
|
||||
styleUrl: './goods.component.scss'
|
||||
styleUrl: './goods.component.scss',
|
||||
providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }],
|
||||
})
|
||||
export class GoodsComponent {
|
||||
@Input() headerid: number = 0;
|
||||
@Input() userPreferences: UserPreferences = {};
|
||||
@Input() isEditMode: boolean = false;
|
||||
@Output() completed = new EventEmitter<boolean>();
|
||||
|
||||
@ViewChild(MatPaginator) paginator!: MatPaginator;
|
||||
@ViewChild(MatSort) sort!: MatSort;
|
||||
|
||||
// Table configuration
|
||||
displayedColumns: string[] = ['itemNumber', 'description', 'pieces', 'weight', 'unitOfMeasure', 'value', 'countryOfOrigin', 'actions'];
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
|
||||
// Form controls
|
||||
goodsForm: FormGroup;
|
||||
itemForm: FormGroup;
|
||||
|
||||
// UI state
|
||||
isEditing = false;
|
||||
currentItem: GoodsItem | null = null;
|
||||
isLoading = false;
|
||||
showItemForm = false;
|
||||
fileToUpload: File | null = null;
|
||||
isProcessing = false;
|
||||
|
||||
// Data
|
||||
countries: Country[] = [];
|
||||
unitsOfMeasures: UnitOfMeasure[] = [];
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
private fb = inject(FormBuilder);
|
||||
private goodsService = inject(GoodsService);
|
||||
private notificationService = inject(NotificationService);
|
||||
private dialog = inject(MatDialog);
|
||||
private errorHandler = inject(ApiErrorHandlerService);
|
||||
private commonService = inject(CommonService);
|
||||
|
||||
constructor() {
|
||||
// Main form for goods section
|
||||
this.goodsForm = this.fb.group({
|
||||
commercialSample: [false],
|
||||
professionalEquipment: [false],
|
||||
exhibitionsFair: [false],
|
||||
roadVehiclesUsed: [false],
|
||||
horseUsed: [false],
|
||||
authorizedRepresentatives: ['', Validators.required]
|
||||
}, { validator: this.atLeastOneRequired }
|
||||
);
|
||||
|
||||
// Form for individual items
|
||||
this.itemForm = this.fb.group({
|
||||
itemNumber: ['', Validators.required],
|
||||
description: ['', Validators.required],
|
||||
pieces: [0, [Validators.required, Validators.min(1)]],
|
||||
weight: [0, [Validators.required, Validators.min(1), Validators.pattern(/^\d+(\.\d{1,4})?$/)]],
|
||||
unitOfMeasure: ['', Validators.required],
|
||||
value: [0, [Validators.required, Validators.min(1), Validators.pattern(/^\d+(\.\d{1,2})?$/)]],
|
||||
countryOfOrigin: ['', Validators.required]
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.dataSource.paginator = this.paginator;
|
||||
this.dataSource.sort = this.sort;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadCountries();
|
||||
this.loadUnitOfMeasures();
|
||||
|
||||
if (this.headerid > 0) {
|
||||
this.goodsService.getGoodDetailsByHeaderId(this.headerid).subscribe({
|
||||
next: (goodsDetail: Goods) => {
|
||||
if (goodsDetail) {
|
||||
this.patchFormData(goodsDetail);
|
||||
}
|
||||
this.isLoading = false;
|
||||
},
|
||||
error: (error: any) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load good details');
|
||||
this.notificationService.showError(errorMessage);
|
||||
this.isLoading = false;
|
||||
console.error('Error loading good details:', error);
|
||||
}
|
||||
});
|
||||
|
||||
this.loadGoodsItems();
|
||||
this.completed.emit(this.goodsForm.valid && this.dataSource.data.length > 0);
|
||||
}
|
||||
}
|
||||
|
||||
loadGoodsItems(): void {
|
||||
this.isLoading = true;
|
||||
|
||||
this.goodsService.getGoodsItemsByHeaderId(this.headerid).subscribe({
|
||||
next: (items: GoodsItem[]) => {
|
||||
this.dataSource.data = items;
|
||||
this.isLoading = false;
|
||||
},
|
||||
error: (error: any) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load goods items');
|
||||
this.notificationService.showError(errorMessage);
|
||||
this.isLoading = false;
|
||||
console.error('Error loading goods items:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadUnitOfMeasures(): void {
|
||||
this.commonService.getUnitOfMeasures()
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: (units) => {
|
||||
this.unitsOfMeasures = units;
|
||||
this.isLoading = false;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Failed to load unit of measures', error);
|
||||
this.isLoading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
patchFormData(data: Goods): void {
|
||||
this.goodsForm.patchValue({
|
||||
commercialSample: data.commercialSample || false,
|
||||
professionalEquipment: data.professionalEquipment || false,
|
||||
exhibitionsFair: data.exhibitionsFair || false,
|
||||
roadVehiclesUsed: data.roadVehiclesUsed || false,
|
||||
horseUsed: data.horseUsed || false,
|
||||
authorizedRepresentatives: data.authorizedRepresentatives || ''
|
||||
});
|
||||
}
|
||||
|
||||
// Add new item to the table
|
||||
addNewItem(): void {
|
||||
this.showItemForm = true;
|
||||
this.isEditing = false;
|
||||
this.currentItem = null;
|
||||
this.itemForm.reset({
|
||||
pieces: 0,
|
||||
weight: 0,
|
||||
value: 0
|
||||
});
|
||||
}
|
||||
|
||||
// Edit existing item
|
||||
editItem(item: GoodsItem): void {
|
||||
this.showItemForm = true;
|
||||
this.isEditing = true;
|
||||
this.currentItem = item;
|
||||
this.itemForm.patchValue({
|
||||
itemNumber: item.itemNumber,
|
||||
description: item.description,
|
||||
pieces: item.pieces,
|
||||
weight: item.weight,
|
||||
unitOfMeasure: item.unitOfMeasure || 'kg',
|
||||
value: item.value,
|
||||
countryOfOrigin: item.countryOfOrigin || ''
|
||||
});
|
||||
}
|
||||
|
||||
saveItem(): void {
|
||||
if (this.itemForm.invalid || this.goodsForm.invalid) {
|
||||
this.itemForm.markAllAsTouched();
|
||||
this.goodsForm.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
|
||||
this.onSubmit();
|
||||
|
||||
const itemData: GoodsItem = this.itemForm.value;
|
||||
|
||||
// create a goods items array with a single item
|
||||
let itemDataArray: GoodsItem[] = [];
|
||||
itemDataArray.push(itemData);
|
||||
|
||||
const saveObservable = this.isEditing && this.currentItem
|
||||
? this.goodsService.updateGoodsItem(this.headerid, itemDataArray)
|
||||
: this.goodsService.addGoodsItem(this.headerid, itemDataArray);
|
||||
|
||||
saveObservable.subscribe({
|
||||
next: () => {
|
||||
this.notificationService.showSuccess(`Goods ${this.isEditing ? 'updated' : 'added'} successfully`);
|
||||
this.loadGoodsItems();
|
||||
this.cancelEdit();
|
||||
this.completed.emit(this.goodsForm.valid && this.dataSource.data.length > 0);
|
||||
},
|
||||
error: (error) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, `Failed to ${this.isEditing ? 'update' : 'add'} goods items`);
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error saving goods item:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Delete item with confirmation
|
||||
deleteItem(item: any): void {
|
||||
this.currentItem = item;
|
||||
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
|
||||
width: '350px',
|
||||
data: {
|
||||
title: 'Confirm Delete',
|
||||
message: 'Are you sure you want to delete this item?',
|
||||
confirmText: 'Delete',
|
||||
cancelText: 'Cancel'
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.goodsService.deleteGoodsItem(this.headerid, this.currentItem!).subscribe({
|
||||
next: () => {
|
||||
this.notificationService.showSuccess('Item deleted successfully');
|
||||
this.loadGoodsItems();
|
||||
this.cancelEdit();
|
||||
},
|
||||
error: (error) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to delete item');
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error deleting item:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle file selection for upload
|
||||
fileUpload(event: any): void {
|
||||
const file: File = event.target.files[0];
|
||||
|
||||
this.processExcelFile(file)
|
||||
.then(response => {
|
||||
console.log('File uploaded successfully:', response);
|
||||
// Handle success (e.g., update UI)
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error processing file:', error);
|
||||
this.notificationService.showError('Failed to upload file');
|
||||
});
|
||||
}
|
||||
|
||||
async processExcelFile(file: File): Promise<void> {
|
||||
|
||||
if (file) {
|
||||
const validTypes = [
|
||||
'application/vnd.ms-excel',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'text/csv'
|
||||
];
|
||||
|
||||
if (!validTypes.includes(file.type)) {
|
||||
this.notificationService.showError('Please upload only Excel (XLSX, XLS) or CSV files');
|
||||
return;
|
||||
}
|
||||
|
||||
this.fileToUpload = file;
|
||||
if (!this.fileToUpload || !this.headerid) return;
|
||||
|
||||
this.isProcessing = true;
|
||||
|
||||
try {
|
||||
const data = await this.readExcelFile(this.fileToUpload);
|
||||
const items = this.parseExcelData(data);
|
||||
|
||||
if (items.length === 0) {
|
||||
this.notificationService.showError('No valid items found in the file');
|
||||
this.isProcessing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// items.map(item => {
|
||||
// item.countryOfOrigin = this.getCountryValue(item.countryOfOrigin);
|
||||
// item.unitOfMeasure = this.getUnitOfMeasureValue(item.unitOfMeasure);
|
||||
// });
|
||||
|
||||
this.goodsService.addGoodsItem(this.headerid, items).subscribe({
|
||||
next: () => {
|
||||
this.notificationService.showSuccess(`Goods uploaded successfully`);
|
||||
this.loadGoodsItems();
|
||||
this.cancelEdit();
|
||||
this.completed.emit(this.goodsForm.valid && this.dataSource.data.length > 0);
|
||||
this.fileToUpload = null;
|
||||
},
|
||||
error: (error) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, `Failed to save the upload goods items`);
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error saving goods items:', error);
|
||||
this.fileToUpload = null;
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(error instanceof Error ? error.message : 'Failed to process file');
|
||||
} finally {
|
||||
this.isProcessing = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Read Excel file
|
||||
private readExcelFile(file: File): Promise<any[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (e: any) => {
|
||||
try {
|
||||
const data = new Uint8Array(e.target.result);
|
||||
const workbook = XLSX.read(data, { type: 'array' });
|
||||
const firstSheetName = workbook.SheetNames[0];
|
||||
const worksheet = workbook.Sheets[firstSheetName];
|
||||
const jsonData = XLSX.utils.sheet_to_json(worksheet);
|
||||
resolve(jsonData);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
reader.onerror = (error) => reject(error);
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
}
|
||||
|
||||
// Parse Excel data into our format
|
||||
private parseExcelData(data: any[]): any[] {
|
||||
return data.map(row => ({
|
||||
itemNumber: row['Item Number'] || row['itemNumber'] || '',
|
||||
description: row['Description'] || row['description'] || '',
|
||||
pieces: Number(row['Pieces'] || row['pieces'] || 0),
|
||||
weight: Number(row['Weight'] || row['weight'] || 0),
|
||||
unitOfMeasure: row['Unit of Measure'] || row['unit of measure'] || row['unitofmeasure'] || row['UnitOfMeasure'],
|
||||
value: Number(row['Value'] || row['value'] || 0),
|
||||
countryOfOrigin: row['Country Of Origin'] || row['Country of Origin'] || row['country of origin'] || row['countryoforigin'] || row['CountryOfOrigin'] || ''
|
||||
})).filter(item =>
|
||||
item.itemNumber &&
|
||||
item.description &&
|
||||
item.pieces > 0
|
||||
);
|
||||
}
|
||||
|
||||
// Save all goods data
|
||||
onSubmit(): void {
|
||||
if (this.goodsForm.invalid) {
|
||||
this.goodsForm.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = this.goodsForm.value;
|
||||
this.isLoading = true;
|
||||
|
||||
this.goodsService.saveGoodsData(this.headerid, formData).subscribe({
|
||||
next: () => {
|
||||
this.notificationService.showSuccess('Goods information saved successfully');
|
||||
this.completed.emit(this.goodsForm.valid && this.dataSource.data.length > 0);
|
||||
},
|
||||
error: (error) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to save goods information');
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error saving goods:', error);
|
||||
},
|
||||
complete: () => {
|
||||
this.isLoading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Cancel item editing
|
||||
cancelEdit(): void {
|
||||
this.showItemForm = false;
|
||||
this.isEditing = false;
|
||||
this.currentItem = null;
|
||||
this.itemForm.reset();
|
||||
}
|
||||
|
||||
private atLeastOneRequired(formGroup: FormGroup) {
|
||||
return (formGroup.get('commercialSample')?.value ||
|
||||
formGroup.get('professionalEquipment')?.value ||
|
||||
formGroup.get('exhibitionsFair')?.value) ? null : { atLeastOneRequired: true };
|
||||
}
|
||||
|
||||
getUnitOfMeasureLabel(value: string): string {
|
||||
const unit = this.unitsOfMeasures.find(u => u.value === value);
|
||||
return unit ? unit.name : value;
|
||||
}
|
||||
|
||||
getCountryLabel(value: string): string {
|
||||
const country = this.countries.find(c => c.value === value);
|
||||
return country ? country.name : value;
|
||||
}
|
||||
|
||||
getUnitOfMeasureValue(name: string): string {
|
||||
const unit = this.unitsOfMeasures.find(u => u.name === name);
|
||||
return unit ? unit.value : name;
|
||||
}
|
||||
|
||||
getCountryValue(name: string): string {
|
||||
const country = this.countries.find(c => c.name === name);
|
||||
return country ? country.value : name;
|
||||
}
|
||||
|
||||
formatDecimalDisplay(value: number, decimalPoints: number): string {
|
||||
if (value === null || value === undefined) return '';
|
||||
|
||||
// Format to show up to n decimal places, removing trailing zeros
|
||||
const parts = value.toString().split('.');
|
||||
if (parts.length === 1) return parts[0]; // No decimals
|
||||
return `${parts[0]}.${parts[1].substring(0, decimalPoints).replace(/0+$/, '')}`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1 +0,0 @@
|
||||
<p>insurance works!</p>
|
||||
@ -1,12 +0,0 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-insurance',
|
||||
imports: [],
|
||||
templateUrl: './insurance.component.html',
|
||||
styleUrl: './insurance.component.scss'
|
||||
})
|
||||
export class InsuranceComponent {
|
||||
@Input() headerid: number = 0;
|
||||
@Output() completed = new EventEmitter<boolean>();
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
<p>payment works!</p>
|
||||
@ -1,12 +0,0 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-payment',
|
||||
imports: [],
|
||||
templateUrl: './payment.component.html',
|
||||
styleUrl: './payment.component.scss'
|
||||
})
|
||||
export class PaymentComponent {
|
||||
@Input() headerid: number = 0;
|
||||
@Output() completed = new EventEmitter<boolean>();
|
||||
}
|
||||
@ -1 +1,302 @@
|
||||
<p>shipping works!</p>
|
||||
<div class="shipping-container">
|
||||
<form [formGroup]="shippingForm" (ngSubmit)="onSubmit()">
|
||||
<!-- Insurance Section -->
|
||||
<div class="section">
|
||||
<h3>Insurance & Bond</h3>
|
||||
<div class="checkbox-group">
|
||||
<mat-checkbox labelPosition="before" formControlName="needsBond">Do you need Bond from
|
||||
us?</mat-checkbox>
|
||||
<mat-checkbox labelPosition="before" formControlName="needsInsurance">Do you need insurance for your
|
||||
goods?</mat-checkbox>
|
||||
<mat-checkbox labelPosition="before" formControlName="needsLostDocProtection">Do you need Lost document
|
||||
protection?</mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Shipping Section -->
|
||||
<div class="section">
|
||||
<h3>Ship To</h3>
|
||||
<mat-radio-group formControlName="shipTo" class="radio-group" (change)="onShipToChange($event)">
|
||||
<mat-radio-button value="preparer">Preparer</mat-radio-button>
|
||||
<mat-radio-button value="holder">Holder</mat-radio-button>
|
||||
<mat-radio-button value="thirdParty">3rd Party</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
|
||||
<mat-card appearance="outlined" *ngIf="!showAddressForm && !showContactForm">
|
||||
<mat-card-content>
|
||||
<p>{{getAddressLabel()}}</p>
|
||||
</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>
|
||||
|
||||
<!-- Address Information -->
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline" class="address1">
|
||||
<mat-label>Address Line 1</mat-label>
|
||||
<input matInput formControlName="address1" required>
|
||||
<mat-error *ngIf="shippingForm.get('address.address1')?.errors?.['required']">
|
||||
Address is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="shippingForm.get('address.address1')?.errors?.['maxlength']">
|
||||
Maximum 100 characters allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline" class="address2">
|
||||
<mat-label>Address Line 2 (Optional)</mat-label>
|
||||
<input matInput formControlName="address2">
|
||||
<mat-error *ngIf="shippingForm.get('address.address2')?.errors?.['maxlength']">
|
||||
Maximum 100 characters allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Location Information -->
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline" class="city">
|
||||
<mat-label>City</mat-label>
|
||||
<input matInput formControlName="city" required>
|
||||
<mat-error *ngIf="shippingForm.get('address.city')?.errors?.['required']">
|
||||
City is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="shippingForm.get('address.city')?.errors?.['maxlength']">
|
||||
Maximum 50 characters allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="country">
|
||||
<mat-label>Country</mat-label>
|
||||
<mat-select formControlName="country" required
|
||||
(selectionChange)="onCountryChange($event.value)">
|
||||
<mat-option *ngFor="let country of countries" [value]="country.value">
|
||||
{{ country.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="shippingForm.get('address.country')?.errors?.['required']">
|
||||
Country is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="state">
|
||||
<mat-label>State/Province</mat-label>
|
||||
<mat-select formControlName="state" required>
|
||||
<mat-option *ngFor="let state of states" [value]="state.value">
|
||||
{{ state.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="shippingForm.get('address.state')?.errors?.['required']">
|
||||
State is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="zip">
|
||||
<mat-label>ZIP/Postal Code</mat-label>
|
||||
<input matInput formControlName="zip" required>
|
||||
<mat-error *ngIf="shippingForm.get('address.zip')?.errors?.['required']">
|
||||
ZIP/Postal code is required
|
||||
</mat-error>
|
||||
<mat-error
|
||||
*ngIf="shippingForm.get('address.country')?.value === 'US' && shippingForm.get('address.zip')?.touched && shippingForm.get('address.zip')?.errors?.['invalidUSZip']">
|
||||
Please enter a valid 5-digit US ZIP code
|
||||
</mat-error>
|
||||
<mat-error
|
||||
*ngIf="shippingForm.get('address.country')?.value === 'CA' && shippingForm.get('address.zip')?.touched && shippingForm.get('address.zip')?.errors?.['invalidCanadaPostal']">
|
||||
Please enter a valid postal code (e.g., A1B2C3)
|
||||
</mat-error>
|
||||
</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 *ngIf="showContactForm" class="contact-form" formGroupName="contact">
|
||||
<h4>Contact Information</h4>
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>First Name</mat-label>
|
||||
<input matInput formControlName="firstName" required>
|
||||
<mat-icon matSuffix>person</mat-icon>
|
||||
<mat-error *ngIf="shippingForm.get('contact.firstName')?.errors?.['required']">
|
||||
First name is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="shippingForm.get('contact.firstName')?.errors?.['maxlength']">
|
||||
Maximum 50 characters allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="small-field">
|
||||
<mat-label>Middle Initial</mat-label>
|
||||
<input matInput formControlName="middleInitial" maxlength="1">
|
||||
<mat-error *ngIf="shippingForm.get('contact.middleInitial')?.errors?.['maxlength']">
|
||||
Only 1 character allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Last Name</mat-label>
|
||||
<input matInput formControlName="lastName" required>
|
||||
<mat-icon matSuffix>person</mat-icon>
|
||||
<mat-error *ngIf="shippingForm.get('contact.lastName')?.errors?.['required']">
|
||||
Last name is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="shippingForm.get('contact.lastName')?.errors?.['maxlength']">
|
||||
Maximum 50 characters allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Title</mat-label>
|
||||
<input matInput formControlName="title" required>
|
||||
<mat-icon matSuffix>work</mat-icon>
|
||||
<mat-error *ngIf="shippingForm.get('contact.title')?.errors?.['required']">
|
||||
Title is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="shippingForm.get('contact.title')?.errors?.['maxlength']">
|
||||
Maximum 100 characters allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Phone</mat-label>
|
||||
<input matInput formControlName="phone" required>
|
||||
<mat-icon matSuffix>phone</mat-icon>
|
||||
<mat-error *ngIf="shippingForm.get('contact.phone')?.errors?.['required']">
|
||||
Phone is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="shippingForm.get('contact.phone')?.errors?.['pattern']">
|
||||
Please enter a valid phone number (10-15 digits)
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Mobile</mat-label>
|
||||
<input matInput formControlName="mobile">
|
||||
<mat-icon matSuffix>smartphone</mat-icon>
|
||||
<mat-error *ngIf="shippingForm.get('contact.mobile')?.errors?.['required']">
|
||||
Mobile is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="shippingForm.get('contact.mobile')?.errors?.['pattern']">
|
||||
Please enter a valid mobile number (10-15 digits)
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Fax</mat-label>
|
||||
<input matInput formControlName="fax">
|
||||
<mat-icon matSuffix>fax</mat-icon>
|
||||
<mat-error *ngIf="shippingForm.get('contact.fax')?.errors?.['pattern']">
|
||||
Please enter a valid fax number (10-15 digits)
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Email</mat-label>
|
||||
<input matInput formControlName="email" required>
|
||||
<mat-icon matSuffix>email</mat-icon>
|
||||
<mat-error *ngIf="shippingForm.get('contact.email')?.errors?.['required']">
|
||||
Email is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="shippingForm.get('contact.email')?.errors?.['email']">
|
||||
Please enter a valid email address
|
||||
</mat-error>
|
||||
<mat-error *ngIf="shippingForm.get('contact.email')?.errors?.['maxlength']">
|
||||
Maximum 100 characters allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>Reference Number</mat-label>
|
||||
<input matInput formControlName="refNumber">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>Notes</mat-label>
|
||||
<textarea matInput formControlName="notes" rows="2"></textarea>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delivery Section -->
|
||||
<div class="section">
|
||||
<h3>Delivery Method</h3>
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Delivery Type</mat-label>
|
||||
<mat-select formControlName="deliveryType" required>
|
||||
<mat-option *ngFor="let deliveryType of deliveryTypes" [value]="deliveryType.value">
|
||||
{{ deliveryType.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="shippingForm.get('deliveryType')?.errors?.['required']">
|
||||
Delivery Type is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Delivery Method</mat-label>
|
||||
<mat-select formControlName="deliveryMethod" required>
|
||||
<mat-option *ngFor="let deliveryMethod of deliveryMethods" [value]="deliveryMethod.value">
|
||||
{{ deliveryMethod.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="shippingForm.get('deliveryMethod')?.errors?.['required']">
|
||||
Delivery Method is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div *ngIf="shippingForm.get('deliveryMethod')?.value === 'customerCourier'" 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-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Payment Section -->
|
||||
<div class="section">
|
||||
<h3>Payment Method</h3>
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Payment Method</mat-label>
|
||||
<mat-select formControlName="paymentMethod" required>
|
||||
<mat-option *ngFor="let paymentType of paymentTypes" [value]="paymentType.value">
|
||||
{{ paymentType.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="shippingForm.get('paymentMethod')?.errors?.['required']">
|
||||
Payment Method is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<!-- <div class="form-row">
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>Payment Notes</mat-label>
|
||||
<textarea matInput formControlName="notes" rows="2"></textarea>
|
||||
</mat-form-field>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button mat-raised-button color="primary" type="submit" [disabled]="shippingForm.invalid || isLoading">
|
||||
<span *ngIf="!isLoading">Save</span>
|
||||
<mat-spinner *ngIf="isLoading" diameter="24"></mat-spinner>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@ -0,0 +1,76 @@
|
||||
.shipping-container {
|
||||
padding: 16px;
|
||||
|
||||
.section {
|
||||
margin-bottom: 24px;
|
||||
padding: 16px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
|
||||
h3 {
|
||||
margin: 0px 0px 16px 0px;
|
||||
color: var(--mat-sys-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 16px 0 8px;
|
||||
color: var(--mat-sys-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.mat-mdc-card-content:first-child {
|
||||
padding: 0 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
|
||||
mat-form-field {
|
||||
flex: 1;
|
||||
margin-bottom: 6px;
|
||||
|
||||
&.full-width {
|
||||
flex: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.small-field {
|
||||
max-width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.shipping-container {
|
||||
.form-row {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
mat-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,12 +1,376 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
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';
|
||||
|
||||
@Component({
|
||||
selector: 'app-shipping',
|
||||
imports: [],
|
||||
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<boolean>();
|
||||
|
||||
private fb = inject(FormBuilder);
|
||||
private shippingService = inject(ShippingService);
|
||||
private notificationService = inject(NotificationService);
|
||||
private errorHandler = inject(ApiErrorHandlerService);
|
||||
private commonService = inject(CommonService);
|
||||
|
||||
shippingForm: FormGroup;
|
||||
isLoading = false;
|
||||
showAddressForm = false;
|
||||
showContactForm = false;
|
||||
|
||||
countriesHasStates = ['US', 'CA', 'MX'];
|
||||
countries: Country[] = [];
|
||||
states: State[] = [];
|
||||
deliveryTypes: DeliveryType[] = [];
|
||||
deliveryMethods: DeliveryMethod[] = [];
|
||||
paymentTypes: PaymentType[] = [];
|
||||
|
||||
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',
|
||||
}
|
||||
|
||||
preparerAddress = {
|
||||
address1: '123 Main St',
|
||||
address2: 'Suite 100',
|
||||
city: 'Anytown',
|
||||
state: 'CA',
|
||||
zip: '12345',
|
||||
country: 'US'
|
||||
};
|
||||
|
||||
holderAddress = {
|
||||
address1: '456 Holder St',
|
||||
address2: 'Apt 200',
|
||||
city: 'Othertown',
|
||||
state: 'NY',
|
||||
zip: '67890',
|
||||
country: 'US'
|
||||
};
|
||||
|
||||
holderContact = {
|
||||
firstName: 'Jane',
|
||||
lastName: 'Smith',
|
||||
middleInitial: 'B',
|
||||
title: 'Ms.',
|
||||
phone: '2345678901',
|
||||
mobile: '1098765432',
|
||||
fax: '2345678901',
|
||||
email: 'jan@sm.cm'
|
||||
};
|
||||
|
||||
constructor() {
|
||||
this.shippingForm = this.fb.group({
|
||||
needsBond: [false],
|
||||
needsInsurance: [false],
|
||||
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')]],
|
||||
}),
|
||||
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)]],
|
||||
refNumber: [''],
|
||||
notes: ['']
|
||||
}),
|
||||
deliveryType: ['', Validators.required],
|
||||
deliveryMethod: ['', Validators.required],
|
||||
courierAccount: [''],
|
||||
paymentMethod: ['', Validators.required],
|
||||
notes: ['']
|
||||
});
|
||||
|
||||
// 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 === 'customerCourier') {
|
||||
courierControl?.setValidators([Validators.required]);
|
||||
} else {
|
||||
courierControl?.clearValidators();
|
||||
}
|
||||
courierControl?.updateValueAndValidity();
|
||||
});
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
onCountryChange(country: string): void {
|
||||
this.shippingForm.get('address.state')?.reset();
|
||||
|
||||
if (country) {
|
||||
this.loadStates(country);
|
||||
}
|
||||
|
||||
this.shippingForm.get('address.zip')?.updateValueAndValidity();
|
||||
}
|
||||
|
||||
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,
|
||||
address: shipping.address,
|
||||
contact: shipping.contact,
|
||||
deliveryType: shipping.deliveryType,
|
||||
deliveryMethod: shipping.deliveryMethod,
|
||||
courierAccount: shipping.courierAccount,
|
||||
paymentMethod: shipping.paymentMethod,
|
||||
notes: shipping.notes
|
||||
});
|
||||
|
||||
if (shipping.address?.country) {
|
||||
this.loadStates(shipping.address?.country);
|
||||
}
|
||||
}
|
||||
|
||||
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 => {
|
||||
// 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();
|
||||
// });
|
||||
// }
|
||||
|
||||
// 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 {
|
||||
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}`;
|
||||
}
|
||||
|
||||
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 '';
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,2 +0,0 @@
|
||||
export interface Delivery {
|
||||
}
|
||||
@ -1,2 +1,20 @@
|
||||
export interface Goods {
|
||||
commercialSample: boolean;
|
||||
professionalEquipment: boolean;
|
||||
exhibitionsFair: boolean;
|
||||
roadVehiclesUsed: boolean;
|
||||
horseUsed: boolean;
|
||||
authorizedRepresentatives: string;
|
||||
items?: GoodsItem[] | null;
|
||||
}
|
||||
|
||||
export interface GoodsItem {
|
||||
id?: number;
|
||||
itemNumber: string;
|
||||
description: string;
|
||||
pieces: number;
|
||||
weight: number;
|
||||
unitOfMeasure: string;
|
||||
value: number;
|
||||
countryOfOrigin: string;
|
||||
}
|
||||
@ -1,2 +0,0 @@
|
||||
export interface Insurance {
|
||||
}
|
||||
@ -1,2 +0,0 @@
|
||||
export interface Payment {
|
||||
}
|
||||
8
src/app/core/models/carnet/shipping-address.ts
Normal file
8
src/app/core/models/carnet/shipping-address.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export interface ShippingAddress {
|
||||
address1: string;
|
||||
address2?: string | null;
|
||||
city: string;
|
||||
state: string;
|
||||
country: string;
|
||||
zip: string;
|
||||
}
|
||||
12
src/app/core/models/carnet/shipping-contact.ts
Normal file
12
src/app/core/models/carnet/shipping-contact.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export interface ShippingContact {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
middleInitial?: string | null;
|
||||
title: string;
|
||||
phone: string;
|
||||
mobile: string;
|
||||
fax?: string | null;
|
||||
email: string;
|
||||
refNumber?: string | null;
|
||||
notes?: string | null;
|
||||
}
|
||||
@ -1,2 +1,23 @@
|
||||
import { ShippingAddress } from "./shipping-address";
|
||||
import { ShippingContact } from "./shipping-contact";
|
||||
|
||||
export interface Shipping {
|
||||
// Insurance details
|
||||
needsBond: boolean;
|
||||
needsInsurance: boolean;
|
||||
needsLostDocProtection: boolean;
|
||||
|
||||
// Shipping details
|
||||
shipTo: string;
|
||||
address?: ShippingAddress;
|
||||
contact?: ShippingContact;
|
||||
|
||||
// Delivery details
|
||||
deliveryType: string;
|
||||
deliveryMethod: string;
|
||||
courierAccount?: string;
|
||||
|
||||
// Payment details
|
||||
paymentMethod: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
5
src/app/core/models/delivery-method.ts
Normal file
5
src/app/core/models/delivery-method.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface DeliveryMethod {
|
||||
name: string;
|
||||
id: string;
|
||||
value: string;
|
||||
}
|
||||
30
src/app/core/models/holder/contact.ts
Normal file
30
src/app/core/models/holder/contact.ts
Normal file
@ -0,0 +1,30 @@
|
||||
export interface HolderContact {
|
||||
HOLDERCONTACTID: number;
|
||||
HOLDERID: number;
|
||||
SPID: number;
|
||||
FIRSTNAME: string;
|
||||
LASTNAME: string;
|
||||
MIDDLEINITIAL: string | null;
|
||||
TITLE: string;
|
||||
PHONE: string;
|
||||
MOBILE: string;
|
||||
FAX: string | null;
|
||||
EMAILADDRESS: string;
|
||||
DATECREATED: string;
|
||||
CREATEDBY: string;
|
||||
INACTIVEFLAG: string;
|
||||
INACTIVEDATE: string | null;
|
||||
LASTUPDATEDBY: string | null;
|
||||
LASTUPDATEDDATE: string | null;
|
||||
}
|
||||
|
||||
export interface ContactFormModel {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
middleInitial: string;
|
||||
title: string;
|
||||
phone: string;
|
||||
mobile: string;
|
||||
fax: string;
|
||||
email: string;
|
||||
};
|
||||
40
src/app/core/models/holder/holder-details.ts
Normal file
40
src/app/core/models/holder/holder-details.ts
Normal file
@ -0,0 +1,40 @@
|
||||
export interface HolderDetail {
|
||||
HOLDERID?: number;
|
||||
SPID: number;
|
||||
LOCATIONID: number;
|
||||
HOLDERNO: string;
|
||||
HOLDERTYPE: string;
|
||||
USCIBMEMBERFLAG: string;
|
||||
GOVAGENCYFLAG: string;
|
||||
HOLDERNAME: string;
|
||||
NAMEQUALIFIER: string | null;
|
||||
ADDLNAME: string | null;
|
||||
ADDRESS1: string;
|
||||
ADDRESS2: string | null;
|
||||
CITY: string;
|
||||
STATE: string;
|
||||
ZIP: string;
|
||||
COUNTRY: string;
|
||||
DATECREATED: string;
|
||||
CREATEDBY: string;
|
||||
INACTIVEFLAG: string;
|
||||
INACTIVEDATE: string | null;
|
||||
LASTUPDATEDBY: string | null;
|
||||
LASTUPDATEDDATE: string | null;
|
||||
}
|
||||
|
||||
|
||||
export interface HolderDetailFormType {
|
||||
holderName: string;
|
||||
DBAName: string;
|
||||
HolderNumber: string;
|
||||
holderType: string;
|
||||
USCIBMember: string;
|
||||
govAgency: string;
|
||||
address1: string;
|
||||
address2: string;
|
||||
city: string;
|
||||
state: string;
|
||||
country: string;
|
||||
zip: string;
|
||||
}
|
||||
5
src/app/core/models/payment-type.ts
Normal file
5
src/app/core/models/payment-type.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface PaymentType {
|
||||
name: string;
|
||||
id: string;
|
||||
value: string;
|
||||
}
|
||||
5
src/app/core/models/unitofmeasure.ts
Normal file
5
src/app/core/models/unitofmeasure.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface UnitOfMeasure {
|
||||
name: string;
|
||||
id: string;
|
||||
value: string;
|
||||
}
|
||||
16
src/app/core/services/carnet/carnet-application.service.ts
Normal file
16
src/app/core/services/carnet/carnet-application.service.ts
Normal file
@ -0,0 +1,16 @@
|
||||
// appli.service.ts
|
||||
import { Injectable, signal } from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class CarnetStateStore {
|
||||
headerid = signal<number>(0);
|
||||
applicationName = signal<string>('');
|
||||
|
||||
resetState() {
|
||||
this.headerid.set(0);
|
||||
this.applicationName.set('');
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DeliveryService {
|
||||
|
||||
constructor() { }
|
||||
}
|
||||
@ -1,9 +1,131 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { environment } from '../../../../environments/environment';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { UserService } from '../common/user.service';
|
||||
import { Goods, GoodsItem } from '../../models/carnet/goods';
|
||||
import { filter, map, Observable } from 'rxjs';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class GoodsService {
|
||||
private apiUrl = environment.apiUrl;
|
||||
private apiDb = environment.apiDb;
|
||||
|
||||
constructor() { }
|
||||
private http = inject(HttpClient);
|
||||
private userService = inject(UserService);
|
||||
|
||||
getGoodDetailsByHeaderId(headerid: number): Goods | any {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetGoodsDetailstoEdit/${this.userService.getUserSpid()}/${this.userService.getUser()}/${headerid}`).pipe(
|
||||
filter(response => response.length > 0),
|
||||
map(response => this.mapToGoods(response?.[0])));
|
||||
}
|
||||
|
||||
private mapToGoods(goodDetails: any): Goods {
|
||||
return {
|
||||
roadVehiclesUsed: goodDetails.AUTOFLAG === 'Y',
|
||||
horseUsed: goodDetails.HORSEFLAG === 'Y',
|
||||
exhibitionsFair: goodDetails.EXHIBITIONSFAIRFLAG === 'Y',
|
||||
professionalEquipment: goodDetails.PROFEQUIPMENTFLAG === 'Y',
|
||||
commercialSample: goodDetails.COMMERCIALSAMPLEFLAG === 'Y',
|
||||
authorizedRepresentatives: goodDetails.AUTHREP
|
||||
};
|
||||
}
|
||||
|
||||
getGoodsItemsByHeaderId(headerid: number): Observable<GoodsItem[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetGoodsItemstoEdit/${this.userService.getUserSpid()}/${this.userService.getUser()}/${headerid}`).pipe(
|
||||
map(response => this.mapToGoodsItems(response)));
|
||||
}
|
||||
|
||||
private mapToGoodsItems(data: any[]): GoodsItem[] {
|
||||
return data.map(data => ({
|
||||
id: data.ID,
|
||||
itemNumber: data.ITEMNO,
|
||||
description: data.GOODSDESCRIPTION,
|
||||
pieces: data.NOOFPIECES,
|
||||
weight: data.ITEMWEIGHT,
|
||||
unitOfMeasure: data.ITEMWEIGHTUOM,
|
||||
value: data.ITEMVALUE,
|
||||
countryOfOrigin: data.GOODSORIGINCOUNTRY
|
||||
}));
|
||||
}
|
||||
|
||||
saveGoodsData(headerid: number, goodsData: Goods): Observable<any> {
|
||||
|
||||
const goods = {
|
||||
P_HEADERID: headerid,
|
||||
P_COMMERCIALSAMPLEFLAG: goodsData.commercialSample ? 'Y' : 'N',
|
||||
P_PROFEQUIPMENTFLAG: goodsData.professionalEquipment ? 'Y' : 'N',
|
||||
P_EXHIBITIONSFAIRFLAG: goodsData.exhibitionsFair ? 'Y' : 'N',
|
||||
P_AUTOFLAG: goodsData.roadVehiclesUsed ? 'Y' : 'N',
|
||||
P_HORSEFLAG: goodsData.horseUsed ? 'Y' : 'N',
|
||||
P_AUTHREP: goodsData.authorizedRepresentatives,
|
||||
}
|
||||
|
||||
return this.http.patch(`${this.apiUrl}/${this.apiDb}/UpdateExpGoodsAuthRep`, goods);
|
||||
}
|
||||
|
||||
addGoodsItem(headerid: number, items: GoodsItem[]): Observable<any> {
|
||||
|
||||
const goodsItems: any[] = items.map(i => ({
|
||||
ITEMNO: i.itemNumber,
|
||||
ITEMDESCRIPTION: i.description,
|
||||
NOOFPIECES: i.pieces,
|
||||
ITEMWEIGHT: i.weight,
|
||||
ITEMWEIGHTUOM: i.unitOfMeasure,
|
||||
ITEMVALUE: i.value,
|
||||
GOODSORIGINCOUNTRY: i.countryOfOrigin,
|
||||
}));
|
||||
|
||||
const goodsItemsObj = {
|
||||
P_HEADERID: headerid,
|
||||
P_GLTABLE: goodsItems,
|
||||
P_USERID: this.userService.getUser(),
|
||||
};
|
||||
|
||||
return this.http.post(`${this.apiUrl}/${this.apiDb}/AddGenerallistItems`, goodsItemsObj);
|
||||
}
|
||||
|
||||
updateGoodsItem(headerid: number, items: GoodsItem[]): Observable<any> {
|
||||
|
||||
const goodsItems: any[] = items.map(i => ({
|
||||
ITEMNO: i.itemNumber,
|
||||
ITEMDESCRIPTION: i.description,
|
||||
NOOFPIECES: i.pieces,
|
||||
ITEMWEIGHT: i.weight,
|
||||
ITEMWEIGHTUOM: i.unitOfMeasure,
|
||||
ITEMVALUE: i.value,
|
||||
GOODSORIGINCOUNTRY: i.countryOfOrigin,
|
||||
}));
|
||||
|
||||
const goodsItemsObj = {
|
||||
P_HEADERID: headerid,
|
||||
P_GLTABLE: goodsItems,
|
||||
P_USERID: this.userService.getUser(),
|
||||
};
|
||||
|
||||
return this.http.put(`${this.apiUrl}/${this.apiDb}/EditGenerallistItems`, goodsItemsObj);
|
||||
}
|
||||
|
||||
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_USERID: this.userService.getUser(),
|
||||
};
|
||||
|
||||
// delete request with body
|
||||
return this.http.delete(`${this.apiUrl}/${this.apiDb}/DeleteGenerallistItems`, { body: goodsItemsObj });
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class PaymentService {
|
||||
|
||||
constructor() { }
|
||||
}
|
||||
@ -1,9 +1,103 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { environment } from '../../../../environments/environment';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { filter, map, Observable } from 'rxjs';
|
||||
import { UserService } from '../common/user.service';
|
||||
import { Shipping } from '../../models/carnet/shipping';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ShippingService {
|
||||
private apiUrl = environment.apiUrl;
|
||||
private apiDb = environment.apiDb;
|
||||
|
||||
constructor() { }
|
||||
private http = inject(HttpClient);
|
||||
private userService = inject(UserService);
|
||||
|
||||
getShippingData(headerid: number): Shipping | any {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetShipPaymentDetailstoEdit/${this.userService.getUserSpid()}/${this.userService.getUser()}/${headerid}`).pipe(
|
||||
filter(response => response.length > 0),
|
||||
map(response => this.mapToShippingData(response?.[0])));
|
||||
}
|
||||
|
||||
private mapToShippingData(shippingDetails: any): Shipping {
|
||||
|
||||
let shippingData: Shipping = {
|
||||
shipTo: shippingDetails.SHIPADDRTYPE,
|
||||
deliveryType: shippingDetails.DELIVERYTYPE,
|
||||
deliveryMethod: shippingDetails.DELIVERYMETHOD,
|
||||
courierAccount: shippingDetails.CUSTCOURIERNO,
|
||||
paymentMethod: shippingDetails.PAYMENTMETHOD,
|
||||
|
||||
notes: shippingDetails.NOTES,
|
||||
needsBond: shippingDetails.INSPROTECTION === 'Y',
|
||||
needsInsurance: shippingDetails.LDIPROTECTION === 'Y',
|
||||
needsLostDocProtection: shippingDetails.LDIPROTECTION === 'Y'
|
||||
};
|
||||
|
||||
if (shippingDetails.address) {
|
||||
shippingData.address = {
|
||||
address1: shippingDetails.ADDRESS1,
|
||||
address2: shippingDetails.ADDRESS2,
|
||||
city: shippingDetails.CITY,
|
||||
state: shippingDetails.STATE,
|
||||
zip: shippingDetails.ZIP,
|
||||
country: shippingDetails.COUNTRY
|
||||
}
|
||||
}
|
||||
|
||||
if (shippingDetails.contact) {
|
||||
shippingData.contact = {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
return shippingData;
|
||||
}
|
||||
|
||||
saveShippingDetails(headerid: number, shippingData: Shipping): Observable<any> {
|
||||
|
||||
const shippingDetails = {
|
||||
P_HEADERID: headerid,
|
||||
P_SHIPTOTYPE: shippingData.shipTo,
|
||||
P_DELIVERYTYPE: shippingData.deliveryType,
|
||||
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 || '',
|
||||
|
||||
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_USERID: this.userService.getUser()
|
||||
}
|
||||
|
||||
return this.http.patch(`${this.apiUrl}/${this.apiDb}/UpdateShippingDetails`, shippingDetails);
|
||||
}
|
||||
}
|
||||
|
||||
@ -49,24 +49,32 @@ export class AuthService {
|
||||
}
|
||||
}
|
||||
|
||||
register(email: string, password: string, token: string | null): Observable<any> {
|
||||
register(email: string, password: string): Observable<any> {
|
||||
|
||||
return this.http.post(
|
||||
`${this.apiUrl}/register`,
|
||||
{ P_EMAILADDR: email, P_PASSWORD: password }
|
||||
);
|
||||
}
|
||||
|
||||
forgotPassword(email: string, password: string, token: string): Observable<any> {
|
||||
let headers = new HttpHeaders();
|
||||
|
||||
if (token) {
|
||||
headers = headers.set('Authorization', `Bearer ${token}`);
|
||||
}
|
||||
|
||||
return this.http.post(
|
||||
`${this.apiUrl}/register`,
|
||||
return this.http.put(
|
||||
`${this.apiUrl}/forgot-password`,
|
||||
{ P_EMAILADDR: email, P_PASSWORD: password },
|
||||
{ headers }
|
||||
);
|
||||
}
|
||||
|
||||
forgotPassword(email: string, password: string): Observable<any> {
|
||||
sendEmail(email: string, type: string): Observable<any> {
|
||||
return this.http.post(
|
||||
`${this.apiUrl}/forgot-password`,
|
||||
{ P_EMAILADDR: email, P_PASSWORD: password }
|
||||
`${this.apiUrl}/sendmail`,
|
||||
{ P_TO: email, P_MAIL_TYPE: type }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,6 +12,9 @@ import { CargoPolicy } from '../../models/cargo-policy';
|
||||
import { CargoSurety } from '../../models/cargo-surety';
|
||||
import { CarnetStatus } from '../../models/carnet-status';
|
||||
import { Country } from '../../models/country';
|
||||
import { UnitOfMeasure } from '../../models/unitofmeasure';
|
||||
import { DeliveryMethod } from '../../models/delivery-method';
|
||||
import { PaymentType } from '../../models/payment-type';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@ -148,6 +151,42 @@ export class CommonService {
|
||||
);
|
||||
}
|
||||
|
||||
getUnitOfMeasures(_spid: number = 0): Observable<UnitOfMeasure[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=013&P_SPID=0`).pipe(
|
||||
map((response) =>
|
||||
response.map((item) => ({
|
||||
name: item.PARAMDESC,
|
||||
id: item.PARAMID,
|
||||
value: item.PARAMVALUE,
|
||||
}))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
getDeliveryMethods(_spid: number = 0): Observable<DeliveryMethod[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=007&P_SPID=0`).pipe(
|
||||
map((response) =>
|
||||
response.map((item) => ({
|
||||
name: item.PARAMDESC,
|
||||
id: item.PARAMID,
|
||||
value: item.PARAMVALUE,
|
||||
}))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
getPaymentTypes(_spid: number = 0): Observable<PaymentType[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=008&P_SPID=0`).pipe(
|
||||
map((response) =>
|
||||
response.map((item) => ({
|
||||
name: item.PARAMDESC,
|
||||
id: item.PARAMID,
|
||||
value: item.PARAMVALUE,
|
||||
}))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
formatUSDate(datetime: Date): string {
|
||||
const date = new Date(datetime);
|
||||
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
|
||||
|
||||
92
src/app/core/services/holder/contact.service.ts
Normal file
92
src/app/core/services/holder/contact.service.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { UserService } from '../common/user.service';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { environment } from '../../../../environments/environment';
|
||||
import { map, Observable, of } from 'rxjs';
|
||||
import { ContactFormModel, HolderContact } from '../../models/holder/contact';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ContactService {
|
||||
private apiUrl = environment.apiUrl;
|
||||
private apiDb = environment.apiDb;
|
||||
|
||||
constructor(private http: HttpClient, private userService: UserService) { }
|
||||
|
||||
getHolderContactsByHolderId(id: number): Observable<HolderContact[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetHolderContacts/${this.userService.getUserSpid()}/${id}`).pipe(
|
||||
map(response => this.mapToContacts(response)));
|
||||
}
|
||||
|
||||
private mapToContacts(data: any[]): HolderContact[] {
|
||||
return data.map(contact => ({
|
||||
HOLDERCONTACTID: contact.HOLDERCONTACTID,
|
||||
HOLDERID: contact.HOLDERID,
|
||||
SPID: contact.SPID,
|
||||
FIRSTNAME: contact.FIRSTNAME,
|
||||
LASTNAME: contact.LASTNAME,
|
||||
MIDDLEINITIAL: contact.MIDDLEINITIAL || null,
|
||||
TITLE: contact.TITLE,
|
||||
PHONE: contact.PHONE,
|
||||
MOBILE: contact.MOBILE,
|
||||
FAX: contact.FAX || null,
|
||||
EMAILADDRESS: contact.EMAILADDRESS,
|
||||
DATECREATED: contact.DATECREATED || null,
|
||||
CREATEDBY: contact.CREATEDBY || null,
|
||||
INACTIVEFLAG: contact.INACTIVEFLAG || 'N',
|
||||
INACTIVEDATE: contact.INACTIVEDATE || null,
|
||||
LASTUPDATEDBY: contact.LASTUPDATEDBY || null,
|
||||
LASTUPDATEDDATE: contact.LASTUPDATEDDATE || null
|
||||
}));
|
||||
}
|
||||
|
||||
createHolderContact(HOLDERID: number, data: ContactFormModel): Observable<any> {
|
||||
const contact = {
|
||||
P_SPID: this.userService.getUserSpid(),
|
||||
P_HOLDERID: HOLDERID,
|
||||
P_CONTACTSTABLE: [{
|
||||
P_FIRSTNAME: data.firstName,
|
||||
P_LASTNAME: data.lastName,
|
||||
P_MIDDLEINITIAL: data.middleInitial,
|
||||
P_TITLE: data.title,
|
||||
P_EMAILADDRESS: data.email,
|
||||
P_MOBILENO: data.mobile,
|
||||
P_PHONENO: data.phone,
|
||||
P_FAXNO: data.fax
|
||||
}],
|
||||
P_USERID: this.userService.getUser()
|
||||
}
|
||||
|
||||
return this.http.post(`${this.apiUrl}/${this.apiDb}/CreateHoldercontact`, contact);
|
||||
}
|
||||
|
||||
updateHolderContact(P_HOLDERCONTACTID: number, data: ContactFormModel): Observable<any> {
|
||||
const contact = {
|
||||
P_HOLDERCONTACTID,
|
||||
P_SPID: this.userService.getUserSpid(),
|
||||
P_FIRSTNAME: data.firstName,
|
||||
P_LASTNAME: data.lastName,
|
||||
P_MIDDLEINITIAL: data.middleInitial,
|
||||
P_TITLE: data.title,
|
||||
P_EMAILADDRESS: data.email,
|
||||
P_MOBILENO: data.mobile,
|
||||
P_PHONENO: data.phone,
|
||||
P_FAXNO: data.fax,
|
||||
P_USERID: this.userService.getUser()
|
||||
}
|
||||
|
||||
console.log("update body : ", contact);
|
||||
|
||||
|
||||
return this.http.put(`${this.apiUrl}/${this.apiDb}/UpdateHolderContact`, contact);
|
||||
}
|
||||
|
||||
InactivateHolderContact(HOLDERCONTACTID: number) {
|
||||
return this.http.patch(`${this.apiUrl}/${this.apiDb}/InactivateHolderContact/${this.userService.getUserSpid()}/${HOLDERCONTACTID}/${this.userService.getSafeUser()}`, {});
|
||||
}
|
||||
|
||||
ReactivateHolderContact(HOLDERCONTACTID: number) {
|
||||
return this.http.patch(`${this.apiUrl}/${this.apiDb}/ReactivateHolderContact/${this.userService.getUserSpid()}/${HOLDERCONTACTID}/${this.userService.getSafeUser()}`, {});
|
||||
}
|
||||
}
|
||||
101
src/app/core/services/holder/holder-detail.service.ts
Normal file
101
src/app/core/services/holder/holder-detail.service.ts
Normal file
@ -0,0 +1,101 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { UserService } from '../common/user.service';
|
||||
import { environment } from '../../../../environments/environment';
|
||||
import { HolderDetail, HolderDetailFormType } from '../../models/holder/holder-details';
|
||||
import { filter, map, Observable, of, tap } from 'rxjs';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class HolderDetailService {
|
||||
private apiUrl = environment.apiUrl;
|
||||
private apiDb = environment.apiDb;
|
||||
|
||||
constructor(private http: HttpClient, private userService: UserService) { }
|
||||
|
||||
getHolderDetailByHolderId(id: number): Observable<HolderDetail> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetHolderRecord/${this.userService.getUserSpid()}/${id}`).pipe(
|
||||
tap(response => console.log('Raw response got is :', response)),
|
||||
// filter(response => response.length > 0),
|
||||
map(response => this.mapToBasicDetail(response)));
|
||||
}
|
||||
|
||||
private mapToBasicDetail(basicDetails: any): HolderDetail {
|
||||
return {
|
||||
HOLDERID: basicDetails.HOLDERID,
|
||||
SPID: basicDetails.SPID,
|
||||
LOCATIONID: basicDetails.LOCATIONID,
|
||||
HOLDERNO: basicDetails.HOLDERNO,
|
||||
HOLDERTYPE: basicDetails.HOLDERTYPE,
|
||||
USCIBMEMBERFLAG: basicDetails.USCIBMEMBERFLAG,
|
||||
GOVAGENCYFLAG: basicDetails.GOVAGENCYFLAG,
|
||||
HOLDERNAME: basicDetails.HOLDERNAME,
|
||||
NAMEQUALIFIER: basicDetails.NAMEQUALIFIER || null,
|
||||
ADDLNAME: basicDetails.ADDLNAME || null,
|
||||
ADDRESS1: basicDetails.ADDRESS1,
|
||||
ADDRESS2: basicDetails.ADDRESS2 || null,
|
||||
CITY: basicDetails.CITY,
|
||||
STATE: basicDetails.STATE,
|
||||
ZIP: basicDetails.ZIP,
|
||||
COUNTRY: basicDetails.COUNTRY,
|
||||
DATECREATED: basicDetails.DATECREATED,
|
||||
CREATEDBY: basicDetails.CREATEDBY,
|
||||
INACTIVEFLAG: basicDetails.INACTIVEFLAG,
|
||||
INACTIVEDATE: basicDetails.INACTIVEDATE || null,
|
||||
LASTUPDATEDBY: basicDetails.LASTUPDATEDBY || null,
|
||||
LASTUPDATEDDATE: basicDetails.LASTUPDATEDDATE || null
|
||||
};
|
||||
}
|
||||
|
||||
createHolderDetail(data: HolderDetailFormType): Observable<any> {
|
||||
const holderDetail = {
|
||||
P_SPID: this.userService.getUserSpid(),
|
||||
P_CLIENTLOCATIONID: this.userService.getUserLocationid(),
|
||||
P_HOLDERNO: data.HolderNumber,
|
||||
P_HOLDERTYPE: data.holderType,
|
||||
P_USCIBMEMBERFLAG: data.USCIBMember,
|
||||
P_GOVAGENCYFLAG: data.govAgency,
|
||||
P_HOLDERNAME: data.holderName,
|
||||
P_NAMEQUALIFIER: null,
|
||||
P_ADDLNAME: data.DBAName,
|
||||
P_ADDRESS1: data.address1,
|
||||
P_ADDRESS2: data.address2,
|
||||
P_CITY: data.city,
|
||||
P_STATE: data.state,
|
||||
P_ZIP: data.zip,
|
||||
P_COUNTRY: data.country,
|
||||
P_USERID: this.userService.getUser()
|
||||
};
|
||||
|
||||
console.log("body : ",holderDetail);
|
||||
|
||||
|
||||
|
||||
return this.http.post(`${this.apiUrl}/${this.apiDb}/CreateHolderData`, holderDetail);
|
||||
}
|
||||
|
||||
updateHolderDetails(id: number, data: HolderDetailFormType): Observable<any> {
|
||||
const holderDetail = {
|
||||
P_HOLDERID: id,
|
||||
P_SPID: this.userService.getUserSpid(),
|
||||
P_LOCATIONID: this.userService.getUserLocationid(),
|
||||
P_HOLDERNO: data.HolderNumber,
|
||||
P_HOLDERTYPE: data.holderType,
|
||||
P_USCIBMEMBERFLAG: data.USCIBMember,
|
||||
P_GOVAGENCYFLAG: data.govAgency,
|
||||
P_HOLDERNAME: data.holderName,
|
||||
P_NAMEQUALIFIER: null,
|
||||
P_ADDLNAME: data.DBAName,
|
||||
P_ADDRESS1: data.address1,
|
||||
P_ADDRESS2: data.address2,
|
||||
P_CITY: data.city,
|
||||
P_STATE: data.state,
|
||||
P_ZIP: data.zip,
|
||||
P_COUNTRY: data.country,
|
||||
P_USERID: this.userService.getUser()
|
||||
};
|
||||
|
||||
return this.http.put(`${this.apiUrl}/${this.apiDb}/UpdateHolder`, holderDetail);
|
||||
}
|
||||
}
|
||||
121
src/app/core/services/holder/holder.service.ts
Normal file
121
src/app/core/services/holder/holder.service.ts
Normal file
@ -0,0 +1,121 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { environment } from '../../../../environments/environment';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { UserService } from '../common/user.service';
|
||||
// import { BasicDetail } from '../../models/preparer/basic-detail';
|
||||
import { map, Observable } from 'rxjs';
|
||||
import { HolderDetail } from '../../models/holder/holder-details';
|
||||
// import { PreparerFilter } from '../../models/preparer/preparer-filter';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class HolderService {
|
||||
private apiUrl = environment.apiUrl;
|
||||
private apiDb = environment.apiDb;
|
||||
|
||||
private http = inject(HttpClient);
|
||||
private userService = inject(UserService);
|
||||
|
||||
searchHolders(filter: { holderName: string }): Observable<HolderDetail[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/SearchHolder/${this.userService.getUserSpid()}/${filter.holderName}`).pipe(
|
||||
map(response => this.mapToHolders(response))
|
||||
)
|
||||
}
|
||||
|
||||
updateApplicationHolder(headerId: number, holderId: number) {
|
||||
const applicationDetail = {
|
||||
P_HEADERID: headerId,
|
||||
P_HOLDERID: holderId
|
||||
}
|
||||
|
||||
return this.http.patch(`${this.apiUrl}/${this.apiDb}/update-holder`, applicationDetail);
|
||||
}
|
||||
|
||||
InactivateHolder(HOLDERID: number) {
|
||||
return this.http.patch(`${this.apiUrl}/${this.apiDb}/InactivateHolder/${this.userService.getUserSpid()}/${HOLDERID}/${this.userService.getSafeUser()}`, {});
|
||||
}
|
||||
|
||||
ReactivateHolder(HOLDERID: number) {
|
||||
return this.http.patch(`${this.apiUrl}/${this.apiDb}/ReactivateHolder/${this.userService.getUserSpid()}/${HOLDERID}/${this.userService.getSafeUser()}`, {});
|
||||
}
|
||||
|
||||
|
||||
// getPreparers(filter: PreparerFilter): Observable<BasicDetail[]> {
|
||||
// return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetPreparers/${this.userService.getUserSpid()}/ACTIVE?P_NAME=${filter.name}&P_LOOKUPCODE=${filter.lookupCode}&P_CITY=${filter.city}&P_STATE=${filter.state}`).pipe(
|
||||
// map(response => this.mapToClients(response)));
|
||||
// }
|
||||
|
||||
private mapToHolders(data: any[]): HolderDetail[] {
|
||||
return data.map((holderDetail: {
|
||||
HOLDERID: number;
|
||||
SPID: number;
|
||||
LOCATIONID: number;
|
||||
HOLDERNO: string;
|
||||
HOLDERTYPE: string;
|
||||
USCIBMEMBERFLAG: string;
|
||||
GOVAGENCYFLAG: string;
|
||||
HOLDERNAME: string;
|
||||
NAMEQUALIFIER: string | null;
|
||||
ADDLNAME: string | null;
|
||||
ADDRESS1: string;
|
||||
ADDRESS2: string;
|
||||
CITY: string;
|
||||
STATE: string;
|
||||
ZIP: string;
|
||||
COUNTRY: string;
|
||||
DATECREATED: string;
|
||||
CREATEDBY: string;
|
||||
INACTIVEFLAG: string;
|
||||
INACTIVEDATE: string | null;
|
||||
LASTUPDATEDBY: string | null;
|
||||
LASTUPDATEDDATE: string | null;
|
||||
}) => ({
|
||||
HOLDERID: holderDetail.HOLDERID,
|
||||
SPID: holderDetail.SPID,
|
||||
LOCATIONID: holderDetail.LOCATIONID,
|
||||
HOLDERNO: holderDetail.HOLDERNO,
|
||||
HOLDERTYPE: holderDetail.HOLDERTYPE,
|
||||
USCIBMEMBERFLAG: holderDetail.USCIBMEMBERFLAG,
|
||||
GOVAGENCYFLAG: holderDetail.GOVAGENCYFLAG,
|
||||
HOLDERNAME: holderDetail.HOLDERNAME,
|
||||
NAMEQUALIFIER: holderDetail.NAMEQUALIFIER,
|
||||
ADDLNAME: holderDetail.ADDLNAME,
|
||||
ADDRESS1: holderDetail.ADDRESS1,
|
||||
ADDRESS2: holderDetail.ADDRESS2,
|
||||
CITY: holderDetail.CITY,
|
||||
STATE: holderDetail.STATE,
|
||||
ZIP: holderDetail.ZIP,
|
||||
COUNTRY: holderDetail.COUNTRY,
|
||||
DATECREATED: holderDetail.DATECREATED,
|
||||
CREATEDBY: holderDetail.CREATEDBY,
|
||||
INACTIVEFLAG: holderDetail.INACTIVEFLAG,
|
||||
INACTIVEDATE: holderDetail.INACTIVEDATE,
|
||||
LASTUPDATEDBY: holderDetail.LASTUPDATEDBY,
|
||||
LASTUPDATEDDATE: holderDetail.LASTUPDATEDDATE
|
||||
}))
|
||||
}
|
||||
|
||||
// private mapToClients(data: any[]): BasicDetail[] {
|
||||
// return data.map(basicDetails => ({
|
||||
// clientid: basicDetails.CLIENTID,
|
||||
// spid: basicDetails.SPID,
|
||||
// name: basicDetails.PREPARERNAME,
|
||||
// lookupCode: basicDetails.LOOKUPCODE,
|
||||
// address1: basicDetails.ADDRESS1,
|
||||
// address2: basicDetails.ADDRESS2,
|
||||
// city: basicDetails.CITY,
|
||||
// state: basicDetails.STATE,
|
||||
// country: basicDetails.COUNTRY,
|
||||
// carnetIssuingRegion: basicDetails.ISSUINGREGION,
|
||||
// revenueLocation: basicDetails.REVENUELOCATION,
|
||||
// zip: basicDetails.ZIP,
|
||||
// // createdBy: basicDetails.CREATEDBY || null,
|
||||
// // dateCreated: basicDetails.DATECREATED || null,
|
||||
// // lastUpdatedBy: basicDetails.LASTUPDATEDBY || null,
|
||||
// // lastUpdatedDate: basicDetails.LASTUPDATEDDATE || null,
|
||||
// // isInactive: basicDetails.INACTIVEFLAG === 'Y' || false,
|
||||
// // inactivatedDate: basicDetails.INACTIVEDATE || null
|
||||
// }));
|
||||
// }
|
||||
}
|
||||
@ -1,8 +1,36 @@
|
||||
<div class="forgot-password-container">
|
||||
<div class="forgot-password-card">
|
||||
<h3 class="subtitle">Forgot Password</h3>
|
||||
<h3 class="subtitle">
|
||||
{{forgotPasswordRequestCompleted ? 'Reset Password' : 'Forgot Password'}}
|
||||
</h3>
|
||||
|
||||
<form [formGroup]="forgotPasswordForm" (ngSubmit)="onSubmit()" class="forgot-password-form">
|
||||
<form *ngIf="!forgotPasswordRequestCompleted" [formGroup]="forgotPasswordRequestForm" (ngSubmit)="onRequest()"
|
||||
class="forgot-password-form">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Email</mat-label>
|
||||
<input matInput formControlName="email" type="email" required>
|
||||
<mat-icon matSuffix>email</mat-icon>
|
||||
<mat-error *ngIf="email?.errors?.['required']">
|
||||
Email is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="email?.errors?.['email']">
|
||||
Please enter a valid email
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<div class="login">
|
||||
<a href="#" routerLink="/login" class="login-link">Already have an account? Login</a>
|
||||
</div>
|
||||
|
||||
<button mat-raised-button color="primary" type="submit"
|
||||
[disabled]="forgotPasswordRequestForm.invalid || isLoading">
|
||||
<span *ngIf="!isLoading">Request</span>
|
||||
<mat-spinner *ngIf="isLoading" diameter="20"></mat-spinner>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form *ngIf="forgotPasswordRequestCompleted" [formGroup]="forgotPasswordForm" (ngSubmit)="onSubmit()"
|
||||
class="forgot-password-form">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Email</mat-label>
|
||||
<input matInput formControlName="email" type="email" required>
|
||||
@ -24,11 +52,32 @@
|
||||
<mat-error *ngIf="password?.errors?.['required']">
|
||||
Password is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="password?.errors?.['minlength']">
|
||||
Password must be at least 8 characters
|
||||
</mat-error>
|
||||
<mat-error *ngIf="password?.errors?.['pattern']">
|
||||
Password must contain at least one uppercase letter, one lowercase letter, one number, and one
|
||||
special character
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Confirm Password</mat-label>
|
||||
<input matInput formControlName="confirmPassword" [type]="hidePassword ? 'password' : 'text'" required>
|
||||
<button type="button" mat-icon-button matSuffix (click)="hidePassword = !hidePassword">
|
||||
<mat-icon>{{ hidePassword ? 'visibility_off' : 'visibility' }}</mat-icon>
|
||||
</button>
|
||||
<mat-error *ngIf="confirmPassword?.errors?.['required']">
|
||||
Please confirm your password
|
||||
</mat-error>
|
||||
<mat-error *ngIf="forgotPasswordForm.errors?.['mismatch'] && confirmPassword?.touched">
|
||||
Passwords do not match
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<!--
|
||||
<div class="login">
|
||||
<a href="#" routerLink="/login" class="login-link">Already have an account? Login</a>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<button mat-raised-button color="primary" type="submit"
|
||||
[disabled]="forgotPasswordForm.invalid || isLoading">
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { FormGroup, FormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ApiErrorHandlerService } from '../core/services/common/api-error-handler.service';
|
||||
import { AuthService } from '../core/services/common/auth.service';
|
||||
import { NotificationService } from '../core/services/common/notification.service';
|
||||
@ -15,10 +15,12 @@ import { CommonModule } from '@angular/common';
|
||||
styleUrl: './forgot-password.component.scss'
|
||||
})
|
||||
export class ForgotPasswordComponent {
|
||||
|
||||
forgotPasswordRequestCompleted: boolean = false;
|
||||
forgotPasswordRequestForm: FormGroup;
|
||||
forgotPasswordForm: FormGroup;
|
||||
isLoading = false;
|
||||
hidePassword = true;
|
||||
token: string | null = null;
|
||||
|
||||
private fb = inject(FormBuilder);
|
||||
private authService = inject(AuthService);
|
||||
@ -26,18 +28,57 @@ export class ForgotPasswordComponent {
|
||||
private notificationService = inject(NotificationService);
|
||||
private errorHandler = inject(ApiErrorHandlerService);
|
||||
private themeService = inject(ThemeService);
|
||||
private route = inject(ActivatedRoute);
|
||||
|
||||
constructor() {
|
||||
this.forgotPasswordRequestForm = this.fb.group({
|
||||
email: ['', [Validators.required, Validators.email]]
|
||||
});
|
||||
|
||||
this.forgotPasswordForm = this.fb.group({
|
||||
email: ['', [Validators.required, Validators.email]],
|
||||
password: ['', [Validators.required]]
|
||||
});
|
||||
password: ['', [Validators.required, Validators.minLength(8),
|
||||
Validators.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]+$/)
|
||||
]],
|
||||
confirmPassword: ['', [Validators.required]]
|
||||
}, { validator: this.passwordMatchValidator });
|
||||
|
||||
// // Get token from URL if present
|
||||
this.token = this.route.snapshot.paramMap.get('token');
|
||||
|
||||
// Check if token is null or empty
|
||||
if (this.token && this.token.trim() !== '') {
|
||||
this.forgotPasswordRequestCompleted = true;
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.themeService.setTheme('default');
|
||||
}
|
||||
|
||||
onRequest(): void {
|
||||
if (this.forgotPasswordRequestForm.invalid) {
|
||||
this.forgotPasswordRequestForm.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
|
||||
this.isLoading = true;
|
||||
const { email } = this.forgotPasswordRequestForm.value;
|
||||
|
||||
this.authService.sendEmail(email, 'FORGOT_PASSWORD').subscribe({
|
||||
next: () => {
|
||||
this.notificationService.showSuccess('A mail has been sent successfully to your email to setup a new password.');
|
||||
this.isLoading = false;
|
||||
},
|
||||
error: (error) => {
|
||||
this.isLoading = false;
|
||||
let errorMessage = this.errorHandler.handleApiError(error, `Request failed. Please try again.`);
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Forgot password request failed:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onSubmit(): void {
|
||||
if (this.forgotPasswordForm.invalid) {
|
||||
this.forgotPasswordForm.markAllAsTouched();
|
||||
@ -47,20 +88,30 @@ export class ForgotPasswordComponent {
|
||||
this.isLoading = true;
|
||||
const { email, password } = this.forgotPasswordForm.value;
|
||||
|
||||
this.authService.forgotPassword(email, password).subscribe({
|
||||
this.authService.forgotPassword(email, password, this.token!).subscribe({
|
||||
next: () => {
|
||||
this.notificationService.showSuccess('Request successful');
|
||||
this.notificationService.showSuccess('Password reset successfully');
|
||||
this.router.navigate(['/login']);
|
||||
},
|
||||
error: (error) => {
|
||||
this.isLoading = false;
|
||||
let errorMessage = this.errorHandler.handleApiError(error, `Request failed. Please try again.`);
|
||||
let errorMessage = this.errorHandler.handleApiError(error, `Password request failed. Please try again.`);
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Forgot password request failed:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private passwordMatchValidator(formGroup: FormGroup) {
|
||||
const password = formGroup.get('password')?.value;
|
||||
const confirmPassword = formGroup.get('confirmPassword')?.value;
|
||||
return password === confirmPassword ? null : { mismatch: true };
|
||||
}
|
||||
|
||||
get emailRequest() {
|
||||
return this.forgotPasswordRequestForm.get('email');
|
||||
}
|
||||
|
||||
get email() {
|
||||
return this.forgotPasswordForm.get('email');
|
||||
}
|
||||
@ -68,4 +119,8 @@ export class ForgotPasswordComponent {
|
||||
get password() {
|
||||
return this.forgotPasswordForm.get('password');
|
||||
}
|
||||
|
||||
get confirmPassword() {
|
||||
return this.forgotPasswordForm.get('confirmPassword');
|
||||
}
|
||||
}
|
||||
|
||||
26
src/app/holder/add/add.component.html
Normal file
26
src/app/holder/add/add.component.html
Normal file
@ -0,0 +1,26 @@
|
||||
<button mat-stroked-button color="accent" class="back-btn" (click)="goToEditCarnet()" [hidden]="!isEditMode">Back</button>
|
||||
|
||||
<div class="client-carnet-container">
|
||||
<!-- Stepper Section (shown after questions are answered) -->
|
||||
<mat-stepper orientation="vertical" [linear]="isLinear" (selectionChange)="onStepChange($event)"
|
||||
[selectedIndex]="currentStep">
|
||||
|
||||
<!-- Holder Detail Step -->
|
||||
<mat-step *ngIf="applicationType === 'new'" [completed]="stepsCompleted.holderDetails"
|
||||
[editable]="stepsCompleted.holderDetails">
|
||||
<ng-template matStepLabel>Holder Details</ng-template>
|
||||
<!-- <app-application [isEditMode]="isEditMode" (headerIdCreated)="onApplicationDetailCreated($event)">
|
||||
</app-application> -->
|
||||
<app-holder-detail [isEditMode]="isEditMode" (holderIdCreated)="onHolderIdCreated($event)"></app-holder-detail>
|
||||
</mat-step>
|
||||
|
||||
<!-- Holder Contact Step -->
|
||||
<mat-step *ngIf="applicationType === 'new'" [completed]="stepsCompleted.holderContacts"
|
||||
[editable]="stepsCompleted.holderContacts">
|
||||
<ng-template matStepLabel>Holder Contacts</ng-template>
|
||||
<!-- <app-application [isEditMode]="isEditMode" (headerIdCreated)="onApplicationDetailCreated($event)">
|
||||
</app-application> -->
|
||||
<app-holder-contacts [holderid]="holderid" [isEditMode]="isEditMode" [userPreferences]="userPreferences"></app-holder-contacts>
|
||||
</mat-step>
|
||||
</mat-stepper>
|
||||
</div>
|
||||
4
src/app/holder/add/add.component.scss
Normal file
4
src/app/holder/add/add.component.scss
Normal file
@ -0,0 +1,4 @@
|
||||
.back-btn{
|
||||
padding: 0 10px !important;
|
||||
margin-bottom: 10px !important;
|
||||
}
|
||||
67
src/app/holder/add/add.component.ts
Normal file
67
src/app/holder/add/add.component.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { StepperSelectionEvent } from '@angular/cdk/stepper';
|
||||
import { HolderDetailComponent } from '../holder-detail/holder-detail.component';
|
||||
import { MatStepperModule } from '@angular/material/stepper';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { HolderContactsComponent } from '../holder-contacts/holder-contacts.component';
|
||||
import { UserPreferences } from '../../core/models/user-preference';
|
||||
import { UserPreferencesService } from '../../core/services/user-preference.service';
|
||||
import { CarnetStateStore } from '../../core/services/carnet/carnet-application.service';
|
||||
import { NavigationService } from '../../core/services/common/navigation.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-add',
|
||||
imports: [
|
||||
AngularMaterialModule,
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
HolderDetailComponent,
|
||||
MatStepperModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
HolderContactsComponent
|
||||
],
|
||||
templateUrl: './add.component.html',
|
||||
styleUrl: './add.component.scss'
|
||||
})
|
||||
export class AddComponent {
|
||||
store = inject(CarnetStateStore);
|
||||
currentStep = 0;
|
||||
isLinear = true;
|
||||
applicationType: 'new' | 'additional' | 'duplicate' | 'extend' | null = 'new';
|
||||
isEditMode = false;
|
||||
holderid: number = 0;
|
||||
userPreferences: UserPreferences;
|
||||
stepsCompleted = {
|
||||
holderDetails: false,
|
||||
holderContacts: false
|
||||
};
|
||||
constructor(
|
||||
private userPrefenceService: UserPreferencesService,
|
||||
private navigationService: NavigationService,
|
||||
) {
|
||||
this.userPreferences = this.userPrefenceService.getPreferences();
|
||||
}
|
||||
|
||||
onStepChange(event: StepperSelectionEvent): void {
|
||||
this.currentStep = event.selectedIndex;
|
||||
}
|
||||
|
||||
onHolderIdCreated(holderid: number): void {
|
||||
this.holderid = holderid;
|
||||
this.stepsCompleted.holderDetails = true;
|
||||
}
|
||||
|
||||
goToEditCarnet(): void {
|
||||
this.navigationService.navigate(["edit-carnet", this.store.headerid()],
|
||||
{
|
||||
state: { isEditMode: true } ,
|
||||
queryParams: { applicationname: this.store.applicationName() }
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
24
src/app/holder/edit/edit.component.html
Normal file
24
src/app/holder/edit/edit.component.html
Normal file
@ -0,0 +1,24 @@
|
||||
<h2 *ngIf="this.holderName" class="page-header">Manage {{this.holderName}}</h2>
|
||||
|
||||
<div class="preparer-action-buttons">
|
||||
<button mat-button (click)="accordion().openAll()">Expand All</button>
|
||||
<button mat-button (click)="accordion().closeAll()">Collapse All</button>
|
||||
</div>
|
||||
<mat-accordion class="preparer-headers-align" multi>
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title> Basic Details </mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
<app-holder-detail [holderid]="holderid" [isEditMode]="isEditMode"
|
||||
(clientName)="onClientNameUpdate($event)"></app-holder-detail>
|
||||
</mat-expansion-panel>
|
||||
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title> holder Contacts </mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
<app-holder-contacts [userPreferences]="userPreferences" [holderid]="holderid" [isEditMode]="isEditMode" ></app-holder-contacts>
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
||||
18
src/app/holder/edit/edit.component.scss
Normal file
18
src/app/holder/edit/edit.component.scss
Normal file
@ -0,0 +1,18 @@
|
||||
.page-header {
|
||||
margin: 0.5rem 0px;
|
||||
color: var(--mat-sys-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.preparer-action-buttons {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.preparer-headers-align .mat-expansion-panel-header-description {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.preparer-headers-align .mat-mdc-form-field+.mat-mdc-form-field {
|
||||
margin-left: 8px;
|
||||
}
|
||||
39
src/app/holder/edit/edit.component.ts
Normal file
39
src/app/holder/edit/edit.component.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { afterNextRender, Component, viewChild } from '@angular/core';
|
||||
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
|
||||
import { MatAccordion } from '@angular/material/expansion';
|
||||
import { UserPreferences } from '../../core/models/user-preference';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { UserPreferencesService } from '../../core/services/user-preference.service';
|
||||
import { HolderDetailComponent } from '../holder-detail/holder-detail.component';
|
||||
import { HolderContactsComponent } from '../holder-contacts/holder-contacts.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-edit',
|
||||
imports: [AngularMaterialModule, CommonModule, HolderDetailComponent, HolderContactsComponent],
|
||||
templateUrl: './edit.component.html',
|
||||
styleUrl: './edit.component.scss'
|
||||
})
|
||||
export class EditComponent {
|
||||
accordion = viewChild.required(MatAccordion);
|
||||
isEditMode = true;
|
||||
holderid = 0;
|
||||
holderName: string | null = null;
|
||||
userPreferences: UserPreferences;
|
||||
|
||||
constructor(private route: ActivatedRoute, private userPrefenceService: UserPreferencesService) {
|
||||
this.userPreferences = userPrefenceService.getPreferences();
|
||||
afterNextRender(() => {
|
||||
this.accordion().openAll();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
const idParam = this.route.snapshot.paramMap.get('holderid');
|
||||
this.holderid = idParam ? parseInt(idParam, 10) : 0;
|
||||
}
|
||||
|
||||
onClientNameUpdate(event: string): void {
|
||||
this.holderName = event;
|
||||
}
|
||||
}
|
||||
264
src/app/holder/holder-contacts/holder-contacts.component.html
Normal file
264
src/app/holder/holder-contacts/holder-contacts.component.html
Normal file
@ -0,0 +1,264 @@
|
||||
<div class="contacts-container">
|
||||
<div class="actions-bar">
|
||||
<mat-slide-toggle (change)="toggleShowInactiveContacts()">
|
||||
Show Inactive Contacts
|
||||
</mat-slide-toggle>
|
||||
|
||||
<button mat-raised-button color="primary" (click)="addNewContact()">
|
||||
<mat-icon>add</mat-icon> Add New Contact
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="table-container mat-elevation-z8">
|
||||
<div class="loading-shade" *ngIf="isLoading">
|
||||
<mat-spinner diameter="50"></mat-spinner>
|
||||
</div>
|
||||
|
||||
<table mat-table [dataSource]="dataSource" matSort>
|
||||
<!-- First Name Column -->
|
||||
<ng-container matColumnDef="firstName">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>First Name</th>
|
||||
<td mat-cell *matCellDef="let contact">{{ contact.FIRSTNAME }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Last Name Column -->
|
||||
<ng-container matColumnDef="lastName">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Last Name</th>
|
||||
<td mat-cell *matCellDef="let contact">{{ contact.LASTNAME }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Title Column -->
|
||||
<ng-container matColumnDef="title">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Title</th>
|
||||
<td mat-cell *matCellDef="let contact">{{ contact.TITLE }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Phone Column -->
|
||||
<ng-container matColumnDef="phone">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Phone</th>
|
||||
<td mat-cell *matCellDef="let contact">{{ contact.PHONE | phone }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Mobile Column -->
|
||||
<ng-container matColumnDef="mobile">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Mobile</th>
|
||||
<td mat-cell *matCellDef="let contact">{{ contact.MOBILE | phone }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Email Column -->
|
||||
<ng-container matColumnDef="email">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Email</th>
|
||||
<td mat-cell *matCellDef="let contact">{{ contact.EMAILADDRESS }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Default Contact Column -->
|
||||
<!-- <ng-container matColumnDef="defaultContact">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Default</th>
|
||||
<td mat-cell *matCellDef="let contact">
|
||||
<mat-icon [color]="contact.defaultContact ? 'primary' : ''">
|
||||
{{ contact.defaultContact ? 'star' : 'star_border' }}
|
||||
</mat-icon>
|
||||
</td>
|
||||
</ng-container> -->
|
||||
|
||||
<!-- Actions Column -->
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef>Actions</th>
|
||||
<td mat-cell *matCellDef="let contact">
|
||||
<div style="display: flex; justify-content: center; align-items: center;">
|
||||
<button mat-icon-button color="primary" (click)="editContact(contact)" matTooltip="Edit">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<!-- <button mat-icon-button color="primary" (click)="createLogin()" matTooltip="Login">
|
||||
<mat-icon>person</mat-icon>
|
||||
</button> -->
|
||||
<button mat-icon-button color="warn" *ngIf="!contact.isInactive"
|
||||
(click)="InactivateHolderContact(contact.HOLDERCONTACTID)" matTooltip="Inactivate">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="warn" *ngIf="contact.isInactive"
|
||||
(click)="ReactivateHolderContact(contact.HOLDERCONTACTID)" matTooltip="Reactivate">
|
||||
<mat-icon>delete_outline</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<!-- <button mat-icon-button (click)="setDefaultContact(contact.contactId)"
|
||||
[color]="contact.defaultContact ? 'primary' : ''" matTooltip="Set as default">
|
||||
<mat-icon>star</mat-icon>
|
||||
</button> -->
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
|
||||
<tr matNoDataRow *matNoDataRow>
|
||||
<td [colSpan]="displayedColumns.length" class="no-data-message">
|
||||
<mat-icon>info</mat-icon>
|
||||
<span>No records available</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<mat-paginator [length]="dataSource.data.length" [pageSizeOptions]="[userPreferences.pageSize || 1]"
|
||||
[hidePageSize]="true" showFirstLastButtons></mat-paginator>
|
||||
</div>
|
||||
|
||||
<!-- Contact Form -->
|
||||
<div class="form-container" *ngIf="showForm">
|
||||
<form [formGroup]="contactForm" (ngSubmit)="saveContact()">
|
||||
<div class="form-header">
|
||||
<h3>{{ isEditing ? 'Edit Contact' : 'Add New Contact' }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>First Name</mat-label>
|
||||
<input matInput formControlName="firstName" required>
|
||||
<mat-icon matSuffix>person</mat-icon>
|
||||
<mat-error *ngIf="contactForm.get('firstName')?.errors?.['required']">
|
||||
First name is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="contactForm.get('firstName')?.errors?.['maxlength']">
|
||||
Maximum 50 characters allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="small-field">
|
||||
<mat-label>Middle Initial</mat-label>
|
||||
<input matInput formControlName="middleInitial" maxlength="1">
|
||||
<mat-error *ngIf="contactForm.get('middleInitial')?.errors?.['maxlength']">
|
||||
Only 1 character allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Last Name</mat-label>
|
||||
<input matInput formControlName="lastName" required>
|
||||
<mat-icon matSuffix>person</mat-icon>
|
||||
<mat-error *ngIf="contactForm.get('lastName')?.errors?.['required']">
|
||||
Last name is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="contactForm.get('lastName')?.errors?.['maxlength']">
|
||||
Maximum 50 characters allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Title</mat-label>
|
||||
<input matInput formControlName="title" required>
|
||||
<mat-icon matSuffix>work</mat-icon>
|
||||
<mat-error *ngIf="contactForm.get('title')?.errors?.['required']">
|
||||
Title is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="contactForm.get('title')?.errors?.['maxlength']">
|
||||
Maximum 100 characters allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Phone</mat-label>
|
||||
<input matInput formControlName="phone" required>
|
||||
<mat-icon matSuffix>phone</mat-icon>
|
||||
<mat-error *ngIf="contactForm.get('phone')?.errors?.['required']">
|
||||
Phone is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="contactForm.get('phone')?.errors?.['pattern']">
|
||||
Please enter a valid phone number (10-15 digits)
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Mobile</mat-label>
|
||||
<input matInput formControlName="mobile">
|
||||
<mat-icon matSuffix>smartphone</mat-icon>
|
||||
<mat-error *ngIf="contactForm.get('mobile')?.errors?.['required']">
|
||||
Mobile is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="contactForm.get('mobile')?.errors?.['pattern']">
|
||||
Please enter a valid mobile number (10-15 digits)
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Fax</mat-label>
|
||||
<input matInput formControlName="fax">
|
||||
<mat-icon matSuffix>fax</mat-icon>
|
||||
<mat-error *ngIf="contactForm.get('fax')?.errors?.['pattern']">
|
||||
Please enter a valid fax number (10-15 digits)
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Email</mat-label>
|
||||
<input matInput formControlName="email" required>
|
||||
<mat-icon matSuffix>email</mat-icon>
|
||||
<mat-error *ngIf="contactForm.get('email')?.errors?.['required']">
|
||||
Email is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="contactForm.get('email')?.errors?.['email']">
|
||||
Please enter a valid email address
|
||||
</mat-error>
|
||||
<mat-error *ngIf="contactForm.get('email')?.errors?.['maxlength']">
|
||||
Maximum 100 characters allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<!--
|
||||
<div class="form-row">
|
||||
<mat-checkbox formControlName="defaultContact">Default Contact</mat-checkbox>
|
||||
</div> -->
|
||||
<div *ngIf="isEditing" class="readonly-section">
|
||||
<div class="readonly-fields">
|
||||
<div class="field-column">
|
||||
<!-- Last Changed By -->
|
||||
<div class="readonly-field">
|
||||
<label>Last Changed By</label>
|
||||
<div class="readonly-value">
|
||||
{{contactReadOnlyFields.lastChangedBy || 'N/A'}}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Inactive status -->
|
||||
<div class="readonly-field">
|
||||
<label>Inactive Status </label>
|
||||
<div class="readonly-value">
|
||||
{{contactReadOnlyFields.isInactive === true ? 'Yes' : 'No' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-column">
|
||||
|
||||
<!-- Last Changed Date -->
|
||||
<div class="readonly-field">
|
||||
<label>Last Changed Date</label>
|
||||
<div class="readonly-value">
|
||||
{{(contactReadOnlyFields.lastChangedDate | date:'mediumDate':'UTC') || 'N/A'}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Inactivated Date -->
|
||||
<div class="readonly-field">
|
||||
<label>Inactivated Date</label>
|
||||
<div class="readonly-value">
|
||||
{{(contactReadOnlyFields.inactivatedDate | date:'mediumDate':'UTC') || 'N/A'}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button mat-button type="button" (click)="cancelEdit()">Cancel</button>
|
||||
<button mat-raised-button color="primary" type="submit" [disabled]="contactForm.invalid">
|
||||
{{ isEditing ? 'Update' : 'Save' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
354
src/app/holder/holder-contacts/holder-contacts.component.scss
Normal file
354
src/app/holder/holder-contacts/holder-contacts.component.scss
Normal file
@ -0,0 +1,354 @@
|
||||
.contacts-container {
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
|
||||
.actions-bar {
|
||||
clear: both;
|
||||
margin-bottom: -16px;
|
||||
|
||||
mat-slide-toggle {
|
||||
transform: scale(0.8);
|
||||
margin-left: -0.5rem;
|
||||
}
|
||||
|
||||
button {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
.table-container {
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
border-radius: 8px;
|
||||
|
||||
.loading-shade {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
mat-table {
|
||||
width: 100%;
|
||||
|
||||
mat-icon {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
.mat-column-actions {
|
||||
width: 180px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mat-column-defaultContact {
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.no-data-message {
|
||||
text-align: center;
|
||||
padding: 0.9rem;
|
||||
color: rgba(0, 0, 0, 0.54);
|
||||
|
||||
mat-icon {
|
||||
font-size: 1rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-bottom: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
mat-paginator {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.12);
|
||||
border-radius: 0 0 8px 8px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-container {
|
||||
background-color: white;
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
margin-top: 16px;
|
||||
|
||||
.form-header {
|
||||
margin-bottom: 24px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
color: var(--mat-sys-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
|
||||
mat-form-field {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.small-field {
|
||||
max-width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.readonly-section {
|
||||
padding-top: 0.5rem;
|
||||
border-top: 1px solid #eee;
|
||||
|
||||
.readonly-fields {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
|
||||
.field-column {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.readonly-field {
|
||||
label {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
color: #666;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.readonly-value {
|
||||
padding: 0.25rem;
|
||||
font-size: 0.9375rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive adjustments
|
||||
@media (max-width: 768px) {
|
||||
.contacts-container {
|
||||
padding: 16px;
|
||||
|
||||
.form-row {
|
||||
flex-direction: column;
|
||||
gap: 16px !important;
|
||||
|
||||
.small-field {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.contacts-container {
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
|
||||
.actions-bar {
|
||||
clear: both;
|
||||
margin-bottom: -16px;
|
||||
|
||||
mat-slide-toggle {
|
||||
transform: scale(0.8);
|
||||
margin-left: -0.5rem;
|
||||
}
|
||||
|
||||
button {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
.table-container {
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
border-radius: 8px;
|
||||
|
||||
.loading-shade {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
mat-table {
|
||||
width: 100%;
|
||||
|
||||
.rowActions {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
.mat-column-actions {
|
||||
width: 180px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mat-column-defaultContact {
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.no-data-message {
|
||||
text-align: center;
|
||||
padding: 0.9rem;
|
||||
color: rgba(0, 0, 0, 0.54);
|
||||
|
||||
mat-icon {
|
||||
font-size: 1rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-bottom: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
mat-paginator {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.12);
|
||||
border-radius: 0 0 8px 8px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-container {
|
||||
background-color: white;
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
margin-top: 16px;
|
||||
|
||||
.form-header {
|
||||
margin-bottom: 24px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
color: var(--mat-sys-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
|
||||
mat-form-field {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.small-field {
|
||||
max-width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.readonly-section {
|
||||
padding-top: 0.5rem;
|
||||
border-top: 1px solid #eee;
|
||||
|
||||
.readonly-fields {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
|
||||
.field-column {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.readonly-field {
|
||||
label {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
color: #666;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.readonly-value {
|
||||
padding: 0.25rem;
|
||||
font-size: 0.9375rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive adjustments
|
||||
@media (max-width: 768px) {
|
||||
.contacts-container {
|
||||
padding: 16px;
|
||||
|
||||
.form-row {
|
||||
flex-direction: column;
|
||||
gap: 16px !important;
|
||||
|
||||
.small-field {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
203
src/app/holder/holder-contacts/holder-contacts.component.ts
Normal file
203
src/app/holder/holder-contacts/holder-contacts.component.ts
Normal file
@ -0,0 +1,203 @@
|
||||
import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core';
|
||||
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { PhonePipe } from '../../shared/pipes/phone.pipe';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { MatPaginator } from '@angular/material/paginator';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { UserPreferences } from '../../core/models/user-preference';
|
||||
import { NotificationService } from '../../core/services/common/notification.service';
|
||||
import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service';
|
||||
import { ContactFormModel, HolderContact } from '../../core/models/holder/contact';
|
||||
import { ContactService } from '../../core/services/holder/contact.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-holder-contacts',
|
||||
imports: [AngularMaterialModule, ReactiveFormsModule, PhonePipe, CommonModule],
|
||||
templateUrl: './holder-contacts.component.html',
|
||||
styleUrl: './holder-contacts.component.scss'
|
||||
})
|
||||
export class HolderContactsComponent {
|
||||
@Input() isEditMode: boolean = false;
|
||||
@Input() holderid: number = 0;
|
||||
@Input() userPreferences: UserPreferences = {};
|
||||
|
||||
@Output() hasContacts = new EventEmitter<boolean>();
|
||||
|
||||
@ViewChild(MatPaginator) paginator!: MatPaginator;
|
||||
@ViewChild(MatSort) sort!: MatSort;
|
||||
|
||||
displayedColumns: string[] = ['firstName', 'lastName', 'title', 'phone', 'mobile', 'email', 'actions'];
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
contactForm: FormGroup;
|
||||
|
||||
isEditing = false;
|
||||
currentContactId: number | null = null;
|
||||
isLoading = false;
|
||||
showForm = false;
|
||||
showInactiveContacts = false;
|
||||
contacts: HolderContact[] = [];
|
||||
|
||||
contactReadOnlyFields: any = {
|
||||
lastChangedDate: null,
|
||||
lastChangedBy: null,
|
||||
isInactive: null,
|
||||
inactivatedDate: null
|
||||
};
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private contactService: ContactService,
|
||||
private notificationService: NotificationService,
|
||||
private errorHandler: ApiErrorHandlerService
|
||||
) {
|
||||
this.contactForm = 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)]],
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.holderid > 0) {
|
||||
this.loadContacts();
|
||||
}
|
||||
}
|
||||
|
||||
InactivateHolderContact(HOLDERCONTACTID: number): void {
|
||||
this.contactService.InactivateHolderContact(HOLDERCONTACTID).subscribe(
|
||||
{
|
||||
next: (basicData: any) => {
|
||||
this.notificationService.showSuccess(`HolderContact Inactivated successfully`);
|
||||
// this.loadContacts()
|
||||
},
|
||||
error: (error: any) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, `Failed to Inactivate HolderContact`);
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error Inactivating HolderContact:', error);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
ReactivateHolderContact(HOLDERCONTACTID: number): void {
|
||||
this.contactService.ReactivateHolderContact(HOLDERCONTACTID).subscribe(
|
||||
{
|
||||
next: (basicData: any) => {
|
||||
this.notificationService.showSuccess(`HolderContact Reactivated successfully`);
|
||||
// this.loadContacts()
|
||||
},
|
||||
error: (error: any) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, `Failed to Reactivate HolderContact`);
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error Reactivating HolderContact:', error);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.dataSource.paginator = this.paginator;
|
||||
this.dataSource.sort = this.sort;
|
||||
}
|
||||
|
||||
loadContacts(): void {
|
||||
this.isLoading = true;
|
||||
|
||||
this.contactService.getHolderContactsByHolderId(this.holderid).subscribe({
|
||||
next: (contacts: HolderContact[]) => {
|
||||
this.contacts = contacts;
|
||||
this.renderContacts();
|
||||
this.isLoading = false;
|
||||
},
|
||||
error: (error: any) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load contacts');
|
||||
this.notificationService.showError(errorMessage);
|
||||
this.isLoading = false;
|
||||
console.error('Error loading contacts:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addNewContact(): void {
|
||||
this.showForm = true;
|
||||
this.isEditing = false;
|
||||
this.currentContactId = null;
|
||||
this.contactForm.reset();
|
||||
}
|
||||
|
||||
editContact(contact: HolderContact): void {
|
||||
this.showForm = true;
|
||||
this.isEditing = true;
|
||||
this.currentContactId = contact.HOLDERCONTACTID;
|
||||
|
||||
this.contactForm.patchValue({
|
||||
firstName: contact.FIRSTNAME,
|
||||
lastName: contact.LASTNAME,
|
||||
middleInitial: contact.MIDDLEINITIAL,
|
||||
title: contact.TITLE,
|
||||
phone: contact.PHONE,
|
||||
mobile: contact.MOBILE,
|
||||
fax: contact.FAX,
|
||||
email: contact.EMAILADDRESS
|
||||
});
|
||||
|
||||
this.contactReadOnlyFields.lastChangedDate = contact.LASTUPDATEDDATE ?? contact.DATECREATED;
|
||||
this.contactReadOnlyFields.lastChangedBy = contact.LASTUPDATEDBY ?? contact.CREATEDBY;
|
||||
this.contactReadOnlyFields.isInactive = contact.INACTIVEFLAG === 'Y';
|
||||
this.contactReadOnlyFields.inactivatedDate = contact.INACTIVEDATE;
|
||||
}
|
||||
|
||||
saveContact(): void {
|
||||
if (this.contactForm.invalid && !(this.holderid > 0)) {
|
||||
this.contactForm.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
|
||||
const contactData: ContactFormModel = this.contactForm.value;
|
||||
|
||||
const saveObservable = this.isEditing && (this.currentContactId! > 0)
|
||||
? this.contactService.updateHolderContact(this.currentContactId!, contactData)
|
||||
: this.contactService.createHolderContact(this.holderid, contactData);
|
||||
|
||||
saveObservable.subscribe({
|
||||
next: () => {
|
||||
this.notificationService.showSuccess(`Contact ${this.isEditing ? 'updated' : 'added'} successfully`);
|
||||
this.loadContacts();
|
||||
this.cancelEdit();
|
||||
this.hasContacts.emit(true);
|
||||
},
|
||||
error: (error) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, `Failed to ${this.isEditing ? 'update' : 'add'} contact`);
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error saving contact:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggleShowInactiveContacts(): void {
|
||||
this.showInactiveContacts = !this.showInactiveContacts;
|
||||
this.renderContacts();
|
||||
}
|
||||
|
||||
renderContacts(): void {
|
||||
if (this.showInactiveContacts) {
|
||||
this.dataSource.data = this.contacts.filter(contact => contact.INACTIVEFLAG === 'Y');
|
||||
} else {
|
||||
this.dataSource.data = this.contacts.filter(contact => contact.INACTIVEFLAG === 'N');
|
||||
}
|
||||
}
|
||||
|
||||
cancelEdit(): void {
|
||||
this.showForm = false;
|
||||
this.isEditing = false;
|
||||
this.currentContactId = null;
|
||||
this.contactForm.reset();
|
||||
}
|
||||
}
|
||||
166
src/app/holder/holder-detail/holder-detail.component.html
Normal file
166
src/app/holder/holder-detail/holder-detail.component.html
Normal file
@ -0,0 +1,166 @@
|
||||
<div class="basic-details-container">
|
||||
<mat-card class="details-card mat-elevation-z4">
|
||||
<mat-card-content>
|
||||
<div class="loading-shade" *ngIf="isLoading">
|
||||
<mat-spinner diameter="50"></mat-spinner>
|
||||
</div>
|
||||
|
||||
<form [formGroup]="holderDetailsForm" class="details-form"
|
||||
(ngSubmit)="saveHolderDetails()">
|
||||
|
||||
<!-- Client Information -->
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline" class="name">
|
||||
<mat-label>Holder Name</mat-label>
|
||||
<input matInput formControlName="holderName" required>
|
||||
<mat-error *ngIf="f['holderName'].errors?.['required']">
|
||||
Name is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="f['holderName'].errors?.['maxlength']">
|
||||
Maximum 100 characters allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="lookup-code">
|
||||
<mat-label>DBA Name</mat-label>
|
||||
<input matInput formControlName="DBAName">
|
||||
<mat-error *ngIf="f['DBAName'].errors?.['required']">
|
||||
Lookup code is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="f['DBAName'].errors?.['maxlength']">
|
||||
Maximum 20 characters allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline" class="HolderNumber">
|
||||
<mat-label>Tax ID No</mat-label>
|
||||
<input matInput formControlName="HolderNumber" required>
|
||||
<mat-error *ngIf="f['HolderNumber'].errors?.['required']">
|
||||
Lookup code is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="f['HolderNumber'].errors?.['maxlength']">
|
||||
Maximum 20 characters allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="question-row">
|
||||
<mat-radio-group formControlName="holderType">
|
||||
<mat-label>Holder Type : </mat-label>
|
||||
<mat-radio-button value="CORP">Corporation</mat-radio-button>
|
||||
<mat-radio-button value="IND">Individual</mat-radio-button>
|
||||
<mat-radio-button value="GOV ">Government Agency </mat-radio-button>
|
||||
</mat-radio-group>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="question-row">
|
||||
<mat-radio-group formControlName="USCIBMember">
|
||||
<mat-label>Are you a member of USCIB ?</mat-label>
|
||||
<mat-radio-button [value]="'Y'">Yes</mat-radio-button>
|
||||
<mat-radio-button [value]="'N'">No</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="question-row">
|
||||
<mat-radio-group formControlName="govAgency">
|
||||
<mat-label>Are you belong to Government Agency ?</mat-label>
|
||||
<mat-radio-button [value]="'Y'">Yes</mat-radio-button>
|
||||
<mat-radio-button [value]="'N'">No</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Address Information -->
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline" class="address1">
|
||||
<mat-label>Address Line 1</mat-label>
|
||||
<input matInput formControlName="address1" required>
|
||||
<mat-error *ngIf="f['address1'].errors?.['required']">
|
||||
Address is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="f['address1'].errors?.['maxlength']">
|
||||
Maximum 100 characters allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline" class="address2">
|
||||
<mat-label>Address Line 2 (Optional)</mat-label>
|
||||
<input matInput formControlName="address2">
|
||||
<mat-error *ngIf="f['address2'].errors?.['maxlength']">
|
||||
Maximum 100 characters allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Location Information -->
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline" class="city">
|
||||
<mat-label>City</mat-label>
|
||||
<input matInput formControlName="city" required>
|
||||
<mat-error *ngIf="f['city'].errors?.['required']">
|
||||
City is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="f['city'].errors?.['maxlength']">
|
||||
Maximum 50 characters allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="country">
|
||||
<mat-label>Country</mat-label>
|
||||
<mat-select formControlName="country" required
|
||||
(selectionChange)="onCountryChange($event.value)">
|
||||
<mat-option *ngFor="let country of countries" [value]="country.value">
|
||||
{{ country.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="f['country'].errors?.['required']">
|
||||
Country is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="state">
|
||||
<mat-label>State/Province</mat-label>
|
||||
<mat-select formControlName="state" required>
|
||||
<mat-option *ngFor="let state of states" [value]="state.value">
|
||||
{{ state.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="f['state'].errors?.['required']">
|
||||
State is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="zip">
|
||||
<mat-label>ZIP/Postal Code</mat-label>
|
||||
<input matInput formControlName="zip" required>
|
||||
<mat-error *ngIf="f['zip'].errors?.['required']">
|
||||
ZIP/Postal code is required
|
||||
</mat-error>
|
||||
<mat-error
|
||||
*ngIf="f['country']?.value === 'US' && f['zip']?.touched && f['zip']?.errors?.['invalidUSZip']">
|
||||
Please enter a valid 5-digit US ZIP code
|
||||
</mat-error>
|
||||
<mat-error
|
||||
*ngIf="f['country']?.value === 'CA' && f['zip']?.touched && f['zip']?.errors?.['invalidCanadaPostal']">
|
||||
Please enter a valid postal code (e.g., A1B2C3)
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button mat-raised-button color="primary" type="submit"
|
||||
[disabled]="holderDetailsForm.invalid || !holderDetailsForm.dirty">
|
||||
{{ isEditMode ? 'Update' : 'Save' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
125
src/app/holder/holder-detail/holder-detail.component.scss
Normal file
125
src/app/holder/holder-detail/holder-detail.component.scss
Normal file
@ -0,0 +1,125 @@
|
||||
.basic-details-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
width: 100%;
|
||||
|
||||
.details-card {
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
|
||||
.loading-shade {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.details-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
|
||||
.question-row {
|
||||
margin-bottom: 1rem;
|
||||
// padding: 1rem;
|
||||
// background: #ffff;
|
||||
border-radius: 4px;
|
||||
// box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
|
||||
mat-radio-group {
|
||||
display: flex;
|
||||
// flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
mat-radio-button {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.lookup-code {
|
||||
grid-column: span 1;
|
||||
}
|
||||
|
||||
.HolderNumber{
|
||||
grid-column: span 1;
|
||||
// background-color: azure;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.address1,
|
||||
.address2 {
|
||||
grid-column: span 3;
|
||||
}
|
||||
|
||||
.city,
|
||||
.state,
|
||||
.country,
|
||||
.zip {
|
||||
grid-column: span 1;
|
||||
}
|
||||
|
||||
.carnet-issuing-region,
|
||||
.revenue-location {
|
||||
grid-column: span 1;
|
||||
}
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.basic-details-container {
|
||||
.details-card {
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
.name,
|
||||
.lookup-code,
|
||||
.address1,
|
||||
.address2,
|
||||
.city,
|
||||
.state,
|
||||
.zip,
|
||||
.country,
|
||||
.carnet-issuing-region,
|
||||
.revenue-location {
|
||||
grid-column: span 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
205
src/app/holder/holder-detail/holder-detail.component.ts
Normal file
205
src/app/holder/holder-detail/holder-detail.component.ts
Normal file
@ -0,0 +1,205 @@
|
||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { State } from '../../core/models/state';
|
||||
import { Region } from '../../core/models/region';
|
||||
import { NotificationService } from '../../core/services/common/notification.service';
|
||||
import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service';
|
||||
import { ZipCodeValidator } from '../../shared/validators/zipcode-validator';
|
||||
import { HolderDetail, HolderDetailFormType } from '../../core/models/holder/holder-details';
|
||||
import { HolderDetailService } from '../../core/services/holder/holder-detail.service';
|
||||
import { CommonService } from '../../core/services/common/common.service';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
import { Country } from '../../core/models/country';
|
||||
|
||||
@Component({
|
||||
selector: 'app-holder-detail',
|
||||
imports: [AngularMaterialModule, ReactiveFormsModule, CommonModule],
|
||||
templateUrl: './holder-detail.component.html',
|
||||
styleUrl: './holder-detail.component.scss'
|
||||
})
|
||||
export class HolderDetailComponent implements OnInit, OnDestroy {
|
||||
|
||||
@Input() isEditMode = false;
|
||||
@Input() holderid: number = 0;
|
||||
|
||||
@Output() holderIdCreated = new EventEmitter<number>();
|
||||
@Output() clientName = new EventEmitter<string>();
|
||||
|
||||
holderDetailsForm: FormGroup;
|
||||
countries: Country[] = [];
|
||||
regions: Region[] = [];
|
||||
states: State[] = [];
|
||||
|
||||
isLoading = false;
|
||||
countriesHasStates = ['US', 'CA', 'MX'];
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private holderDetailService: HolderDetailService,
|
||||
private notificationService: NotificationService,
|
||||
private commonService: CommonService,
|
||||
private errorHandler: ApiErrorHandlerService) {
|
||||
this.holderDetailsForm = this.createForm();
|
||||
}
|
||||
|
||||
createForm(): FormGroup {
|
||||
return this.fb.group({
|
||||
holderName: ['', [Validators.required, Validators.maxLength(100)]],
|
||||
DBAName: ['', [Validators.maxLength(20)]],
|
||||
HolderNumber: ['', [Validators.required, Validators.maxLength(20)]],
|
||||
holderType: ['', [Validators.required, Validators.maxLength(20)]],
|
||||
USCIBMember: ['', [Validators.required]],
|
||||
govAgency: ['', [Validators.required]],
|
||||
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')]]
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadLookupData();
|
||||
|
||||
if (this.holderid > 0) {
|
||||
this.isLoading = true;
|
||||
this.holderDetailService.getHolderDetailByHolderId(this.holderid).subscribe({
|
||||
next: (basicDetail: HolderDetail) => {
|
||||
this.patchFormData(basicDetail);
|
||||
this.clientName.emit(basicDetail.HOLDERNAME);
|
||||
this.isLoading = false;
|
||||
},
|
||||
error: (error: any) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load holder details');
|
||||
this.notificationService.showError(errorMessage);
|
||||
this.isLoading = false;
|
||||
console.error('Error loading holder details:', error);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
patchFormData(data: any): void {
|
||||
|
||||
this.holderDetailsForm.patchValue({
|
||||
holderName: data.HOLDERNAME,
|
||||
DBAName: data.ADDLNAME,
|
||||
HolderNumber: data.HOLDERNO,
|
||||
holderType: data.HOLDERTYPE,
|
||||
USCIBMember: data.USCIBMEMBERFLAG,
|
||||
govAgency: data.GOVAGENCYFLAG,
|
||||
address1: data.ADDRESS1,
|
||||
address2: data.ADDRESS2,
|
||||
city: data.CITY,
|
||||
state: data.STATE,
|
||||
country: data.COUNTRY,
|
||||
zip: data.ZIP
|
||||
})
|
||||
|
||||
if (data.COUNTRY) {
|
||||
this.loadStates(data.COUNTRY);
|
||||
}
|
||||
}
|
||||
|
||||
get f() {
|
||||
return this.holderDetailsForm.controls;
|
||||
}
|
||||
|
||||
onCountryChange(country: string): void {
|
||||
this.holderDetailsForm.get('state')?.reset();
|
||||
|
||||
if (country) {
|
||||
this.loadStates(country);
|
||||
}
|
||||
|
||||
this.holderDetailsForm.get('zip')?.updateValueAndValidity();
|
||||
}
|
||||
|
||||
saveHolderDetails() {
|
||||
if (this.holderDetailsForm.invalid) {
|
||||
this.holderDetailsForm.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
|
||||
const holderDetailData: HolderDetailFormType = this.holderDetailsForm.value;
|
||||
|
||||
const saveObservable = this.isEditMode && this.holderid > 0
|
||||
? this.holderDetailService.updateHolderDetails(this.holderid, holderDetailData)
|
||||
: this.holderDetailService.createHolderDetail(holderDetailData);
|
||||
|
||||
saveObservable.subscribe({
|
||||
next: (basicData: any) => {
|
||||
this.notificationService.showSuccess(`Holder details ${this.isEditMode ? 'updated' : 'added'} successfully`);
|
||||
|
||||
if (!this.isEditMode) {
|
||||
this.holderIdCreated.emit(basicData.HOLDERID);
|
||||
}
|
||||
|
||||
if (this.isEditMode) {
|
||||
this.clientName.emit(holderDetailData.holderName);
|
||||
}
|
||||
},
|
||||
error: (error: any) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, `Failed to ${this.isEditMode ? 'update' : 'add'} Holder details`);
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error saving basic details:', error);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
updateStateControl(controlName: string, country: string): void {
|
||||
const stateControl = this.holderDetailsForm.get(controlName);
|
||||
if (this.countriesHasStates.includes(country)) {
|
||||
stateControl?.enable();
|
||||
} else {
|
||||
stateControl?.disable();
|
||||
stateControl?.setValue('FN');
|
||||
}
|
||||
}
|
||||
|
||||
loadLookupData(): void {
|
||||
|
||||
this.commonService.getCountries(0)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: (countries) => {
|
||||
this.countries = countries;
|
||||
this.isLoading = false;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Failed to load countries', error);
|
||||
this.isLoading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
loadStates(country: string): void {
|
||||
this.isLoading = true;
|
||||
country = this.countriesHasStates.includes(country) ? country : 'FN';
|
||||
this.commonService.getStates(country, this.holderid)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: (states) => {
|
||||
this.states = states;
|
||||
this.updateStateControl('state', country);
|
||||
this.isLoading = false;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Failed to load states', error);
|
||||
this.isLoading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
131
src/app/holder/search/search.component.html
Normal file
131
src/app/holder/search/search.component.html
Normal file
@ -0,0 +1,131 @@
|
||||
<div style="margin-top: 16px;" class="manage-preparers-container">
|
||||
<h2 class="page-header">Search Holders</h2>
|
||||
|
||||
<div class="search-section">
|
||||
<form class="search-form" [formGroup]="searchForm" (ngSubmit)="onSearch()">
|
||||
<div class="search-fields">
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline" class="name">
|
||||
<mat-label>Holder Name</mat-label>
|
||||
<input matInput formControlName="holderName" placeholder="Enter holder name">
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="search-actions">
|
||||
<button mat-raised-button color="primary" type="submit" [disabled]="HolderSaved">
|
||||
<mat-icon>search</mat-icon>
|
||||
Search
|
||||
</button>
|
||||
<button mat-raised-button type="button" (click)="onClearSearch()" [disabled]="HolderSaved">
|
||||
<mat-icon>clear</mat-icon>
|
||||
Clear Search
|
||||
</button>
|
||||
|
||||
<button mat-raised-button color="primary" (click)="addNewHolder()" [disabled]="HolderSaved">
|
||||
<mat-icon>add</mat-icon>
|
||||
Add New Holder
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="results-section">
|
||||
<div class="loading-shade" *ngIf="isLoading">
|
||||
<mat-spinner diameter="50"></mat-spinner>
|
||||
</div>
|
||||
|
||||
<div class="actions-bar" style="margin-bottom: 10px;">
|
||||
<mat-slide-toggle (change)="toggleShowInactiveHolders()" [disabled]="holdersSearched.length === 0">
|
||||
Show Inactive Holders
|
||||
</mat-slide-toggle>
|
||||
</div>
|
||||
|
||||
<table mat-table [dataSource]="dataSource" class="results-table" matSort>
|
||||
|
||||
<!-- Name Column -->
|
||||
<ng-container matColumnDef="HolderName">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Holder Name</th>
|
||||
<td mat-cell *matCellDef="let client">{{ client.HOLDERNAME }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Address Column -->
|
||||
<ng-container matColumnDef="DBAName">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>DBA Name</th>
|
||||
<td mat-cell *matCellDef="let client">{{ client.ADDLNAME || '--'}}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- City Column -->
|
||||
<ng-container matColumnDef="Address">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Address</th>
|
||||
<td mat-cell *matCellDef="let client">{{ client.ADDRESS1 }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- State Column -->
|
||||
<ng-container matColumnDef="USCIBMember">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>USCIB Member</th>
|
||||
<td mat-cell *matCellDef="let client">{{ client.USCIBMEMBERFLAG === 'Y' ? 'Yes' : 'No' }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Country Column -->
|
||||
<ng-container matColumnDef="HolderType">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Holder Type</th>
|
||||
<td mat-cell *matCellDef="let client">{{ client.HOLDERTYPE }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Actions Column -->
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef class="actions-header">Actions</th>
|
||||
<td mat-cell *matCellDef="let client" class="actions-cell">
|
||||
<button mat-icon-button color="primary" (click)="onEdit(client.HOLDERID)" matTooltip="Edit"
|
||||
[disabled]="HolderSaved">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
|
||||
<button mat-icon-button color="warn" *ngIf="client.INACTIVEFLAG === 'N'" matTooltip="Inactivate"
|
||||
(click)="InactivateHolder(client.HOLDERID)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="warn" *ngIf="client.INACTIVEFLAG === 'Y'" matTooltip="Reactivate"
|
||||
(click)="ReactivateHolder(client.HOLDERID)">
|
||||
<mat-icon>delete_outline</mat-icon>
|
||||
</button>
|
||||
|
||||
<mat-radio-button style="position: absolute; margin-left: 10px" matTooltip="Select Holder"
|
||||
class="selectHolder"
|
||||
[value]="client.HOLDERID"
|
||||
[checked]="selectedHolderId === client.HOLDERID"
|
||||
(change)="onRadioSelect(client)" aria-label="Select row"
|
||||
[hidden]="client.INACTIVEFLAG === 'Y'">
|
||||
</mat-radio-button>
|
||||
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
|
||||
<tr matNoDataRow *matNoDataRow>
|
||||
<td [attr.colspan]="displayedColumns.length" class="no-data-message">
|
||||
<mat-icon>info</mat-icon>
|
||||
<span>No records found matching your criteria</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<mat-paginator [length]="dataSource.data.length" [pageSizeOptions]="[userPreferences.pageSize || 2]"
|
||||
[hidePageSize]="true" showFirstLastButtons></mat-paginator>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button mat-button type="button" (click)="cancelEdit()">Cancel</button>
|
||||
<button mat-raised-button color="primary" type="submit" [disabled]="selectedHolderId && HolderSaved"
|
||||
(click)="completeSelection()">
|
||||
{{ 'Save' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
135
src/app/holder/search/search.component.scss
Normal file
135
src/app/holder/search/search.component.scss
Normal file
@ -0,0 +1,135 @@
|
||||
.page-header {
|
||||
margin: 0.5rem 0px;
|
||||
color: var(--mat-sys-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.manage-preparers-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
|
||||
.search-fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
|
||||
.name,
|
||||
.address {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.city,
|
||||
.state,
|
||||
.lookup-code {
|
||||
grid-column: span 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
|
||||
button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.results-section {
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
border-radius: 8px;
|
||||
|
||||
.loading-shade {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
mat-table {
|
||||
width: 100%;
|
||||
|
||||
mat-icon {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
.mat-column-actions {
|
||||
width: 180px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mat-column-defaultContact {
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.no-data-message {
|
||||
text-align: center;
|
||||
padding: 0.9rem;
|
||||
color: rgba(0, 0, 0, 0.54);
|
||||
|
||||
mat-icon {
|
||||
font-size: 1rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-bottom: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
mat-paginator {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.12);
|
||||
border-radius: 0 0 8px 8px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.manage-preparers-container {
|
||||
.search-form {
|
||||
.search-fields {
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
.name,
|
||||
.address,
|
||||
.city,
|
||||
.state,
|
||||
.lookup-code {
|
||||
grid-column: span 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
201
src/app/holder/search/search.component.ts
Normal file
201
src/app/holder/search/search.component.ts
Normal file
@ -0,0 +1,201 @@
|
||||
import { Component, EventEmitter, inject, Input, Output, ViewChild } from '@angular/core';
|
||||
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { HolderDetail } from '../../core/models/holder/holder-details';
|
||||
import { NavigationService } from '../../core/services/common/navigation.service';
|
||||
import { UserPreferencesService } from '../../core/services/user-preference.service';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { NotificationService } from '../../core/services/common/notification.service';
|
||||
import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service';
|
||||
import { UserPreferences } from '../../core/models/user-preference';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { MatPaginator } from '@angular/material/paginator';
|
||||
import { HolderService } from '../../core/services/holder/holder.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-search',
|
||||
imports: [AngularMaterialModule, ReactiveFormsModule, CommonModule],
|
||||
templateUrl: './search.component.html',
|
||||
styleUrl: './search.component.scss'
|
||||
})
|
||||
export class SearchComponent {
|
||||
@ViewChild(MatPaginator) paginator!: MatPaginator;
|
||||
@ViewChild(MatSort) sort!: MatSort;
|
||||
|
||||
@Output() selectHolderCompleted = new EventEmitter<boolean>();
|
||||
|
||||
@Input() headerid: number = 0;
|
||||
@Input() HOLDERID: number = 0
|
||||
|
||||
selectedHolder: any = null;
|
||||
selectedHolderId: number = 0;
|
||||
HolderSaved: boolean = false;
|
||||
showInactiveHolders: boolean = false;
|
||||
holdersSearched: any = []
|
||||
isLoading: boolean = false;
|
||||
|
||||
userPreferences: UserPreferences;
|
||||
searchForm: FormGroup;
|
||||
|
||||
private holderService = inject(HolderService);
|
||||
private errorHandler = inject(ApiErrorHandlerService);
|
||||
private notificationService = inject(NotificationService);
|
||||
dataSource = new MatTableDataSource<any>([]);
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.dataSource.paginator = this.paginator;
|
||||
this.dataSource.sort = this.sort;
|
||||
}
|
||||
|
||||
private fb = inject(FormBuilder);
|
||||
|
||||
displayedColumns: string[] = ['HolderName', 'DBAName', 'Address', 'USCIBMember', 'HolderType', 'actions'];
|
||||
|
||||
constructor(
|
||||
private userPrefenceService: UserPreferencesService,
|
||||
private navigationService: NavigationService,
|
||||
) {
|
||||
this.userPreferences = this.userPrefenceService.getPreferences();
|
||||
this.searchForm = this.createSearchForm();
|
||||
}
|
||||
|
||||
createSearchForm(): FormGroup {
|
||||
return this.fb.group({
|
||||
holderName: ['']
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.HOLDERID > 0) {
|
||||
this.selectedHolderId = this.HOLDERID;
|
||||
}
|
||||
}
|
||||
|
||||
searchHolders(): void {
|
||||
|
||||
this.isLoading = true;
|
||||
|
||||
const filterData: { holderName: string } = this.searchForm.value;
|
||||
|
||||
this.holderService.searchHolders(filterData).subscribe({
|
||||
next: (holders: HolderDetail[]) => {
|
||||
this.holdersSearched = holders
|
||||
this.dataSource.data = this.holdersSearched;
|
||||
this.renderHolders()
|
||||
this.isLoading = false;
|
||||
},
|
||||
error: (error: any) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to search preparers');
|
||||
this.notificationService.showError(errorMessage);
|
||||
this.isLoading = false;
|
||||
console.error('Error loading preparers:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
isSearchCriteriaProvided(): boolean {
|
||||
const values = this.searchForm.value;
|
||||
return !!values.holderName
|
||||
}
|
||||
|
||||
onSearch(): void {
|
||||
if (this.searchForm.invalid || !this.isSearchCriteriaProvided()) {
|
||||
return;
|
||||
}
|
||||
this.searchHolders();
|
||||
}
|
||||
|
||||
completeSelection() {
|
||||
this.saveApplication();
|
||||
}
|
||||
|
||||
saveApplication(): void {
|
||||
const saveObservable = this.holderService.updateApplicationHolder(this.headerid, this.selectedHolderId ? Number(this.selectedHolderId) : 0);
|
||||
|
||||
saveObservable.subscribe({
|
||||
next: (basicData: any) => {
|
||||
this.notificationService.showSuccess(`Holder updated successfully`);
|
||||
this.selectHolderCompleted.emit(true);
|
||||
},
|
||||
error: (error: any) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, `Failed to update holder details`);
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error saving holder details:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggleShowInactiveHolders(): void {
|
||||
this.showInactiveHolders = !this.showInactiveHolders;
|
||||
this.renderHolders();
|
||||
}
|
||||
|
||||
renderHolders() {
|
||||
if (this.showInactiveHolders) {
|
||||
this.dataSource.data = this.holdersSearched.filter((holder: any) => holder?.INACTIVEFLAG === 'Y');
|
||||
} else {
|
||||
this.dataSource.data = this.holdersSearched.filter((holder: any) => holder?.INACTIVEFLAG === 'N');
|
||||
}
|
||||
}
|
||||
|
||||
addNewHolder(): void {
|
||||
this.navigationService.navigate(["add-holder"], { state: { isEditMode: false } })
|
||||
}
|
||||
|
||||
onEdit(id: string) {
|
||||
this.navigationService.navigate(['edit-holder', id]);
|
||||
}
|
||||
|
||||
cancelEdit() {
|
||||
this.clearSelection();
|
||||
}
|
||||
|
||||
clearSelection(): void {
|
||||
this.selectedHolder = null;
|
||||
this.selectedHolderId = 0;
|
||||
}
|
||||
|
||||
InactivateHolder(HOLDERID: number): void {
|
||||
this.holderService.InactivateHolder(HOLDERID).subscribe(
|
||||
{
|
||||
next: (basicData: any) => {
|
||||
this.notificationService.showSuccess(`Holder Inactivated successfully`);
|
||||
this.searchHolders();
|
||||
},
|
||||
error: (error: any) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, `Failed to Inactivate Holder`);
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error Inactivating Holder:', error);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
ReactivateHolder(HOLDERID: number): void {
|
||||
this.holderService.ReactivateHolder(HOLDERID).subscribe(
|
||||
{
|
||||
next: (basicData: any) => {
|
||||
this.notificationService.showSuccess(`Holder Reactivated successfully`);
|
||||
this.searchHolders();
|
||||
},
|
||||
error: (error: any) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, `Failed to Reactivate Holder`);
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error Reactivating Holder:', error);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
onClearSearch(): void {
|
||||
this.searchForm.reset();
|
||||
this.dataSource.data = []
|
||||
this.holdersSearched = [];
|
||||
}
|
||||
|
||||
onRadioSelect(holder: any): void {
|
||||
this.selectedHolder = holder;
|
||||
this.selectedHolderId = holder.HOLDERID;
|
||||
}
|
||||
}
|
||||
@ -11,7 +11,7 @@
|
||||
<div class="questions-section">
|
||||
<div class="question-row">
|
||||
<mat-radio-group (change)="newCarnet($event)">
|
||||
<mat-label>Do you need a new carnet?</mat-label>
|
||||
<mat-label class="question">Do you need a new carnet?</mat-label>
|
||||
<mat-radio-button [value]="true">Yes</mat-radio-button>
|
||||
<mat-radio-button [value]="false">No</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
@ -19,7 +19,7 @@
|
||||
|
||||
<div class="question-row">
|
||||
<mat-radio-group (change)="additionalSets($event)">
|
||||
<mat-label>Do you need additional sets?</mat-label>
|
||||
<mat-label class="question">Do you need additional sets?</mat-label>
|
||||
<mat-radio-button [value]="true">Yes</mat-radio-button>
|
||||
<mat-radio-button [value]="false">No</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
@ -27,7 +27,8 @@
|
||||
|
||||
<div class="question-row">
|
||||
<mat-radio-group (change)="duplicateCarnet($event)">
|
||||
<mat-label>Did you lose your carnet? Are you looking for duplicates?</mat-label>
|
||||
<mat-label class="question">Did you lose your carnet? Are you looking for
|
||||
duplicates?</mat-label>
|
||||
<mat-radio-button [value]="true">Yes</mat-radio-button>
|
||||
<mat-radio-button [value]="false">No</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
@ -35,7 +36,7 @@
|
||||
|
||||
<div class="question-row">
|
||||
<mat-radio-group (change)="extendCarnet($event)">
|
||||
<mat-label>Do you need to extend the carnet?</mat-label>
|
||||
<mat-label class="question">Do you need to extend the carnet?</mat-label>
|
||||
<mat-radio-button [value]="true">Yes</mat-radio-button>
|
||||
<mat-radio-button [value]="false">No</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
@ -49,6 +50,13 @@
|
||||
<mat-spinner diameter="50"></mat-spinner>
|
||||
</div>
|
||||
|
||||
<div class="table-actions">
|
||||
<button mat-raised-button color="accent" (click)="exportData()" *ngIf="dataSource.data.length > 0"
|
||||
matTooltip="Export to Excel" [disabled]="dataSource.data.length === 0">
|
||||
<mat-icon>download</mat-icon> Export
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="table-container mat-elevation-z8" *ngIf="showTable">
|
||||
<table mat-table [dataSource]="dataSource" matSort>
|
||||
<ng-container matColumnDef="applicationName">
|
||||
@ -111,8 +119,9 @@
|
||||
<th mat-header-cell *matHeaderCellDef>Actions</th>
|
||||
<td mat-cell *matCellDef="let item">
|
||||
<button mat-icon-button color="primary" (click)="editCarnet(item)"
|
||||
*ngIf="getCarnetStatusLabel(item.carnetStatus) === 'Pending'"
|
||||
[hidden]="getCarnetStatusLabel(item.carnetStatus) === 'Pending'" matTooltip="Edit">
|
||||
*ngIf="getCarnetStatusLabel(item.carnetStatus) === 'Not Submitted'"
|
||||
[hidden]="getCarnetStatusLabel(item.carnetStatus) !== 'Not Submitted'"
|
||||
matTooltip="Edit">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
|
||||
@ -21,9 +21,12 @@
|
||||
|
||||
.questions-section {
|
||||
flex: 0 0 70%;
|
||||
padding: 24px 0px;
|
||||
margin-bottom: 24px;
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 46px;
|
||||
margin-left: 1rem;
|
||||
margin-top: 24px;
|
||||
background: var(--mat-sys-surface);
|
||||
border-radius: 4px;
|
||||
|
||||
.question-row {
|
||||
margin-bottom: 1rem;
|
||||
@ -32,6 +35,15 @@
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
.question {
|
||||
color: var(--mat-sys-on-surface);
|
||||
font-family: var(--mat-sys-body-medium-font);
|
||||
line-height: var(--mat-sys-body-medium-line-height);
|
||||
font-size: var(--mat-sys-body-medium-size);
|
||||
letter-spacing: var(--mat-sys-body-medium-tracking);
|
||||
font-weight: var(--mat-sys-medium-font-weight);
|
||||
}
|
||||
|
||||
mat-radio-button {
|
||||
margin-top: -5px;
|
||||
}
|
||||
@ -40,6 +52,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
.table-actions {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
|
||||
button {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-container {
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
|
||||
@ -15,6 +15,8 @@ import { CommonService } from '../core/services/common/common.service';
|
||||
import { NotificationService } from '../core/services/common/notification.service';
|
||||
import { NavigationService } from '../core/services/common/navigation.service';
|
||||
import { MatRadioChange } from '@angular/material/radio';
|
||||
import * as XLSX from 'xlsx';
|
||||
import { CarnetStateStore } from '../core/services/carnet/carnet-application.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
@ -24,6 +26,7 @@ import { MatRadioChange } from '@angular/material/radio';
|
||||
providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }],
|
||||
})
|
||||
export class HomeComponent {
|
||||
store = inject(CarnetStateStore);
|
||||
carnetData: any[] = [];
|
||||
isLoading = false;
|
||||
showTable = false;
|
||||
@ -105,6 +108,40 @@ export class HomeComponent {
|
||||
}
|
||||
|
||||
exportData() {
|
||||
try {
|
||||
// Prepare worksheet data
|
||||
const worksheetData = this.prepareDownloadData();
|
||||
|
||||
// Create workbook and worksheet
|
||||
const workbook = XLSX.utils.book_new();
|
||||
const worksheet = XLSX.utils.json_to_sheet(worksheetData);
|
||||
|
||||
// Add worksheet to workbook
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, 'Carnet Data');
|
||||
|
||||
// Generate Excel file
|
||||
const fileName = `carnet_data_${new Date().toISOString()}.xlsx`;
|
||||
XLSX.writeFile(workbook, fileName);
|
||||
} catch (error) {
|
||||
console.error('Error downloading goods items:', error);
|
||||
this.notificationService.showError('Failed to download Excel file');
|
||||
}
|
||||
}
|
||||
|
||||
prepareDownloadData(): any[] {
|
||||
return this.dataSource.data.map(item => ({
|
||||
'Application Name': item.applicationName,
|
||||
'Holder Name': item.holderName,
|
||||
'Carnet Number': item.carnetNumber,
|
||||
'US Sets': item.usSets,
|
||||
'Foreign Sets': item.foreignSets,
|
||||
'Transit Sets': item.transitSets,
|
||||
'Carnet Value': item.carnetValue,
|
||||
'Issue Date': item.issueDate ? new Date(item.issueDate).toLocaleDateString() : '',
|
||||
'Expiry Date': item.expiryDate ? new Date(item.expiryDate).toLocaleDateString() : '',
|
||||
'Order Type': item.orderType,
|
||||
'Carnet Status': this.getCarnetStatusLabel(item.carnetStatus)
|
||||
}));
|
||||
}
|
||||
|
||||
getCarnetStatusLabel(value: string): string {
|
||||
@ -114,7 +151,8 @@ export class HomeComponent {
|
||||
|
||||
newCarnet(event: MatRadioChange): void {
|
||||
if (event.value) {
|
||||
this.navigateTo('add-carnet');
|
||||
this.store.resetState();
|
||||
this.navigateTo(['add-carnet']);
|
||||
}
|
||||
}
|
||||
|
||||
@ -129,13 +167,15 @@ export class HomeComponent {
|
||||
|
||||
editCarnet(item: any): void {
|
||||
if (item && item.headerId) {
|
||||
this.navigateTo(`edit-carnet/${item.headerId}?ApplicationName=${item.applicationName}`);
|
||||
this.navigateTo(['edit-carnet', item.headerId], {
|
||||
queryParams: { applicationname: item.applicationName }
|
||||
});
|
||||
} else {
|
||||
this.notificationService.showError('Invalid carnet data');
|
||||
}
|
||||
}
|
||||
|
||||
navigateTo(route: string): void {
|
||||
this.navigationService.navigate([route]);
|
||||
navigateTo(route: any[], extras?: any): void {
|
||||
this.navigationService.navigate(route, extras);
|
||||
}
|
||||
}
|
||||
@ -21,8 +21,12 @@
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<div class="forgot-password">
|
||||
<a href="#" routerLink="/forgot-password" class="forgot-link">Forgot your password?</a>
|
||||
<div class="additional-options">
|
||||
<a href="#" routerLink="/forgot-password" class="additional-options-link">Forgot your password?</a>
|
||||
</div>
|
||||
|
||||
<div class="additional-options">
|
||||
<a href="#" routerLink="/register" class="additional-options-link">New User? Register</a>
|
||||
</div>
|
||||
|
||||
<button mat-raised-button color="primary" type="submit" [disabled]="!loginForm.valid">
|
||||
|
||||
@ -29,11 +29,11 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.forgot-password {
|
||||
.additional-options {
|
||||
text-align: right;
|
||||
margin-top: -1rem;
|
||||
|
||||
.forgot-link {
|
||||
.additional-options-link {
|
||||
color: #666;
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
|
||||
@ -2,16 +2,7 @@
|
||||
<div class="registration-card">
|
||||
<h3 class="subtitle">User Registration</h3>
|
||||
|
||||
<!-- Show error message if token is invalid -->
|
||||
<div *ngIf="tokenError" class="token-error">
|
||||
<mat-error>
|
||||
<mat-icon>error_outline</mat-icon>
|
||||
<span>Invalid registration link. Please request a new registration link from your
|
||||
administrator.</span>
|
||||
</mat-error>
|
||||
</div>
|
||||
|
||||
<form *ngIf="!tokenError" [formGroup]="registrationForm" (ngSubmit)="onSubmit()" class="registration-form">
|
||||
<form [formGroup]="registrationForm" (ngSubmit)="onSubmit()" class="registration-form">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Email</mat-label>
|
||||
<input matInput formControlName="email" type="email" required>
|
||||
@ -33,6 +24,27 @@
|
||||
<mat-error *ngIf="password?.errors?.['required']">
|
||||
Password is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="password?.errors?.['minlength']">
|
||||
Password must be at least 8 characters
|
||||
</mat-error>
|
||||
<mat-error *ngIf="password?.errors?.['pattern']">
|
||||
Password must contain at least one uppercase letter, one lowercase letter, one number, and one
|
||||
special character
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Confirm Password</mat-label>
|
||||
<input matInput formControlName="confirmPassword" [type]="hidePassword ? 'password' : 'text'" required>
|
||||
<button type="button" mat-icon-button matSuffix (click)="hidePassword = !hidePassword">
|
||||
<mat-icon>{{ hidePassword ? 'visibility_off' : 'visibility' }}</mat-icon>
|
||||
</button>
|
||||
<mat-error *ngIf="confirmPassword?.errors?.['required']">
|
||||
Please confirm your password
|
||||
</mat-error>
|
||||
<mat-error *ngIf="registrationForm.errors?.['mismatch'] && confirmPassword?.touched">
|
||||
Passwords do not match
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<div class="login">
|
||||
|
||||
@ -19,8 +19,9 @@ export class RegisterComponent {
|
||||
registrationForm: FormGroup;
|
||||
isLoading = false;
|
||||
hidePassword = true;
|
||||
tokenError: boolean = false;
|
||||
token: string | null = null;
|
||||
|
||||
// tokenError: boolean = false;
|
||||
//token: string | null = null;
|
||||
|
||||
private fb = inject(FormBuilder);
|
||||
private authService = inject(AuthService);
|
||||
@ -33,16 +34,19 @@ export class RegisterComponent {
|
||||
constructor() {
|
||||
this.registrationForm = this.fb.group({
|
||||
email: ['', [Validators.required, Validators.email]],
|
||||
password: ['', [Validators.required]]
|
||||
});
|
||||
password: ['', [Validators.required, Validators.minLength(8),
|
||||
Validators.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]+$/)
|
||||
]],
|
||||
confirmPassword: ['', [Validators.required]]
|
||||
}, { validator: this.passwordMatchValidator });
|
||||
|
||||
// Get token from URL if present
|
||||
this.token = this.route.snapshot.paramMap.get('token');
|
||||
// // Get token from URL if present
|
||||
// this.token = this.route.snapshot.paramMap.get('token');
|
||||
|
||||
// Check if token is null or empty
|
||||
if (!this.token || this.token.trim() === '') {
|
||||
this.tokenError = true;
|
||||
}
|
||||
// // Check if token is null or empty
|
||||
// if (!this.token || this.token.trim() === '') {
|
||||
// this.tokenError = true;
|
||||
// }
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
@ -58,7 +62,7 @@ export class RegisterComponent {
|
||||
this.isLoading = true;
|
||||
const { email, password } = this.registrationForm.value;
|
||||
|
||||
this.authService.register(email, password, this.token).subscribe({
|
||||
this.authService.register(email, password).subscribe({
|
||||
next: () => {
|
||||
this.notificationService.showSuccess('Registration successful');
|
||||
this.router.navigate(['/login']);
|
||||
@ -72,6 +76,12 @@ export class RegisterComponent {
|
||||
});
|
||||
}
|
||||
|
||||
private passwordMatchValidator(formGroup: FormGroup) {
|
||||
const password = formGroup.get('password')?.value;
|
||||
const confirmPassword = formGroup.get('confirmPassword')?.value;
|
||||
return password === confirmPassword ? null : { mismatch: true };
|
||||
}
|
||||
|
||||
get email() {
|
||||
return this.registrationForm.get('email');
|
||||
}
|
||||
@ -79,4 +89,8 @@ export class RegisterComponent {
|
||||
get password() {
|
||||
return this.registrationForm.get('password');
|
||||
}
|
||||
|
||||
get confirmPassword() {
|
||||
return this.registrationForm.get('confirmPassword');
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,6 +26,7 @@ import { MatAccordion, MatExpansionModule } from '@angular/material/expansion';
|
||||
import { MatStepperModule } from '@angular/material/stepper';
|
||||
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
||||
import { MAT_MOMENT_DATE_ADAPTER_OPTIONS, MatMomentDateModule } from '@angular/material-moment-adapter';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
@ -55,7 +56,8 @@ import { MAT_MOMENT_DATE_ADAPTER_OPTIONS, MatMomentDateModule } from '@angular/m
|
||||
MatAccordion,
|
||||
MatStepperModule,
|
||||
MatMomentDateModule,
|
||||
MatSlideToggleModule
|
||||
MatSlideToggleModule,
|
||||
MatProgressBarModule
|
||||
],
|
||||
exports: [
|
||||
MatButtonModule,
|
||||
@ -82,7 +84,8 @@ import { MAT_MOMENT_DATE_ADAPTER_OPTIONS, MatMomentDateModule } from '@angular/m
|
||||
MatAccordion,
|
||||
MatStepperModule,
|
||||
MatMomentDateModule,
|
||||
MatSlideToggleModule
|
||||
MatSlideToggleModule,
|
||||
MatProgressBarModule
|
||||
],
|
||||
providers: [
|
||||
{ provide: MAT_MOMENT_DATE_ADAPTER_OPTIONS, useValue: { useUtc: true } }
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user