application name, registration and forgot password updates

This commit is contained in:
Cyril Joseph 2025-07-07 19:09:47 -03:00
parent f9fe0569d3
commit 716f063ecf
33 changed files with 868 additions and 249 deletions

View File

@ -1,20 +1,19 @@
import { Routes } from '@angular/router'; import { Routes } from '@angular/router';
import { AppIdGuard } from './guards/appid.guard'; import { AppIdGuard } from './guards/appid.guard';
import { AuthGuard } from './guards/auth.guard'; import { AuthGuard } from './guards/auth.guard';
import { HomeComponent } from './home/home.component';
import { LoginComponent } from './login/login.component';
import { NotFoundComponent } from './shared/components/not-found/not-found.component';
import { UserSettingsComponent } from './user-settings/user-settings.component';
export const routes: Routes = [ export const routes: Routes = [
{ path: 'login', component: LoginComponent }, { path: 'login', loadComponent: () => import('./login/login.component').then(m => m.LoginComponent) },
{ path: '404', component: NotFoundComponent }, { 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: '404', loadComponent: () => import('./shared/components/not-found/not-found.component').then(m => m.NotFoundComponent) },
{ {
path: ':appId', path: ':appId',
children: [ children: [
{ path: 'home', component: HomeComponent }, { path: 'home', loadComponent: () => import('./home/home.component').then(m => m.HomeComponent) },
{ path: 'usersettings', component: UserSettingsComponent }, { path: 'usersettings', loadComponent: () => import('./user-settings/user-settings.component').then(m => m.UserSettingsComponent) },
{ path: 'add-carnet', loadComponent: () => import('./carnet/add/add-carnet.component').then(m => m.AddCarnetComponent) }, { path: 'add-carnet', loadComponent: () => import('./carnet/add/add-carnet.component').then(m => m.AddCarnetComponent) },
{ path: 'edit-carnet/:headerid', loadComponent: () => import('./carnet/edit/edit-carnet.component').then(m => m.EditCarnetComponent) },
{ path: '', redirectTo: 'home', pathMatch: 'full' } { path: '', redirectTo: 'home', pathMatch: 'full' }
], ],
canActivate: [AuthGuard, AppIdGuard] canActivate: [AuthGuard, AppIdGuard]

View File

@ -1,64 +1,21 @@
<div class="client-carnet-container"> <div class="client-carnet-container">
<!-- Initial Questions Section -->
<div *ngIf="!questionsCompleted" class="questions-section">
<h2>Add New Carnet</h2>
<form [formGroup]="questionsForm" (ngSubmit)="onQuestionsSubmit()">
<div class="question-row">
<mat-radio-group formControlName="newCarnet">
<mat-label>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>
</div>
<div class="question-row">
<mat-radio-group formControlName="additionalSets">
<mat-label>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>
</div>
<div class="question-row">
<mat-radio-group formControlName="duplicateCarnet">
<mat-label>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>
</div>
<div class="question-row">
<mat-radio-group formControlName="extendCarnet">
<mat-label>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>
</div>
<div class="actions">
<button mat-raised-button color="primary" type="submit" [disabled]="!questionsForm.valid">
Continue
</button>
</div>
</form>
</div>
<!-- Stepper Section (shown after questions are answered) --> <!-- Stepper Section (shown after questions are answered) -->
<mat-stepper *ngIf="questionsCompleted" orientation="vertical" [linear]="isLinear" <mat-stepper orientation="vertical" [linear]="isLinear" (selectionChange)="onStepChange($event)"
(selectionChange)="onStepChange($event)" [selectedIndex]="currentStep"> [selectedIndex]="currentStep">
<!-- Application Name Step --> <!-- Application Name Step -->
<mat-step [completed]="stepsCompleted.applicationDetail" [editable]="stepsCompleted.applicationDetail"> <mat-step *ngIf="applicationType === 'new'" [completed]="stepsCompleted.applicationDetail"
[editable]="stepsCompleted.applicationDetail">
<ng-template matStepLabel>Application Name</ng-template> <ng-template matStepLabel>Application Name</ng-template>
<app-application [isEditMode]="isEditMode" (applicationIdCreated)="onApplicationDetailCreated($event)"> <app-application [isEditMode]="isEditMode" (headerIdCreated)="onApplicationDetailCreated($event)">
</app-application> </app-application>
</mat-step> </mat-step>
<!-- Holder Selection Step --> <!-- Holder Selection Step -->
<mat-step [completed]="stepsCompleted.holderSelection" [editable]="stepsCompleted.applicationDetail"> <mat-step *ngIf="applicationType === 'new'" [completed]="stepsCompleted.holderSelection"
[editable]="stepsCompleted.applicationDetail">
<ng-template matStepLabel>Holder Selection</ng-template> <ng-template matStepLabel>Holder Selection</ng-template>
<app-holder (completed)="onHolderSelectionSaved($event)"> <app-holder (completed)="onHolderSelectionSaved($event)" [headerid]="headerid">
</app-holder> </app-holder>
</mat-step> </mat-step>
@ -66,44 +23,45 @@
<mat-step *ngIf="applicationType === 'new'" [completed]="stepsCompleted.goodsSection" <mat-step *ngIf="applicationType === 'new'" [completed]="stepsCompleted.goodsSection"
[editable]="stepsCompleted.applicationDetail"> [editable]="stepsCompleted.applicationDetail">
<ng-template matStepLabel>Goods Section</ng-template> <ng-template matStepLabel>Goods Section</ng-template>
<app-goods (completed)="onGoodsSectionSaved($event)"> <app-goods (completed)="onGoodsSectionSaved($event)" [headerid]="headerid">
</app-goods> </app-goods>
</mat-step> </mat-step>
<!-- Travel Plan Step --> <!-- Travel Plan Step -->
<mat-step *ngIf="applicationType === 'new' || applicationType === 'extend'" <mat-step *ngIf="applicationType === 'new' || applicationType === 'extend' || applicationType === 'additional'"
[completed]="stepsCompleted.travelPlan" [editable]="stepsCompleted.applicationDetail"> [completed]="stepsCompleted.travelPlan" [editable]="stepsCompleted.applicationDetail">
<ng-template matStepLabel>Travel Plan</ng-template> <ng-template matStepLabel>Travel Plan</ng-template>
<!-- <app-travel-plan (completed)="onTravelPlanSaved($event)"> <app-travel-plan (completed)="onTravelPlanSaved($event)" [headerid]="headerid">
</app-travel-plan> --> </app-travel-plan>
</mat-step> </mat-step>
<!-- Insurance Step --> <!-- Insurance Step -->
<mat-step [completed]="stepsCompleted.insurance" [editable]="stepsCompleted.applicationDetail"> <mat-step *ngIf="applicationType === 'new' || applicationType === 'extend' || applicationType === 'additional'"
[completed]="stepsCompleted.insurance" [editable]="stepsCompleted.applicationDetail">
<ng-template matStepLabel>Insurance</ng-template> <ng-template matStepLabel>Insurance</ng-template>
<!-- <app-insurance (completed)="onInsuranceSaved($event)"> <app-insurance (completed)="onInsuranceSaved($event)" [headerid]="headerid">
</app-insurance> --> </app-insurance>
</mat-step> </mat-step>
<!-- Shipping Step --> <!-- Shipping Step -->
<mat-step [completed]="stepsCompleted.shipping" [editable]="stepsCompleted.applicationDetail"> <mat-step [completed]="stepsCompleted.shipping" [editable]="stepsCompleted.applicationDetail">
<ng-template matStepLabel>Shipping</ng-template> <ng-template matStepLabel>Shipping</ng-template>
<!-- <app-shipping (completed)="onShippingSaved($event)"> <app-shipping (completed)="onShippingSaved($event)" [headerid]="headerid">
</app-shipping> --> </app-shipping>
</mat-step> </mat-step>
<!-- Delivery Method Step --> <!-- Delivery Method Step -->
<mat-step [completed]="stepsCompleted.deliveryMethod" [editable]="stepsCompleted.applicationDetail"> <mat-step [completed]="stepsCompleted.deliveryMethod" [editable]="stepsCompleted.applicationDetail">
<ng-template matStepLabel>Delivery Method</ng-template> <ng-template matStepLabel>Delivery Method</ng-template>
<!-- <app-delivery-method (completed)="onDeliveryMethodSaved($event)"> <app-delivery (completed)="onDeliveryMethodSaved($event)" [headerid]="headerid">
</app-delivery-method> --> </app-delivery>
</mat-step> </mat-step>
<!-- Payment Step --> <!-- Payment Step -->
<mat-step [completed]="stepsCompleted.payment" [editable]="stepsCompleted.applicationDetail"> <mat-step [completed]="stepsCompleted.payment" [editable]="stepsCompleted.applicationDetail">
<ng-template matStepLabel>Payment</ng-template> <ng-template matStepLabel>Payment</ng-template>
<!-- <app-payment (completed)="onPaymentSaved($event)"> <app-payment (completed)="onPaymentSaved($event)" [headerid]="headerid">
</app-payment> --> </app-payment>
</mat-step> </mat-step>
</mat-stepper> </mat-stepper>
</div> </div>

View File

@ -1,51 +1,11 @@
.client-carnet-container { // .client-carnet-container {
// padding: 24px; // // padding: 24px;
// max-width: 1200px; // // max-width: 1200px;
// margin: 0 auto; // // margin: 0 auto;
.questions-section { // .actions {
// padding: 20px; // margin-top: 24px;
// border-radius: 8px; // display: flex;
// margin-bottom: 24px; // justify-content: flex-start;
// }
// h2 { // }
// margin-bottom: 24px;
// color: #3f51b5;
// }
.question-row {
margin-bottom: 1rem;
padding: 1rem;
background: #ffff;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
mat-radio-group {
display: flex;
flex-direction: column;
gap: 8px;
// mat-label {
// margin-bottom: 8px;
// font-weight: 500;
// }
mat-radio-button {
margin-right: 1rem;
}
}
}
.actions {
margin-top: 24px;
display: flex;
justify-content: flex-end;
}
}
.actions {
margin-top: 24px;
display: flex;
justify-content: flex-start;
}
}

View File

@ -6,31 +6,26 @@ import { StepperSelectionEvent } from '@angular/cdk/stepper';
import { ApplicationComponent } from "../application/application.component"; import { ApplicationComponent } from "../application/application.component";
import { HolderComponent } from '../holder/holder.component'; import { HolderComponent } from '../holder/holder.component';
import { GoodsComponent } from '../goods/goods.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';
@Component({ @Component({
selector: 'app-add-carnet', selector: 'app-add-carnet',
imports: [AngularMaterialModule, CommonModule, ReactiveFormsModule, imports: [AngularMaterialModule, CommonModule, ReactiveFormsModule,
ApplicationComponent, HolderComponent, GoodsComponent], ApplicationComponent, HolderComponent, GoodsComponent, TravelPlanComponent, InsuranceComponent,
ShippingComponent, DeliveryComponent, PaymentComponent],
templateUrl: './add-carnet.component.html', templateUrl: './add-carnet.component.html',
styleUrl: './add-carnet.component.scss' styleUrl: './add-carnet.component.scss'
}) })
export class AddCarnetComponent { export class AddCarnetComponent {
currentStep = 0; currentStep = 0;
questionsCompleted = false;
isLinear = true; isLinear = true;
applicationType: 'new' | 'additional' | 'duplicate' | 'extend' | null = null; applicationType: 'new' | 'additional' | 'duplicate' | 'extend' | null = 'new';
isEditMode = false; isEditMode = false;
applicationid: number = 0; headerid: number = 0;
private fb = inject(FormBuilder);
// Form group for initial questions
questionsForm: FormGroup = this.fb.group({
newCarnet: [null, Validators.required],
additionalSets: [false],
duplicateCarnet: [false],
extendCarnet: [false]
});
// Track completion of each step // Track completion of each step
stepsCompleted = { stepsCompleted = {
@ -44,30 +39,12 @@ export class AddCarnetComponent {
payment: false payment: false
}; };
onQuestionsSubmit(): void {
const answers = this.questionsForm.value;
if (answers.newCarnet) {
this.applicationType = 'new';
} else if (answers.additionalSets) {
this.applicationType = 'additional';
} else if (answers.duplicateCarnet) {
this.applicationType = 'duplicate';
} else if (answers.extendCarnet) {
this.applicationType = 'extend';
}
// Move to the first step after questions
this.questionsCompleted = true;
this.currentStep = 0;
}
onStepChange(event: StepperSelectionEvent): void { onStepChange(event: StepperSelectionEvent): void {
this.currentStep = event.selectedIndex; this.currentStep = event.selectedIndex;
} }
onApplicationDetailCreated(applicationid: number): void { onApplicationDetailCreated(headerid: number): void {
this.applicationid = applicationid; this.headerid = this.headerid;
this.stepsCompleted.applicationDetail = true; this.stepsCompleted.applicationDetail = true;
this.isLinear = false; // Disable linear mode after application detail is created this.isLinear = false; // Disable linear mode after application detail is created
} }

View File

@ -7,6 +7,7 @@ import { ApplicationDetail } from '../../core/models/carnet/application-detail';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { AngularMaterialModule } from '../../shared/module/angular-material.module'; import { AngularMaterialModule } from '../../shared/module/angular-material.module';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { ActivatedRoute } from '@angular/router';
@Component({ @Component({
selector: 'app-application', selector: 'app-application',
@ -16,9 +17,10 @@ import { CommonModule } from '@angular/common';
}) })
export class ApplicationComponent { export class ApplicationComponent {
@Input() isEditMode = false; @Input() isEditMode = false;
@Input() applicationid: number = 0; @Input() headerid: number = 0;
@Input() applicationName: string = '';
@Output() applicationIdCreated = new EventEmitter<number>(); @Output() headerIdCreated = new EventEmitter<number>();
applicationDetailsForm: FormGroup; applicationDetailsForm: FormGroup;
isLoading = false; isLoading = false;
@ -30,31 +32,33 @@ export class ApplicationComponent {
private applicationDetailService = inject(ApplicationDetailService); private applicationDetailService = inject(ApplicationDetailService);
private notificationService = inject(NotificationService); private notificationService = inject(NotificationService);
private errorHandler = inject(ApiErrorHandlerService); private errorHandler = inject(ApiErrorHandlerService);
private route = inject(ActivatedRoute);
constructor() { constructor() {
this.applicationDetailsForm = this.createForm(); this.applicationDetailsForm = this.createForm();
} }
ngOnInit(): void { ngOnInit(): void {
// this.spidCreated.emit(this.spid?.toString()); this.applicationName = this.route.snapshot.queryParamMap.get('applicationName') || '';
// Patch edit form data // Patch edit form data
if (this.applicationid > 0) { if (this.applicationName) {
this.isLoading = true; this.patchFormData();
this.applicationDetailService.getApplicationDetailsById(this.applicationid).subscribe({
next: (applicationDetail: ApplicationDetail) => { // this.applicationDetailService.getApplicationDetailsById(this.headerid).subscribe({
if (applicationDetail?.applicationId > 0) { // next: (applicationDetail: ApplicationDetail) => {
this.patchFormData(applicationDetail); // if (applicationDetail?.applicationId > 0) {
} // this.patchFormData(applicationDetail);
this.isLoading = false; // }
}, // this.isLoading = false;
error: (error: any) => { // },
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load application details'); // error: (error: any) => {
this.notificationService.showError(errorMessage); // let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load application details');
this.isLoading = false; // this.notificationService.showError(errorMessage);
console.error('Error loading application details:', error); // this.isLoading = false;
} // console.error('Error loading application details:', error);
}); // }
// });
} }
} }
@ -69,9 +73,9 @@ export class ApplicationComponent {
}); });
} }
patchFormData(data: ApplicationDetail): void { patchFormData(): void {
this.applicationDetailsForm.patchValue({ this.applicationDetailsForm.patchValue({
name: data.name, name: this.applicationName,
}); });
if (this.isEditMode) { if (this.isEditMode) {
@ -93,12 +97,12 @@ export class ApplicationComponent {
const applicationDetailData: ApplicationDetail = this.applicationDetailsForm.value; const applicationDetailData: ApplicationDetail = this.applicationDetailsForm.value;
if (!this.isEditMode && this.applicationid == 0) { if (!this.isEditMode && this.headerid == 0) {
this.isLoading = true; this.isLoading = true;
this.applicationDetailService.createApplicationDetails(applicationDetailData).subscribe({ this.applicationDetailService.createApplicationDetails(applicationDetailData).subscribe({
next: (applicationData: any) => { next: (applicationData: any) => {
this.notificationService.showSuccess(`Application details added successfully`); this.notificationService.showSuccess(`Application details added successfully`);
this.applicationIdCreated.emit(applicationData.P_HEADERID); this.headerIdCreated.emit(applicationData.P_HEADERID);
this.applicationDetailsForm.get('name')?.disable(); this.applicationDetailsForm.get('name')?.disable();
this.disableSaveButton = true; this.disableSaveButton = true;
this.isLoading = false; this.isLoading = false;

View File

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

View File

@ -1,4 +1,4 @@
import { Component } from '@angular/core'; import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({ @Component({
selector: 'app-delivery', selector: 'app-delivery',
@ -7,5 +7,7 @@ import { Component } from '@angular/core';
styleUrl: './delivery.component.scss' styleUrl: './delivery.component.scss'
}) })
export class DeliveryComponent { export class DeliveryComponent {
@Input() headerid: number = 0;
@Output() completed = new EventEmitter<boolean>();
} }

View File

@ -1 +1,67 @@
<p>edit-carnet works!</p> <div class="client-carnet-container">
<!-- Stepper Section (shown after questions are answered) -->
<mat-stepper orientation="vertical" [linear]="isLinear" (selectionChange)="onStepChange($event)"
[selectedIndex]="currentStep">
<!-- Application Name Step -->
<mat-step *ngIf="applicationType === 'new'" [completed]="stepsCompleted.applicationDetail"
[editable]="stepsCompleted.applicationDetail">
<ng-template matStepLabel>Application Name</ng-template>
<app-application [isEditMode]="isEditMode">
</app-application>
</mat-step>
<!-- Holder Selection Step -->
<mat-step *ngIf="applicationType === 'new'" [completed]="stepsCompleted.holderSelection"
[editable]="stepsCompleted.applicationDetail">
<ng-template matStepLabel>Holder Selection</ng-template>
<app-holder (completed)="onHolderSelectionSaved($event)" [headerid]="headerid">
</app-holder>
</mat-step>
<!-- Goods Section Step -->
<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>
</mat-step>
<!-- Travel Plan Step -->
<mat-step *ngIf="applicationType === 'new' || applicationType === 'extend' || applicationType === 'additional'"
[completed]="stepsCompleted.travelPlan" [editable]="stepsCompleted.applicationDetail">
<ng-template matStepLabel>Travel Plan</ng-template>
<app-travel-plan (completed)="onTravelPlanSaved($event)" [headerid]="headerid">
</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 -->
<mat-step [completed]="stepsCompleted.shipping" [editable]="stepsCompleted.applicationDetail">
<ng-template matStepLabel>Shipping</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

@ -1,11 +1,81 @@
import { Component } from '@angular/core'; import { CommonModule } from '@angular/common';
import { Component, inject } from '@angular/core';
import { AngularMaterialModule } from '../../shared/module/angular-material.module';
import { ReactiveFormsModule } from '@angular/forms';
import { ApplicationComponent } from '../application/application.component';
import { StepperSelectionEvent } from '@angular/cdk/stepper';
import { GoodsComponent } from '../goods/goods.component';
import { HolderComponent } from '../holder/holder.component';
import { ActivatedRoute } from '@angular/router';
import { 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';
@Component({ @Component({
selector: 'app-edit-carnet', selector: 'app-edit-carnet',
imports: [], imports: [AngularMaterialModule, CommonModule, ReactiveFormsModule,
ApplicationComponent, HolderComponent, GoodsComponent, TravelPlanComponent, InsuranceComponent,
ShippingComponent, DeliveryComponent, PaymentComponent],
templateUrl: './edit-carnet.component.html', templateUrl: './edit-carnet.component.html',
styleUrl: './edit-carnet.component.scss' styleUrl: './edit-carnet.component.scss'
}) })
export class EditCarnetComponent { export class EditCarnetComponent {
currentStep = 0;
isLinear = false;
applicationType: 'new' | 'additional' | 'duplicate' | 'extend' | null = 'new';
isEditMode = true; // Set to true for edit mode
headerid: number = 0;
// Track completion of each step
stepsCompleted = {
applicationDetail: true,
holderSelection: false,
goodsSection: false,
travelPlan: false,
insurance: false,
shipping: false,
deliveryMethod: false,
payment: false
};
private route = inject(ActivatedRoute);
ngOnInit(): void {
const idParam = this.route.snapshot.paramMap.get('headerid');
this.headerid = idParam ? parseInt(idParam, 10) : 0;
}
onStepChange(event: StepperSelectionEvent): void {
this.currentStep = event.selectedIndex;
}
onHolderSelectionSaved(completed: boolean): void {
this.stepsCompleted.holderSelection = completed;
}
onGoodsSectionSaved(completed: boolean): void {
this.stepsCompleted.goodsSection = completed;
}
onTravelPlanSaved(completed: boolean): void {
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

@ -9,6 +9,6 @@ import { CommonModule } from '@angular/common';
styleUrl: './goods.component.scss' styleUrl: './goods.component.scss'
}) })
export class GoodsComponent { export class GoodsComponent {
@Input() applicationid: number = 0; @Input() headerid: number = 0;
@Output() completed = new EventEmitter<boolean>(); @Output() completed = new EventEmitter<boolean>();
} }

View File

@ -7,6 +7,6 @@ import { Component, EventEmitter, Input, Output } from '@angular/core';
styleUrl: './holder.component.scss' styleUrl: './holder.component.scss'
}) })
export class HolderComponent { export class HolderComponent {
@Input() applicationid: number = 0; @Input() headerid: number = 0;
@Output() completed = new EventEmitter<boolean>(); @Output() completed = new EventEmitter<boolean>();
} }

View File

@ -1,4 +1,4 @@
import { Component } from '@angular/core'; import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({ @Component({
selector: 'app-insurance', selector: 'app-insurance',
@ -7,5 +7,6 @@ import { Component } from '@angular/core';
styleUrl: './insurance.component.scss' styleUrl: './insurance.component.scss'
}) })
export class InsuranceComponent { export class InsuranceComponent {
@Input() headerid: number = 0;
@Output() completed = new EventEmitter<boolean>();
} }

View File

@ -1,4 +1,4 @@
import { Component } from '@angular/core'; import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({ @Component({
selector: 'app-payment', selector: 'app-payment',
@ -7,5 +7,6 @@ import { Component } from '@angular/core';
styleUrl: './payment.component.scss' styleUrl: './payment.component.scss'
}) })
export class PaymentComponent { export class PaymentComponent {
@Input() headerid: number = 0;
@Output() completed = new EventEmitter<boolean>();
} }

View File

@ -1,4 +1,4 @@
import { Component } from '@angular/core'; import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({ @Component({
selector: 'app-shipping', selector: 'app-shipping',
@ -7,5 +7,6 @@ import { Component } from '@angular/core';
styleUrl: './shipping.component.scss' styleUrl: './shipping.component.scss'
}) })
export class ShippingComponent { export class ShippingComponent {
@Input() headerid: number = 0;
@Output() completed = new EventEmitter<boolean>();
} }

View File

@ -1,4 +1,4 @@
import { Component } from '@angular/core'; import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({ @Component({
selector: 'app-travel-plan', selector: 'app-travel-plan',
@ -7,5 +7,6 @@ import { Component } from '@angular/core';
styleUrl: './travel-plan.component.scss' styleUrl: './travel-plan.component.scss'
}) })
export class TravelPlanComponent { export class TravelPlanComponent {
@Input() headerid: number = 0;
@Output() completed = new EventEmitter<boolean>();
} }

View File

@ -7,6 +7,7 @@ import { AuthService } from '../services/common/auth.service';
@Injectable() @Injectable()
export class AuthInterceptor implements HttpInterceptor { export class AuthInterceptor implements HttpInterceptor {
private excludedUrls = ['/register', '/forgot-password'];
private isRefreshing = false; private isRefreshing = false;
private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null); private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null);
@ -17,7 +18,11 @@ export class AuthInterceptor implements HttpInterceptor {
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// Add withCredentials to all requests // Add withCredentials to all requests except excluded URLs
if (this.excludedUrls.some(url => request.url.includes(url))) {
return next.handle(request);
}
request = request.clone({ request = request.clone({
withCredentials: true withCredentials: true
}); });

View File

@ -13,6 +13,7 @@ export interface Menu {
export interface UserDetail { export interface UserDetail {
spid: number; spid: number;
clientid: number; clientid: number;
locationid: number;
urlKey: string; urlKey: string;
logoName: string; logoName: string;
themeName: string; themeName: string;

View File

@ -15,29 +15,37 @@ export class ApplicationDetailService {
private http = inject(HttpClient); private http = inject(HttpClient);
private userService = inject(UserService); private userService = inject(UserService);
getApplicationDetailsById(id: number): ApplicationDetail | any { // getApplicationDetailsById(id: number): ApplicationDetail | any {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetPreparerByClientid/${this.userService.getUserSpid()}/${id}`).pipe( // return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetPreparerByClientid/${this.userService.getUserSpid()}/${id}`).pipe(
filter(response => response.length > 0), // filter(response => response.length > 0),
map(response => this.mapToApplicationDetail(response?.[0]))); // map(response => this.mapToApplicationDetail(response?.[0])));
} // }
private mapToApplicationDetail(applicationDetails: any): ApplicationDetail { // private mapToApplicationDetail(applicationDetails: any): ApplicationDetail {
return { // return {
clientid: applicationDetails.CLIENTID, // clientid: applicationDetails.CLIENTID,
spid: applicationDetails.SPID, // spid: applicationDetails.SPID,
name: applicationDetails.P_APPLICATIONNAME, // name: applicationDetails.P_APPLICATIONNAME,
applicationId: applicationDetails.P_HEADERID // applicationId: applicationDetails.P_HEADERID
}; // };
} // }
createApplicationDetails(data: ApplicationDetail): Observable<any> { createApplicationDetails(data: ApplicationDetail): Observable<any> {
const userDetails = this.userService.getUserDetails();
if (!userDetails || !userDetails.userDetails) {
throw new Error('User details are not available');
}
const applicationDetails = { const applicationDetails = {
P_SPID: this.userService.getUserSpid(), P_SPID: userDetails.userDetails.spid,
P_CLIENTID: this.userService.getUserClientid(), P_CLIENTID: userDetails.userDetails.clientid,
P_LOCATIONID: userDetails.userDetails.locationid,
P_APPLICATIONNAME: data.name, P_APPLICATIONNAME: data.name,
P_ORDERTYPE: 'ORIGINAL',
P_USERID: this.userService.getUser(), P_USERID: this.userService.getUser(),
} }
return this.http.post(`${this.apiUrl}/${this.apiDb}/SaveCarnetApplication`, applicationDetails); return this.http.post(`${this.apiUrl}/${this.apiDb}/CreateApplication`, applicationDetails);
} }
} }

View File

@ -1,4 +1,4 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient, HttpHeaders } from '@angular/common/http';
import { inject, Injectable } from '@angular/core'; import { inject, Injectable } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { environment } from '../../../../environments/environment'; import { environment } from '../../../../environments/environment';
@ -48,4 +48,25 @@ export class AuthService {
}); });
} }
} }
register(email: string, password: string, token: string | null): Observable<any> {
let headers = new HttpHeaders();
if (token) {
headers = headers.set('Authorization', `Bearer ${token}`);
}
return this.http.post(
`${this.apiUrl}/register`,
{ P_EMAILADDR: email, P_PASSWORD: password },
{ headers }
);
}
forgotPassword(email: string, password: string): Observable<any> {
return this.http.post(
`${this.apiUrl}/forgot-password`,
{ P_EMAILADDR: email, P_PASSWORD: password }
);
}
} }

View File

@ -12,9 +12,6 @@ export class UserService {
private apiUrl = environment.apiUrl; private apiUrl = environment.apiUrl;
private apiDb = environment.apiDb; private apiDb = environment.apiDb;
private spid: number = 0;
private clientid: number = 0;
userDetailsSignal = signal<User>({}); userDetailsSignal = signal<User>({});
private readonly USER_EMAIL_KEY = 'CurrentUserEmail'; private readonly USER_EMAIL_KEY = 'CurrentUserEmail';
@ -98,23 +95,18 @@ export class UserService {
} }
getUserSpid(): number { getUserSpid(): number {
if (this.spid === 0) {
const userDetails = this.getUserDetails(); const userDetails = this.getUserDetails();
if (userDetails && userDetails.userDetails && userDetails.userDetails.spid) { return userDetails?.userDetails?.spid ?? 0;
this.spid = userDetails.userDetails.spid;
}
}
return this.spid;
} }
getUserClientid(): number { getUserClientid(): number {
if (this.clientid === 0) {
const userDetails = this.getUserDetails(); const userDetails = this.getUserDetails();
if (userDetails && userDetails.userDetails && userDetails.userDetails.clientid) { return userDetails?.userDetails?.clientid ?? 0;
this.clientid = userDetails.userDetails.clientid;
} }
}
return this.clientid; getUserLocationid(): number {
const userDetails = this.getUserDetails();
return userDetails?.userDetails?.locationid ?? 0;
} }
private mapToUser(data: any): User { private mapToUser(data: any): User {
@ -141,6 +133,7 @@ export class UserService {
return { return {
spid: userDetails.SPID, spid: userDetails.SPID,
clientid: userDetails.CLIENTID, clientid: userDetails.CLIENTID,
locationid: userDetails.LOCATIONID,
urlKey: userDetails.ENCURLKEY, urlKey: userDetails.ENCURLKEY,
logoName: userDetails.LOGONAME, logoName: userDetails.LOGONAME,
themeName: userDetails.THEMENAME themeName: userDetails.THEMENAME

View File

@ -37,7 +37,8 @@ export class HomeService {
issueDate: item.ISSUEDATE || null, issueDate: item.ISSUEDATE || null,
expiryDate: item.EXPDATE || null, expiryDate: item.EXPDATE || null,
orderType: item.ORDERTYPE, orderType: item.ORDERTYPE,
carnetStatus: item.CARNETSTATUS carnetStatus: item.CARNETSTATUS,
headerId: item.HEADERID,
})); }));
} }
} }

View File

@ -0,0 +1,56 @@
<div class="forgot-password-container">
<div class="forgot-password-card">
<h3 class="subtitle">Forgot Password</h3>
<form [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>
<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>
<mat-form-field appearance="outline">
<mat-label>Password</mat-label>
<input matInput formControlName="password" [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="password?.errors?.['required']">
Password is required
</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]="forgotPasswordForm.invalid || isLoading">
<span *ngIf="!isLoading">Submit</span>
<mat-spinner *ngIf="isLoading" diameter="20"></mat-spinner>
</button>
</form>
</div>
<div class="info-section">
<h3>ATA Carnet</h3>
<p>
Also known as the "Merchandise Passport," is an international customs document that simplifies temporary
exports to over 79 countries and territories.
It allows businesses to explore new markets, showcase products at trade shows, and attend global conferences
without paying duties or taxes.
</p>
<p>
It simplifies customs procedures for the temporary movement of goods and allows goods to
enter Customs territories of the ATA Carnet system free of customs duties and taxes for up to
one year.
</p>
</div>
</div>

View File

@ -0,0 +1,95 @@
/* forgot-password.component.scss */
.forgot-password-container {
display: flex;
min-height: 85vh;
background-color: #f5f5f5;
.forgot-password-card {
flex: 1;
max-width: 500px;
padding: 2rem;
display: flex;
flex-direction: column;
justify-content: center;
background-color: white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
.subtitle {
text-align: center;
color: #666;
font-size: 1.5rem;
margin-bottom: 2rem;
}
.forgot-password-form {
display: flex;
flex-direction: column;
gap: 1.5rem;
mat-form-field {
width: 100%;
}
.login {
text-align: right;
margin-top: -1rem;
.login-link {
color: #666;
text-decoration: none;
font-size: 0.875rem;
&:hover {
text-decoration: underline;
}
}
}
.mdc-icon-button {
margin-right: 8px;
}
// button {
// margin-top: 1rem;
// padding: 0.5rem;
// font-size: 1rem;
// }
}
}
.info-section {
flex: 1;
padding: 4rem;
background-color: #597b7c;
color: white;
display: flex;
flex-direction: column;
justify-content: center;
h3 {
font-size: 2rem;
margin-bottom: 1.5rem;
}
p {
line-height: 1.6;
font-size: 1rem;
}
}
}
@media (max-width: 768px) {
.forgot-password-container {
flex-direction: column;
.forgot-password-card {
max-width: 100%;
padding: 1.5rem;
}
.info-section {
padding: 2rem;
display: none;
}
}
}

View File

@ -0,0 +1,71 @@
import { Component, inject } from '@angular/core';
import { FormGroup, FormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
import { 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';
import { ThemeService } from '../core/services/theme.service';
import { AngularMaterialModule } from '../shared/module/angular-material.module';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-forgot-password',
imports: [AngularMaterialModule, ReactiveFormsModule, CommonModule],
templateUrl: './forgot-password.component.html',
styleUrl: './forgot-password.component.scss'
})
export class ForgotPasswordComponent {
forgotPasswordForm: FormGroup;
isLoading = false;
hidePassword = true;
private fb = inject(FormBuilder);
private authService = inject(AuthService);
private router = inject(Router);
private notificationService = inject(NotificationService);
private errorHandler = inject(ApiErrorHandlerService);
private themeService = inject(ThemeService);
constructor() {
this.forgotPasswordForm = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required]]
});
}
ngOnInit(): void {
this.themeService.setTheme('default');
}
onSubmit(): void {
if (this.forgotPasswordForm.invalid) {
this.forgotPasswordForm.markAllAsTouched();
return;
}
this.isLoading = true;
const { email, password } = this.forgotPasswordForm.value;
this.authService.forgotPassword(email, password).subscribe({
next: () => {
this.notificationService.showSuccess('Request successful');
this.router.navigate(['/login']);
},
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);
}
});
}
get email() {
return this.forgotPasswordForm.get('email');
}
get password() {
return this.forgotPasswordForm.get('password');
}
}

View File

@ -5,6 +5,7 @@ import { BaseChartDirective } from 'ng2-charts';
import { AngularMaterialModule } from '../../shared/module/angular-material.module'; import { AngularMaterialModule } from '../../shared/module/angular-material.module';
import { CarnetStatus } from '../../core/models/carnet-status'; import { CarnetStatus } from '../../core/models/carnet-status';
import { NavigationService } from '../../core/services/common/navigation.service'; import { NavigationService } from '../../core/services/common/navigation.service';
import { UserService } from '../../core/services/common/user.service';
@Component({ @Component({
selector: 'app-chart', selector: 'app-chart',
@ -18,6 +19,7 @@ export class ChartComponent {
@Output() carnetStatusData = new EventEmitter<any>(); @Output() carnetStatusData = new EventEmitter<any>();
private navigationService = inject(NavigationService); private navigationService = inject(NavigationService);
private userService = inject(UserService);
navigateToManageProvider(spid: number): void { navigateToManageProvider(spid: number): void {
this.navigationService.navigate(['service-provider', spid]); this.navigationService.navigate(['service-provider', spid]);
@ -98,8 +100,8 @@ export class ChartComponent {
}); });
return { return {
title: provider.Service_Provider_Name, title: provider.Service_Provider_Name ?? provider.Preparer_Name,
spid: provider.SPID, spid: provider.SPID ?? this.userService.getUserSpid(),
data: { data: {
labels: labels, labels: labels,
datasets: [{ datasets: [{

View File

@ -1,17 +1,48 @@
<div class="dashboard-container"> <div class="dashboard-container">
<div class="content"> <div class="content">
<div class="quick-actions"> <div class="app-container">
<button mat-raised-button (click)="navigateTo('add-carnet')" color="accent">
<mat-icon>add</mat-icon> Add New Carnet
</button>
</div>
<!-- Chart Section --> <!-- Chart Section -->
<div class="chart-section"> <div class="chart-section">
<app-chart [chartData]="carnetData" [carnetStatuses]="carnetStatuses" <app-chart [chartData]="carnetData" [carnetStatuses]="carnetStatuses"
(carnetStatusData)="onCarnetStatusClick($event)"></app-chart> (carnetStatusData)="onCarnetStatusClick($event)"></app-chart>
</div> </div>
<!-- Questions Section -->
<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-radio-button [value]="true">Yes</mat-radio-button>
<mat-radio-button [value]="false">No</mat-radio-button>
</mat-radio-group>
</div>
<div class="question-row">
<mat-radio-group (change)="additionalSets($event)">
<mat-label>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>
</div>
<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-radio-button [value]="true">Yes</mat-radio-button>
<mat-radio-button [value]="false">No</mat-radio-button>
</mat-radio-group>
</div>
<div class="question-row">
<mat-radio-group (change)="extendCarnet($event)">
<mat-label>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>
</div>
</div>
</div>
<!-- Carnet Details Section --> <!-- Carnet Details Section -->
<div class="carnet-details-section"> <div class="carnet-details-section">
<div class="loading-shade" *ngIf="isLoading"> <div class="loading-shade" *ngIf="isLoading">
@ -79,7 +110,7 @@
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>Actions</th> <th mat-header-cell *matHeaderCellDef>Actions</th>
<td mat-cell *matCellDef="let item"> <td mat-cell *matCellDef="let item">
<button mat-icon-button color="primary" <button mat-icon-button color="primary" (click)="editCarnet(item)"
*ngIf="getCarnetStatusLabel(item.carnetStatus) === 'Pending'" *ngIf="getCarnetStatusLabel(item.carnetStatus) === 'Pending'"
[hidden]="getCarnetStatusLabel(item.carnetStatus) === 'Pending'" matTooltip="Edit"> [hidden]="getCarnetStatusLabel(item.carnetStatus) === 'Pending'" matTooltip="Edit">
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>

View File

@ -1,12 +1,42 @@
.dashboard-container { .dashboard-container {
padding: 24px; // padding: 24px;
.quick-actions { // .quick-actions {
// display: flex;
// justify-content: end;
// button {
// margin-bottom: 16px;
// }
// }
.app-container {
display: flex; display: flex;
justify-content: end; gap: 16px;
box-sizing: border-box;
button { .chart-section {
margin-bottom: 16px; flex: 0 0 30%;
}
.questions-section {
flex: 0 0 70%;
padding: 24px 0px;
margin-bottom: 24px;
margin-left: 1rem;
.question-row {
margin-bottom: 1rem;
mat-radio-group {
display: flex;
gap: 8px;
mat-radio-button {
margin-top: -5px;
}
}
}
} }
} }

View File

@ -14,6 +14,7 @@ import { ApiErrorHandlerService } from '../core/services/common/api-error-handle
import { CommonService } from '../core/services/common/common.service'; import { CommonService } from '../core/services/common/common.service';
import { NotificationService } from '../core/services/common/notification.service'; import { NotificationService } from '../core/services/common/notification.service';
import { NavigationService } from '../core/services/common/navigation.service'; import { NavigationService } from '../core/services/common/navigation.service';
import { MatRadioChange } from '@angular/material/radio';
@Component({ @Component({
selector: 'app-home', selector: 'app-home',
@ -111,6 +112,29 @@ export class HomeComponent {
return carnetStatus ? carnetStatus.name : value; return carnetStatus ? carnetStatus.name : value;
} }
newCarnet(event: MatRadioChange): void {
if (event.value) {
this.navigateTo('add-carnet');
}
}
additionalSets(event: MatRadioChange): void {
}
duplicateCarnet(event: MatRadioChange): void {
}
extendCarnet(event: MatRadioChange): void {
}
editCarnet(item: any): void {
if (item && item.headerId) {
this.navigateTo(`edit-carnet/${item.headerId}?ApplicationName=${item.applicationName}`);
} else {
this.notificationService.showError('Invalid carnet data');
}
}
navigateTo(route: string): void { navigateTo(route: string): void {
this.navigationService.navigate([route]); this.navigationService.navigate([route]);
} }

View File

@ -22,7 +22,7 @@
</mat-form-field> </mat-form-field>
<div class="forgot-password"> <div class="forgot-password">
<a href="#" class="forgot-link">Forgot your password?</a> <a href="#" routerLink="/forgot-password" class="forgot-link">Forgot your password?</a>
</div> </div>
<button mat-raised-button color="primary" type="submit" [disabled]="!loginForm.valid"> <button mat-raised-button color="primary" type="submit" [disabled]="!loginForm.valid">

View File

@ -7,10 +7,11 @@ import { NavigationService } from '../core/services/common/navigation.service';
import { UserService } from '../core/services/common/user.service'; import { UserService } from '../core/services/common/user.service';
import { User } from '../core/models/user'; import { User } from '../core/models/user';
import { ThemeService } from '../core/services/theme.service'; import { ThemeService } from '../core/services/theme.service';
import { RouterModule } from '@angular/router';
@Component({ @Component({
selector: 'app-login', selector: 'app-login',
imports: [ReactiveFormsModule, CommonModule, AngularMaterialModule], imports: [ReactiveFormsModule, CommonModule, AngularMaterialModule, RouterModule],
templateUrl: './login.component.html', templateUrl: './login.component.html',
styleUrl: './login.component.scss' styleUrl: './login.component.scss'
}) })

View File

@ -0,0 +1,64 @@
<div class="registration-container">
<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">
<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>
<mat-form-field appearance="outline">
<mat-label>Password</mat-label>
<input matInput formControlName="password" [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="password?.errors?.['required']">
Password is required
</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]="registrationForm.invalid || isLoading">
<span *ngIf="!isLoading">Register</span>
<mat-spinner *ngIf="isLoading" diameter="20"></mat-spinner>
</button>
</form>
</div>
<div class="info-section">
<h3>ATA Carnet</h3>
<p>
Also known as the "Merchandise Passport," is an international customs document that simplifies temporary
exports to over 79 countries and territories.
It allows businesses to explore new markets, showcase products at trade shows, and attend global conferences
without paying duties or taxes.
</p>
<p>
It simplifies customs procedures for the temporary movement of goods and allows goods to
enter Customs territories of the ATA Carnet system free of customs duties and taxes for up to
one year.
</p>
</div>
</div>

View File

@ -0,0 +1,95 @@
/* registration.component.scss */
.registration-container {
display: flex;
min-height: 85vh;
background-color: #f5f5f5;
.registration-card {
flex: 1;
max-width: 500px;
padding: 2rem;
display: flex;
flex-direction: column;
justify-content: center;
background-color: white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
.subtitle {
text-align: center;
color: #666;
font-size: 1.5rem;
margin-bottom: 2rem;
}
.registration-form {
display: flex;
flex-direction: column;
gap: 1.5rem;
mat-form-field {
width: 100%;
}
.login {
text-align: right;
margin-top: -1rem;
.login-link {
color: #666;
text-decoration: none;
font-size: 0.875rem;
&:hover {
text-decoration: underline;
}
}
}
.mdc-icon-button {
margin-right: 8px;
}
// button {
// margin-top: 1rem;
// padding: 0.5rem;
// font-size: 1rem;
// }
}
}
.info-section {
flex: 1;
padding: 4rem;
background-color: #597b7c;
color: white;
display: flex;
flex-direction: column;
justify-content: center;
h3 {
font-size: 2rem;
margin-bottom: 1.5rem;
}
p {
line-height: 1.6;
font-size: 1rem;
}
}
}
@media (max-width: 768px) {
.registration-container {
flex-direction: column;
.registration-card {
max-width: 100%;
padding: 1.5rem;
}
.info-section {
padding: 2rem;
display: none;
}
}
}

View File

@ -0,0 +1,82 @@
import { Component, inject } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { AuthService } from '../core/services/common/auth.service';
import { NotificationService } from '../core/services/common/notification.service';
import { AngularMaterialModule } from '../shared/module/angular-material.module';
import { ApiErrorHandlerService } from '../core/services/common/api-error-handler.service';
import { CommonModule } from '@angular/common';
import { ThemeService } from '../core/services/theme.service';
@Component({
selector: 'app-register',
imports: [AngularMaterialModule, ReactiveFormsModule, CommonModule],
templateUrl: './register.component.html',
styleUrl: './register.component.scss'
})
export class RegisterComponent {
registrationForm: FormGroup;
isLoading = false;
hidePassword = true;
tokenError: boolean = false;
token: string | null = null;
private fb = inject(FormBuilder);
private authService = inject(AuthService);
private router = inject(Router);
private route = inject(ActivatedRoute);
private notificationService = inject(NotificationService);
private errorHandler = inject(ApiErrorHandlerService);
private themeService = inject(ThemeService);
constructor() {
this.registrationForm = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required]]
});
// 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;
}
}
ngOnInit(): void {
this.themeService.setTheme('default');
}
onSubmit(): void {
if (this.registrationForm.invalid) {
this.registrationForm.markAllAsTouched();
return;
}
this.isLoading = true;
const { email, password } = this.registrationForm.value;
this.authService.register(email, password, this.token).subscribe({
next: () => {
this.notificationService.showSuccess('Registration successful');
this.router.navigate(['/login']);
},
error: (error) => {
this.isLoading = false;
let errorMessage = `Registration failed. Please request a new registration link from your administrator.`;
this.notificationService.showError(errorMessage);
console.error('Registration failed:', error);
}
});
}
get email() {
return this.registrationForm.get('email');
}
get password() {
return this.registrationForm.get('password');
}
}