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 +