This commit is contained in:
Kallesh B S 2025-07-14 19:45:47 +05:30
commit d28be8dc01
49 changed files with 2326 additions and 209 deletions

104
package-lock.json generated
View File

@ -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",

View File

@ -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": {

View File

@ -1,9 +1,12 @@
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/add-carnet', renderMode: RenderMode.Client },
{ path: ':appId/edit-carnet/:headerid', renderMode: RenderMode.Client },
{
path: '**',
renderMode: RenderMode.Prerender

View File

@ -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',

View File

@ -27,7 +27,8 @@
<mat-step *ngIf="store.applicationType() === 'new'" [completed]="store.stepsCompleted().goodsSection"
[editable]="store.stepsCompleted().applicationDetail">
<ng-template matStepLabel>Goods Section</ng-template>
<app-goods (completed)="onGoodsSectionSaved($event)" [headerid]="store.headerId()">
<app-goods (completed)="onGoodsSectionSaved($event)" [headerid]="store.headerId()"
[userPreferences]="userPreferences">
</app-goods>
</mat-step>
@ -40,34 +41,11 @@
</app-travel-plan>
</mat-step>
<!-- Insurance Step -->
<mat-step
*ngIf="store.applicationType() === 'new' || store.applicationType() === 'extend' || store.applicationType() === 'additional'"
[completed]="store.stepsCompleted().insurance" [editable]="store.stepsCompleted().applicationDetail">
<ng-template matStepLabel>Insurance</ng-template>
<app-insurance (completed)="onInsuranceSaved($event)" [headerid]="store.headerId()">
</app-insurance>
</mat-step>
<!-- Shipping Step -->
<!-- Shipping & Payment Step -->
<mat-step [completed]="store.stepsCompleted().shipping" [editable]="store.stepsCompleted().applicationDetail">
<ng-template matStepLabel>Shipping</ng-template>
<ng-template matStepLabel>Shipping & Payment</ng-template>
<app-shipping (completed)="onShippingSaved($event)" [headerid]="store.headerId()">
</app-shipping>
</mat-step>
<!-- Delivery Method Step -->
<mat-step [completed]="store.stepsCompleted().deliveryMethod" [editable]="store.stepsCompleted().applicationDetail">
<ng-template matStepLabel>Delivery Method</ng-template>
<app-delivery (completed)="onDeliveryMethodSaved($event)" [headerid]="store.headerId()">
</app-delivery>
</mat-step>
<!-- Payment Step -->
<mat-step [completed]="store.stepsCompleted().payment" [editable]="store.stepsCompleted().applicationDetail">
<ng-template matStepLabel>Payment</ng-template>
<app-payment (completed)="onPaymentSaved($event)" [headerid]="store.headerId()">
</app-payment>
</mat-step>
</mat-stepper>
</div>

View File

@ -7,22 +7,22 @@ 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 { DeliveryComponent } from '../delivery/delivery.component';
import { SearchComponent } from '../../holder/search/search.component';
import { CarnetStateStore } from '../../core/services/carnet/carnet-application.service';
import { ActivatedRoute, NavigationStart, Router } from '@angular/router';
import { filter, Subscription } from 'rxjs';
import { UserService } from '../../core/services/common/user.service';
import { UserPreferences } from '../../core/models/user-preference';
import { UserPreferencesService } from '../../core/services/user-preference.service';
@Component({
selector: 'app-add-carnet',
imports: [AngularMaterialModule, CommonModule, ReactiveFormsModule,
ApplicationComponent, GoodsComponent, TravelPlanComponent, InsuranceComponent,
ShippingComponent, DeliveryComponent, PaymentComponent, SearchComponent],
ApplicationComponent, GoodsComponent, TravelPlanComponent,
ShippingComponent, SearchComponent],
templateUrl: './add-carnet.component.html',
styleUrl: './add-carnet.component.scss'
})
@ -31,6 +31,7 @@ export class AddCarnetComponent {
private router = inject(Router);
private route = inject(ActivatedRoute);
private navigationSub!: Subscription;
userPreferences: UserPreferences;
private previousUrl: string = '';
@ -41,7 +42,10 @@ export class AddCarnetComponent {
constructor(
private readonly userService: UserService,
) { }
userPrefenceService: UserPreferencesService,
) {
this.userPreferences = userPrefenceService.getPreferences();
}
ngOnInit() {

View File

@ -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<boolean>();
}

View File

@ -23,7 +23,8 @@
<mat-step *ngIf="applicationType === 'new'" [completed]="stepsCompleted.goodsSection"
[editable]="stepsCompleted.applicationDetail">
<ng-template matStepLabel>Goods Section</ng-template>
<app-goods (completed)="onGoodsSectionSaved($event)" [headerid]="headerid">
<app-goods (completed)="onGoodsSectionSaved($event)" [headerid]="headerid" [isEditMode]="isEditMode"
[userPreferences]="userPreferences">
</app-goods>
</mat-step>
@ -35,33 +36,11 @@
</app-travel-plan>
</mat-step>
<!-- Insurance Step -->
<mat-step *ngIf="applicationType === 'new' || applicationType === 'extend' || applicationType === 'additional'"
[completed]="stepsCompleted.insurance" [editable]="stepsCompleted.applicationDetail">
<ng-template matStepLabel>Insurance</ng-template>
<app-insurance (completed)="onInsuranceSaved($event)" [headerid]="headerid">
</app-insurance>
</mat-step>
<!-- Shipping Step -->
<!-- Shipping & Payment Step -->
<mat-step [completed]="stepsCompleted.shipping" [editable]="stepsCompleted.applicationDetail">
<ng-template matStepLabel>Shipping</ng-template>
<ng-template matStepLabel>Shipping & Payment</ng-template>
<app-shipping (completed)="onShippingSaved($event)" [headerid]="headerid">
</app-shipping>
</mat-step>
<!-- Delivery Method Step -->
<mat-step [completed]="stepsCompleted.deliveryMethod" [editable]="stepsCompleted.applicationDetail">
<ng-template matStepLabel>Delivery Method</ng-template>
<app-delivery (completed)="onDeliveryMethodSaved($event)" [headerid]="headerid">
</app-delivery>
</mat-step>
<!-- Payment Step -->
<mat-step [completed]="stepsCompleted.payment" [editable]="stepsCompleted.applicationDetail">
<ng-template matStepLabel>Payment</ng-template>
<app-payment (completed)="onPaymentSaved($event)" [headerid]="headerid">
</app-payment>
</mat-step>
</mat-stepper>
</div>

View File

@ -7,17 +7,15 @@ 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';
import { UserPreferencesService } from '../../core/services/user-preference.service';
@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'
})
@ -27,6 +25,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 = {
@ -34,14 +33,15 @@ export class EditCarnetComponent {
holderSelection: false,
goodsSection: false,
travelPlan: false,
insurance: false,
shipping: false,
deliveryMethod: false,
payment: false
shipping: false
};
private route = inject(ActivatedRoute);
constructor(userPrefenceService: UserPreferencesService) {
this.userPreferences = userPrefenceService.getPreferences();
}
ngOnInit(): void {
const idParam = this.route.snapshot.paramMap.get('headerid');
this.headerid = idParam ? parseInt(idParam, 10) : 0;
@ -63,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;
}
}

View File

@ -1 +1,235 @@
<p>goods works!</p>
<div class="goods-container">
<form [formGroup]="goodsForm">
<!-- Purpose Section -->
<div class="form-section">
<div class="checkbox-group">
<mat-label>Goods to be imported as </mat-label>
<mat-checkbox formControlName="commercialSample">Commercial Sample</mat-checkbox>
<mat-checkbox formControlName="professionalEquipment">Professional Equipment</mat-checkbox>
<mat-checkbox formControlName="exhibitionsFair">Exhibitions and Fair</mat-checkbox>
<mat-error *ngIf="goodsForm.errors?.['atLeastOneRequired']">
At least one must be selected
</mat-error>
</div>
</div>
<div class="form-section">
<div class="checkbox-group">
<mat-checkbox labelPosition="before" formControlName="roadVehiclesUsed">Road Vehicles
used?</mat-checkbox>
<mat-checkbox labelPosition="before" formControlName="horseUsed">Horse used?</mat-checkbox>
</div>
</div>
<!-- Authorized Representatives Section -->
<div class="form-section">
<mat-form-field appearance="outline" class="full-width">
<mat-label>Authorized Representative(s)</mat-label>
<textarea matInput formControlName="authorizedRepresentatives" rows="3"></textarea>
<mat-error *ngIf="goodsForm.get('authorizedRepresentatives')?.errors?.['required']">
Authorized Representative(s) is required
</mat-error>
</mat-form-field>
</div>
</form>
<div class="table-actions">
<h3>Goods Items</h3>
<div class="actions">
<button mat-raised-button color="primary" type="button" (click)="addNewItem()">
<mat-icon>add</mat-icon> Add Item
</button>
<div class="upload-section">
<input type="file" accept=".xlsx,.xls,.csv" (change)="fileUpload($event)" hidden #fileInput>
<button mat-raised-button type="button" (click)="fileInput.click()" [disabled]="isProcessing">
<mat-icon>upload</mat-icon> Upload
</button>
</div>
</div>
</div>
<div class="table-container mat-elevation-z8">
<div class="loading-shade" *ngIf="isLoading">
<mat-spinner diameter="50"></mat-spinner>
</div>
<table mat-table [dataSource]="dataSource" matSort>
<!-- Item Number Column -->
<ng-container matColumnDef="itemNumber">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Item Number</th>
<td mat-cell *matCellDef="let item">{{ item.itemNumber }}</td>
</ng-container>
<!-- Description Column -->
<ng-container matColumnDef="description">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Description</th>
<td mat-cell *matCellDef="let item">{{ item.description }}</td>
</ng-container>
<!-- Pieces Column -->
<ng-container matColumnDef="pieces">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Pieces</th>
<td mat-cell *matCellDef="let item">{{ item.pieces }}</td>
</ng-container>
<!-- Weight Column -->
<ng-container matColumnDef="weight">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Weight</th>
<td mat-cell *matCellDef="let item">{{ formatDecimalDisplay(item.weight, 4) }}</td>
</ng-container>
<!-- Unit of Measure Column -->
<ng-container matColumnDef="unitOfMeasure">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Unit Of Measure</th>
<td mat-cell *matCellDef="let item">{{ getUnitOfMeasureLabel(item.unitOfMeasure) }}</td>
</ng-container>
<!-- Value Column -->
<ng-container matColumnDef="value">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Value</th>
<td mat-cell *matCellDef="let item">{{ formatDecimalDisplay(item.value, 2) | currency}}</td>
</ng-container>
<!-- Country of Origin Column -->
<ng-container matColumnDef="countryOfOrigin">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Country of Origin</th>
<td mat-cell *matCellDef="let item">{{ getCountryLabel(item.countryOfOrigin) }}</td>
</ng-container>
<!-- Actions Column -->
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>Actions</th>
<td mat-cell *matCellDef="let item; let i = index">
<button mat-icon-button color="primary" (click)="editItem(item)" matTooltip="Edit">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button color="warn" (click)="deleteItem(item)" matTooltip="Delete">
<mat-icon>delete</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
<tr matNoDataRow *matNoDataRow>
<td [colSpan]="displayedColumns.length" class="no-data-message">
<mat-icon>info</mat-icon>
<span>No items added</span>
</td>
</tr>
</table>
<mat-paginator [length]="dataSource.data.length" [pageSizeOptions]="[userPreferences.pageSize!]"
[hidePageSize]="true" showFirstLastButtons></mat-paginator>
</div>
<!-- Item Form -->
<div class="form-container" *ngIf="showItemForm">
<form [formGroup]="itemForm" (ngSubmit)="saveItem()">
<div class="form-header">
<h3>{{ isEditing ? 'Edit Item' : 'Add New Item' }}</h3>
</div>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Item Number</mat-label>
<input matInput formControlName="itemNumber" required>
<mat-error *ngIf="itemForm.get('itemNumber')?.errors?.['required']">
Item Number is required
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="description">
<mat-label>Description</mat-label>
<input matInput formControlName="description" required>
<mat-error *ngIf="itemForm.get('description')?.errors?.['required']">
Description is required
</mat-error>
</mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Pieces</mat-label>
<input matInput type="number" formControlName="pieces" required>
<mat-error *ngIf="itemForm.get('pieces')?.errors?.['required']">
Pieces is required
</mat-error>
<mat-error *ngIf="itemForm.get('pieces')?.errors?.['min']">
Must be at least 1
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Weight</mat-label>
<input matInput type="number" formControlName="weight" required>
<mat-error *ngIf="itemForm.get('weight')?.errors?.['required']">
Weight is required
</mat-error>
<mat-error *ngIf="itemForm.get('weight')?.errors?.['min']">
Must be positive
</mat-error>
<mat-error *ngIf="itemForm.get('weight')?.errors?.['pattern']">
Maximum 4 decimal places allowed
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Unit of Measure</mat-label>
<mat-select formControlName="unitOfMeasure" required>
<mat-option *ngFor="let unit of unitsOfMeasures" [value]="unit.value">
{{ unit.name }}
</mat-option>
</mat-select>
<mat-error *ngIf="itemForm.get('unitOfMeasure')?.errors?.['required']">
Unit of measure is required
</mat-error>
</mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Value</mat-label>
<input matInput type="number" formControlName="value" required>
<mat-error *ngIf="itemForm.get('value')?.errors?.['required']">
Value is required
</mat-error>
<mat-error *ngIf="itemForm.get('value')?.errors?.['min']">
Must be positive
</mat-error>
<mat-error *ngIf="itemForm.get('value')?.errors?.['pattern']">
Maximum 2 decimal places allowed
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="country">
<mat-label>Country of Origin</mat-label>
<mat-select formControlName="countryOfOrigin" required>
<mat-option *ngFor="let country of countries" [value]="country.value">
{{ country.name }}
</mat-option>
</mat-select>
<mat-error *ngIf="itemForm.get('countryOfOrigin')?.errors?.['required']">
Country of origin is required
</mat-error>
</mat-form-field>
</div>
<div class="form-actions">
<button mat-button type="button" (click)="cancelEdit()">Cancel</button>
<button mat-raised-button color="primary" type="submit" [disabled]="itemForm.invalid">
Save
</button>
</div>
</form>
</div>
<div *ngIf="!showItemForm" class="form-actions">
<button mat-raised-button color="primary" (click)="onSubmit()" [disabled]="goodsForm.invalid">
Save
</button>
</div>
</div>

View File

@ -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;
}
}

View File

@ -1,14 +1,451 @@
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 = {};
@Input() isEditMode: boolean = false;
@Output() completed = new EventEmitter<boolean>();
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort;
// Table configuration
displayedColumns: string[] = ['itemNumber', 'description', 'pieces', 'weight', 'unitOfMeasure', 'value', 'countryOfOrigin', 'actions'];
dataSource = new MatTableDataSource<any>();
// Form controls
goodsForm: FormGroup;
itemForm: FormGroup;
// UI state
isEditing = false;
currentItem: GoodsItem | null = null;
isLoading = false;
showItemForm = false;
fileToUpload: File | null = null;
isProcessing = false;
// Data
countries: Country[] = [];
unitsOfMeasures: UnitOfMeasure[] = [];
private destroy$ = new Subject<void>();
private fb = inject(FormBuilder);
private goodsService = inject(GoodsService);
private notificationService = inject(NotificationService);
private dialog = inject(MatDialog);
private errorHandler = inject(ApiErrorHandlerService);
private commonService = inject(CommonService);
constructor() {
// Main form for goods section
this.goodsForm = this.fb.group({
commercialSample: [false],
professionalEquipment: [false],
exhibitionsFair: [false],
roadVehiclesUsed: [false],
horseUsed: [false],
authorizedRepresentatives: ['', Validators.required]
}, { validator: this.atLeastOneRequired }
);
// Form for individual items
this.itemForm = this.fb.group({
itemNumber: ['', Validators.required],
description: ['', Validators.required],
pieces: [0, [Validators.required, Validators.min(1)]],
weight: [0, [Validators.required, Validators.min(1), Validators.pattern(/^\d+(\.\d{1,4})?$/)]],
unitOfMeasure: ['', Validators.required],
value: [0, [Validators.required, Validators.min(1), Validators.pattern(/^\d+(\.\d{1,2})?$/)]],
countryOfOrigin: ['', Validators.required]
});
}
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();
this.completed.emit(this.goodsForm.valid && this.dataSource.data.length > 0);
}
}
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<void> {
if (file) {
const validTypes = [
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'text/csv'
];
if (!validTypes.includes(file.type)) {
this.notificationService.showError('Please upload only Excel (XLSX, XLS) or CSV files');
return;
}
this.fileToUpload = file;
if (!this.fileToUpload || !this.headerid) return;
this.isProcessing = true;
try {
const data = await this.readExcelFile(this.fileToUpload);
const items = this.parseExcelData(data);
if (items.length === 0) {
this.notificationService.showError('No valid items found in the file');
this.isProcessing = false;
return;
}
// items.map(item => {
// item.countryOfOrigin = this.getCountryValue(item.countryOfOrigin);
// item.unitOfMeasure = this.getUnitOfMeasureValue(item.unitOfMeasure);
// });
this.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<any[]> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e: any) => {
try {
const data = new Uint8Array(e.target.result);
const workbook = XLSX.read(data, { type: 'array' });
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
const jsonData = XLSX.utils.sheet_to_json(worksheet);
resolve(jsonData);
} catch (error) {
reject(error);
}
};
reader.onerror = (error) => reject(error);
reader.readAsArrayBuffer(file);
});
}
// Parse Excel data into our format
private parseExcelData(data: any[]): any[] {
return data.map(row => ({
itemNumber: row['Item Number'] || row['itemNumber'] || '',
description: row['Description'] || row['description'] || '',
pieces: Number(row['Pieces'] || row['pieces'] || 0),
weight: Number(row['Weight'] || row['weight'] || 0),
unitOfMeasure: row['Unit of Measure'] || row['unit of measure'] || row['unitofmeasure'] || row['UnitOfMeasure'],
value: Number(row['Value'] || row['value'] || 0),
countryOfOrigin: row['Country Of Origin'] || row['Country of Origin'] || row['country of origin'] || row['countryoforigin'] || row['CountryOfOrigin'] || ''
})).filter(item =>
item.itemNumber &&
item.description &&
item.pieces > 0
);
}
// Save all goods data
onSubmit(): void {
if (this.goodsForm.invalid) {
this.goodsForm.markAllAsTouched();
return;
}
const formData = this.goodsForm.value;
this.isLoading = true;
this.goodsService.saveGoodsData(this.headerid, formData).subscribe({
next: () => {
this.notificationService.showSuccess('Goods information saved successfully');
this.completed.emit(this.goodsForm.valid && this.dataSource.data.length > 0);
},
error: (error) => {
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to save goods information');
this.notificationService.showError(errorMessage);
console.error('Error saving goods:', error);
},
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+$/, '')}`;
}
}

View File

@ -1 +0,0 @@
<p>insurance works!</p>

View File

@ -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<boolean>();
}

View File

@ -1 +0,0 @@
<p>payment works!</p>

View File

@ -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<boolean>();
}

View File

@ -1 +1,302 @@
<p>shipping works!</p>
<div class="shipping-container">
<form [formGroup]="shippingForm" (ngSubmit)="onSubmit()">
<!-- Insurance Section -->
<div class="section">
<h3>Insurance & Bond</h3>
<div class="checkbox-group">
<mat-checkbox labelPosition="before" formControlName="needsBond">Do you need Bond from
us?</mat-checkbox>
<mat-checkbox labelPosition="before" formControlName="needsInsurance">Do you need insurance for your
goods?</mat-checkbox>
<mat-checkbox labelPosition="before" formControlName="needsLostDocProtection">Do you need Lost document
protection?</mat-checkbox>
</div>
</div>
<!-- Shipping Section -->
<div class="section">
<h3>Ship To</h3>
<mat-radio-group formControlName="shipTo" class="radio-group" (change)="onShipToChange($event)">
<mat-radio-button value="preparer">Preparer</mat-radio-button>
<mat-radio-button value="holder">Holder</mat-radio-button>
<mat-radio-button value="thirdParty">3rd Party</mat-radio-button>
</mat-radio-group>
<mat-card appearance="outlined" *ngIf="!showAddressForm && !showContactForm">
<mat-card-content>
<p>{{getAddressLabel()}}</p>
</mat-card-content>
<mat-card-actions>
<button mat-button (click)="editAddressForm()">Edit</button>
</mat-card-actions>
</mat-card>
<div *ngIf="showAddressForm" class="address-form" formGroupName="address">
<h4>Shipping Address</h4>
<!-- Address Information -->
<div class="form-row">
<mat-form-field appearance="outline" class="address1">
<mat-label>Address Line 1</mat-label>
<input matInput formControlName="address1" required>
<mat-error *ngIf="shippingForm.get('address.address1')?.errors?.['required']">
Address is required
</mat-error>
<mat-error *ngIf="shippingForm.get('address.address1')?.errors?.['maxlength']">
Maximum 100 characters allowed
</mat-error>
</mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline" class="address2">
<mat-label>Address Line 2 (Optional)</mat-label>
<input matInput formControlName="address2">
<mat-error *ngIf="shippingForm.get('address.address2')?.errors?.['maxlength']">
Maximum 100 characters allowed
</mat-error>
</mat-form-field>
</div>
<!-- Location Information -->
<div class="form-row">
<mat-form-field appearance="outline" class="city">
<mat-label>City</mat-label>
<input matInput formControlName="city" required>
<mat-error *ngIf="shippingForm.get('address.city')?.errors?.['required']">
City is required
</mat-error>
<mat-error *ngIf="shippingForm.get('address.city')?.errors?.['maxlength']">
Maximum 50 characters allowed
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="country">
<mat-label>Country</mat-label>
<mat-select formControlName="country" required
(selectionChange)="onCountryChange($event.value)">
<mat-option *ngFor="let country of countries" [value]="country.value">
{{ country.name }}
</mat-option>
</mat-select>
<mat-error *ngIf="shippingForm.get('address.country')?.errors?.['required']">
Country is required
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="state">
<mat-label>State/Province</mat-label>
<mat-select formControlName="state" required>
<mat-option *ngFor="let state of states" [value]="state.value">
{{ state.name }}
</mat-option>
</mat-select>
<mat-error *ngIf="shippingForm.get('address.state')?.errors?.['required']">
State is required
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="zip">
<mat-label>ZIP/Postal Code</mat-label>
<input matInput formControlName="zip" required>
<mat-error *ngIf="shippingForm.get('address.zip')?.errors?.['required']">
ZIP/Postal code is required
</mat-error>
<mat-error
*ngIf="shippingForm.get('address.country')?.value === 'US' && shippingForm.get('address.zip')?.touched && shippingForm.get('address.zip')?.errors?.['invalidUSZip']">
Please enter a valid 5-digit US ZIP code
</mat-error>
<mat-error
*ngIf="shippingForm.get('address.country')?.value === 'CA' && shippingForm.get('address.zip')?.touched && shippingForm.get('address.zip')?.errors?.['invalidCanadaPostal']">
Please enter a valid postal code (e.g., A1B2C3)
</mat-error>
</mat-form-field>
</div>
<div class="form-actions" *ngIf="showAddressForm && shippingForm.get('shipTo')?.value !== 'thirdParty'">
<button mat-button type="button" (click)="cancelEditAddressForm()">Cancel</button>
</div>
</div>
<div *ngIf="showContactForm" class="contact-form" formGroupName="contact">
<h4>Contact Information</h4>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>First Name</mat-label>
<input matInput formControlName="firstName" required>
<mat-icon matSuffix>person</mat-icon>
<mat-error *ngIf="shippingForm.get('contact.firstName')?.errors?.['required']">
First name is required
</mat-error>
<mat-error *ngIf="shippingForm.get('contact.firstName')?.errors?.['maxlength']">
Maximum 50 characters allowed
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="small-field">
<mat-label>Middle Initial</mat-label>
<input matInput formControlName="middleInitial" maxlength="1">
<mat-error *ngIf="shippingForm.get('contact.middleInitial')?.errors?.['maxlength']">
Only 1 character allowed
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Last Name</mat-label>
<input matInput formControlName="lastName" required>
<mat-icon matSuffix>person</mat-icon>
<mat-error *ngIf="shippingForm.get('contact.lastName')?.errors?.['required']">
Last name is required
</mat-error>
<mat-error *ngIf="shippingForm.get('contact.lastName')?.errors?.['maxlength']">
Maximum 50 characters allowed
</mat-error>
</mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Title</mat-label>
<input matInput formControlName="title" required>
<mat-icon matSuffix>work</mat-icon>
<mat-error *ngIf="shippingForm.get('contact.title')?.errors?.['required']">
Title is required
</mat-error>
<mat-error *ngIf="shippingForm.get('contact.title')?.errors?.['maxlength']">
Maximum 100 characters allowed
</mat-error>
</mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Phone</mat-label>
<input matInput formControlName="phone" required>
<mat-icon matSuffix>phone</mat-icon>
<mat-error *ngIf="shippingForm.get('contact.phone')?.errors?.['required']">
Phone is required
</mat-error>
<mat-error *ngIf="shippingForm.get('contact.phone')?.errors?.['pattern']">
Please enter a valid phone number (10-15 digits)
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Mobile</mat-label>
<input matInput formControlName="mobile">
<mat-icon matSuffix>smartphone</mat-icon>
<mat-error *ngIf="shippingForm.get('contact.mobile')?.errors?.['required']">
Mobile is required
</mat-error>
<mat-error *ngIf="shippingForm.get('contact.mobile')?.errors?.['pattern']">
Please enter a valid mobile number (10-15 digits)
</mat-error>
</mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Fax</mat-label>
<input matInput formControlName="fax">
<mat-icon matSuffix>fax</mat-icon>
<mat-error *ngIf="shippingForm.get('contact.fax')?.errors?.['pattern']">
Please enter a valid fax number (10-15 digits)
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Email</mat-label>
<input matInput formControlName="email" required>
<mat-icon matSuffix>email</mat-icon>
<mat-error *ngIf="shippingForm.get('contact.email')?.errors?.['required']">
Email is required
</mat-error>
<mat-error *ngIf="shippingForm.get('contact.email')?.errors?.['email']">
Please enter a valid email address
</mat-error>
<mat-error *ngIf="shippingForm.get('contact.email')?.errors?.['maxlength']">
Maximum 100 characters allowed
</mat-error>
</mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline" class="full-width">
<mat-label>Reference Number</mat-label>
<input matInput formControlName="refNumber">
</mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline" class="full-width">
<mat-label>Notes</mat-label>
<textarea matInput formControlName="notes" rows="2"></textarea>
</mat-form-field>
</div>
</div>
</div>
<!-- Delivery Section -->
<div class="section">
<h3>Delivery Method</h3>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Delivery Type</mat-label>
<mat-select formControlName="deliveryType" required>
<mat-option *ngFor="let deliveryType of deliveryTypes" [value]="deliveryType.value">
{{ deliveryType.name }}
</mat-option>
</mat-select>
<mat-error *ngIf="shippingForm.get('deliveryType')?.errors?.['required']">
Delivery Type is required
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Delivery Method</mat-label>
<mat-select formControlName="deliveryMethod" required>
<mat-option *ngFor="let deliveryMethod of deliveryMethods" [value]="deliveryMethod.value">
{{ deliveryMethod.name }}
</mat-option>
</mat-select>
<mat-error *ngIf="shippingForm.get('deliveryMethod')?.errors?.['required']">
Delivery Method is required
</mat-error>
</mat-form-field>
</div>
<div *ngIf="shippingForm.get('deliveryMethod')?.value === 'customerCourier'" class="form-row">
<mat-form-field appearance="outline" class="full-width">
<mat-label>Courier Account Number</mat-label>
<input matInput formControlName="courierAccount">
<mat-error>Required when using customer courier</mat-error>
</mat-form-field>
</div>
</div>
<!-- Payment Section -->
<div class="section">
<h3>Payment Method</h3>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Payment Method</mat-label>
<mat-select formControlName="paymentMethod" required>
<mat-option *ngFor="let paymentType of paymentTypes" [value]="paymentType.value">
{{ paymentType.name }}
</mat-option>
</mat-select>
<mat-error *ngIf="shippingForm.get('paymentMethod')?.errors?.['required']">
Payment Method is required
</mat-error>
</mat-form-field>
</div>
<!-- <div class="form-row">
<mat-form-field appearance="outline" class="full-width">
<mat-label>Payment Notes</mat-label>
<textarea matInput formControlName="notes" rows="2"></textarea>
</mat-form-field>
</div> -->
</div>
<div class="form-actions">
<button mat-raised-button color="primary" type="submit" [disabled]="shippingForm.invalid || isLoading">
<span *ngIf="!isLoading">Save</span>
<mat-spinner *ngIf="isLoading" diameter="24"></mat-spinner>
</button>
</div>
</form>
</div>

View File

@ -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%;
}
}
}
}

View File

@ -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<boolean>();
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<void>();
// 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;
}
}
}

View File

@ -1,2 +0,0 @@
export interface Delivery {
}

View File

@ -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;
}

View File

@ -1,2 +0,0 @@
export interface Insurance {
}

View File

@ -1,2 +0,0 @@
export interface Payment {
}

View File

@ -0,0 +1,8 @@
export interface ShippingAddress {
address1: string;
address2?: string | null;
city: string;
state: string;
country: string;
zip: string;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -0,0 +1,5 @@
export interface DeliveryMethod {
name: string;
id: string;
value: string;
}

View File

@ -0,0 +1,5 @@
export interface PaymentType {
name: string;
id: string;
value: string;
}

View File

@ -0,0 +1,5 @@
export interface UnitOfMeasure {
name: string;
id: string;
value: string;
}

View File

@ -1,9 +0,0 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class DeliveryService {
constructor() { }
}

View File

@ -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<any[]>(`${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<GoodsItem[]> {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetGoodsItemstoEdit/${this.userService.getUserSpid()}/${this.userService.getUser()}/${headerid}`).pipe(
map(response => this.mapToGoodsItems(response)));
}
private mapToGoodsItems(data: any[]): GoodsItem[] {
return data.map(data => ({
id: data.ID,
itemNumber: data.ITEMNO,
description: data.GOODSDESCRIPTION,
pieces: data.NOOFPIECES,
weight: data.ITEMWEIGHT,
unitOfMeasure: data.ITEMWEIGHTUOM,
value: data.ITEMVALUE,
countryOfOrigin: data.GOODSORIGINCOUNTRY
}));
}
saveGoodsData(headerid: number, goodsData: Goods): Observable<any> {
const goods = {
P_HEADERID: headerid,
P_COMMERCIALSAMPLEFLAG: goodsData.commercialSample ? 'Y' : 'N',
P_PROFEQUIPMENTFLAG: goodsData.professionalEquipment ? 'Y' : 'N',
P_EXHIBITIONSFAIRFLAG: goodsData.exhibitionsFair ? 'Y' : 'N',
P_AUTOFLAG: goodsData.roadVehiclesUsed ? 'Y' : 'N',
P_HORSEFLAG: goodsData.horseUsed ? 'Y' : 'N',
P_AUTHREP: goodsData.authorizedRepresentatives,
}
return this.http.patch(`${this.apiUrl}/${this.apiDb}/UpdateExpGoodsAuthRep`, goods);
}
addGoodsItem(headerid: number, items: GoodsItem[]): Observable<any> {
const goodsItems: any[] = items.map(i => ({
ITEMNO: i.itemNumber,
ITEMDESCRIPTION: i.description,
NOOFPIECES: i.pieces,
ITEMWEIGHT: i.weight,
ITEMWEIGHTUOM: i.unitOfMeasure,
ITEMVALUE: i.value,
GOODSORIGINCOUNTRY: i.countryOfOrigin,
}));
const goodsItemsObj = {
P_HEADERID: headerid,
P_GLTABLE: goodsItems,
P_USERID: this.userService.getUser(),
};
return this.http.post(`${this.apiUrl}/${this.apiDb}/AddGenerallistItems`, goodsItemsObj);
}
updateGoodsItem(headerid: number, items: GoodsItem[]): Observable<any> {
const goodsItems: any[] = items.map(i => ({
ITEMNO: i.itemNumber,
ITEMDESCRIPTION: i.description,
NOOFPIECES: i.pieces,
ITEMWEIGHT: i.weight,
ITEMWEIGHTUOM: i.unitOfMeasure,
ITEMVALUE: i.value,
GOODSORIGINCOUNTRY: i.countryOfOrigin,
}));
const goodsItemsObj = {
P_HEADERID: headerid,
P_GLTABLE: goodsItems,
P_USERID: this.userService.getUser(),
};
return this.http.put(`${this.apiUrl}/${this.apiDb}/EditGenerallistItems`, goodsItemsObj);
}
deleteGoodsItem(headerid: number, item: GoodsItem): Observable<any> {
const 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 });
}
}

View File

@ -1,9 +0,0 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class PaymentService {
constructor() { }
}

View File

@ -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<any[]>(`${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<any> {
const shippingDetails = {
P_HEADERID: headerid,
P_SHIPTOTYPE: shippingData.shipTo,
P_DELIVERYTYPE: shippingData.deliveryType,
P_DELIVERYMETHOD: shippingData.deliveryMethod,
P_CUSTCOURIERNO: shippingData.courierAccount,
P_PAYMENTMETHOD: shippingData.paymentMethod,
P_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);
}
}

View File

@ -49,24 +49,32 @@ export class AuthService {
}
}
register(email: string, password: string, token: string | null): Observable<any> {
register(email: string, password: string): Observable<any> {
return this.http.post(
`${this.apiUrl}/register`,
{ P_EMAILADDR: email, P_PASSWORD: password }
);
}
forgotPassword(email: string, password: string, token: string): Observable<any> {
let headers = new HttpHeaders();
if (token) {
headers = headers.set('Authorization', `Bearer ${token}`);
}
return this.http.post(
`${this.apiUrl}/register`,
return this.http.put(
`${this.apiUrl}/forgot-password`,
{ P_EMAILADDR: email, P_PASSWORD: password },
{ headers }
);
}
forgotPassword(email: string, password: string): Observable<any> {
sendEmail(email: string, type: string): Observable<any> {
return this.http.post(
`${this.apiUrl}/forgot-password`,
{ P_EMAILADDR: email, P_PASSWORD: password }
`${this.apiUrl}/sendmail`,
{ P_TO: email, P_MAIL_TYPE: type }
);
}
}

View File

@ -12,6 +12,9 @@ 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';
import { DeliveryMethod } from '../../models/delivery-method';
import { PaymentType } from '../../models/payment-type';
@Injectable({
providedIn: 'root'
@ -148,6 +151,42 @@ export class CommonService {
);
}
getUnitOfMeasures(_spid: number = 0): Observable<UnitOfMeasure[]> {
return this.http.get<any[]>(`${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,
}))
)
);
}
getDeliveryMethods(_spid: number = 0): Observable<DeliveryMethod[]> {
return this.http.get<any[]>(`${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<PaymentType[]> {
return this.http.get<any[]>(`${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,
}))
)
);
}
formatUSDate(datetime: Date): string {
const date = new Date(datetime);
const month = String(date.getUTCMonth() + 1).padStart(2, '0');

View File

@ -1,8 +1,36 @@
<div class="forgot-password-container">
<div class="forgot-password-card">
<h3 class="subtitle">Forgot Password</h3>
<h3 class="subtitle">
{{forgotPasswordRequestCompleted ? 'Reset Password' : 'Forgot Password'}}
</h3>
<form [formGroup]="forgotPasswordForm" (ngSubmit)="onSubmit()" class="forgot-password-form">
<form *ngIf="!forgotPasswordRequestCompleted" [formGroup]="forgotPasswordRequestForm" (ngSubmit)="onRequest()"
class="forgot-password-form">
<mat-form-field appearance="outline">
<mat-label>Email</mat-label>
<input matInput formControlName="email" type="email" required>
<mat-icon matSuffix>email</mat-icon>
<mat-error *ngIf="email?.errors?.['required']">
Email is required
</mat-error>
<mat-error *ngIf="email?.errors?.['email']">
Please enter a valid email
</mat-error>
</mat-form-field>
<div class="login">
<a href="#" routerLink="/login" class="login-link">Already have an account? Login</a>
</div>
<button mat-raised-button color="primary" type="submit"
[disabled]="forgotPasswordRequestForm.invalid || isLoading">
<span *ngIf="!isLoading">Request</span>
<mat-spinner *ngIf="isLoading" diameter="20"></mat-spinner>
</button>
</form>
<form *ngIf="forgotPasswordRequestCompleted" [formGroup]="forgotPasswordForm" (ngSubmit)="onSubmit()"
class="forgot-password-form">
<mat-form-field appearance="outline">
<mat-label>Email</mat-label>
<input matInput formControlName="email" type="email" required>
@ -24,11 +52,32 @@
<mat-error *ngIf="password?.errors?.['required']">
Password is required
</mat-error>
<mat-error *ngIf="password?.errors?.['minlength']">
Password must be at least 8 characters
</mat-error>
<mat-error *ngIf="password?.errors?.['pattern']">
Password must contain at least one uppercase letter, one lowercase letter, one number, and one
special character
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Confirm Password</mat-label>
<input matInput formControlName="confirmPassword" [type]="hidePassword ? 'password' : 'text'" required>
<button type="button" mat-icon-button matSuffix (click)="hidePassword = !hidePassword">
<mat-icon>{{ hidePassword ? 'visibility_off' : 'visibility' }}</mat-icon>
</button>
<mat-error *ngIf="confirmPassword?.errors?.['required']">
Please confirm your password
</mat-error>
<mat-error *ngIf="forgotPasswordForm.errors?.['mismatch'] && confirmPassword?.touched">
Passwords do not match
</mat-error>
</mat-form-field>
<!--
<div class="login">
<a href="#" routerLink="/login" class="login-link">Already have an account? Login</a>
</div>
</div> -->
<button mat-raised-button color="primary" type="submit"
[disabled]="forgotPasswordForm.invalid || isLoading">

View File

@ -1,6 +1,6 @@
import { Component, inject } from '@angular/core';
import { FormGroup, FormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
import { Router } from '@angular/router';
import { ActivatedRoute, Router } from '@angular/router';
import { ApiErrorHandlerService } from '../core/services/common/api-error-handler.service';
import { AuthService } from '../core/services/common/auth.service';
import { NotificationService } from '../core/services/common/notification.service';
@ -15,10 +15,12 @@ import { CommonModule } from '@angular/common';
styleUrl: './forgot-password.component.scss'
})
export class ForgotPasswordComponent {
forgotPasswordRequestCompleted: boolean = false;
forgotPasswordRequestForm: FormGroup;
forgotPasswordForm: FormGroup;
isLoading = false;
hidePassword = true;
token: string | null = null;
private fb = inject(FormBuilder);
private authService = inject(AuthService);
@ -26,18 +28,57 @@ export class ForgotPasswordComponent {
private notificationService = inject(NotificationService);
private errorHandler = inject(ApiErrorHandlerService);
private themeService = inject(ThemeService);
private route = inject(ActivatedRoute);
constructor() {
this.forgotPasswordRequestForm = this.fb.group({
email: ['', [Validators.required, Validators.email]]
});
this.forgotPasswordForm = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required]]
});
password: ['', [Validators.required, Validators.minLength(8),
Validators.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]+$/)
]],
confirmPassword: ['', [Validators.required]]
}, { validator: this.passwordMatchValidator });
// // Get token from URL if present
this.token = this.route.snapshot.paramMap.get('token');
// Check if token is null or empty
if (this.token && this.token.trim() !== '') {
this.forgotPasswordRequestCompleted = true;
}
}
ngOnInit(): void {
this.themeService.setTheme('default');
}
onRequest(): void {
if (this.forgotPasswordRequestForm.invalid) {
this.forgotPasswordRequestForm.markAllAsTouched();
return;
}
this.isLoading = true;
const { email } = this.forgotPasswordRequestForm.value;
this.authService.sendEmail(email, 'FORGOT_PASSWORD').subscribe({
next: () => {
this.notificationService.showSuccess('A mail has been sent successfully to your email to setup a new password.');
this.isLoading = false;
},
error: (error) => {
this.isLoading = false;
let errorMessage = this.errorHandler.handleApiError(error, `Request failed. Please try again.`);
this.notificationService.showError(errorMessage);
console.error('Forgot password request failed:', error);
}
});
}
onSubmit(): void {
if (this.forgotPasswordForm.invalid) {
this.forgotPasswordForm.markAllAsTouched();
@ -47,20 +88,30 @@ export class ForgotPasswordComponent {
this.isLoading = true;
const { email, password } = this.forgotPasswordForm.value;
this.authService.forgotPassword(email, password).subscribe({
this.authService.forgotPassword(email, password, this.token!).subscribe({
next: () => {
this.notificationService.showSuccess('Request successful');
this.notificationService.showSuccess('Password reset successfully');
this.router.navigate(['/login']);
},
error: (error) => {
this.isLoading = false;
let errorMessage = this.errorHandler.handleApiError(error, `Request failed. Please try again.`);
let errorMessage = this.errorHandler.handleApiError(error, `Password request failed. Please try again.`);
this.notificationService.showError(errorMessage);
console.error('Forgot password request failed:', error);
}
});
}
private passwordMatchValidator(formGroup: FormGroup) {
const password = formGroup.get('password')?.value;
const confirmPassword = formGroup.get('confirmPassword')?.value;
return password === confirmPassword ? null : { mismatch: true };
}
get emailRequest() {
return this.forgotPasswordRequestForm.get('email');
}
get email() {
return this.forgotPasswordForm.get('email');
}
@ -68,4 +119,8 @@ export class ForgotPasswordComponent {
get password() {
return this.forgotPasswordForm.get('password');
}
get confirmPassword() {
return this.forgotPasswordForm.get('confirmPassword');
}
}

View File

@ -11,7 +11,7 @@
<div class="questions-section">
<div class="question-row">
<mat-radio-group (change)="newCarnet($event)">
<mat-label>Do you need a new carnet?</mat-label>
<mat-label class="question">Do you need a new carnet?</mat-label>
<mat-radio-button [value]="true">Yes</mat-radio-button>
<mat-radio-button [value]="false">No</mat-radio-button>
</mat-radio-group>
@ -19,7 +19,7 @@
<div class="question-row">
<mat-radio-group (change)="additionalSets($event)">
<mat-label>Do you need additional sets?</mat-label>
<mat-label class="question">Do you need additional sets?</mat-label>
<mat-radio-button [value]="true">Yes</mat-radio-button>
<mat-radio-button [value]="false">No</mat-radio-button>
</mat-radio-group>
@ -27,7 +27,8 @@
<div class="question-row">
<mat-radio-group (change)="duplicateCarnet($event)">
<mat-label>Did you lose your carnet? Are you looking for duplicates?</mat-label>
<mat-label class="question">Did you lose your carnet? Are you looking for
duplicates?</mat-label>
<mat-radio-button [value]="true">Yes</mat-radio-button>
<mat-radio-button [value]="false">No</mat-radio-button>
</mat-radio-group>
@ -35,7 +36,7 @@
<div class="question-row">
<mat-radio-group (change)="extendCarnet($event)">
<mat-label>Do you need to extend the carnet?</mat-label>
<mat-label class="question">Do you need to extend the carnet?</mat-label>
<mat-radio-button [value]="true">Yes</mat-radio-button>
<mat-radio-button [value]="false">No</mat-radio-button>
</mat-radio-group>
@ -49,6 +50,13 @@
<mat-spinner diameter="50"></mat-spinner>
</div>
<div class="table-actions">
<button mat-raised-button color="accent" (click)="exportData()" *ngIf="dataSource.data.length > 0"
matTooltip="Export to Excel" [disabled]="dataSource.data.length === 0">
<mat-icon>download</mat-icon> Export
</button>
</div>
<div class="table-container mat-elevation-z8" *ngIf="showTable">
<table mat-table [dataSource]="dataSource" matSort>
<ng-container matColumnDef="applicationName">
@ -111,8 +119,9 @@
<th mat-header-cell *matHeaderCellDef>Actions</th>
<td mat-cell *matCellDef="let item">
<button mat-icon-button color="primary" (click)="editCarnet(item)"
*ngIf="getCarnetStatusLabel(item.carnetStatus) === 'Pending'"
[hidden]="getCarnetStatusLabel(item.carnetStatus) === 'Pending'" matTooltip="Edit">
*ngIf="getCarnetStatusLabel(item.carnetStatus) === 'Not Submitted'"
[hidden]="getCarnetStatusLabel(item.carnetStatus) !== 'Not Submitted'"
matTooltip="Edit">
<mat-icon>edit</mat-icon>
</button>
</td>

View File

@ -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;
}
@ -40,6 +52,15 @@
}
}
.table-actions {
display: flex;
justify-content: end;
button {
margin-bottom: 16px;
}
}
.table-container {
position: relative;
overflow: auto;

View File

@ -16,6 +16,7 @@ import { NotificationService } from '../core/services/common/notification.servic
import { NavigationService } from '../core/services/common/navigation.service';
import { MatRadioChange } from '@angular/material/radio';
import { CarnetStateStore } from '../core/services/carnet/carnet-application.service';
import * as XLSX from 'xlsx';
@Component({
selector: 'app-home',
@ -107,6 +108,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 {
@ -117,7 +152,7 @@ export class HomeComponent {
newCarnet(event: MatRadioChange): void {
if (event.value) {
this.store.resetState();
this.navigateTo('add-carnet');
this.navigateTo(['add-carnet']);
}
}
@ -132,13 +167,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);
}
}

View File

@ -21,8 +21,12 @@
</mat-error>
</mat-form-field>
<div class="forgot-password">
<a href="#" routerLink="/forgot-password" class="forgot-link">Forgot your password?</a>
<div class="additional-options">
<a href="#" routerLink="/forgot-password" class="additional-options-link">Forgot your password?</a>
</div>
<div class="additional-options">
<a href="#" routerLink="/register" class="additional-options-link">New User? Register</a>
</div>
<button mat-raised-button color="primary" type="submit" [disabled]="!loginForm.valid">

View File

@ -29,11 +29,11 @@
width: 100%;
}
.forgot-password {
.additional-options {
text-align: right;
margin-top: -1rem;
.forgot-link {
.additional-options-link {
color: #666;
text-decoration: none;
font-size: 0.875rem;

View File

@ -2,16 +2,7 @@
<div class="registration-card">
<h3 class="subtitle">User Registration</h3>
<!-- Show error message if token is invalid -->
<div *ngIf="tokenError" class="token-error">
<mat-error>
<mat-icon>error_outline</mat-icon>
<span>Invalid registration link. Please request a new registration link from your
administrator.</span>
</mat-error>
</div>
<form *ngIf="!tokenError" [formGroup]="registrationForm" (ngSubmit)="onSubmit()" class="registration-form">
<form [formGroup]="registrationForm" (ngSubmit)="onSubmit()" class="registration-form">
<mat-form-field appearance="outline">
<mat-label>Email</mat-label>
<input matInput formControlName="email" type="email" required>
@ -33,6 +24,27 @@
<mat-error *ngIf="password?.errors?.['required']">
Password is required
</mat-error>
<mat-error *ngIf="password?.errors?.['minlength']">
Password must be at least 8 characters
</mat-error>
<mat-error *ngIf="password?.errors?.['pattern']">
Password must contain at least one uppercase letter, one lowercase letter, one number, and one
special character
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Confirm Password</mat-label>
<input matInput formControlName="confirmPassword" [type]="hidePassword ? 'password' : 'text'" required>
<button type="button" mat-icon-button matSuffix (click)="hidePassword = !hidePassword">
<mat-icon>{{ hidePassword ? 'visibility_off' : 'visibility' }}</mat-icon>
</button>
<mat-error *ngIf="confirmPassword?.errors?.['required']">
Please confirm your password
</mat-error>
<mat-error *ngIf="registrationForm.errors?.['mismatch'] && confirmPassword?.touched">
Passwords do not match
</mat-error>
</mat-form-field>
<div class="login">

View File

@ -19,8 +19,9 @@ export class RegisterComponent {
registrationForm: FormGroup;
isLoading = false;
hidePassword = true;
tokenError: boolean = false;
token: string | null = null;
// tokenError: boolean = false;
//token: string | null = null;
private fb = inject(FormBuilder);
private authService = inject(AuthService);
@ -33,16 +34,19 @@ export class RegisterComponent {
constructor() {
this.registrationForm = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required]]
});
password: ['', [Validators.required, Validators.minLength(8),
Validators.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]+$/)
]],
confirmPassword: ['', [Validators.required]]
}, { validator: this.passwordMatchValidator });
// Get token from URL if present
this.token = this.route.snapshot.paramMap.get('token');
// // Get token from URL if present
// this.token = this.route.snapshot.paramMap.get('token');
// Check if token is null or empty
if (!this.token || this.token.trim() === '') {
this.tokenError = true;
}
// // Check if token is null or empty
// if (!this.token || this.token.trim() === '') {
// this.tokenError = true;
// }
}
ngOnInit(): void {
@ -58,7 +62,7 @@ export class RegisterComponent {
this.isLoading = true;
const { email, password } = this.registrationForm.value;
this.authService.register(email, password, this.token).subscribe({
this.authService.register(email, password).subscribe({
next: () => {
this.notificationService.showSuccess('Registration successful');
this.router.navigate(['/login']);
@ -72,6 +76,12 @@ export class RegisterComponent {
});
}
private passwordMatchValidator(formGroup: FormGroup) {
const password = formGroup.get('password')?.value;
const confirmPassword = formGroup.get('confirmPassword')?.value;
return password === confirmPassword ? null : { mismatch: true };
}
get email() {
return this.registrationForm.get('email');
}
@ -79,4 +89,8 @@ export class RegisterComponent {
get password() {
return this.registrationForm.get('password');
}
get confirmPassword() {
return this.registrationForm.get('confirmPassword');
}
}

View File

@ -26,6 +26,7 @@ import { MatAccordion, MatExpansionModule } from '@angular/material/expansion';
import { MatStepperModule } from '@angular/material/stepper';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { MAT_MOMENT_DATE_ADAPTER_OPTIONS, MatMomentDateModule } from '@angular/material-moment-adapter';
import { MatProgressBarModule } from '@angular/material/progress-bar';
@NgModule({
declarations: [],
@ -55,7 +56,8 @@ import { MAT_MOMENT_DATE_ADAPTER_OPTIONS, MatMomentDateModule } from '@angular/m
MatAccordion,
MatStepperModule,
MatMomentDateModule,
MatSlideToggleModule
MatSlideToggleModule,
MatProgressBarModule
],
exports: [
MatButtonModule,
@ -82,7 +84,8 @@ import { MAT_MOMENT_DATE_ADAPTER_OPTIONS, MatMomentDateModule } from '@angular/m
MatAccordion,
MatStepperModule,
MatMomentDateModule,
MatSlideToggleModule
MatSlideToggleModule,
MatProgressBarModule
],
providers: [
{ provide: MAT_MOMENT_DATE_ADAPTER_OPTIONS, useValue: { useUtc: true } }