diff --git a/package-lock.json b/package-lock.json
index bad5f70..53704f1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index e62eeb6..ce2af0e 100644
--- a/package.json
+++ b/package.json
@@ -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",
@@ -47,4 +48,4 @@
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.8.3"
}
-}
\ No newline at end of file
+}
diff --git a/src/app/app.routes.server.ts b/src/app/app.routes.server.ts
index 959b2db..082105a 100644
--- a/src/app/app.routes.server.ts
+++ b/src/app/app.routes.server.ts
@@ -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
diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts
index ba68ece..42b4a27 100644
--- a/src/app/app.routes.ts
+++ b/src/app/app.routes.ts
@@ -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]
diff --git a/src/app/carnet/add/add-carnet.component.html b/src/app/carnet/add/add-carnet.component.html
new file mode 100644
index 0000000..431e46c
--- /dev/null
+++ b/src/app/carnet/add/add-carnet.component.html
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+ Application Name
+
+
+
+
+
+
+ Holder Selection
+
+
+
+
+
+
+ Goods Section
+
+
+
+
+
+
+ Travel Plan
+
+
+
+
+
+
+ Shipping & Payment
+
+
+
+
+
+
+ done
+
+
+
\ No newline at end of file
diff --git a/src/app/carnet/add/add-carnet.component.scss b/src/app/carnet/add/add-carnet.component.scss
new file mode 100644
index 0000000..c7e2a38
--- /dev/null
+++ b/src/app/carnet/add/add-carnet.component.scss
@@ -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;
+// }
+// }
\ No newline at end of file
diff --git a/src/app/carnet/add/add-carnet.component.ts b/src/app/carnet/add/add-carnet.component.ts
new file mode 100644
index 0000000..85277e5
--- /dev/null
+++ b/src/app/carnet/add/add-carnet.component.ts
@@ -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;
+ }
+}
diff --git a/src/app/carnet/application/application.component.html b/src/app/carnet/application/application.component.html
new file mode 100644
index 0000000..342eb28
--- /dev/null
+++ b/src/app/carnet/application/application.component.html
@@ -0,0 +1,32 @@
+
\ No newline at end of file
diff --git a/src/app/carnet/application/application.component.scss b/src/app/carnet/application/application.component.scss
new file mode 100644
index 0000000..5c09e32
--- /dev/null
+++ b/src/app/carnet/application/application.component.scss
@@ -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;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/app/carnet/application/application.component.ts b/src/app/carnet/application/application.component.ts
new file mode 100644
index 0000000..fa99e4d
--- /dev/null
+++ b/src/app/carnet/application/application.component.ts
@@ -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();
+
+ 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;
+ }
+ });
+ }
+ }
+}
diff --git a/src/app/carnet/edit/edit-carnet.component.html b/src/app/carnet/edit/edit-carnet.component.html
new file mode 100644
index 0000000..d9aabc0
--- /dev/null
+++ b/src/app/carnet/edit/edit-carnet.component.html
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+ Application Name
+
+
+
+
+
+
+ Holder Selection
+
+
+
+
+
+
+ Goods Section
+
+
+
+
+
+
+ Travel Plan
+
+
+
+
+
+
+ Shipping & Payment
+
+
+
+
+
+
+ done
+
+
+
\ No newline at end of file
diff --git a/src/app/carnet/edit/edit-carnet.component.scss b/src/app/carnet/edit/edit-carnet.component.scss
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/carnet/edit/edit-carnet.component.ts b/src/app/carnet/edit/edit-carnet.component.ts
new file mode 100644
index 0000000..5cb1750
--- /dev/null
+++ b/src/app/carnet/edit/edit-carnet.component.ts
@@ -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;
+ }
+}
diff --git a/src/app/carnet/goods/goods.component.html b/src/app/carnet/goods/goods.component.html
new file mode 100644
index 0000000..963033e
--- /dev/null
+++ b/src/app/carnet/goods/goods.component.html
@@ -0,0 +1,240 @@
+
+
+
+
+
+
Goods Items
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Item Number |
+ {{ item.itemNumber }} |
+
+
+
+
+ Description |
+ {{ item.description }} |
+
+
+
+
+ Pieces |
+ {{ item.pieces }} |
+
+
+
+
+ Weight |
+ {{ formatDecimalDisplay(item.weight, 4) }} |
+
+
+
+
+ Unit Of Measure |
+ {{ getUnitOfMeasureLabel(item.unitOfMeasure) }} |
+
+
+
+
+ Value |
+ {{ formatDecimalDisplay(item.value, 2) | currency}} |
+
+
+
+
+ Country of Origin |
+ {{ getCountryLabel(item.countryOfOrigin) }} |
+
+
+
+
+ Actions |
+
+
+
+ |
+
+
+
+
+
+
+ |
+ info
+ No items added
+ |
+
+
+
+
userPreferences.pageSize!" [length]="dataSource.data.length"
+ [pageSizeOptions]="[userPreferences.pageSize!]" [hidePageSize]="true" showFirstLastButtons>
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/carnet/goods/goods.component.scss b/src/app/carnet/goods/goods.component.scss
new file mode 100644
index 0000000..c70ca04
--- /dev/null
+++ b/src/app/carnet/goods/goods.component.scss
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/src/app/carnet/goods/goods.component.ts b/src/app/carnet/goods/goods.component.ts
new file mode 100644
index 0000000..2659b9a
--- /dev/null
+++ b/src/app/carnet/goods/goods.component.ts
@@ -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();
+
+ @ViewChild(MatPaginator) paginator!: MatPaginator;
+ @ViewChild(MatSort) sort!: MatSort;
+
+ // Table configuration
+ displayedColumns: string[] = ['itemNumber', 'description', 'pieces', 'weight', 'unitOfMeasure', 'value', 'countryOfOrigin', 'actions'];
+ dataSource = new MatTableDataSource();
+
+ // 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();
+
+ 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 {
+
+ 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 {
+ 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+$/, '')}`;
+ }
+}
diff --git a/src/app/carnet/holder/holder.component.html b/src/app/carnet/holder/holder.component.html
new file mode 100644
index 0000000..71e6104
--- /dev/null
+++ b/src/app/carnet/holder/holder.component.html
@@ -0,0 +1,2 @@
+
\ No newline at end of file
diff --git a/src/app/carnet/holder/holder.component.scss b/src/app/carnet/holder/holder.component.scss
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/carnet/holder/holder.component.ts b/src/app/carnet/holder/holder.component.ts
new file mode 100644
index 0000000..ff852d7
--- /dev/null
+++ b/src/app/carnet/holder/holder.component.ts
@@ -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();
+ @Output() updated = new EventEmitter();
+
+ 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
+ }
+}
diff --git a/src/app/carnet/shipping/contact-dialog.component.ts b/src/app/carnet/shipping/contact-dialog.component.ts
new file mode 100644
index 0000000..a870f41
--- /dev/null
+++ b/src/app/carnet/shipping/contact-dialog.component.ts
@@ -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: `
+ Select Contact
+
+
+
+ {{getContactLabel(contact)}}
+
+
+
+
+
+
+
+ `,
+ 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,
+ @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(', ') || '';
+ }
+}
\ No newline at end of file
diff --git a/src/app/carnet/shipping/shipping.component.html b/src/app/carnet/shipping/shipping.component.html
new file mode 100644
index 0000000..247e96c
--- /dev/null
+++ b/src/app/carnet/shipping/shipping.component.html
@@ -0,0 +1,366 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/carnet/shipping/shipping.component.scss b/src/app/carnet/shipping/shipping.component.scss
new file mode 100644
index 0000000..543ca56
--- /dev/null
+++ b/src/app/carnet/shipping/shipping.component.scss
@@ -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%;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/app/carnet/shipping/shipping.component.ts b/src/app/carnet/shipping/shipping.component.ts
new file mode 100644
index 0000000..4fdc336
--- /dev/null
+++ b/src/app/carnet/shipping/shipping.component.ts
@@ -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();
+
+ 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();
+
+ 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);
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/src/app/carnet/terms-conditions/terms-conditions.component.html b/src/app/carnet/terms-conditions/terms-conditions.component.html
new file mode 100644
index 0000000..e18b6ba
--- /dev/null
+++ b/src/app/carnet/terms-conditions/terms-conditions.component.html
@@ -0,0 +1,112 @@
+
+
+
+ ATA Carnet Terms and Conditions
+
+
+
+
+
+
+ 1. Definitions
+ ATA Carnet: An international customs document that permits the tax-free and duty-free
+ temporary export and import of goods for up to one year.
+ Holder: The individual or entity in whose name the carnet is issued and who is
+ responsible for complying with all terms.
+ Goods: Merchandise, equipment, or products covered by the carnet.
+
+ 2. Carnet Usage
+ 2.1 The carnet may be used for:
+
+ - Commercial samples
+ - Professional equipment
+ - Goods for exhibitions and fairs
+ - Goods for scientific, educational, or cultural purposes
+
+
+ 2.2 The carnet cannot be used for:
+
+ - Consumable or disposable items
+ - Goods for processing or repair
+ - Goods intended for sale or permanent export
+ - Prohibited or restricted items under any applicable laws
+
+
+ 3. Holder Responsibilities
+ 3.1 The holder must:
+
+ - Present the carnet to customs when crossing borders
+ - Ensure all goods listed in the carnet are returned by the expiration date
+ - Pay all applicable duties and taxes if goods are not re-exported
+ - Notify the issuing association immediately of any lost or stolen carnets
+
+
+ 4. Validity Period
+ 4.1 The carnet is valid for one year from the date of issue.
+ 4.2 Goods must be re-exported before the carnet expires.
+ 4.3 Extensions may be granted in exceptional circumstances with approval from all relevant customs
+ authorities.
+
+ 5. Customs Procedures
+ 5.1 The holder must present the carnet to customs:
+
+ - When first exporting goods from the home country
+ - When entering each foreign country
+ - When re-exporting goods from each foreign country
+ - When finally re-importing goods to the home country
+
+
+ 6. Security Requirements
+ 6.1 The issuing association may require security up to 40% of the total value of goods.
+ 6.2 Security will be refunded when the carnet is fully discharged.
+ 6.3 The security may be forfeited if terms are violated.
+
+ 7. Liability and Insurance
+ 7.1 The holder is solely responsible for:
+
+ - All customs duties and taxes if goods are not re-exported
+ - Any damage to goods while in transit
+ - Compliance with all import/export regulations
+
+ 7.2 We recommend obtaining comprehensive insurance coverage for all goods.
+
+ 8. Fees and Charges
+ 8.1 The following fees apply:
+
+
+ - Basic fee: {{estimatedFees.basicFee | currency}}
+ - Counterfoil Fee: {{estimatedFees.counterFoilFee | currency}}
+
+ - Continuation sheet fee:
+ {{estimatedFees.continuationSheetFee | currency}}
+ - Expedited fee: {{estimatedFees.expeditedFee | currency}}
+ - Shipping fee: {{estimatedFees.shippingFee | currency}}
+ - Bond Premium: {{estimatedFees.bondPremium | currency}}
+ - Cargo Premium: {{estimatedFees.cargoPremium | currency}}
+ - LDI Premium: {{estimatedFees.ldiPremium | currency}}
+
+
+ 9. Dispute Resolution
+ 9.1 Any disputes shall be resolved through arbitration in the jurisdiction of the issuing association.
+
+ 9.2 The holder agrees to be bound by the arbitration decision.
+
+ 10. Governing Law
+ These terms shall be governed by the laws of the country where the carnet was issued.
+
+
+ I have read and agree to all terms and conditions above
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/carnet/terms-conditions/terms-conditions.component.scss b/src/app/carnet/terms-conditions/terms-conditions.component.scss
new file mode 100644
index 0000000..dcaa108
--- /dev/null
+++ b/src/app/carnet/terms-conditions/terms-conditions.component.scss
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/src/app/carnet/terms-conditions/terms-conditions.component.ts b/src/app/carnet/terms-conditions/terms-conditions.component.ts
new file mode 100644
index 0000000..46af8fa
--- /dev/null
+++ b/src/app/carnet/terms-conditions/terms-conditions.component.ts
@@ -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'
+ }
+ })
+ }
+}
\ No newline at end of file
diff --git a/src/app/carnet/travel-plan/travel-plan.component.html b/src/app/carnet/travel-plan/travel-plan.component.html
new file mode 100644
index 0000000..889b45c
--- /dev/null
+++ b/src/app/carnet/travel-plan/travel-plan.component.html
@@ -0,0 +1,151 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/carnet/travel-plan/travel-plan.component.scss b/src/app/carnet/travel-plan/travel-plan.component.scss
new file mode 100644
index 0000000..dd3a1c3
--- /dev/null
+++ b/src/app/carnet/travel-plan/travel-plan.component.scss
@@ -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%;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/app/carnet/travel-plan/travel-plan.component.ts b/src/app/carnet/travel-plan/travel-plan.component.ts
new file mode 100644
index 0000000..4ef21fe
--- /dev/null
+++ b/src/app/carnet/travel-plan/travel-plan.component.ts
@@ -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();
+
+ 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);
+ }
+ });
+ }
+}
diff --git a/src/app/carnet/view/view-carnet.component.html b/src/app/carnet/view/view-carnet.component.html
new file mode 100644
index 0000000..6b9b54e
--- /dev/null
+++ b/src/app/carnet/view/view-carnet.component.html
@@ -0,0 +1,42 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/carnet/view/view-carnet.component.scss b/src/app/carnet/view/view-carnet.component.scss
new file mode 100644
index 0000000..bb26aa7
--- /dev/null
+++ b/src/app/carnet/view/view-carnet.component.scss
@@ -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;
+}
\ No newline at end of file
diff --git a/src/app/carnet/view/view-carnet.component.ts b/src/app/carnet/view/view-carnet.component.ts
new file mode 100644
index 0000000..ebbedc5
--- /dev/null
+++ b/src/app/carnet/view/view-carnet.component.ts
@@ -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') || '';
+ }
+}
diff --git a/src/app/common/secured-header/secured-header.component.html b/src/app/common/secured-header/secured-header.component.html
index 7721448..ce0ba80 100644
--- a/src/app/common/secured-header/secured-header.component.html
+++ b/src/app/common/secured-header/secured-header.component.html
@@ -12,8 +12,12 @@
-
-
+
+
+
+
+
+