process carnet and param country message updates
This commit is contained in:
parent
882d73c248
commit
a808c677bd
11
package-lock.json
generated
11
package-lock.json
generated
@ -21,6 +21,7 @@
|
||||
"@angular/router": "^20.0.5",
|
||||
"@angular/ssr": "^20.0.4",
|
||||
"chart.js": "^4.3.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"express": "^4.18.2",
|
||||
"ng2-charts": "^8.0.0",
|
||||
"ngx-cookie-service": "^20.0.1",
|
||||
@ -4645,6 +4646,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",
|
||||
|
||||
@ -24,6 +24,7 @@
|
||||
"@angular/router": "^20.0.5",
|
||||
"@angular/ssr": "^20.0.4",
|
||||
"chart.js": "^4.3.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"express": "^4.18.2",
|
||||
"ng2-charts": "^8.0.0",
|
||||
"ngx-cookie-service": "^20.0.1",
|
||||
|
||||
@ -10,7 +10,12 @@ export const serverRoutes: ServerRoute[] = [
|
||||
{ path: ':appId/preparer/:id', renderMode: RenderMode.Client },
|
||||
{ path: ':appId/add-preparer', renderMode: RenderMode.Client },
|
||||
{ path: ':appId/table-record', renderMode: RenderMode.Client },
|
||||
{ path: ':appId/country-messages', renderMode: RenderMode.Client },
|
||||
{ path: ':appId/add-preparer', renderMode: RenderMode.Client },
|
||||
{ path: ':appId/edit-carnet/:headerid', renderMode: RenderMode.Client },
|
||||
{ path: ':appId/view-carnet/:headerid', renderMode: RenderMode.Client },
|
||||
{ path: ':appId/add-holder', renderMode: RenderMode.Client },
|
||||
{ path: ':appId/edit-holder/:holderid', renderMode: RenderMode.Client },
|
||||
{
|
||||
path: '**',
|
||||
renderMode: RenderMode.Prerender
|
||||
|
||||
@ -19,6 +19,11 @@ export const routes: Routes = [
|
||||
{ path: 'add-preparer', loadComponent: () => import('./preparer/add/add-preparer.component').then(m => m.AddPreparerComponent) },
|
||||
{ path: 'table-record', loadComponent: () => import('./param/manage-table-record/manage-table-record.component').then(m => m.ManageTableRecordComponent) },
|
||||
{ path: 'param-record/:id', loadComponent: () => import('./param/manage-param-record/manage-param-record.component').then(m => m.ManageParamRecordComponent) },
|
||||
{ path: 'country-messages', loadComponent: () => import('./param/manage-country/manage-country.component').then(m => m.ManageCountryComponent) },
|
||||
{ path: 'edit-carnet/:headerid', loadComponent: () => import('./carnet/edit/edit-carnet.component').then(m => m.EditCarnetComponent) },
|
||||
{ path: 'view-carnet/:headerid', loadComponent: () => import('./carnet/view/view-carnet.component').then(m => m.ViewCarnetComponent) },
|
||||
{ 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: '', redirectTo: 'home', pathMatch: 'full' }
|
||||
],
|
||||
canActivate: [AuthGuard, AppIdGuard]
|
||||
|
||||
53
src/app/carnet/add/add-carnet.component.html
Normal file
53
src/app/carnet/add/add-carnet.component.html
Normal file
@ -0,0 +1,53 @@
|
||||
<div class="client-carnet-container">
|
||||
<!-- Stepper Section (shown after questions are answered) -->
|
||||
<mat-stepper orientation="vertical" [linear]="isLinear" (selectionChange)="onStepChange($event)"
|
||||
[selectedIndex]="currentStep">
|
||||
|
||||
<!-- Application Name Step -->
|
||||
<mat-step *ngIf="applicationType === 'new'" [completed]="stepsCompleted.applicationDetail"
|
||||
[editable]="stepsCompleted.applicationDetail">
|
||||
<ng-template matStepLabel>Application Name</ng-template>
|
||||
<app-application [isEditMode]="isEditMode" (applicationCreated)="onApplicationDetailCreated($event)">
|
||||
</app-application>
|
||||
</mat-step>
|
||||
|
||||
<!-- Holder Selection Step -->
|
||||
<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"
|
||||
[applicationName]="applicationName" (updated)="onHolderSelectionUpdated($event)">
|
||||
</app-holder>
|
||||
</mat-step>
|
||||
|
||||
<!-- Goods Section Step -->
|
||||
<mat-step *ngIf="applicationType === 'new'" [completed]="stepsCompleted.goodsSection"
|
||||
[editable]="stepsCompleted.applicationDetail">
|
||||
<ng-template matStepLabel>Goods Section</ng-template>
|
||||
<app-goods (completed)="onGoodsSectionSaved($event)" [headerid]="headerid"
|
||||
[userPreferences]="userPreferences">
|
||||
</app-goods>
|
||||
</mat-step>
|
||||
|
||||
<!-- Travel Plan Step -->
|
||||
<mat-step *ngIf="applicationType === 'new' || applicationType === 'extend' || applicationType === 'additional'"
|
||||
[completed]="stepsCompleted.travelPlan" [editable]="stepsCompleted.applicationDetail">
|
||||
<ng-template matStepLabel>Travel Plan</ng-template>
|
||||
<app-travel-plan (completed)="onTravelPlanSaved($event)" [headerid]="headerid">
|
||||
</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"
|
||||
[applicationName]="applicationName" [enableSubmitButton]="allSectionsCompleted">
|
||||
</app-shipping>
|
||||
</mat-step>
|
||||
|
||||
<!-- Icon overrides. -->
|
||||
<ng-template matStepperIcon="edit">
|
||||
<mat-icon>done</mat-icon>
|
||||
</ng-template>
|
||||
</mat-stepper>
|
||||
</div>
|
||||
11
src/app/carnet/add/add-carnet.component.scss
Normal file
11
src/app/carnet/add/add-carnet.component.scss
Normal file
@ -0,0 +1,11 @@
|
||||
// .client-carnet-container {
|
||||
// // padding: 24px;
|
||||
// // max-width: 1200px;
|
||||
// // margin: 0 auto;
|
||||
|
||||
// .actions {
|
||||
// margin-top: 24px;
|
||||
// display: flex;
|
||||
// justify-content: flex-start;
|
||||
// }
|
||||
// }
|
||||
90
src/app/carnet/add/add-carnet.component.ts
Normal file
90
src/app/carnet/add/add-carnet.component.ts
Normal file
@ -0,0 +1,90 @@
|
||||
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';
|
||||
import { ApplicationComponent } from "../application/application.component";
|
||||
import { HolderComponent } from '../holder/holder.component';
|
||||
import { GoodsComponent } from '../goods/goods.component';
|
||||
import { TravelPlanComponent } from '../travel-plan/travel-plan.component';
|
||||
import { ShippingComponent } from '../shipping/shipping.component';
|
||||
import { UserPreferencesService } from '../../core/services/user-preference.service';
|
||||
import { UserPreferences } from '../../core/models/user-preference';
|
||||
|
||||
@Component({
|
||||
selector: 'app-add-carnet',
|
||||
imports: [AngularMaterialModule, CommonModule, ReactiveFormsModule,
|
||||
ApplicationComponent, HolderComponent, GoodsComponent, TravelPlanComponent,
|
||||
ShippingComponent],
|
||||
templateUrl: './add-carnet.component.html',
|
||||
styleUrl: './add-carnet.component.scss'
|
||||
})
|
||||
export class AddCarnetComponent {
|
||||
currentStep = 0;
|
||||
isLinear = true;
|
||||
applicationType: 'new' | 'additional' | 'duplicate' | 'extend' | null = 'new';
|
||||
isEditMode = false;
|
||||
headerid: number = 0;
|
||||
applicationName: string = '';
|
||||
userPreferences: UserPreferences;
|
||||
allSectionsCompleted: boolean = false;
|
||||
|
||||
// Track completion of each step
|
||||
stepsCompleted = {
|
||||
applicationDetail: false,
|
||||
holderSelection: false,
|
||||
goodsSection: false,
|
||||
travelPlan: false,
|
||||
shipping: false
|
||||
};
|
||||
|
||||
@ViewChild(ShippingComponent, { static: false })
|
||||
private shippingComponent!: ShippingComponent;
|
||||
|
||||
constructor(userPrefenceService: UserPreferencesService) {
|
||||
this.userPreferences = userPrefenceService.getPreferences();
|
||||
}
|
||||
|
||||
onStepChange(event: StepperSelectionEvent): void {
|
||||
this.currentStep = event.selectedIndex;
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
this.allSectionsCompleted = this.stepsCompleted.applicationDetail
|
||||
&& this.stepsCompleted.holderSelection && this.stepsCompleted.goodsSection
|
||||
&& this.stepsCompleted.shipping && this.stepsCompleted.travelPlan;
|
||||
}
|
||||
}
|
||||
32
src/app/carnet/application/application.component.html
Normal file
32
src/app/carnet/application/application.component.html
Normal file
@ -0,0 +1,32 @@
|
||||
<div class="application-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]="applicationDetailsForm" class="details-form" *ngIf="!isLoading"
|
||||
(ngSubmit)="saveApplicationDetails()">
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline" class="name">
|
||||
<mat-label>Name</mat-label>
|
||||
<input matInput formControlName="name" required>
|
||||
<mat-error *ngIf="f['name'].errors?.['required']">
|
||||
Name is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="f['name'].errors?.['maxlength']">
|
||||
Maximum 50 characters allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button mat-raised-button color="primary" type="submit" *ngIf="!isViewMode"
|
||||
[disabled]="applicationDetailsForm.invalid || disableSaveButton ">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
102
src/app/carnet/application/application.component.scss
Normal file
102
src/app/carnet/application/application.component.scss
Normal file
@ -0,0 +1,102 @@
|
||||
.application-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;
|
||||
|
||||
mat-card-content {
|
||||
padding: inherit 0;
|
||||
}
|
||||
|
||||
.loading-shade {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.details-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
|
||||
.name {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.lookup-code {
|
||||
grid-column: span 1;
|
||||
}
|
||||
|
||||
.address1,
|
||||
.address2 {
|
||||
grid-column: span 3;
|
||||
}
|
||||
|
||||
.city,
|
||||
.state,
|
||||
.country,
|
||||
.zip {
|
||||
grid-column: span 1;
|
||||
}
|
||||
|
||||
.carnet-issuing-region,
|
||||
.revenue-location {
|
||||
grid-column: span 1;
|
||||
}
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.application-details-container {
|
||||
.details-card {
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
.name,
|
||||
.lookup-code,
|
||||
.address1,
|
||||
.address2,
|
||||
.city,
|
||||
.state,
|
||||
.zip,
|
||||
.country,
|
||||
.carnet-issuing-region,
|
||||
.revenue-location {
|
||||
grid-column: span 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
118
src/app/carnet/application/application.component.ts
Normal file
118
src/app/carnet/application/application.component.ts
Normal file
@ -0,0 +1,118 @@
|
||||
import { Component, EventEmitter, inject, Input, Output } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { ApplicationDetailService } from '../../core/services/carnet/application-detail.service';
|
||||
import { NotificationService } from '../../core/services/common/notification.service';
|
||||
import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service';
|
||||
import { ApplicationDetail } from '../../core/models/carnet/application-detail';
|
||||
import { Subject } from 'rxjs';
|
||||
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-application',
|
||||
imports: [AngularMaterialModule, CommonModule, ReactiveFormsModule],
|
||||
templateUrl: './application.component.html',
|
||||
styleUrl: './application.component.scss'
|
||||
})
|
||||
export class ApplicationComponent {
|
||||
@Input() isEditMode = false;
|
||||
@Input() isViewMode = false;
|
||||
@Input() headerid: number = 0;
|
||||
@Input() applicationName: string = '';
|
||||
|
||||
@Output() applicationCreated = new EventEmitter<{ headerid: number, applicationName: string }>();
|
||||
|
||||
applicationDetailsForm: FormGroup;
|
||||
isLoading = false;
|
||||
disableSaveButton = false;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
private fb = inject(FormBuilder);
|
||||
private applicationDetailService = inject(ApplicationDetailService);
|
||||
private notificationService = inject(NotificationService);
|
||||
private errorHandler = inject(ApiErrorHandlerService);
|
||||
private route = inject(ActivatedRoute);
|
||||
|
||||
constructor() {
|
||||
this.applicationDetailsForm = this.createForm();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// Patch edit form data
|
||||
if (this.applicationName) {
|
||||
this.patchFormData();
|
||||
|
||||
// this.applicationDetailService.getApplicationDetailsById(this.headerid).subscribe({
|
||||
// next: (applicationDetail: ApplicationDetail) => {
|
||||
// if (applicationDetail?.applicationId > 0) {
|
||||
// this.patchFormData(applicationDetail);
|
||||
// }
|
||||
// this.isLoading = false;
|
||||
// },
|
||||
// error: (error: any) => {
|
||||
// let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load application details');
|
||||
// this.notificationService.showError(errorMessage);
|
||||
// this.isLoading = false;
|
||||
// console.error('Error loading application details:', error);
|
||||
// }
|
||||
// });
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
createForm(): FormGroup {
|
||||
return this.fb.group({
|
||||
name: ['', [Validators.required, Validators.maxLength(50)]],
|
||||
});
|
||||
}
|
||||
|
||||
patchFormData(): void {
|
||||
this.applicationDetailsForm.patchValue({
|
||||
name: this.applicationName,
|
||||
});
|
||||
|
||||
if (this.isEditMode) {
|
||||
this.applicationDetailsForm.get('name')?.disable();
|
||||
this.disableSaveButton = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Convenience getter for easy access to form fields
|
||||
get f() {
|
||||
return this.applicationDetailsForm.controls;
|
||||
}
|
||||
|
||||
saveApplicationDetails(): void {
|
||||
if (this.applicationDetailsForm.invalid) {
|
||||
this.applicationDetailsForm.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
|
||||
const applicationDetailData: ApplicationDetail = this.applicationDetailsForm.value;
|
||||
|
||||
if (!this.isEditMode && this.headerid == 0) {
|
||||
this.isLoading = true;
|
||||
this.applicationDetailService.createApplicationDetails(applicationDetailData).subscribe({
|
||||
next: (applicationData: any) => {
|
||||
this.notificationService.showSuccess(`Application details added successfully`);
|
||||
this.applicationCreated.emit({ headerid: +applicationData.HEADERID, applicationName: applicationDetailData.name });
|
||||
this.applicationDetailsForm.get('name')?.disable();
|
||||
this.disableSaveButton = true;
|
||||
this.isLoading = false;
|
||||
},
|
||||
error: (error) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, `Failed to add application details`);
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error saving application details:', error);
|
||||
this.isLoading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
53
src/app/carnet/edit/edit-carnet.component.html
Normal file
53
src/app/carnet/edit/edit-carnet.component.html
Normal file
@ -0,0 +1,53 @@
|
||||
<div class="client-carnet-container">
|
||||
<!-- Stepper Section (shown after questions are answered) -->
|
||||
<mat-stepper orientation="vertical" [linear]="isLinear" (selectionChange)="onStepChange($event)"
|
||||
[selectedIndex]="currentStep">
|
||||
|
||||
<!-- Application Name Step -->
|
||||
<mat-step *ngIf="applicationType === 'new'" [completed]="stepsCompleted.applicationDetail"
|
||||
[editable]="stepsCompleted.applicationDetail">
|
||||
<ng-template matStepLabel>Application Name</ng-template>
|
||||
<app-application [isEditMode]="isEditMode" [applicationName]="applicationName">
|
||||
</app-application>
|
||||
</mat-step>
|
||||
|
||||
<!-- Holder Selection Step -->
|
||||
<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"
|
||||
[applicationName]="applicationName" (updated)="onHolderSelectionUpdated($event)">
|
||||
</app-holder>
|
||||
</mat-step>
|
||||
|
||||
<!-- Goods Section Step -->
|
||||
<mat-step *ngIf="applicationType === 'new'" [completed]="stepsCompleted.goodsSection"
|
||||
[editable]="stepsCompleted.applicationDetail">
|
||||
<ng-template matStepLabel>Goods Section</ng-template>
|
||||
<app-goods (completed)="onGoodsSectionSaved($event)" [headerid]="headerid" [isEditMode]="isEditMode"
|
||||
[userPreferences]="userPreferences">
|
||||
</app-goods>
|
||||
</mat-step>
|
||||
|
||||
<!-- Travel Plan Step -->
|
||||
<mat-step *ngIf="applicationType === 'new' || applicationType === 'extend' || applicationType === 'additional'"
|
||||
[completed]="stepsCompleted.travelPlan" [editable]="stepsCompleted.applicationDetail">
|
||||
<ng-template matStepLabel>Travel Plan</ng-template>
|
||||
<app-travel-plan (completed)="onTravelPlanSaved($event)" [headerid]="headerid">
|
||||
</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" [isEditMode]="isEditMode"
|
||||
[applicationName]="applicationName" [enableSubmitButton]="allSectionsCompleted">
|
||||
</app-shipping>
|
||||
</mat-step>
|
||||
|
||||
<!-- Icon overrides. -->
|
||||
<ng-template matStepperIcon="edit">
|
||||
<mat-icon>done</mat-icon>
|
||||
</ng-template>
|
||||
</mat-stepper>
|
||||
</div>
|
||||
0
src/app/carnet/edit/edit-carnet.component.scss
Normal file
0
src/app/carnet/edit/edit-carnet.component.scss
Normal file
102
src/app/carnet/edit/edit-carnet.component.ts
Normal file
102
src/app/carnet/edit/edit-carnet.component.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
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';
|
||||
import { StepperSelectionEvent } from '@angular/cdk/stepper';
|
||||
import { GoodsComponent } from '../goods/goods.component';
|
||||
import { HolderComponent } from '../holder/holder.component';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
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';
|
||||
|
||||
@Component({
|
||||
selector: 'app-edit-carnet',
|
||||
imports: [AngularMaterialModule, CommonModule, ReactiveFormsModule,
|
||||
ApplicationComponent, HolderComponent, GoodsComponent, TravelPlanComponent, ShippingComponent],
|
||||
templateUrl: './edit-carnet.component.html',
|
||||
styleUrl: './edit-carnet.component.scss'
|
||||
})
|
||||
export class EditCarnetComponent {
|
||||
currentStep = 0;
|
||||
isLinear = false;
|
||||
applicationType: 'new' | 'additional' | 'duplicate' | 'extend' | null = 'new';
|
||||
isEditMode = true; // Set to true for edit mode
|
||||
headerid: number = 0;
|
||||
userPreferences: UserPreferences;
|
||||
applicationName: string = '';
|
||||
allSectionsCompleted: boolean = false;
|
||||
|
||||
@ViewChild(ShippingComponent, { static: false })
|
||||
private shippingComponent!: ShippingComponent;
|
||||
|
||||
// Track completion of each step
|
||||
stepsCompleted = {
|
||||
applicationDetail: true,
|
||||
holderSelection: false,
|
||||
goodsSection: false,
|
||||
travelPlan: false,
|
||||
shipping: false
|
||||
};
|
||||
|
||||
private route = inject(ActivatedRoute);
|
||||
|
||||
constructor(userPrefenceService: UserPreferencesService) {
|
||||
this.userPreferences = userPrefenceService.getPreferences();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
const idParam = this.route.snapshot.paramMap.get('headerid');
|
||||
this.headerid = idParam ? parseInt(idParam, 10) : 0;
|
||||
|
||||
this.applicationName = this.route.snapshot.queryParamMap.get('applicationname') || '';
|
||||
|
||||
let returnTo = this.route.snapshot.queryParamMap.get('return');
|
||||
|
||||
if (returnTo === 'holder') {
|
||||
this.currentStep = 1;
|
||||
} else if (returnTo === 'shipping') {
|
||||
this.currentStep = 4;
|
||||
}
|
||||
}
|
||||
|
||||
onStepChange(event: StepperSelectionEvent): void {
|
||||
this.currentStep = event.selectedIndex;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
isAllSectionsCompleted() {
|
||||
this.allSectionsCompleted = this.stepsCompleted.applicationDetail
|
||||
&& this.stepsCompleted.holderSelection && this.stepsCompleted.goodsSection
|
||||
&& this.stepsCompleted.shipping && this.stepsCompleted.travelPlan;
|
||||
}
|
||||
}
|
||||
240
src/app/carnet/goods/goods.component.html
Normal file
240
src/app/carnet/goods/goods.component.html
Normal file
@ -0,0 +1,240 @@
|
||||
<div class="goods-container">
|
||||
|
||||
<form [formGroup]="goodsForm">
|
||||
<!-- Purpose Section -->
|
||||
<div class="form-section">
|
||||
<div class="checkbox-group">
|
||||
<mat-label>Goods to be imported as </mat-label>
|
||||
<mat-checkbox formControlName="commercialSample">Commercial Sample</mat-checkbox>
|
||||
<mat-checkbox formControlName="professionalEquipment">Professional Equipment</mat-checkbox>
|
||||
<mat-checkbox formControlName="exhibitionsFair">Exhibitions and Fair</mat-checkbox>
|
||||
|
||||
<mat-error *ngIf="goodsForm.errors?.['atLeastOneRequired']">
|
||||
At least one must be selected
|
||||
</mat-error>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<div class="checkbox-group">
|
||||
<mat-checkbox formControlName="roadVehiclesUsed">Road Vehicles
|
||||
used?</mat-checkbox>
|
||||
<mat-checkbox formControlName="horseUsed">Horse used?</mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Authorized Representatives Section -->
|
||||
<div class="form-section">
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>Authorized Representative(s)</mat-label>
|
||||
<textarea matInput formControlName="authorizedRepresentatives" rows="3"></textarea>
|
||||
<mat-error *ngIf="goodsForm.get('authorizedRepresentatives')?.errors?.['required']">
|
||||
Authorized Representative(s) is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="table-actions">
|
||||
<h3>Goods Items</h3>
|
||||
<div class="actions">
|
||||
<button mat-raised-button color="primary" type="button" *ngIf="!isViewMode" (click)="addNewItem()">
|
||||
<mat-icon>add</mat-icon> Add Item
|
||||
</button>
|
||||
<div class="upload-section">
|
||||
<input type="file" accept=".xlsx,.xls,.csv" (change)="fileUpload($event)" hidden #fileInput>
|
||||
<button mat-raised-button type="button" *ngIf="!isViewMode" (click)="fileInput.click()"
|
||||
[disabled]="isProcessing">
|
||||
<mat-icon>upload</mat-icon> Upload
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-container mat-elevation-z8">
|
||||
<div class="loading-shade" *ngIf="isLoading">
|
||||
<mat-spinner diameter="50"></mat-spinner>
|
||||
</div>
|
||||
|
||||
<table mat-table [dataSource]="dataSource" matSort>
|
||||
<!-- Item Number Column -->
|
||||
<ng-container matColumnDef="itemNumber">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Item Number</th>
|
||||
<td mat-cell *matCellDef="let item">{{ item.itemNumber }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Description Column -->
|
||||
<ng-container matColumnDef="description">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Description</th>
|
||||
<td mat-cell *matCellDef="let item">{{ item.description }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Pieces Column -->
|
||||
<ng-container matColumnDef="pieces">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Pieces</th>
|
||||
<td mat-cell *matCellDef="let item">{{ item.pieces }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Weight Column -->
|
||||
<ng-container matColumnDef="weight">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Weight</th>
|
||||
<td mat-cell *matCellDef="let item">{{ formatDecimalDisplay(item.weight, 4) }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Unit of Measure Column -->
|
||||
<ng-container matColumnDef="unitOfMeasure">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Unit Of Measure</th>
|
||||
<td mat-cell *matCellDef="let item">{{ getUnitOfMeasureLabel(item.unitOfMeasure) }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Value Column -->
|
||||
<ng-container matColumnDef="value">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Value</th>
|
||||
<td mat-cell *matCellDef="let item">{{ formatDecimalDisplay(item.value, 2) | currency}}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Country of Origin Column -->
|
||||
<ng-container matColumnDef="countryOfOrigin">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Country of Origin</th>
|
||||
<td mat-cell *matCellDef="let item">{{ getCountryLabel(item.countryOfOrigin) }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Actions Column -->
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef>Actions</th>
|
||||
<td mat-cell *matCellDef="let item; let i = index">
|
||||
<button mat-icon-button color="primary" *ngIf="!isViewMode" (click)="editItem(item)"
|
||||
matTooltip="Edit">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="warn" *ngIf="!isViewMode" (click)="deleteItem(item)"
|
||||
matTooltip="Delete">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
|
||||
<tr matNoDataRow *matNoDataRow>
|
||||
<td [colSpan]="displayedColumns.length" class="no-data-message">
|
||||
<mat-icon>info</mat-icon>
|
||||
<span>No items added</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<mat-paginator *ngIf="dataSource.data.length > userPreferences.pageSize!" [length]="dataSource.data.length"
|
||||
[pageSizeOptions]="[userPreferences.pageSize!]" [hidePageSize]="true" showFirstLastButtons></mat-paginator>
|
||||
</div>
|
||||
|
||||
<!-- Item Form -->
|
||||
<div class="form-container" *ngIf="showItemForm">
|
||||
<form [formGroup]="itemForm" (ngSubmit)="saveItem()">
|
||||
<div class="form-header">
|
||||
<h3>{{ isEditing ? 'Edit Item' : 'Add New Item' }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Item Number</mat-label>
|
||||
<input matInput formControlName="itemNumber" required>
|
||||
<mat-error *ngIf="itemForm.get('itemNumber')?.errors?.['required']">
|
||||
Item Number is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="description">
|
||||
<mat-label>Description</mat-label>
|
||||
<input matInput formControlName="description" required>
|
||||
<mat-error *ngIf="itemForm.get('description')?.errors?.['required']">
|
||||
Description is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Pieces</mat-label>
|
||||
<input matInput type="number" formControlName="pieces" required>
|
||||
<mat-error *ngIf="itemForm.get('pieces')?.errors?.['required']">
|
||||
Pieces is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="itemForm.get('pieces')?.errors?.['min']">
|
||||
Must be at least 1
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Weight</mat-label>
|
||||
<input matInput type="number" formControlName="weight" required>
|
||||
<mat-error *ngIf="itemForm.get('weight')?.errors?.['required']">
|
||||
Weight is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="itemForm.get('weight')?.errors?.['min']">
|
||||
Must be positive
|
||||
</mat-error>
|
||||
<mat-error *ngIf="itemForm.get('weight')?.errors?.['pattern']">
|
||||
Maximum 4 decimal places allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Unit of Measure</mat-label>
|
||||
<mat-select formControlName="unitOfMeasure" required>
|
||||
<mat-option *ngFor="let unit of unitsOfMeasures" [value]="unit.value">
|
||||
{{ unit.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="itemForm.get('unitOfMeasure')?.errors?.['required']">
|
||||
Unit of measure is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Value</mat-label>
|
||||
<input matInput type="number" formControlName="value" required>
|
||||
<mat-error *ngIf="itemForm.get('value')?.errors?.['required']">
|
||||
Value is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="itemForm.get('value')?.errors?.['min']">
|
||||
Must be positive
|
||||
</mat-error>
|
||||
<mat-error *ngIf="itemForm.get('value')?.errors?.['pattern']">
|
||||
Maximum 2 decimal places allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="country">
|
||||
<mat-label>Country of Origin</mat-label>
|
||||
<mat-select formControlName="countryOfOrigin" required>
|
||||
<mat-option *ngFor="let country of countries" [value]="country.value">
|
||||
{{ country.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="itemForm.get('countryOfOrigin')?.errors?.['required']">
|
||||
Country of origin is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button mat-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()" *ngIf="!isViewMode"
|
||||
[disabled]="goodsForm.invalid || changeInProgress">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
168
src/app/carnet/goods/goods.component.scss
Normal file
168
src/app/carnet/goods/goods.component.scss
Normal file
@ -0,0 +1,168 @@
|
||||
.goods-container {
|
||||
|
||||
.form-section {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
|
||||
mat-label {
|
||||
margin-top: 5px;
|
||||
color: var(--mat-checkbox-label-text-color, var(--mat-sys-on-surface));
|
||||
font-family: var(--mat-checkbox-label-text-font, var(--mat-sys-body-medium-font));
|
||||
line-height: var(--mat-checkbox-label-text-line-height, var(--mat-sys-body-medium-line-height));
|
||||
font-size: var(--mat-checkbox-label-text-size, var(--mat-sys-body-medium-size));
|
||||
letter-spacing: var(--mat-checkbox-label-text-tracking, var(--mat-sys-body-medium-tracking));
|
||||
font-weight: var(--mat-checkbox-label-text-weight, var(--mat-sys-body-medium-weight));
|
||||
}
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.table-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.75rem;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
color: var(--mat-sys-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
span {
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.table-container {
|
||||
position: relative;
|
||||
overflow-x: auto;
|
||||
|
||||
.loading-shade {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
mat-table {
|
||||
width: 100%;
|
||||
|
||||
mat-icon {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
.mat-column-actions {
|
||||
width: 180px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.no-data-message {
|
||||
text-align: center;
|
||||
padding: 0.9rem;
|
||||
color: rgba(0, 0, 0, 0.54);
|
||||
|
||||
mat-icon {
|
||||
font-size: 1rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-bottom: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
mat-paginator {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.12);
|
||||
border-radius: 0 0 8px 8px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-container {
|
||||
// position: fixed;
|
||||
// top: 0;
|
||||
// right: 0;
|
||||
// bottom: 0;
|
||||
// width: 400px;
|
||||
// background: white;
|
||||
// box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1);
|
||||
// padding: 24px;
|
||||
// overflow-y: auto;
|
||||
// z-index: 100;
|
||||
|
||||
.form-header {
|
||||
margin-bottom: 24px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
color: var(--mat-sys-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
align-items: start;
|
||||
|
||||
mat-form-field {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.description {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.country {
|
||||
flex: 2;
|
||||
}
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 16px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 0.9rem;
|
||||
}
|
||||
}
|
||||
460
src/app/carnet/goods/goods.component.ts
Normal file
460
src/app/carnet/goods/goods.component.ts
Normal file
@ -0,0 +1,460 @@
|
||||
import { Component, EventEmitter, inject, Input, Output, ViewChild } from '@angular/core';
|
||||
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
|
||||
import { GoodsService } from '../../core/services/carnet/goods.service';
|
||||
import { NotificationService } from '../../core/services/common/notification.service';
|
||||
import * as XLSX from 'xlsx';
|
||||
import { CustomPaginator } from '../../shared/custom-paginator';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatPaginatorIntl, MatPaginator } from '@angular/material/paginator';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { ConfirmDialogComponent } from '../../shared/components/confirm-dialog/confirm-dialog.component';
|
||||
import { UserPreferences } from '../../core/models/user-preference';
|
||||
import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service';
|
||||
import { Country } from '../../core/models/country';
|
||||
import { UnitOfMeasure } from '../../core/models/unitofmeasure';
|
||||
import { CommonService } from '../../core/services/common/common.service';
|
||||
import { finalize, Subject, takeUntil } from 'rxjs';
|
||||
import { Goods, GoodsItem } from '../../core/models/carnet/goods';
|
||||
|
||||
@Component({
|
||||
selector: 'app-goods',
|
||||
imports: [AngularMaterialModule, CommonModule, ReactiveFormsModule],
|
||||
templateUrl: './goods.component.html',
|
||||
styleUrl: './goods.component.scss',
|
||||
providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }],
|
||||
})
|
||||
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;
|
||||
|
||||
// Table configuration
|
||||
displayedColumns: string[] = ['itemNumber', 'description', 'pieces', 'weight', 'unitOfMeasure', 'value', 'countryOfOrigin', 'actions'];
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
|
||||
// Form controls
|
||||
goodsForm: FormGroup;
|
||||
itemForm: FormGroup;
|
||||
|
||||
// UI state
|
||||
isEditing = false;
|
||||
currentItem: GoodsItem | null = null;
|
||||
isLoading = false;
|
||||
changeInProgress = false;
|
||||
showItemForm = false;
|
||||
fileToUpload: File | null = null;
|
||||
isProcessing = false;
|
||||
|
||||
// Data
|
||||
countries: Country[] = [];
|
||||
unitsOfMeasures: UnitOfMeasure[] = [];
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
private fb = inject(FormBuilder);
|
||||
private goodsService = inject(GoodsService);
|
||||
private notificationService = inject(NotificationService);
|
||||
private dialog = inject(MatDialog);
|
||||
private errorHandler = inject(ApiErrorHandlerService);
|
||||
private commonService = inject(CommonService);
|
||||
|
||||
constructor() {
|
||||
// Main form for goods section
|
||||
this.goodsForm = this.fb.group({
|
||||
commercialSample: [false],
|
||||
professionalEquipment: [false],
|
||||
exhibitionsFair: [false],
|
||||
roadVehiclesUsed: [false],
|
||||
horseUsed: [false],
|
||||
authorizedRepresentatives: ['', Validators.required]
|
||||
}, { validator: this.atLeastOneRequired }
|
||||
);
|
||||
|
||||
// Form for individual items
|
||||
this.itemForm = this.fb.group({
|
||||
itemNumber: ['', Validators.required],
|
||||
description: ['', Validators.required],
|
||||
pieces: [0, [Validators.required, Validators.min(1)]],
|
||||
weight: [0, [Validators.required, Validators.min(1), Validators.pattern(/^\d+(\.\d{1,4})?$/)]],
|
||||
unitOfMeasure: ['', Validators.required],
|
||||
value: [0, [Validators.required, Validators.min(1), Validators.pattern(/^\d+(\.\d{1,2})?$/)]],
|
||||
countryOfOrigin: ['US', Validators.required]
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.dataSource.paginator = this.paginator;
|
||||
this.dataSource.sort = this.sort;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadCountries();
|
||||
this.loadUnitOfMeasures();
|
||||
|
||||
if (this.headerid > 0) {
|
||||
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.dataSource.data.length > 0);
|
||||
}
|
||||
},
|
||||
error: (error: any) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load good details');
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error loading good details:', error);
|
||||
}
|
||||
});
|
||||
|
||||
this.loadGoodsItems();
|
||||
}
|
||||
}
|
||||
|
||||
loadGoodsItems(): void {
|
||||
this.isLoading = true;
|
||||
|
||||
this.goodsService.getGoodsItemsByHeaderId(this.headerid).pipe(finalize(() => {
|
||||
this.isLoading = false;
|
||||
})).subscribe({
|
||||
next: (items: GoodsItem[]) => {
|
||||
this.dataSource.data = items;
|
||||
this.completed.emit(this.goodsForm.valid && this.dataSource.data.length > 0);
|
||||
},
|
||||
error: (error: any) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load goods items');
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error loading goods items:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadCountries(): void {
|
||||
this.commonService.getCountries()
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: (countries) => {
|
||||
this.countries = countries;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Failed to load countries', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadUnitOfMeasures(): void {
|
||||
this.commonService.getUnitOfMeasures()
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: (units) => {
|
||||
this.unitsOfMeasures = units;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Failed to load unit of measures', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
patchFormData(data: Goods): void {
|
||||
this.goodsForm.patchValue({
|
||||
commercialSample: data.commercialSample || false,
|
||||
professionalEquipment: data.professionalEquipment || false,
|
||||
exhibitionsFair: data.exhibitionsFair || false,
|
||||
roadVehiclesUsed: data.roadVehiclesUsed || false,
|
||||
horseUsed: data.horseUsed || false,
|
||||
authorizedRepresentatives: data.authorizedRepresentatives || ''
|
||||
});
|
||||
}
|
||||
|
||||
// Add new item to the table
|
||||
addNewItem(): void {
|
||||
this.showItemForm = true;
|
||||
this.isEditing = false;
|
||||
this.currentItem = null;
|
||||
this.itemForm.reset({
|
||||
pieces: 0,
|
||||
weight: 0,
|
||||
value: 0,
|
||||
countryOfOrigin: 'US',
|
||||
});
|
||||
this.itemForm.markAsUntouched();
|
||||
}
|
||||
|
||||
// Edit existing item
|
||||
editItem(item: GoodsItem): void {
|
||||
this.showItemForm = true;
|
||||
this.isEditing = true;
|
||||
this.currentItem = item;
|
||||
this.itemForm.patchValue({
|
||||
itemNumber: item.itemNumber,
|
||||
description: item.description,
|
||||
pieces: item.pieces,
|
||||
weight: item.weight,
|
||||
unitOfMeasure: item.unitOfMeasure || 'kg',
|
||||
value: item.value,
|
||||
countryOfOrigin: item.countryOfOrigin || ''
|
||||
});
|
||||
}
|
||||
|
||||
saveItem(): void {
|
||||
if (this.itemForm.invalid || this.goodsForm.invalid) {
|
||||
this.itemForm.markAllAsTouched();
|
||||
this.goodsForm.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
|
||||
this.onSubmit();
|
||||
|
||||
const itemData: GoodsItem = this.itemForm.value;
|
||||
|
||||
// create a goods items array with a single item
|
||||
let itemDataArray: GoodsItem[] = [];
|
||||
itemDataArray.push(itemData);
|
||||
|
||||
this.changeInProgress = true;
|
||||
|
||||
const saveObservable = this.isEditing && this.currentItem
|
||||
? this.goodsService.updateGoodsItem(this.headerid, itemDataArray)
|
||||
: this.goodsService.addGoodsItem(this.headerid, itemDataArray);
|
||||
|
||||
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`);
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error saving goods item:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Delete item with confirmation
|
||||
deleteItem(item: any): void {
|
||||
this.currentItem = item;
|
||||
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
|
||||
width: '350px',
|
||||
data: {
|
||||
title: 'Confirm Delete',
|
||||
message: 'Are you sure you want to delete this item?',
|
||||
confirmText: 'Delete',
|
||||
cancelText: 'Cancel'
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.goodsService.deleteGoodsItem(this.headerid, this.currentItem!).subscribe({
|
||||
next: () => {
|
||||
this.notificationService.showSuccess('Item deleted successfully');
|
||||
this.loadGoodsItems();
|
||||
this.cancelEdit();
|
||||
},
|
||||
error: (error) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to delete item');
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error deleting item:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle file selection for upload
|
||||
fileUpload(event: any): void {
|
||||
const file: File = event.target.files[0];
|
||||
|
||||
this.processExcelFile(file)
|
||||
// .then(response => {
|
||||
// // console.log('File uploaded successfully:', response);
|
||||
// // Handle success (e.g., update UI)
|
||||
// })
|
||||
.catch(error => {
|
||||
console.error('Error processing file:', error);
|
||||
this.notificationService.showError('Failed to upload file');
|
||||
});
|
||||
}
|
||||
|
||||
async processExcelFile(file: File): Promise<void> {
|
||||
|
||||
if (file) {
|
||||
const validTypes = [
|
||||
'application/vnd.ms-excel',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'text/csv'
|
||||
];
|
||||
|
||||
if (!validTypes.includes(file.type)) {
|
||||
this.notificationService.showError('Please upload only Excel (XLSX, XLS) or CSV files');
|
||||
return;
|
||||
}
|
||||
|
||||
this.fileToUpload = file;
|
||||
if (!this.fileToUpload || !this.headerid) return;
|
||||
|
||||
this.isProcessing = true;
|
||||
|
||||
try {
|
||||
const data = await this.readExcelFile(this.fileToUpload);
|
||||
const items = this.parseExcelData(data);
|
||||
|
||||
if (items.length === 0) {
|
||||
this.notificationService.showError('No valid items found in the file');
|
||||
this.isProcessing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// items.map(item => {
|
||||
// item.countryOfOrigin = this.getCountryValue(item.countryOfOrigin);
|
||||
// item.unitOfMeasure = this.getUnitOfMeasureValue(item.unitOfMeasure);
|
||||
// });
|
||||
|
||||
this.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) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, `Failed to save the upload goods items`);
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error saving goods items:', error);
|
||||
this.fileToUpload = null;
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(error instanceof Error ? error.message : 'Failed to process file');
|
||||
} finally {
|
||||
this.isProcessing = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Read Excel file
|
||||
private readExcelFile(file: File): Promise<any[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (e: any) => {
|
||||
try {
|
||||
const data = new Uint8Array(e.target.result);
|
||||
const workbook = XLSX.read(data, { type: 'array' });
|
||||
const firstSheetName = workbook.SheetNames[0];
|
||||
const worksheet = workbook.Sheets[firstSheetName];
|
||||
const jsonData = XLSX.utils.sheet_to_json(worksheet);
|
||||
resolve(jsonData);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
reader.onerror = (error) => reject(error);
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
}
|
||||
|
||||
// Parse Excel data into our format
|
||||
private parseExcelData(data: any[]): any[] {
|
||||
return data.map(row => ({
|
||||
itemNumber: row['Item Number'] || row['itemNumber'] || '',
|
||||
description: row['Description'] || row['description'] || '',
|
||||
pieces: Number(row['Pieces'] || row['pieces'] || 0),
|
||||
weight: Number(row['Weight'] || row['weight'] || 0),
|
||||
unitOfMeasure: row['Unit of Measure'] || row['unit of measure'] || row['unitofmeasure'] || row['UnitOfMeasure'],
|
||||
value: Number(row['Value'] || row['value'] || 0),
|
||||
countryOfOrigin: row['Country Of Origin'] || row['Country of Origin'] || row['country of origin'] || row['countryoforigin'] || row['CountryOfOrigin'] || ''
|
||||
})).filter(item =>
|
||||
item.itemNumber &&
|
||||
item.description &&
|
||||
item.pieces > 0
|
||||
);
|
||||
}
|
||||
|
||||
// Save all goods data
|
||||
onSubmit(): void {
|
||||
if (this.goodsForm.invalid) {
|
||||
this.goodsForm.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = this.goodsForm.value;
|
||||
this.changeInProgress = true;
|
||||
|
||||
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);
|
||||
},
|
||||
error: (error) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to save goods information');
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error saving goods:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Cancel item editing
|
||||
cancelEdit(): void {
|
||||
this.showItemForm = false;
|
||||
this.isEditing = false;
|
||||
this.currentItem = null;
|
||||
this.itemForm.reset();
|
||||
}
|
||||
|
||||
private atLeastOneRequired(formGroup: FormGroup) {
|
||||
return (formGroup.get('commercialSample')?.value ||
|
||||
formGroup.get('professionalEquipment')?.value ||
|
||||
formGroup.get('exhibitionsFair')?.value) ? null : { atLeastOneRequired: true };
|
||||
}
|
||||
|
||||
getUnitOfMeasureLabel(value: string): string {
|
||||
const unit = this.unitsOfMeasures.find(u => u.value === value);
|
||||
return unit ? unit.name : value;
|
||||
}
|
||||
|
||||
getCountryLabel(value: string): string {
|
||||
const country = this.countries.find(c => c.value === value);
|
||||
return country ? country.name : value;
|
||||
}
|
||||
|
||||
getUnitOfMeasureValue(name: string): string {
|
||||
const unit = this.unitsOfMeasures.find(u => u.name === name);
|
||||
return unit ? unit.value : name;
|
||||
}
|
||||
|
||||
getCountryValue(name: string): string {
|
||||
const country = this.countries.find(c => c.name === name);
|
||||
return country ? country.value : name;
|
||||
}
|
||||
|
||||
formatDecimalDisplay(value: number, decimalPoints: number): string {
|
||||
if (value === null || value === undefined) return '';
|
||||
|
||||
// Format to show up to n decimal places, removing trailing zeros
|
||||
const parts = value.toString().split('.');
|
||||
if (parts.length === 1) return parts[0]; // No decimals
|
||||
return `${parts[0]}.${parts[1].substring(0, decimalPoints).replace(/0+$/, '')}`;
|
||||
}
|
||||
}
|
||||
2
src/app/carnet/holder/holder.component.html
Normal file
2
src/app/carnet/holder/holder.component.html
Normal file
@ -0,0 +1,2 @@
|
||||
<app-holder-search (holderSelectionCompleted)="onHolderSelectionSaved($event)" [applicationName]="applicationName"
|
||||
[headerid]="headerid" [selectedHolderId]="selectedHolderId" [isViewMode]="isViewMode"></app-holder-search>
|
||||
0
src/app/carnet/holder/holder.component.scss
Normal file
0
src/app/carnet/holder/holder.component.scss
Normal file
52
src/app/carnet/holder/holder.component.ts
Normal file
52
src/app/carnet/holder/holder.component.ts
Normal file
@ -0,0 +1,52 @@
|
||||
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: [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 = 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onHolderSelectionSaved(completed: boolean): void {
|
||||
this.completed.emit(completed);
|
||||
this.updated.emit(true); // to update dependent data
|
||||
}
|
||||
}
|
||||
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(', ') || '';
|
||||
}
|
||||
}
|
||||
366
src/app/carnet/shipping/shipping.component.html
Normal file
366
src/app/carnet/shipping/shipping.component.html
Normal file
@ -0,0 +1,366 @@
|
||||
<div class="shipping-container">
|
||||
<div class="loading-shade" *ngIf="isLoading">
|
||||
<mat-spinner diameter="50"></mat-spinner>
|
||||
</div>
|
||||
|
||||
<form [formGroup]="shippingForm" (ngSubmit)="onSubmit()">
|
||||
<!-- Insurance Section -->
|
||||
<div class="section">
|
||||
<h3>Insurance & Bond</h3>
|
||||
<div class="checkbox-group">
|
||||
<!-- <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 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" required>
|
||||
<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>
|
||||
|
||||
<!-- Shipping Section -->
|
||||
<div class="section">
|
||||
<h3>Ship To</h3>
|
||||
<mat-radio-group formControlName="shipTo" class="radio-group" (change)="onShipToChange($event)">
|
||||
<mat-radio-button value="PREPARER">Preparer</mat-radio-button>
|
||||
<mat-radio-button value="HOLDER">Holder</mat-radio-button>
|
||||
<mat-radio-button value="3RDPARTY">3rd Party</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
|
||||
<mat-card appearance="outlined" *ngIf="!showAddressForm && !showContactForm">
|
||||
<mat-card-content>
|
||||
<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>
|
||||
<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">
|
||||
<mat-label>Address Line 1</mat-label>
|
||||
<input matInput formControlName="address1" required>
|
||||
<mat-error *ngIf="shippingForm.get('address.address1')?.errors?.['required']">
|
||||
Address is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="shippingForm.get('address.address1')?.errors?.['maxlength']">
|
||||
Maximum 100 characters allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline" class="address2">
|
||||
<mat-label>Address Line 2 (Optional)</mat-label>
|
||||
<input matInput formControlName="address2">
|
||||
<mat-error *ngIf="shippingForm.get('address.address2')?.errors?.['maxlength']">
|
||||
Maximum 100 characters allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Location Information -->
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline" class="city">
|
||||
<mat-label>City</mat-label>
|
||||
<input matInput formControlName="city" required>
|
||||
<mat-error *ngIf="shippingForm.get('address.city')?.errors?.['required']">
|
||||
City is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="shippingForm.get('address.city')?.errors?.['maxlength']">
|
||||
Maximum 50 characters allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="country">
|
||||
<mat-label>Country</mat-label>
|
||||
<mat-select formControlName="country" required
|
||||
(selectionChange)="onCountryChange($event.value)">
|
||||
<mat-option *ngFor="let country of countries" [value]="country.value">
|
||||
{{ country.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="shippingForm.get('address.country')?.errors?.['required']">
|
||||
Country is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="state">
|
||||
<mat-label>State/Province</mat-label>
|
||||
<mat-select formControlName="state" required>
|
||||
<mat-option *ngFor="let state of states" [value]="state.value">
|
||||
{{ state.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="shippingForm.get('address.state')?.errors?.['required']">
|
||||
State is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="zip">
|
||||
<mat-label>ZIP/Postal Code</mat-label>
|
||||
<input matInput formControlName="zip" required>
|
||||
<mat-error *ngIf="shippingForm.get('address.zip')?.errors?.['required']">
|
||||
ZIP/Postal code is required
|
||||
</mat-error>
|
||||
<mat-error
|
||||
*ngIf="shippingForm.get('address.country')?.value === 'US' && shippingForm.get('address.zip')?.touched && shippingForm.get('address.zip')?.errors?.['invalidUSZip']">
|
||||
Please enter a valid 5-digit US ZIP code
|
||||
</mat-error>
|
||||
<mat-error
|
||||
*ngIf="shippingForm.get('address.country')?.value === 'CA' && shippingForm.get('address.zip')?.touched && shippingForm.get('address.zip')?.errors?.['invalidCanadaPostal']">
|
||||
Please enter a valid postal code (e.g., A1B2C3)
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-actions" *ngIf="showAddressForm && shippingForm.get('shipTo')?.value !== '3RDPARTY'">
|
||||
<button mat-button type="button" (click)="cancelEditAddressForm()">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="showContactForm" class="contact-form" formGroupName="contact">
|
||||
<h4>Contact Information</h4>
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>First Name</mat-label>
|
||||
<input matInput formControlName="firstName" required>
|
||||
<mat-icon matSuffix>person</mat-icon>
|
||||
<mat-error *ngIf="shippingForm.get('contact.firstName')?.errors?.['required']">
|
||||
First name is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="shippingForm.get('contact.firstName')?.errors?.['maxlength']">
|
||||
Maximum 50 characters allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="small-field">
|
||||
<mat-label>Middle Initial</mat-label>
|
||||
<input matInput formControlName="middleInitial" maxlength="1">
|
||||
<mat-error *ngIf="shippingForm.get('contact.middleInitial')?.errors?.['maxlength']">
|
||||
Only 1 character allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Last Name</mat-label>
|
||||
<input matInput formControlName="lastName" required>
|
||||
<mat-icon matSuffix>person</mat-icon>
|
||||
<mat-error *ngIf="shippingForm.get('contact.lastName')?.errors?.['required']">
|
||||
Last name is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="shippingForm.get('contact.lastName')?.errors?.['maxlength']">
|
||||
Maximum 50 characters allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Title</mat-label>
|
||||
<input matInput formControlName="title" required>
|
||||
<mat-icon matSuffix>work</mat-icon>
|
||||
<mat-error *ngIf="shippingForm.get('contact.title')?.errors?.['required']">
|
||||
Title is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="shippingForm.get('contact.title')?.errors?.['maxlength']">
|
||||
Maximum 100 characters allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Phone</mat-label>
|
||||
<input matInput formControlName="phone" required>
|
||||
<mat-icon matSuffix>phone</mat-icon>
|
||||
<mat-error *ngIf="shippingForm.get('contact.phone')?.errors?.['required']">
|
||||
Phone is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="shippingForm.get('contact.phone')?.errors?.['pattern']">
|
||||
Please enter a valid phone number (10-15 digits)
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Mobile</mat-label>
|
||||
<input matInput formControlName="mobile">
|
||||
<mat-icon matSuffix>smartphone</mat-icon>
|
||||
<mat-error *ngIf="shippingForm.get('contact.mobile')?.errors?.['required']">
|
||||
Mobile is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="shippingForm.get('contact.mobile')?.errors?.['pattern']">
|
||||
Please enter a valid mobile number (10-15 digits)
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Fax</mat-label>
|
||||
<input matInput formControlName="fax">
|
||||
<mat-icon matSuffix>fax</mat-icon>
|
||||
<mat-error *ngIf="shippingForm.get('contact.fax')?.errors?.['pattern']">
|
||||
Please enter a valid fax number (10-15 digits)
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Email</mat-label>
|
||||
<input matInput formControlName="email" required>
|
||||
<mat-icon matSuffix>email</mat-icon>
|
||||
<mat-error *ngIf="shippingForm.get('contact.email')?.errors?.['required']">
|
||||
Email is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="shippingForm.get('contact.email')?.errors?.['email']">
|
||||
Please enter a valid email address
|
||||
</mat-error>
|
||||
<mat-error *ngIf="shippingForm.get('contact.email')?.errors?.['maxlength']">
|
||||
Maximum 100 characters allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>Reference Number</mat-label>
|
||||
<input matInput formControlName="refNumber">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>Notes</mat-label>
|
||||
<textarea matInput formControlName="notes" rows="2"></textarea>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div 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</h3>
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Delivery Type</mat-label>
|
||||
<mat-select formControlName="deliveryType" required (change)="onDeliveryTypeChange()">
|
||||
<mat-option *ngFor="let deliveryType of deliveryTypes" [value]="deliveryType.value">
|
||||
{{ deliveryType.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="shippingForm.get('deliveryType')?.errors?.['required']">
|
||||
Delivery Type is required
|
||||
</mat-error>
|
||||
<mat-hint align="start" *ngIf="deliveryEstimate" class="delivery-estimate">
|
||||
<span>{{ deliveryEstimate }}</span>
|
||||
</mat-hint>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Delivery Method</mat-label>
|
||||
<mat-select formControlName="deliveryMethod" required
|
||||
(selectionChange)="onDeliveryMethodChange($event.value)">
|
||||
<mat-option *ngFor="let deliveryMethod of deliveryMethods" [value]="deliveryMethod.value">
|
||||
{{ deliveryMethod.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="shippingForm.get('deliveryMethod')?.errors?.['required']">
|
||||
Delivery Method is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div *ngIf="shippingForm.get('deliveryMethod')?.value === '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 *ngIf="shippingForm.get('courierAccount')?.errors?.['required']">
|
||||
Required when using customer courier
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Payment Section -->
|
||||
<div class="section">
|
||||
<h3>Payment Method</h3>
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Payment Method</mat-label>
|
||||
<mat-select formControlName="paymentMethod" required>
|
||||
<mat-option *ngFor="let paymentType of paymentTypes" [value]="paymentType.value">
|
||||
{{ paymentType.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="shippingForm.get('paymentMethod')?.errors?.['required']">
|
||||
Payment Method is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<!-- <div class="form-row">
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>Payment Notes</mat-label>
|
||||
<textarea matInput formControlName="notes" rows="2"></textarea>
|
||||
</mat-form-field>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button mat-raised-button color="primary" type="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="submit" *ngIf="!isViewMode"
|
||||
[disabled]="shippingForm.invalid || changeInProgress">
|
||||
Save
|
||||
</button>
|
||||
|
||||
<button mat-raised-button color="primary" type="button" (click)="returnToHome()" *ngIf="isViewMode">
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
105
src/app/carnet/shipping/shipping.component.scss
Normal file
105
src/app/carnet/shipping/shipping.component.scss
Normal file
@ -0,0 +1,105 @@
|
||||
.shipping-container {
|
||||
padding-top: 0.5rem;
|
||||
|
||||
.section {
|
||||
margin-bottom: 1rem;
|
||||
padding: 16px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
|
||||
h3 {
|
||||
margin: 0 0 0.9rem 0;
|
||||
color: var(--mat-sys-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 16px 0 8px;
|
||||
color: var(--mat-sys-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.mat-mdc-card-content:first-child {
|
||||
padding: 0 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
|
||||
.formofsecurity {
|
||||
margin-left: 1rem;
|
||||
min-width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 0.9rem;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 0.5rem;
|
||||
align-items: start;
|
||||
|
||||
mat-form-field {
|
||||
flex: 1;
|
||||
margin-bottom: 6px;
|
||||
|
||||
&.full-width {
|
||||
flex: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.small-field {
|
||||
max-width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 16px;
|
||||
margin-top: 0.9rem;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.shipping-container {
|
||||
.form-row {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
mat-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
709
src/app/carnet/shipping/shipping.component.ts
Normal file
709
src/app/carnet/shipping/shipping.component.ts
Normal file
@ -0,0 +1,709 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, EventEmitter, inject, Input, Output } from '@angular/core';
|
||||
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { NotificationService } from '../../core/services/common/notification.service';
|
||||
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
|
||||
import { ShippingService } from '../../core/services/carnet/shipping.service';
|
||||
import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service';
|
||||
import { CommonService } from '../../core/services/common/common.service';
|
||||
import { Shipping } from '../../core/models/carnet/shipping';
|
||||
import { Country } from '../../core/models/country';
|
||||
import { State } from '../../core/models/state';
|
||||
import { ZipCodeValidator } from '../../shared/validators/zipcode-validator';
|
||||
import { finalize, forkJoin, map, of, Subject, switchMap, takeUntil, throwError } from 'rxjs';
|
||||
import { DeliveryType } from '../../core/models/delivery-type';
|
||||
import { DeliveryMethod } from '../../core/models/delivery-method';
|
||||
import { PaymentType } from '../../core/models/payment-type';
|
||||
import { format, isAfter, isWeekend, addBusinessDays } from 'date-fns';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ShippingContact } from '../../core/models/carnet/shipping-contact';
|
||||
import { ShippingAddress } from '../../core/models/carnet/shipping-address';
|
||||
import { ContactDialogComponent } from './contact-dialog.component';
|
||||
import { HolderService } from '../../core/services/carnet/holder.service';
|
||||
import { Holder } from '../../core/models/carnet/holder';
|
||||
import { NavigationService } from '../../core/services/common/navigation.service';
|
||||
import { StorageService } from '../../core/services/common/storage.service';
|
||||
import { FormOfSecurity } from '../../core/models/formofsecurity';
|
||||
import { CarnetService } from '../../core/services/carnet/carnet.service';
|
||||
import { environment } from '../../../environments/environment';
|
||||
|
||||
@Component({
|
||||
selector: 'app-shipping',
|
||||
imports: [AngularMaterialModule, CommonModule, ReactiveFormsModule],
|
||||
templateUrl: './shipping.component.html',
|
||||
styleUrl: './shipping.component.scss'
|
||||
})
|
||||
export class ShippingComponent {
|
||||
@Input() headerid: number = 0;
|
||||
@Input() isEditMode = false;
|
||||
@Input() isViewMode = false;
|
||||
@Input() enableSubmitButton = false;
|
||||
@Input() applicationName: string = '';
|
||||
|
||||
@Output() completed = new EventEmitter<boolean>();
|
||||
|
||||
private fb = inject(FormBuilder);
|
||||
private dialog = inject(MatDialog);
|
||||
|
||||
private shippingService = inject(ShippingService);
|
||||
private notificationService = inject(NotificationService);
|
||||
private errorHandler = inject(ApiErrorHandlerService);
|
||||
private commonService = inject(CommonService);
|
||||
private holderService = inject(HolderService);
|
||||
private navigationService = inject(NavigationService);
|
||||
private storageService = inject(StorageService);
|
||||
private carnetService = inject(CarnetService);
|
||||
|
||||
shippingForm: FormGroup;
|
||||
isLoading = false;
|
||||
changeInProgress = false;
|
||||
showAddressForm = false;
|
||||
showContactForm = false;
|
||||
deliveryEstimate: string = '';
|
||||
holderid: number = 0;
|
||||
holder: Holder | undefined | null = null;
|
||||
showSubmitButton: boolean = environment.appType === 'client';
|
||||
showProcessButton: boolean = environment.appType === 'service-provider';
|
||||
|
||||
countriesHasStates = ['US', 'CA', 'MX'];
|
||||
countries: Country[] = [];
|
||||
states: State[] = [];
|
||||
deliveryTypes: DeliveryType[] = [];
|
||||
deliveryMethods: DeliveryMethod[] = [];
|
||||
paymentTypes: PaymentType[] = [];
|
||||
formOfSecurities: FormOfSecurity[] = [];
|
||||
govFormOfSecurities: FormOfSecurity[] = [];
|
||||
nonGovFormOfSecurities: FormOfSecurity[] = [];
|
||||
shippingFromDB: Shipping | null = null;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
preparerAddress: ShippingAddress | null | undefined = null;
|
||||
holderAddress: ShippingAddress | null | undefined = null;
|
||||
preparerContact: ShippingContact | null | undefined = null;
|
||||
preparerContacts: ShippingContact[] = [];
|
||||
holderContact: ShippingContact | null | undefined = null;
|
||||
holderContacts: ShippingContact[] = [];
|
||||
|
||||
constructor() {
|
||||
this.shippingForm = this.fb.group({
|
||||
// needsBond: [false],
|
||||
needsInsurance: [false],
|
||||
needsLostDocProtection: [false],
|
||||
formOfSecurity: ['', Validators.required],
|
||||
shipTo: ['PREPARER', Validators.required],
|
||||
address: this.fb.group({
|
||||
companyName: [''],
|
||||
address1: [''],
|
||||
address2: [''],
|
||||
city: [''],
|
||||
state: [''],
|
||||
country: ['US'],
|
||||
zip: [''],
|
||||
}),
|
||||
contact: this.fb.group({
|
||||
firstName: [''],
|
||||
lastName: [''],
|
||||
middleInitial: [''],
|
||||
title: [''],
|
||||
phone: [''],
|
||||
mobile: [''],
|
||||
fax: [''],
|
||||
email: [''],
|
||||
refNumber: [''],
|
||||
notes: ['']
|
||||
}),
|
||||
deliveryType: ['', Validators.required],
|
||||
deliveryMethod: ['', Validators.required],
|
||||
courierAccount: [''],
|
||||
paymentMethod: ['', Validators.required],
|
||||
paymentNotes: ['']
|
||||
});
|
||||
|
||||
this.shippingForm.get('deliveryType')?.valueChanges.subscribe(() => {
|
||||
this.calculateDeliveryEstimate();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadCountries();
|
||||
this.loadDeliveryTypes();
|
||||
this.loadDeliveryMethods();
|
||||
this.loadPaymentTypes();
|
||||
this.loadFormOfSecurities();
|
||||
|
||||
if (this.headerid && this.headerid > 0) {
|
||||
this.loadShippingData();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
onSubmit(): void {
|
||||
if (this.shippingForm.invalid) {
|
||||
this.shippingForm.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
|
||||
this.changeInProgress = true;
|
||||
const shippingData: Shipping = this.shippingForm.value;
|
||||
|
||||
if (shippingData.shipTo !== '3RDPARTY') {
|
||||
shippingData.address = shippingData.shipTo === 'PREPARER' ? this.preparerAddress ?? undefined : this.holderAddress ?? undefined;
|
||||
shippingData.contact = shippingData.shipTo === 'PREPARER' ? this.preparerContact ?? undefined : this.holderContact ?? undefined;
|
||||
}
|
||||
|
||||
this.shippingService.saveShippingDetails(this.headerid, shippingData).pipe(finalize(() => {
|
||||
this.changeInProgress = false;
|
||||
})).subscribe({
|
||||
next: () => {
|
||||
this.notificationService.showSuccess('Shipping & payments information saved successfully');
|
||||
this.completed.emit(true);
|
||||
},
|
||||
error: (error) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to save shipping and payment information');
|
||||
this.notificationService.showError(errorMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onDeliveryTypeChange(): void {
|
||||
this.calculateDeliveryEstimate();
|
||||
}
|
||||
|
||||
onDeliveryMethodChange(deliveryMethod: string): void {
|
||||
const courierControl = this.shippingForm.get('courierAccount');
|
||||
if (deliveryMethod === 'CLC') {
|
||||
courierControl?.setValidators([Validators.required]);
|
||||
} else {
|
||||
courierControl?.clearValidators();
|
||||
}
|
||||
courierControl?.updateValueAndValidity();
|
||||
}
|
||||
|
||||
onCountryChange(country: string): void {
|
||||
this.shippingForm.get('address.state')?.reset();
|
||||
|
||||
if (country) {
|
||||
this.loadStates(country);
|
||||
}
|
||||
|
||||
this.shippingForm.get('address.zip')?.updateValueAndValidity();
|
||||
}
|
||||
|
||||
public refreshShippingData(): void {
|
||||
if (this.headerid > 0) {
|
||||
this.loadAddressContactData();
|
||||
}
|
||||
}
|
||||
|
||||
editAddressForm(): void {
|
||||
this.showAddressForm = true;
|
||||
let shipTo = this.shippingForm.get('shipTo')?.value;
|
||||
|
||||
if (shipTo === 'PREPARER' && this.preparerAddress) {
|
||||
this.shippingForm.get('address')?.patchValue(this.preparerAddress);
|
||||
this.loadStates(this.preparerAddress.country);
|
||||
} else if (shipTo === 'HOLDER' && this.holderAddress) {
|
||||
this.shippingForm.get('address')?.patchValue(this.holderAddress);
|
||||
this.loadStates(this.holderAddress.country);
|
||||
}
|
||||
}
|
||||
|
||||
cancelEditAddressForm(): void {
|
||||
this.showAddressForm = false;
|
||||
this.shippingForm.get('address')?.reset({ country: 'US' });
|
||||
this.loadStates('US');
|
||||
}
|
||||
|
||||
onShipToChange(event: any): void {
|
||||
const shipTo = event.value;
|
||||
this.showAddressForm = false;
|
||||
this.showContactForm = false;
|
||||
|
||||
this.updateAddressContactValidation(shipTo);
|
||||
|
||||
if (shipTo === '3RDPARTY') {
|
||||
this.shippingForm.get('contact')?.reset();
|
||||
this.shippingForm.get('address')?.reset({ country: 'US' });
|
||||
this.loadStates('US');
|
||||
this.showAddressForm = true;
|
||||
this.showContactForm = true;
|
||||
}
|
||||
}
|
||||
|
||||
loadCountries(): void {
|
||||
this.commonService.getCountries()
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: (countries) => {
|
||||
this.countries = countries;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Failed to load countries', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadDeliveryTypes(): void {
|
||||
this.commonService.getDeliveryTypes()
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: (deliveryTypes) => {
|
||||
this.deliveryTypes = deliveryTypes;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Failed to load delivery types', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadDeliveryMethods(): void {
|
||||
this.commonService.getDeliveryMethods()
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: (deliveryMethods) => {
|
||||
this.deliveryMethods = deliveryMethods;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Failed to load delivery methods', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadPaymentTypes(): void {
|
||||
this.commonService.getPaymentTypes()
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: (paymentTypes) => {
|
||||
this.paymentTypes = paymentTypes;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Failed to load payment types', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadStates(country: string): void {
|
||||
this.isLoading = true;
|
||||
country = this.countriesHasStates.includes(country) ? country : 'FN';
|
||||
this.commonService.getStates(country)
|
||||
.pipe(takeUntil(this.destroy$),
|
||||
finalize(() => {
|
||||
this.isLoading = false;
|
||||
})).subscribe({
|
||||
next: (states) => {
|
||||
this.states = states;
|
||||
const stateControl = this.shippingForm.get('contact.state');
|
||||
if (this.countriesHasStates.includes(country)) {
|
||||
stateControl?.enable();
|
||||
} else {
|
||||
stateControl?.disable();
|
||||
stateControl?.setValue('FN');
|
||||
}
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Failed to load states', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadFormOfSecurities(): void {
|
||||
this.commonService.getFormOfSecurities()
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: (fos) => {
|
||||
this.govFormOfSecurities = fos.filter(f => f.isGov);
|
||||
this.nonGovFormOfSecurities = fos.filter(f => !f.isGov);
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Failed to load form of securities', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadShippingData(): void {
|
||||
this.isLoading = true;
|
||||
this.shippingService.getShippingData(this.headerid).pipe(
|
||||
finalize(() => {
|
||||
this.isLoading = false;
|
||||
})
|
||||
).subscribe({
|
||||
next: (results: Shipping) => {
|
||||
|
||||
if (!results) { // do nothing if empty
|
||||
return;
|
||||
}
|
||||
this.shippingFromDB = results;
|
||||
this.patchShippingData();
|
||||
this.loadAddressContactData();
|
||||
},
|
||||
error: (error: any) => {
|
||||
console.error('Error loading shipping data', error);
|
||||
const errorMessage = this.errorHandler.handleApiError(error, 'Failed to load shipping data');
|
||||
this.notificationService.showError(errorMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadAddressContactData(): void {
|
||||
this.isLoading = true;
|
||||
|
||||
this.holderService.getHolder(this.headerid).pipe(
|
||||
switchMap((holder: Holder) => {
|
||||
if (!holder?.holderid) {
|
||||
return of(undefined);
|
||||
}
|
||||
|
||||
this.holder = holder;
|
||||
this.formOfSecurities = holder.holderType?.trim() === 'GOV' ? this.govFormOfSecurities : this.nonGovFormOfSecurities;
|
||||
|
||||
return forkJoin({
|
||||
holderContacts: this.shippingService.getHolderContactsById(holder.holderid),
|
||||
holderAddress: this.shippingService.getHolderAddress(holder),
|
||||
preparerAddress: this.shippingService.getPreparerAddress(),
|
||||
preparerContacts: this.shippingService.getPreparerContacts()
|
||||
}).pipe(
|
||||
// Combine the forkJoin results with the original holder object
|
||||
map((apiResults: any) => ({ ...apiResults, holder }))
|
||||
);
|
||||
}),
|
||||
finalize(() => this.isLoading = false)
|
||||
).subscribe({
|
||||
next: (results) => {
|
||||
|
||||
if (!results && !this.shippingFromDB) { // do nothing if empty
|
||||
return;
|
||||
}
|
||||
|
||||
this.holderid = results.holder.holderid;
|
||||
this.holderContacts = results.holderContacts;
|
||||
this.holderAddress = results.holderAddress;
|
||||
this.preparerContacts = results.preparerContacts;
|
||||
this.preparerAddress = results.preparerAddress;
|
||||
|
||||
// Set holder contact
|
||||
if (this.shippingFromDB?.shipTo === 'HOLDER' && this.shippingFromDB?.contact?.contactid) {
|
||||
this.holderContact = this.holderContacts.find(hc => hc.contactid === this.shippingFromDB?.contact?.contactid);
|
||||
} else {
|
||||
this.holderContact = this.holderContacts?.[0];
|
||||
}
|
||||
|
||||
// Set preparer contact
|
||||
if (this.shippingFromDB?.shipTo === 'PREPARER' && this.shippingFromDB?.contact?.contactid) {
|
||||
this.preparerContact = this.preparerContacts.find(pc => pc.contactid === this.shippingFromDB?.contact?.contactid);
|
||||
} else {
|
||||
this.preparerContact = this.preparerContacts.find(pc => pc.defaultContact);
|
||||
}
|
||||
this.patchAddressContactData();
|
||||
},
|
||||
error: (error: any) => {
|
||||
console.error('Error loading shipping data', error);
|
||||
const errorMessage = this.errorHandler.handleApiError(error, 'Failed to load shipping data');
|
||||
this.notificationService.showError(errorMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
patchShippingData(): void {
|
||||
if (!this.shippingFromDB) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.shippingForm.patchValue({
|
||||
// needsBond: shipping.needsBond,
|
||||
needsInsurance: this.shippingFromDB.needsInsurance,
|
||||
needsLostDocProtection: this.shippingFromDB.needsLostDocProtection,
|
||||
formOfSecurity: this.shippingFromDB.formOfSecurity,
|
||||
shipTo: this.shippingFromDB.shipTo,
|
||||
deliveryType: this.shippingFromDB.deliveryType,
|
||||
deliveryMethod: this.shippingFromDB.deliveryMethod,
|
||||
courierAccount: this.shippingFromDB.courierAccount,
|
||||
paymentMethod: this.shippingFromDB.paymentMethod
|
||||
});
|
||||
|
||||
if (this.shippingFromDB.deliveryMethod === 'CLC') {
|
||||
this.onDeliveryMethodChange(this.shippingFromDB.deliveryMethod);
|
||||
}
|
||||
|
||||
this.calculateDeliveryEstimate();
|
||||
this.shippingForm.markAsUntouched();
|
||||
this.completed.emit(this.shippingForm.valid);
|
||||
}
|
||||
|
||||
patchAddressContactData(): void {
|
||||
|
||||
if (!this.shippingFromDB) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.shippingFromDB.address?.country) {
|
||||
this.loadStates(this.shippingFromDB.address?.country);
|
||||
}
|
||||
|
||||
let formOfSecurityControl = this.shippingForm.get('formOfSecurity');
|
||||
if (this.holder?.holderType.trim() === 'GOV') {
|
||||
formOfSecurityControl?.setValue(this.govFormOfSecurities?.[0].value);
|
||||
formOfSecurityControl?.disable();
|
||||
} else {
|
||||
formOfSecurityControl?.enable();
|
||||
}
|
||||
|
||||
this.calculateDeliveryEstimate();
|
||||
this.updateAddressContactValidation(this.shippingFromDB.shipTo);
|
||||
|
||||
if (this.shippingFromDB.shipTo === '3RDPARTY') {
|
||||
this.showAddressForm = true;
|
||||
this.showContactForm = true;
|
||||
|
||||
const addressGroup = this.shippingForm.get('address') as FormGroup;
|
||||
const contactGroup = this.shippingForm.get('contact') as FormGroup;
|
||||
|
||||
addressGroup.patchValue({
|
||||
addressid: this.shippingFromDB.address?.addressid,
|
||||
companyName: this.shippingFromDB.address?.companyName,
|
||||
address1: this.shippingFromDB.address?.address1,
|
||||
address2: this.shippingFromDB.address?.address2,
|
||||
city: this.shippingFromDB.address?.city,
|
||||
state: this.shippingFromDB.address?.state,
|
||||
zip: this.shippingFromDB.address?.zip,
|
||||
country: this.shippingFromDB.address?.country
|
||||
})
|
||||
|
||||
contactGroup.patchValue({
|
||||
contactid: this.shippingFromDB.contact?.contactid,
|
||||
firstName: this.shippingFromDB.contact?.firstName,
|
||||
lastName: this.shippingFromDB.contact?.lastName,
|
||||
middleInitial: this.shippingFromDB.contact?.middleInitial,
|
||||
title: this.shippingFromDB.contact?.title,
|
||||
phone: this.shippingFromDB.contact?.phone,
|
||||
mobile: this.shippingFromDB.contact?.mobile,
|
||||
fax: this.shippingFromDB.contact?.fax,
|
||||
email: this.shippingFromDB.contact?.email,
|
||||
refNumber: this.shippingFromDB.contact?.refNumber,
|
||||
notes: this.shippingFromDB.contact?.notes,
|
||||
})
|
||||
}
|
||||
this.shippingForm.markAsUntouched();
|
||||
this.completed.emit(this.shippingForm.valid);
|
||||
}
|
||||
|
||||
updateAddressContactValidation(shipTo: string): void {
|
||||
const addressGroup = this.shippingForm.get('address') as FormGroup;
|
||||
const contactGroup = this.shippingForm.get('contact') as FormGroup;
|
||||
|
||||
if (shipTo === '3RDPARTY') {
|
||||
Object.keys(addressGroup.controls).forEach(key => {
|
||||
if (key === 'companyName') {
|
||||
addressGroup.get(key)?.setValidators([Validators.required, Validators.maxLength(100)]);
|
||||
} else if (key === 'address1') {
|
||||
addressGroup.get(key)?.setValidators([Validators.required, Validators.maxLength(100)]);
|
||||
} else if (key === 'address2') {
|
||||
addressGroup.get(key)?.setValidators([Validators.maxLength(100)]);
|
||||
} else if (key === 'city') {
|
||||
addressGroup.get(key)?.setValidators([Validators.required, Validators.maxLength(50)]);
|
||||
} else if (key === 'state') {
|
||||
addressGroup.get(key)?.setValidators(Validators.required);
|
||||
} else if (key === 'country') {
|
||||
addressGroup.get(key)?.setValidators(Validators.required);
|
||||
} else if (key === 'zip') {
|
||||
addressGroup.get(key)?.setValidators([Validators.required, ZipCodeValidator('country')]);
|
||||
}
|
||||
addressGroup.get(key)?.updateValueAndValidity();
|
||||
});
|
||||
Object.keys(contactGroup.controls).forEach(key => {
|
||||
if (key === 'firstName') {
|
||||
contactGroup.get(key)?.setValidators([Validators.required, Validators.maxLength(50)]);
|
||||
} else if (key === 'lastName') {
|
||||
contactGroup.get(key)?.setValidators([Validators.required, Validators.maxLength(50)]);
|
||||
} else if (key === 'middleInitial') {
|
||||
contactGroup.get(key)?.setValidators([Validators.maxLength(1)]);
|
||||
} else if (key === 'title') {
|
||||
contactGroup.get(key)?.setValidators([Validators.required, Validators.maxLength(100)]);
|
||||
} else if (key === 'phone') {
|
||||
contactGroup.get(key)?.setValidators([Validators.required, Validators.pattern(/^[0-9\-\(\)]{10,15}$/)]);
|
||||
} else if (key === 'mobile') {
|
||||
contactGroup.get(key)?.setValidators([Validators.required, Validators.pattern(/^[0-9\-\(\)]{10,15}$/)]);
|
||||
} else if (key === 'fax') {
|
||||
contactGroup.get(key)?.setValidators([Validators.pattern(/^[0-9\-\(\)]{10,15}$/)]);
|
||||
} else if (key === 'email') {
|
||||
contactGroup.get(key)?.setValidators([Validators.required, Validators.email, Validators.maxLength(100)]);
|
||||
}
|
||||
contactGroup.get(key)?.updateValueAndValidity();
|
||||
});
|
||||
} else {
|
||||
Object.keys(addressGroup.controls).forEach(key => {
|
||||
addressGroup.get(key)?.clearValidators();
|
||||
addressGroup.get(key)?.updateValueAndValidity();
|
||||
});
|
||||
Object.keys(contactGroup.controls).forEach(key => {
|
||||
contactGroup.get(key)?.clearValidators();
|
||||
contactGroup.get(key)?.updateValueAndValidity();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getAddressLabel(): string {
|
||||
let shipTo = this.shippingForm.get('shipTo')?.value;
|
||||
|
||||
if (shipTo === 'PREPARER' && this.preparerAddress) {
|
||||
return `${this.preparerAddress.companyName}, ${this.preparerAddress.address1},
|
||||
${this.preparerAddress.city}, ${this.preparerAddress.state}, ${this.preparerAddress.zip},
|
||||
${this.preparerAddress.country}`;
|
||||
}
|
||||
|
||||
if (shipTo === 'HOLDER' && this.holderAddress) {
|
||||
return `${this.holderAddress.companyName}, ${this.holderAddress.address1},
|
||||
${this.holderAddress.city}, ${this.holderAddress.state}, ${this.holderAddress.zip},
|
||||
${this.holderAddress.country}`;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
getContactLabel(): string {
|
||||
const shipTo = this.shippingForm.get('shipTo')?.value;
|
||||
const contact = shipTo === 'PREPARER' ? this.preparerContact :
|
||||
shipTo === 'HOLDER' ? this.holderContact : null;
|
||||
|
||||
if (!contact) {
|
||||
return 'No contact information available';
|
||||
}
|
||||
|
||||
// Build name parts, filtering out null/undefined/empty strings
|
||||
const nameParts = [
|
||||
contact.firstName,
|
||||
contact.middleInitial,
|
||||
contact.lastName
|
||||
].filter(part => part != null && part.trim() !== '');
|
||||
|
||||
// Build contact info parts
|
||||
const contactInfoParts = [
|
||||
contact.email,
|
||||
contact.phone
|
||||
].filter(part => part != null && part.trim() !== '');
|
||||
|
||||
// Combine all non-empty parts
|
||||
const allParts = [
|
||||
nameParts.join(' '), // Join name parts with single spaces
|
||||
...contactInfoParts // Add email and phone if they exist
|
||||
].filter(part => part.trim() !== '');
|
||||
|
||||
return allParts.join(', ') || '';
|
||||
}
|
||||
|
||||
calculateDeliveryEstimate(): void {
|
||||
const deliveryType = this.shippingForm.get('deliveryType')?.value;
|
||||
const deliveryTypeObj = this.deliveryTypes.find(dt => dt.value === deliveryType);
|
||||
const daysToDelivery: number = Number(deliveryTypeObj?.daysToDelivery) !== 0 ? Number(deliveryTypeObj?.daysToDelivery) : 3;
|
||||
const cutOffTime: number = Number(deliveryTypeObj?.cutOffTime) !== 0 ? Number(deliveryTypeObj?.cutOffTime) : 16;
|
||||
|
||||
const now = new Date();
|
||||
const cutoffTime = new Date();
|
||||
cutoffTime.setHours(cutOffTime, 0, 0, 0);
|
||||
|
||||
let deliveryDate: Date;
|
||||
let message = 'Estimated delivery: ';
|
||||
const isAfterCutoff = isAfter(now, cutoffTime);
|
||||
const isWeekendDay = isWeekend(now);
|
||||
|
||||
switch (deliveryType) {
|
||||
case 'SAME':
|
||||
if (isAfterCutoff || isWeekendDay) {
|
||||
deliveryDate = addBusinessDays(now, 1);
|
||||
message += format(deliveryDate, 'EEEE, MMMM do, yyyy');
|
||||
} else {
|
||||
message += format(now, 'EEEE, MMMM do, yyyy');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'STD':
|
||||
// Standard is 3 business days
|
||||
if (isAfterCutoff || isWeekendDay) {
|
||||
// If after cutoff or weekend, add 4 business days
|
||||
deliveryDate = addBusinessDays(now, daysToDelivery);
|
||||
} else {
|
||||
// Before cutoff on weekday, add 3 business days
|
||||
deliveryDate = addBusinessDays(now, daysToDelivery - 1);
|
||||
}
|
||||
|
||||
message += format(deliveryDate, 'EEEE, MMMM do, yyyy');
|
||||
break;
|
||||
|
||||
case 'NBD':
|
||||
// Next business day for pickup
|
||||
if (isAfterCutoff || isWeekendDay) {
|
||||
// If after cutoff or weekend, add 2 business days
|
||||
deliveryDate = addBusinessDays(now, daysToDelivery);
|
||||
} else {
|
||||
// Before cutoff on weekday, add 1 business day
|
||||
deliveryDate = addBusinessDays(now, daysToDelivery);
|
||||
}
|
||||
|
||||
message += format(deliveryDate, 'EEEE, MMMM do, yyyy');
|
||||
break;
|
||||
|
||||
default:
|
||||
message = '';
|
||||
}
|
||||
|
||||
this.deliveryEstimate = message;
|
||||
}
|
||||
|
||||
selectContact(): void {
|
||||
let shipTo = this.shippingForm.get('shipTo')?.value;
|
||||
|
||||
const contacts = shipTo === 'PREPARER' ? this.preparerContacts : this.holderContacts;
|
||||
const currentContact = shipTo === 'PREPARER' ? this.preparerContact : this.holderContact;
|
||||
|
||||
const dialogRef = this.dialog.open(ContactDialogComponent, {
|
||||
width: '500px',
|
||||
data: { contacts: contacts, currentContact: currentContact }
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(selectedItem => {
|
||||
if (selectedItem) {
|
||||
const selectedContact = contacts.find(c => c.contactid === selectedItem.contactid);
|
||||
if (shipTo === 'PREPARER') {
|
||||
this.preparerContact = selectedContact;
|
||||
} else {
|
||||
this.holderContact = selectedContact;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
submitApplication(): void {
|
||||
if (this.headerid) {
|
||||
let currentApplicationDetails = { headerid: this.headerid, applicationName: this.applicationName }
|
||||
this.storageService.set("currentapplication", currentApplicationDetails);
|
||||
}
|
||||
|
||||
this.navigationService.navigate(["terms"])
|
||||
}
|
||||
|
||||
returnToHome(): void {
|
||||
this.navigationService.navigate(["home"])
|
||||
}
|
||||
|
||||
processApplication(): void {
|
||||
this.changeInProgress = true;
|
||||
this.carnetService.processApplication(this.headerid).pipe(finalize(() => {
|
||||
this.changeInProgress = false;
|
||||
})).subscribe({
|
||||
next: () => {
|
||||
this.notificationService.showSuccess('Application processed successfully');
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
112
src/app/carnet/terms-conditions/terms-conditions.component.html
Normal file
112
src/app/carnet/terms-conditions/terms-conditions.component.html
Normal file
@ -0,0 +1,112 @@
|
||||
<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">
|
||||
<!-- Comprehensive Carnet Terms Content -->
|
||||
<h3>1. Definitions</h3>
|
||||
<p><strong>ATA Carnet:</strong> An international customs document that permits the tax-free and duty-free
|
||||
temporary export and import of goods for up to one year.</p>
|
||||
<p><strong>Holder:</strong> The individual or entity in whose name the carnet is issued and who is
|
||||
responsible for complying with all terms.</p>
|
||||
<p><strong>Goods:</strong> Merchandise, equipment, or products covered by the carnet.</p>
|
||||
|
||||
<h3>2. Carnet Usage</h3>
|
||||
<p>2.1 The carnet may be used for:</p>
|
||||
<ul>
|
||||
<li>Commercial samples</li>
|
||||
<li>Professional equipment</li>
|
||||
<li>Goods for exhibitions and fairs</li>
|
||||
<li>Goods for scientific, educational, or cultural purposes</li>
|
||||
</ul>
|
||||
|
||||
<p>2.2 The carnet cannot be used for:</p>
|
||||
<ul>
|
||||
<li>Consumable or disposable items</li>
|
||||
<li>Goods for processing or repair</li>
|
||||
<li>Goods intended for sale or permanent export</li>
|
||||
<li>Prohibited or restricted items under any applicable laws</li>
|
||||
</ul>
|
||||
|
||||
<h3>3. Holder Responsibilities</h3>
|
||||
<p>3.1 The holder must:</p>
|
||||
<ul>
|
||||
<li>Present the carnet to customs when crossing borders</li>
|
||||
<li>Ensure all goods listed in the carnet are returned by the expiration date</li>
|
||||
<li>Pay all applicable duties and taxes if goods are not re-exported</li>
|
||||
<li>Notify the issuing association immediately of any lost or stolen carnets</li>
|
||||
</ul>
|
||||
|
||||
<h3>4. Validity Period</h3>
|
||||
<p>4.1 The carnet is valid for one year from the date of issue.</p>
|
||||
<p>4.2 Goods must be re-exported before the carnet expires.</p>
|
||||
<p>4.3 Extensions may be granted in exceptional circumstances with approval from all relevant customs
|
||||
authorities.</p>
|
||||
|
||||
<h3>5. Customs Procedures</h3>
|
||||
<p>5.1 The holder must present the carnet to customs:</p>
|
||||
<ul>
|
||||
<li>When first exporting goods from the home country</li>
|
||||
<li>When entering each foreign country</li>
|
||||
<li>When re-exporting goods from each foreign country</li>
|
||||
<li>When finally re-importing goods to the home country</li>
|
||||
</ul>
|
||||
|
||||
<h3>6. Security Requirements</h3>
|
||||
<p>6.1 The issuing association may require security up to 40% of the total value of goods.</p>
|
||||
<p>6.2 Security will be refunded when the carnet is fully discharged.</p>
|
||||
<p>6.3 The security may be forfeited if terms are violated.</p>
|
||||
|
||||
<h3>7. Liability and Insurance</h3>
|
||||
<p>7.1 The holder is solely responsible for:</p>
|
||||
<ul>
|
||||
<li>All customs duties and taxes if goods are not re-exported</li>
|
||||
<li>Any damage to goods while in transit</li>
|
||||
<li>Compliance with all import/export regulations</li>
|
||||
</ul>
|
||||
<p>7.2 We recommend obtaining comprehensive insurance coverage for all goods.</p>
|
||||
|
||||
<h3>8. Fees and Charges</h3>
|
||||
<p>8.1 The following fees apply:</p>
|
||||
<ul>
|
||||
|
||||
<li *ngIf="estimatedFees.basicFee">Basic fee: {{estimatedFees.basicFee | currency}}</li>
|
||||
<li *ngIf="estimatedFees.counterFoilFee">Counterfoil Fee: {{estimatedFees.counterFoilFee | currency}}
|
||||
</li>
|
||||
<li *ngIf="estimatedFees.continuationSheetFee">Continuation sheet fee:
|
||||
{{estimatedFees.continuationSheetFee | currency}}</li>
|
||||
<li *ngIf="estimatedFees.expeditedFee">Expedited fee: {{estimatedFees.expeditedFee | currency}}</li>
|
||||
<li *ngIf="estimatedFees.shippingFee">Shipping fee: {{estimatedFees.shippingFee | currency}}</li>
|
||||
<li *ngIf="estimatedFees.bondPremium">Bond Premium: {{estimatedFees.bondPremium | currency}}</li>
|
||||
<li *ngIf="estimatedFees.cargoPremium">Cargo Premium: {{estimatedFees.cargoPremium | currency}}</li>
|
||||
<li *ngIf="estimatedFees.ldiPremium">LDI Premium: {{estimatedFees.ldiPremium | currency}}</li>
|
||||
</ul>
|
||||
|
||||
<h3>9. Dispute Resolution</h3>
|
||||
<p>9.1 Any disputes shall be resolved through arbitration in the jurisdiction of the issuing association.
|
||||
</p>
|
||||
<p>9.2 The holder agrees to be bound by the arbitration decision.</p>
|
||||
|
||||
<h3>10. Governing Law</h3>
|
||||
<p>These terms shall be governed by the laws of the country where the carnet was issued.</p>
|
||||
|
||||
<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>
|
||||
@ -0,0 +1,84 @@
|
||||
.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;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 56px;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,92 @@
|
||||
|
||||
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';
|
||||
|
||||
@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 {
|
||||
|
||||
currentDate = new Date();
|
||||
hasReadAllTerms = false;
|
||||
currentApplicationDetails: { headerid: number, applicationName: string } | null = null;
|
||||
changeInProgress: boolean = false;
|
||||
estimatedFees: Fees = {};
|
||||
|
||||
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 }>('currentapplication')
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.currentApplicationDetails?.headerid) {
|
||||
this.carnetService.getEstimatedFees(this.currentApplicationDetails?.headerid).subscribe({
|
||||
next: (data: Fees) => {
|
||||
if (data) {
|
||||
this.estimatedFees = data;
|
||||
}
|
||||
},
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onAccept(): void {
|
||||
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');
|
||||
this.navigationService.navigate(["edit-carnet", this.currentApplicationDetails?.headerid],
|
||||
{
|
||||
state: { isEditMode: true },
|
||||
queryParams: {
|
||||
applicationname: this.currentApplicationDetails?.applicationName,
|
||||
return: 'shipping'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
151
src/app/carnet/travel-plan/travel-plan.component.html
Normal file
151
src/app/carnet/travel-plan/travel-plan.component.html
Normal file
@ -0,0 +1,151 @@
|
||||
<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>
|
||||
|
||||
<button mat-raised-button color="primary" type="submit" *ngIf="!isViewMode" [disabled]="changeInProgress">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
143
src/app/carnet/travel-plan/travel-plan.component.scss
Normal file
143
src/app/carnet/travel-plan/travel-plan.component.scss
Normal file
@ -0,0 +1,143 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.travel-container {
|
||||
.form-row {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
mat-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
317
src/app/carnet/travel-plan/travel-plan.component.ts
Normal file
317
src/app/carnet/travel-plan/travel-plan.component.ts
Normal file
@ -0,0 +1,317 @@
|
||||
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: [AngularMaterialModule, ReactiveFormsModule, CommonModule, MatListModule],
|
||||
templateUrl: './travel-plan.component.html',
|
||||
styleUrl: './travel-plan.component.scss'
|
||||
})
|
||||
export class TravelPlanComponent {
|
||||
@Input() headerid: number = 0;
|
||||
@Input() isViewMode = false;
|
||||
|
||||
@Output() completed = 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;
|
||||
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 === 'WARNING') {
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
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',
|
||||
cancelText: 'Cancel'
|
||||
}
|
||||
});
|
||||
|
||||
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: 'Ok',
|
||||
cancelText: 'Cancel'
|
||||
}
|
||||
});
|
||||
|
||||
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: 'Ok',
|
||||
cancelText: 'Cancel'
|
||||
}
|
||||
});
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
// Patch form values
|
||||
this.travelForm.patchValue({
|
||||
usaEntries: plan?.usSets,
|
||||
visitsInput: 1,
|
||||
transitsInput: 1 // Reset to default
|
||||
});
|
||||
|
||||
this.completed.emit(this.travelForm.valid);
|
||||
}
|
||||
|
||||
onSubmit(): void {
|
||||
if (this.travelForm.invalid) {
|
||||
this.travelForm.markAllAsTouched();
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
42
src/app/carnet/view/view-carnet.component.html
Normal file
42
src/app/carnet/view/view-carnet.component.html
Normal file
@ -0,0 +1,42 @@
|
||||
<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>
|
||||
<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;
|
||||
}
|
||||
46
src/app/carnet/view/view-carnet.component.ts
Normal file
46
src/app/carnet/view/view-carnet.component.ts
Normal file
@ -0,0 +1,46 @@
|
||||
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';
|
||||
|
||||
@Component({
|
||||
selector: 'app-view-carnet',
|
||||
imports: [AngularMaterialModule, CommonModule, ReactiveFormsModule,
|
||||
ApplicationComponent, HolderComponent, GoodsComponent, TravelPlanComponent, ShippingComponent],
|
||||
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;
|
||||
|
||||
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('headerid');
|
||||
this.headerid = idParam ? parseInt(idParam, 10) : 0;
|
||||
|
||||
this.applicationName = this.route.snapshot.queryParamMap.get('applicationname') || '';
|
||||
}
|
||||
}
|
||||
@ -12,7 +12,11 @@
|
||||
<!-- <button mat-menu-item (click)="navigateTo('holder')">Holder</button>
|
||||
<button mat-menu-item (click)="navigateTo('carnet')">Carnet</button> -->
|
||||
</mat-menu>
|
||||
<button mat-button (click)="navigateTo('table-record')">Configurations</button>
|
||||
<button mat-button [matMenuTriggerFor]="configurations">Configurations</button>
|
||||
<mat-menu #configurations="matMenu">
|
||||
<button mat-menu-item (click)="navigateTo('table-record')">Configurations</button>
|
||||
<button mat-menu-item (click)="navigateTo('country-messages')">Country Messages</button>
|
||||
</mat-menu>
|
||||
|
||||
<div class="profile-container">
|
||||
<button mat-icon-button (click)="toggleProfileMenu()" class="profile-button">
|
||||
|
||||
6
src/app/core/models/carnet/application-detail.ts
Normal file
6
src/app/core/models/carnet/application-detail.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export interface ApplicationDetail {
|
||||
clientid: number;
|
||||
spid: number;
|
||||
applicationId: number;
|
||||
name: string;
|
||||
}
|
||||
10
src/app/core/models/carnet/fee.ts
Normal file
10
src/app/core/models/carnet/fee.ts
Normal file
@ -0,0 +1,10 @@
|
||||
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;
|
||||
}
|
||||
20
src/app/core/models/carnet/goods.ts
Normal file
20
src/app/core/models/carnet/goods.ts
Normal file
@ -0,0 +1,20 @@
|
||||
export interface Goods {
|
||||
commercialSample: boolean;
|
||||
professionalEquipment: boolean;
|
||||
exhibitionsFair: boolean;
|
||||
roadVehiclesUsed: boolean;
|
||||
horseUsed: boolean;
|
||||
authorizedRepresentatives: string;
|
||||
items?: GoodsItem[] | null;
|
||||
}
|
||||
|
||||
export interface GoodsItem {
|
||||
id?: number;
|
||||
itemNumber: string;
|
||||
description: string;
|
||||
pieces: number;
|
||||
weight: number;
|
||||
unitOfMeasure: string;
|
||||
value: number;
|
||||
countryOfOrigin: string;
|
||||
}
|
||||
16
src/app/core/models/carnet/holder.ts
Normal file
16
src/app/core/models/carnet/holder.ts
Normal file
@ -0,0 +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;
|
||||
}
|
||||
10
src/app/core/models/carnet/shipping-address.ts
Normal file
10
src/app/core/models/carnet/shipping-address.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export interface ShippingAddress {
|
||||
companyName: string;
|
||||
addressid: number;
|
||||
address1: string;
|
||||
address2?: string | null;
|
||||
city: string;
|
||||
state: string;
|
||||
country: string;
|
||||
zip: string;
|
||||
}
|
||||
15
src/app/core/models/carnet/shipping-contact.ts
Normal file
15
src/app/core/models/carnet/shipping-contact.ts
Normal file
@ -0,0 +1,15 @@
|
||||
export interface ShippingContact {
|
||||
contactid: number;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
middleInitial?: string | null;
|
||||
title: string;
|
||||
phone: string;
|
||||
mobile: string;
|
||||
fax?: string | null;
|
||||
email: string;
|
||||
refNumber?: string | null;
|
||||
notes?: string | null;
|
||||
defaultContact?: boolean;
|
||||
isInactive?: boolean;
|
||||
}
|
||||
24
src/app/core/models/carnet/shipping.ts
Normal file
24
src/app/core/models/carnet/shipping.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { ShippingAddress } from "./shipping-address";
|
||||
import { ShippingContact } from "./shipping-contact";
|
||||
|
||||
export interface Shipping {
|
||||
// Insurance details
|
||||
// needsBond: boolean;
|
||||
needsInsurance: boolean;
|
||||
needsLostDocProtection: boolean;
|
||||
formOfSecurity: string;
|
||||
|
||||
// Shipping details
|
||||
shipTo: string;
|
||||
address?: ShippingAddress;
|
||||
contact?: ShippingContact;
|
||||
|
||||
// Delivery details
|
||||
deliveryType: string;
|
||||
deliveryMethod: string;
|
||||
courierAccount?: string;
|
||||
|
||||
// Payment details
|
||||
paymentMethod: string;
|
||||
paymentNotes?: string;
|
||||
}
|
||||
17
src/app/core/models/carnet/travel-plan.ts
Normal file
17
src/app/core/models/carnet/travel-plan.ts
Normal file
@ -0,0 +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/delivery-method.ts
Normal file
5
src/app/core/models/delivery-method.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface DeliveryMethod {
|
||||
name: string;
|
||||
id: string;
|
||||
value: string;
|
||||
}
|
||||
@ -2,4 +2,6 @@ export interface DeliveryType {
|
||||
name: string;
|
||||
id: string;
|
||||
value: string;
|
||||
daysToDelivery: string;
|
||||
cutOffTime: 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;
|
||||
}
|
||||
3
src/app/core/models/holder/holder-filter.ts
Normal file
3
src/app/core/models/holder/holder-filter.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export interface HolderFilter {
|
||||
holderName?: string;
|
||||
}
|
||||
5
src/app/core/models/payment-type.ts
Normal file
5
src/app/core/models/payment-type.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface PaymentType {
|
||||
name: string;
|
||||
id: string;
|
||||
value: string;
|
||||
}
|
||||
5
src/app/core/models/unitofmeasure.ts
Normal file
5
src/app/core/models/unitofmeasure.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface UnitOfMeasure {
|
||||
name: string;
|
||||
id: string;
|
||||
value: string;
|
||||
}
|
||||
36
src/app/core/services/carnet/application-detail.service.ts
Normal file
36
src/app/core/services/carnet/application-detail.service.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
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 { Observable } from 'rxjs';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ApplicationDetailService {
|
||||
private apiUrl = environment.apiUrl;
|
||||
private apiDb = environment.apiDb;
|
||||
|
||||
private http = inject(HttpClient);
|
||||
private userService = inject(UserService);
|
||||
|
||||
createApplicationDetails(data: ApplicationDetail): Observable<any> {
|
||||
const userDetails = this.userService.getUserDetails();
|
||||
|
||||
if (!userDetails || !userDetails.userDetails) {
|
||||
throw new Error('User details are not available');
|
||||
}
|
||||
|
||||
const applicationDetails = {
|
||||
P_SPID: userDetails.userDetails.spid,
|
||||
P_CLIENTID: userDetails.userDetails.clientid,
|
||||
P_LOCATIONID: userDetails.userDetails.locationid,
|
||||
P_APPLICATIONNAME: data.name,
|
||||
P_ORDERTYPE: 'ORIGINAL',
|
||||
P_USERID: this.userService.getUser(),
|
||||
}
|
||||
|
||||
return this.http.post(`${this.apiUrl}/${this.apiDb}/CreateApplication`, applicationDetails);
|
||||
}
|
||||
}
|
||||
59
src/app/core/services/carnet/carnet.service.ts
Normal file
59
src/app/core/services/carnet/carnet.service.ts
Normal file
@ -0,0 +1,59 @@
|
||||
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,
|
||||
};
|
||||
|
||||
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.post<any>(`${this.apiUrl}/${this.apiDb}//ProcessCarnet`, appData);
|
||||
}
|
||||
}
|
||||
120
src/app/core/services/carnet/goods.service.ts
Normal file
120
src/app/core/services/carnet/goods.service.ts
Normal file
@ -0,0 +1,120 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { environment } from '../../../../environments/environment';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { UserService } from '../common/user.service';
|
||||
import { Goods, GoodsItem } from '../../models/carnet/goods';
|
||||
import { map, Observable } from 'rxjs';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class GoodsService {
|
||||
private apiUrl = environment.apiUrl;
|
||||
private apiDb = environment.apiDb;
|
||||
|
||||
private http = inject(HttpClient);
|
||||
private userService = inject(UserService);
|
||||
|
||||
getGoodDetailsByHeaderId(headerid: number): Goods | any {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetGoodsDetailstoEdit/${this.userService.getUserSpid()}/${this.userService.getUser()}/${headerid}`).pipe(
|
||||
map(response => this.mapToGoods(response)));
|
||||
}
|
||||
|
||||
private mapToGoods(goodDetails: any): Goods {
|
||||
return {
|
||||
roadVehiclesUsed: goodDetails.AUTOFLAG === 'Y',
|
||||
horseUsed: goodDetails.HORSEFLAG === 'Y',
|
||||
exhibitionsFair: goodDetails.EXHIBITIONSFAIRFLAG === 'Y',
|
||||
professionalEquipment: goodDetails.PROFEQUIPMENTFLAG === 'Y',
|
||||
commercialSample: goodDetails.COMMERCIALSAMPLEFLAG === 'Y',
|
||||
authorizedRepresentatives: goodDetails.AUTHREP
|
||||
};
|
||||
}
|
||||
|
||||
getGoodsItemsByHeaderId(headerid: number): Observable<GoodsItem[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetGoodsItemstoEdit/${this.userService.getUserSpid()}/${this.userService.getUser()}/${headerid}`).pipe(
|
||||
map(response => this.mapToGoodsItems(response)));
|
||||
}
|
||||
|
||||
private mapToGoodsItems(data: any[]): GoodsItem[] {
|
||||
return data.map(data => ({
|
||||
id: data.ID,
|
||||
itemNumber: data.ITEMNO,
|
||||
description: data.GOODSDESCRIPTION,
|
||||
pieces: data.NOOFPIECES,
|
||||
weight: data.ITEMWEIGHT,
|
||||
unitOfMeasure: data.ITEMWEIGHTUOM,
|
||||
value: data.ITEMVALUE,
|
||||
countryOfOrigin: data.GOODSORIGINCOUNTRY
|
||||
}));
|
||||
}
|
||||
|
||||
saveGoodsData(headerid: number, goodsData: Goods): Observable<any> {
|
||||
|
||||
const goods = {
|
||||
P_HEADERID: headerid,
|
||||
P_COMMERCIALSAMPLEFLAG: goodsData.commercialSample ? 'Y' : 'N',
|
||||
P_PROFEQUIPMENTFLAG: goodsData.professionalEquipment ? 'Y' : 'N',
|
||||
P_EXHIBITIONSFAIRFLAG: goodsData.exhibitionsFair ? 'Y' : 'N',
|
||||
P_AUTOFLAG: goodsData.roadVehiclesUsed ? 'Y' : 'N',
|
||||
P_HORSEFLAG: goodsData.horseUsed ? 'Y' : 'N',
|
||||
P_AUTHREP: goodsData.authorizedRepresentatives,
|
||||
}
|
||||
|
||||
return this.http.patch(`${this.apiUrl}/${this.apiDb}/UpdateExpGoodsAuthRep`, goods);
|
||||
}
|
||||
|
||||
addGoodsItem(headerid: number, items: GoodsItem[]): Observable<any> {
|
||||
|
||||
const goodsItems: any[] = items.map(i => ({
|
||||
ITEMNO: i.itemNumber,
|
||||
ITEMDESCRIPTION: i.description,
|
||||
NOOFPIECES: i.pieces,
|
||||
ITEMWEIGHT: i.weight,
|
||||
ITEMWEIGHTUOM: i.unitOfMeasure,
|
||||
ITEMVALUE: i.value,
|
||||
GOODSORIGINCOUNTRY: i.countryOfOrigin,
|
||||
}));
|
||||
|
||||
const goodsItemsObj = {
|
||||
P_HEADERID: headerid,
|
||||
P_GLTABLE: goodsItems,
|
||||
P_USERID: this.userService.getUser(),
|
||||
};
|
||||
|
||||
return this.http.post(`${this.apiUrl}/${this.apiDb}/AddGenerallistItems`, goodsItemsObj);
|
||||
}
|
||||
|
||||
updateGoodsItem(headerid: number, items: GoodsItem[]): Observable<any> {
|
||||
|
||||
const goodsItems: any[] = items.map(i => ({
|
||||
ITEMNO: i.itemNumber,
|
||||
ITEMDESCRIPTION: i.description,
|
||||
NOOFPIECES: i.pieces,
|
||||
ITEMWEIGHT: i.weight,
|
||||
ITEMWEIGHTUOM: i.unitOfMeasure,
|
||||
ITEMVALUE: i.value,
|
||||
GOODSORIGINCOUNTRY: i.countryOfOrigin,
|
||||
}));
|
||||
|
||||
const goodsItemsObj = {
|
||||
P_HEADERID: headerid,
|
||||
P_GLTABLE: goodsItems,
|
||||
P_USERID: this.userService.getUser(),
|
||||
};
|
||||
|
||||
return this.http.put(`${this.apiUrl}/${this.apiDb}/EditGenerallistItems`, goodsItemsObj);
|
||||
}
|
||||
|
||||
deleteGoodsItem(headerid: number, item: GoodsItem): Observable<any> {
|
||||
|
||||
const goodsItemsObj = {
|
||||
P_HEADERID: headerid,
|
||||
P_ITEMNO: item.itemNumber,
|
||||
P_USERID: this.userService.getUser(),
|
||||
};
|
||||
|
||||
// delete request with body
|
||||
return this.http.delete(`${this.apiUrl}/${this.apiDb}/DeleteGenerallistItems`, { body: goodsItemsObj });
|
||||
}
|
||||
}
|
||||
51
src/app/core/services/carnet/holder.service.ts
Normal file
51
src/app/core/services/carnet/holder.service.ts
Normal file
@ -0,0 +1,51 @@
|
||||
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;
|
||||
|
||||
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.HOLDERNAME,
|
||||
//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);
|
||||
}
|
||||
}
|
||||
9
src/app/core/services/carnet/insurance.service.ts
Normal file
9
src/app/core/services/carnet/insurance.service.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class InsuranceService {
|
||||
|
||||
constructor() { }
|
||||
}
|
||||
157
src/app/core/services/carnet/shipping.service.ts
Normal file
157
src/app/core/services/carnet/shipping.service.ts
Normal file
@ -0,0 +1,157 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { environment } from '../../../../environments/environment';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
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'
|
||||
})
|
||||
export class ShippingService {
|
||||
private apiUrl = environment.apiUrl;
|
||||
private apiDb = environment.apiDb;
|
||||
|
||||
private http = inject(HttpClient);
|
||||
private userService = inject(UserService);
|
||||
|
||||
getShippingData(headerid: number): Shipping | any {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetShipPaymentDetailstoEdit/${this.userService.getUserSpid()}/${this.userService.getUser()}/${headerid}`).pipe(
|
||||
map(response => this.mapToShippingData(response)));
|
||||
}
|
||||
|
||||
private mapToShippingData(shippingDetails: any): Shipping {
|
||||
|
||||
let shippingData: Shipping = {
|
||||
shipTo: shippingDetails.SHIPADDRTYPE,
|
||||
deliveryType: shippingDetails.DELIVERYTYPE,
|
||||
deliveryMethod: shippingDetails.DELIVERYMETHOD,
|
||||
courierAccount: shippingDetails.CUSTCOURIERNO,
|
||||
paymentMethod: shippingDetails.PAYMENTMETHOD,
|
||||
|
||||
//needsBond: shippingDetails.BONDPROTECTION === 'Y',
|
||||
needsInsurance: shippingDetails.INSPROTECTION === 'Y',
|
||||
needsLostDocProtection: shippingDetails.LDIPROTECTION === 'Y',
|
||||
formOfSecurity: shippingDetails.FORMOFSECURITY
|
||||
};
|
||||
|
||||
shippingData.address = {
|
||||
companyName: shippingDetails.P_SHIPNAME,
|
||||
addressid: shippingDetails.SHIPADDRESSID,
|
||||
address1: shippingDetails.ADDRESS1,
|
||||
address2: shippingDetails.ADDRESS2,
|
||||
city: shippingDetails.CITY,
|
||||
state: shippingDetails.STATE,
|
||||
zip: shippingDetails.ZIP,
|
||||
country: shippingDetails.COUNTRY
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
saveShippingDetails(headerid: number, shippingData: Shipping): Observable<any> {
|
||||
|
||||
const shippingDetails = {
|
||||
P_HEADERID: headerid,
|
||||
P_SHIPTOTYPE: shippingData.shipTo,
|
||||
P_DELIVERYTYPE: shippingData.deliveryType,
|
||||
P_DELIVERYMETHOD: shippingData.deliveryMethod,
|
||||
P_CUSTCOURIERNO: shippingData.courierAccount,
|
||||
P_PAYMENTMETHOD: shippingData.paymentMethod,
|
||||
|
||||
// P_BONDPROTECTION: shippingData.needsBond ? 'Y' : 'N',
|
||||
P_INSPROTECTION: shippingData.needsInsurance ? 'Y' : 'N',
|
||||
P_LDIPROTECTION: shippingData.needsLostDocProtection ? 'Y' : 'N',
|
||||
P_FORMOFSECURITY: shippingData.formOfSecurity,
|
||||
|
||||
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,
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
93
src/app/core/services/carnet/travel-plan.service.ts
Normal file
93
src/app/core/services/carnet/travel-plan.service.ts
Normal file
@ -0,0 +1,93 @@
|
||||
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;
|
||||
|
||||
private http = inject(HttpClient);
|
||||
private userService = inject(UserService);
|
||||
|
||||
getCountriesAndMessages(): Observable<Country[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetCountriesAndMessages`).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);
|
||||
}
|
||||
}
|
||||
@ -13,6 +13,10 @@ import { CargoSurety } from '../../models/cargo-surety';
|
||||
import { CarnetStatus } from '../../models/carnet-status';
|
||||
import { Country } from '../../models/country';
|
||||
import { UserService } from './user.service';
|
||||
import { DeliveryMethod } from '../../models/delivery-method';
|
||||
import { FormOfSecurity } from '../../models/formofsecurity';
|
||||
import { PaymentType } from '../../models/payment-type';
|
||||
import { UnitOfMeasure } from '../../models/unitofmeasure';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@ -71,7 +75,9 @@ export class CommonService {
|
||||
response.map((item) => ({
|
||||
name: item.PARAMDESC,
|
||||
id: item.PARAMID,
|
||||
value: item.PARAMVALUE
|
||||
value: item.PARAMVALUE,
|
||||
daysToDelivery: item.ADDLPARAMVALUE1,
|
||||
cutOffTime: item.ADDLPARAMVALUE2,
|
||||
}))
|
||||
)
|
||||
);
|
||||
@ -150,6 +156,55 @@ export class CommonService {
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
id: item.PARAMID,
|
||||
value: item.PARAMVALUE,
|
||||
}))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
id: item.PARAMID,
|
||||
value: item.PARAMVALUE,
|
||||
}))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
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',
|
||||
}))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
formatUSDate(datetime: Date): string {
|
||||
const date = new Date(datetime);
|
||||
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
|
||||
|
||||
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()}`, {});
|
||||
}
|
||||
}
|
||||
54
src/app/core/services/holder/holder.service.ts
Normal file
54
src/app/core/services/holder/holder.service.ts
Normal file
@ -0,0 +1,54 @@
|
||||
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[]> {
|
||||
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/SearchHolder/${this.userService.getUserSpid()}/${this.userService.getSafeUser()}?P_HOLDERNAME=${filter.holderName}`).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'
|
||||
}))
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
227
src/app/holder/basic-details/basic-details.component.ts
Normal file
227
src/app/holder/basic-details/basic-details.component.ts
Normal file
@ -0,0 +1,227 @@
|
||||
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';
|
||||
|
||||
@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'];
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
// 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(["edit-carnet", this.currentApplicationDetails?.headerid],
|
||||
{
|
||||
state: { isEditMode: true },
|
||||
queryParams: {
|
||||
applicationname: this.currentApplicationDetails?.applicationName,
|
||||
return: 'holder'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
266
src/app/holder/contacts/contacts.component.html
Normal file
266
src/app/holder/contacts/contacts.component.html
Normal file
@ -0,0 +1,266 @@
|
||||
<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>
|
||||
|
||||
<!-- Last Name Column -->
|
||||
<ng-container matColumnDef="lastName">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Last Name</th>
|
||||
<td mat-cell *matCellDef="let contact">{{ contact.lastName }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Title Column -->
|
||||
<ng-container matColumnDef="title">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Title</th>
|
||||
<td mat-cell *matCellDef="let contact">{{ contact.title }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Phone Column -->
|
||||
<ng-container matColumnDef="phone">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Phone</th>
|
||||
<td mat-cell *matCellDef="let contact">{{ contact.phone | phone }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Mobile Column -->
|
||||
<ng-container matColumnDef="mobile">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Mobile</th>
|
||||
<td mat-cell *matCellDef="let contact">{{ contact.mobile | phone }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Email Column -->
|
||||
<ng-container matColumnDef="email">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Email</th>
|
||||
<td mat-cell *matCellDef="let contact">{{ contact.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>
|
||||
<input matInput formControlName="title" required>
|
||||
<mat-icon matSuffix>work</mat-icon>
|
||||
<mat-error *ngIf="contactForm.get('title')?.errors?.['required']">
|
||||
Title is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="contactForm.get('title')?.errors?.['maxlength']">
|
||||
Maximum 100 characters allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Phone</mat-label>
|
||||
<input matInput formControlName="phone" required>
|
||||
<mat-icon matSuffix>phone</mat-icon>
|
||||
<mat-error *ngIf="contactForm.get('phone')?.errors?.['required']">
|
||||
Phone is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="contactForm.get('phone')?.errors?.['pattern']">
|
||||
Please enter a valid phone number (10-15 digits)
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Mobile</mat-label>
|
||||
<input matInput formControlName="mobile">
|
||||
<mat-icon matSuffix>smartphone</mat-icon>
|
||||
<mat-error *ngIf="contactForm.get('mobile')?.errors?.['required']">
|
||||
Mobile is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="contactForm.get('mobile')?.errors?.['pattern']">
|
||||
Please enter a valid mobile number (10-15 digits)
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Fax</mat-label>
|
||||
<input matInput formControlName="fax">
|
||||
<mat-icon matSuffix>fax</mat-icon>
|
||||
<mat-error *ngIf="contactForm.get('fax')?.errors?.['pattern']">
|
||||
Please enter a valid fax number (10-15 digits)
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Email</mat-label>
|
||||
<input matInput formControlName="email" required>
|
||||
<mat-icon matSuffix>email</mat-icon>
|
||||
<mat-error *ngIf="contactForm.get('email')?.errors?.['required']">
|
||||
Email is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="contactForm.get('email')?.errors?.['email']">
|
||||
Please enter a valid email address
|
||||
</mat-error>
|
||||
<mat-error *ngIf="contactForm.get('email')?.errors?.['maxlength']">
|
||||
Maximum 100 characters allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<!--
|
||||
<div class="form-row">
|
||||
<mat-checkbox formControlName="defaultContact">Default Contact</mat-checkbox>
|
||||
</div> -->
|
||||
<div *ngIf="isEditing" class="readonly-section">
|
||||
<div class="readonly-fields">
|
||||
<div class="field-column">
|
||||
<!-- Last Changed By -->
|
||||
<div class="readonly-field">
|
||||
<label>Last Changed By</label>
|
||||
<div class="readonly-value">
|
||||
{{contactReadOnlyFields.lastChangedBy || 'N/A'}}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Inactive status -->
|
||||
<div class="readonly-field">
|
||||
<label>Inactive Status </label>
|
||||
<div class="readonly-value">
|
||||
{{contactReadOnlyFields.isInactive === true ? 'Yes' : 'No' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-column">
|
||||
|
||||
<!-- Last Changed Date -->
|
||||
<div class="readonly-field">
|
||||
<label>Last Changed Date</label>
|
||||
<div class="readonly-value">
|
||||
{{(contactReadOnlyFields.lastChangedDate | date:'mediumDate':'UTC') || 'N/A'}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Inactivated Date -->
|
||||
<div class="readonly-field">
|
||||
<label>Inactivated Date</label>
|
||||
<div class="readonly-value">
|
||||
{{(contactReadOnlyFields.inactivatedDate | date:'mediumDate':'UTC') || 'N/A'}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button mat-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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
257
src/app/holder/contacts/contacts.component.ts
Normal file
257
src/app/holder/contacts/contacts.component.ts
Normal file
@ -0,0 +1,257 @@
|
||||
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 } from 'rxjs';
|
||||
|
||||
@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) paginator!: MatPaginator;
|
||||
@ViewChild(MatSort) sort!: MatSort;
|
||||
|
||||
displayedColumns: string[] = ['firstName', 'lastName', 'title', 'phone', 'mobile', 'email', 'actions'];
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
contactForm: FormGroup;
|
||||
|
||||
isEditing = false;
|
||||
currentContactId: number | null = null;
|
||||
isLoading = false;
|
||||
changeInProgress = false;
|
||||
showForm = false;
|
||||
showInactiveContacts = false;
|
||||
contacts: Contact[] = [];
|
||||
currentApplicationDetails: { headerid: number, applicationName: string } | null = null;
|
||||
|
||||
contactReadOnlyFields: any = {
|
||||
lastChangedDate: null,
|
||||
lastChangedBy: null,
|
||||
isInactive: null,
|
||||
inactivatedDate: null
|
||||
};
|
||||
|
||||
private fb = inject(FormBuilder);
|
||||
private dialog = inject(MatDialog);
|
||||
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, Validators.maxLength(100)]],
|
||||
phone: ['', [Validators.required, Validators.pattern(/^[0-9\-\(\)]{10,15}$/)]],
|
||||
mobile: ['', [Validators.required, Validators.pattern(/^[0-9\-\(\)]{10,15}$/)]],
|
||||
fax: ['', [Validators.pattern(/^[0-9\-\(\)]{10,15}$/)]],
|
||||
email: ['', [Validators.required, Validators.email, Validators.maxLength(100)]],
|
||||
});
|
||||
|
||||
this.currentApplicationDetails = this.storageService.get<{ headerid: number, applicationName: string }>('currentapplication')
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.holderid > 0) {
|
||||
this.loadContacts();
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
} 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(["edit-carnet", this.currentApplicationDetails?.headerid],
|
||||
{
|
||||
state: { isEditMode: true },
|
||||
queryParams: {
|
||||
applicationname: this.currentApplicationDetails?.applicationName,
|
||||
return: 'holder'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
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">Search Holders</h3>
|
||||
<div class="search-section">
|
||||
<form class="search-form" [formGroup]="searchForm" (ngSubmit)="onSearch()">
|
||||
<div class="search-fields">
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline" class="name">
|
||||
<mat-label>Holder Name</mat-label>
|
||||
<input matInput formControlName="holderName" placeholder="Enter holder name">
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="search-actions">
|
||||
<button mat-raised-button color="primary" type="submit">
|
||||
<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" *ngIf="!isViewMode" (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">
|
||||
<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="!isViewMode"
|
||||
(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 && !isViewMode"
|
||||
matTooltip="Inactivate" (click)="inactivateHolder(holder.holderid)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="warn" *ngIf="holder.isInactive && !isViewMode"
|
||||
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 matching your criteria</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>
|
||||
116
src/app/holder/search/search-holder.component.scss
Normal file
116
src/app/holder/search/search-holder.component.scss
Normal file
@ -0,0 +1,116 @@
|
||||
.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;
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
266
src/app/holder/search/search-holder.component.ts
Normal file
266
src/app/holder/search/search-holder.component.ts
Normal file
@ -0,0 +1,266 @@
|
||||
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';
|
||||
|
||||
@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 {
|
||||
private _paginator!: MatPaginator;
|
||||
|
||||
@ViewChild(MatPaginator, { static: false })
|
||||
set paginator(value: MatPaginator) {
|
||||
this._paginator = value;
|
||||
this.dataSource.paginator = value;
|
||||
}
|
||||
get paginator(): MatPaginator {
|
||||
return this._paginator;
|
||||
}
|
||||
|
||||
@ViewChild(MatSort, { static: false })
|
||||
set sort(value: MatSort) {
|
||||
this.dataSource.sort = value;
|
||||
}
|
||||
|
||||
@Input() isViewMode = false;
|
||||
@Input() headerid: number = 0;
|
||||
@Input() selectedHolderId: number = 0;
|
||||
@Input() applicationName: string = '';
|
||||
|
||||
@Output() holderSelectionCompleted = new EventEmitter<boolean>();
|
||||
|
||||
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 is different from other pages to show the item selected in the table.
|
||||
//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: ['']
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.searchHolders();
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes['selectedHolderId'] && this.paginator) {
|
||||
if (this.selectedHolderId && this.dataSource.data.length) {
|
||||
this.goToItemPage(this.selectedHolderId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
searchHolders(): void {
|
||||
this.isLoading = true;
|
||||
const filterData: HolderFilter = this.searchForm.value;
|
||||
|
||||
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;
|
||||
} 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(', ');
|
||||
}
|
||||
|
||||
goToItemPage(holderid: number): void {
|
||||
const itemIndex = this.dataSource.data.findIndex(dataItem => dataItem.holderid === holderid);
|
||||
if (itemIndex > -1 && this.paginator) {
|
||||
const targetPageIndex = Math.floor(itemIndex / this.paginator.pageSize);
|
||||
this.paginator.pageIndex = targetPageIndex;
|
||||
this.dataSource.paginator = this.paginator;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -86,32 +86,38 @@
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef>Actions</th>
|
||||
<td mat-cell *matCellDef="let item">
|
||||
<button mat-icon-button color="primary" (click)="processCarnet(item)"
|
||||
*ngIf="getCarnetStatusLabel(item.carnetStatus) === 'Submitted'"
|
||||
[hidden]="getCarnetStatusLabel(item.carnetStatus) !== 'Submitted'" matTooltip="Process">
|
||||
<mat-icon>pending_actions</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="primary" (click)="viewCarnet(item)"
|
||||
*ngIf="getCarnetStatusLabel(item.carnetStatus) === 'Submitted'"
|
||||
[hidden]="getCarnetStatusLabel(item.carnetStatus) !== 'Submitted'" matTooltip="View">
|
||||
<mat-icon>article</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="primary" (click)="resetClient(item)"
|
||||
*ngIf="getCarnetStatusLabel(item.carnetStatus) === 'Submitted'"
|
||||
[hidden]="getCarnetStatusLabel(item.carnetStatus) !== 'Submitted'"
|
||||
matTooltip="Reset to Client">
|
||||
<mat-icon>assignment_return</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="primary" (click)="deleteCarnet(item.HEADERID)"
|
||||
*ngIf="getCarnetStatusLabel(item.carnetStatus) === 'Submitted'"
|
||||
[hidden]="getCarnetStatusLabel(item.carnetStatus) !== 'Submitted'" matTooltip="Delete">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="primary" (click)="printCarnet(item)"
|
||||
*ngIf="getCarnetStatusLabel(item.carnetStatus) === 'Submitted'"
|
||||
[hidden]="getCarnetStatusLabel(item.carnetStatus) !== 'Submitted'" matTooltip="Print">
|
||||
<mat-icon>print</mat-icon>
|
||||
</button>
|
||||
<div class="action-buttons">
|
||||
<button mat-icon-button color="primary" (click)="processCarnet(item)"
|
||||
*ngIf="getCarnetStatusLabel(item.carnetStatus) === 'Submitted'"
|
||||
[hidden]="getCarnetStatusLabel(item.carnetStatus) !== 'Submitted'"
|
||||
matTooltip="Process">
|
||||
<mat-icon>pending_actions</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="primary" (click)="viewCarnet(item)"
|
||||
*ngIf="getCarnetStatusLabel(item.carnetStatus) === 'Submitted'"
|
||||
[hidden]="getCarnetStatusLabel(item.carnetStatus) !== 'Submitted'"
|
||||
matTooltip="View">
|
||||
<mat-icon>article</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="primary" (click)="resetClient(item)"
|
||||
*ngIf="getCarnetStatusLabel(item.carnetStatus) === 'Submitted'"
|
||||
[hidden]="getCarnetStatusLabel(item.carnetStatus) !== 'Submitted'"
|
||||
matTooltip="Reset to Client">
|
||||
<mat-icon>assignment_return</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="primary" (click)="deleteCarnet(item.HEADERID)"
|
||||
*ngIf="getCarnetStatusLabel(item.carnetStatus) === 'Submitted'"
|
||||
[hidden]="getCarnetStatusLabel(item.carnetStatus) !== 'Submitted'"
|
||||
matTooltip="Delete">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="primary" (click)="printCarnet(item)"
|
||||
*ngIf="getCarnetStatusLabel(item.carnetStatus) === 'Submitted'"
|
||||
[hidden]="getCarnetStatusLabel(item.carnetStatus) !== 'Submitted'"
|
||||
matTooltip="Print">
|
||||
<mat-icon>print</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
|
||||
@ -37,7 +37,7 @@
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
mat-table {
|
||||
table {
|
||||
width: 100%;
|
||||
|
||||
mat-icon {
|
||||
@ -48,25 +48,35 @@
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-data-message {
|
||||
text-align: center;
|
||||
padding: 0.9rem;
|
||||
color: rgba(0, 0, 0, 0.54);
|
||||
.mat-column-actions {
|
||||
width: 160px;
|
||||
|
||||
mat-icon {
|
||||
font-size: 1rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-bottom: -3px;
|
||||
.action-buttons {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mat-paginator {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.12);
|
||||
border-radius: 0 0 8px 8px;
|
||||
padding-top: 4px;
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -165,8 +165,8 @@ export class HomeComponent {
|
||||
}
|
||||
|
||||
processCarnet(item: any): void {
|
||||
if (item && item.headerId) {
|
||||
this.navigateTo(['edit-carnet', item.headerId], {
|
||||
if (item && item.HEADERID) {
|
||||
this.navigateTo(['edit-carnet', item.HEADERID], {
|
||||
queryParams: { applicationname: item.applicationName }
|
||||
});
|
||||
} else {
|
||||
@ -175,8 +175,8 @@ export class HomeComponent {
|
||||
}
|
||||
|
||||
viewCarnet(item: any): void {
|
||||
if (item && item.headerId) {
|
||||
this.navigateTo(['view-carnet', item.headerId], {
|
||||
if (item && item.HEADERID) {
|
||||
this.navigateTo(['view-carnet', item.HEADERID], {
|
||||
queryParams: { applicationname: item.applicationName }
|
||||
});
|
||||
} else {
|
||||
@ -185,7 +185,7 @@ export class HomeComponent {
|
||||
}
|
||||
|
||||
printCarnet(item: any): void {
|
||||
if (item && item.headerId) {
|
||||
if (item && item.HEADERID) {
|
||||
} else {
|
||||
this.notificationService.showError('Invalid carnet data');
|
||||
}
|
||||
|
||||
103
src/app/param/manage-country/manage-country.component.html
Normal file
103
src/app/param/manage-country/manage-country.component.html
Normal file
@ -0,0 +1,103 @@
|
||||
<h2 class="page-header">Manage Country Messages</h2>
|
||||
<div class="manage-container">
|
||||
|
||||
<!-- Country Messages Form -->
|
||||
<div class="form-container">
|
||||
<form [formGroup]="countryForm" (ngSubmit)="saveRecord()">
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Country</mat-label>
|
||||
<mat-select [(value)]="currentCountryId" (selectionChange)="onCountrySelectionChanged($event)">
|
||||
<mat-option *ngFor="let country of countries" [value]="country.paramId">
|
||||
{{ country.paramDesc }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Param Type</mat-label>
|
||||
<input matInput formControlName="paramType" readonly required>
|
||||
<mat-error *ngIf="countryForm.get('paramType')?.errors?.['required']">
|
||||
Param Type is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="countryForm.get('paramType')?.errors?.['maxlength']">
|
||||
Maximum 10 characters allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Description</mat-label>
|
||||
<input matInput formControlName="paramDesc" required>
|
||||
<mat-error *ngIf="countryForm.get('paramDesc')?.errors?.['required']">
|
||||
Param Description is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="countryForm.get('paramDesc')?.errors?.['maxlength']">
|
||||
Maximum 100 characters allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Additional Value 1</mat-label>
|
||||
<input matInput formControlName="addlParamValue1">
|
||||
<mat-error *ngIf="countryForm.get('addlParamValue1')?.errors?.['maxlength']">
|
||||
Maximum 20 characters allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Additional Value 2</mat-label>
|
||||
<input matInput formControlName="addlParamValue2">
|
||||
<mat-error *ngIf="countryForm.get('addlParamValue2')?.errors?.['maxlength']">
|
||||
Maximum 20 characters allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Additional Value 3</mat-label>
|
||||
<input matInput formControlName="addlParamValue3">
|
||||
<mat-error *ngIf="countryForm.get('addlParamValue3')?.errors?.['maxlength']">
|
||||
Maximum 20 characters allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Additional Value 4</mat-label>
|
||||
<input matInput formControlName="addlParamValue4">
|
||||
<mat-error *ngIf="countryForm.get('addlParamValue4')?.errors?.['maxlength']">
|
||||
Maximum 20 characters allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Additional Value 5</mat-label>
|
||||
<input matInput formControlName="addlParamValue5">
|
||||
<mat-error *ngIf="countryForm.get('addlParamValue5')?.errors?.['maxlength']">
|
||||
Maximum 20 characters allowed
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button mat-raised-button color="primary" type="submit"
|
||||
[disabled]="countryForm.invalid || changeInProgress">
|
||||
Save
|
||||
</button>
|
||||
<button mat-button type="button" (click)="inActivateParamRecord(1)">Inactivate Record</button>
|
||||
<button mat-button type="button" (click)="reActivateParamRecord(1)">Reactivate Record</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
108
src/app/param/manage-country/manage-country.component.scss
Normal file
108
src/app/param/manage-country/manage-country.component.scss
Normal file
@ -0,0 +1,108 @@
|
||||
.page-header {
|
||||
margin: 0.5rem 0px;
|
||||
color: var(--mat-sys-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.manage-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.readonly-section {
|
||||
padding-top: 0.5rem;
|
||||
border-top: 1px solid #eee;
|
||||
|
||||
.readonly-fields {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
|
||||
.field-column {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.readonly-field {
|
||||
label {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
color: #666;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.readonly-value {
|
||||
padding: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.results-section {
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
border-radius: 8px;
|
||||
|
||||
.loading-shade {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
156
src/app/param/manage-country/manage-country.component.ts
Normal file
156
src/app/param/manage-country/manage-country.component.ts
Normal file
@ -0,0 +1,156 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { ReactiveFormsModule, FormGroup, Validators, FormBuilder } from '@angular/forms';
|
||||
import { finalize } from 'rxjs';
|
||||
import { ParamProperties } from '../../core/models/param/parameters';
|
||||
import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service';
|
||||
import { NotificationService } from '../../core/services/common/notification.service';
|
||||
import { ParamService } from '../../core/services/param/param.service';
|
||||
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
|
||||
import { MatSelectChange } from '@angular/material/select';
|
||||
|
||||
@Component({
|
||||
selector: 'app-manage-country',
|
||||
imports: [AngularMaterialModule, CommonModule, ReactiveFormsModule],
|
||||
templateUrl: './manage-country.component.html',
|
||||
styleUrl: './manage-country.component.scss'
|
||||
})
|
||||
export class ManageCountryComponent {
|
||||
|
||||
isLoading: boolean = false;
|
||||
changeInProgress: boolean = false;
|
||||
countries: ParamProperties[] = [];
|
||||
currentCountryId: number | null = null;
|
||||
currentCountry: ParamProperties | undefined | null = null;
|
||||
currentParamid: number | null = null;
|
||||
|
||||
private paramService = inject(ParamService);
|
||||
private errorHandler = inject(ApiErrorHandlerService);
|
||||
private notificationService = inject(NotificationService);
|
||||
private fb = inject(FormBuilder);
|
||||
|
||||
countryForm: FormGroup;
|
||||
|
||||
constructor() {
|
||||
this.countryForm = this.initializeForm();
|
||||
}
|
||||
|
||||
initializeForm(): FormGroup {
|
||||
return this.fb.group({
|
||||
paramType: ['', [Validators.required, Validators.maxLength(10)]],
|
||||
paramValue: ['', [Validators.required, Validators.maxLength(20)]],
|
||||
paramDesc: ['', [Validators.required, Validators.maxLength(100)]],
|
||||
addlParamValue1: ['', [Validators.maxLength(20)]],
|
||||
addlParamValue2: ['', [Validators.maxLength(20)]],
|
||||
addlParamValue3: ['', [Validators.maxLength(20)]],
|
||||
addlParamValue4: ['', [Validators.maxLength(20)]],
|
||||
addlParamValue5: ['', [Validators.maxLength(20)]],
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.getCountries();
|
||||
}
|
||||
|
||||
onCountrySelectionChanged(event: MatSelectChange) {
|
||||
console.log(event);
|
||||
this.currentCountry = this.countries.find(c => c.paramId === +event.value);
|
||||
this.currentCountryId = this.currentCountry?.paramId ?? 0;
|
||||
|
||||
this.patchCountryData();
|
||||
}
|
||||
|
||||
getCountries(): void {
|
||||
this.isLoading = true;
|
||||
this.paramService.getParameters('014').pipe(finalize(() => {
|
||||
this.isLoading = false;
|
||||
})).subscribe({
|
||||
next: (paramData: ParamProperties[]) => {
|
||||
this.countries = paramData;
|
||||
this.currentCountry = this.countries.find(c => c.paramValue === 'US');
|
||||
this.currentCountryId = this.currentCountry?.paramId ?? 0;
|
||||
|
||||
this.patchCountryData();
|
||||
},
|
||||
error: (error: any) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to get countries');
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error loading countries :', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
patchCountryData(): void {
|
||||
if (!this.currentCountry) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.countryForm.patchValue({
|
||||
paramType: this.currentCountry.paramType,
|
||||
paramValue: this.currentCountry.paramValue,
|
||||
paramDesc: this.currentCountry.paramDesc,
|
||||
addlParamValue1: this.currentCountry.addlParamValue1,
|
||||
addlParamValue2: this.currentCountry.addlParamValue2,
|
||||
addlParamValue3: this.currentCountry.addlParamValue3,
|
||||
addlParamValue4: this.currentCountry.addlParamValue4,
|
||||
addlParamValue5: this.currentCountry.addlParamValue5
|
||||
});
|
||||
}
|
||||
|
||||
saveRecord(): void {
|
||||
if (this.countryForm.invalid) {
|
||||
this.countryForm.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
|
||||
const paramData: ParamProperties = this.countryForm.value;
|
||||
paramData.paramId = this.currentParamid ?? 0;
|
||||
paramData.sortSeq = 1;
|
||||
|
||||
const saveObservable = this.paramService.updateParamRecord(paramData as ParamProperties);
|
||||
|
||||
this.changeInProgress = true;
|
||||
saveObservable.pipe(finalize(() => {
|
||||
this.changeInProgress = false;
|
||||
})).subscribe({
|
||||
next: () => {
|
||||
this.notificationService.showSuccess(`Country message updated successfully`);
|
||||
},
|
||||
error: (error) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, `Failed to update country message`);
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error saving country message:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
inActivateParamRecord(paramId: number): void {
|
||||
this.paramService.inActivateParamRecord(paramId).subscribe(
|
||||
{
|
||||
next: (data: any) => {
|
||||
this.notificationService.showSuccess(`Country message inactivated successfully`);
|
||||
},
|
||||
error: (error: any) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, `Failed to inactivate country message`);
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error inactivating country message:', error);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
reActivateParamRecord(paramId: number): void {
|
||||
this.paramService.reActivateParamRecord(paramId).subscribe(
|
||||
{
|
||||
next: (data: any) => {
|
||||
this.notificationService.showSuccess(`Country message reactivated successfully`);
|
||||
},
|
||||
error: (error: any) => {
|
||||
let errorMessage = this.errorHandler.handleApiError(error, `Failed to reactivate country message`);
|
||||
this.notificationService.showError(errorMessage);
|
||||
console.error('Error reactivating country message:', error);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
export const environment = {
|
||||
production: false,
|
||||
apiUrl: 'https://dev.alphaomegainfosys.com/test-api',
|
||||
apiDb: 'oracle'
|
||||
apiDb: 'oracle',
|
||||
appType: 'service-provider'
|
||||
};
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
export const environment = {
|
||||
production: true,
|
||||
apiUrl: 'https://dev.alphaomegainfosys.com/test-api',
|
||||
apiDb: 'oracle'
|
||||
apiDb: 'oracle',
|
||||
appType: 'service-provider'
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user