Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9bd9b8e7f6 | |||
| 2be4a0175e | |||
| 19663213be | |||
| b29d3dbac3 | |||
| 7a92afa60d | |||
| 23162d7ccb | |||
| 09d7691cb8 | |||
| 1ffd6713f4 | |||
| c10991a468 | |||
| 0ff9232f3b | |||
| d229c9c447 | |||
| 1fcd1829f9 | |||
| 15ef3925d8 | |||
| 2fa8ce079c | |||
| ed2761d90a | |||
| a695cc8f4e | |||
| 07e7a0da63 | |||
| abfbdb94c0 | |||
| ca979c7488 | |||
| 6787450019 | |||
| 98acaa709b | |||
| b784d16902 | |||
| 0cd13190b6 | |||
| 420900fec1 | |||
| a62273476d | |||
| d31084fad3 | |||
| cfcc565099 | |||
| 5be3f005ac | |||
| 3d07921cec | |||
| d7318a0316 | |||
| 912788b6c2 | |||
| 127011b416 | |||
| 534f693a50 | |||
| 2db8d71231 | |||
| 47ff5377c1 | |||
| a541e3788b | |||
| 5fd0cd0eaa | |||
| 54a4505085 | |||
| c58b276316 | |||
| e89f968786 | |||
| 377b062ace | |||
| e4e4679bd5 | |||
| dbc4bc523f | |||
| d97afa0244 | |||
| 1b6ceb6be7 | |||
| 78f7b114fa |
34
angular.json
34
angular.json
@ -34,6 +34,7 @@
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"@angular/material/prebuilt-themes/azure-blue.css",
|
||||
"src/styles.scss"
|
||||
],
|
||||
"server": "src/main.server.ts",
|
||||
@ -68,6 +69,28 @@
|
||||
"with": "src/environments/environment.development.ts"
|
||||
}
|
||||
]
|
||||
},
|
||||
"dev-1": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true,
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment-dev-1.ts"
|
||||
}
|
||||
]
|
||||
},
|
||||
"dev-2": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true,
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment-dev-2.ts"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
@ -77,7 +100,9 @@
|
||||
"options": {
|
||||
"allowedHosts": [
|
||||
"localhost",
|
||||
"client.alphaomegainfosys.com"
|
||||
"client.alphaomegainfosys.com",
|
||||
"client-1.alphaomegainfosys.com",
|
||||
"client-2.alphaomegainfosys.com"
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
@ -87,6 +112,12 @@
|
||||
"development": {
|
||||
"buildTarget": "client-app:build:development",
|
||||
"port": 5173
|
||||
},
|
||||
"dev-1": {
|
||||
"buildTarget": "client-app:build:dev-1"
|
||||
},
|
||||
"dev-2": {
|
||||
"buildTarget": "client-app:build:dev-2"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
@ -110,6 +141,7 @@
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"@angular/material/prebuilt-themes/azure-blue.css",
|
||||
"src/styles.scss"
|
||||
]
|
||||
}
|
||||
|
||||
11
package-lock.json
generated
11
package-lock.json
generated
@ -20,6 +20,7 @@
|
||||
"@angular/router": "^20.0.0",
|
||||
"@angular/ssr": "^20.0.4",
|
||||
"chart.js": "^4.3.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"express": "^5.1.0",
|
||||
"ng2-charts": "^8.0.0",
|
||||
"ngx-cookie-service": "^20.0.1",
|
||||
@ -4464,6 +4465,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
}
|
||||
},
|
||||
"node_modules/date-format": {
|
||||
"version": "4.0.14",
|
||||
"resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz",
|
||||
|
||||
@ -33,6 +33,7 @@
|
||||
"@angular/router": "^20.0.0",
|
||||
"@angular/ssr": "^20.0.4",
|
||||
"chart.js": "^4.3.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"express": "^5.1.0",
|
||||
"ng2-charts": "^8.0.0",
|
||||
"ngx-cookie-service": "^20.0.1",
|
||||
|
||||
@ -4,10 +4,10 @@ html {
|
||||
color-scheme: light;
|
||||
|
||||
/* Primary palette variables */
|
||||
--mat-sys-primary: light-dark(#486363, #afcccc);
|
||||
--mat-sys-primary: light-dark(#607c7c, #afcccc);
|
||||
--mat-sys-on-primary: light-dark(#ffffff, #193535);
|
||||
--mat-sys-primary-container: light-dark(#cae8e8, #304b4c);
|
||||
--mat-sys-on-primary-container: light-dark(#022020, #cae8e8);
|
||||
--mat-sys-primary-container: light-dark(#ffffff, #304b4c);
|
||||
--mat-sys-on-primary-container: light-dark(#607c7c, #cae8e8);
|
||||
--mat-sys-inverse-primary: light-dark(#afcccc, #486363);
|
||||
--mat-sys-primary-fixed: light-dark(#cae8e8, #cae8e8);
|
||||
--mat-sys-primary-fixed-dim: light-dark(#afcccc, #afcccc);
|
||||
@ -15,10 +15,10 @@ html {
|
||||
--mat-sys-on-primary-fixed-variant: light-dark(#304b4c, #304b4c);
|
||||
|
||||
/* Secondary palette variables */
|
||||
--mat-sys-secondary: light-dark(#4c6266, #b3cbcf);
|
||||
--mat-sys-secondary: light-dark(#607c7c, #b3cbcf);
|
||||
--mat-sys-on-secondary: light-dark(#ffffff, #1d3437);
|
||||
--mat-sys-secondary-container: light-dark(#cee7eb, #344a4e);
|
||||
--mat-sys-on-secondary-container: light-dark(#071f22, #cee7eb);
|
||||
--mat-sys-secondary-container: light-dark(#607c7c, #344a4e);
|
||||
--mat-sys-on-secondary-container: light-dark(#ffffff, #cee7eb);
|
||||
--mat-sys-secondary-fixed: light-dark(#cee7eb, #cee7eb);
|
||||
--mat-sys-secondary-fixed-dim: light-dark(#b3cbcf, #b3cbcf);
|
||||
--mat-sys-on-secondary-fixed: light-dark(#071f22, #071f22);
|
||||
@ -53,7 +53,8 @@ html {
|
||||
--mat-sys-inverse-on-surface: light-dark(#f1f0f0, #2f3130);
|
||||
--mat-sys-outline: light-dark(#727878, #8b9292);
|
||||
--mat-sys-outline-variant: light-dark(#c1c8c7, #414848);
|
||||
--mat-sys-neutral10: light-dark(#1a1c1c, #1a1c1c); /* Variable used for the form field native select option text color */
|
||||
--mat-sys-neutral10: light-dark(#1a1c1c, #1a1c1c);
|
||||
/* Variable used for the form field native select option text color */
|
||||
|
||||
/* Error palette variables */
|
||||
--mat-sys-error: light-dark(#ba1a1a, #ffb4ab);
|
||||
@ -64,16 +65,22 @@ html {
|
||||
/* Neutral variant palette variables */
|
||||
--mat-sys-surface-variant: light-dark(#dde4e3, #414848);
|
||||
--mat-sys-on-surface-variant: light-dark(#414848, #c1c8c7);
|
||||
--mat-sys-neutral-variant20: light-dark(#2b3232, #2b3232); /* Variable used for the sidenav scrim (container background shadow when opened) */
|
||||
--mat-sys-neutral-variant20: light-dark(#2b3232, #2b3232);
|
||||
/* Variable used for the sidenav scrim (container background shadow when opened) */
|
||||
|
||||
/* TYPOGRAPHY SYSTEM VARIABLES */
|
||||
|
||||
/* Typography variables. Only used in the different typescale system variables. */
|
||||
--mat-sys-brand-font-family: Roboto; /* The font-family to use for brand text. */
|
||||
--mat-sys-plain-font-family: Roboto; /* The font-family to use for plain text. */
|
||||
--mat-sys-bold-font-weight: 700; /* The font-weight to use for bold text. */
|
||||
--mat-sys-medium-font-weight: 500; /* The font-weight to use for medium text. */
|
||||
--mat-sys-regular-font-weight: 400; /* The font-weight to use for regular text. */
|
||||
--mat-sys-brand-font-family: Roboto;
|
||||
/* The font-family to use for brand text. */
|
||||
--mat-sys-plain-font-family: Roboto;
|
||||
/* The font-family to use for plain text. */
|
||||
--mat-sys-bold-font-weight: 700;
|
||||
/* The font-weight to use for bold text. */
|
||||
--mat-sys-medium-font-weight: 500;
|
||||
/* The font-weight to use for medium text. */
|
||||
--mat-sys-regular-font-weight: 400;
|
||||
/* The font-weight to use for regular text. */
|
||||
|
||||
/* Typescale variables. */
|
||||
/* Warning: Risk of reduced fidelity from using the composite typography tokens (ex. --mat-sys-body-large) since
|
||||
@ -233,4 +240,13 @@ html {
|
||||
--mat-sys-focus-state-layer-opacity: 0.12;
|
||||
--mat-sys-hover-state-layer-opacity: 0.08;
|
||||
--mat-sys-pressed-state-layer-opacity: 0.12;
|
||||
|
||||
/* CUSTOM BUTTON COLORS */
|
||||
--custom-button-color: #ffffff;
|
||||
--custom-button-background-color: #627f9a;
|
||||
--custom-hover-color: #384857;
|
||||
|
||||
/* CUSTOM NAV COLORS */
|
||||
--custom-nav-color: #ffffff;
|
||||
--custom-nav-background-color: #607c7c;
|
||||
}
|
||||
@ -1,13 +1,13 @@
|
||||
/* Note: Color palettes are generated from primary: #103454, secondary: #18749c */
|
||||
/* Note: Color palettes are generated from primary: #003057, secondary: #18749c */
|
||||
html {
|
||||
/* COLOR SYSTEM VARIABLES */
|
||||
color-scheme: light;
|
||||
|
||||
/* Primary palette variables */
|
||||
--mat-sys-primary: light-dark(#416183, #aac9f1);
|
||||
--mat-sys-primary: light-dark(#003057, #aac9f1);
|
||||
--mat-sys-on-primary: light-dark(#ffffff, #0d3252);
|
||||
--mat-sys-primary-container: light-dark(#d1e4ff, #29496a);
|
||||
--mat-sys-on-primary-container: light-dark(#001d35, #d1e4ff);
|
||||
--mat-sys-primary-container: light-dark(#ffffff, #29496a);
|
||||
--mat-sys-on-primary-container: light-dark(#003057, #d1e4ff);
|
||||
--mat-sys-inverse-primary: light-dark(#aac9f1, #416183);
|
||||
--mat-sys-primary-fixed: light-dark(#d1e4ff, #d1e4ff);
|
||||
--mat-sys-primary-fixed-dim: light-dark(#aac9f1, #aac9f1);
|
||||
@ -15,10 +15,10 @@ html {
|
||||
--mat-sys-on-primary-fixed-variant: light-dark(#29496a, #29496a);
|
||||
|
||||
/* Secondary palette variables */
|
||||
--mat-sys-secondary: light-dark(#00658b, #84cffc);
|
||||
--mat-sys-secondary: light-dark(#003057, #84cffc);
|
||||
--mat-sys-on-secondary: light-dark(#ffffff, #00344a);
|
||||
--mat-sys-secondary-container: light-dark(#c5e7ff, #004c6a);
|
||||
--mat-sys-on-secondary-container: light-dark(#001e2d, #c5e7ff);
|
||||
--mat-sys-secondary-container: light-dark(#003057, #004c6a);
|
||||
--mat-sys-on-secondary-container: light-dark(#ffffff, #c5e7ff);
|
||||
--mat-sys-secondary-fixed: light-dark(#c5e7ff, #c5e7ff);
|
||||
--mat-sys-secondary-fixed-dim: light-dark(#84cffc, #84cffc);
|
||||
--mat-sys-on-secondary-fixed: light-dark(#001e2d, #001e2d);
|
||||
@ -53,7 +53,8 @@ html {
|
||||
--mat-sys-inverse-on-surface: light-dark(#f1f0f3, #2f3033);
|
||||
--mat-sys-outline: light-dark(#73777e, #8d9198);
|
||||
--mat-sys-outline-variant: light-dark(#c3c7ce, #43474e);
|
||||
--mat-sys-neutral10: light-dark(#1a1c1e, #1a1c1e); /* Variable used for the form field native select option text color */
|
||||
--mat-sys-neutral10: light-dark(#1a1c1e, #1a1c1e);
|
||||
/* Variable used for the form field native select option text color */
|
||||
|
||||
/* Error palette variables */
|
||||
--mat-sys-error: light-dark(#ba1a1a, #ffb4ab);
|
||||
@ -64,16 +65,22 @@ html {
|
||||
/* Neutral variant palette variables */
|
||||
--mat-sys-surface-variant: light-dark(#dfe2eb, #43474e);
|
||||
--mat-sys-on-surface-variant: light-dark(#43474e, #c3c7ce);
|
||||
--mat-sys-neutral-variant20: light-dark(#2c3137, #2c3137); /* Variable used for the sidenav scrim (container background shadow when opened) */
|
||||
--mat-sys-neutral-variant20: light-dark(#2c3137, #2c3137);
|
||||
/* Variable used for the sidenav scrim (container background shadow when opened) */
|
||||
|
||||
/* TYPOGRAPHY SYSTEM VARIABLES */
|
||||
|
||||
/* Typography variables. Only used in the different typescale system variables. */
|
||||
--mat-sys-brand-font-family: Roboto; /* The font-family to use for brand text. */
|
||||
--mat-sys-plain-font-family: Roboto; /* The font-family to use for plain text. */
|
||||
--mat-sys-bold-font-weight: 700; /* The font-weight to use for bold text. */
|
||||
--mat-sys-medium-font-weight: 500; /* The font-weight to use for medium text. */
|
||||
--mat-sys-regular-font-weight: 400; /* The font-weight to use for regular text. */
|
||||
--mat-sys-brand-font-family: Roboto;
|
||||
/* The font-family to use for brand text. */
|
||||
--mat-sys-plain-font-family: Roboto;
|
||||
/* The font-family to use for plain text. */
|
||||
--mat-sys-bold-font-weight: 700;
|
||||
/* The font-weight to use for bold text. */
|
||||
--mat-sys-medium-font-weight: 500;
|
||||
/* The font-weight to use for medium text. */
|
||||
--mat-sys-regular-font-weight: 400;
|
||||
/* The font-weight to use for regular text. */
|
||||
|
||||
/* Typescale variables. */
|
||||
/* Warning: Risk of reduced fidelity from using the composite typography tokens (ex. --mat-sys-body-large) since
|
||||
@ -233,4 +240,13 @@ html {
|
||||
--mat-sys-focus-state-layer-opacity: 0.12;
|
||||
--mat-sys-hover-state-layer-opacity: 0.08;
|
||||
--mat-sys-pressed-state-layer-opacity: 0.12;
|
||||
|
||||
/* CUSTOM BUTTON COLORS */
|
||||
--custom-button-color: #fff;
|
||||
--custom-button-background-color: #FF6A00;
|
||||
--custom-hover-color: #FFAC70;
|
||||
|
||||
/* CUSTOM NAV COLORS */
|
||||
--custom-nav-color: #003057;
|
||||
--custom-nav-background-color: #ffffff;
|
||||
}
|
||||
@ -4,4 +4,4 @@
|
||||
<router-outlet></router-outlet>
|
||||
</main>
|
||||
|
||||
<app-footer></app-footer>
|
||||
<app-footer [isUserLoggedIn]="isUserLoggedIn" ></app-footer>
|
||||
@ -6,6 +6,7 @@ import { provideClientHydration, withEventReplay } from '@angular/platform-brows
|
||||
import { provideCharts, withDefaultRegisterables } from 'ng2-charts';
|
||||
import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
|
||||
import { AuthInterceptor } from './core/interceptors/auth.interceptor';
|
||||
import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
@ -16,5 +17,6 @@ export const appConfig: ApplicationConfig = {
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
provideCharts(withDefaultRegisterables()),
|
||||
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
|
||||
{ provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { subscriptSizing: 'dynamic' } }
|
||||
]
|
||||
};
|
||||
|
||||
@ -7,6 +7,15 @@ export const serverRoutes: ServerRoute[] = [
|
||||
{ path: ':appId/usersettings', renderMode: RenderMode.Client },
|
||||
{ path: ':appId/add-carnet', renderMode: RenderMode.Client },
|
||||
{ path: ':appId/edit-carnet/:headerid', renderMode: RenderMode.Client },
|
||||
{ path: ':appId/view-carnet/:headerid', renderMode: RenderMode.Client },
|
||||
{ path: ':appId/view-extend-carnet/:headerid', renderMode: RenderMode.Client },
|
||||
{ path: ':appId/duplicate-carnet/:headerid', renderMode: RenderMode.Client },
|
||||
{ path: ':appId/extend-carnet/:headerid', renderMode: RenderMode.Client },
|
||||
{ path: ':appId/additional-sets/:headerid', renderMode: RenderMode.Client },
|
||||
{ path: ':appId/add-holder', renderMode: RenderMode.Client },
|
||||
{ path: ':appId/edit-holder/:holderid', renderMode: RenderMode.Client },
|
||||
{ path: ':appId/terms', renderMode: RenderMode.Client },
|
||||
{ path: ':appId/checkout', renderMode: RenderMode.Client },
|
||||
{
|
||||
path: '**',
|
||||
renderMode: RenderMode.Prerender
|
||||
|
||||
@ -15,6 +15,15 @@ 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-holder.component').then(m => m.AddHolderComponent) },
|
||||
{ path: 'edit-holder/:holderid', loadComponent: () => import('./holder/edit/edit-holder.component').then(m => m.EditHolderComponent) },
|
||||
{ path: 'view-carnet/:headerid', loadComponent: () => import('./carnet/view/view-carnet.component').then(m => m.ViewCarnetComponent) },
|
||||
{ path: 'view-extend-carnet/:headerid', loadComponent: () => import('./carnet/view/view-carnet.component').then(m => m.ViewCarnetComponent) },
|
||||
{ path: 'duplicate-carnet/:headerid', loadComponent: () => import('./carnet/edit/edit-carnet.component').then(m => m.EditCarnetComponent) },
|
||||
{ path: 'extend-carnet/:headerid', loadComponent: () => import('./carnet/edit/edit-carnet.component').then(m => m.EditCarnetComponent) },
|
||||
{ path: 'additional-sets/:headerid', loadComponent: () => import('./carnet/edit/edit-carnet.component').then(m => m.EditCarnetComponent) },
|
||||
{ path: 'terms', loadComponent: () => import('./carnet/terms-conditions/terms-conditions.component').then(m => m.TermsConditionsComponent) },
|
||||
{ path: 'checkout', loadComponent: () => import('./carnet/checkout/checkout.component').then(m => m.CheckoutComponent) },
|
||||
{ path: '', redirectTo: 'home', pathMatch: 'full' }
|
||||
],
|
||||
canActivate: [AuthGuard, AppIdGuard]
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
<mat-step *ngIf="applicationType === 'new'" [completed]="stepsCompleted.applicationDetail"
|
||||
[editable]="stepsCompleted.applicationDetail">
|
||||
<ng-template matStepLabel>Application Name</ng-template>
|
||||
<app-application [isEditMode]="isEditMode" (headerIdCreated)="onApplicationDetailCreated($event)">
|
||||
<app-application [isEditMode]="isEditMode" (applicationCreated)="onApplicationDetailCreated($event)">
|
||||
</app-application>
|
||||
</mat-step>
|
||||
|
||||
@ -15,7 +15,8 @@
|
||||
<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 (completed)="onHolderSelectionSaved($event)" [headerid]="headerid"
|
||||
[applicationName]="applicationName" (updated)="onHolderSelectionUpdated($event)">
|
||||
</app-holder>
|
||||
</mat-step>
|
||||
|
||||
@ -39,8 +40,14 @@
|
||||
<!-- Shipping & Payment Step -->
|
||||
<mat-step [completed]="stepsCompleted.shipping" [editable]="stepsCompleted.applicationDetail">
|
||||
<ng-template matStepLabel>Shipping & Payment</ng-template>
|
||||
<app-shipping (completed)="onShippingSaved($event)" [headerid]="headerid">
|
||||
<app-shipping (completed)="onShippingSaved($event)" [headerid]="headerid"
|
||||
[applicationName]="applicationName" [missingSectionMessage]="missingSectionMessage">
|
||||
</app-shipping>
|
||||
</mat-step>
|
||||
|
||||
<!-- Icon overrides. -->
|
||||
<ng-template matStepperIcon="edit">
|
||||
<mat-icon>done</mat-icon>
|
||||
</ng-template>
|
||||
</mat-stepper>
|
||||
</div>
|
||||
@ -1,5 +1,5 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { Component, inject, ViewChild } from '@angular/core';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { StepperSelectionEvent } from '@angular/cdk/stepper';
|
||||
@ -25,7 +25,9 @@ export class AddCarnetComponent {
|
||||
applicationType: 'new' | 'additional' | 'duplicate' | 'extend' | null = 'new';
|
||||
isEditMode = false;
|
||||
headerid: number = 0;
|
||||
applicationName: string = '';
|
||||
userPreferences: UserPreferences;
|
||||
missingSectionMessage: string = '';
|
||||
|
||||
// Track completion of each step
|
||||
stepsCompleted = {
|
||||
@ -36,6 +38,12 @@ export class AddCarnetComponent {
|
||||
shipping: false
|
||||
};
|
||||
|
||||
@ViewChild(HolderComponent, { static: false })
|
||||
private holderComponent!: HolderComponent;
|
||||
|
||||
@ViewChild(ShippingComponent, { static: false })
|
||||
private shippingComponent!: ShippingComponent;
|
||||
|
||||
constructor(userPrefenceService: UserPreferencesService) {
|
||||
this.userPreferences = userPrefenceService.getPreferences();
|
||||
}
|
||||
@ -44,25 +52,74 @@ export class AddCarnetComponent {
|
||||
this.currentStep = event.selectedIndex;
|
||||
}
|
||||
|
||||
onApplicationDetailCreated(headerid: number): void {
|
||||
this.headerid = headerid;
|
||||
onApplicationDetailCreated(data: { headerid: number, applicationName: string }): void {
|
||||
this.headerid = data.headerid;
|
||||
this.applicationName = data.applicationName;
|
||||
this.stepsCompleted.applicationDetail = true;
|
||||
this.isLinear = false; // Disable linear mode after application detail is created
|
||||
|
||||
this.holderComponent.refreshHolder();
|
||||
}
|
||||
|
||||
onHolderSelectionSaved(completed: boolean): void {
|
||||
this.stepsCompleted.holderSelection = completed;
|
||||
this.isAllSectionsCompleted();
|
||||
}
|
||||
|
||||
onHolderSelectionUpdated(updated: boolean): void {
|
||||
if (updated) {
|
||||
this.shippingComponent?.refreshShippingData();
|
||||
}
|
||||
}
|
||||
|
||||
onGoodsSectionSaved(completed: boolean): void {
|
||||
this.stepsCompleted.goodsSection = completed;
|
||||
this.isAllSectionsCompleted();
|
||||
}
|
||||
|
||||
onTravelPlanSaved(completed: boolean): void {
|
||||
this.stepsCompleted.travelPlan = completed;
|
||||
this.isAllSectionsCompleted();
|
||||
}
|
||||
|
||||
onShippingSaved(completed: boolean): void {
|
||||
this.stepsCompleted.shipping = completed;
|
||||
this.isAllSectionsCompleted();
|
||||
}
|
||||
|
||||
isAllSectionsCompleted(): void {
|
||||
let allSectionsCompleted = this.stepsCompleted.applicationDetail
|
||||
&& this.stepsCompleted.holderSelection && this.stepsCompleted.goodsSection
|
||||
&& this.stepsCompleted.shipping && this.stepsCompleted.travelPlan;
|
||||
|
||||
if (allSectionsCompleted) {
|
||||
this.missingSectionMessage = '';
|
||||
}
|
||||
else {
|
||||
let message = 'Missing details in: ';
|
||||
|
||||
if (!this.stepsCompleted.applicationDetail) {
|
||||
message += 'Application Name, ';
|
||||
}
|
||||
|
||||
if (!this.stepsCompleted.holderSelection) {
|
||||
message += 'Holder Selection, ';
|
||||
}
|
||||
|
||||
if (!this.stepsCompleted.goodsSection) {
|
||||
message += 'Goods Section, ';
|
||||
}
|
||||
|
||||
if (!this.stepsCompleted.travelPlan) {
|
||||
message += 'Travel Plan, ';
|
||||
}
|
||||
|
||||
if (!this.stepsCompleted.shipping) {
|
||||
message += 'Shipping & Payment, ';
|
||||
}
|
||||
|
||||
// Remove trailing comma and space
|
||||
this.missingSectionMessage = message.replace(/, $/, '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,13 +15,13 @@
|
||||
Name is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="f['name'].errors?.['maxlength']">
|
||||
Maximum 100 characters allowed
|
||||
Maximum 50 characters allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button mat-raised-button color="primary" type="submit"
|
||||
<button mat-raised-button color="primary" type="submit" *ngIf="!isViewMode"
|
||||
[disabled]="applicationDetailsForm.invalid || disableSaveButton ">
|
||||
Save
|
||||
</button>
|
||||
|
||||
@ -11,6 +11,10 @@
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
|
||||
mat-card-content {
|
||||
padding: inherit 0;
|
||||
}
|
||||
|
||||
.loading-shade {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
||||
@ -17,10 +17,11 @@ import { ActivatedRoute } from '@angular/router';
|
||||
})
|
||||
export class ApplicationComponent {
|
||||
@Input() isEditMode = false;
|
||||
@Input() isViewMode = false;
|
||||
@Input() headerid: number = 0;
|
||||
@Input() applicationName: string = '';
|
||||
|
||||
@Output() headerIdCreated = new EventEmitter<number>();
|
||||
@Output() applicationCreated = new EventEmitter<{ headerid: number, applicationName: string }>();
|
||||
|
||||
applicationDetailsForm: FormGroup;
|
||||
isLoading = false;
|
||||
@ -39,8 +40,6 @@ export class ApplicationComponent {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.applicationName = this.route.snapshot.queryParamMap.get('applicationname') || '';
|
||||
|
||||
// Patch edit form data
|
||||
if (this.applicationName) {
|
||||
this.patchFormData();
|
||||
@ -69,7 +68,7 @@ export class ApplicationComponent {
|
||||
|
||||
createForm(): FormGroup {
|
||||
return this.fb.group({
|
||||
name: ['', [Validators.required, Validators.maxLength(100)]],
|
||||
name: ['', [Validators.required, Validators.maxLength(50)]],
|
||||
});
|
||||
}
|
||||
|
||||
@ -78,7 +77,7 @@ export class ApplicationComponent {
|
||||
name: this.applicationName,
|
||||
});
|
||||
|
||||
if (this.isEditMode) {
|
||||
if (this.isEditMode || this.isViewMode) {
|
||||
this.applicationDetailsForm.get('name')?.disable();
|
||||
this.disableSaveButton = true;
|
||||
}
|
||||
@ -102,7 +101,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.applicationCreated.emit({ headerid: +applicationData.HEADERID, applicationName: applicationDetailData.name });
|
||||
this.applicationDetailsForm.get('name')?.disable();
|
||||
this.disableSaveButton = true;
|
||||
this.isLoading = false;
|
||||
|
||||
61
src/app/carnet/checkout/checkout.component.html
Normal file
61
src/app/carnet/checkout/checkout.component.html
Normal file
@ -0,0 +1,61 @@
|
||||
<div class="checkout-container">
|
||||
<mat-card class="card-body" appearance="outlined">
|
||||
<mat-card-header>
|
||||
<mat-card-title>Checkout</mat-card-title>
|
||||
</mat-card-header>
|
||||
|
||||
<mat-card-content>
|
||||
<!-- Carnet Summary -->
|
||||
<div class="order-summary">
|
||||
<div *ngIf="estimatedFees.basicFee" class="summary-item">
|
||||
<span>Basic fee: </span>
|
||||
<span>{{estimatedFees.basicFee | currency}}</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="estimatedFees.counterFoilFee" class="summary-item">
|
||||
<span>Counterfoil fee: </span>
|
||||
<span>{{estimatedFees.counterFoilFee | currency}}</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="estimatedFees.continuationSheetFee" class="summary-item">
|
||||
<span>Continuation sheet fee: </span>
|
||||
<span>{{estimatedFees.continuationSheetFee | currency}}</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="estimatedFees.expeditedFee" class="summary-item">
|
||||
<span>Expedited fee: </span>
|
||||
<span>{{estimatedFees.expeditedFee | currency}}</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="estimatedFees.shippingFee" class="summary-item">
|
||||
<span>Shipping fee: </span>
|
||||
<span>{{estimatedFees.shippingFee | currency}}</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="estimatedFees.bondPremium" class="summary-item">
|
||||
<span>{{bondPremiumLabel}}: </span>
|
||||
<span>{{estimatedFees.bondPremium | currency}}</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="estimatedFees.cargoPremium" class="summary-item">
|
||||
<span>Cargo Premium: </span>
|
||||
<span>{{estimatedFees.cargoPremium | currency}}</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="estimatedFees.ldiPremium" class="summary-item">
|
||||
<span>LDI Premium: </span>
|
||||
<span>{{estimatedFees.ldiPremium | currency}}</span>
|
||||
</div>
|
||||
|
||||
<div class="summary-item total">
|
||||
<strong>Total:</strong>
|
||||
<strong>{{ orderTotal | currency }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="paypal-container">
|
||||
<div #paypalcontrol></div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
36
src/app/carnet/checkout/checkout.component.scss
Normal file
36
src/app/carnet/checkout/checkout.component.scss
Normal file
@ -0,0 +1,36 @@
|
||||
.checkout-container {
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1rem;
|
||||
|
||||
.mat-mdc-card-header {
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid #ddd;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
mat-card-title {
|
||||
color: var(--mat-sys-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.order-summary {
|
||||
.summary-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.summary-item.total {
|
||||
border-top: 1px solid #ddd;
|
||||
padding-top: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.paypal-container {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
}
|
||||
207
src/app/carnet/checkout/checkout.component.ts
Normal file
207
src/app/carnet/checkout/checkout.component.ts
Normal file
@ -0,0 +1,207 @@
|
||||
import { Component, ElementRef, inject, ViewChild } from '@angular/core';
|
||||
import { PaymentService } from '../../core/services/carnet/payment.service';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Fees } from '../../core/models/carnet/fee';
|
||||
import { CarnetService } from '../../core/services/carnet/carnet.service';
|
||||
import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service';
|
||||
import { NavigationService } from '../../core/services/common/navigation.service';
|
||||
import { NotificationService } from '../../core/services/common/notification.service';
|
||||
import { StorageService } from '../../core/services/common/storage.service';
|
||||
import { finalize } from 'rxjs';
|
||||
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
|
||||
import { PaymentDetail } from '../../core/models/carnet/payment-details';
|
||||
|
||||
// This is necessary to tell TypeScript that a global 'paypal' object exists.
|
||||
declare var paypal: any;
|
||||
|
||||
@Component({
|
||||
selector: 'app-checkout',
|
||||
imports: [CommonModule, AngularMaterialModule],
|
||||
templateUrl: './checkout.component.html',
|
||||
styleUrl: './checkout.component.scss'
|
||||
})
|
||||
export class CheckoutComponent {
|
||||
paymentCompleted: boolean = false;
|
||||
paymentError: boolean = false;
|
||||
isLoading: boolean = false;
|
||||
paymentDetails: any = null;
|
||||
currentApplicationDetails: { headerid: number, applicationName: string, applicationType: string } | null = null;
|
||||
changeInProgress: boolean = false;
|
||||
estimatedFees: Fees = {};
|
||||
bondPremiumLabel: string = 'Bond Premium';
|
||||
|
||||
@ViewChild('paypalcontrol', { static: true }) paypalElement!: ElementRef;
|
||||
|
||||
orderDetails: PaymentDetail = {};
|
||||
|
||||
orderTotal = 0.00;
|
||||
currency = 'USD';
|
||||
|
||||
private paymentService = inject(PaymentService);
|
||||
private notificationService = inject(NotificationService);
|
||||
private errorHandler = inject(ApiErrorHandlerService);
|
||||
private storageService = inject(StorageService);
|
||||
private navigationService = inject(NavigationService);
|
||||
private carnetService = inject(CarnetService);
|
||||
|
||||
constructor() {
|
||||
this.currentApplicationDetails = this.storageService.get<{ headerid: number, applicationName: string, applicationType: string }>('currentapplication')
|
||||
this.orderDetails.headerid = this.currentApplicationDetails?.headerid;
|
||||
this.orderDetails.applicationName = this.currentApplicationDetails?.applicationName;
|
||||
this.orderDetails.description = ` 'Carnet Application - ${this.currentApplicationDetails?.headerid} - ${this.currentApplicationDetails?.applicationName}`
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.currentApplicationDetails?.headerid) {
|
||||
this.carnetService.getEstimatedFees(this.currentApplicationDetails?.headerid).subscribe({
|
||||
next: (data: Fees) => {
|
||||
if (data) {
|
||||
this.estimatedFees = data;
|
||||
|
||||
if (this.estimatedFees.securityType === 'B') {
|
||||
this.bondPremiumLabel = 'Bond Premium';
|
||||
} else if (this.estimatedFees.securityType === 'C') {
|
||||
this.bondPremiumLabel = 'Cash Deposit';
|
||||
} else if (this.estimatedFees.securityType === 'G') {
|
||||
this.bondPremiumLabel = 'Refundable claim deposit';
|
||||
}
|
||||
|
||||
this.calculateTotal();
|
||||
}
|
||||
},
|
||||
error: (error: any) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to get estimated fees');
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error getting estimated fees:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.initConfig();
|
||||
}
|
||||
|
||||
initConfig(): void {
|
||||
paypal
|
||||
.Buttons({
|
||||
style: {
|
||||
layout: 'vertical', // or 'horizontal'
|
||||
color: 'gold',
|
||||
shape: 'pill',
|
||||
label: 'pay',
|
||||
height: 35 // Height in pixels (default is ~35-45px)
|
||||
|
||||
// 'paypal' – Standard PayPal button [defoult]
|
||||
// 'checkout' – "Checkout with PayPal"
|
||||
// 'buynow' – "Buy Now with PayPal"
|
||||
// 'pay' – "Pay with PayPal"
|
||||
// 'installment' – Displays installment information
|
||||
// 'subscribe' – For subscriptions
|
||||
// 'donate' – For donations
|
||||
// pay : Pay with [PayPal]
|
||||
// buynow : [PayPal] Buy Now
|
||||
// Error: Expected style.height to be between 25px and 55px - got 10px
|
||||
// width width usually doesn’t need to be set or must be reasonable.
|
||||
},
|
||||
// 1. Call your server to set up the transaction
|
||||
createOrder: async (data: any, actions: any) => {
|
||||
try {
|
||||
// Call the createOrder method from your service
|
||||
const orderId = await this.paymentService.createOrder(this.orderDetails);
|
||||
this.orderDetails.orderId = orderId;
|
||||
return orderId;
|
||||
} catch (error) {
|
||||
this.notificationService.showError('Failed to initiate the payment');
|
||||
console.error('Error creating order:', error);
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
// 2. Call your server to finalize the transaction
|
||||
onApprove: async (data: any, actions: any) => {
|
||||
try {
|
||||
// data.orderID is the ID of the transaction from PayPal
|
||||
await this.paymentService.completePayment(this.orderDetails);
|
||||
|
||||
// You can now redirect the user or update the UI
|
||||
this.changeInProgress = true;
|
||||
this.carnetService.submitApplication(this.currentApplicationDetails?.headerid!).pipe(finalize(() => {
|
||||
this.changeInProgress = false;
|
||||
})).subscribe({
|
||||
next: () => {
|
||||
this.notificationService.showSuccess('Application submitted successfully');
|
||||
this.storageService.removeItem('currentapplication');
|
||||
this.navigationService.navigate(["home"]);
|
||||
},
|
||||
error: (error) => {
|
||||
this.notificationService.showError('Failed to submit application');
|
||||
console.error('Error submitting the application', error);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
this.notificationService.showError('Failed to complete the payment');
|
||||
console.error('Error completing the payment:', error);
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
// 3. Handle errors
|
||||
onError: (error: any) => {
|
||||
this.notificationService.showError('Payment transaction failed');
|
||||
|
||||
this.orderDetails.paymentStatus = 'Failed';
|
||||
this.orderDetails.paymentErrorDetails = JSON.stringify(error);
|
||||
this.paymentService.logTransactionDetails(this.orderDetails);
|
||||
console.error('An error occurred during the transaction:', error);
|
||||
},
|
||||
|
||||
// 4. Handle cancellation
|
||||
onCancel: (data: any) => {
|
||||
this.orderDetails.paymentStatus = 'Cancelled';
|
||||
if (this.orderDetails.orderId) {
|
||||
this.paymentService.logTransactionDetails(this.orderDetails);
|
||||
}
|
||||
|
||||
this.onCancel();
|
||||
}
|
||||
}).render(this.paypalElement.nativeElement)
|
||||
.catch((error: any) => {
|
||||
console.error('Failed to render PayPal Buttons:', error);
|
||||
});
|
||||
}
|
||||
|
||||
calculateTotal(): void {
|
||||
const excludedProperties = ['securityAmount', 'insuredValue', 'securityType'];
|
||||
const validFees = Object.entries(this.estimatedFees)
|
||||
.filter(([key]) => !excludedProperties.includes(key))
|
||||
.map(([, value]) => value)
|
||||
.filter((fee): fee is number => typeof fee === 'number');
|
||||
|
||||
this.orderTotal = validFees.reduce((total, fee) => total + fee, 0);
|
||||
this.orderDetails.price = this.orderTotal?.toString() ?? '0.00';
|
||||
}
|
||||
|
||||
onCancel(): void {
|
||||
this.storageService.removeItem('currentapplication');
|
||||
|
||||
let route: string = 'edit-carnet';
|
||||
if (this.currentApplicationDetails?.applicationType === 'duplicate') {
|
||||
route = 'duplicate-carnet';
|
||||
}
|
||||
else if (this.currentApplicationDetails?.applicationType === 'extend') {
|
||||
route = 'extend-carnet';
|
||||
} else if (this.currentApplicationDetails?.applicationType === 'additional') {
|
||||
route = 'additional-sets';
|
||||
}
|
||||
|
||||
this.navigationService.navigate([route, this.currentApplicationDetails?.headerid],
|
||||
{
|
||||
state: { isEditMode: true },
|
||||
queryParams: {
|
||||
applicationname: this.currentApplicationDetails?.applicationName,
|
||||
return: 'shipping'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -3,44 +3,61 @@
|
||||
<mat-stepper orientation="vertical" [linear]="isLinear" (selectionChange)="onStepChange($event)"
|
||||
[selectedIndex]="currentStep">
|
||||
|
||||
<!-- Application Name Step -->
|
||||
<mat-step *ngIf="applicationType === 'new'" [completed]="stepsCompleted.applicationDetail"
|
||||
<!-- Replacement Application Step -->
|
||||
<mat-step *ngIf="applicationType === 'extend'" [completed]="stepsCompleted.extension"
|
||||
[editable]="stepsCompleted.applicationDetail">
|
||||
<ng-template matStepLabel>Extension Application</ng-template>
|
||||
<app-replacement-set (completed)="onExtensionApplicationSaved($event)" [headerid]="headerid"
|
||||
[isEditMode]="isEditMode && applicationType === 'extend'">
|
||||
</app-replacement-set>
|
||||
</mat-step>
|
||||
|
||||
<!-- Application Name Step -->
|
||||
<mat-step [completed]="stepsCompleted.applicationDetail" [editable]="stepsCompleted.applicationDetail">
|
||||
<ng-template matStepLabel>Application Name</ng-template>
|
||||
<app-application [isEditMode]="isEditMode">
|
||||
<app-application [isEditMode]="isEditMode" [applicationName]="applicationName">
|
||||
</app-application>
|
||||
</mat-step>
|
||||
|
||||
<!-- Holder Selection Step -->
|
||||
<mat-step *ngIf="applicationType === 'new'" [completed]="stepsCompleted.holderSelection"
|
||||
[editable]="stepsCompleted.applicationDetail">
|
||||
<mat-step [completed]="stepsCompleted.holderSelection" [editable]="stepsCompleted.applicationDetail">
|
||||
<ng-template matStepLabel>Holder Selection</ng-template>
|
||||
<app-holder (completed)="onHolderSelectionSaved($event)" [headerid]="headerid">
|
||||
<app-holder (completed)="onHolderSelectionSaved($event)" [headerid]="headerid"
|
||||
[isViewMode]="applicationType !== 'edit'" [applicationName]="applicationName"
|
||||
(updated)="onHolderSelectionUpdated($event)">
|
||||
</app-holder>
|
||||
</mat-step>
|
||||
|
||||
<!-- Goods Section Step -->
|
||||
<mat-step *ngIf="applicationType === 'new'" [completed]="stepsCompleted.goodsSection"
|
||||
[editable]="stepsCompleted.applicationDetail">
|
||||
<mat-step [completed]="stepsCompleted.goodsSection" [editable]="stepsCompleted.applicationDetail">
|
||||
<ng-template matStepLabel>Goods Section</ng-template>
|
||||
<app-goods (completed)="onGoodsSectionSaved($event)" [headerid]="headerid" [isEditMode]="isEditMode"
|
||||
<app-goods (completed)="onGoodsSectionSaved($event)" [headerid]="headerid"
|
||||
[isEditMode]="isEditMode && applicationType === 'edit'" [isViewMode]="applicationType !== 'edit'"
|
||||
[userPreferences]="userPreferences">
|
||||
</app-goods>
|
||||
</mat-step>
|
||||
|
||||
<!-- Travel Plan Step -->
|
||||
<mat-step *ngIf="applicationType === 'new' || applicationType === 'extend' || applicationType === 'additional'"
|
||||
[completed]="stepsCompleted.travelPlan" [editable]="stepsCompleted.applicationDetail">
|
||||
<mat-step [completed]="stepsCompleted.travelPlan" [editable]="stepsCompleted.applicationDetail">
|
||||
<ng-template matStepLabel>Travel Plan</ng-template>
|
||||
<app-travel-plan (completed)="onTravelPlanSaved($event)" [headerid]="headerid">
|
||||
<app-travel-plan (completed)="onTravelPlanSaved($event)" [headerid]="headerid"
|
||||
[applicationType]="applicationType" (needsInsurance)="onNeedsInsurance($event)"
|
||||
[isViewMode]="applicationType !== 'edit' && applicationType !== 'additional' && applicationType !== 'extend'">
|
||||
</app-travel-plan>
|
||||
</mat-step>
|
||||
|
||||
<!-- Shipping & Payment Step -->
|
||||
<mat-step [completed]="stepsCompleted.shipping" [editable]="stepsCompleted.applicationDetail">
|
||||
<ng-template matStepLabel>Shipping & Payment</ng-template>
|
||||
<app-shipping (completed)="onShippingSaved($event)" [headerid]="headerid">
|
||||
<app-shipping (completed)="onShippingSaved($event)" [headerid]="headerid" [isEditMode]="isEditMode"
|
||||
[applicationType]="applicationType" [applicationName]="applicationName"
|
||||
[missingSectionMessage]="missingSectionMessage">
|
||||
</app-shipping>
|
||||
</mat-step>
|
||||
|
||||
<!-- Icon overrides. -->
|
||||
<ng-template matStepperIcon="edit">
|
||||
<mat-icon>done</mat-icon>
|
||||
</ng-template>
|
||||
</mat-stepper>
|
||||
</div>
|
||||
@ -1,5 +1,5 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { Component, inject, ViewChild } from '@angular/core';
|
||||
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { ApplicationComponent } from '../application/application.component';
|
||||
@ -11,21 +11,28 @@ 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 { ReplacementSetComponent } from '../replacement-set/replacement-set.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-edit-carnet',
|
||||
imports: [AngularMaterialModule, CommonModule, ReactiveFormsModule,
|
||||
ApplicationComponent, HolderComponent, GoodsComponent, TravelPlanComponent, ShippingComponent],
|
||||
ApplicationComponent, HolderComponent, GoodsComponent, TravelPlanComponent, ShippingComponent, ReplacementSetComponent],
|
||||
templateUrl: './edit-carnet.component.html',
|
||||
styleUrl: './edit-carnet.component.scss'
|
||||
})
|
||||
export class EditCarnetComponent {
|
||||
currentStep = 0;
|
||||
isLinear = false;
|
||||
applicationType: 'new' | 'additional' | 'duplicate' | 'extend' | null = 'new';
|
||||
applicationType: 'new' | 'edit' | 'additional' | 'duplicate' | 'extend' | null = 'edit';
|
||||
isEditMode = true; // Set to true for edit mode
|
||||
headerid: number = 0;
|
||||
userPreferences: UserPreferences;
|
||||
applicationName: string = '';
|
||||
missingSectionMessage: string = '';
|
||||
|
||||
|
||||
@ViewChild(ShippingComponent, { static: false })
|
||||
private shippingComponent!: ShippingComponent;
|
||||
|
||||
// Track completion of each step
|
||||
stepsCompleted = {
|
||||
@ -33,7 +40,8 @@ export class EditCarnetComponent {
|
||||
holderSelection: false,
|
||||
goodsSection: false,
|
||||
travelPlan: false,
|
||||
shipping: false
|
||||
shipping: false,
|
||||
extension: false
|
||||
};
|
||||
|
||||
private route = inject(ActivatedRoute);
|
||||
@ -45,6 +53,35 @@ export class EditCarnetComponent {
|
||||
ngOnInit(): void {
|
||||
const idParam = this.route.snapshot.paramMap.get('headerid');
|
||||
this.headerid = idParam ? parseInt(idParam, 10) : 0;
|
||||
|
||||
this.applicationName = this.route.snapshot.queryParamMap.get('applicationname') || '';
|
||||
|
||||
let returnTo = this.route.snapshot.queryParamMap.get('return');
|
||||
|
||||
// set application type based on the route url
|
||||
const urlSegments = this.route.snapshot.url.map(segment => segment.path);
|
||||
if (urlSegments.includes('duplicate-carnet') || urlSegments.includes('process-duplicate-carnet')) {
|
||||
this.applicationType = 'duplicate';
|
||||
} else if (urlSegments.includes('extend-carnet') || urlSegments.includes('process-extend-carnet')) {
|
||||
this.applicationType = 'extend';
|
||||
}
|
||||
else if (urlSegments.includes('additional-sets') || urlSegments.includes('process-additional-sets')) {
|
||||
this.applicationType = 'additional';
|
||||
} else {
|
||||
this.applicationType = 'edit';
|
||||
}
|
||||
|
||||
if (returnTo === 'holder') {
|
||||
this.currentStep = 1;
|
||||
} else if (returnTo === 'shipping' && this.applicationType === 'extend') {
|
||||
this.currentStep = 5;
|
||||
} else if (returnTo === 'shipping') {
|
||||
this.currentStep = 4;
|
||||
} else if (this.applicationType === 'additional') {
|
||||
this.currentStep = 3;
|
||||
} else if (this.applicationType === 'duplicate') {
|
||||
this.currentStep = 4;
|
||||
}
|
||||
}
|
||||
|
||||
onStepChange(event: StepperSelectionEvent): void {
|
||||
@ -53,17 +90,79 @@ export class EditCarnetComponent {
|
||||
|
||||
onHolderSelectionSaved(completed: boolean): void {
|
||||
this.stepsCompleted.holderSelection = completed;
|
||||
this.isAllSectionsCompleted();
|
||||
}
|
||||
|
||||
onGoodsSectionSaved(completed: boolean): void {
|
||||
this.stepsCompleted.goodsSection = completed;
|
||||
this.isAllSectionsCompleted();
|
||||
}
|
||||
|
||||
onHolderSelectionUpdated(updated: boolean): void {
|
||||
if (updated) {
|
||||
this.shippingComponent?.refreshShippingData();
|
||||
}
|
||||
|
||||
this.isAllSectionsCompleted();
|
||||
}
|
||||
|
||||
onTravelPlanSaved(completed: boolean): void {
|
||||
this.stepsCompleted.travelPlan = completed;
|
||||
this.isAllSectionsCompleted();
|
||||
}
|
||||
|
||||
onShippingSaved(completed: boolean): void {
|
||||
this.stepsCompleted.shipping = completed;
|
||||
this.isAllSectionsCompleted();
|
||||
}
|
||||
|
||||
onNeedsInsurance(data: boolean): void {
|
||||
this.shippingComponent?.refreshInsuranceData(data);
|
||||
}
|
||||
|
||||
onExtensionApplicationSaved(completed: boolean): void {
|
||||
this.stepsCompleted.extension = completed;
|
||||
this.isAllSectionsCompleted();
|
||||
}
|
||||
|
||||
isAllSectionsCompleted() {
|
||||
let allSectionsCompleted = this.stepsCompleted.applicationDetail
|
||||
&& this.stepsCompleted.holderSelection && this.stepsCompleted.goodsSection
|
||||
&& this.stepsCompleted.shipping && this.stepsCompleted.travelPlan
|
||||
&& (this.applicationType !== 'extend' || this.stepsCompleted.extension);
|
||||
|
||||
if (allSectionsCompleted) {
|
||||
this.missingSectionMessage = '';
|
||||
}
|
||||
else {
|
||||
let message = 'Missing details in: ';
|
||||
|
||||
if(this.applicationType === 'extend' && !this.stepsCompleted.extension) {
|
||||
message += 'Extension Application, ';
|
||||
}
|
||||
|
||||
if (!this.stepsCompleted.applicationDetail) {
|
||||
message += 'Application Name, ';
|
||||
}
|
||||
|
||||
if (!this.stepsCompleted.holderSelection) {
|
||||
message += 'Holder Selection, ';
|
||||
}
|
||||
|
||||
if (!this.stepsCompleted.goodsSection) {
|
||||
message += 'Goods Section, ';
|
||||
}
|
||||
|
||||
if (!this.stepsCompleted.travelPlan) {
|
||||
message += 'Travel Plan, ';
|
||||
}
|
||||
|
||||
if (!this.stepsCompleted.shipping) {
|
||||
message += 'Shipping & Payment, ';
|
||||
}
|
||||
|
||||
// Remove trailing comma and space
|
||||
this.missingSectionMessage = message.replace(/, $/, '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,11 +16,16 @@
|
||||
</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>
|
||||
<mat-radio-group formControlName="roadVehiclesUsed" class="radio-group">
|
||||
<mat-label class="question">Are Road vehicle included in your General List?</mat-label>
|
||||
<mat-radio-button value="Y" class="radio-button">Yes</mat-radio-button>
|
||||
<mat-radio-button value="N" class="radio-button">No</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
<mat-radio-group formControlName="horseUsed" class="radio-group">
|
||||
<mat-label class="question">Are Horse included in your General List?</mat-label>
|
||||
<mat-radio-button value="Y" class="radio-button">Yes</mat-radio-button>
|
||||
<mat-radio-button value="N" class="radio-button">No</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
</div>
|
||||
|
||||
<!-- Authorized Representatives Section -->
|
||||
@ -38,12 +43,16 @@
|
||||
<div class="table-actions">
|
||||
<h3>Goods Items</h3>
|
||||
<div class="actions">
|
||||
<button mat-raised-button color="primary" type="button" (click)="addNewItem()">
|
||||
<button mat-raised-button color="primary" type="button" *ngIf="!isViewMode" (click)="addNewItem()">
|
||||
<mat-icon>add</mat-icon> Add Item
|
||||
</button>
|
||||
<button mat-raised-button color="primary" type="button" *ngIf="!isViewMode" (click)="deleteAllItems()">
|
||||
<mat-icon>delete</mat-icon> Delete All
|
||||
</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">
|
||||
<button mat-raised-button type="button" *ngIf="!isViewMode" (click)="fileInput.click()"
|
||||
[disabled]="isProcessing">
|
||||
<mat-icon>upload</mat-icon> Upload
|
||||
</button>
|
||||
</div>
|
||||
@ -77,7 +86,7 @@
|
||||
<!-- 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>
|
||||
<td mat-cell *matCellDef="let item">{{ item.weight | number:'1.2-2' }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Unit of Measure Column -->
|
||||
@ -102,10 +111,12 @@
|
||||
<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">
|
||||
<button mat-icon-button color="primary" *ngIf="!isViewMode" (click)="editItem(item)"
|
||||
matTooltip="Edit">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="warn" (click)="deleteItem(item)" matTooltip="Delete">
|
||||
<button mat-icon-button color="warn" *ngIf="!isViewMode" (click)="deleteItem(item)"
|
||||
matTooltip="Delete">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
@ -122,8 +133,8 @@
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<mat-paginator [length]="dataSource.data.length" [pageSizeOptions]="[userPreferences.pageSize!]"
|
||||
[hidePageSize]="true" showFirstLastButtons></mat-paginator>
|
||||
<mat-paginator *ngIf="dataSource.data.length > userPreferences.pageSize!" [length]="dataSource.data.length"
|
||||
[pageSizeOptions]="[userPreferences.pageSize!]" [hidePageSize]="true" showFirstLastButtons></mat-paginator>
|
||||
</div>
|
||||
|
||||
<!-- Item Form -->
|
||||
@ -219,16 +230,26 @@
|
||||
</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">
|
||||
<button mat-raised-button color="primary" type="submit" *ngIf="!isViewMode"
|
||||
[disabled]="itemForm.invalid || changeInProgress">
|
||||
Save
|
||||
</button>
|
||||
<button mat-button type="button" (click)="cancelEdit()">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!showItemForm" class="form-actions">
|
||||
<button mat-raised-button color="primary" (click)="onSubmit()" [disabled]="goodsForm.invalid">
|
||||
<!-- Totals -->
|
||||
<div class="totals-section">
|
||||
<div class="total-item" *ngIf="totalGoodsValue > 0">
|
||||
<span>Total Goods Value:</span>
|
||||
<span class="total-value">{{totalGoodsValue | currency}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button mat-raised-button color="primary" (click)="onSubmit()" *ngIf="!isViewMode"
|
||||
[disabled]="goodsForm.invalid || changeInProgress">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
.goods-container {
|
||||
padding: 16px;
|
||||
|
||||
.form-section {
|
||||
margin-bottom: 1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
@ -21,6 +20,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
display: flex;
|
||||
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%;
|
||||
}
|
||||
@ -29,7 +44,7 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 0.75rem;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
@ -60,7 +75,6 @@
|
||||
.table-container {
|
||||
position: relative;
|
||||
overflow-x: auto;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.loading-shade {
|
||||
position: absolute;
|
||||
@ -139,6 +153,7 @@
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
align-items: start;
|
||||
|
||||
mat-form-field {
|
||||
flex: 1;
|
||||
@ -164,6 +179,23 @@
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 24px;
|
||||
margin-top: 0.9rem;
|
||||
justify-content: space-between;
|
||||
|
||||
.totals-section {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
font-size: 0.875rem;
|
||||
|
||||
.total-item {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
.total-value {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -16,7 +16,7 @@ import { ApiErrorHandlerService } from '../../core/services/common/api-error-han
|
||||
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 { finalize, Subject, takeUntil } from 'rxjs';
|
||||
import { Goods, GoodsItem } from '../../core/models/carnet/goods';
|
||||
|
||||
@Component({
|
||||
@ -28,12 +28,20 @@ import { Goods, GoodsItem } from '../../core/models/carnet/goods';
|
||||
})
|
||||
export class GoodsComponent {
|
||||
@Input() headerid: number = 0;
|
||||
@Input() isViewMode = false;
|
||||
@Input() userPreferences: UserPreferences = {};
|
||||
@Input() isEditMode: boolean = false;
|
||||
@Output() completed = new EventEmitter<boolean>();
|
||||
|
||||
@ViewChild(MatPaginator) paginator!: MatPaginator;
|
||||
@ViewChild(MatSort) sort!: MatSort;
|
||||
@ViewChild(MatPaginator, { static: false })
|
||||
set paginator(value: MatPaginator) {
|
||||
this.dataSource.paginator = value;
|
||||
}
|
||||
|
||||
@ViewChild(MatSort, { static: false })
|
||||
set sort(value: MatSort) {
|
||||
this.dataSource.sort = value;
|
||||
}
|
||||
|
||||
// Table configuration
|
||||
displayedColumns: string[] = ['itemNumber', 'description', 'pieces', 'weight', 'unitOfMeasure', 'value', 'countryOfOrigin', 'actions'];
|
||||
@ -47,9 +55,11 @@ export class GoodsComponent {
|
||||
isEditing = false;
|
||||
currentItem: GoodsItem | null = null;
|
||||
isLoading = false;
|
||||
changeInProgress = false;
|
||||
showItemForm = false;
|
||||
fileToUpload: File | null = null;
|
||||
isProcessing = false;
|
||||
totalGoodsValue: number = 0;
|
||||
|
||||
// Data
|
||||
countries: Country[] = [];
|
||||
@ -70,8 +80,8 @@ export class GoodsComponent {
|
||||
commercialSample: [false],
|
||||
professionalEquipment: [false],
|
||||
exhibitionsFair: [false],
|
||||
roadVehiclesUsed: [false],
|
||||
horseUsed: [false],
|
||||
roadVehiclesUsed: [''],
|
||||
horseUsed: [''],
|
||||
authorizedRepresentatives: ['', Validators.required]
|
||||
}, { validator: this.atLeastOneRequired }
|
||||
);
|
||||
@ -84,7 +94,7 @@ export class GoodsComponent {
|
||||
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]
|
||||
countryOfOrigin: ['US', Validators.required]
|
||||
});
|
||||
}
|
||||
|
||||
@ -98,45 +108,48 @@ export class GoodsComponent {
|
||||
this.loadUnitOfMeasures();
|
||||
|
||||
if (this.headerid > 0) {
|
||||
this.goodsService.getGoodDetailsByHeaderId(this.headerid).subscribe({
|
||||
this.isLoading = true;
|
||||
this.goodsService.getGoodDetailsByHeaderId(this.headerid).pipe(finalize(() => {
|
||||
this.isLoading = false;
|
||||
})).subscribe({
|
||||
next: (goodsDetail: Goods) => {
|
||||
if (goodsDetail) {
|
||||
this.patchFormData(goodsDetail);
|
||||
this.completed.emit((this.goodsForm.valid || this.goodsForm.disabled) && this.dataSource.data.length > 0);
|
||||
}
|
||||
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({
|
||||
this.goodsService.getGoodsItemsByHeaderId(this.headerid).pipe(finalize(() => {
|
||||
this.isLoading = false;
|
||||
})).subscribe({
|
||||
next: (items: GoodsItem[]) => {
|
||||
this.dataSource.data = items;
|
||||
this.isLoading = false;
|
||||
this.totalGoodsValue = items.reduce((total, item) => total + Number(item.value || 0), 0);
|
||||
this.completed.emit((this.goodsForm.valid || this.goodsForm.disabled) && this.dataSource.data.length > 0);
|
||||
},
|
||||
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)
|
||||
this.commonService.getCountries()
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: (countries) => {
|
||||
@ -144,7 +157,6 @@ export class GoodsComponent {
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Failed to load countries', error);
|
||||
this.isLoading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -155,11 +167,9 @@ export class GoodsComponent {
|
||||
.subscribe({
|
||||
next: (units) => {
|
||||
this.unitsOfMeasures = units;
|
||||
this.isLoading = false;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Failed to load unit of measures', error);
|
||||
this.isLoading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -168,11 +178,15 @@ export class GoodsComponent {
|
||||
this.goodsForm.patchValue({
|
||||
commercialSample: data.commercialSample || false,
|
||||
professionalEquipment: data.professionalEquipment || false,
|
||||
exhibitionsFair: data.exhibitionsFair || false,
|
||||
roadVehiclesUsed: data.roadVehiclesUsed || false,
|
||||
exhibitionsFair: data.exhibitionsFair || '',
|
||||
roadVehiclesUsed: data.roadVehiclesUsed || '',
|
||||
horseUsed: data.horseUsed || false,
|
||||
authorizedRepresentatives: data.authorizedRepresentatives || ''
|
||||
});
|
||||
|
||||
if (this.isViewMode) {
|
||||
this.goodsForm.disable();
|
||||
}
|
||||
}
|
||||
|
||||
// Add new item to the table
|
||||
@ -183,8 +197,10 @@ export class GoodsComponent {
|
||||
this.itemForm.reset({
|
||||
pieces: 0,
|
||||
weight: 0,
|
||||
value: 0
|
||||
value: 0,
|
||||
countryOfOrigin: 'US',
|
||||
});
|
||||
this.itemForm.markAsUntouched();
|
||||
}
|
||||
|
||||
// Edit existing item
|
||||
@ -218,16 +234,19 @@ export class GoodsComponent {
|
||||
let itemDataArray: GoodsItem[] = [];
|
||||
itemDataArray.push(itemData);
|
||||
|
||||
this.changeInProgress = true;
|
||||
|
||||
const saveObservable = this.isEditing && this.currentItem
|
||||
? this.goodsService.updateGoodsItem(this.headerid, itemDataArray)
|
||||
: this.goodsService.addGoodsItem(this.headerid, itemDataArray);
|
||||
|
||||
saveObservable.subscribe({
|
||||
saveObservable.pipe(finalize(() => {
|
||||
this.changeInProgress = false;
|
||||
})).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`);
|
||||
@ -268,18 +287,56 @@ export class GoodsComponent {
|
||||
});
|
||||
}
|
||||
|
||||
// Delete all items with confirmation
|
||||
deleteAllItems(): void {
|
||||
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
|
||||
width: '350px',
|
||||
data: {
|
||||
title: 'Confirm Delete',
|
||||
message: 'Are you sure you want to delete all the items?',
|
||||
confirmText: 'Delete',
|
||||
cancelText: 'Cancel'
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.changeInProgress = true;
|
||||
this.goodsService.deleteAllGoodsItems(this.headerid).pipe(finalize(() => {
|
||||
this.changeInProgress = false;
|
||||
})).subscribe({
|
||||
next: () => {
|
||||
this.notificationService.showSuccess('All items deleted successfully');
|
||||
this.loadGoodsItems();
|
||||
this.cancelEdit();
|
||||
},
|
||||
error: (error) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to delete all items');
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error deleting all items:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle file selection for upload
|
||||
fileUpload(event: any): void {
|
||||
const file: File = event.target.files[0];
|
||||
const fileInput = event.target as HTMLInputElement;
|
||||
const file: File | null = fileInput?.files ? fileInput.files[0] : null;
|
||||
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.processExcelFile(file)
|
||||
.then(response => {
|
||||
console.log('File uploaded successfully:', response);
|
||||
// Handle success (e.g., update UI)
|
||||
.then(() => {
|
||||
fileInput.value = '';
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error processing file:', error);
|
||||
this.notificationService.showError('Failed to upload file');
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
@ -317,12 +374,14 @@ export class GoodsComponent {
|
||||
// item.unitOfMeasure = this.getUnitOfMeasureValue(item.unitOfMeasure);
|
||||
// });
|
||||
|
||||
this.goodsService.addGoodsItem(this.headerid, items).subscribe({
|
||||
this.changeInProgress = true;
|
||||
this.goodsService.addGoodsItem(this.headerid, items).pipe(finalize(() => {
|
||||
this.changeInProgress = false;
|
||||
})).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) => {
|
||||
@ -365,15 +424,20 @@ export class GoodsComponent {
|
||||
|
||||
// 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 =>
|
||||
return data.map(row => {
|
||||
// Convert row to array if it's not already
|
||||
const rowArray = Array.isArray(row) ? row : Object.values(row);
|
||||
|
||||
return {
|
||||
itemNumber: rowArray[0]?.toString().trim() || '',
|
||||
description: rowArray[1]?.toString().trim() || '',
|
||||
pieces: Number(rowArray[2]) || 0,
|
||||
weight: Number(rowArray[3]) || 0,
|
||||
unitOfMeasure: rowArray[4]?.toString().trim() || '',
|
||||
value: Number(rowArray[5]) || 0,
|
||||
countryOfOrigin: rowArray[6]?.toString().trim() || ''
|
||||
};
|
||||
}).filter(item =>
|
||||
item.itemNumber &&
|
||||
item.description &&
|
||||
item.pieces > 0
|
||||
@ -388,9 +452,11 @@ export class GoodsComponent {
|
||||
}
|
||||
|
||||
const formData = this.goodsForm.value;
|
||||
this.isLoading = true;
|
||||
this.changeInProgress = true;
|
||||
|
||||
this.goodsService.saveGoodsData(this.headerid, formData).subscribe({
|
||||
this.goodsService.saveGoodsData(this.headerid, formData).pipe(finalize(() => {
|
||||
this.changeInProgress = false;
|
||||
})).subscribe({
|
||||
next: () => {
|
||||
this.notificationService.showSuccess('Goods information saved successfully');
|
||||
this.completed.emit(this.goodsForm.valid && this.dataSource.data.length > 0);
|
||||
@ -399,9 +465,6 @@ export class GoodsComponent {
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -1 +1,2 @@
|
||||
<p>holder works!</p>
|
||||
<app-holder-search (holderSelectionCompleted)="onHolderSelectionSaved($event)" [applicationName]="applicationName"
|
||||
[headerid]="headerid" [selectedHolderId]="selectedHolderId" [isViewMode]="isViewMode"></app-holder-search>
|
||||
@ -1,12 +1,56 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { Component, EventEmitter, inject, Input, Output } from '@angular/core';
|
||||
import { UserPreferences } from '../../core/models/user-preference';
|
||||
import { SearchHolderComponent } from '../../holder/search/search-holder.component';
|
||||
import { HolderService } from '../../core/services/carnet/holder.service';
|
||||
import { NotificationService } from '../../core/services/common/notification.service';
|
||||
import { Holder } from '../../core/models/carnet/holder';
|
||||
import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-holder',
|
||||
imports: [],
|
||||
imports: [SearchHolderComponent],
|
||||
templateUrl: './holder.component.html',
|
||||
styleUrl: './holder.component.scss'
|
||||
})
|
||||
export class HolderComponent {
|
||||
@Input() isViewMode = false;
|
||||
@Input() headerid: number = 0;
|
||||
@Input() applicationName: string = '';
|
||||
@Input() userPreferences: UserPreferences = {};
|
||||
@Input() isEditMode: boolean = false;
|
||||
@Output() completed = new EventEmitter<boolean>();
|
||||
@Output() updated = new EventEmitter<boolean>();
|
||||
|
||||
selectedHolderId: number | undefined = 0;
|
||||
|
||||
private holdersService = inject(HolderService);
|
||||
private notificationService = inject(NotificationService);
|
||||
private errorHandler = inject(ApiErrorHandlerService);
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.headerid > 0) {
|
||||
this.holdersService.getHolder(this.headerid).subscribe({
|
||||
next: (holder: Holder) => {
|
||||
if (holder) {
|
||||
this.selectedHolderId = holder.holderid;
|
||||
this.completed.emit(true);
|
||||
}
|
||||
},
|
||||
error: (error: any) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to get holder details');
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error getting holder details:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public refreshHolder(): void {
|
||||
this.selectedHolderId = undefined;
|
||||
}
|
||||
|
||||
onHolderSelectionSaved(completed: boolean): void {
|
||||
this.completed.emit(completed);
|
||||
this.updated.emit(true); // to update dependent data
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,98 @@
|
||||
<!-- extension-section.component.html -->
|
||||
<div class="extension-container">
|
||||
<div class="loading-shade" *ngIf="isLoading">
|
||||
<mat-spinner diameter="50"></mat-spinner>
|
||||
</div>
|
||||
|
||||
<!-- General Section -->
|
||||
<div class="general-section">
|
||||
<h3>General</h3>
|
||||
<div class="detail-row">
|
||||
<label>Carnet No:</label>
|
||||
<span>{{ replacementDetails?.replacementCarnetNumber || 'N/A' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-row">
|
||||
<label>Issue Date:</label>
|
||||
<span>{{ replacementDetails?.issueDate | date:'mediumDate':'UTC' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-row">
|
||||
<label>Preparer Name:</label>
|
||||
<span>{{ replacementDetails?.preparerName || 'N/A' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-row">
|
||||
<label>Address:</label>
|
||||
<span>{{ getAddressLabel() || 'N/A' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Extension Section -->
|
||||
<div class="extension-section">
|
||||
<h3>Extension</h3>
|
||||
|
||||
<form [formGroup]="extensionForm" (ngSubmit)="onSubmit()">
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline" class="current-port">
|
||||
<mat-label>Current port where the goods are located</mat-label>
|
||||
<input matInput formControlName="portName">
|
||||
<!-- <mat-error *ngIf="f['portName'].errors?.['required']">
|
||||
Port location is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="f['portName'].errors?.['maxlength']">
|
||||
Maximum 100 characters allowed
|
||||
</mat-error> -->
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="country">
|
||||
<mat-label>Country</mat-label>
|
||||
<mat-select formControlName="portCountry">
|
||||
<mat-option *ngFor="let country of countries" [value]="country.value">
|
||||
{{ country.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<!-- <mat-error *ngIf="f['portCountry'].errors?.['required']">
|
||||
Port Country is required
|
||||
</mat-error> -->
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline" class="reason">
|
||||
<mat-label>Reason for Extension</mat-label>
|
||||
<mat-select formControlName="reasonForExtension" required>
|
||||
<mat-option *ngFor="let reason of extensionReasons" [value]="reason.value">
|
||||
{{ reason.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="f['reasonForExtension'].errors?.['required']">
|
||||
Reason is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="duration">
|
||||
<mat-label>Duration</mat-label>
|
||||
<input matInput type="number" formControlName="duration">
|
||||
<span matTextSuffix>Month(s)</span>
|
||||
<!-- <mat-error *ngIf="f['duration'].errors?.['required']">
|
||||
Duration is required
|
||||
</mat-error> -->
|
||||
<!-- <mat-error *ngIf="f['duration'].errors?.['min']">
|
||||
Minimum 1 month required
|
||||
</mat-error> -->
|
||||
<mat-error *ngIf="f['duration'].errors?.['max']">
|
||||
Maximum 12 months allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button mat-raised-button color="primary" type="submit" *ngIf="!isViewMode"
|
||||
[disabled]="extensionForm.invalid || changeInProgress">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@ -0,0 +1,51 @@
|
||||
.extension-container {
|
||||
padding: 0.5rem;
|
||||
|
||||
.general-section,
|
||||
.extension-section {
|
||||
margin-bottom: 24px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--mat-sys-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
align-items: start;
|
||||
|
||||
mat-form-field {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.current-port {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
172
src/app/carnet/replacement-set/replacement-set.component.ts
Normal file
172
src/app/carnet/replacement-set/replacement-set.component.ts
Normal file
@ -0,0 +1,172 @@
|
||||
import { Component, EventEmitter, inject, Input, Output } from '@angular/core';
|
||||
import { FormGroup, FormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
|
||||
import { finalize, Subject, takeUntil } from 'rxjs';
|
||||
import { Country } from '../../core/models/country';
|
||||
import { CommonService } from '../../core/services/common/common.service';
|
||||
import { Replacement } from '../../core/models/carnet/replacement';
|
||||
import { ReplacementService } from '../../core/services/carnet/replacement.service';
|
||||
import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service';
|
||||
import { NotificationService } from '../../core/services/common/notification.service';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
|
||||
import { ExtensionReason } from '../../core/models/extension-reason';
|
||||
|
||||
@Component({
|
||||
selector: 'app-replacement-set',
|
||||
imports: [AngularMaterialModule, CommonModule, ReactiveFormsModule],
|
||||
templateUrl: './replacement-set.component.html',
|
||||
styleUrl: './replacement-set.component.scss'
|
||||
})
|
||||
export class ReplacementSetComponent {
|
||||
@Input() headerid: number = 0;
|
||||
@Input() isViewMode = false;
|
||||
@Input() isEditMode: boolean = false;
|
||||
@Output() completed = new EventEmitter<boolean>();
|
||||
|
||||
extensionForm: FormGroup;
|
||||
countries: Country[] = [];
|
||||
extensionReasons: ExtensionReason[] = [];
|
||||
replacementDetails: Replacement | undefined;
|
||||
|
||||
// UI state
|
||||
isEditing = false;
|
||||
isLoading = false;
|
||||
changeInProgress = false;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
private fb = inject(FormBuilder);
|
||||
private commonService = inject(CommonService);
|
||||
private notificationService = inject(NotificationService);
|
||||
private replacementService = inject(ReplacementService);
|
||||
private errorHandler = inject(ApiErrorHandlerService);
|
||||
|
||||
constructor(
|
||||
) {
|
||||
this.extensionForm = this.createForm();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadCountries();
|
||||
this.loadExtensionReasons();
|
||||
|
||||
if (this.headerid > 0) {
|
||||
this.isLoading = true;
|
||||
this.replacementService.getExtensionData(this.headerid).pipe(finalize(() => {
|
||||
this.isLoading = false;
|
||||
})).subscribe({
|
||||
next: (replacementData: Replacement) => {
|
||||
if (replacementData) {
|
||||
this.patchFormData(replacementData);
|
||||
this.replacementDetails = replacementData;
|
||||
this.completed.emit(this.extensionForm.valid || this.extensionForm.disabled);
|
||||
}
|
||||
},
|
||||
error: (error: any) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load extension details');
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error loading extension details:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
patchFormData(data: Replacement): void {
|
||||
this.extensionForm.patchValue({
|
||||
portName: data.portName,
|
||||
portCountry: data.portCountry,
|
||||
reasonForExtension: data.reasonForExtension,
|
||||
duration: data.duration
|
||||
});
|
||||
|
||||
if (this.isViewMode) {
|
||||
this.extensionForm.disable();
|
||||
}
|
||||
}
|
||||
|
||||
createForm(): FormGroup {
|
||||
return this.fb.group({
|
||||
portName: [''],
|
||||
portCountry: [''],
|
||||
reasonForExtension: ['', Validators.required],
|
||||
duration: ['', [Validators.max(12)]]
|
||||
});
|
||||
}
|
||||
|
||||
loadCountries(): void {
|
||||
this.commonService.getCountries()
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: (countries) => {
|
||||
this.countries = countries;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Failed to load countries', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadExtensionReasons(): void {
|
||||
this.commonService.getExtensionReasons()
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: (extensionReasons) => {
|
||||
this.extensionReasons = extensionReasons;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Failed to load extension reasons', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onSubmit(): void {
|
||||
if (this.extensionForm.invalid) {
|
||||
this.extensionForm.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = this.extensionForm.value;
|
||||
this.changeInProgress = true;
|
||||
|
||||
this.replacementService.saveExtensionDetails(this.headerid, formData).pipe(finalize(() => {
|
||||
this.changeInProgress = false;
|
||||
})).subscribe({
|
||||
next: () => {
|
||||
this.notificationService.showSuccess('Extension details saved successfully');
|
||||
this.completed.emit(this.extensionForm.valid);
|
||||
},
|
||||
error: (error) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to save extension information');
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error saving extension information:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get f() {
|
||||
return this.extensionForm.controls;
|
||||
}
|
||||
|
||||
getAddressLabel(): string {
|
||||
if (!this.replacementDetails) {
|
||||
return 'No contact information available';
|
||||
}
|
||||
|
||||
// Build name parts, filtering out null/undefined/empty strings
|
||||
const allParts = [
|
||||
this.replacementDetails.address1,
|
||||
this.replacementDetails.address2,
|
||||
this.replacementDetails.city,
|
||||
this.replacementDetails.state,
|
||||
this.replacementDetails.country,
|
||||
this.replacementDetails.zip
|
||||
].filter(part => part != null && part.trim() !== '');
|
||||
|
||||
return allParts.join(', ') || '';
|
||||
}
|
||||
}
|
||||
86
src/app/carnet/shipping/contact-dialog.component.ts
Normal file
86
src/app/carnet/shipping/contact-dialog.component.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-contact-dialog',
|
||||
imports: [AngularMaterialModule, CommonModule, FormsModule],
|
||||
template: `
|
||||
<h2 mat-dialog-title>Select Contact</h2>
|
||||
<mat-dialog-content>
|
||||
<mat-radio-group [(ngModel)]="selectedContact">
|
||||
<mat-radio-button *ngFor="let contact of contacts" [value]="contact">
|
||||
{{getContactLabel(contact)}}
|
||||
</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions>
|
||||
<button mat-raised-button color="primary" (click)="onSelect()" [disabled]="!selectedContact">
|
||||
Select
|
||||
</button>
|
||||
<button mat-button (click)="onCancel()">Cancel</button>
|
||||
</mat-dialog-actions>
|
||||
`,
|
||||
styles: [`
|
||||
mat-radio-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
mat-radio-button {
|
||||
margin: 4px 0;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class ContactDialogComponent {
|
||||
selectedContact: any | null = null;
|
||||
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<ContactDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: { contacts: any[], currentContact: any }
|
||||
) {
|
||||
this.selectedContact = data.currentContact;
|
||||
}
|
||||
|
||||
get contacts() {
|
||||
return this.data.contacts;
|
||||
}
|
||||
|
||||
onSelect(): void {
|
||||
this.dialogRef.close(this.selectedContact);
|
||||
}
|
||||
|
||||
onCancel(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
getContactLabel(contact: any): string {
|
||||
|
||||
if (!contact) {
|
||||
return 'No contact information available';
|
||||
}
|
||||
|
||||
// Build name parts, filtering out null/undefined/empty strings
|
||||
const nameParts = [
|
||||
contact.firstName,
|
||||
contact.middleInitial,
|
||||
contact.lastName
|
||||
].filter(part => part != null && part.trim() !== '');
|
||||
|
||||
// Build contact info parts
|
||||
const contactInfoParts = [
|
||||
contact.email,
|
||||
contact.phone
|
||||
].filter(part => part != null && part.trim() !== '');
|
||||
|
||||
// Combine all non-empty parts
|
||||
const allParts = [
|
||||
nameParts.join(' '), // Join name parts with single spaces
|
||||
...contactInfoParts // Add email and phone if they exist
|
||||
].filter(part => part.trim() !== '');
|
||||
|
||||
return allParts.join(', ') || '';
|
||||
}
|
||||
}
|
||||
116
src/app/carnet/shipping/fees-dialog.component.ts
Normal file
116
src/app/carnet/shipping/fees-dialog.component.ts
Normal file
@ -0,0 +1,116 @@
|
||||
import { Component, inject, Inject } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { CarnetService } from '../../core/services/carnet/carnet.service';
|
||||
import { Fees } from '../../core/models/carnet/fee';
|
||||
import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service';
|
||||
import { NotificationService } from '../../core/services/common/notification.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-fees-dialog',
|
||||
imports: [AngularMaterialModule, CommonModule, FormsModule],
|
||||
template: `
|
||||
<h3 mat-dialog-title>Fees and Charges</h3>
|
||||
<mat-dialog-content>
|
||||
<p> The following fees apply:</p>
|
||||
<ul>
|
||||
<li *ngIf="estimatedFees.basicFee">Basic fee: <span> {{estimatedFees.basicFee | currency}} </span></li>
|
||||
<li *ngIf="estimatedFees.counterFoilFee">Counterfoil Fee: <span> {{estimatedFees.counterFoilFee | currency}}
|
||||
</span></li>
|
||||
<li *ngIf="estimatedFees.continuationSheetFee">Continuation sheet fee:
|
||||
<span> {{estimatedFees.continuationSheetFee | currency}}</span></li>
|
||||
<li *ngIf="estimatedFees.expeditedFee">Expedited fee: <span> {{estimatedFees.expeditedFee | currency}}</span></li>
|
||||
<li *ngIf="estimatedFees.shippingFee">Shipping fee: <span> {{estimatedFees.shippingFee | currency}}</span></li>
|
||||
<li *ngIf="estimatedFees.bondPremium">{{bondPremiumLabel}}: <span> {{estimatedFees.bondPremium | currency}}</span></li>
|
||||
<li *ngIf="estimatedFees.cargoPremium">Cargo Premium: <span> {{estimatedFees.cargoPremium | currency}}</span></li>
|
||||
<li *ngIf="estimatedFees.ldiPremium">LDI Premium: <span> {{estimatedFees.ldiPremium | currency}}</span></li>
|
||||
</ul>
|
||||
<hr/>
|
||||
<ul>
|
||||
<li class="total">Total: <span>{{total | currency}}</span></li>
|
||||
</ul>
|
||||
<hr/>
|
||||
<ul>
|
||||
<li *ngIf="estimatedFees.securityAmount">Security Amount: <span>{{estimatedFees.securityAmount | currency}}</span></li>
|
||||
<li *ngIf="estimatedFees.insuredValue">Insured Value: <span>{{estimatedFees.insuredValue | currency}}</span></li>
|
||||
</ul>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions>
|
||||
<button mat-raised-button color="primary" (click)="onClose()">
|
||||
Ok
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
`,
|
||||
styles: [`
|
||||
li {
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.total {
|
||||
font-weight: bold;
|
||||
margin-top: 12px;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class FeesDialogComponent {
|
||||
estimatedFees: Fees = {};
|
||||
headerid: number | null = null;
|
||||
bondPremiumLabel: string = 'Bond Premium';
|
||||
total: number = 0;
|
||||
|
||||
private carnetService = inject(CarnetService);
|
||||
private notificationService = inject(NotificationService);
|
||||
private errorHandler = inject(ApiErrorHandlerService);
|
||||
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<FeesDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: { headerid: any }
|
||||
) {
|
||||
this.headerid = data.headerid;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.headerid && this.headerid > 0) {
|
||||
this.carnetService.getEstimatedFees(this.headerid).subscribe({
|
||||
next: (data: Fees) => {
|
||||
if (data) {
|
||||
this.estimatedFees = data;
|
||||
|
||||
if (this.estimatedFees.securityType === 'B') {
|
||||
this.bondPremiumLabel = 'Bond Premium';
|
||||
} else if (this.estimatedFees.securityType === 'C') {
|
||||
this.bondPremiumLabel = 'Cash Deposit';
|
||||
} else if (this.estimatedFees.securityType === 'G') {
|
||||
this.bondPremiumLabel = 'Refundable claim deposit';
|
||||
}
|
||||
|
||||
this.calculateTotal();
|
||||
}
|
||||
},
|
||||
error: (error: any) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to get estimated fees');
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error getting estimated fees:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
calculateTotal(): void {
|
||||
const excludedProperties = ['securityAmount', 'insuredValue', 'securityType'];
|
||||
const validFees = Object.entries(this.estimatedFees)
|
||||
.filter(([key]) => !excludedProperties.includes(key))
|
||||
.map(([, value]) => value)
|
||||
.filter((fee): fee is number => typeof fee === 'number');
|
||||
|
||||
this.total = validFees.reduce((total, fee) => total + fee, 0);
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
||||
@ -1,15 +1,31 @@
|
||||
<div class="shipping-container">
|
||||
<form [formGroup]="shippingForm" (ngSubmit)="onSubmit()">
|
||||
<div class="loading-shade" *ngIf="isLoading">
|
||||
<mat-spinner diameter="50"></mat-spinner>
|
||||
</div>
|
||||
|
||||
<form [formGroup]="shippingForm" (ngSubmit)="onSubmit(false)">
|
||||
<!-- 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
|
||||
<!-- <mat-checkbox formControlName="needsBond">Do you need Bond from
|
||||
us?</mat-checkbox> -->
|
||||
<mat-checkbox formControlName="needsInsurance">Do you need insurance for your
|
||||
goods?</mat-checkbox>
|
||||
<mat-checkbox labelPosition="before" formControlName="needsLostDocProtection">Do you need Lost document
|
||||
<mat-checkbox formControlName="needsLostDocProtection">Do you need Lost document
|
||||
protection?</mat-checkbox>
|
||||
|
||||
<mat-form-field appearance="outline" class="formofsecurity">
|
||||
<mat-label>Form Of Security</mat-label>
|
||||
<mat-select formControlName="formOfSecurity">
|
||||
<mat-option *ngFor="let formOfSecurity of formOfSecurities" [value]="formOfSecurity.value">
|
||||
{{ formOfSecurity.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="shippingForm.get('formOfSecurity')?.errors?.['required']">
|
||||
Form Of Security is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -17,22 +33,45 @@
|
||||
<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-button value="PREPARER">Preparer</mat-radio-button>
|
||||
<mat-radio-button value="HOLDER">Holder</mat-radio-button>
|
||||
<mat-radio-button value="3RDPARTY">3rd Party</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
|
||||
<mat-card appearance="outlined" *ngIf="!showAddressForm && !showContactForm">
|
||||
<mat-card-content>
|
||||
<p>{{getAddressLabel()}}</p>
|
||||
<div class="presaved-address">
|
||||
<div class="presaved-address-content">
|
||||
<p>{{getAddressLabel()}}</p>
|
||||
<p>{{getContactLabel()}}</p>
|
||||
</div>
|
||||
<div class="presaved-address-actions">
|
||||
<!-- <button type="button" mat-icon-button color="primary" matTooltip="Edit">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button> -->
|
||||
<button type="button" mat-icon-button color="primary" *ngIf="!isViewMode"
|
||||
(click)="selectContact()" matTooltip="Change contact">
|
||||
<mat-icon>compare_arrows</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
<mat-card-actions>
|
||||
<button mat-button (click)="editAddressForm()">Edit</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
<div *ngIf="showAddressForm" class="address-form" formGroupName="address">
|
||||
<h4>Shipping Address</h4>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline" class="companyName">
|
||||
<mat-label>Company Name</mat-label>
|
||||
<input matInput formControlName="companyName" required>
|
||||
<mat-error *ngIf="shippingForm.get('address.companyName')?.errors?.['required']">
|
||||
Company name is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="shippingForm.get('address.companyName')?.errors?.['maxlength']">
|
||||
Maximum 100 characters allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<!-- Address Information -->
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline" class="address1">
|
||||
@ -112,7 +151,7 @@
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-actions" *ngIf="showAddressForm && shippingForm.get('shipTo')?.value !== 'thirdParty'">
|
||||
<div class="form-actions" *ngIf="showAddressForm && shippingForm.get('shipTo')?.value !== '3RDPARTY'">
|
||||
<button mat-button type="button" (click)="cancelEditAddressForm()">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -229,16 +268,20 @@
|
||||
<textarea matInput formControlName="notes" rows="2"></textarea>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-actions" *ngIf="showAddressForm && shippingForm.get('shipTo')?.value !== '3RDPARTY'">
|
||||
<button mat-button type="button" (click)="cancelEditAddressForm()">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delivery Section -->
|
||||
<div class="section">
|
||||
<h3>Delivery Method</h3>
|
||||
<h3>Delivery</h3>
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Delivery Type</mat-label>
|
||||
<mat-select formControlName="deliveryType" required>
|
||||
<mat-select formControlName="deliveryType" required (change)="onDeliveryTypeChange()">
|
||||
<mat-option *ngFor="let deliveryType of deliveryTypes" [value]="deliveryType.value">
|
||||
{{ deliveryType.name }}
|
||||
</mat-option>
|
||||
@ -246,10 +289,14 @@
|
||||
<mat-error *ngIf="shippingForm.get('deliveryType')?.errors?.['required']">
|
||||
Delivery Type is required
|
||||
</mat-error>
|
||||
<mat-hint align="start" *ngIf="deliveryEstimate && !isViewMode" class="delivery-estimate">
|
||||
<span>{{ deliveryEstimate }}</span>
|
||||
</mat-hint>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Delivery Method</mat-label>
|
||||
<mat-select formControlName="deliveryMethod" required>
|
||||
<mat-select formControlName="deliveryMethod" required
|
||||
(selectionChange)="onDeliveryMethodChange($event.value)">
|
||||
<mat-option *ngFor="let deliveryMethod of deliveryMethods" [value]="deliveryMethod.value">
|
||||
{{ deliveryMethod.name }}
|
||||
</mat-option>
|
||||
@ -259,11 +306,14 @@
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div *ngIf="shippingForm.get('deliveryMethod')?.value === 'customerCourier'" class="form-row">
|
||||
|
||||
<div *ngIf="shippingForm.get('deliveryMethod')?.value === 'CLC'" class="form-row">
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>Courier Account Number</mat-label>
|
||||
<input matInput formControlName="courierAccount">
|
||||
<mat-error>Required when using customer courier</mat-error>
|
||||
<mat-error *ngIf="shippingForm.get('courierAccount')?.errors?.['required']">
|
||||
Required when using customer courier
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
@ -293,10 +343,36 @@
|
||||
</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 class="error-message">
|
||||
<mat-icon *ngIf="missingSectionMessage">error</mat-icon>
|
||||
<span *ngIf="missingSectionMessage">{{ missingSectionMessage }}</span>
|
||||
</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
<button mat-raised-button color="primary" type="button" (click)="showEstimatedFees()"
|
||||
*ngIf="showEstimatedFeesButton && !isViewMode">
|
||||
<span>Estimated Fees</span>
|
||||
</button>
|
||||
|
||||
<button mat-raised-button color="primary" type="submit" *ngIf="!isViewMode"
|
||||
[disabled]="shippingForm.invalid || changeInProgress">
|
||||
Save
|
||||
</button>
|
||||
|
||||
<button mat-raised-button color="primary" type="button" (click)="processApplication()"
|
||||
[disabled]="!enableSubmitButton" *ngIf="showProcessButton && !isViewMode">
|
||||
<span>Process Application</span>
|
||||
</button>
|
||||
|
||||
<button mat-raised-button color="primary" type="button" (click)="submitApplication()"
|
||||
[disabled]="!enableSubmitButton" *ngIf="showSubmitButton && !isViewMode">
|
||||
<span>Submit Application</span>
|
||||
</button>
|
||||
|
||||
<button mat-raised-button color="primary" type="button" (click)="returnToHome()" *ngIf="isViewMode">
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@ -1,14 +1,14 @@
|
||||
.shipping-container {
|
||||
padding: 16px;
|
||||
padding-top: 0.5rem;
|
||||
|
||||
.section {
|
||||
margin-bottom: 24px;
|
||||
margin-bottom: 1rem;
|
||||
padding: 16px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
|
||||
h3 {
|
||||
margin: 0px 0px 16px 0px;
|
||||
margin: 0 0 0.9rem 0;
|
||||
color: var(--mat-sys-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
@ -20,25 +20,32 @@
|
||||
}
|
||||
|
||||
.mat-mdc-card-content:first-child {
|
||||
padding: 0 16px;
|
||||
padding: 0 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
|
||||
.formofsecurity {
|
||||
margin-left: 1rem;
|
||||
min-width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 0.9rem;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 0.5rem;
|
||||
align-items: start;
|
||||
|
||||
mat-form-field {
|
||||
flex: 1;
|
||||
@ -56,9 +63,54 @@
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
margin-top: 0.9rem;
|
||||
|
||||
.error-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--mat-form-field-error-text-color, var(--mat-sys-error));
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-family: var(--mat-form-field-subscript-text-font, var(--mat-sys-body-small-font));
|
||||
line-height: var(--mat-form-field-subscript-text-line-height, var(--mat-sys-body-small-line-height));
|
||||
font-size: var(--mat-form-field-subscript-text-size, var(--mat-sys-body-small-size));
|
||||
letter-spacing: var(--mat-form-field-subscript-text-tracking, var(--mat-sys-body-small-tracking));
|
||||
font-weight: var(--mat-form-field-subscript-text-weight, var(--mat-sys-body-small-weight));
|
||||
|
||||
mat-icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.presaved-address {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
font-size: 0.875rem;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.presaved-address-actions {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.delivery-estimate {
|
||||
color: #28a745;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,95 @@
|
||||
<div class="terms-container">
|
||||
<mat-card class="terms-card">
|
||||
<mat-card-header>
|
||||
<mat-card-title>ATA Carnet Terms and Conditions</mat-card-title>
|
||||
</mat-card-header>
|
||||
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<mat-card-content class="terms-content">
|
||||
<p>Agreement language will go here</p>
|
||||
<h3>Fees and Charges</h3>
|
||||
<p> The following fees apply:</p>
|
||||
<!-- Fee List with Table-like Structure -->
|
||||
<div class="fee-table">
|
||||
<div class="fee-row" *ngIf="estimatedFees.basicFee">
|
||||
<span class="fee-label">Basic fee:</span>
|
||||
<span class="fee-value">{{ estimatedFees.basicFee | currency }}</span>
|
||||
</div>
|
||||
|
||||
<div class="fee-row" *ngIf="estimatedFees.counterFoilFee">
|
||||
<span class="fee-label">Counterfoil Fee:</span>
|
||||
<span class="fee-value">{{ estimatedFees.counterFoilFee | currency }}</span>
|
||||
</div>
|
||||
|
||||
<div class="fee-row" *ngIf="estimatedFees.continuationSheetFee">
|
||||
<span class="fee-label">Continuation sheet fee:</span>
|
||||
<span class="fee-value">{{ estimatedFees.continuationSheetFee | currency }}</span>
|
||||
</div>
|
||||
|
||||
<div class="fee-row" *ngIf="estimatedFees.expeditedFee">
|
||||
<span class="fee-label">Expedited fee:</span>
|
||||
<span class="fee-value">{{ estimatedFees.expeditedFee | currency }}</span>
|
||||
</div>
|
||||
|
||||
<div class="fee-row" *ngIf="estimatedFees.shippingFee">
|
||||
<span class="fee-label">Shipping fee:</span>
|
||||
<span class="fee-value">{{ estimatedFees.shippingFee | currency }}</span>
|
||||
</div>
|
||||
|
||||
<div class="fee-row" *ngIf="estimatedFees.bondPremium">
|
||||
<span class="fee-label">{{ bondPremiumLabel }}:</span>
|
||||
<span class="fee-value">{{ estimatedFees.bondPremium | currency }}</span>
|
||||
</div>
|
||||
|
||||
<div class="fee-row" *ngIf="estimatedFees.cargoPremium">
|
||||
<span class="fee-label">Cargo Premium:</span>
|
||||
<span class="fee-value">{{ estimatedFees.cargoPremium | currency }}</span>
|
||||
</div>
|
||||
|
||||
<div class="fee-row" *ngIf="estimatedFees.ldiPremium">
|
||||
<span class="fee-label">LDI Premium:</span>
|
||||
<span class="fee-value">{{ estimatedFees.ldiPremium | currency }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<!-- Total -->
|
||||
<div class="fee-table">
|
||||
<div class="fee-row total">
|
||||
<span class="fee-label">Total:</span>
|
||||
<span class="fee-value">{{ total | currency }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<!-- Security & Insurance -->
|
||||
<div class="fee-table">
|
||||
<div class="fee-row" *ngIf="estimatedFees.securityAmount">
|
||||
<span class="fee-label">Security Amount:</span>
|
||||
<span class="fee-value">{{ estimatedFees.securityAmount | currency }}</span>
|
||||
</div>
|
||||
|
||||
<div class="fee-row" *ngIf="estimatedFees.insuredValue">
|
||||
<span class="fee-label">Insured Value:</span>
|
||||
<span class="fee-value">{{ estimatedFees.insuredValue | currency }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<mat-checkbox [(ngModel)]="hasReadAllTerms" color="primary" class="read-checkbox">
|
||||
I have read and agree to all terms and conditions above
|
||||
</mat-checkbox>
|
||||
</mat-card-content>
|
||||
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<mat-card-actions align="end">
|
||||
<button mat-raised-button color="primary" (click)="onAccept()"
|
||||
[disabled]="!hasReadAllTerms || changeInProgress">
|
||||
Accept
|
||||
</button>
|
||||
<button mat-button (click)="onDecline()">Decline</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
</div>
|
||||
105
src/app/carnet/terms-conditions/terms-conditions.component.scss
Normal file
105
src/app/carnet/terms-conditions/terms-conditions.component.scss
Normal file
@ -0,0 +1,105 @@
|
||||
.terms-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.terms-card {
|
||||
width: 100%;
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
mat-card-header {
|
||||
justify-content: center;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.terms-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
font-size: 0.875rem;
|
||||
|
||||
h3 {
|
||||
color: var(--mat-sys-primary);
|
||||
margin-top: 24px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
p {
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.fee-table {
|
||||
width: 100%;
|
||||
max-width: 325px;
|
||||
}
|
||||
|
||||
.fee-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
.fee-label {
|
||||
text-align: left;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.fee-value {
|
||||
text-align: right;
|
||||
min-width: 120px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.total {
|
||||
font-weight: bold;
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.read-checkbox {
|
||||
margin: 24px 0;
|
||||
display: block;
|
||||
padding: 8px 0;
|
||||
border-top: 1px solid #eee;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
// Custom scrollbar
|
||||
.terms-content::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.terms-content::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.terms-content::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.terms-content::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
// Responsive adjustments
|
||||
@media (max-width: 768px) {
|
||||
.terms-container {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.terms-card {
|
||||
max-height: 95vh;
|
||||
}
|
||||
}
|
||||
155
src/app/carnet/terms-conditions/terms-conditions.component.ts
Normal file
155
src/app/carnet/terms-conditions/terms-conditions.component.ts
Normal file
@ -0,0 +1,155 @@
|
||||
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service';
|
||||
import { NavigationService } from '../../core/services/common/navigation.service';
|
||||
import { NotificationService } from '../../core/services/common/notification.service';
|
||||
import { StorageService } from '../../core/services/common/storage.service';
|
||||
import { CarnetService } from '../../core/services/carnet/carnet.service';
|
||||
import { Fees } from '../../core/models/carnet/fee';
|
||||
import { finalize } from 'rxjs';
|
||||
import { ShippingService } from '../../core/services/carnet/shipping.service';
|
||||
import { Shipping } from '../../core/models/carnet/shipping';
|
||||
|
||||
@Component({
|
||||
selector: 'app-terms-conditions',
|
||||
imports: [MatCardModule,
|
||||
MatButtonModule,
|
||||
MatDividerModule,
|
||||
MatCheckboxModule,
|
||||
CommonModule,
|
||||
FormsModule],
|
||||
templateUrl: './terms-conditions.component.html',
|
||||
styleUrl: './terms-conditions.component.scss'
|
||||
})
|
||||
export class TermsConditionsComponent {
|
||||
isLoading: boolean = false;
|
||||
currentDate = new Date();
|
||||
hasReadAllTerms = false;
|
||||
currentApplicationDetails: { headerid: number, applicationName: string, applicationType: string } | null = null;
|
||||
changeInProgress: boolean = false;
|
||||
estimatedFees: Fees = {};
|
||||
needCheckOut: boolean = false;
|
||||
bondPremiumLabel: string = 'Bond Premium';
|
||||
total: number = 0;
|
||||
|
||||
private notificationService = inject(NotificationService);
|
||||
private errorHandler = inject(ApiErrorHandlerService);
|
||||
private storageService = inject(StorageService);
|
||||
private navigationService = inject(NavigationService);
|
||||
private carnetService = inject(CarnetService);
|
||||
private shippingService = inject(ShippingService);
|
||||
|
||||
constructor() {
|
||||
this.currentApplicationDetails = this.storageService.get<{ headerid: number, applicationName: string, applicationType: string }>('currentapplication')
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.currentApplicationDetails?.headerid) {
|
||||
this.carnetService.getEstimatedFees(this.currentApplicationDetails?.headerid).subscribe({
|
||||
next: (data: Fees) => {
|
||||
if (data) {
|
||||
this.estimatedFees = data;
|
||||
|
||||
if (this.estimatedFees.securityType === 'B') {
|
||||
this.bondPremiumLabel = 'Bond Premium';
|
||||
} else if (this.estimatedFees.securityType === 'C') {
|
||||
this.bondPremiumLabel = 'Cash Deposit';
|
||||
} else if (this.estimatedFees.securityType === 'G') {
|
||||
this.bondPremiumLabel = 'Refundable claim deposit';
|
||||
}
|
||||
|
||||
this.calculateTotal();
|
||||
}
|
||||
},
|
||||
error: (error: any) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to get estimated fees');
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error getting estimated fees:', error);
|
||||
}
|
||||
});
|
||||
|
||||
this.getPaymentType(this.currentApplicationDetails?.headerid);
|
||||
}
|
||||
}
|
||||
|
||||
onAccept(): void {
|
||||
if (this.needCheckOut) {
|
||||
this.navigationService.navigate(['checkout']);
|
||||
} else {
|
||||
this.changeInProgress = true;
|
||||
this.carnetService.submitApplication(this.currentApplicationDetails?.headerid!).pipe(finalize(() => {
|
||||
this.changeInProgress = false;
|
||||
})).subscribe({
|
||||
next: () => {
|
||||
this.notificationService.showSuccess('Application submitted successfully');
|
||||
this.storageService.removeItem('currentapplication');
|
||||
this.navigationService.navigate(["home"]);
|
||||
},
|
||||
error: (error) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to submit application');
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error submitting the application', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onDecline(): void {
|
||||
this.storageService.removeItem('currentapplication');
|
||||
|
||||
let route: string = 'edit-carnet';
|
||||
if (this.currentApplicationDetails?.applicationType === 'duplicate') {
|
||||
route = 'duplicate-carnet';
|
||||
}
|
||||
else if (this.currentApplicationDetails?.applicationType === 'extend') {
|
||||
route = 'extend-carnet';
|
||||
} else if (this.currentApplicationDetails?.applicationType === 'additional') {
|
||||
route = 'additional-sets';
|
||||
}
|
||||
|
||||
this.navigationService.navigate([route, this.currentApplicationDetails?.headerid],
|
||||
{
|
||||
state: { isEditMode: true },
|
||||
queryParams: {
|
||||
applicationname: this.currentApplicationDetails?.applicationName,
|
||||
return: 'shipping'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
getPaymentType(headerid: number): void {
|
||||
this.isLoading = true;
|
||||
this.shippingService.getShippingData(headerid).pipe(
|
||||
finalize(() => {
|
||||
this.isLoading = false;
|
||||
})
|
||||
).subscribe({
|
||||
next: (shippingData: Shipping) => {
|
||||
if (!shippingData) { // do nothing if empty
|
||||
return;
|
||||
}
|
||||
this.needCheckOut = shippingData.paymentMethod !== 'ACH' && shippingData.paymentMethod !== 'INV';
|
||||
},
|
||||
error: (error: any) => {
|
||||
console.error('Error loading shipping data', error);
|
||||
this.errorHandler.handleApiError(error, 'Failed to load shipping data');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
calculateTotal(): void {
|
||||
const excludedProperties = ['securityAmount', 'insuredValue', 'securityType'];
|
||||
const validFees = Object.entries(this.estimatedFees)
|
||||
.filter(([key]) => !excludedProperties.includes(key))
|
||||
.map(([, value]) => value)
|
||||
.filter((fee): fee is number => typeof fee === 'number');
|
||||
|
||||
this.total = validFees.reduce((total, fee) => total + fee, 0);
|
||||
}
|
||||
}
|
||||
@ -1 +1,157 @@
|
||||
<p>travel-plan works!</p>
|
||||
<div class="travel-plan-container">
|
||||
<div class="loading-shade" *ngIf="isLoading">
|
||||
<mat-spinner diameter="50"></mat-spinner>
|
||||
</div>
|
||||
<form [formGroup]="travelForm" (ngSubmit)="onSubmit()">
|
||||
<!-- USA Entries -->
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline" class="usa-entries">
|
||||
<mat-label>No of times entering & leaving USA</mat-label>
|
||||
<input matInput type="number" formControlName="usaEntries" min="1" max="99">
|
||||
<mat-error *ngIf="travelForm.get('usaEntries')?.hasError('required')">
|
||||
Required
|
||||
</mat-error>
|
||||
<mat-error
|
||||
*ngIf="travelForm.get('usaEntries')?.hasError('min') || travelForm.get('usaEntries')?.hasError('max')">
|
||||
Must be between 1-99
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="country-selection-container">
|
||||
<!-- Available Countries Listbox -->
|
||||
<div class="available-countries">
|
||||
<h3>Visit Countries</h3>
|
||||
|
||||
<mat-selection-list [multiple]="false" hideSingleSelectionIndicator>
|
||||
<mat-list-option *ngFor="let country of countries" [value]="country"
|
||||
[disabled]="isVisitCountrySelected(country)"
|
||||
(click)="onAvailableVisitCountrySelection(visitavailableoption)" #visitavailableoption>
|
||||
{{country.name}}
|
||||
</mat-list-option>
|
||||
</mat-selection-list>
|
||||
</div>
|
||||
|
||||
<!-- Controls -->
|
||||
<div class="controls">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>No of times</mat-label>
|
||||
<input matInput type="number" formControlName="visitsInput" min="1" max="99"
|
||||
[(ngModel)]="visitsCount">
|
||||
<mat-error
|
||||
*ngIf="travelForm.get('visitsInput')?.hasError('min') || travelForm.get('visitsInput')?.hasError('max')">
|
||||
Must be between 1-99
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<div class="action-buttons">
|
||||
<button mat-raised-button color="primary" type="button" (click)="addVisitCountry()"
|
||||
*ngIf="!isViewMode" class="icon-end">
|
||||
<mat-icon>keyboard_double_arrow_right</mat-icon>
|
||||
<span> Add</span>
|
||||
</button>
|
||||
<button mat-raised-button type="button" (click)="removeVisitCountry()" *ngIf="!isViewMode">
|
||||
<mat-icon>keyboard_double_arrow_left</mat-icon>
|
||||
<span> Remove</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Selected Countries -->
|
||||
<div class="selected-countries">
|
||||
<div class="visit-countries">
|
||||
<h3>Selected</h3>
|
||||
<mat-selection-list [multiple]="false" hideSingleSelectionIndicator>
|
||||
<mat-list-option *ngFor="let country of selectedVisitCountries" [value]="country"
|
||||
(click)="onVisitCountrySelection(visitoption)" #visitoption>
|
||||
{{country.name}} ({{country.visits}})
|
||||
</mat-list-option>
|
||||
<mat-list-item *ngIf="selectedVisitCountries.length === 0">
|
||||
<span class="empty-message">No visit countries selected</span>
|
||||
</mat-list-item>
|
||||
</mat-selection-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="country-selection-container">
|
||||
<!-- Available Countries Listbox -->
|
||||
<div class="available-countries">
|
||||
<h3>Transit Countries</h3>
|
||||
<mat-selection-list [multiple]="false" hideSingleSelectionIndicator>
|
||||
<mat-list-option *ngFor="let country of countries" [value]="country"
|
||||
[disabled]="isTransitCountrySelected(country)"
|
||||
(click)="onAvailableTransitCountrySelection(transitavailableoption)" #transitavailableoption>
|
||||
{{country.name}}
|
||||
</mat-list-option>
|
||||
</mat-selection-list>
|
||||
</div>
|
||||
|
||||
<!-- Controls -->
|
||||
<div class="controls">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>No of times</mat-label>
|
||||
<input matInput type="number" formControlName="transitsInput" min="1" max="99"
|
||||
[(ngModel)]="transitsCount">
|
||||
<mat-error
|
||||
*ngIf="travelForm.get('transitsInput')?.hasError('min') || travelForm.get('transitsInput')?.hasError('max')">
|
||||
Must be between 1-99
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<div class="action-buttons">
|
||||
<button mat-raised-button color="primary" type="button" (click)="addTransitCountry()"
|
||||
*ngIf="!isViewMode" class="icon-end">
|
||||
<mat-icon>keyboard_double_arrow_right</mat-icon>
|
||||
<span> Add</span>
|
||||
</button>
|
||||
<button mat-raised-button type="button" (click)="removeTransitCountry()" *ngIf="!isViewMode">
|
||||
<mat-icon>keyboard_double_arrow_left</mat-icon>
|
||||
<span> Remove</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Selected Countries -->
|
||||
<div class="selected-countries">
|
||||
<div class="transit-countries">
|
||||
<h3>Selected</h3>
|
||||
<mat-selection-list [multiple]="false" hideSingleSelectionIndicator>
|
||||
<mat-list-option *ngFor="let country of selectedTransitCountries" [value]="country"
|
||||
(click)="onTransitCountrySelection(transitoption)" #transitoption>
|
||||
{{country.name}} ({{country.visits}})
|
||||
</mat-list-option>
|
||||
<mat-list-item *ngIf="selectedTransitCountries.length === 0">
|
||||
<span class="empty-message">No transit countries selected</span>
|
||||
</mat-list-item>
|
||||
</mat-selection-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<!-- Totals -->
|
||||
<div class="totals-section">
|
||||
<div class="total-item">
|
||||
<span>Total Visits:</span>
|
||||
<span class="total-value">{{totalVisits}}</span>
|
||||
</div>
|
||||
<div class="total-item">
|
||||
<span>Total Transits:</span>
|
||||
<span class="total-value">{{totalTransits}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="message-section" *ngIf="showAdditionalSetsValidationMessage">
|
||||
<mat-error>
|
||||
At least one of the sets to be entered. (either number of times entering & leaving, Foreign sets or
|
||||
Transit sets)
|
||||
</mat-error>
|
||||
</div>
|
||||
<button mat-raised-button color="primary" type="submit" *ngIf="!isViewMode" [disabled]="changeInProgress">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
@ -0,0 +1,147 @@
|
||||
.travel-plan-container {
|
||||
padding: 0.75rem 0 0 0;
|
||||
// max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
|
||||
// .form-row {
|
||||
// margin-bottom: 24px;
|
||||
// max-width: 300px;
|
||||
// }
|
||||
|
||||
|
||||
.form-row {
|
||||
// display: flex;
|
||||
// gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
//align-items: start;
|
||||
|
||||
// mat-form-field {
|
||||
// flex: 1;
|
||||
// }
|
||||
}
|
||||
|
||||
.usa-entries {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.country-selection-container {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
margin-bottom: 12px;
|
||||
border: 1px solid;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.available-countries,
|
||||
.selected-countries {
|
||||
flex: 1;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
overflow: hidden;
|
||||
|
||||
h3 {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
mat-selection-list {
|
||||
height: 150px;
|
||||
overflow-y: auto;
|
||||
padding: 0;
|
||||
|
||||
mat-list-item {
|
||||
.empty-message {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
width: 150px;
|
||||
padding: 16px;
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
// gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
button.icon-end {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.selected-countries {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.visit-countries,
|
||||
.transit-countries {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
justify-content: space-between;
|
||||
|
||||
.totals-section {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
font-size: 0.875rem;
|
||||
|
||||
.total-item {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
.total-value {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-section {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.travel-container {
|
||||
.form-row {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
mat-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,12 +1,355 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { Component, EventEmitter, inject, Input, Output, SimpleChanges } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { finalize, forkJoin } from 'rxjs';
|
||||
import { TravelPlanService } from '../../core/services/carnet/travel-plan.service';
|
||||
import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service';
|
||||
import { NotificationService } from '../../core/services/common/notification.service';
|
||||
import { Country, TravelEntry, TravelPlan } from '../../core/models/carnet/travel-plan';
|
||||
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { MatListModule } from '@angular/material/list';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ConfirmDialogComponent } from '../../shared/components/confirm-dialog/confirm-dialog.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-travel-plan',
|
||||
imports: [],
|
||||
imports: [AngularMaterialModule, ReactiveFormsModule, CommonModule, MatListModule],
|
||||
templateUrl: './travel-plan.component.html',
|
||||
styleUrl: './travel-plan.component.scss'
|
||||
})
|
||||
export class TravelPlanComponent {
|
||||
@Input() headerid: number = 0;
|
||||
@Input() isViewMode = false;
|
||||
@Input() applicationType: 'new' | 'edit' | 'additional' | 'duplicate' | 'extend' | null = 'edit';
|
||||
|
||||
@Output() completed = new EventEmitter<boolean>();
|
||||
@Output() needsInsurance = new EventEmitter<boolean>();
|
||||
|
||||
private fb = inject(FormBuilder);
|
||||
private dialog = inject(MatDialog);
|
||||
|
||||
private travelPlanService = inject(TravelPlanService);
|
||||
private notificationService = inject(NotificationService);
|
||||
private errorHandler = inject(ApiErrorHandlerService);
|
||||
|
||||
travelForm: FormGroup;
|
||||
isLoading = false;
|
||||
changeInProgress = false;
|
||||
showAdditionalSetsValidationMessage: boolean = false;
|
||||
visitsCount = 1;
|
||||
transitsCount = 1;
|
||||
|
||||
countries: Country[] = [];
|
||||
selectedVisitCountries: TravelEntry[] = [];
|
||||
selectedTransitCountries: TravelEntry[] = [];
|
||||
|
||||
// Currently selected country in the listbox
|
||||
selectedAvailableVisitCountry: Country | null = null;
|
||||
selectedAvailableTransitCountry: Country | null = null;
|
||||
selectedVisitCountry: Country | null = null;
|
||||
selectedTransitCountry: Country | null = null;
|
||||
|
||||
constructor() {
|
||||
this.travelForm = this.fb.group({
|
||||
usaEntries: ['', [Validators.required, Validators.min(1), Validators.max(99)]],
|
||||
visitsInput: ['', [Validators.min(1), Validators.max(99)]],
|
||||
transitsInput: ['', [Validators.min(1), Validators.max(99)]],
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.headerid) {
|
||||
this.loadTravelPlan();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
if (changes['headerid'] && this.headerid > 0) {
|
||||
this.loadTravelPlan();
|
||||
}
|
||||
}
|
||||
|
||||
onAvailableVisitCountrySelection(event: any) {
|
||||
this.selectedAvailableVisitCountry = event.value;
|
||||
}
|
||||
|
||||
onVisitCountrySelection(event: any) {
|
||||
this.selectedVisitCountry = event.value;
|
||||
}
|
||||
|
||||
onAvailableTransitCountrySelection(event: any) {
|
||||
this.selectedAvailableTransitCountry = event.value;
|
||||
}
|
||||
|
||||
onTransitCountrySelection(event: any) {
|
||||
this.selectedTransitCountry = event.value;
|
||||
}
|
||||
|
||||
// Add selected country to visits
|
||||
addVisitCountry(): void {
|
||||
if (!this.selectedAvailableVisitCountry) return;
|
||||
|
||||
if (this.selectedAvailableVisitCountry.actionType === 'FYI') {
|
||||
this.fyiAction(this.selectedAvailableVisitCountry);
|
||||
} else if (this.selectedAvailableVisitCountry.actionType === 'WARN') {
|
||||
this.warningAction(this.selectedAvailableVisitCountry);
|
||||
} else if (this.selectedAvailableVisitCountry.actionType === 'ACTION') {
|
||||
this.takeAction(this.selectedAvailableVisitCountry);
|
||||
} else {
|
||||
this.addVisitCountryToCollection();
|
||||
}
|
||||
}
|
||||
|
||||
addVisitCountryToCollection(): void {
|
||||
if (!this.selectedAvailableVisitCountry) return;
|
||||
|
||||
const existingIndex = this.selectedVisitCountries.findIndex(
|
||||
c => c.value === this.selectedAvailableVisitCountry!.value
|
||||
);
|
||||
|
||||
if (existingIndex === -1) {
|
||||
this.selectedVisitCountries.push({
|
||||
...this.selectedAvailableVisitCountry,
|
||||
visits: this.visitsCount
|
||||
});
|
||||
|
||||
if (this.applicationType === 'additional' || this.applicationType === 'extend') {
|
||||
this.needsInsurance.emit(this.selectedVisitCountries.length > 0);
|
||||
}
|
||||
}
|
||||
|
||||
this.visitsCount = 1;
|
||||
}
|
||||
|
||||
fyiAction(country: Country): void {
|
||||
|
||||
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
|
||||
width: '560px',
|
||||
data: {
|
||||
title: 'For your information',
|
||||
message: country.actionMessage,
|
||||
confirmText: 'Ok',
|
||||
hideCancel: true
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.addVisitCountryToCollection();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
warningAction(country: Country): void {
|
||||
|
||||
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
|
||||
width: '560px',
|
||||
data: {
|
||||
title: 'Warning',
|
||||
message: country.actionMessage,
|
||||
confirmText: 'Yes',
|
||||
cancelText: 'No'
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.addVisitCountryToCollection();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
takeAction(country: Country): void {
|
||||
|
||||
if (this.IsCountryExists('transit', country)) {
|
||||
this.addVisitCountryToCollection();
|
||||
return;
|
||||
}
|
||||
|
||||
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
|
||||
width: '560px',
|
||||
data: {
|
||||
title: 'Act',
|
||||
message: country.actionMessage,
|
||||
confirmText: 'Yes',
|
||||
cancelText: 'No'
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.transitsCount = this.visitsCount;
|
||||
this.selectedAvailableTransitCountry = this.selectedAvailableVisitCountry;
|
||||
this.addVisitCountryToCollection();
|
||||
this.addTransitCountryToCollection();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add selected country to transits
|
||||
addTransitCountry(): void {
|
||||
if (!this.selectedAvailableTransitCountry) return;
|
||||
this.addTransitCountryToCollection();
|
||||
}
|
||||
|
||||
addTransitCountryToCollection(): void {
|
||||
if (!this.selectedAvailableTransitCountry) return;
|
||||
|
||||
const existingIndex = this.selectedTransitCountries.findIndex(
|
||||
c => c.value === this.selectedAvailableTransitCountry!.value
|
||||
);
|
||||
|
||||
if (existingIndex === -1) {
|
||||
this.selectedTransitCountries.push({
|
||||
...this.selectedAvailableTransitCountry,
|
||||
visits: this.transitsCount
|
||||
});
|
||||
}
|
||||
this.transitsCount = 1;
|
||||
}
|
||||
|
||||
IsCountryExists(countryType: string, country: Country): boolean {
|
||||
|
||||
const existingIndex = countryType === 'transit' ? this.selectedTransitCountries.findIndex(
|
||||
c => c.value === country!.value
|
||||
) : this.selectedVisitCountries.findIndex(
|
||||
c => c.value === country!.value
|
||||
);
|
||||
|
||||
return existingIndex > -1;
|
||||
}
|
||||
|
||||
// Remove country from visits
|
||||
removeVisitCountry(): void {
|
||||
if (!this.selectedVisitCountry) return;
|
||||
|
||||
this.selectedVisitCountries = this.selectedVisitCountries.filter(
|
||||
c => c.value !== this.selectedVisitCountry!.value
|
||||
);
|
||||
|
||||
if (this.applicationType === 'additional' || this.applicationType === 'extend') {
|
||||
this.needsInsurance.emit(this.selectedVisitCountries.length > 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove country from transits
|
||||
removeTransitCountry(): void {
|
||||
if (!this.selectedTransitCountry) return;
|
||||
|
||||
this.selectedTransitCountries = this.selectedTransitCountries.filter(
|
||||
c => c.value !== this.selectedTransitCountry!.value
|
||||
);
|
||||
}
|
||||
|
||||
// Calculate total visits
|
||||
get totalVisits(): number {
|
||||
return this.selectedVisitCountries.reduce((sum, country) => sum + country.visits, 0);
|
||||
}
|
||||
|
||||
// Calculate total transits
|
||||
get totalTransits(): number {
|
||||
return this.selectedTransitCountries.reduce((sum, country) => sum + country.visits, 0);
|
||||
}
|
||||
|
||||
// Check if a country is already selected (visit)
|
||||
isVisitCountrySelected(country: Country): boolean {
|
||||
return this.selectedVisitCountries.some(c => c.value === country.value)
|
||||
}
|
||||
|
||||
// Check if a country is already selected (transit)
|
||||
isTransitCountrySelected(country: Country): boolean {
|
||||
return this.selectedTransitCountries.some(c => c.value === country.value);
|
||||
}
|
||||
|
||||
loadTravelPlan(): void {
|
||||
this.isLoading = true;
|
||||
|
||||
forkJoin({
|
||||
countries: this.travelPlanService.getCountriesAndMessages(),
|
||||
travelPlanData: this.travelPlanService.getTravelPlan(this.headerid)
|
||||
}).pipe(finalize(() => {
|
||||
this.isLoading = false;
|
||||
})).subscribe({
|
||||
next: (results) => {
|
||||
this.countries = results.countries as Country[];
|
||||
this.patchTravelPlanData(results.travelPlanData);
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error loading travel plan data', error);
|
||||
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load travel plan data');
|
||||
this.notificationService.showError(errorMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private patchTravelPlanData(plan: TravelPlan): void {
|
||||
|
||||
this.selectedVisitCountries = plan?.entries;
|
||||
this.selectedTransitCountries = plan?.transits;
|
||||
|
||||
if (this.applicationType === 'additional' || this.applicationType === 'extend') {
|
||||
this.needsInsurance.emit(this.selectedVisitCountries.length > 0)
|
||||
}
|
||||
|
||||
// Patch form values
|
||||
this.travelForm.patchValue({
|
||||
usaEntries: plan?.usSets === 0 ? '' : plan?.usSets,
|
||||
visitsInput: 1,
|
||||
transitsInput: 1 // Reset to default
|
||||
});
|
||||
|
||||
if (this.isViewMode) {
|
||||
this.travelForm.disable();
|
||||
}
|
||||
|
||||
if (this.applicationType === 'additional' || this.applicationType === 'extend') {
|
||||
const usentriesControl = this.travelForm.get('usaEntries');
|
||||
usentriesControl?.setValidators([Validators.min(0), Validators.max(99)]);
|
||||
usentriesControl?.updateValueAndValidity();
|
||||
}
|
||||
|
||||
this.completed.emit(this.travelForm.valid || this.travelForm.disabled
|
||||
|| ((this.applicationType === 'additional' || this.applicationType === 'extend')
|
||||
&& (this.travelForm.value.usaEntries
|
||||
|| this.selectedVisitCountries.length > 0 || this.selectedTransitCountries.length > 0)));
|
||||
}
|
||||
|
||||
onSubmit(): void {
|
||||
if (this.travelForm.invalid) {
|
||||
this.travelForm.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
|
||||
this.showAdditionalSetsValidationMessage = false;
|
||||
if ((this.applicationType === 'additional' || this.applicationType === 'extend')
|
||||
&& (!this.travelForm.value.usaEntries &&
|
||||
this.selectedVisitCountries.length === 0 &&
|
||||
this.selectedTransitCountries.length === 0
|
||||
)) {
|
||||
this.showAdditionalSetsValidationMessage = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const travelPlan: TravelPlan = {
|
||||
usSets: this.travelForm.value.usaEntries,
|
||||
entries: this.selectedVisitCountries,
|
||||
transits: this.selectedTransitCountries
|
||||
};
|
||||
|
||||
this.changeInProgress = true;
|
||||
|
||||
this.travelPlanService.saveTravelPlan(this.headerid, travelPlan).pipe(finalize(() => {
|
||||
this.changeInProgress = false;
|
||||
})).subscribe({
|
||||
next: () => {
|
||||
this.notificationService.showSuccess('Travel plan saved successfully');
|
||||
this.completed.emit(true);
|
||||
},
|
||||
error: (error) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to save travel plan');
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error saving travel plan data : ', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
51
src/app/carnet/view/view-carnet.component.html
Normal file
51
src/app/carnet/view/view-carnet.component.html
Normal file
@ -0,0 +1,51 @@
|
||||
<div class="carnet-action-buttons">
|
||||
<button mat-button (click)="accordion().openAll()">Expand All</button>
|
||||
<button mat-button (click)="accordion().closeAll()">Collapse All</button>
|
||||
</div>
|
||||
<mat-accordion class="carnet-headers-align" multi>
|
||||
|
||||
<mat-expansion-panel *ngIf="applicationType==='extend'">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title> Extension Application </mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<app-replacement-set [headerid]=" headerid" [isViewMode]="isViewMode">
|
||||
</app-replacement-set>
|
||||
</mat-expansion-panel>
|
||||
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title> Application Name </mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<app-application [isViewMode]="isViewMode" [applicationName]="applicationName">
|
||||
</app-application>
|
||||
</mat-expansion-panel>
|
||||
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title> Holder Selection </mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<app-holder [headerid]="headerid" [isViewMode]="isViewMode">
|
||||
</app-holder>
|
||||
</mat-expansion-panel>
|
||||
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title> Goods Section </mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<app-goods [headerid]="headerid" [isViewMode]="isViewMode" [userPreferences]="userPreferences">
|
||||
</app-goods>
|
||||
</mat-expansion-panel>
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title> Travel Plan </mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<app-travel-plan [headerid]="headerid" [isViewMode]="isViewMode"></app-travel-plan>
|
||||
</mat-expansion-panel>
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title> Shipping & Payment </mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<app-shipping [headerid]="headerid" [isViewMode]="isViewMode">
|
||||
</app-shipping>
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
||||
8
src/app/carnet/view/view-carnet.component.scss
Normal file
8
src/app/carnet/view/view-carnet.component.scss
Normal file
@ -0,0 +1,8 @@
|
||||
.carnet-headers-align .mat-expansion-panel-header-description {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.carnet-headers-align .mat-mdc-form-field+.mat-mdc-form-field {
|
||||
margin-left: 8px;
|
||||
}
|
||||
55
src/app/carnet/view/view-carnet.component.ts
Normal file
55
src/app/carnet/view/view-carnet.component.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { afterNextRender, Component, inject, 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 { CommonModule } from '@angular/common';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { UserPreferencesService } from '../../core/services/user-preference.service';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { ApplicationComponent } from '../application/application.component';
|
||||
import { GoodsComponent } from '../goods/goods.component';
|
||||
import { HolderComponent } from '../holder/holder.component';
|
||||
import { ShippingComponent } from '../shipping/shipping.component';
|
||||
import { TravelPlanComponent } from '../travel-plan/travel-plan.component';
|
||||
import { ReplacementSetComponent } from '../replacement-set/replacement-set.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-view-carnet',
|
||||
imports: [AngularMaterialModule, CommonModule, ReactiveFormsModule,
|
||||
ApplicationComponent, HolderComponent, GoodsComponent, TravelPlanComponent, ShippingComponent, ReplacementSetComponent],
|
||||
templateUrl: './view-carnet.component.html',
|
||||
styleUrl: './view-carnet.component.scss'
|
||||
})
|
||||
export class ViewCarnetComponent {
|
||||
accordion = viewChild.required(MatAccordion);
|
||||
isViewMode = true;
|
||||
headerid: number = 0;
|
||||
applicationName: string = '';
|
||||
userPreferences: UserPreferences;
|
||||
applicationType: 'new' | 'edit' | 'additional' | 'duplicate' | 'extend' | null = 'edit';
|
||||
|
||||
private route = inject(ActivatedRoute);
|
||||
|
||||
constructor(userPrefenceService: UserPreferencesService) {
|
||||
this.userPreferences = userPrefenceService.getPreferences();
|
||||
|
||||
// set application type based on the route url
|
||||
const urlSegments = this.route.snapshot.url.map(segment => segment.path);
|
||||
|
||||
if (urlSegments.includes('view-extend-carnet')) {
|
||||
this.applicationType = 'extend';
|
||||
}
|
||||
|
||||
afterNextRender(() => {
|
||||
// Open all panels
|
||||
this.accordion().openAll();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
const idParam = this.route.snapshot.paramMap.get('headerid');
|
||||
this.headerid = idParam ? parseInt(idParam, 10) : 0;
|
||||
|
||||
this.applicationName = this.route.snapshot.queryParamMap.get('applicationname') || '';
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
<footer class="footer">
|
||||
<footer class="footer" [ngClass]="{'general': !isUserLoggedIn}">
|
||||
<div class="footer-content">
|
||||
<div class="footer-links">
|
||||
<a href="#">Privacy Policy</a>
|
||||
@ -7,7 +7,7 @@
|
||||
</div>
|
||||
|
||||
<div class="copyright">
|
||||
© {{ currentYear }} USCIB Carnet Portal. All rights reserved.
|
||||
© {{ currentYear }} Alpha Omega Infosys. All rights reserved.
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
@ -1,9 +1,8 @@
|
||||
.footer {
|
||||
color: var(--mat-sys-primary);
|
||||
background: var(--mat-sys-primary-container);
|
||||
color: var(--mat-sys-on-primary);
|
||||
background: var(--mat-sys-primary);
|
||||
padding: 8px;
|
||||
border-top: 1px solid var(--mat-sys-outline-variant);
|
||||
backdrop-filter: blur(10px);
|
||||
|
||||
/* Fixed footer styles */
|
||||
position: fixed;
|
||||
@ -20,6 +19,7 @@
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: var(--mat-sys-primary);
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
@ -27,22 +27,46 @@
|
||||
gap: 24px;
|
||||
|
||||
a {
|
||||
color: var(--mat-sys-secondary);
|
||||
color: var(--mat-sys-on-primary);
|
||||
background: var(--mat-sys-primary);
|
||||
text-decoration: none;
|
||||
transition: color 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--mat-sys-primary);
|
||||
transform: scale(103%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.copyright {
|
||||
color: var(--mat-sys-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* footer styles - not logged in users */
|
||||
.footer.general {
|
||||
color: #000000;
|
||||
background: #ffffff;
|
||||
|
||||
.footer-content {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
a {
|
||||
color: #000000;
|
||||
background: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.footer {
|
||||
padding: 16px;
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-footer',
|
||||
imports: [AngularMaterialModule],
|
||||
imports: [AngularMaterialModule, CommonModule],
|
||||
templateUrl: './footer.component.html',
|
||||
styleUrl: './footer.component.scss'
|
||||
})
|
||||
export class FooterComponent {
|
||||
currentYear = new Date().getFullYear();
|
||||
@Input() isUserLoggedIn = false;
|
||||
|
||||
}
|
||||
@ -6,17 +6,6 @@
|
||||
|
||||
<div class="navbar-menu">
|
||||
<button mat-button (click)="navigateTo('home')">Home</button>
|
||||
<button mat-button (click)="navigateTo('usersettings')">User Settings</button>
|
||||
<!--
|
||||
<button mat-icon-button [matMenuTriggerFor]="profile">
|
||||
<mat-icon>account_circle</mat-icon>
|
||||
</button>
|
||||
<mat-menu #profile="matMenu">
|
||||
<button mat-menu-item (click)="logout()">
|
||||
<mat-icon>logout</mat-icon>
|
||||
<span>Logout</span>
|
||||
</button>
|
||||
</mat-menu> -->
|
||||
|
||||
<div class="profile-container">
|
||||
<button mat-icon-button (click)="toggleProfileMenu()" class="profile-button">
|
||||
@ -29,6 +18,11 @@
|
||||
<span>{{ userEmail }}</span>
|
||||
</div>
|
||||
|
||||
<button mat-menu-item (click)="navigateTo('usersettings')">
|
||||
<mat-icon>settings</mat-icon>
|
||||
<span>User Preferences</span>
|
||||
</button>
|
||||
|
||||
<button mat-menu-item (click)="logout()">
|
||||
<mat-icon>logout</mat-icon>
|
||||
<span>Logout</span>
|
||||
|
||||
@ -4,9 +4,9 @@
|
||||
align-items: center;
|
||||
padding: 0 24px;
|
||||
height: 64px;
|
||||
color: var(--mat-sys-primary);
|
||||
background: var(--mat-sys-primary-container);
|
||||
box-shadow: var(--mat-sys-level2);
|
||||
color: var(--custom-nav-color);
|
||||
background: var(--custom-nav-background-color);
|
||||
border-bottom: 1px solid var(--mat-sys-outline-variant);
|
||||
position: relative;
|
||||
z-index: 100; // Higher than footer's z-index
|
||||
|
||||
@ -31,6 +31,20 @@
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
|
||||
button {
|
||||
color: var(--custom-nav-color);
|
||||
background-color: var(--custom-nav-background-color);
|
||||
|
||||
&:hover {
|
||||
color: var(--custom-nav-background-color);
|
||||
background-color: var(--custom-nav-color);
|
||||
}
|
||||
}
|
||||
|
||||
.mat-mdc-button:hover>.mat-mdc-button-persistent-ripple::before {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
// .nav-link {
|
||||
// cursor: pointer;
|
||||
// padding: 8px 12px;
|
||||
@ -48,15 +62,21 @@
|
||||
position: relative;
|
||||
|
||||
.profile-button {
|
||||
color: var(--mat-sys-primary);
|
||||
background: var(--mat-sys-primary-container);
|
||||
color: var(--custom-nav-background-color);
|
||||
background-color: var(--custom-nav-color);
|
||||
|
||||
&:hover {
|
||||
color: var(--custom-nav-color);
|
||||
background-color: var(--custom-nav-background-color);
|
||||
}
|
||||
}
|
||||
|
||||
.profile-menu {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 48px;
|
||||
background-color: var(--mat-sys-secondary-container);
|
||||
color: var(--mat-sys-on-primary-container);
|
||||
background-color: var(--mat-sys-primary-container);
|
||||
border-radius: 4px;
|
||||
box-shadow: var(--mat-sys-level3);
|
||||
min-width: 200px;
|
||||
@ -68,12 +88,12 @@
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
background-color: #fff;
|
||||
color: var(--mat-sys-secondary);
|
||||
color: var(--mat-sys-on-primary-container);
|
||||
background-color: var(--mat-sys-primary-container);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
||||
|
||||
mat-icon {
|
||||
color: var(--mat-sys-secondary);
|
||||
color: var(--mat-sys-on-primary-container);
|
||||
}
|
||||
}
|
||||
|
||||
@ -85,15 +105,16 @@
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
background-color: var(--mat-sys-surface-container);
|
||||
color: var(--mat-sys-secondary);
|
||||
color: var(--mat-sys-on-primary-container);
|
||||
background-color: var(--mat-sys-primary-container);
|
||||
|
||||
mat-icon {
|
||||
color: var(--mat-sys-secondary);
|
||||
}
|
||||
// mat-icon {
|
||||
// color: var(--mat-sys-on-primary-container);
|
||||
// }
|
||||
|
||||
&:hover {
|
||||
background-color: var(--mat-sys-secondary-container);
|
||||
color: var(--mat-sys-on-primary);
|
||||
background-color: var(--mat-sys-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
13
src/app/core/models/carnet/fee.ts
Normal file
13
src/app/core/models/carnet/fee.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export interface Fees {
|
||||
basicFee?: number | null | undefined;
|
||||
counterFoilFee?: number | null | undefined;
|
||||
continuationSheetFee?: number | null | undefined;
|
||||
expeditedFee?: number | null | undefined;
|
||||
shippingFee?: number | null | undefined;
|
||||
bondPremium?: number | null | undefined;
|
||||
cargoPremium?: number | null | undefined;
|
||||
ldiPremium?: number | null | undefined;
|
||||
securityAmount?: number | null | undefined;
|
||||
securityType?: string | null | undefined;
|
||||
insuredValue?: number | null | undefined;
|
||||
}
|
||||
@ -2,8 +2,8 @@ export interface Goods {
|
||||
commercialSample: boolean;
|
||||
professionalEquipment: boolean;
|
||||
exhibitionsFair: boolean;
|
||||
roadVehiclesUsed: boolean;
|
||||
horseUsed: boolean;
|
||||
roadVehiclesUsed: string;
|
||||
horseUsed: string;
|
||||
authorizedRepresentatives: string;
|
||||
items?: GoodsItem[] | null;
|
||||
}
|
||||
|
||||
@ -1,2 +1,16 @@
|
||||
export interface Holder {
|
||||
holderid: number;
|
||||
locationid?: number;
|
||||
holderName: string;
|
||||
dbaName?: string | null;
|
||||
holderNumber: string;
|
||||
holderType: string;
|
||||
uscibMember: boolean;
|
||||
govAgency?: boolean;
|
||||
address1: string;
|
||||
address2: string;
|
||||
city: string;
|
||||
state: string;
|
||||
country: string;
|
||||
zip: string;
|
||||
}
|
||||
|
||||
9
src/app/core/models/carnet/payment-details.ts
Normal file
9
src/app/core/models/carnet/payment-details.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export interface PaymentDetail {
|
||||
headerid?: number;
|
||||
applicationName?: string;
|
||||
price?: string;
|
||||
description?: string;
|
||||
orderId?: string;
|
||||
paymentStatus?: string;
|
||||
paymentErrorDetails?: string;
|
||||
}
|
||||
15
src/app/core/models/carnet/replacement.ts
Normal file
15
src/app/core/models/carnet/replacement.ts
Normal file
@ -0,0 +1,15 @@
|
||||
export interface Replacement {
|
||||
replacementCarnetNumber: string;
|
||||
issueDate: Date;
|
||||
preparerName: string;
|
||||
address1: string;
|
||||
address2?: string | null;
|
||||
city: string;
|
||||
state: string;
|
||||
country: string;
|
||||
zip: string;
|
||||
portName: string;
|
||||
portCountry: string;
|
||||
reasonForExtension: string;
|
||||
duration: number;
|
||||
}
|
||||
@ -1,4 +1,6 @@
|
||||
export interface ShippingAddress {
|
||||
companyName: string;
|
||||
addressid: number;
|
||||
address1: string;
|
||||
address2?: string | null;
|
||||
city: string;
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
export interface ShippingContact {
|
||||
contactid: number;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
middleInitial?: string | null;
|
||||
@ -9,4 +10,6 @@ export interface ShippingContact {
|
||||
email: string;
|
||||
refNumber?: string | null;
|
||||
notes?: string | null;
|
||||
defaultContact?: boolean;
|
||||
isInactive?: boolean;
|
||||
}
|
||||
|
||||
@ -3,9 +3,10 @@ import { ShippingContact } from "./shipping-contact";
|
||||
|
||||
export interface Shipping {
|
||||
// Insurance details
|
||||
needsBond: boolean;
|
||||
// needsBond: boolean;
|
||||
needsInsurance: boolean;
|
||||
needsLostDocProtection: boolean;
|
||||
formOfSecurity: string;
|
||||
|
||||
// Shipping details
|
||||
shipTo: string;
|
||||
@ -19,5 +20,5 @@ export interface Shipping {
|
||||
|
||||
// Payment details
|
||||
paymentMethod: string;
|
||||
notes?: string;
|
||||
paymentNotes?: string;
|
||||
}
|
||||
|
||||
@ -1,2 +1,17 @@
|
||||
export interface TravelPlan {
|
||||
usSets?: number;
|
||||
entries: TravelEntry[];
|
||||
transits: TravelEntry[];
|
||||
}
|
||||
|
||||
export interface Country {
|
||||
name?: string;
|
||||
value: string;
|
||||
actionMessage?: string | null;
|
||||
actionType?: string | null;
|
||||
action?: string | null;
|
||||
}
|
||||
|
||||
export interface TravelEntry extends Country {
|
||||
visits: number;
|
||||
}
|
||||
|
||||
5
src/app/core/models/contact-title.ts
Normal file
5
src/app/core/models/contact-title.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface ContactTitle {
|
||||
name: string;
|
||||
id: string;
|
||||
value: string;
|
||||
}
|
||||
@ -2,4 +2,6 @@ export interface DeliveryType {
|
||||
name: string;
|
||||
id: string;
|
||||
value: string;
|
||||
daysToDelivery: string;
|
||||
cutOffTime: string;
|
||||
}
|
||||
|
||||
5
src/app/core/models/extension-reason.ts
Normal file
5
src/app/core/models/extension-reason.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface ExtensionReason {
|
||||
name: string;
|
||||
id: string;
|
||||
value: string;
|
||||
}
|
||||
6
src/app/core/models/formofsecurity.ts
Normal file
6
src/app/core/models/formofsecurity.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export interface FormOfSecurity {
|
||||
name: string;
|
||||
id: string;
|
||||
value: string;
|
||||
isGov: boolean;
|
||||
}
|
||||
18
src/app/core/models/holder/basic-detail.ts
Normal file
18
src/app/core/models/holder/basic-detail.ts
Normal file
@ -0,0 +1,18 @@
|
||||
export interface BasicDetail {
|
||||
holderid?: number;
|
||||
spid: number;
|
||||
locationid: number;
|
||||
holderName: string;
|
||||
dbaName?: string | null;
|
||||
holderNumber: string;
|
||||
holderType: string;
|
||||
uscibMember: boolean;
|
||||
govAgency?: boolean;
|
||||
address1: string;
|
||||
address2: string;
|
||||
city: string;
|
||||
state: string;
|
||||
country: string;
|
||||
zip: string;
|
||||
isInactive?: boolean | null;
|
||||
}
|
||||
19
src/app/core/models/holder/contact.ts
Normal file
19
src/app/core/models/holder/contact.ts
Normal file
@ -0,0 +1,19 @@
|
||||
export interface Contact {
|
||||
holdercontactid: number;
|
||||
holderid: number;
|
||||
spid: number;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
middleInitial: string | null;
|
||||
title: string;
|
||||
phone: string;
|
||||
mobile: string;
|
||||
fax?: string | null;
|
||||
email: string;
|
||||
dateCreated?: Date | null;
|
||||
createdBy?: string | null;
|
||||
lastUpdatedBy?: string | null;
|
||||
lastUpdatedDate?: Date | null;
|
||||
isInactive?: boolean | null;
|
||||
inactivatedDate?: Date | null;
|
||||
}
|
||||
4
src/app/core/models/holder/holder-filter.ts
Normal file
4
src/app/core/models/holder/holder-filter.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export interface HolderFilter {
|
||||
holderName?: string;
|
||||
holderId?: number;
|
||||
}
|
||||
@ -17,4 +17,5 @@ export interface UserDetail {
|
||||
urlKey: string;
|
||||
logoName: string;
|
||||
themeName: string;
|
||||
serviceProviderName: string;
|
||||
}
|
||||
@ -3,7 +3,7 @@ import { environment } from '../../../../environments/environment';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { UserService } from '../common/user.service';
|
||||
import { ApplicationDetail } from '../../models/carnet/application-detail';
|
||||
import { filter, map, Observable } from 'rxjs';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@ -15,21 +15,6 @@ export class ApplicationDetailService {
|
||||
private http = inject(HttpClient);
|
||||
private userService = inject(UserService);
|
||||
|
||||
// getApplicationDetailsById(id: number): ApplicationDetail | any {
|
||||
// return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetPreparerByClientid/${this.userService.getUserSpid()}/${id}`).pipe(
|
||||
// filter(response => response.length > 0),
|
||||
// map(response => this.mapToApplicationDetail(response?.[0])));
|
||||
// }
|
||||
|
||||
// private mapToApplicationDetail(applicationDetails: any): ApplicationDetail {
|
||||
// return {
|
||||
// clientid: applicationDetails.CLIENTID,
|
||||
// spid: applicationDetails.SPID,
|
||||
// name: applicationDetails.P_APPLICATIONNAME,
|
||||
// applicationId: applicationDetails.P_HEADERID
|
||||
// };
|
||||
// }
|
||||
|
||||
createApplicationDetails(data: ApplicationDetail): Observable<any> {
|
||||
const userDetails = this.userService.getUserDetails();
|
||||
|
||||
|
||||
140
src/app/core/services/carnet/carnet.service.ts
Normal file
140
src/app/core/services/carnet/carnet.service.ts
Normal file
@ -0,0 +1,140 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { Observable, map } from 'rxjs';
|
||||
import { environment } from '../../../../environments/environment';
|
||||
import { UserService } from '../common/user.service';
|
||||
import { Fees } from '../../models/carnet/fee';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class CarnetService {
|
||||
private apiUrl = environment.apiUrl;
|
||||
private apiDb = environment.apiDb;
|
||||
|
||||
private http = inject(HttpClient);
|
||||
private userService = inject(UserService);
|
||||
|
||||
getEstimatedFees(headerid: number): Observable<Fees> {
|
||||
return this.http.get<any>(`${this.apiUrl}/${this.apiDb}/EstimatedFees/${this.userService.getUserSpid()}/${this.userService.getUser()}/${headerid}`).pipe(
|
||||
map(response => this.mapToFeesData(response)));
|
||||
}
|
||||
|
||||
private mapToFeesData(estimatedFeesDetails: any): Fees {
|
||||
let estimatedFeesData: Fees = {
|
||||
basicFee: estimatedFeesDetails.BASICFEE,
|
||||
counterFoilFee: estimatedFeesDetails.CFFEE,
|
||||
continuationSheetFee: estimatedFeesDetails.CSFEE,
|
||||
expeditedFee: estimatedFeesDetails.EFFEE,
|
||||
shippingFee: estimatedFeesDetails.SHIPFEE,
|
||||
bondPremium: estimatedFeesDetails.BONDPREMIUM,
|
||||
cargoPremium: estimatedFeesDetails.CARGOPREMIUM,
|
||||
ldiPremium: estimatedFeesDetails.LDIPREMIUM,
|
||||
securityAmount: estimatedFeesDetails.SECURITYAMOUNT,
|
||||
securityType: estimatedFeesDetails.SECURITYTYPE,
|
||||
insuredValue: estimatedFeesDetails.INSUREDVALUE
|
||||
};
|
||||
|
||||
return estimatedFeesData;
|
||||
}
|
||||
|
||||
submitApplication(headerid: number): Observable<any> {
|
||||
|
||||
let appData = {
|
||||
P_SPID: this.userService.getUserSpid(),
|
||||
P_USERID: this.userService.getUser(),
|
||||
P_HEADERID: headerid
|
||||
}
|
||||
|
||||
return this.http.post<any>(`${this.apiUrl}/${this.apiDb}/TransmitApplicationtoProcess`, appData);
|
||||
}
|
||||
|
||||
processApplication(headerid: number): Observable<any> {
|
||||
|
||||
let appData = {
|
||||
// P_SPID: this.userService.getUserSpid(),
|
||||
P_USERID: this.userService.getUser(),
|
||||
P_HEADERID: headerid
|
||||
}
|
||||
|
||||
return this.http.patch<any>(`${this.apiUrl}/${this.apiDb}//ProcessCarnet`, appData);
|
||||
}
|
||||
|
||||
deleteCarnet(headerid: number): Observable<any> {
|
||||
const data = {
|
||||
P_USERID: this.userService.getUser(),
|
||||
P_HEADERID: headerid
|
||||
}
|
||||
return this.http.delete(`${this.apiUrl}/${this.apiDb}/DeleteCarnet`, { body: data });
|
||||
}
|
||||
|
||||
printCarnet(headerid: number): Observable<any> {
|
||||
const data = {
|
||||
P_SPID: this.userService.getUserSpid(),
|
||||
P_HEADERID: headerid,
|
||||
P_PRINTGL: 'Y'
|
||||
}
|
||||
return this.http.post(`${this.apiUrl}/${this.apiDb}/PrintCarnet`, data, {
|
||||
responseType: 'blob', // Important: Set responseType to 'blob'
|
||||
});
|
||||
}
|
||||
|
||||
printGeneralList(headerid: number): Observable<any> {
|
||||
const data = {
|
||||
P_SPID: this.userService.getUserSpid(),
|
||||
P_HEADERID: headerid
|
||||
}
|
||||
return this.http.post(`${this.apiUrl}/${this.apiDb}/PrintGL`, data, {
|
||||
responseType: 'blob', // Important: Set responseType to 'blob'
|
||||
});
|
||||
}
|
||||
|
||||
duplicateCarnet(carnetNumber: string): Observable<any> {
|
||||
const data = {
|
||||
P_USERID: this.userService.getUser(),
|
||||
P_CARNETNO: carnetNumber
|
||||
}
|
||||
return this.http.patch(`${this.apiUrl}/${this.apiDb}/DuplicateCarnet`, data);
|
||||
}
|
||||
|
||||
extendCarnet(carnetNumber: string): Observable<any> {
|
||||
const data = {
|
||||
P_USERID: this.userService.getUser(),
|
||||
P_CARNETNO: carnetNumber
|
||||
}
|
||||
return this.http.patch(`${this.apiUrl}/${this.apiDb}/ExtendCarnet`, data);
|
||||
}
|
||||
|
||||
additionalSets(carnetNumber: string): Observable<any> {
|
||||
const data = {
|
||||
P_USERID: this.userService.getUser(),
|
||||
P_CARNETNO: carnetNumber
|
||||
}
|
||||
return this.http.patch(`${this.apiUrl}/${this.apiDb}/AddlSets`, data);
|
||||
}
|
||||
|
||||
copyCarnet(headerid: number, applicationName: string): Observable<any> {
|
||||
const data = {
|
||||
P_USERID: this.userService.getUser(),
|
||||
P_HEADERID: headerid,
|
||||
P_APPLICATIONNAME: applicationName
|
||||
}
|
||||
return this.http.patch(`${this.apiUrl}/${this.apiDb}/CopyCarnet`, data);
|
||||
}
|
||||
|
||||
resetCarnet(headerid: number): Observable<any> {
|
||||
const data = {
|
||||
P_USERID: this.userService.getUser(),
|
||||
P_HEADERID: headerid
|
||||
}
|
||||
return this.http.patch(`${this.apiUrl}/${this.apiDb}/ResetCarnet`, data);
|
||||
}
|
||||
|
||||
voidCarnet(headerid: number): Observable<any> {
|
||||
const data = {
|
||||
P_USERID: this.userService.getUser(),
|
||||
P_HEADERID: headerid
|
||||
}
|
||||
return this.http.patch(`${this.apiUrl}/${this.apiDb}/VoidCarnet`, data);
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,7 @@ 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';
|
||||
import { map, Observable } from 'rxjs';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@ -17,14 +17,13 @@ export class GoodsService {
|
||||
|
||||
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])));
|
||||
map(response => this.mapToGoods(response)));
|
||||
}
|
||||
|
||||
private mapToGoods(goodDetails: any): Goods {
|
||||
return {
|
||||
roadVehiclesUsed: goodDetails.AUTOFLAG === 'Y',
|
||||
horseUsed: goodDetails.HORSEFLAG === 'Y',
|
||||
roadVehiclesUsed: goodDetails.AUTOFLAG,
|
||||
horseUsed: goodDetails.HORSEFLAG,
|
||||
exhibitionsFair: goodDetails.EXHIBITIONSFAIRFLAG === 'Y',
|
||||
professionalEquipment: goodDetails.PROFEQUIPMENTFLAG === 'Y',
|
||||
commercialSample: goodDetails.COMMERCIALSAMPLEFLAG === 'Y',
|
||||
@ -57,8 +56,8 @@ export class GoodsService {
|
||||
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_AUTOFLAG: goodsData.roadVehiclesUsed,
|
||||
P_HORSEFLAG: goodsData.horseUsed,
|
||||
P_AUTHREP: goodsData.authorizedRepresentatives,
|
||||
}
|
||||
|
||||
@ -109,19 +108,20 @@ export class GoodsService {
|
||||
|
||||
deleteGoodsItem(headerid: number, item: GoodsItem): Observable<any> {
|
||||
|
||||
const goodsItem = {
|
||||
ITEMNO: item.itemNumber,
|
||||
ITEMDESCRIPTION: item.description,
|
||||
NOOFPIECES: item.pieces,
|
||||
ITEMWEIGHT: item.weight,
|
||||
ITEMWEIGHTUOM: item.unitOfMeasure,
|
||||
ITEMVALUE: item.value,
|
||||
GOODSORIGINCOUNTRY: item.countryOfOrigin,
|
||||
}
|
||||
|
||||
const goodsItemsObj = {
|
||||
P_HEADERID: headerid,
|
||||
P_GLTABLE: [goodsItem],
|
||||
P_ITEMNO: item.itemNumber,
|
||||
P_USERID: this.userService.getUser(),
|
||||
};
|
||||
|
||||
// delete request with body
|
||||
return this.http.delete(`${this.apiUrl}/${this.apiDb}/DeleteGenerallistItems`, { body: goodsItemsObj });
|
||||
}
|
||||
|
||||
deleteAllGoodsItems(headerid: number): Observable<any> {
|
||||
const goodsItemsObj = {
|
||||
P_HEADERID: headerid,
|
||||
P_ITEMNO: 99999, // special value to indicate all items
|
||||
P_USERID: this.userService.getUser(),
|
||||
};
|
||||
|
||||
|
||||
@ -1,9 +1,51 @@
|
||||
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 { Holder } from '../../models/carnet/holder';
|
||||
import { map, Observable } from 'rxjs';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class HolderService {
|
||||
private apiUrl = environment.apiUrl;
|
||||
private apiDb = environment.apiDb;
|
||||
|
||||
constructor() { }
|
||||
private http = inject(HttpClient);
|
||||
private userService = inject(UserService);
|
||||
|
||||
getHolder(id: number): Observable<Holder> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetHolderstoEdit/${this.userService.getUserSpid()}/${this.userService.getUser()}/${id}`).pipe(
|
||||
map(response => this.mapToHolder(response)));
|
||||
}
|
||||
|
||||
private mapToHolder(holder: any): Holder {
|
||||
return {
|
||||
holderid: holder.HOLDERID,
|
||||
locationid: holder.LOCATIONID,
|
||||
holderNumber: holder.HOLDERNO,
|
||||
holderType: holder.HOLDERTYPE,
|
||||
uscibMember: holder.USCIBMEMBERFLAG === 'Y',
|
||||
// govAgency: holder.GOVAGENCYFLAG === 'Y',
|
||||
holderName: holder.NAMEOF,
|
||||
//NAMEQUALIFIER: holder.NAMEQUALIFIER || null,
|
||||
dbaName: holder.ADDLNAME || null,
|
||||
address1: holder.ADDRESS1,
|
||||
address2: holder.ADDRESS2 || null,
|
||||
city: holder.CITY,
|
||||
state: holder.STATE,
|
||||
zip: holder.ZIP,
|
||||
country: holder.COUNTRY
|
||||
};
|
||||
}
|
||||
|
||||
saveApplicationHolder(headerId: number, holderId: number) {
|
||||
const applicationHolder = {
|
||||
P_HEADERID: headerId,
|
||||
P_HOLDERID: holderId
|
||||
}
|
||||
|
||||
return this.http.patch(`${this.apiUrl}/${this.apiDb}/update-holder`, applicationHolder);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class InsuranceService {
|
||||
|
||||
constructor() { }
|
||||
}
|
||||
56
src/app/core/services/carnet/payment.service.ts
Normal file
56
src/app/core/services/carnet/payment.service.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { environment } from '../../../../environments/environment';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { PaymentDetail } from '../../models/carnet/payment-details';
|
||||
import { UserService } from '../common/user.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class PaymentService {
|
||||
private apiUrl = environment.apiUrl;
|
||||
private apiDb = environment.apiDb;
|
||||
|
||||
private http = inject(HttpClient);
|
||||
private userService = inject(UserService);
|
||||
|
||||
createOrder(paymentDetails: PaymentDetail): Promise<string> {
|
||||
const data = {
|
||||
P_APPLICATIONNAME: paymentDetails.applicationName,
|
||||
P_PRICE: paymentDetails.price,
|
||||
P_DESCRIPTION: paymentDetails.description,
|
||||
P_USERID: this.userService.getUser()
|
||||
}
|
||||
|
||||
return firstValueFrom(
|
||||
this.http.post<{ id: string }>(`${this.apiUrl}/${this.apiDb}/InitiatePayment`, data)
|
||||
).then(order => order.id);
|
||||
}
|
||||
|
||||
completePayment(paymentDetails: PaymentDetail): Promise<any> {
|
||||
const data = {
|
||||
P_ORDERID: paymentDetails.orderId,
|
||||
P_APPLICATIONNAME: paymentDetails.applicationName,
|
||||
P_PRICE: paymentDetails.price,
|
||||
P_USERID: this.userService.getUser()
|
||||
}
|
||||
|
||||
return firstValueFrom(
|
||||
this.http.post(`${this.apiUrl}/${this.apiDb}/CompletePayment`, data));
|
||||
}
|
||||
|
||||
logTransactionDetails(paymentDetails: PaymentDetail): Promise<any> {
|
||||
const data = {
|
||||
P_APPLICATIONNAME: paymentDetails.applicationName,
|
||||
P_ORDERID: paymentDetails.orderId,
|
||||
P_PAYMENTAMOUNT: paymentDetails.price,
|
||||
P_STATUS: paymentDetails.paymentStatus,
|
||||
P_USERID: this.userService.getUser(),
|
||||
P_PAYMENTERROR: paymentDetails.paymentErrorDetails
|
||||
}
|
||||
|
||||
return firstValueFrom(
|
||||
this.http.post(`${this.apiUrl}/${this.apiDb}/SaveHistory`, data));
|
||||
}
|
||||
}
|
||||
59
src/app/core/services/carnet/replacement.service.ts
Normal file
59
src/app/core/services/carnet/replacement.service.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { environment } from '../../../../environments/environment';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { map, Observable } from 'rxjs';
|
||||
import { UserService } from '../common/user.service';
|
||||
import { Replacement } from '../../models/carnet/replacement';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ReplacementService {
|
||||
private apiUrl = environment.apiUrl;
|
||||
private apiDb = environment.apiDb;
|
||||
|
||||
private http = inject(HttpClient);
|
||||
private userService = inject(UserService);
|
||||
|
||||
getExtensionData(headerid: number): Replacement | any {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetExtendedSection/${this.userService.getUserSpid()}/${headerid}`).pipe(
|
||||
map(response => this.mapToExtensionData(response)));
|
||||
}
|
||||
|
||||
private mapToExtensionData(extensionDataCollection: any): Replacement {
|
||||
let extensionData = extensionDataCollection;
|
||||
|
||||
let replacementData: Replacement = {
|
||||
replacementCarnetNumber: extensionData.REPLACECARNETNO,
|
||||
issueDate: extensionData.ISSUEDATE,
|
||||
preparerName: extensionData.PREPARERNAME,
|
||||
address1: extensionData.ADDRESS1,
|
||||
address2: extensionData.ADDRESS2,
|
||||
city: extensionData.CITY,
|
||||
state: extensionData.STATE,
|
||||
country: extensionData.COUNTRY,
|
||||
zip: extensionData.ZIP,
|
||||
portName: extensionData.GOODSPORT,
|
||||
portCountry: extensionData.GOODSCOUNTRY,
|
||||
reasonForExtension: extensionData.REASONCODE,
|
||||
duration: extensionData.EXTENSIONPERIOD,
|
||||
};
|
||||
|
||||
return replacementData;
|
||||
}
|
||||
|
||||
saveExtensionDetails(headerid: number, extensionData: Replacement): Observable<any> {
|
||||
|
||||
const extensionDetails = {
|
||||
P_HEADERID: headerid,
|
||||
P_GOODSPORT: extensionData.portName,
|
||||
P_GOODSCOUNTRY: extensionData.portCountry,
|
||||
P_REASONCODE: extensionData.reasonForExtension,
|
||||
P_EXTENSIONPERIOD: extensionData.duration,
|
||||
P_USERID: this.userService.getUser(),
|
||||
P_SPID: this.userService.getUserSpid()
|
||||
}
|
||||
|
||||
return this.http.post(`${this.apiUrl}/${this.apiDb}/SaveExtensionApplication`, extensionDetails);
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,12 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { environment } from '../../../../environments/environment';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { filter, map, Observable } from 'rxjs';
|
||||
import { filter, map, Observable, of } from 'rxjs';
|
||||
import { UserService } from '../common/user.service';
|
||||
import { Shipping } from '../../models/carnet/shipping';
|
||||
import { ShippingContact } from '../../models/carnet/shipping-contact';
|
||||
import { ShippingAddress } from '../../models/carnet/shipping-address';
|
||||
import { Holder } from '../../models/carnet/holder';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@ -17,8 +20,7 @@ export class ShippingService {
|
||||
|
||||
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])));
|
||||
map(response => this.mapToShippingData(response)));
|
||||
}
|
||||
|
||||
private mapToShippingData(shippingDetails: any): Shipping {
|
||||
@ -30,35 +32,35 @@ export class ShippingService {
|
||||
courierAccount: shippingDetails.CUSTCOURIERNO,
|
||||
paymentMethod: shippingDetails.PAYMENTMETHOD,
|
||||
|
||||
notes: shippingDetails.NOTES,
|
||||
needsBond: shippingDetails.INSPROTECTION === 'Y',
|
||||
needsInsurance: shippingDetails.LDIPROTECTION === 'Y',
|
||||
needsLostDocProtection: shippingDetails.LDIPROTECTION === 'Y'
|
||||
//needsBond: shippingDetails.BONDPROTECTION === 'Y',
|
||||
needsInsurance: shippingDetails.INSPROTECTION === 'ACCEPT',
|
||||
needsLostDocProtection: shippingDetails.LDIPROTECTION === 'ACCEPT',
|
||||
formOfSecurity: shippingDetails.FORMOFSECURITY
|
||||
};
|
||||
|
||||
if (shippingDetails.address) {
|
||||
shippingData.address = {
|
||||
address1: shippingDetails.ADDRESS1,
|
||||
address2: shippingDetails.ADDRESS2,
|
||||
city: shippingDetails.CITY,
|
||||
state: shippingDetails.STATE,
|
||||
zip: shippingDetails.ZIP,
|
||||
country: shippingDetails.COUNTRY
|
||||
}
|
||||
shippingData.address = {
|
||||
companyName: shippingDetails.SHIPNAME,
|
||||
addressid: shippingDetails.SHIPADDRESSID,
|
||||
address1: shippingDetails.ADDRESS1,
|
||||
address2: shippingDetails.ADDRESS2,
|
||||
city: shippingDetails.CITY,
|
||||
state: shippingDetails.STATE,
|
||||
zip: shippingDetails.ZIP,
|
||||
country: shippingDetails.COUNTRY
|
||||
}
|
||||
|
||||
if (shippingDetails.contact) {
|
||||
shippingData.contact = {
|
||||
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,
|
||||
}
|
||||
shippingData.contact = {
|
||||
contactid: shippingDetails.SHIPCONTACTID,
|
||||
firstName: shippingDetails.FIRSTNAME,
|
||||
lastName: shippingDetails.LASTNAME,
|
||||
middleInitial: shippingDetails.MIDDLEINITIAL,
|
||||
title: shippingDetails.TITLE,
|
||||
phone: shippingDetails.PHONENO,
|
||||
mobile: shippingDetails.MOBILENO,
|
||||
fax: shippingDetails.FAXNO,
|
||||
email: shippingDetails.EMAILADDRESS,
|
||||
refNumber: shippingDetails.REFNO,
|
||||
notes: shippingDetails.NOTES,
|
||||
}
|
||||
|
||||
return shippingData;
|
||||
@ -73,31 +75,83 @@ export class ShippingService {
|
||||
P_DELIVERYMETHOD: shippingData.deliveryMethod,
|
||||
P_CUSTCOURIERNO: shippingData.courierAccount,
|
||||
P_PAYMENTMETHOD: shippingData.paymentMethod,
|
||||
P_NOTES: shippingData.notes,
|
||||
|
||||
P_FORMOFSECURITY: shippingData.needsBond ? 'Y' : 'N',
|
||||
P_INSPROTECTION: shippingData.needsInsurance ? 'Y' : 'N',
|
||||
P_LDIPROTECTION: shippingData.needsLostDocProtection ? 'Y' : 'N',
|
||||
// P_BONDPROTECTION: shippingData.needsBond ? 'Y' : 'N',
|
||||
P_INSPROTECTION: shippingData.needsInsurance ? 'ACCEPT' : 'DECLINE',
|
||||
P_LDIPROTECTION: shippingData.needsLostDocProtection ? 'ACCEPT' : 'DECLINE',
|
||||
P_FORMOFSECURITY: shippingData.formOfSecurity,
|
||||
|
||||
address1: shippingData.address?.address1 || '',
|
||||
address2: shippingData.address?.address2 || null,
|
||||
city: shippingData.address?.city || '',
|
||||
state: shippingData.address?.state || '',
|
||||
zip: shippingData.address?.zip || '',
|
||||
country: shippingData.address?.country || '',
|
||||
P_SHIPNAME: shippingData.address?.companyName,
|
||||
P_ADDRESS1: shippingData.address?.address1 || null,
|
||||
P_ADDRESS2: shippingData.address?.address2 || null,
|
||||
P_CITY: shippingData.address?.city || null,
|
||||
P_STATE: shippingData.address?.state || null,
|
||||
P_ZIP: shippingData.address?.zip || null,
|
||||
P_COUNTRY: shippingData.address?.country || null,
|
||||
|
||||
firstName: shippingData.contact?.firstName || '',
|
||||
lastName: shippingData.contact?.lastName || '',
|
||||
middleInitial: shippingData.contact?.middleInitial || null,
|
||||
title: shippingData.contact?.title || '',
|
||||
phone: shippingData.contact?.phone || '',
|
||||
mobile: shippingData.contact?.mobile || '',
|
||||
fax: shippingData.contact?.fax || null,
|
||||
emailAddress: shippingData.contact?.email || '',
|
||||
refNo: shippingData.contact?.refNumber || '',
|
||||
P_SHIPCONTACTID: shippingData.contact?.contactid || 0,
|
||||
P_FIRSTNAME: shippingData.contact?.firstName || '',
|
||||
P_LASTNAME: shippingData.contact?.lastName || '',
|
||||
P_MIDDLEINITIAL: shippingData.contact?.middleInitial || null,
|
||||
P_TITLE: shippingData.contact?.title || '',
|
||||
P_PHONENO: shippingData.contact?.phone || '',
|
||||
P_MOBILENO: shippingData.contact?.mobile || '',
|
||||
P_FAXNO: shippingData.contact?.fax || null,
|
||||
P_EMAILADDRESS: shippingData.contact?.email || '',
|
||||
P_REFNO: shippingData.contact?.refNumber || '',
|
||||
P_NOTES: shippingData.contact?.notes,
|
||||
P_USERID: this.userService.getUser()
|
||||
}
|
||||
|
||||
return this.http.patch(`${this.apiUrl}/${this.apiDb}/UpdateShippingDetails`, shippingDetails);
|
||||
}
|
||||
|
||||
getPreparerContacts(): ShippingContact[] | any {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetPreparerContactsByClientid/${this.userService.getUserSpid()}/${this.userService.getUserClientid()}`).pipe(
|
||||
map(response => this.mapToContacts(response)));
|
||||
}
|
||||
|
||||
getHolderContactsById(id: number): ShippingContact[] | any {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetHolderContacts/${this.userService.getUserSpid()}/${id}`).pipe(
|
||||
map(response => this.mapToContacts(response)));
|
||||
}
|
||||
|
||||
private mapToContacts(data: any[]): ShippingContact[] {
|
||||
return data.map(contact => ({
|
||||
contactid: contact.CLIENTCONTACTID ?? contact.HOLDERCONTACTID,
|
||||
defaultContact: contact.DEFCONTACTFLAG === 'Y',
|
||||
firstName: contact.FIRSTNAME,
|
||||
lastName: contact.LASTNAME,
|
||||
title: contact.TITLE,
|
||||
phone: contact.PHONENO,
|
||||
mobile: contact.MOBILENO,
|
||||
fax: contact.FAXNO || null,
|
||||
email: contact.EMAILADDRESS,
|
||||
middleInitial: contact.MIDDLEINITIAL || null,
|
||||
isInactive: contact.INACTIVEFLAG === 'Y' || false
|
||||
}));
|
||||
}
|
||||
|
||||
getPreparerAddress(): ShippingAddress | any {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetPreparerLocByClientid/${this.userService.getUserSpid()}/${this.userService.getUserClientid()}`).pipe(
|
||||
filter(response => response.length > 0),
|
||||
map(response => this.mapToAddress(response?.[0])));
|
||||
}
|
||||
|
||||
getHolderAddress(holder: Holder): ShippingAddress | any {
|
||||
return of(this.mapToAddress(holder));
|
||||
}
|
||||
|
||||
private mapToAddress(address: any): ShippingAddress {
|
||||
return {
|
||||
companyName: address.NAMEOF ?? address.holderName,
|
||||
addressid: address.LOCATIONID ?? address.locationid,
|
||||
address1: address.ADDRESS1 ?? address.address1,
|
||||
address2: address.ADDRESS2 ?? address.address2,
|
||||
city: address.CITY ?? address.city,
|
||||
state: address.STATE ?? address.state,
|
||||
zip: address.ZIP ?? address.zip,
|
||||
country: address.COUNTRY ?? address.country
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,93 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { Observable, map } from 'rxjs';
|
||||
import { environment } from '../../../../environments/environment';
|
||||
import { Country, TravelPlan } from '../../models/carnet/travel-plan';
|
||||
import { UserService } from '../common/user.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class TravelPlanService {
|
||||
private apiUrl = environment.apiUrl;
|
||||
private apiDb = environment.apiDb;
|
||||
|
||||
constructor() { }
|
||||
private http = inject(HttpClient);
|
||||
private userService = inject(UserService);
|
||||
|
||||
getCountriesAndMessages(): Observable<Country[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetCountriesAndMessages/${this.userService.getUserSpid()}`).pipe(
|
||||
map((response) =>
|
||||
response.map((item) => ({
|
||||
name: item.COUNTRYNAME,
|
||||
value: item.COUNTRYCODE,
|
||||
actionMessage: item.COUNTRYMESSAGE,
|
||||
actionType: item.ADDLPARAMVALUE1,
|
||||
action: item.ADDLPARAMVALUE2
|
||||
}))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
getTravelPlan(headerid: number): Observable<TravelPlan> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetCountryDetailstoEdit/${this.userService.getUserSpid()}/${this.userService.getUser()}/${headerid}`).pipe(
|
||||
map(response => this.mapToTravelPlanData(response)));
|
||||
}
|
||||
|
||||
private mapToTravelPlanData(travelPlanDetails: any): TravelPlan {
|
||||
|
||||
if (!travelPlanDetails?.P_COUNTRYTABLE && !travelPlanDetails?.NOOFUSSETS) {
|
||||
return {
|
||||
entries: [],
|
||||
transits: []
|
||||
} as TravelPlan;
|
||||
}
|
||||
|
||||
const countryTable: any[] = travelPlanDetails.P_COUNTRYTABLE;
|
||||
|
||||
let travelPlanData: TravelPlan = {
|
||||
usSets: travelPlanDetails.NOOFUSSETS,
|
||||
entries: countryTable
|
||||
.filter((item) => item.VISISTTRANSITIND === 'V')
|
||||
.map(item => ({
|
||||
name: item.COUNTRYNAME,
|
||||
value: item.COUNTRYCODE,
|
||||
visits: item.NOOFTIMESENTLEAVE
|
||||
})),
|
||||
transits: countryTable
|
||||
.filter(item => item.VISISTTRANSITIND === 'T')
|
||||
.map(item => ({
|
||||
name: item.COUNTRYNAME,
|
||||
value: item.COUNTRYCODE,
|
||||
visits: item.NOOFTIMESENTLEAVE
|
||||
}))
|
||||
};
|
||||
|
||||
return travelPlanData;
|
||||
}
|
||||
|
||||
saveTravelPlan(headerid: number, travelPlan: TravelPlan): Observable<TravelPlan> {
|
||||
|
||||
const countryTable: any[] = [
|
||||
...travelPlan.entries.map(entry => ({
|
||||
P_VISISTTRANSITIND: 'V' as const,
|
||||
COUNTRYCODE: entry.value,
|
||||
NOOFTIMESENTLEAVE: entry.visits
|
||||
})),
|
||||
...travelPlan.transits.map(transit => ({
|
||||
P_VISISTTRANSITIND: 'T' as const,
|
||||
COUNTRYCODE: transit.value,
|
||||
NOOFTIMESENTLEAVE: transit.visits
|
||||
}))
|
||||
];
|
||||
|
||||
let travelPlanData = {
|
||||
P_HEADERID: headerid,
|
||||
P_USSETS: travelPlan.usSets,
|
||||
P_COUNTRYTABLE: countryTable,
|
||||
P_USERID: this.userService.getUser()
|
||||
}
|
||||
|
||||
return this.http.post<TravelPlan>(`${this.apiUrl}/${this.apiDb}/AddCountries`, travelPlanData);
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,7 +36,12 @@ export class ApiErrorHandlerService {
|
||||
return this.genericErrorMessage;
|
||||
}
|
||||
|
||||
return messageString.slice(0, -1);
|
||||
// slice off the last pipe character only if the last character is a pipe
|
||||
if (messageString.endsWith('|')) {
|
||||
return messageString.slice(0, -1);
|
||||
}
|
||||
|
||||
return messageString;
|
||||
// .split('|')
|
||||
// .map(msg => msg.trim())
|
||||
// .filter(msg => msg.length > 0);
|
||||
|
||||
@ -14,6 +14,8 @@ import { ApiErrorHandlerService } from './api-error-handler.service';
|
||||
})
|
||||
export class AuthService {
|
||||
private apiUrl = environment.apiUrl;
|
||||
private applicationName = environment.appType;
|
||||
private apiDb = environment.apiDb;
|
||||
|
||||
private http = inject(HttpClient);
|
||||
private userService = inject(UserService);
|
||||
@ -24,16 +26,16 @@ export class AuthService {
|
||||
private errorHandler = inject(ApiErrorHandlerService);
|
||||
|
||||
login(username: string, password: string): Observable<any> {
|
||||
return this.http.post(`${this.apiUrl}/login`, { p_emailaddr: username, p_password: password });
|
||||
return this.http.post(`${this.apiUrl}/${this.apiDb}/login`, { p_emailaddr: username, p_password: password, P_APPLICATIONNAME: this.applicationName });
|
||||
}
|
||||
|
||||
refreshToken(): Observable<any> {
|
||||
return this.http.get(`${this.apiUrl}/refresh-tokens`, {});
|
||||
return this.http.get(`${this.apiUrl}/${this.apiDb}/refresh-tokens`, {});
|
||||
}
|
||||
|
||||
logout(): void {
|
||||
if (this.userService.isLoggedIn()) {
|
||||
this.http.post(`${this.apiUrl}/logout`, {}).subscribe({
|
||||
this.http.post(`${this.apiUrl}/${this.apiDb}/logout`, {}).subscribe({
|
||||
next: (response) => {
|
||||
this.userService.clearUser();
|
||||
this.storageService.clear();
|
||||
@ -52,7 +54,7 @@ export class AuthService {
|
||||
register(email: string, password: string): Observable<any> {
|
||||
|
||||
return this.http.post(
|
||||
`${this.apiUrl}/register`,
|
||||
`${this.apiUrl}/${this.apiDb}/register`,
|
||||
{ P_EMAILADDR: email, P_PASSWORD: password }
|
||||
);
|
||||
}
|
||||
@ -65,7 +67,7 @@ export class AuthService {
|
||||
}
|
||||
|
||||
return this.http.put(
|
||||
`${this.apiUrl}/forgot-password`,
|
||||
`${this.apiUrl}/${this.apiDb}/forgot-password`,
|
||||
{ P_EMAILADDR: email, P_PASSWORD: password },
|
||||
{ headers }
|
||||
);
|
||||
@ -73,7 +75,7 @@ export class AuthService {
|
||||
|
||||
sendEmail(email: string, type: string): Observable<any> {
|
||||
return this.http.post(
|
||||
`${this.apiUrl}/sendmail`,
|
||||
`${this.apiUrl}/${this.apiDb}/sendmail`,
|
||||
{ P_TO: email, P_MAIL_TYPE: type }
|
||||
);
|
||||
}
|
||||
|
||||
@ -15,6 +15,10 @@ import { Country } from '../../models/country';
|
||||
import { UnitOfMeasure } from '../../models/unitofmeasure';
|
||||
import { DeliveryMethod } from '../../models/delivery-method';
|
||||
import { PaymentType } from '../../models/payment-type';
|
||||
import { FormOfSecurity } from '../../models/formofsecurity';
|
||||
import { UserService } from './user.service';
|
||||
import { ExtensionReason } from '../../models/extension-reason';
|
||||
import { ContactTitle } from '../../models/contact-title';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@ -24,9 +28,10 @@ export class CommonService {
|
||||
private apiDb = environment.apiDb;
|
||||
|
||||
private http = inject(HttpClient);
|
||||
private userService = inject(UserService);
|
||||
|
||||
getCountries(spid: number = 0): Observable<Country[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=002&P_SPID=0`).pipe(
|
||||
getCountries(): Observable<Country[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=002&P_SPID=${this.userService.getUserSpid()}`).pipe(
|
||||
map((response) =>
|
||||
response.map((item) => ({
|
||||
name: item.PARAMDESC,
|
||||
@ -37,8 +42,8 @@ export class CommonService {
|
||||
);
|
||||
}
|
||||
|
||||
getStates(country: string, spid: number = 0): Observable<State[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=001&P_SPID=0`).pipe(
|
||||
getStates(country: string): Observable<State[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=001&P_SPID=${this.userService.getUserSpid()}`).pipe(
|
||||
map((response) =>
|
||||
response.map((item) => ({
|
||||
name: item.PARAMDESC,
|
||||
@ -55,7 +60,7 @@ export class CommonService {
|
||||
}
|
||||
|
||||
getRegions(): Observable<Region[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetRegions`).pipe(
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetRegions/${this.userService.getUserSpid()}`).pipe(
|
||||
map((response) =>
|
||||
response.map((item) => ({
|
||||
id: item.REGIONID,
|
||||
@ -66,8 +71,22 @@ export class CommonService {
|
||||
);
|
||||
}
|
||||
|
||||
getDeliveryTypes(spid: number = 0): Observable<DeliveryType[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=006&P_SPID=0`).pipe(
|
||||
getDeliveryTypes(): Observable<DeliveryType[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=006&P_SPID=${this.userService.getUserSpid()}`).pipe(
|
||||
map((response) =>
|
||||
response.map((item) => ({
|
||||
name: item.PARAMDESC,
|
||||
id: item.PARAMID,
|
||||
value: item.PARAMVALUE,
|
||||
daysToDelivery: item.ADDLPARAMVALUE1,
|
||||
cutOffTime: item.ADDLPARAMVALUE2,
|
||||
}))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
getTimezones(): Observable<TimeZone[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=010&P_SPID=${this.userService.getUserSpid()}`).pipe(
|
||||
map((response) =>
|
||||
response.map((item) => ({
|
||||
name: item.PARAMDESC,
|
||||
@ -78,8 +97,8 @@ export class CommonService {
|
||||
);
|
||||
}
|
||||
|
||||
getTimezones(spid: number = 0): Observable<TimeZone[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=010&P_SPID=0`).pipe(
|
||||
getFeeTypes(): Observable<FeeType[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=009&P_SPID=${this.userService.getUserSpid()}`).pipe(
|
||||
map((response) =>
|
||||
response.map((item) => ({
|
||||
name: item.PARAMDESC,
|
||||
@ -90,8 +109,8 @@ export class CommonService {
|
||||
);
|
||||
}
|
||||
|
||||
getFeeTypes(spid: number = 0): Observable<FeeType[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=009&P_SPID=0`).pipe(
|
||||
getBondSuretys(): Observable<BondSurety[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=003&P_SPID=${this.userService.getUserSpid()}`).pipe(
|
||||
map((response) =>
|
||||
response.map((item) => ({
|
||||
name: item.PARAMDESC,
|
||||
@ -102,8 +121,8 @@ export class CommonService {
|
||||
);
|
||||
}
|
||||
|
||||
getBondSuretys(spid: number = 0): Observable<BondSurety[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=003&P_SPID=0`).pipe(
|
||||
getCargoPolicies(): Observable<CargoPolicy[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=004&P_SPID=${this.userService.getUserSpid()}`).pipe(
|
||||
map((response) =>
|
||||
response.map((item) => ({
|
||||
name: item.PARAMDESC,
|
||||
@ -114,8 +133,8 @@ export class CommonService {
|
||||
);
|
||||
}
|
||||
|
||||
getCargoPolicies(spid: number = 0): Observable<CargoPolicy[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=004&P_SPID=0`).pipe(
|
||||
getCargoSuretys(): Observable<CargoSurety[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=005&P_SPID=${this.userService.getUserSpid()}`).pipe(
|
||||
map((response) =>
|
||||
response.map((item) => ({
|
||||
name: item.PARAMDESC,
|
||||
@ -126,20 +145,8 @@ export class CommonService {
|
||||
);
|
||||
}
|
||||
|
||||
getCargoSuretys(spid: number = 0): Observable<CargoSurety[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=005&P_SPID=0`).pipe(
|
||||
map((response) =>
|
||||
response.map((item) => ({
|
||||
name: item.PARAMDESC,
|
||||
id: item.PARAMID,
|
||||
value: item.PARAMVALUE
|
||||
}))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
getCarnetStatuses(spid: number = 0): Observable<CarnetStatus[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=011&P_SPID=0`).pipe(
|
||||
getCarnetStatuses(): Observable<CarnetStatus[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=011&P_SPID=${this.userService.getUserSpid()}`).pipe(
|
||||
map((response) =>
|
||||
response.map((item) => ({
|
||||
name: item.PARAMDESC,
|
||||
@ -151,8 +158,8 @@ 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(
|
||||
getUnitOfMeasures(): Observable<UnitOfMeasure[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=013&P_SPID=${this.userService.getUserSpid()}`).pipe(
|
||||
map((response) =>
|
||||
response.map((item) => ({
|
||||
name: item.PARAMDESC,
|
||||
@ -163,8 +170,8 @@ export class CommonService {
|
||||
);
|
||||
}
|
||||
|
||||
getDeliveryMethods(_spid: number = 0): Observable<DeliveryMethod[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=007&P_SPID=0`).pipe(
|
||||
getDeliveryMethods(): Observable<DeliveryMethod[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=007&P_SPID=${this.userService.getUserSpid()}`).pipe(
|
||||
map((response) =>
|
||||
response.map((item) => ({
|
||||
name: item.PARAMDESC,
|
||||
@ -175,8 +182,45 @@ export class CommonService {
|
||||
);
|
||||
}
|
||||
|
||||
getPaymentTypes(_spid: number = 0): Observable<PaymentType[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=008&P_SPID=0`).pipe(
|
||||
getPaymentTypes(): Observable<PaymentType[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=008&P_SPID=${this.userService.getUserSpid()}`).pipe(
|
||||
map((response) =>
|
||||
response.map((item) => ({
|
||||
name: item.PARAMDESC,
|
||||
id: item.PARAMID,
|
||||
value: item.PARAMVALUE,
|
||||
}))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
getFormOfSecurities(): Observable<FormOfSecurity[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=016&P_SPID=${this.userService.getUserSpid()}`).pipe(
|
||||
map((response) =>
|
||||
response.map((item) => ({
|
||||
name: item.PARAMDESC,
|
||||
id: item.PARAMID,
|
||||
value: item.PARAMVALUE,
|
||||
isGov: item.ADDLPARAMVALUE1 === 'GOV',
|
||||
}))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
getExtensionReasons(): Observable<ExtensionReason[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=015&P_SPID=${this.userService.getUserSpid()}`).pipe(
|
||||
map((response) =>
|
||||
response.map((item) => ({
|
||||
name: item.PARAMDESC,
|
||||
id: item.PARAMID,
|
||||
value: item.PARAMVALUE,
|
||||
}))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
getContactTitles(): Observable<ContactTitle[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=012&P_SPID=${this.userService.getUserSpid()}`).pipe(
|
||||
map((response) =>
|
||||
response.map((item) => ({
|
||||
name: item.PARAMDESC,
|
||||
|
||||
@ -136,7 +136,8 @@ export class UserService {
|
||||
locationid: userDetails.LOCATIONID,
|
||||
urlKey: userDetails.ENCURLKEY,
|
||||
logoName: userDetails.LOGONAME,
|
||||
themeName: userDetails.THEMENAME
|
||||
themeName: userDetails.THEMENAME,
|
||||
serviceProviderName: userDetails.SPNAME
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
90
src/app/core/services/holder/basic-detail.service.ts
Normal file
90
src/app/core/services/holder/basic-detail.service.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { UserService } from '../common/user.service';
|
||||
import { environment } from '../../../../environments/environment';
|
||||
import { map, Observable, } from 'rxjs';
|
||||
import { BasicDetail } from '../../models/holder/basic-detail';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class BasicDetailService {
|
||||
private apiUrl = environment.apiUrl;
|
||||
private apiDb = environment.apiDb;
|
||||
|
||||
private http = inject(HttpClient);
|
||||
private userService = inject(UserService);
|
||||
|
||||
getBasicDetailByHolderId(id: number): Observable<BasicDetail> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetHolderRecord/${this.userService.getUserSpid()}/${id}`).pipe(
|
||||
map(response => this.mapToBasicDetail(response)));
|
||||
}
|
||||
|
||||
private mapToBasicDetail(basicDetails: any): BasicDetail {
|
||||
return {
|
||||
holderid: basicDetails.HOLDERID,
|
||||
spid: basicDetails.SPID,
|
||||
locationid: basicDetails.LOCATIONID,
|
||||
holderNumber: basicDetails.HOLDERNO,
|
||||
holderType: basicDetails.HOLDERTYPE,
|
||||
uscibMember: basicDetails.USCIBMEMBERFLAG === 'Y',
|
||||
// govAgency: basicDetails.GOVAGENCYFLAG === 'Y',
|
||||
holderName: basicDetails.HOLDERNAME,
|
||||
//NAMEQUALIFIER: basicDetails.NAMEQUALIFIER || null,
|
||||
dbaName: basicDetails.ADDLNAME || null,
|
||||
address1: basicDetails.ADDRESS1,
|
||||
address2: basicDetails.ADDRESS2 || null,
|
||||
city: basicDetails.CITY,
|
||||
state: basicDetails.STATE,
|
||||
zip: basicDetails.ZIP,
|
||||
country: basicDetails.COUNTRY
|
||||
};
|
||||
}
|
||||
|
||||
createBasicDetail(data: BasicDetail): Observable<any> {
|
||||
const basicDetail = {
|
||||
P_SPID: this.userService.getUserSpid(),
|
||||
P_CLIENTLOCATIONID: this.userService.getUserLocationid(),
|
||||
P_HOLDERNO: data.holderNumber,
|
||||
P_HOLDERTYPE: data.holderType,
|
||||
P_USCIBMEMBERFLAG: data.uscibMember ? 'Y' : 'N',
|
||||
P_GOVAGENCYFLAG: 'N',
|
||||
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.post(`${this.apiUrl}/${this.apiDb}/CreateHolderData`, basicDetail);
|
||||
}
|
||||
|
||||
updateBasicDetails(id: number, data: BasicDetail): Observable<any> {
|
||||
const basicDetail = {
|
||||
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 ? 'Y' : 'N',
|
||||
P_GOVAGENCYFLAG: 'N',
|
||||
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`, basicDetail);
|
||||
}
|
||||
}
|
||||
90
src/app/core/services/holder/contact.service.ts
Normal file
90
src/app/core/services/holder/contact.service.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import { inject, 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 { Contact } from '../../models/holder/contact';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ContactService {
|
||||
private apiUrl = environment.apiUrl;
|
||||
private apiDb = environment.apiDb;
|
||||
|
||||
private http = inject(HttpClient);
|
||||
private userService = inject(UserService);
|
||||
|
||||
getContactsById(id: number): Observable<Contact[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetHolderContacts/${this.userService.getUserSpid()}/${id}`).pipe(
|
||||
map(response => this.mapToContacts(response)));
|
||||
}
|
||||
|
||||
private mapToContacts(data: any[]): Contact[] {
|
||||
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,
|
||||
email: contact.EMAILADDRESS,
|
||||
createdBy: contact.CREATEDBY || null,
|
||||
dateCreated: contact.DATECREATED || null,
|
||||
lastUpdatedBy: contact.LASTUPDATEDBY || null,
|
||||
lastUpdatedDate: contact.LASTUPDATEDDATE || null,
|
||||
isInactive: contact.INACTIVEFLAG === 'Y' || false,
|
||||
inactivatedDate: contact.INACTIVEDATE || null
|
||||
}));
|
||||
}
|
||||
|
||||
createContact(holderid: number, data: Contact): 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);
|
||||
}
|
||||
|
||||
updateContact(holdercontactid: number, data: Contact): Observable<any> {
|
||||
const contact = {
|
||||
P_HOLDERCONTACTID: 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()
|
||||
}
|
||||
|
||||
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()}`, {});
|
||||
}
|
||||
}
|
||||
64
src/app/core/services/holder/holder.service.ts
Normal file
64
src/app/core/services/holder/holder.service.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { environment } from '../../../../environments/environment';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { UserService } from '../common/user.service';
|
||||
import { map, Observable } from 'rxjs';
|
||||
import { BasicDetail } from '../../models/holder/basic-detail';
|
||||
import { HolderFilter } from '../../models/holder/holder-filter';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class HolderService {
|
||||
private apiUrl = environment.apiUrl;
|
||||
private apiDb = environment.apiDb;
|
||||
|
||||
private http = inject(HttpClient);
|
||||
private userService = inject(UserService);
|
||||
|
||||
getHolders(filter: HolderFilter): Observable<BasicDetail[]> {
|
||||
|
||||
// add query parameters to the URL based on the filter
|
||||
let queryParams = ``;
|
||||
if (filter.holderName) {
|
||||
queryParams += `P_HOLDERNAME=${filter.holderName}`;
|
||||
}
|
||||
if (filter.holderId) {
|
||||
queryParams += `P_HOLDERID=${filter.holderId}`;
|
||||
}
|
||||
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/SearchHolder/${this.userService.getUserSpid()}/${this.userService.getSafeUser()}?${queryParams}`).pipe(
|
||||
map(response => this.mapToHolders(response))
|
||||
)
|
||||
}
|
||||
|
||||
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()}`, {});
|
||||
}
|
||||
|
||||
private mapToHolders(data: any[]): BasicDetail[] {
|
||||
return data.map((holderDetail) => ({
|
||||
holderid: holderDetail.HOLDERID,
|
||||
spid: holderDetail.SPID,
|
||||
locationid: holderDetail.LOCATIONID,
|
||||
holderNumber: holderDetail.HOLDERNO,
|
||||
holderType: holderDetail.HOLDERTYPE,
|
||||
uscibMember: holderDetail.USCIBMEMBERFLAG === 'Y',
|
||||
// govAgency: holderDetail.GOVAGENCYFLAG === 'Y',
|
||||
holderName: holderDetail.HOLDERNAME,
|
||||
//: holderDetail.NAMEQUALIFIER,
|
||||
dbaName: holderDetail.ADDLNAME,
|
||||
address1: holderDetail.ADDRESS1,
|
||||
address2: holderDetail.ADDRESS2,
|
||||
city: holderDetail.CITY,
|
||||
state: holderDetail.STATE,
|
||||
zip: holderDetail.ZIP,
|
||||
country: holderDetail.COUNTRY,
|
||||
isInactive: holderDetail.INACTIVEFLAG === 'Y'
|
||||
}))
|
||||
}
|
||||
}
|
||||
@ -38,7 +38,11 @@ export class HomeService {
|
||||
expiryDate: item.EXPDATE || null,
|
||||
orderType: item.ORDERTYPE,
|
||||
carnetStatus: item.CARNETSTATUS,
|
||||
headerId: item.HEADERID,
|
||||
headerid: item.HEADERID,
|
||||
printGLStatus: item.PRINTGLSTATUS,
|
||||
securityType: item.SECURITYTYPE,
|
||||
deliveryType: item.DELIVERYTYPE,
|
||||
clientName: item.CLIENTNAMNE
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
20
src/app/holder/add/add-holder.component.html
Normal file
20
src/app/holder/add/add-holder.component.html
Normal file
@ -0,0 +1,20 @@
|
||||
<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 [completed]="stepsCompleted.holderDetails" [editable]="stepsCompleted.holderDetails">
|
||||
<ng-template matStepLabel>Holder Details</ng-template>
|
||||
<app-basic-detail [isEditMode]="isEditMode" (holderIdCreated)="onHolderIdCreated($event)">
|
||||
</app-basic-detail>
|
||||
</mat-step>
|
||||
|
||||
<!-- Holder Contact Step -->
|
||||
<mat-step [completed]="stepsCompleted.holderContacts" [editable]="stepsCompleted.holderContacts">
|
||||
<ng-template matStepLabel>Holder Contacts</ng-template>
|
||||
<app-contacts [holderid]="holderid" [isEditMode]="isEditMode"
|
||||
[userPreferences]="userPreferences"></app-contacts>
|
||||
</mat-step>
|
||||
</mat-stepper>
|
||||
</div>
|
||||
0
src/app/holder/add/add-holder.component.scss
Normal file
0
src/app/holder/add/add-holder.component.scss
Normal file
55
src/app/holder/add/add-holder.component.ts
Normal file
55
src/app/holder/add/add-holder.component.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { Component } 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 { BasicDetailComponent } from '../basic-details/basic-details.component';
|
||||
import { MatStepperModule } from '@angular/material/stepper';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { ContactsComponent } from '../contacts/contacts.component';
|
||||
import { UserPreferences } from '../../core/models/user-preference';
|
||||
import { UserPreferencesService } from '../../core/services/user-preference.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-add-holder',
|
||||
imports: [
|
||||
AngularMaterialModule,
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
BasicDetailComponent,
|
||||
MatStepperModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
ContactsComponent
|
||||
],
|
||||
templateUrl: './add-holder.component.html',
|
||||
styleUrl: './add-holder.component.scss'
|
||||
})
|
||||
export class AddHolderComponent {
|
||||
currentStep = 0;
|
||||
isLinear = true;
|
||||
isEditMode = false;
|
||||
holderid: number = 0;
|
||||
userPreferences: UserPreferences;
|
||||
|
||||
stepsCompleted = {
|
||||
holderDetails: false,
|
||||
holderContacts: false
|
||||
};
|
||||
|
||||
constructor(
|
||||
userPrefenceService: UserPreferencesService
|
||||
) {
|
||||
this.userPreferences = userPrefenceService.getPreferences();
|
||||
}
|
||||
|
||||
onStepChange(event: StepperSelectionEvent): void {
|
||||
this.currentStep = event.selectedIndex;
|
||||
}
|
||||
|
||||
onHolderIdCreated(holderid: number): void {
|
||||
this.holderid = holderid;
|
||||
this.stepsCompleted.holderDetails = true;
|
||||
}
|
||||
}
|
||||
158
src/app/holder/basic-details/basic-details.component.html
Normal file
158
src/app/holder/basic-details/basic-details.component.html
Normal file
@ -0,0 +1,158 @@
|
||||
<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]="basicDetailsForm" class="details-form" (ngSubmit)="saveBasicDetails()">
|
||||
|
||||
<!-- Holder 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']">
|
||||
DBA name 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="holder-number">
|
||||
<mat-label>Tax ID No</mat-label>
|
||||
<input matInput formControlName="holderNumber" required>
|
||||
<mat-error *ngIf="f['holderNumber'].errors?.['required']">
|
||||
Tax ID No is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="f['holderNumber'].errors?.['maxlength']">
|
||||
Maximum 20 characters allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-radio-group formControlName="holderType" class="radio-group">
|
||||
<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 class="form-row">
|
||||
<mat-radio-group formControlName="uscibMember" class="radio-group">
|
||||
<mat-label>Are you a member of USCIB ?</mat-label>
|
||||
<mat-radio-button [value]="true">Yes</mat-radio-button>
|
||||
<mat-radio-button [value]="false">No</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
<!-- <mat-radio-group formControlName="govAgency" class="radio-group">
|
||||
<mat-label>Are you belong to Government Agency ?</mat-label>
|
||||
<mat-radio-button [value]="true">Yes</mat-radio-button>
|
||||
<mat-radio-button [value]="false">No</mat-radio-button>
|
||||
</mat-radio-group> -->
|
||||
</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="accent" *ngIf="currentApplicationDetails" type="button"
|
||||
(click)="goBackToCarnetApplication()">
|
||||
<mat-icon>chevron_left</mat-icon> Back to application
|
||||
</button>
|
||||
<button mat-raised-button color="primary" type="submit"
|
||||
[disabled]="basicDetailsForm.invalid || !basicDetailsForm.dirty || changeInProgress">
|
||||
{{ isEditMode ? 'Update' : 'Save' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
111
src/app/holder/basic-details/basic-details.component.scss
Normal file
111
src/app/holder/basic-details/basic-details.component.scss
Normal file
@ -0,0 +1,111 @@
|
||||
.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: 8px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
|
||||
.name {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.lookup-code {
|
||||
grid-column: span 1;
|
||||
}
|
||||
|
||||
.holder-number {
|
||||
grid-column: span 1;
|
||||
}
|
||||
|
||||
.address1,
|
||||
.address2 {
|
||||
grid-column: span 3;
|
||||
}
|
||||
|
||||
.city,
|
||||
.state,
|
||||
.country,
|
||||
.zip {
|
||||
grid-column: span 1;
|
||||
}
|
||||
|
||||
.mat-mdc-radio-group {
|
||||
padding-bottom: 12px;
|
||||
|
||||
mat-label {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.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 {
|
||||
grid-column: span 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
252
src/app/holder/basic-details/basic-details.component.ts
Normal file
252
src/app/holder/basic-details/basic-details.component.ts
Normal file
@ -0,0 +1,252 @@
|
||||
import { Component, EventEmitter, inject, 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 { CommonService } from '../../core/services/common/common.service';
|
||||
import { finalize, Subject, takeUntil } from 'rxjs';
|
||||
import { Country } from '../../core/models/country';
|
||||
import { BasicDetailService } from '../../core/services/holder/basic-detail.service';
|
||||
import { BasicDetail } from '../../core/models/holder/basic-detail';
|
||||
import { StorageService } from '../../core/services/common/storage.service';
|
||||
import { NavigationService } from '../../core/services/common/navigation.service';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { HolderService } from '../../core/services/carnet/holder.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-basic-detail',
|
||||
imports: [AngularMaterialModule, ReactiveFormsModule, CommonModule],
|
||||
templateUrl: './basic-details.component.html',
|
||||
styleUrl: './basic-details.component.scss'
|
||||
})
|
||||
export class BasicDetailComponent implements OnInit, OnDestroy {
|
||||
|
||||
@Input() isEditMode = false;
|
||||
@Input() holderid: number = 0;
|
||||
|
||||
@Output() holderIdCreated = new EventEmitter<number>();
|
||||
|
||||
basicDetailsForm: FormGroup;
|
||||
countries: Country[] = [];
|
||||
regions: Region[] = [];
|
||||
states: State[] = [];
|
||||
currentApplicationDetails: { headerid: number, applicationName: string } | null = null;
|
||||
|
||||
isLoading = false;
|
||||
changeInProgress = false;
|
||||
countriesHasStates = ['US', 'CA', 'MX'];
|
||||
returnTo: string = environment.appType === 'client' ? 'edit-carnet' : 'process-carnet';
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
private fb = inject(FormBuilder);
|
||||
private basicDetailService = inject(BasicDetailService);
|
||||
private notificationService = inject(NotificationService);
|
||||
private commonService = inject(CommonService);
|
||||
private errorHandler = inject(ApiErrorHandlerService);
|
||||
private storageService = inject(StorageService);
|
||||
private navigationService = inject(NavigationService);
|
||||
private carnetHolderService = inject(HolderService);
|
||||
|
||||
constructor() {
|
||||
this.basicDetailsForm = this.createForm();
|
||||
this.currentApplicationDetails = this.storageService.get<{ headerid: number, applicationName: string }>('currentapplication')
|
||||
}
|
||||
|
||||
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: [false, [Validators.required]],
|
||||
// govAgency: [false, [Validators.required]],
|
||||
address1: ['', [Validators.required, Validators.maxLength(100)]],
|
||||
address2: ['', Validators.maxLength(100)],
|
||||
city: ['', [Validators.required, Validators.maxLength(50)]],
|
||||
state: ['', Validators.required],
|
||||
country: ['US', Validators.required],
|
||||
zip: ['', [Validators.required, ZipCodeValidator('country')]]
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadLookupData();
|
||||
|
||||
if (this.holderid > 0) {
|
||||
this.isLoading = true;
|
||||
this.basicDetailService.getBasicDetailByHolderId(this.holderid).pipe(finalize(() => {
|
||||
this.isLoading = false;
|
||||
})).subscribe({
|
||||
next: (basicDetail: BasicDetail) => {
|
||||
this.patchFormData(basicDetail);
|
||||
// this.holderName.emit(basicDetail.holderName);
|
||||
},
|
||||
error: (error: any) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load basic details');
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error loading basic details:', error);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.loadStates('US'); // Load states for default country
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
onCountryChange(country: string): void {
|
||||
this.basicDetailsForm.get('state')?.reset();
|
||||
|
||||
if (country) {
|
||||
this.loadStates(country);
|
||||
}
|
||||
|
||||
this.basicDetailsForm.get('zip')?.updateValueAndValidity();
|
||||
}
|
||||
|
||||
saveBasicDetails() {
|
||||
if (this.basicDetailsForm.invalid) {
|
||||
this.basicDetailsForm.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
|
||||
const basicDetailData: BasicDetail = this.basicDetailsForm.value;
|
||||
|
||||
const saveObservable = this.isEditMode && this.holderid > 0
|
||||
? this.basicDetailService.updateBasicDetails(this.holderid, basicDetailData)
|
||||
: this.basicDetailService.createBasicDetail(basicDetailData);
|
||||
|
||||
this.changeInProgress = true;
|
||||
|
||||
saveObservable.pipe(finalize(() => {
|
||||
this.changeInProgress = false;
|
||||
})).subscribe({
|
||||
next: (basicData: any) => {
|
||||
this.notificationService.showSuccess(`Basic details ${this.isEditMode ? 'updated' : 'added'} successfully`);
|
||||
|
||||
if (!this.isEditMode) {
|
||||
this.holderIdCreated.emit(basicData.HOLDERID);
|
||||
|
||||
// save the created holder for the current application
|
||||
this.saveHolderToApplication(basicData.HOLDERID);
|
||||
|
||||
// change to edit mode after creation
|
||||
this.isEditMode = true;
|
||||
this.holderid = basicData.HOLDERID;
|
||||
}
|
||||
|
||||
// if (this.isEditMode) {
|
||||
// this.holderName.emit(basicDetailData.holderName);
|
||||
// }
|
||||
},
|
||||
error: (error: any) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, `Failed to ${this.isEditMode ? 'update' : 'add'} basic details`);
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error saving basic details:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateStateControl(controlName: string, country: string): void {
|
||||
const stateControl = this.basicDetailsForm.get(controlName);
|
||||
if (this.countriesHasStates.includes(country)) {
|
||||
stateControl?.enable();
|
||||
} else {
|
||||
stateControl?.disable();
|
||||
stateControl?.setValue('FN');
|
||||
}
|
||||
}
|
||||
|
||||
patchFormData(data: BasicDetail): void {
|
||||
|
||||
this.basicDetailsForm.patchValue({
|
||||
holderName: data.holderName,
|
||||
dbaName: data.dbaName,
|
||||
holderNumber: data.holderNumber,
|
||||
holderType: data.holderType,
|
||||
uscibMember: data.uscibMember,
|
||||
// govAgency: data.govAgency,
|
||||
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.basicDetailsForm.controls;
|
||||
}
|
||||
|
||||
loadLookupData(): void {
|
||||
|
||||
this.commonService.getCountries()
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: (countries) => {
|
||||
this.countries = countries;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Failed to load countries', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadStates(country: string): void {
|
||||
country = this.countriesHasStates.includes(country) ? country : 'FN';
|
||||
this.commonService.getStates(country)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: (states) => {
|
||||
this.states = states;
|
||||
this.updateStateControl('state', country);
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Failed to load states', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
goBackToCarnetApplication(): void {
|
||||
this.storageService.removeItem('currentapplication')
|
||||
this.navigationService.navigate([this.returnTo, this.currentApplicationDetails?.headerid],
|
||||
{
|
||||
state: { isEditMode: true },
|
||||
queryParams: {
|
||||
applicationname: this.currentApplicationDetails?.applicationName,
|
||||
return: 'holder'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
saveHolderToApplication(holderid: number): void {
|
||||
this.changeInProgress = true;
|
||||
const saveObservable = this.carnetHolderService.saveApplicationHolder(this.currentApplicationDetails?.headerid!, holderid);
|
||||
|
||||
saveObservable.pipe(finalize(() => {
|
||||
this.changeInProgress = false;
|
||||
})).subscribe({
|
||||
next: (basicData: any) => {
|
||||
},
|
||||
error: (error: any) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, `Failed to save holder details`);
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error saving holder details:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
276
src/app/holder/contacts/contacts.component.html
Normal file
276
src/app/holder/contacts/contacts.component.html
Normal file
@ -0,0 +1,276 @@
|
||||
<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>
|
||||
|
||||
<button mat-raised-button color="accent" *ngIf="currentApplicationDetails" type="button"
|
||||
(click)="goBackToCarnetApplication()">
|
||||
<mat-icon>chevron_left</mat-icon> Back to application
|
||||
</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>
|
||||
|
||||
<!-- Middle Initial Column -->
|
||||
<ng-container matColumnDef="middleInitial">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Middle Initial</th>
|
||||
<td mat-cell *matCellDef="let contact">{{ contact.middleInitial }}</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">{{ getContactTitleLabel(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>
|
||||
|
||||
<!-- Fax Column -->
|
||||
<ng-container matColumnDef="fax">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Fax</th>
|
||||
<td mat-cell *matCellDef="let contact">{{ contact.fax }}</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.email }}</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>
|
||||
<button mat-icon-button color="primary" (click)="editContact(contact)" matTooltip="Edit">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="warn" *ngIf="!contact.isInactive"
|
||||
(click)="inactivateContact(contact.holdercontactid)" matTooltip="Inactivate">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="warn" *ngIf="contact.isInactive"
|
||||
(click)="reactivateContact(contact.holdercontactid)" matTooltip="Reactivate">
|
||||
<mat-icon>loupe</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 *ngIf="dataSource.data.length > userPreferences.pageSize!" [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>
|
||||
<mat-select formControlName="title" required>
|
||||
<mat-option *ngFor="let contactTitle of contactTitles" [value]="contactTitle.value">
|
||||
{{ contactTitle.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="contactForm.get('title')?.errors?.['required']">
|
||||
Title is required
|
||||
</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-raised-button color="primary" type="submit" [disabled]="contactForm.invalid || changeInProgress">
|
||||
{{ isEditing ? 'Update' : 'Save' }}
|
||||
</button>
|
||||
<button mat-button type="button" (click)="cancelEdit()">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
183
src/app/holder/contacts/contacts.component.scss
Normal file
183
src/app/holder/contacts/contacts.component.scss
Normal file
@ -0,0 +1,183 @@
|
||||
.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;
|
||||
margin-left: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
align-items: start;
|
||||
|
||||
mat-form-field {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.small-field {
|
||||
max-width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.top-divider {
|
||||
padding-top: 0.5rem;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.bottom-divider {
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.readonly-section {
|
||||
|
||||
.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.875rem;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
294
src/app/holder/contacts/contacts.component.ts
Normal file
294
src/app/holder/contacts/contacts.component.ts
Normal file
@ -0,0 +1,294 @@
|
||||
import { Component, EventEmitter, inject, 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, MatPaginatorIntl } 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 { ContactService } from '../../core/services/holder/contact.service';
|
||||
import { Contact } from '../../core/models/holder/contact';
|
||||
import { CustomPaginator } from '../../shared/custom-paginator';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ConfirmDialogComponent } from '../../shared/components/confirm-dialog/confirm-dialog.component';
|
||||
import { NavigationService } from '../../core/services/common/navigation.service';
|
||||
import { StorageService } from '../../core/services/common/storage.service';
|
||||
import { finalize, Subject, takeUntil } from 'rxjs';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { ContactTitle } from '../../core/models/contact-title';
|
||||
import { CommonService } from '../../core/services/common/common.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-contacts',
|
||||
imports: [AngularMaterialModule, ReactiveFormsModule, PhonePipe, CommonModule],
|
||||
templateUrl: './contacts.component.html',
|
||||
styleUrl: './contacts.component.scss',
|
||||
providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }],
|
||||
})
|
||||
export class ContactsComponent {
|
||||
@Input() isEditMode: boolean = false;
|
||||
@Input() holderid: number = 0;
|
||||
@Input() userPreferences: UserPreferences = {};
|
||||
|
||||
@Output() hasContacts = new EventEmitter<boolean>();
|
||||
|
||||
@ViewChild(MatPaginator, { static: false })
|
||||
set paginator(value: MatPaginator) {
|
||||
this.dataSource.paginator = value;
|
||||
}
|
||||
|
||||
@ViewChild(MatSort) sort!: MatSort;
|
||||
|
||||
displayedColumns: string[] = ['firstName', 'middleInitial', 'lastName', 'title', 'phone', 'mobile', 'fax', 'email', 'actions'];
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
contactForm: FormGroup;
|
||||
|
||||
isEditing = false;
|
||||
currentContactId: number | null = null;
|
||||
isLoading = false;
|
||||
changeInProgress = false;
|
||||
showForm = false;
|
||||
showInactiveContacts = false;
|
||||
contacts: Contact[] = [];
|
||||
contactTitles: ContactTitle[] = [];
|
||||
|
||||
currentApplicationDetails: { headerid: number, applicationName: string } | null = null;
|
||||
returnTo: string = environment.appType === 'client' ? 'edit-carnet' : 'process-carnet';
|
||||
|
||||
contactReadOnlyFields: any = {
|
||||
lastChangedDate: null,
|
||||
lastChangedBy: null,
|
||||
isInactive: null,
|
||||
inactivatedDate: null
|
||||
};
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
private fb = inject(FormBuilder);
|
||||
private dialog = inject(MatDialog);
|
||||
private commonService = inject(CommonService);
|
||||
private contactService = inject(ContactService);
|
||||
private notificationService = inject(NotificationService);
|
||||
private errorHandler = inject(ApiErrorHandlerService);
|
||||
private navigationService = inject(NavigationService);
|
||||
private storageService = inject(StorageService);
|
||||
|
||||
constructor() {
|
||||
this.contactForm = this.fb.group({
|
||||
firstName: ['', [Validators.required, Validators.maxLength(50)]],
|
||||
lastName: ['', [Validators.required, Validators.maxLength(50)]],
|
||||
middleInitial: ['', [Validators.maxLength(1)]],
|
||||
title: ['', [Validators.required]],
|
||||
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)]],
|
||||
});
|
||||
|
||||
this.currentApplicationDetails = this.storageService.get<{ headerid: number, applicationName: string }>('currentapplication')
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadContactTitles();
|
||||
if (this.holderid > 0) {
|
||||
this.loadContacts();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.dataSource.paginator = this.paginator;
|
||||
this.dataSource.sort = this.sort;
|
||||
}
|
||||
|
||||
loadContacts(): void {
|
||||
this.isLoading = true;
|
||||
|
||||
this.contactService.getContactsById(this.holderid).pipe(finalize(() => {
|
||||
this.isLoading = false;
|
||||
})).subscribe({
|
||||
next: (contacts: Contact[]) => {
|
||||
this.contacts = contacts;
|
||||
this.renderContacts();
|
||||
},
|
||||
error: (error: any) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load contacts');
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error loading contacts:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadContactTitles(): void {
|
||||
this.commonService.getContactTitles()
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: (contactTitles) => {
|
||||
this.contactTitles = contactTitles;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Failed to load contact titles', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addNewContact(): void {
|
||||
this.showForm = true;
|
||||
this.isEditing = false;
|
||||
this.currentContactId = null;
|
||||
this.contactForm.reset();
|
||||
}
|
||||
|
||||
editContact(contact: Contact): 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.email
|
||||
});
|
||||
|
||||
this.contactReadOnlyFields.lastChangedDate = contact.lastUpdatedDate ?? contact.dateCreated;
|
||||
this.contactReadOnlyFields.lastChangedBy = contact.lastUpdatedBy ?? contact.createdBy;
|
||||
this.contactReadOnlyFields.isInactive = contact.isInactive;
|
||||
this.contactReadOnlyFields.inactivatedDate = contact.inactivatedDate;
|
||||
}
|
||||
|
||||
saveContact(): void {
|
||||
if (this.contactForm.invalid && !(this.holderid > 0)) {
|
||||
this.contactForm.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
|
||||
const contactData: Contact = this.contactForm.value;
|
||||
|
||||
const saveObservable = this.isEditing && (this.currentContactId! > 0)
|
||||
? this.contactService.updateContact(this.currentContactId!, contactData)
|
||||
: this.contactService.createContact(this.holderid, contactData);
|
||||
|
||||
this.changeInProgress = true;
|
||||
|
||||
saveObservable.pipe(finalize(() => {
|
||||
this.changeInProgress = false;
|
||||
})).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.isInactive);
|
||||
} else {
|
||||
this.dataSource.data = this.contacts.filter(contact => !contact.isInactive);
|
||||
}
|
||||
}
|
||||
|
||||
cancelEdit(): void {
|
||||
this.showForm = false;
|
||||
this.isEditing = false;
|
||||
this.currentContactId = null;
|
||||
this.contactForm.reset();
|
||||
}
|
||||
|
||||
inactivateContact(contactId: number): void {
|
||||
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
|
||||
width: '350px',
|
||||
data: {
|
||||
title: 'Confirm Inactivation',
|
||||
message: 'Are you sure you want to inactivate this contact?',
|
||||
confirmText: 'Yes',
|
||||
cancelText: 'Cancel'
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.contactService.inactivateHolderContact(contactId).subscribe({
|
||||
next: () => {
|
||||
this.notificationService.showSuccess('Contact inactivated successfully');
|
||||
this.loadContacts();
|
||||
},
|
||||
error: (error) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to inactivate contact');
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error inactivating contact:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
reactivateContact(contactId: number): void {
|
||||
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
|
||||
width: '350px',
|
||||
data: {
|
||||
title: 'Confirm Reactivation',
|
||||
message: 'Are you sure you want to reactivate this contact?',
|
||||
confirmText: 'Yes',
|
||||
cancelText: 'Cancel'
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.contactService.reactivateHolderContact(contactId).subscribe({
|
||||
next: () => {
|
||||
this.notificationService.showSuccess('Contact reactivated successfully');
|
||||
this.loadContacts();
|
||||
},
|
||||
error: (error) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to reactivate contact');
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error reactivating contact:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
goBackToCarnetApplication(): void {
|
||||
this.storageService.removeItem('currentapplication')
|
||||
this.navigationService.navigate([this.returnTo, this.currentApplicationDetails?.headerid],
|
||||
{
|
||||
state: { isEditMode: true },
|
||||
queryParams: {
|
||||
applicationname: this.currentApplicationDetails?.applicationName,
|
||||
return: 'holder'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
getContactTitleLabel(value: string): string {
|
||||
const type = this.contactTitles.find(t => t.value === value);
|
||||
return type ? type.name : value;
|
||||
}
|
||||
}
|
||||
23
src/app/holder/edit/edit-holder.component.html
Normal file
23
src/app/holder/edit/edit-holder.component.html
Normal file
@ -0,0 +1,23 @@
|
||||
<!-- <h2 *ngIf="this.holderName" class="page-header">Manage {{this.holderName}}</h2> -->
|
||||
|
||||
<div class="holder-action-buttons">
|
||||
<button mat-button (click)="accordion().openAll()">Expand All</button>
|
||||
<button mat-button (click)="accordion().closeAll()">Collapse All</button>
|
||||
</div>
|
||||
<mat-accordion class="holder-headers-align" multi>
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title> Basic Details </mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<app-basic-detail [holderid]="holderid" [isEditMode]="isEditMode"></app-basic-detail>
|
||||
</mat-expansion-panel>
|
||||
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title> Contacts </mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
<app-contacts [userPreferences]="userPreferences" [holderid]="holderid"
|
||||
[isEditMode]="isEditMode"></app-contacts>
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
||||
18
src/app/holder/edit/edit-holder.component.scss
Normal file
18
src/app/holder/edit/edit-holder.component.scss
Normal file
@ -0,0 +1,18 @@
|
||||
// .page-header {
|
||||
// margin: 0.5rem 0px;
|
||||
// color: var(--mat-sys-primary);
|
||||
// font-weight: 500;
|
||||
// }
|
||||
|
||||
.holder-action-buttons {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.holder-headers-align .mat-expansion-panel-header-description {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.holder-headers-align .mat-mdc-form-field+.mat-mdc-form-field {
|
||||
margin-left: 8px;
|
||||
}
|
||||
42
src/app/holder/edit/edit-holder.component.ts
Normal file
42
src/app/holder/edit/edit-holder.component.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { afterNextRender, Component, inject, 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 { BasicDetailComponent } from '../basic-details/basic-details.component';
|
||||
import { ContactsComponent } from '../contacts/contacts.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-edit-holder',
|
||||
imports: [AngularMaterialModule, CommonModule, BasicDetailComponent, ContactsComponent],
|
||||
templateUrl: './edit-holder.component.html',
|
||||
styleUrl: './edit-holder.component.scss'
|
||||
})
|
||||
export class EditHolderComponent {
|
||||
accordion = viewChild.required(MatAccordion);
|
||||
isEditMode = true;
|
||||
holderid = 0;
|
||||
//holderName: string | null = null;
|
||||
userPreferences: UserPreferences;
|
||||
|
||||
private route = inject(ActivatedRoute);
|
||||
|
||||
constructor(userPrefenceService: UserPreferencesService) {
|
||||
this.userPreferences = userPrefenceService.getPreferences();
|
||||
afterNextRender(() => {
|
||||
// Open all panels
|
||||
this.accordion().openAll();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
const idParam = this.route.snapshot.paramMap.get('holderid');
|
||||
this.holderid = idParam ? parseInt(idParam, 10) : 0;
|
||||
}
|
||||
|
||||
// onHolderNameUpdate(event: string): void {
|
||||
// this.holderName = event;
|
||||
// }
|
||||
}
|
||||
126
src/app/holder/search/search-holder.component.html
Normal file
126
src/app/holder/search/search-holder.component.html
Normal file
@ -0,0 +1,126 @@
|
||||
<div class="manage-holder-container">
|
||||
<h3 class="page-header" *ngIf="allowActions">Search Holders</h3>
|
||||
<div class="search-section" *ngIf="allowActions">
|
||||
<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">
|
||||
<mat-icon>search</mat-icon>
|
||||
Search
|
||||
</button>
|
||||
<button mat-raised-button type="button" (click)="onClearSearch()">
|
||||
<mat-icon>clear</mat-icon>
|
||||
Clear Search
|
||||
</button>
|
||||
|
||||
<button mat-raised-button color="primary" (click)="addNewHolder()">
|
||||
<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" *ngIf="allowActions">
|
||||
<mat-slide-toggle (change)="toggleShowInactiveHolders()" [disabled]="holders.length === 0">
|
||||
Show Inactive Holders
|
||||
</mat-slide-toggle>
|
||||
</div>
|
||||
|
||||
<table mat-table [dataSource]="dataSource" class="results-table" matSort>
|
||||
<ng-container matColumnDef="holderName">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Holder Name</th>
|
||||
<td mat-cell *matCellDef="let holder">{{ holder.holderName }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- DBA Name Column -->
|
||||
<ng-container matColumnDef="dbaName">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>DBA Name</th>
|
||||
<td mat-cell *matCellDef="let holder">{{ holder.dbaName || '--'}}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Address Column -->
|
||||
<ng-container matColumnDef="address">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Address</th>
|
||||
<td mat-cell *matCellDef="let holder">{{ getAddressLabel(holder) }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- USCIB Member Column -->
|
||||
<ng-container matColumnDef="uscibMember">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>USCIB Member</th>
|
||||
<td mat-cell *matCellDef="let holder">{{ holder.uscibMember ? 'Yes' : 'No' }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Holder Column -->
|
||||
<ng-container matColumnDef="holderType">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Holder Type</th>
|
||||
<td mat-cell *matCellDef="let holder">{{ holder.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 holder" class="actions-cell">
|
||||
|
||||
<div class="actions-icons">
|
||||
<mat-radio-button matTooltip="Select Holder" class="selectHolder" [value]="holder.holderid"
|
||||
[checked]="selectedHolderId === holder.holderid" *ngIf="allowActions"
|
||||
(change)="onHolderSelection(holder)" aria-label="Select row" [hidden]="holder.isInactive">
|
||||
</mat-radio-button>
|
||||
|
||||
<button mat-icon-button color="primary" *ngIf="!isViewMode" (click)="onEdit(holder.holderid)"
|
||||
matTooltip="Edit">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
|
||||
<button mat-icon-button color="warn" *ngIf="!holder.isInactive && allowActions"
|
||||
matTooltip="Inactivate" (click)="inactivateHolder(holder.holderid)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="warn" *ngIf="holder.isInactive && allowActions"
|
||||
matTooltip="Reactivate" (click)="reactivateHolder(holder.holderid)">
|
||||
<mat-icon>loupe</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"
|
||||
[ngClass]="{'selected-holder': selectedHolderId === row.holderid}"></tr>
|
||||
|
||||
<tr matNoDataRow *matNoDataRow>
|
||||
<td [attr.colspan]="displayedColumns.length" class="no-data-message">
|
||||
<mat-icon>info</mat-icon>
|
||||
<span>No records found</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<mat-paginator *ngIf="dataSource.data.length > userPreferences.pageSize!" [length]="dataSource.data.length"
|
||||
[pageSizeOptions]="[userPreferences.pageSize || 2]" [hidePageSize]="true"
|
||||
showFirstLastButtons></mat-paginator>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button mat-raised-button color="primary" type="submit" [disabled]="changeInProgress" *ngIf="!isViewMode"
|
||||
(click)="saveHolderSelection()">
|
||||
{{ 'Save' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
117
src/app/holder/search/search-holder.component.scss
Normal file
117
src/app/holder/search/search-holder.component.scss
Normal file
@ -0,0 +1,117 @@
|
||||
.page-header {
|
||||
margin: 0.5rem 0 0;
|
||||
color: var(--mat-sys-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.manage-holder-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 {
|
||||
grid-column: span 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.9rem;
|
||||
|
||||
button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.results-section {
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
border-radius: 8px;
|
||||
margin-top: 0.5rem;
|
||||
|
||||
.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-slide-toggle {
|
||||
transform: scale(0.8);
|
||||
margin-left: -0.5rem;
|
||||
}
|
||||
|
||||
.selected-holder {
|
||||
color: #28a745;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.actions-icons {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.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-holder-container {
|
||||
.search-form {
|
||||
.search-fields {
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
.name {
|
||||
grid-column: span 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
303
src/app/holder/search/search-holder.component.ts
Normal file
303
src/app/holder/search/search-holder.component.ts
Normal file
@ -0,0 +1,303 @@
|
||||
import { Component, EventEmitter, inject, Input, Output, SimpleChanges, 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 { 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, MatPaginatorIntl } from '@angular/material/paginator';
|
||||
import { HolderService as HolderService } from '../../core/services/holder/holder.service';
|
||||
import { HolderService as CarnetApplicationHolderService } from '../../core/services/carnet/holder.service';
|
||||
import { CustomPaginator } from '../../shared/custom-paginator';
|
||||
import { BasicDetail } from '../../core/models/holder/basic-detail';
|
||||
import { HolderFilter } from '../../core/models/holder/holder-filter';
|
||||
import { ConfirmDialogComponent } from '../../shared/components/confirm-dialog/confirm-dialog.component';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { StorageService } from '../../core/services/common/storage.service';
|
||||
import { finalize } from 'rxjs';
|
||||
import { environment } from '../../../environments/environment';
|
||||
|
||||
@Component({
|
||||
selector: 'app-holder-search',
|
||||
imports: [AngularMaterialModule, ReactiveFormsModule, CommonModule],
|
||||
templateUrl: './search-holder.component.html',
|
||||
styleUrl: './search-holder.component.scss',
|
||||
providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }],
|
||||
})
|
||||
export class SearchHolderComponent {
|
||||
|
||||
@ViewChild(MatPaginator, { static: false }) paginator!: MatPaginator;
|
||||
@ViewChild(MatSort, { static: false }) sort!: MatSort;
|
||||
|
||||
@Input() isViewMode = false;
|
||||
@Input() headerid: number = 0;
|
||||
@Input() selectedHolderId: number | undefined= 0;
|
||||
@Input() applicationName: string = '';
|
||||
|
||||
@Output() holderSelectionCompleted = new EventEmitter<boolean>();
|
||||
|
||||
allowActions: boolean = environment.appType === 'client' && !this.isViewMode;
|
||||
|
||||
showInactiveHolders: boolean = false;
|
||||
holders: BasicDetail[] = []
|
||||
isLoading: boolean = false;
|
||||
changeInProgress: boolean = false;
|
||||
userPreferences: UserPreferences;
|
||||
searchForm: FormGroup;
|
||||
|
||||
private fb = inject(FormBuilder);
|
||||
private holderService = inject(HolderService);
|
||||
private navigationService = inject(NavigationService);
|
||||
private carnetHolderService = inject(CarnetApplicationHolderService);
|
||||
private errorHandler = inject(ApiErrorHandlerService);
|
||||
private notificationService = inject(NotificationService);
|
||||
private storageService = inject(StorageService);
|
||||
private dialog = inject(MatDialog);
|
||||
|
||||
dataSource = new MatTableDataSource<any>([]);
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.dataSource.paginator = this.paginator;
|
||||
this.dataSource.sort = this.sort;
|
||||
}
|
||||
|
||||
displayedColumns: string[] = ['holderName', 'dbaName', 'address', 'uscibMember', 'holderType', 'actions'];
|
||||
|
||||
constructor(
|
||||
userPrefenceService: UserPreferencesService
|
||||
) {
|
||||
this.userPreferences = userPrefenceService.getPreferences();
|
||||
this.searchForm = this.createSearchForm();
|
||||
}
|
||||
|
||||
createSearchForm(): FormGroup {
|
||||
return this.fb.group({
|
||||
holderName: ['']
|
||||
});
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes['selectedHolderId']) {
|
||||
if (this.selectedHolderId) {
|
||||
this.showSelectedHolder(this.selectedHolderId);
|
||||
}
|
||||
|
||||
if (this.selectedHolderId === undefined) {
|
||||
this.autoSelectHolder();
|
||||
}
|
||||
}
|
||||
|
||||
if (changes['isViewMode'] && this.isViewMode) {
|
||||
this.allowActions = environment.appType === 'client' && !this.isViewMode;
|
||||
}
|
||||
}
|
||||
|
||||
// If only one holder is returned, auto select it
|
||||
autoSelectHolder(): void {
|
||||
this.isLoading = true;
|
||||
const filterData: HolderFilter = {};
|
||||
|
||||
this.holderService.getHolders(filterData).pipe(finalize(() => {
|
||||
this.isLoading = false;
|
||||
})).subscribe({
|
||||
next: (holders: BasicDetail[]) => {
|
||||
if (holders && holders.length === 1) {
|
||||
this.selectedHolderId = holders[0].holderid!;
|
||||
this.saveHolderSelection();
|
||||
this.showSelectedHolder(this.selectedHolderId);
|
||||
|
||||
}
|
||||
},
|
||||
error: (error: any) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to get holders');
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error loading holders:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onSearch(): void {
|
||||
this.searchHolders();
|
||||
}
|
||||
|
||||
saveHolderSelection(): void {
|
||||
this.changeInProgress = true;
|
||||
const saveObservable = this.carnetHolderService.saveApplicationHolder(this.headerid, this.selectedHolderId ? Number(this.selectedHolderId) : 0);
|
||||
|
||||
saveObservable.pipe(finalize(() => {
|
||||
this.changeInProgress = false;
|
||||
})).subscribe({
|
||||
next: (basicData: any) => {
|
||||
this.notificationService.showSuccess(`Holder updated successfully`);
|
||||
this.holderSelectionCompleted.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();
|
||||
}
|
||||
|
||||
showSelectedHolder(holderid: number): void {
|
||||
this.isLoading = true;
|
||||
const filterData: HolderFilter = {
|
||||
holderId: holderid
|
||||
}
|
||||
|
||||
this.holderService.getHolders(filterData).pipe(finalize(() => {
|
||||
this.isLoading = false;
|
||||
})).subscribe({
|
||||
next: (holders: BasicDetail[]) => {
|
||||
this.dataSource.data = this.holders = holders;
|
||||
this.renderHolders()
|
||||
},
|
||||
error: (error: any) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load selected holder');
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error loading selected holder:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
searchHolders(): void {
|
||||
this.isLoading = true;
|
||||
const filterData: HolderFilter = this.searchForm.value;
|
||||
|
||||
if (!this.searchForm.value.holderName && this.selectedHolderId) {
|
||||
this.showSelectedHolder(this.selectedHolderId);
|
||||
return;
|
||||
}
|
||||
|
||||
this.holderService.getHolders(filterData).pipe(finalize(() => {
|
||||
this.isLoading = false;
|
||||
})).subscribe({
|
||||
next: (holders: BasicDetail[]) => {
|
||||
this.dataSource.data = this.holders = holders;
|
||||
this.renderHolders()
|
||||
},
|
||||
error: (error: any) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to search holders');
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error loading holders:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
renderHolders() {
|
||||
if (this.showInactiveHolders) {
|
||||
this.dataSource.data = this.holders.filter((holder: any) => holder?.isInactive);
|
||||
} else if (this.isViewMode) {
|
||||
this.dataSource.data = this.holders;
|
||||
} else {
|
||||
this.dataSource.data = this.holders.filter((holder: any) => !holder?.isInactive);
|
||||
}
|
||||
}
|
||||
|
||||
addNewHolder(): void {
|
||||
if (this.headerid) {
|
||||
let currentApplicationDetails = { headerid: this.headerid, applicationName: this.applicationName }
|
||||
this.storageService.set("currentapplication", currentApplicationDetails);
|
||||
}
|
||||
|
||||
this.navigationService.navigate(["add-holder"])
|
||||
}
|
||||
|
||||
onEdit(id: string) {
|
||||
if (this.headerid) {
|
||||
let currentApplicationDetails = { headerid: this.headerid, applicationName: this.applicationName }
|
||||
this.storageService.set("currentapplication", currentApplicationDetails);
|
||||
}
|
||||
|
||||
this.navigationService.navigate(['edit-holder', id]);
|
||||
}
|
||||
|
||||
inactivateHolder(holderid: number): void {
|
||||
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
|
||||
width: '350px',
|
||||
data: {
|
||||
title: 'Confirm Inactivation',
|
||||
message: 'Are you sure you want to inactivate this holder?',
|
||||
confirmText: 'Yes',
|
||||
cancelText: 'Cancel'
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.holderService.inactivateHolder(holderid).subscribe({
|
||||
next: () => {
|
||||
this.notificationService.showSuccess('Holder inactivated successfully');
|
||||
this.searchHolders();
|
||||
},
|
||||
error: (error) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to inactivate holder');
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error inactivating holder:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
reactivateHolder(holderid: number): void {
|
||||
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
|
||||
width: '350px',
|
||||
data: {
|
||||
title: 'Confirm Reactivation',
|
||||
message: 'Are you sure you want to reactivate this holder?',
|
||||
confirmText: 'Yes',
|
||||
cancelText: 'Cancel'
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.holderService.reactivateHolder(holderid).subscribe({
|
||||
next: () => {
|
||||
this.notificationService.showSuccess('Holder reactivated successfully');
|
||||
this.searchHolders();
|
||||
},
|
||||
error: (error) => {
|
||||
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.searchHolders();
|
||||
}
|
||||
|
||||
onHolderSelection(holder: any): void {
|
||||
this.selectedHolderId = holder.holderid;
|
||||
}
|
||||
|
||||
getAddressLabel(holder: BasicDetail): string {
|
||||
|
||||
const parts = [
|
||||
holder.address1,
|
||||
holder.address2,
|
||||
holder.city,
|
||||
holder.state,
|
||||
holder.zip,
|
||||
holder.country
|
||||
];
|
||||
|
||||
// Filter out any empty, null, or undefined parts and then join them.
|
||||
return parts.filter(part => part).join(', ');
|
||||
}
|
||||
}
|
||||
63
src/app/home/bor-dialog.component.ts
Normal file
63
src/app/home/bor-dialog.component.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import { Component, inject, Inject } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { AngularMaterialModule } from '../shared/module/angular-material.module';
|
||||
import { UserService } from '../core/services/common/user.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bor-dialog',
|
||||
imports: [AngularMaterialModule, CommonModule, ReactiveFormsModule],
|
||||
template: `
|
||||
<h2 mat-dialog-title>{{title}}</h2>
|
||||
<mat-dialog-content>
|
||||
This is NOT your carnet. Did you send BOR document to {{spName}}?
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions>
|
||||
<button mat-raised-button color="primary"
|
||||
(click)="onSubmit()">
|
||||
Yes
|
||||
</button>
|
||||
<button mat-button (click)="onCancel()">No</button>
|
||||
</mat-dialog-actions>
|
||||
`,
|
||||
styles: [`
|
||||
`]
|
||||
})
|
||||
export class BorDialogComponent {
|
||||
carnetAction: any | null = null;
|
||||
title: string = '';
|
||||
spName: string = '';
|
||||
|
||||
private userService = inject(UserService);
|
||||
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<BorDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: { carnetAction: string },
|
||||
) {
|
||||
this.carnetAction = data.carnetAction;
|
||||
this.spName = this.userService.getUserDetails()?.userDetails?.serviceProviderName || '';
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.setTitle();
|
||||
}
|
||||
|
||||
setTitle(): void {
|
||||
if (this.carnetAction === 'duplicate') {
|
||||
this.title = 'Duplicate Carnet';
|
||||
} else if (this.carnetAction === 'extend') {
|
||||
this.title = 'Extend Carnet';
|
||||
} else if (this.carnetAction === 'additional') {
|
||||
this.title = 'Additional Sets';
|
||||
}
|
||||
}
|
||||
|
||||
onSubmit(): void {
|
||||
this.dialogRef.close(this.carnetAction);
|
||||
}
|
||||
|
||||
onCancel(): void {
|
||||
this.dialogRef.close('delete');
|
||||
}
|
||||
}
|
||||
87
src/app/home/carnet-dialog.component.ts
Normal file
87
src/app/home/carnet-dialog.component.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import { Component, inject, Inject } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { AngularMaterialModule } from '../shared/module/angular-material.module';
|
||||
|
||||
@Component({
|
||||
selector: 'app-carnet-dialog',
|
||||
imports: [AngularMaterialModule, CommonModule, ReactiveFormsModule],
|
||||
template: `
|
||||
<h2 mat-dialog-title>{{title}}</h2>
|
||||
<mat-dialog-content>
|
||||
<form [formGroup]="carnetForm">
|
||||
<div class="form-row">
|
||||
<!-- Carnet Number Field -->
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Carnet Number</mat-label>
|
||||
<input matInput formControlName="carnetNumber" required>
|
||||
<mat-error *ngIf="carnetForm.get('carnetNumber')?.errors?.['required']">
|
||||
Carnet number is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</form>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions>
|
||||
<button mat-raised-button color="primary" (click)="onSubmit()" [disabled]="!carnetForm.valid">
|
||||
Ok
|
||||
</button>
|
||||
<button mat-button (click)="onCancel()">Cancel</button>
|
||||
</mat-dialog-actions>
|
||||
`,
|
||||
styles: [`
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-top: 0.5rem;
|
||||
align-items: start;
|
||||
|
||||
mat-form-field {
|
||||
flex: 1;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class CarnetDialogComponent {
|
||||
carnetAction: any | null = null;
|
||||
carnetForm: FormGroup;
|
||||
title: string = '';
|
||||
|
||||
private fb = inject(FormBuilder);
|
||||
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<CarnetDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: { carnetAction: string }
|
||||
) {
|
||||
this.carnetAction = data.carnetAction;
|
||||
|
||||
this.carnetForm = this.fb.group({
|
||||
carnetNumber: ['', Validators.required]
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.setTitle();
|
||||
}
|
||||
|
||||
setTitle(): void {
|
||||
if (this.carnetAction === 'duplicate') {
|
||||
this.title = 'Duplicate Carnet';
|
||||
} else if (this.carnetAction === 'extend') {
|
||||
this.title = 'Extend Carnet';
|
||||
} else if (this.carnetAction === 'additional') {
|
||||
this.title = 'Additional Sets';
|
||||
}
|
||||
}
|
||||
|
||||
onSubmit(): void {
|
||||
this.dialogRef.close(this.carnetForm.value.carnetNumber);
|
||||
}
|
||||
|
||||
onCancel(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
||||
75
src/app/home/copy-carnet-dialog.component.ts
Normal file
75
src/app/home/copy-carnet-dialog.component.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import { Component, inject, Inject } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { AngularMaterialModule } from '../shared/module/angular-material.module';
|
||||
|
||||
@Component({
|
||||
selector: 'app-copy-carnet-dialog',
|
||||
imports: [AngularMaterialModule, CommonModule, ReactiveFormsModule],
|
||||
template: `
|
||||
<h2 mat-dialog-title>Copy Carnet</h2>
|
||||
<mat-dialog-content>
|
||||
<form [formGroup]="carnetForm">
|
||||
<div class="form-row">
|
||||
<!-- Application Name Field -->
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Application Name</mat-label>
|
||||
<input matInput formControlName="applicationName" required>
|
||||
<mat-error *ngIf="carnetForm.get('applicationName')?.errors?.['required']">
|
||||
Application name is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</form>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions>
|
||||
<button mat-raised-button color="primary" (click)="onSubmit()" [disabled]="!carnetForm.valid">
|
||||
Ok
|
||||
</button>
|
||||
<button mat-button (click)="onCancel()">Cancel</button>
|
||||
</mat-dialog-actions>
|
||||
`,
|
||||
styles: [`
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-top: 0.5rem;
|
||||
align-items: start;
|
||||
|
||||
mat-form-field {
|
||||
flex: 1;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class CopyCarnetDialogComponent {
|
||||
headerid: number = 0;
|
||||
carnetForm: FormGroup;
|
||||
|
||||
private fb = inject(FormBuilder);
|
||||
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<CopyCarnetDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: { headerid: number }
|
||||
) {
|
||||
this.headerid = data.headerid;
|
||||
this.carnetForm = this.fb.group({
|
||||
applicationName: ['', Validators.required]
|
||||
});
|
||||
}
|
||||
|
||||
onSubmit(): void {
|
||||
const data = {
|
||||
applicationName: this.carnetForm.value.applicationName,
|
||||
headerid: this.headerid
|
||||
}
|
||||
this.dialogRef.close(data);
|
||||
}
|
||||
|
||||
onCancel(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,7 @@
|
||||
<div class="content">
|
||||
<div class="app-container">
|
||||
<!-- Chart Section -->
|
||||
<div class="chart-section">
|
||||
<div class="chart-section" *ngIf="carnetData.length > 0">
|
||||
<app-chart [chartData]="carnetData" [carnetStatuses]="carnetStatuses"
|
||||
(carnetStatusData)="onCarnetStatusClick($event)"></app-chart>
|
||||
</div>
|
||||
@ -12,33 +12,51 @@
|
||||
<div class="question-row">
|
||||
<mat-radio-group (change)="newCarnet($event)">
|
||||
<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>
|
||||
<div class="answers">
|
||||
<mat-radio-button [value]="true">Yes</mat-radio-button>
|
||||
<mat-radio-button [value]="false">No</mat-radio-button>
|
||||
</div>
|
||||
</mat-radio-group>
|
||||
</div>
|
||||
|
||||
<div class="question-row">
|
||||
<mat-radio-group (change)="additionalSets($event)">
|
||||
<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>
|
||||
<div class="answers">
|
||||
<mat-radio-button [value]="true">Yes</mat-radio-button>
|
||||
<mat-radio-button [value]="false">No</mat-radio-button>
|
||||
</div>
|
||||
</mat-radio-group>
|
||||
</div>
|
||||
|
||||
<div class="question-row">
|
||||
<mat-radio-group (change)="toggleDuplicateQuestion($event)">
|
||||
<mat-label class="question">Did you misplace your carnet?</mat-label>
|
||||
<div class="answers">
|
||||
<mat-radio-button [value]="true">Yes</mat-radio-button>
|
||||
<mat-radio-button [value]="false">No</mat-radio-button>
|
||||
</div>
|
||||
</mat-radio-group>
|
||||
</div>
|
||||
|
||||
<div class="question-row" *ngIf="duplicateQuestion">
|
||||
<mat-radio-group (change)="duplicateCarnet($event)">
|
||||
<mat-label class="question">Did you lose your carnet? Are you looking for
|
||||
<mat-label class="question sub-question">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>
|
||||
<div class="answers">
|
||||
<mat-radio-button [value]="true">Yes</mat-radio-button>
|
||||
<mat-radio-button [value]="false">No</mat-radio-button>
|
||||
</div>
|
||||
</mat-radio-group>
|
||||
</div>
|
||||
|
||||
<div class="question-row">
|
||||
<mat-radio-group (change)="extendCarnet($event)">
|
||||
<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>
|
||||
<div class="answers">
|
||||
<mat-radio-button [value]="true">Yes</mat-radio-button>
|
||||
<mat-radio-button [value]="false">No</mat-radio-button>
|
||||
</div>
|
||||
</mat-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
@ -51,6 +69,34 @@
|
||||
</div>
|
||||
|
||||
<div class="table-actions">
|
||||
<div class="filter-controls" *ngIf="dataSource.data.length > 0">
|
||||
<!-- Column Selection Dropdown -->
|
||||
<mat-form-field appearance="outline" class="filter-field">
|
||||
<mat-label>Filter By</mat-label>
|
||||
<mat-select [(value)]="selectedColumn" (selectionChange)="onColumnChange()">
|
||||
<mat-option value=""></mat-option>
|
||||
<mat-option *ngFor="let column of filterableColumns" [value]="column.value">
|
||||
{{column.displayName}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Search Input -->
|
||||
<mat-form-field appearance="outline" class="filter-field">
|
||||
<mat-label>Search Value</mat-label>
|
||||
<input matInput [(ngModel)]="filterValue" (keyup)="applyFilter()"
|
||||
placeholder="Enter search term...">
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Clear Filter Button -->
|
||||
<button mat-raised-button type="button" (click)="clearFilter()"
|
||||
[disabled]="!selectedColumn && !filterValue">
|
||||
<mat-icon>clear_all</mat-icon>
|
||||
Clear Filter
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<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
|
||||
@ -74,6 +120,16 @@
|
||||
<td mat-cell *matCellDef="let item">{{ item.carnetNumber }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="deliveryType">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Delivery Type</th>
|
||||
<td mat-cell *matCellDef="let item">{{ item.deliveryType }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="securityType">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Security Type</th>
|
||||
<td mat-cell *matCellDef="let item">{{ item.securityType }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="usSets">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>US Sets</th>
|
||||
<td mat-cell *matCellDef="let item">{{ item.usSets }}</td>
|
||||
@ -90,7 +146,7 @@
|
||||
|
||||
<ng-container matColumnDef="carnetValue">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Carnet Value</th>
|
||||
<td mat-cell *matCellDef="let item">{{ item.carnetValue }}</td>
|
||||
<td mat-cell *matCellDef="let item">{{ item.carnetValue | currency}}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="issueDate">
|
||||
@ -118,12 +174,36 @@
|
||||
<ng-container matColumnDef="actions">
|
||||
<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) === 'Not Submitted'"
|
||||
[hidden]="getCarnetStatusLabel(item.carnetStatus) !== 'Not Submitted'"
|
||||
matTooltip="Edit">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<div class="action-buttons">
|
||||
<button matIconButton [matMenuTriggerFor]="carnetActions">
|
||||
<mat-icon>more_horiz</mat-icon>
|
||||
</button>
|
||||
<mat-menu #carnetActions="matMenu">
|
||||
<button mat-menu-item (click)="editCarnet(item)"
|
||||
*ngIf="getCarnetStatusLabel(item.carnetStatus) === 'Not Submitted'">
|
||||
<mat-icon>edit</mat-icon>
|
||||
<span>Edit</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="viewCarnet(item)">
|
||||
<mat-icon>article</mat-icon>
|
||||
<span>View</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="deleteCarnet(item.headerid)"
|
||||
*ngIf="getCarnetStatusLabel(item.carnetStatus) === 'Not Submitted'">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span>Delete</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="printGeneralList(item.headerid)"
|
||||
*ngIf="item.printGLStatus === 'PRINTGL'">
|
||||
<mat-icon>print</mat-icon>
|
||||
<span>Print GL</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="copyCarnet(item)">
|
||||
<mat-icon>file_copy</mat-icon>
|
||||
<span>Copy</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
@ -138,7 +218,8 @@
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<mat-paginator [length]="dataSource.data.length" [pageSizeOptions]="[userPreferences.pageSize!]"
|
||||
<mat-paginator *ngIf="dataSource.data.length > userPreferences.pageSize!"
|
||||
[length]="dataSource.data.length" [pageSizeOptions]="[userPreferences.pageSize!]"
|
||||
[hidePageSize]="true" showFirstLastButtons></mat-paginator>
|
||||
</div>
|
||||
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
}
|
||||
|
||||
.questions-section {
|
||||
flex: 0 0 70%;
|
||||
flex: 0 0 65%;
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 46px;
|
||||
margin-left: 1rem;
|
||||
@ -33,7 +33,9 @@
|
||||
|
||||
mat-radio-group {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 51%;
|
||||
|
||||
.question {
|
||||
color: var(--mat-sys-on-surface);
|
||||
@ -44,8 +46,8 @@
|
||||
font-weight: var(--mat-sys-medium-font-weight);
|
||||
}
|
||||
|
||||
mat-radio-button {
|
||||
margin-top: -5px;
|
||||
.sub-question {
|
||||
padding-left: 1.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -54,10 +56,13 @@
|
||||
|
||||
.table-actions {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
|
||||
button {
|
||||
margin-bottom: 16px;
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
@ -79,7 +84,7 @@
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
mat-table {
|
||||
table {
|
||||
width: 100%;
|
||||
|
||||
mat-icon {
|
||||
@ -90,6 +95,16 @@
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
// .mat-column-actions {
|
||||
// width: 135px;
|
||||
|
||||
// .action-buttons {
|
||||
// width: 100%;
|
||||
// display: flex;
|
||||
// gap: 4px;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
.no-data-message {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user