From 404adc0d3d40d457ab4ac9aa720c0905ef62c27e Mon Sep 17 00:00:00 2001 From: Cyril Joseph Date: Wed, 9 Jul 2025 13:42:16 -0300 Subject: [PATCH 1/5] register, forgot password and carnet goods section updates --- package-lock.json | 104 ++++ package.json | 1 + src/app/app.routes.server.ts | 2 + src/app/app.routes.ts | 3 +- src/app/carnet/add/add-carnet.component.html | 3 +- src/app/carnet/add/add-carnet.component.ts | 7 + .../application/application.component.ts | 2 +- .../carnet/edit/edit-carnet.component.html | 3 +- src/app/carnet/edit/edit-carnet.component.ts | 7 + src/app/carnet/goods/goods.component.html | 236 +++++++++- src/app/carnet/goods/goods.component.scss | 169 +++++++ src/app/carnet/goods/goods.component.ts | 445 +++++++++++++++++- src/app/core/models/carnet/goods.ts | 18 + src/app/core/models/unitofmeasure.ts | 5 + src/app/core/services/carnet/goods.service.ts | 126 ++++- src/app/core/services/common/auth.service.ts | 20 +- .../core/services/common/common.service.ts | 14 + .../forgot-password.component.html | 55 ++- .../forgot-password.component.ts | 69 ++- src/app/home/home.component.html | 4 +- src/app/home/home.component.ts | 10 +- src/app/register/register.component.html | 32 +- src/app/register/register.component.ts | 36 +- .../shared/module/angular-material.module.ts | 7 +- 24 files changed, 1323 insertions(+), 55 deletions(-) create mode 100644 src/app/core/models/unitofmeasure.ts diff --git a/package-lock.json b/package-lock.json index 1d000bc..bdd4ef5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "ngx-cookie-service": "^20.0.1", "rxjs": "~7.8.0", "tslib": "^2.3.0", + "xlsx": "^0.18.5", "zone.js": "~0.15.0" }, "devDependencies": { @@ -3509,6 +3510,15 @@ "node": ">= 0.6" } }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/agent-base": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", @@ -3955,6 +3965,19 @@ ], "license": "CC-BY-4.0" }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/chalk": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", @@ -4173,6 +4196,15 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -4352,6 +4384,18 @@ "node": ">= 0.10" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -5083,6 +5127,15 @@ "node": ">= 0.6" } }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fresh": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", @@ -8539,6 +8592,18 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/ssri": { "version": "12.0.0", "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", @@ -9214,6 +9279,24 @@ "which": "bin/which" } }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", @@ -9427,6 +9510,27 @@ "node": ">= 6" } }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 51f5aac..e2a482a 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "ngx-cookie-service": "^20.0.1", "rxjs": "~7.8.0", "tslib": "^2.3.0", + "xlsx": "^0.18.5", "zone.js": "~0.15.0" }, "devDependencies": { diff --git a/src/app/app.routes.server.ts b/src/app/app.routes.server.ts index b63129b..75a74b9 100644 --- a/src/app/app.routes.server.ts +++ b/src/app/app.routes.server.ts @@ -1,9 +1,11 @@ import { RenderMode, ServerRoute } from '@angular/ssr'; export const serverRoutes: ServerRoute[] = [ + { path: 'forgot-password/:token', renderMode: RenderMode.Client }, { path: ':appId', renderMode: RenderMode.Client }, { path: ':appId/home', renderMode: RenderMode.Client }, { path: ':appId/usersettings', renderMode: RenderMode.Client }, + { path: ':appId/edit-carnet/:headerid', renderMode: RenderMode.Client }, { path: '**', renderMode: RenderMode.Prerender diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 3b1b0e6..d87ef85 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -4,8 +4,9 @@ import { AuthGuard } from './guards/auth.guard'; export const routes: Routes = [ { path: 'login', loadComponent: () => import('./login/login.component').then(m => m.LoginComponent) }, + { path: 'forgot-password/:token', loadComponent: () => import('./forgot-password/forgot-password.component').then(m => m.ForgotPasswordComponent) }, { path: 'forgot-password', loadComponent: () => import('./forgot-password/forgot-password.component').then(m => m.ForgotPasswordComponent) }, - { path: 'register/:token', loadComponent: () => import('./register/register.component').then(m => m.RegisterComponent) }, + { path: 'register', loadComponent: () => import('./register/register.component').then(m => m.RegisterComponent) }, { path: '404', loadComponent: () => import('./shared/components/not-found/not-found.component').then(m => m.NotFoundComponent) }, { path: ':appId', diff --git a/src/app/carnet/add/add-carnet.component.html b/src/app/carnet/add/add-carnet.component.html index 4422733..1671e6a 100644 --- a/src/app/carnet/add/add-carnet.component.html +++ b/src/app/carnet/add/add-carnet.component.html @@ -23,7 +23,8 @@ Goods Section - + diff --git a/src/app/carnet/add/add-carnet.component.ts b/src/app/carnet/add/add-carnet.component.ts index 671d65b..d81ef99 100644 --- a/src/app/carnet/add/add-carnet.component.ts +++ b/src/app/carnet/add/add-carnet.component.ts @@ -11,6 +11,8 @@ import { InsuranceComponent } from '../insurance/insurance.component'; import { PaymentComponent } from '../payment/payment.component'; import { ShippingComponent } from '../shipping/shipping.component'; import { DeliveryComponent } from '../delivery/delivery.component'; +import { UserPreferencesService } from '../../core/services/user-preference.service'; +import { UserPreferences } from '../../core/models/user-preference'; @Component({ selector: 'app-add-carnet', @@ -26,6 +28,7 @@ export class AddCarnetComponent { applicationType: 'new' | 'additional' | 'duplicate' | 'extend' | null = 'new'; isEditMode = false; headerid: number = 0; + userPreferences: UserPreferences; // Track completion of each step stepsCompleted = { @@ -39,6 +42,10 @@ export class AddCarnetComponent { payment: false }; + constructor(userPrefenceService: UserPreferencesService) { + this.userPreferences = userPrefenceService.getPreferences(); + } + onStepChange(event: StepperSelectionEvent): void { this.currentStep = event.selectedIndex; } diff --git a/src/app/carnet/application/application.component.ts b/src/app/carnet/application/application.component.ts index d8b07d4..4081bc2 100644 --- a/src/app/carnet/application/application.component.ts +++ b/src/app/carnet/application/application.component.ts @@ -39,7 +39,7 @@ export class ApplicationComponent { } ngOnInit(): void { - this.applicationName = this.route.snapshot.queryParamMap.get('applicationName') || ''; + this.applicationName = this.route.snapshot.queryParamMap.get('applicationname') || ''; // Patch edit form data if (this.applicationName) { diff --git a/src/app/carnet/edit/edit-carnet.component.html b/src/app/carnet/edit/edit-carnet.component.html index 6615637..761da22 100644 --- a/src/app/carnet/edit/edit-carnet.component.html +++ b/src/app/carnet/edit/edit-carnet.component.html @@ -23,7 +23,8 @@ Goods Section - + diff --git a/src/app/carnet/edit/edit-carnet.component.ts b/src/app/carnet/edit/edit-carnet.component.ts index fa3f043..9ef173b 100644 --- a/src/app/carnet/edit/edit-carnet.component.ts +++ b/src/app/carnet/edit/edit-carnet.component.ts @@ -12,6 +12,8 @@ import { InsuranceComponent } from '../insurance/insurance.component'; import { PaymentComponent } from '../payment/payment.component'; import { ShippingComponent } from '../shipping/shipping.component'; import { TravelPlanComponent } from '../travel-plan/travel-plan.component'; +import { UserPreferences } from '../../core/models/user-preference'; +import { UserPreferencesService } from '../../core/services/user-preference.service'; @Component({ selector: 'app-edit-carnet', @@ -27,6 +29,7 @@ export class EditCarnetComponent { applicationType: 'new' | 'additional' | 'duplicate' | 'extend' | null = 'new'; isEditMode = true; // Set to true for edit mode headerid: number = 0; + userPreferences: UserPreferences; // Track completion of each step stepsCompleted = { @@ -42,6 +45,10 @@ export class EditCarnetComponent { 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; diff --git a/src/app/carnet/goods/goods.component.html b/src/app/carnet/goods/goods.component.html index 2a9b2bc..b7671a1 100644 --- a/src/app/carnet/goods/goods.component.html +++ b/src/app/carnet/goods/goods.component.html @@ -1 +1,235 @@ -

goods works!

+
+ +
+ +
+
+ Goods to be imported as + Commercial Sample + Professional Equipment + Exhibitions and Fair + + + At least one must be selected + +
+
+ +
+
+ Road Vehicles + used? + Horse used? +
+
+ + +
+ + Authorized Representative(s) + + + Authorized Representative(s) is required + + +
+
+ +
+

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) }}Country of Origin{{ getCountryLabel(item.countryOfOrigin) }}Actions + + +
+ info + No items added +
+ + +
+ + +
+
+
+

{{ isEditing ? 'Edit Item' : 'Add New Item' }}

+
+ +
+ + Item Number + + + Item Number is required + + + + + Description + + + Description is required + + +
+ +
+ + Pieces + + + Pieces is required + + + Must be at least 1 + + + + + Weight + + + Weight is required + + + Must be positive + + + Maximum 4 decimal places allowed + + + + + Unit of Measure + + + {{ unit.name }} + + + + Unit of measure is required + + +
+ +
+ + Value + + + Value is required + + + Must be positive + + + Maximum 2 decimal places allowed + + + + + Country of Origin + + + {{ country.name }} + + + + Country of origin is required + + +
+ +
+ + +
+
+
+ +
+ +
+
\ No newline at end of file diff --git a/src/app/carnet/goods/goods.component.scss b/src/app/carnet/goods/goods.component.scss index e69de29..0cd65f3 100644 --- a/src/app/carnet/goods/goods.component.scss +++ b/src/app/carnet/goods/goods.component.scss @@ -0,0 +1,169 @@ +.goods-container { + padding: 16px; + + .form-section { + margin-bottom: 1rem; + } + + .checkbox-group { + display: flex; + flex-wrap: wrap; + gap: 16px; + + mat-label { + margin-top: 5px; + color: var(--mat-checkbox-label-text-color, var(--mat-sys-on-surface)); + font-family: var(--mat-checkbox-label-text-font, var(--mat-sys-body-medium-font)); + line-height: var(--mat-checkbox-label-text-line-height, var(--mat-sys-body-medium-line-height)); + font-size: var(--mat-checkbox-label-text-size, var(--mat-sys-body-medium-size)); + letter-spacing: var(--mat-checkbox-label-text-tracking, var(--mat-sys-body-medium-tracking)); + font-weight: var(--mat-checkbox-label-text-weight, var(--mat-sys-body-medium-weight)); + } + } + + .full-width { + width: 100%; + } + + .table-actions { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + + h3 { + margin: 0; + color: var(--mat-sys-primary); + font-weight: 500; + } + + .actions { + display: flex; + gap: 16px; + align-items: center; + } + } + + .upload-section { + display: flex; + align-items: center; + gap: 8px; + + span { + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + + .table-container { + position: relative; + overflow-x: auto; + margin-bottom: 16px; + + .loading-shade { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(255, 255, 255, 0.7); + display: flex; + align-items: center; + justify-content: center; + z-index: 1; + } + + mat-table { + width: 100%; + + mat-icon { + cursor: pointer; + transition: all 0.2s ease; + + &:hover { + transform: scale(1.1); + } + } + + .mat-column-actions { + width: 180px; + text-align: center; + } + } + + .no-data-message { + text-align: center; + padding: 0.9rem; + color: rgba(0, 0, 0, 0.54); + + mat-icon { + font-size: 1rem; + width: 1rem; + height: 1rem; + margin-bottom: -3px; + } + } + + mat-paginator { + border-top: 1px solid rgba(0, 0, 0, 0.12); + border-radius: 0 0 8px 8px; + padding-top: 4px; + } + } + + .form-container { + // position: fixed; + // top: 0; + // right: 0; + // bottom: 0; + // width: 400px; + // background: white; + // box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1); + // padding: 24px; + // overflow-y: auto; + // z-index: 100; + + .form-header { + margin-bottom: 24px; + + h3 { + margin: 0; + color: var(--mat-sys-primary); + font-weight: 500; + } + } + + .form-row { + display: flex; + gap: 16px; + margin-bottom: 16px; + + mat-form-field { + flex: 1; + } + + .description { + flex: 2; + } + + .country { + flex: 2; + } + } + + .form-actions { + display: flex; + justify-content: flex-end; + gap: 16px; + margin-top: 24px; + } + } + + .form-actions { + display: flex; + justify-content: flex-end; + margin-top: 24px; + } +} \ No newline at end of file diff --git a/src/app/carnet/goods/goods.component.ts b/src/app/carnet/goods/goods.component.ts index 3c870cc..5ca457c 100644 --- a/src/app/carnet/goods/goods.component.ts +++ b/src/app/carnet/goods/goods.component.ts @@ -1,14 +1,453 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { Component, EventEmitter, inject, Input, Output, ViewChild } from '@angular/core'; import { AngularMaterialModule } from '../../shared/module/angular-material.module'; import { CommonModule } from '@angular/common'; +import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms'; +import { GoodsService } from '../../core/services/carnet/goods.service'; +import { NotificationService } from '../../core/services/common/notification.service'; +import * as XLSX from 'xlsx'; +import { CustomPaginator } from '../../shared/custom-paginator'; +import { MatDialog } from '@angular/material/dialog'; +import { MatPaginatorIntl, MatPaginator } from '@angular/material/paginator'; +import { MatSort } from '@angular/material/sort'; +import { MatTableDataSource } from '@angular/material/table'; +import { ConfirmDialogComponent } from '../../shared/components/confirm-dialog/confirm-dialog.component'; +import { UserPreferences } from '../../core/models/user-preference'; +import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service'; +import { Country } from '../../core/models/country'; +import { UnitOfMeasure } from '../../core/models/unitofmeasure'; +import { CommonService } from '../../core/services/common/common.service'; +import { Subject, takeUntil } from 'rxjs'; +import { Goods, GoodsItem } from '../../core/models/carnet/goods'; @Component({ selector: 'app-goods', - imports: [AngularMaterialModule, CommonModule], + imports: [AngularMaterialModule, CommonModule, ReactiveFormsModule], templateUrl: './goods.component.html', - styleUrl: './goods.component.scss' + styleUrl: './goods.component.scss', + providers: [{ provide: MatPaginatorIntl, useClass: CustomPaginator }], }) export class GoodsComponent { @Input() headerid: number = 0; + @Input() userPreferences: UserPreferences = {}; @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; + 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: ['', Validators.required] + }); + + this.goodsForm.valueChanges.subscribe(() => { + this.completed.emit(this.goodsForm.valid && this.dataSource.data.length > 0); + }); + } + + ngAfterViewInit() { + this.dataSource.paginator = this.paginator; + this.dataSource.sort = this.sort; + } + + ngOnInit(): void { + this.loadCountries(); + this.loadUnitOfMeasures(); + + if (this.headerid > 0) { + this.goodsService.getGoodDetailsByHeaderId(this.headerid).subscribe({ + next: (goodsDetail: Goods) => { + if (goodsDetail) { + this.patchFormData(goodsDetail); + } + this.isLoading = false; + }, + error: (error: any) => { + let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load good details'); + this.notificationService.showError(errorMessage); + this.isLoading = false; + console.error('Error loading good details:', error); + } + }); + + this.loadGoodsItems(); + } + } + + loadGoodsItems(): void { + this.isLoading = true; + + this.goodsService.getGoodsItemsByHeaderId(this.headerid).subscribe({ + next: (items: GoodsItem[]) => { + this.dataSource.data = items; + this.isLoading = false; + }, + error: (error: any) => { + let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load goods items'); + this.notificationService.showError(errorMessage); + this.isLoading = false; + console.error('Error loading goods items:', error); + } + }); + } + + loadCountries(): void { + this.commonService.getCountries(0) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (countries) => { + this.countries = countries; + }, + error: (error) => { + console.error('Failed to load countries', error); + this.isLoading = false; + } + }); + } + + loadUnitOfMeasures(): void { + this.commonService.getUnitOfMeasures() + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (units) => { + this.unitsOfMeasures = units; + this.isLoading = false; + }, + error: (error) => { + console.error('Failed to load unit of measures', error); + this.isLoading = false; + } + }); + } + + patchFormData(data: Goods): void { + this.goodsForm.patchValue({ + commercialSample: data.commercialSample || false, + professionalEquipment: data.professionalEquipment || false, + exhibitionsFair: data.exhibitionsFair || false, + roadVehiclesUsed: data.roadVehiclesUsed || false, + horseUsed: data.horseUsed || false, + authorizedRepresentatives: data.authorizedRepresentatives || '' + }); + } + + // Add new item to the table + addNewItem(): void { + this.showItemForm = true; + this.isEditing = false; + this.currentItem = null; + this.itemForm.reset({ + pieces: 0, + weight: 0, + value: 0 + }); + } + + // Edit existing item + editItem(item: GoodsItem): void { + this.showItemForm = true; + this.isEditing = true; + this.currentItem = item; + this.itemForm.patchValue({ + itemNumber: item.itemNumber, + description: item.description, + pieces: item.pieces, + weight: item.weight, + unitOfMeasure: item.unitOfMeasure || 'kg', + value: item.value, + countryOfOrigin: item.countryOfOrigin || '' + }); + } + + saveItem(): void { + if (this.itemForm.invalid || this.goodsForm.invalid) { + this.itemForm.markAllAsTouched(); + this.goodsForm.markAllAsTouched(); + return; + } + + this.onSubmit(); + + const itemData: GoodsItem = this.itemForm.value; + + // create a goods items array with a single item + let itemDataArray: GoodsItem[] = []; + itemDataArray.push(itemData); + + const saveObservable = this.isEditing && this.currentItem + ? this.goodsService.updateGoodsItem(this.headerid, itemDataArray) + : this.goodsService.addGoodsItem(this.headerid, itemDataArray); + + saveObservable.subscribe({ + next: () => { + this.notificationService.showSuccess(`Goods ${this.isEditing ? 'updated' : 'added'} successfully`); + this.loadGoodsItems(); + this.cancelEdit(); + this.completed.emit(this.goodsForm.valid && this.dataSource.data.length > 0); + }, + error: (error) => { + let errorMessage = this.errorHandler.handleApiError(error, `Failed to ${this.isEditing ? 'update' : 'add'} goods items`); + this.notificationService.showError(errorMessage); + console.error('Error saving goods item:', error); + } + }); + } + + // Delete item with confirmation + deleteItem(item: any): void { + this.currentItem = item; + const dialogRef = this.dialog.open(ConfirmDialogComponent, { + width: '350px', + data: { + title: 'Confirm Delete', + message: 'Are you sure you want to delete this item?', + confirmText: 'Delete', + cancelText: 'Cancel' + } + }); + + dialogRef.afterClosed().subscribe(result => { + if (result) { + this.goodsService.deleteGoodsItem(this.headerid, this.currentItem!).subscribe({ + next: () => { + this.notificationService.showSuccess('Item deleted successfully'); + this.loadGoodsItems(); + this.cancelEdit(); + }, + error: (error) => { + let errorMessage = this.errorHandler.handleApiError(error, 'Failed to delete item'); + this.notificationService.showError(errorMessage); + console.error('Error deleting item:', error); + } + }); + } + }); + } + + // Handle file selection for upload + fileUpload(event: any): void { + const file: File = event.target.files[0]; + + this.processExcelFile(file) + .then(response => { + console.log('File uploaded successfully:', response); + // Handle success (e.g., update UI) + }) + .catch(error => { + console.error('Error processing file:', error); + this.notificationService.showError('Failed to upload file'); + }); + } + + async processExcelFile(file: File): Promise { + + if (file) { + const validTypes = [ + 'application/vnd.ms-excel', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'text/csv' + ]; + + if (!validTypes.includes(file.type)) { + this.notificationService.showError('Please upload only Excel (XLSX, XLS) or CSV files'); + return; + } + + this.fileToUpload = file; + if (!this.fileToUpload || !this.headerid) return; + + this.isProcessing = true; + + try { + const data = await this.readExcelFile(this.fileToUpload); + const items = this.parseExcelData(data); + + if (items.length === 0) { + this.notificationService.showError('No valid items found in the file'); + this.isProcessing = false; + return; + } + + items.map(item => { + item.countryOfOrigin = this.getCountryValue(item.countryOfOrigin); + item.unitOfMeasure = this.getUnitOfMeasureValue(item.unitOfMeasure); + }); + + this.goodsService.addGoodsItem(this.headerid, items).subscribe({ + next: () => { + this.notificationService.showSuccess(`Goods uploaded successfully`); + this.loadGoodsItems(); + this.cancelEdit(); + this.completed.emit(this.goodsForm.valid && this.dataSource.data.length > 0); + this.fileToUpload = null; + }, + error: (error) => { + let errorMessage = this.errorHandler.handleApiError(error, `Failed to save the upload goods items`); + this.notificationService.showError(errorMessage); + console.error('Error saving goods items:', error); + this.fileToUpload = null; + } + }); + } catch (error) { + throw new Error(error instanceof Error ? error.message : 'Failed to process file'); + } finally { + this.isProcessing = false; + } + } + } + + // Read Excel file + private readExcelFile(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + + reader.onload = (e: any) => { + try { + const data = new Uint8Array(e.target.result); + const workbook = XLSX.read(data, { type: 'array' }); + const firstSheetName = workbook.SheetNames[0]; + const worksheet = workbook.Sheets[firstSheetName]; + const jsonData = XLSX.utils.sheet_to_json(worksheet); + resolve(jsonData); + } catch (error) { + reject(error); + } + }; + + reader.onerror = (error) => reject(error); + reader.readAsArrayBuffer(file); + }); + } + + // Parse Excel data into our format + private parseExcelData(data: any[]): any[] { + return data.map(row => ({ + itemNumber: row['Item Number'] || row['itemNumber'] || '', + description: row['Description'] || row['description'] || '', + pieces: Number(row['Pieces'] || row['pieces'] || 0), + weight: Number(row['Weight'] || row['weight'] || 0), + unitOfMeasure: row['Unit of Measure'] || row['unit of measure'] || row['unitofmeasure'] || row['UnitOfMeasure'], + value: Number(row['Value'] || row['value'] || 0), + countryOfOrigin: row['Country Of Origin'] || row['Country of Origin'] || row['country of origin'] || row['countryoforigin'] || row['CountryOfOrigin'] || '' + })).filter(item => + item.itemNumber && + item.description && + item.pieces > 0 + ); + } + + // Save all goods data + onSubmit(): void { + if (this.goodsForm.invalid) { + this.goodsForm.markAllAsTouched(); + return; + } + + const formData = this.goodsForm.value; + this.isLoading = true; + + this.goodsService.saveGoodsData(this.headerid, formData).subscribe({ + next: () => { + this.notificationService.showSuccess('Goods information saved successfully'); + this.completed.emit(true); + }, + error: (error) => { + let errorMessage = this.errorHandler.handleApiError(error, 'Failed to save goods information'); + this.notificationService.showError(errorMessage); + console.error('Error saving goods:', error); + }, + complete: () => { + this.isLoading = false; + } + }); + } + + // Cancel item editing + cancelEdit(): void { + this.showItemForm = false; + this.isEditing = false; + this.currentItem = null; + this.itemForm.reset(); + } + + private atLeastOneRequired(formGroup: FormGroup) { + return (formGroup.get('commercialSample')?.value || + formGroup.get('professionalEquipment')?.value || + formGroup.get('exhibitionsFair')?.value) ? null : { atLeastOneRequired: true }; + } + + getUnitOfMeasureLabel(value: string): string { + const unit = this.unitsOfMeasures.find(u => u.value === value); + return unit ? unit.name : value; + } + + getCountryLabel(value: string): string { + const country = this.countries.find(c => c.value === value); + return country ? country.name : value; + } + + getUnitOfMeasureValue(name: string): string { + const unit = this.unitsOfMeasures.find(u => u.name === name); + return unit ? unit.value : name; + } + + getCountryValue(name: string): string { + const country = this.countries.find(c => c.name === name); + return country ? country.value : name; + } + + formatDecimalDisplay(value: number, decimalPoints: number): string { + if (value === null || value === undefined) return ''; + + // Format to show up to n decimal places, removing trailing zeros + const parts = value.toString().split('.'); + if (parts.length === 1) return parts[0]; // No decimals + return `${parts[0]}.${parts[1].substring(0, decimalPoints).replace(/0+$/, '')}`; + } } diff --git a/src/app/core/models/carnet/goods.ts b/src/app/core/models/carnet/goods.ts index a282e27..3d1fbff 100644 --- a/src/app/core/models/carnet/goods.ts +++ b/src/app/core/models/carnet/goods.ts @@ -1,2 +1,20 @@ export interface Goods { + commercialSample: boolean; + professionalEquipment: boolean; + exhibitionsFair: boolean; + roadVehiclesUsed: boolean; + horseUsed: boolean; + authorizedRepresentatives: string; + items?: GoodsItem[] | null; } + +export interface GoodsItem { + id?: number; + itemNumber: string; + description: string; + pieces: number; + weight: number; + unitOfMeasure: string; + value: number; + countryOfOrigin: string; +} \ No newline at end of file diff --git a/src/app/core/models/unitofmeasure.ts b/src/app/core/models/unitofmeasure.ts new file mode 100644 index 0000000..450192b --- /dev/null +++ b/src/app/core/models/unitofmeasure.ts @@ -0,0 +1,5 @@ +export interface UnitOfMeasure { + name: string; + id: string; + value: string; +} diff --git a/src/app/core/services/carnet/goods.service.ts b/src/app/core/services/carnet/goods.service.ts index 07cc805..2bc707d 100644 --- a/src/app/core/services/carnet/goods.service.ts +++ b/src/app/core/services/carnet/goods.service.ts @@ -1,9 +1,131 @@ -import { Injectable } from '@angular/core'; +import { inject, Injectable } from '@angular/core'; +import { environment } from '../../../../environments/environment'; +import { HttpClient } from '@angular/common/http'; +import { UserService } from '../common/user.service'; +import { Goods, GoodsItem } from '../../models/carnet/goods'; +import { filter, map, Observable } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class GoodsService { + private apiUrl = environment.apiUrl; + private apiDb = environment.apiDb; - constructor() { } + private http = inject(HttpClient); + private userService = inject(UserService); + + getGoodDetailsByHeaderId(headerid: number): Goods | any { + return this.http.get(`${this.apiUrl}/${this.apiDb}/GetGoodsDetailstoEdit/${this.userService.getUserSpid()}/${this.userService.getUser()}/${headerid}`).pipe( + filter(response => response.length > 0), + map(response => this.mapToGoods(response?.[0]))); + } + + private mapToGoods(goodDetails: any): Goods { + return { + roadVehiclesUsed: goodDetails.AUTOFLAG === 'Y', + horseUsed: goodDetails.HORSEFLAG === 'Y', + exhibitionsFair: goodDetails.EXHIBITIONSFAIRFLAG === 'Y', + professionalEquipment: goodDetails.PROFEQUIPMENTFLAG === 'Y', + commercialSample: goodDetails.COMMERCIALSAMPLEFLAG === 'Y', + authorizedRepresentatives: goodDetails.AUTHREP + }; + } + + getGoodsItemsByHeaderId(headerid: number): Observable { + return this.http.get(`${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 { + + 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 { + + 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 { + + 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 { + + const goodsItem = { + ITEMNO: item.itemNumber, + ITEMDESCRIPTION: item.description, + NOOFPIECES: item.pieces, + ITEMWEIGHT: item.weight, + ITEMWEIGHTUOM: item.unitOfMeasure, + ITEMVALUE: item.value, + GOODSORIGINCOUNTRY: item.countryOfOrigin, + } + + const goodsItemsObj = { + P_HEADERID: headerid, + P_GLTABLE: [goodsItem], + P_USERID: this.userService.getUser(), + }; + + // delete request with body + return this.http.delete(`${this.apiUrl}/${this.apiDb}/DeleteGenerallistItems`, { body: goodsItemsObj }); + } } diff --git a/src/app/core/services/common/auth.service.ts b/src/app/core/services/common/auth.service.ts index 7fbc112..04d5f36 100644 --- a/src/app/core/services/common/auth.service.ts +++ b/src/app/core/services/common/auth.service.ts @@ -49,24 +49,32 @@ export class AuthService { } } - register(email: string, password: string, token: string | null): Observable { - let headers = new HttpHeaders(); + register(email: string, password: string): Observable { + + return this.http.post( + `${this.apiUrl}/register`, + { P_EMAILADDR: email, P_PASSWORD: password } + ); + } + + forgotPassword(email: string, password: string, token: string): Observable { + let headers = new HttpHeaders(); if (token) { headers = headers.set('Authorization', `Bearer ${token}`); } return this.http.post( - `${this.apiUrl}/register`, + `${this.apiUrl}/forgot-password`, { P_EMAILADDR: email, P_PASSWORD: password }, { headers } ); } - forgotPassword(email: string, password: string): Observable { + sendEmail(email: string, type: string): Observable { return this.http.post( - `${this.apiUrl}/forgot-password`, - { P_EMAILADDR: email, P_PASSWORD: password } + `${this.apiUrl}/sendmail`, + { P_TO: email, P_MAIL_TYPE: type } ); } } diff --git a/src/app/core/services/common/common.service.ts b/src/app/core/services/common/common.service.ts index d0a1995..e0c45ef 100644 --- a/src/app/core/services/common/common.service.ts +++ b/src/app/core/services/common/common.service.ts @@ -12,6 +12,7 @@ import { CargoPolicy } from '../../models/cargo-policy'; import { CargoSurety } from '../../models/cargo-surety'; import { CarnetStatus } from '../../models/carnet-status'; import { Country } from '../../models/country'; +import { UnitOfMeasure } from '../../models/unitofmeasure'; @Injectable({ providedIn: 'root' @@ -148,6 +149,19 @@ export class CommonService { ); } + getUnitOfMeasures(_spid: number = 0): Observable { + return this.http.get(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=013&P_SPID=0`).pipe( + map((response) => + response.map((item) => ({ + name: item.PARAMDESC, + id: item.PARAMID, + value: item.PARAMVALUE, + color: item.ADDLPARAMVALUE1, + })) + ) + ); + } + formatUSDate(datetime: Date): string { const date = new Date(datetime); const month = String(date.getUTCMonth() + 1).padStart(2, '0'); diff --git a/src/app/forgot-password/forgot-password.component.html b/src/app/forgot-password/forgot-password.component.html index 2968847..dfdd1b3 100644 --- a/src/app/forgot-password/forgot-password.component.html +++ b/src/app/forgot-password/forgot-password.component.html @@ -1,8 +1,36 @@
-

Forgot Password

+

+ {{forgotPasswordRequestCompleted ? 'Reset Password' : 'Forgot Password'}} +

-
+ + + Email + + email + + Email is required + + + Please enter a valid email + + + + + + +
+ +
Email @@ -24,11 +52,32 @@ Password is required + + Password must be at least 8 characters + + + Password must contain at least one uppercase letter, one lowercase letter, one number, and one + special character + + + Confirm Password + + + + Please confirm your password + + + Passwords do not match + + + diff --git a/src/app/home/home.component.ts b/src/app/home/home.component.ts index 4c155d6..b33b2ee 100644 --- a/src/app/home/home.component.ts +++ b/src/app/home/home.component.ts @@ -114,7 +114,7 @@ export class HomeComponent { newCarnet(event: MatRadioChange): void { if (event.value) { - this.navigateTo('add-carnet'); + this.navigateTo(['add-carnet']); } } @@ -129,13 +129,15 @@ export class HomeComponent { editCarnet(item: any): void { if (item && item.headerId) { - this.navigateTo(`edit-carnet/${item.headerId}?ApplicationName=${item.applicationName}`); + this.navigateTo(['edit-carnet', item.headerId], { + queryParams: { applicationname: item.applicationName } + }); } else { this.notificationService.showError('Invalid carnet data'); } } - navigateTo(route: string): void { - this.navigationService.navigate([route]); + navigateTo(route: any[], extras?: any): void { + this.navigationService.navigate(route, extras); } } \ No newline at end of file diff --git a/src/app/register/register.component.html b/src/app/register/register.component.html index 4e0da88..8a053e2 100644 --- a/src/app/register/register.component.html +++ b/src/app/register/register.component.html @@ -2,16 +2,7 @@

User Registration

- -
- - error_outline - Invalid registration link. Please request a new registration link from your - administrator. - -
- - + Email @@ -33,6 +24,27 @@ Password is required + + Password must be at least 8 characters + + + Password must contain at least one uppercase letter, one lowercase letter, one number, and one + special character + + + + + Confirm Password + + + + Please confirm your password + + + Passwords do not match + +
+ +
+
@@ -112,7 +119,8 @@ diff --git a/src/app/home/home.component.scss b/src/app/home/home.component.scss index f64f9df..48a842b 100644 --- a/src/app/home/home.component.scss +++ b/src/app/home/home.component.scss @@ -40,6 +40,15 @@ } } + .table-actions { + display: flex; + justify-content: end; + + button { + margin-bottom: 16px; + } + } + .table-container { position: relative; overflow: auto; diff --git a/src/app/home/home.component.ts b/src/app/home/home.component.ts index b33b2ee..be98861 100644 --- a/src/app/home/home.component.ts +++ b/src/app/home/home.component.ts @@ -15,6 +15,7 @@ import { CommonService } from '../core/services/common/common.service'; import { NotificationService } from '../core/services/common/notification.service'; import { NavigationService } from '../core/services/common/navigation.service'; import { MatRadioChange } from '@angular/material/radio'; +import * as XLSX from 'xlsx'; @Component({ selector: 'app-home', @@ -105,6 +106,40 @@ export class HomeComponent { } exportData() { + try { + // Prepare worksheet data + const worksheetData = this.prepareDownloadData(); + + // Create workbook and worksheet + const workbook = XLSX.utils.book_new(); + const worksheet = XLSX.utils.json_to_sheet(worksheetData); + + // Add worksheet to workbook + XLSX.utils.book_append_sheet(workbook, worksheet, 'Carnet Data'); + + // Generate Excel file + const fileName = `carnet_data_${new Date().toISOString()}.xlsx`; + XLSX.writeFile(workbook, fileName); + } catch (error) { + console.error('Error downloading goods items:', error); + this.notificationService.showError('Failed to download Excel file'); + } + } + + prepareDownloadData(): any[] { + return this.dataSource.data.map(item => ({ + 'Application Name': item.applicationName, + 'Holder Name': item.holderName, + 'Carnet Number': item.carnetNumber, + 'US Sets': item.usSets, + 'Foreign Sets': item.foreignSets, + 'Transit Sets': item.transitSets, + 'Carnet Value': item.carnetValue, + 'Issue Date': item.issueDate ? new Date(item.issueDate).toLocaleDateString() : '', + 'Expiry Date': item.expiryDate ? new Date(item.expiryDate).toLocaleDateString() : '', + 'Order Type': item.orderType, + 'Carnet Status': this.getCarnetStatusLabel(item.carnetStatus) + })); } getCarnetStatusLabel(value: string): string { diff --git a/src/app/login/login.component.html b/src/app/login/login.component.html index b1d61ca..3edc5da 100644 --- a/src/app/login/login.component.html +++ b/src/app/login/login.component.html @@ -21,8 +21,12 @@ -
- Forgot your password? + + +
\ 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 index d81ef99..ecb8611 100644 --- a/src/app/carnet/add/add-carnet.component.ts +++ b/src/app/carnet/add/add-carnet.component.ts @@ -7,18 +7,15 @@ import { ApplicationComponent } from "../application/application.component"; import { HolderComponent } from '../holder/holder.component'; import { GoodsComponent } from '../goods/goods.component'; import { TravelPlanComponent } from '../travel-plan/travel-plan.component'; -import { InsuranceComponent } from '../insurance/insurance.component'; -import { PaymentComponent } from '../payment/payment.component'; import { ShippingComponent } from '../shipping/shipping.component'; -import { DeliveryComponent } from '../delivery/delivery.component'; import { UserPreferencesService } from '../../core/services/user-preference.service'; import { UserPreferences } from '../../core/models/user-preference'; @Component({ selector: 'app-add-carnet', imports: [AngularMaterialModule, CommonModule, ReactiveFormsModule, - ApplicationComponent, HolderComponent, GoodsComponent, TravelPlanComponent, InsuranceComponent, - ShippingComponent, DeliveryComponent, PaymentComponent], + ApplicationComponent, HolderComponent, GoodsComponent, TravelPlanComponent, + ShippingComponent], templateUrl: './add-carnet.component.html', styleUrl: './add-carnet.component.scss' }) @@ -36,10 +33,7 @@ export class AddCarnetComponent { holderSelection: false, goodsSection: false, travelPlan: false, - insurance: false, - shipping: false, - deliveryMethod: false, - payment: false + shipping: false }; constructor(userPrefenceService: UserPreferencesService) { @@ -51,7 +45,7 @@ export class AddCarnetComponent { } onApplicationDetailCreated(headerid: number): void { - this.headerid = this.headerid; + this.headerid = headerid; this.stepsCompleted.applicationDetail = true; this.isLinear = false; // Disable linear mode after application detail is created } @@ -68,19 +62,7 @@ export class AddCarnetComponent { this.stepsCompleted.travelPlan = completed; } - onInsuranceSaved(completed: boolean): void { - this.stepsCompleted.insurance = completed; - } - onShippingSaved(completed: boolean): void { this.stepsCompleted.shipping = completed; } - - onDeliveryMethodSaved(completed: boolean): void { - this.stepsCompleted.deliveryMethod = completed; - } - - onPaymentSaved(completed: boolean): void { - this.stepsCompleted.payment = completed; - } } diff --git a/src/app/carnet/application/application.component.ts b/src/app/carnet/application/application.component.ts index 4081bc2..19ede44 100644 --- a/src/app/carnet/application/application.component.ts +++ b/src/app/carnet/application/application.component.ts @@ -102,7 +102,7 @@ export class ApplicationComponent { this.applicationDetailService.createApplicationDetails(applicationDetailData).subscribe({ next: (applicationData: any) => { this.notificationService.showSuccess(`Application details added successfully`); - this.headerIdCreated.emit(applicationData.HEADERID); + this.headerIdCreated.emit(+applicationData.HEADERID); this.applicationDetailsForm.get('name')?.disable(); this.disableSaveButton = true; this.isLoading = false; diff --git a/src/app/carnet/delivery/delivery.component.html b/src/app/carnet/delivery/delivery.component.html deleted file mode 100644 index e69de29..0000000 diff --git a/src/app/carnet/delivery/delivery.component.scss b/src/app/carnet/delivery/delivery.component.scss deleted file mode 100644 index e69de29..0000000 diff --git a/src/app/carnet/delivery/delivery.component.ts b/src/app/carnet/delivery/delivery.component.ts deleted file mode 100644 index 8ccfec4..0000000 --- a/src/app/carnet/delivery/delivery.component.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; - -@Component({ - selector: 'app-delivery', - imports: [], - templateUrl: './delivery.component.html', - styleUrl: './delivery.component.scss' -}) -export class DeliveryComponent { - @Input() headerid: number = 0; - @Output() completed = new EventEmitter(); - -} diff --git a/src/app/carnet/edit/edit-carnet.component.html b/src/app/carnet/edit/edit-carnet.component.html index 761da22..49d6cfa 100644 --- a/src/app/carnet/edit/edit-carnet.component.html +++ b/src/app/carnet/edit/edit-carnet.component.html @@ -23,7 +23,7 @@ Goods Section - @@ -36,33 +36,11 @@ - - - Insurance - - - - - + - Shipping + Shipping & Payment - - - - Delivery Method - - - - - - - Payment - - - \ No newline at end of file diff --git a/src/app/carnet/edit/edit-carnet.component.ts b/src/app/carnet/edit/edit-carnet.component.ts index 9ef173b..64e3d50 100644 --- a/src/app/carnet/edit/edit-carnet.component.ts +++ b/src/app/carnet/edit/edit-carnet.component.ts @@ -7,9 +7,6 @@ import { StepperSelectionEvent } from '@angular/cdk/stepper'; import { GoodsComponent } from '../goods/goods.component'; import { HolderComponent } from '../holder/holder.component'; import { ActivatedRoute } from '@angular/router'; -import { DeliveryComponent } from '../delivery/delivery.component'; -import { InsuranceComponent } from '../insurance/insurance.component'; -import { PaymentComponent } from '../payment/payment.component'; import { ShippingComponent } from '../shipping/shipping.component'; import { TravelPlanComponent } from '../travel-plan/travel-plan.component'; import { UserPreferences } from '../../core/models/user-preference'; @@ -18,8 +15,7 @@ import { UserPreferencesService } from '../../core/services/user-preference.serv @Component({ selector: 'app-edit-carnet', imports: [AngularMaterialModule, CommonModule, ReactiveFormsModule, - ApplicationComponent, HolderComponent, GoodsComponent, TravelPlanComponent, InsuranceComponent, - ShippingComponent, DeliveryComponent, PaymentComponent], + ApplicationComponent, HolderComponent, GoodsComponent, TravelPlanComponent, ShippingComponent], templateUrl: './edit-carnet.component.html', styleUrl: './edit-carnet.component.scss' }) @@ -37,10 +33,7 @@ export class EditCarnetComponent { holderSelection: false, goodsSection: false, travelPlan: false, - insurance: false, - shipping: false, - deliveryMethod: false, - payment: false + shipping: false }; private route = inject(ActivatedRoute); @@ -70,19 +63,7 @@ export class EditCarnetComponent { this.stepsCompleted.travelPlan = completed; } - onInsuranceSaved(completed: boolean): void { - this.stepsCompleted.insurance = completed; - } - onShippingSaved(completed: boolean): void { this.stepsCompleted.shipping = completed; } - - onDeliveryMethodSaved(completed: boolean): void { - this.stepsCompleted.deliveryMethod = completed; - } - - onPaymentSaved(completed: boolean): void { - this.stepsCompleted.payment = completed; - } } diff --git a/src/app/carnet/goods/goods.component.html b/src/app/carnet/goods/goods.component.html index b7671a1..9457a02 100644 --- a/src/app/carnet/goods/goods.component.html +++ b/src/app/carnet/goods/goods.component.html @@ -89,7 +89,7 @@ - + diff --git a/src/app/carnet/goods/goods.component.ts b/src/app/carnet/goods/goods.component.ts index 5ca457c..4eeffda 100644 --- a/src/app/carnet/goods/goods.component.ts +++ b/src/app/carnet/goods/goods.component.ts @@ -29,6 +29,7 @@ import { Goods, GoodsItem } from '../../core/models/carnet/goods'; export class GoodsComponent { @Input() headerid: number = 0; @Input() userPreferences: UserPreferences = {}; + @Input() isEditMode: boolean = false; @Output() completed = new EventEmitter(); @ViewChild(MatPaginator) paginator!: MatPaginator; @@ -85,10 +86,6 @@ export class GoodsComponent { value: [0, [Validators.required, Validators.min(1), Validators.pattern(/^\d+(\.\d{1,2})?$/)]], countryOfOrigin: ['', Validators.required] }); - - this.goodsForm.valueChanges.subscribe(() => { - this.completed.emit(this.goodsForm.valid && this.dataSource.data.length > 0); - }); } ngAfterViewInit() { @@ -117,6 +114,7 @@ export class GoodsComponent { }); this.loadGoodsItems(); + this.completed.emit(this.goodsForm.valid && this.dataSource.data.length > 0); } } @@ -314,10 +312,10 @@ export class GoodsComponent { return; } - items.map(item => { - item.countryOfOrigin = this.getCountryValue(item.countryOfOrigin); - item.unitOfMeasure = this.getUnitOfMeasureValue(item.unitOfMeasure); - }); + // items.map(item => { + // item.countryOfOrigin = this.getCountryValue(item.countryOfOrigin); + // item.unitOfMeasure = this.getUnitOfMeasureValue(item.unitOfMeasure); + // }); this.goodsService.addGoodsItem(this.headerid, items).subscribe({ next: () => { @@ -395,7 +393,7 @@ export class GoodsComponent { this.goodsService.saveGoodsData(this.headerid, formData).subscribe({ next: () => { this.notificationService.showSuccess('Goods information saved successfully'); - this.completed.emit(true); + this.completed.emit(this.goodsForm.valid && this.dataSource.data.length > 0); }, error: (error) => { let errorMessage = this.errorHandler.handleApiError(error, 'Failed to save goods information'); diff --git a/src/app/carnet/insurance/insurance.component.html b/src/app/carnet/insurance/insurance.component.html deleted file mode 100644 index d4f2ff5..0000000 --- a/src/app/carnet/insurance/insurance.component.html +++ /dev/null @@ -1 +0,0 @@ -

insurance works!

diff --git a/src/app/carnet/insurance/insurance.component.scss b/src/app/carnet/insurance/insurance.component.scss deleted file mode 100644 index e69de29..0000000 diff --git a/src/app/carnet/insurance/insurance.component.ts b/src/app/carnet/insurance/insurance.component.ts deleted file mode 100644 index 353e5cf..0000000 --- a/src/app/carnet/insurance/insurance.component.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; - -@Component({ - selector: 'app-insurance', - imports: [], - templateUrl: './insurance.component.html', - styleUrl: './insurance.component.scss' -}) -export class InsuranceComponent { - @Input() headerid: number = 0; - @Output() completed = new EventEmitter(); -} diff --git a/src/app/carnet/payment/payment.component.html b/src/app/carnet/payment/payment.component.html deleted file mode 100644 index d9cf5ff..0000000 --- a/src/app/carnet/payment/payment.component.html +++ /dev/null @@ -1 +0,0 @@ -

payment works!

diff --git a/src/app/carnet/payment/payment.component.scss b/src/app/carnet/payment/payment.component.scss deleted file mode 100644 index e69de29..0000000 diff --git a/src/app/carnet/payment/payment.component.ts b/src/app/carnet/payment/payment.component.ts deleted file mode 100644 index 2637d52..0000000 --- a/src/app/carnet/payment/payment.component.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; - -@Component({ - selector: 'app-payment', - imports: [], - templateUrl: './payment.component.html', - styleUrl: './payment.component.scss' -}) -export class PaymentComponent { - @Input() headerid: number = 0; - @Output() completed = new EventEmitter(); -} diff --git a/src/app/carnet/shipping/shipping.component.html b/src/app/carnet/shipping/shipping.component.html index 892d52b..6c8ec95 100644 --- a/src/app/carnet/shipping/shipping.component.html +++ b/src/app/carnet/shipping/shipping.component.html @@ -1 +1,302 @@ -

shipping works!

+
+ + +
+

Insurance & Bond

+
+ Do you need Bond from + us? + Do you need insurance for your + goods? + Do you need Lost document + protection? +
+
+ + +
+

Ship To

+ + Preparer + Holder + 3rd Party + + + + +

{{getAddressLabel()}}

+
+ + + +
+
+

Shipping Address

+ + +
+ + Address Line 1 + + + Address is required + + + Maximum 100 characters allowed + + +
+ +
+ + Address Line 2 (Optional) + + + Maximum 100 characters allowed + + +
+ + +
+ + City + + + City is required + + + Maximum 50 characters allowed + + + + + Country + + + {{ country.name }} + + + + Country is required + + + + + State/Province + + + {{ state.name }} + + + + State is required + + + + + ZIP/Postal Code + + + ZIP/Postal code is required + + + Please enter a valid 5-digit US ZIP code + + + Please enter a valid postal code (e.g., A1B2C3) + + +
+ +
+ +
+
+ +
+

Contact Information

+
+ + First Name + + person + + First name is required + + + Maximum 50 characters allowed + + + + + Middle Initial + + + Only 1 character allowed + + + + + Last Name + + person + + Last name is required + + + Maximum 50 characters allowed + + +
+
+ + Title + + work + + Title is required + + + Maximum 100 characters allowed + + +
+ +
+ + Phone + + phone + + Phone is required + + + Please enter a valid phone number (10-15 digits) + + + + + Mobile + + smartphone + + Mobile is required + + + Please enter a valid mobile number (10-15 digits) + + +
+ +
+ + Fax + + fax + + Please enter a valid fax number (10-15 digits) + + + + + Email + + email + + Email is required + + + Please enter a valid email address + + + Maximum 100 characters allowed + + +
+
+ + Reference Number + + +
+
+ + Notes + + +
+
+
+ + +
+

Delivery Method

+
+ + Delivery Type + + + {{ deliveryType.name }} + + + + Delivery Type is required + + + + Delivery Method + + + {{ deliveryMethod.name }} + + + + Delivery Method is required + + +
+
+ + Courier Account Number + + Required when using customer courier + +
+
+ + +
+

Payment Method

+
+ + Payment Method + + + {{ paymentType.name }} + + + + Payment Method is required + + +
+ +
+ +
+ +
+ +
\ No newline at end of file diff --git a/src/app/carnet/shipping/shipping.component.scss b/src/app/carnet/shipping/shipping.component.scss index e69de29..e1973a7 100644 --- a/src/app/carnet/shipping/shipping.component.scss +++ b/src/app/carnet/shipping/shipping.component.scss @@ -0,0 +1,76 @@ +.shipping-container { + padding: 16px; + + .section { + margin-bottom: 24px; + padding: 16px; + border: 1px solid #e0e0e0; + border-radius: 4px; + + h3 { + margin: 0px 0px 16px 0px; + color: var(--mat-sys-primary); + font-weight: 500; + } + + h4 { + margin: 16px 0 8px; + color: var(--mat-sys-primary); + font-weight: 500; + } + + .mat-mdc-card-content:first-child { + padding: 0 16px; + } + } + + .checkbox-group { + display: flex; + flex-direction: column; + gap: 8px; + } + + .radio-group { + display: flex; + gap: 16px; + margin-bottom: 16px; + } + + .form-row { + display: flex; + gap: 16px; + + mat-form-field { + flex: 1; + margin-bottom: 6px; + + &.full-width { + flex: 100%; + } + } + + .small-field { + max-width: 120px; + } + } + + .form-actions { + display: flex; + justify-content: flex-end; + gap: 16px; + margin-top: 16px; + } +} + +@media (max-width: 768px) { + .shipping-container { + .form-row { + flex-direction: column; + gap: 8px; + + mat-form-field { + width: 100%; + } + } + } +} \ No newline at end of file diff --git a/src/app/carnet/shipping/shipping.component.ts b/src/app/carnet/shipping/shipping.component.ts index a972ee4..4cb0ade 100644 --- a/src/app/carnet/shipping/shipping.component.ts +++ b/src/app/carnet/shipping/shipping.component.ts @@ -1,12 +1,376 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Component, EventEmitter, inject, Input, Output } from '@angular/core'; +import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { NotificationService } from '../../core/services/common/notification.service'; +import { AngularMaterialModule } from '../../shared/module/angular-material.module'; +import { ShippingService } from '../../core/services/carnet/shipping.service'; +import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service'; +import { CommonService } from '../../core/services/common/common.service'; +import { Shipping } from '../../core/models/carnet/shipping'; +import { Country } from '../../core/models/country'; +import { State } from '../../core/models/state'; +import { ZipCodeValidator } from '../../shared/validators/zipcode-validator'; +import { Subject, takeUntil } from 'rxjs'; +import { DeliveryType } from '../../core/models/delivery-type'; +import { DeliveryMethod } from '../../core/models/delivery-method'; +import { PaymentType } from '../../core/models/payment-type'; @Component({ selector: 'app-shipping', - imports: [], + imports: [AngularMaterialModule, CommonModule, ReactiveFormsModule], templateUrl: './shipping.component.html', styleUrl: './shipping.component.scss' }) export class ShippingComponent { @Input() headerid: number = 0; + @Input() isEditMode = false; @Output() completed = new EventEmitter(); -} + + private fb = inject(FormBuilder); + private shippingService = inject(ShippingService); + private notificationService = inject(NotificationService); + private errorHandler = inject(ApiErrorHandlerService); + private commonService = inject(CommonService); + + shippingForm: FormGroup; + isLoading = false; + showAddressForm = false; + showContactForm = false; + + countriesHasStates = ['US', 'CA', 'MX']; + countries: Country[] = []; + states: State[] = []; + deliveryTypes: DeliveryType[] = []; + deliveryMethods: DeliveryMethod[] = []; + paymentTypes: PaymentType[] = []; + + private destroy$ = new Subject(); + + // preparer contact and address mock data + preparerContact = { + firstName: 'John', + lastName: 'Doe', + middleInitial: 'A', + title: 'Mr.', + phone: '1234567890', + mobile: '0987654321', + fax: '1234567890', + email: 'j@doe.com', + } + + preparerAddress = { + address1: '123 Main St', + address2: 'Suite 100', + city: 'Anytown', + state: 'CA', + zip: '12345', + country: 'US' + }; + + holderAddress = { + address1: '456 Holder St', + address2: 'Apt 200', + city: 'Othertown', + state: 'NY', + zip: '67890', + country: 'US' + }; + + holderContact = { + firstName: 'Jane', + lastName: 'Smith', + middleInitial: 'B', + title: 'Ms.', + phone: '2345678901', + mobile: '1098765432', + fax: '2345678901', + email: 'jan@sm.cm' + }; + + constructor() { + this.shippingForm = this.fb.group({ + needsBond: [false], + needsInsurance: [false], + needsLostDocProtection: [false], + shipTo: ['preparer', Validators.required], + address: this.fb.group({ + address1: ['', [Validators.required, Validators.maxLength(100)]], + address2: ['', [Validators.maxLength(100)]], + city: ['', [Validators.required, Validators.maxLength(50)]], + state: ['', Validators.required], + country: ['', Validators.required], + zip: ['', [Validators.required, ZipCodeValidator('country')]], + }), + contact: this.fb.group({ + firstName: ['', [Validators.required, Validators.maxLength(50)]], + lastName: ['', [Validators.required, Validators.maxLength(50)]], + middleInitial: ['', [Validators.maxLength(1)]], + title: ['', [Validators.required, Validators.maxLength(100)]], + phone: ['', [Validators.required, Validators.pattern(/^[0-9]{10,15}$/)]], + mobile: ['', [Validators.required, Validators.pattern(/^[0-9]{10,15}$/)]], + fax: ['', [Validators.pattern(/^[0-9]{10,15}$/)]], + email: ['', [Validators.required, Validators.email, Validators.maxLength(100)]], + refNumber: [''], + notes: [''] + }), + deliveryType: ['', Validators.required], + deliveryMethod: ['', Validators.required], + courierAccount: [''], + paymentMethod: ['', Validators.required], + notes: [''] + }); + + // Update form validation based on selections + // this.shippingForm.get('shipTo')?.valueChanges.subscribe(value => { + // this.updateShippingValidation(value); + // }); + + this.shippingForm.get('deliveryMethod')?.valueChanges.subscribe(value => { + const courierControl = this.shippingForm.get('courierAccount'); + if (value === 'customerCourier') { + courierControl?.setValidators([Validators.required]); + } else { + courierControl?.clearValidators(); + } + courierControl?.updateValueAndValidity(); + }); + + this.shippingForm.valueChanges.subscribe(() => { + this.completed.emit(this.shippingForm.valid); + }); + } + + ngOnInit(): void { + this.loadCountries(); + this.loadDeliveryTypes(); + this.loadDeliveryMethods(); + this.loadPaymentTypes(); + + if (this.headerid && this.isEditMode) { + this.loadShippingData(); + } + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + onCountryChange(country: string): void { + this.shippingForm.get('address.state')?.reset(); + + if (country) { + this.loadStates(country); + } + + this.shippingForm.get('address.zip')?.updateValueAndValidity(); + } + + loadCountries(): void { + this.commonService.getCountries(0) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (countries) => { + this.countries = countries; + }, + error: (error) => { + console.error('Failed to load countries', error); + this.isLoading = false; + } + }); + } + + loadDeliveryTypes(): void { + this.commonService.getDeliveryTypes(0) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (deliveryTypes) => { + this.deliveryTypes = deliveryTypes; + }, + error: (error) => { + console.error('Failed to load delivery types', error); + this.isLoading = false; + } + }); + } + + loadDeliveryMethods(): void { + this.commonService.getDeliveryMethods(0) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (deliveryMethods) => { + this.deliveryMethods = deliveryMethods; + }, + error: (error) => { + console.error('Failed to load delivery methods', error); + this.isLoading = false; + } + }); + } + + loadPaymentTypes(): void { + this.commonService.getPaymentTypes(0) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (paymentTypes) => { + this.paymentTypes = paymentTypes; + }, + error: (error) => { + console.error('Failed to load payment types', error); + this.isLoading = false; + } + }); + } + + loadStates(country: string): void { + this.isLoading = true; + country = this.countriesHasStates.includes(country) ? country : 'FN'; + this.commonService.getStates(country, 0) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (states) => { + this.states = states; + const stateControl = this.shippingForm.get('contact.state'); + if (this.countriesHasStates.includes(country)) { + stateControl?.enable(); + } else { + stateControl?.disable(); + stateControl?.setValue('FN'); + } + this.isLoading = false; + }, + error: (error) => { + console.error('Failed to load states', error); + this.isLoading = false; + } + }); + } + + loadShippingData(): void { + this.isLoading = true; + this.shippingService.getShippingData(this.headerid).subscribe({ + next: (data: Shipping) => { + this.patchShippingData(data); + this.isLoading = false; + }, + error: (error: any) => { + let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load shipping and payments data'); + this.notificationService.showError(errorMessage); + this.isLoading = false; + } + }); + } + + patchShippingData(shipping: Shipping): void { + this.shippingForm.patchValue({ + needsBond: shipping.needsBond, + needsInsurance: shipping.needsInsurance, + needsLostDocProtection: shipping.needsLostDocProtection, + shipTo: shipping.shipTo, + address: shipping.address, + contact: shipping.contact, + deliveryType: shipping.deliveryType, + deliveryMethod: shipping.deliveryMethod, + courierAccount: shipping.courierAccount, + paymentMethod: shipping.paymentMethod, + notes: shipping.notes + }); + + if (shipping.address?.country) { + this.loadStates(shipping.address?.country); + } + } + + updateShippingValidation(shipTo: string): void { + // const addressGroup = this.shippingForm.get('address') as FormGroup; + // const contactGroup = this.shippingForm.get('contact') as FormGroup; + + // if (shipTo === 'thirdParty') { + // Object.keys(addressGroup.controls).forEach(key => { + // addressGroup.get(key)?.setValidators(Validators.required); + // }); + // Object.keys(contactGroup.controls).forEach(key => { + // if (key !== 'middleInitial' && key !== 'fax' && key !== 'notes' && key !== 'refNumber') { + // contactGroup.get(key)?.setValidators(Validators.required); + // } + // }); + // } else { + // Object.keys(addressGroup.controls).forEach(key => { + // addressGroup.get(key)?.clearValidators(); + // }); + // Object.keys(contactGroup.controls).forEach(key => { + // contactGroup.get(key)?.clearValidators(); + // }); + // } + + // addressGroup.updateValueAndValidity(); + // contactGroup.updateValueAndValidity(); + } + + onSubmit(): void { + if (this.shippingForm.invalid) { + this.shippingForm.markAllAsTouched(); + return; + } + + this.isLoading = true; + const shippingData = this.shippingForm.value; + + this.shippingService.saveShippingDetails(this.headerid, shippingData).subscribe({ + next: () => { + this.notificationService.showSuccess('Shipping & payments information saved successfully'); + this.completed.emit(true); + this.isLoading = false; + }, + error: (error) => { + let errorMessage = this.errorHandler.handleApiError(error, 'Failed to save shipping and payment information'); + this.notificationService.showError(errorMessage); + this.isLoading = false; + } + }); + } + + getAddressLabel(): string { + let shipTo = this.shippingForm.get('shipTo')?.value; + + if (shipTo === 'preparer') { + return `${this.preparerContact.firstName} ${this.preparerContact.lastName}, ${this.preparerAddress.address1}, ${this.preparerAddress.city}, ${this.preparerAddress.state}, ${this.preparerAddress.zip}, ${this.preparerAddress.country}`; + } + + if (shipTo === 'holder') { + return `${this.holderContact.firstName} ${this.holderContact.lastName}, ${this.holderAddress.address1}, ${this.holderAddress.city}, ${this.holderAddress.state}, ${this.holderAddress.zip}, ${this.holderAddress.country}`; + } + return ''; + } + + editAddressForm(): void { + this.showAddressForm = true; + let shipTo = this.shippingForm.get('shipTo')?.value; + + if (shipTo === 'preparer') { + this.shippingForm.get('address')?.patchValue(this.preparerAddress); + this.loadStates(this.preparerAddress.country); + } else if (shipTo === 'holder') { + this.shippingForm.get('address')?.patchValue(this.holderAddress); + this.loadStates(this.preparerAddress.country); + } + } + + cancelEditAddressForm(): void { + this.showAddressForm = false; + this.shippingForm.get('address')?.reset(); + } + + onShipToChange(event: any): void { + const shipTo = event.value; + this.showAddressForm = false; + this.showContactForm = false; + + if (shipTo === 'thirdParty') { + this.shippingForm.get('contact')?.reset(); + this.shippingForm.get('address')?.reset(); + this.showAddressForm = true; + this.showContactForm = true; + } + } +} \ No newline at end of file diff --git a/src/app/core/models/carnet/delivery.ts b/src/app/core/models/carnet/delivery.ts deleted file mode 100644 index 964b03f..0000000 --- a/src/app/core/models/carnet/delivery.ts +++ /dev/null @@ -1,2 +0,0 @@ -export interface Delivery { -} diff --git a/src/app/core/models/carnet/insurance.ts b/src/app/core/models/carnet/insurance.ts deleted file mode 100644 index 786147b..0000000 --- a/src/app/core/models/carnet/insurance.ts +++ /dev/null @@ -1,2 +0,0 @@ -export interface Insurance { -} diff --git a/src/app/core/models/carnet/payment.ts b/src/app/core/models/carnet/payment.ts deleted file mode 100644 index 820c46e..0000000 --- a/src/app/core/models/carnet/payment.ts +++ /dev/null @@ -1,2 +0,0 @@ -export interface Payment { -} diff --git a/src/app/core/models/carnet/shipping-address.ts b/src/app/core/models/carnet/shipping-address.ts new file mode 100644 index 0000000..ea647c4 --- /dev/null +++ b/src/app/core/models/carnet/shipping-address.ts @@ -0,0 +1,8 @@ +export interface ShippingAddress { + address1: string; + address2?: string | null; + city: string; + state: string; + country: string; + zip: string; +} diff --git a/src/app/core/models/carnet/shipping-contact.ts b/src/app/core/models/carnet/shipping-contact.ts new file mode 100644 index 0000000..39d532f --- /dev/null +++ b/src/app/core/models/carnet/shipping-contact.ts @@ -0,0 +1,12 @@ +export interface ShippingContact { + firstName: string; + lastName: string; + middleInitial?: string | null; + title: string; + phone: string; + mobile: string; + fax?: string | null; + email: string; + refNumber?: string | null; + notes?: string | null; +} diff --git a/src/app/core/models/carnet/shipping.ts b/src/app/core/models/carnet/shipping.ts index a1b72da..a191c7b 100644 --- a/src/app/core/models/carnet/shipping.ts +++ b/src/app/core/models/carnet/shipping.ts @@ -1,2 +1,23 @@ +import { ShippingAddress } from "./shipping-address"; +import { ShippingContact } from "./shipping-contact"; + export interface Shipping { + // Insurance details + needsBond: boolean; + needsInsurance: boolean; + needsLostDocProtection: boolean; + + // Shipping details + shipTo: string; + address?: ShippingAddress; + contact?: ShippingContact; + + // Delivery details + deliveryType: string; + deliveryMethod: string; + courierAccount?: string; + + // Payment details + paymentMethod: string; + notes?: string; } diff --git a/src/app/core/models/delivery-method.ts b/src/app/core/models/delivery-method.ts new file mode 100644 index 0000000..1adc1f1 --- /dev/null +++ b/src/app/core/models/delivery-method.ts @@ -0,0 +1,5 @@ +export interface DeliveryMethod { + name: string; + id: string; + value: string; +} diff --git a/src/app/core/models/payment-type.ts b/src/app/core/models/payment-type.ts new file mode 100644 index 0000000..10baebb --- /dev/null +++ b/src/app/core/models/payment-type.ts @@ -0,0 +1,5 @@ +export interface PaymentType { + name: string; + id: string; + value: string; +} diff --git a/src/app/core/services/carnet/delivery.service.ts b/src/app/core/services/carnet/delivery.service.ts deleted file mode 100644 index b5dec8a..0000000 --- a/src/app/core/services/carnet/delivery.service.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Injectable } from '@angular/core'; - -@Injectable({ - providedIn: 'root' -}) -export class DeliveryService { - - constructor() { } -} diff --git a/src/app/core/services/carnet/payment.service.ts b/src/app/core/services/carnet/payment.service.ts deleted file mode 100644 index f58bf94..0000000 --- a/src/app/core/services/carnet/payment.service.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Injectable } from '@angular/core'; - -@Injectable({ - providedIn: 'root' -}) -export class PaymentService { - - constructor() { } -} diff --git a/src/app/core/services/carnet/shipping.service.ts b/src/app/core/services/carnet/shipping.service.ts index 227ebda..b045e2e 100644 --- a/src/app/core/services/carnet/shipping.service.ts +++ b/src/app/core/services/carnet/shipping.service.ts @@ -1,9 +1,103 @@ -import { Injectable } from '@angular/core'; +import { inject, Injectable } from '@angular/core'; +import { environment } from '../../../../environments/environment'; +import { HttpClient } from '@angular/common/http'; +import { filter, map, Observable } from 'rxjs'; +import { UserService } from '../common/user.service'; +import { Shipping } from '../../models/carnet/shipping'; @Injectable({ providedIn: 'root' }) export class ShippingService { + private apiUrl = environment.apiUrl; + private apiDb = environment.apiDb; - constructor() { } + private http = inject(HttpClient); + private userService = inject(UserService); + + getShippingData(headerid: number): Shipping | any { + return this.http.get(`${this.apiUrl}/${this.apiDb}/GetShipPaymentDetailstoEdit/${this.userService.getUserSpid()}/${this.userService.getUser()}/${headerid}`).pipe( + filter(response => response.length > 0), + map(response => this.mapToShippingData(response?.[0]))); + } + + private mapToShippingData(shippingDetails: any): Shipping { + + let shippingData: Shipping = { + shipTo: shippingDetails.SHIPADDRTYPE, + deliveryType: shippingDetails.DELIVERYTYPE, + deliveryMethod: shippingDetails.DELIVERYMETHOD, + courierAccount: shippingDetails.CUSTCOURIERNO, + paymentMethod: shippingDetails.PAYMENTMETHOD, + + notes: shippingDetails.NOTES, + needsBond: shippingDetails.INSPROTECTION === 'Y', + needsInsurance: shippingDetails.LDIPROTECTION === 'Y', + needsLostDocProtection: shippingDetails.LDIPROTECTION === 'Y' + }; + + if (shippingDetails.address) { + shippingData.address = { + address1: shippingDetails.ADDRESS1, + address2: shippingDetails.ADDRESS2, + city: shippingDetails.CITY, + state: shippingDetails.STATE, + zip: shippingDetails.ZIP, + country: shippingDetails.COUNTRY + } + } + + if (shippingDetails.contact) { + shippingData.contact = { + firstName: shippingDetails.FIRSTNAME, + lastName: shippingDetails.LASTNAME, + middleInitial: shippingDetails.MIDDLEINITIAL, + title: shippingDetails.TITLE, + phone: shippingDetails.PHONENO, + mobile: shippingDetails.MOBILENO, + fax: shippingDetails.FAXNO, + email: shippingDetails.EMAILADDRESS, + refNumber: shippingDetails.REFNO, + } + } + + return shippingData; + } + + saveShippingDetails(headerid: number, shippingData: Shipping): Observable { + + const shippingDetails = { + P_HEADERID: headerid, + P_SHIPTOTYPE: shippingData.shipTo, + P_DELIVERYTYPE: shippingData.deliveryType, + P_DELIVERYMETHOD: shippingData.deliveryMethod, + P_CUSTCOURIERNO: shippingData.courierAccount, + P_PAYMENTMETHOD: shippingData.paymentMethod, + P_NOTES: shippingData.notes, + + P_FORMOFSECURITY: shippingData.needsBond ? 'Y' : 'N', + P_INSPROTECTION: shippingData.needsInsurance ? 'Y' : 'N', + P_LDIPROTECTION: shippingData.needsLostDocProtection ? 'Y' : 'N', + + address1: shippingData.address?.address1 || '', + address2: shippingData.address?.address2 || null, + city: shippingData.address?.city || '', + state: shippingData.address?.state || '', + zip: shippingData.address?.zip || '', + country: shippingData.address?.country || '', + + firstName: shippingData.contact?.firstName || '', + lastName: shippingData.contact?.lastName || '', + middleInitial: shippingData.contact?.middleInitial || null, + title: shippingData.contact?.title || '', + phone: shippingData.contact?.phone || '', + mobile: shippingData.contact?.mobile || '', + fax: shippingData.contact?.fax || null, + emailAddress: shippingData.contact?.email || '', + refNo: shippingData.contact?.refNumber || '', + P_USERID: this.userService.getUser() + } + + return this.http.patch(`${this.apiUrl}/${this.apiDb}/UpdateShippingDetails`, shippingDetails); + } } diff --git a/src/app/core/services/common/common.service.ts b/src/app/core/services/common/common.service.ts index e0c45ef..3e93f0e 100644 --- a/src/app/core/services/common/common.service.ts +++ b/src/app/core/services/common/common.service.ts @@ -13,6 +13,8 @@ import { CargoSurety } from '../../models/cargo-surety'; import { CarnetStatus } from '../../models/carnet-status'; import { Country } from '../../models/country'; import { UnitOfMeasure } from '../../models/unitofmeasure'; +import { DeliveryMethod } from '../../models/delivery-method'; +import { PaymentType } from '../../models/payment-type'; @Injectable({ providedIn: 'root' @@ -156,7 +158,30 @@ export class CommonService { name: item.PARAMDESC, id: item.PARAMID, value: item.PARAMVALUE, - color: item.ADDLPARAMVALUE1, + })) + ) + ); + } + + getDeliveryMethods(_spid: number = 0): Observable { + return this.http.get(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=007&P_SPID=0`).pipe( + map((response) => + response.map((item) => ({ + name: item.PARAMDESC, + id: item.PARAMID, + value: item.PARAMVALUE, + })) + ) + ); + } + + getPaymentTypes(_spid: number = 0): Observable { + return this.http.get(`${this.apiUrl}/${this.apiDb}/GetParamValues?P_PARAMTYPE=008&P_SPID=0`).pipe( + map((response) => + response.map((item) => ({ + name: item.PARAMDESC, + id: item.PARAMID, + value: item.PARAMVALUE, })) ) ); diff --git a/src/app/home/home.component.html b/src/app/home/home.component.html index 90acb7e..0a392d6 100644 --- a/src/app/home/home.component.html +++ b/src/app/home/home.component.html @@ -11,7 +11,7 @@
- Do you need a new carnet? + Do you need a new carnet? Yes No @@ -19,7 +19,7 @@
- Do you need additional sets? + Do you need additional sets? Yes No @@ -27,7 +27,8 @@
- Did you lose your carnet? Are you looking for duplicates? + Did you lose your carnet? Are you looking for + duplicates? Yes No @@ -35,7 +36,7 @@
- Do you need to extend the carnet? + Do you need to extend the carnet? Yes No diff --git a/src/app/home/home.component.scss b/src/app/home/home.component.scss index 48a842b..7dd0a34 100644 --- a/src/app/home/home.component.scss +++ b/src/app/home/home.component.scss @@ -21,9 +21,12 @@ .questions-section { flex: 0 0 70%; - padding: 24px 0px; - margin-bottom: 24px; + padding: 12px 16px; + margin-bottom: 46px; margin-left: 1rem; + margin-top: 24px; + background: var(--mat-sys-surface); + border-radius: 4px; .question-row { margin-bottom: 1rem; @@ -32,6 +35,15 @@ display: flex; gap: 8px; + .question { + color: var(--mat-sys-on-surface); + font-family: var(--mat-sys-body-medium-font); + line-height: var(--mat-sys-body-medium-line-height); + font-size: var(--mat-sys-body-medium-size); + letter-spacing: var(--mat-sys-body-medium-tracking); + font-weight: var(--mat-sys-medium-font-weight); + } + mat-radio-button { margin-top: -5px; }
Value{{ formatDecimalDisplay(item.value, 2) }}{{ formatDecimalDisplay(item.value, 2) | currency}}