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 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
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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'}}
+
-
+
+