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 { AppIdGuard } from './guards/appid.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 = [
{ path: 'login', component: LoginComponent },
{ path: '404', component: NotFoundComponent },
{ path: 'login', loadComponent: () => import('./login/login.component').then(m => m.LoginComponent) },
{ 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',
children: [
{ path: 'home', component: HomeComponent },
{ path: 'usersettings', component: UserSettingsComponent },
{ path: 'home', loadComponent: () => import('./home/home.component').then(m => m.HomeComponent) },
{ 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: 'edit-carnet/:headerid', loadComponent: () => import('./carnet/edit/edit-carnet.component').then(m => m.EditCarnetComponent) },
{ path: '', redirectTo: 'home', pathMatch: 'full' }
],
canActivate: [AuthGuard, AppIdGuard]

View File

@ -1,64 +1,21 @@
<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) -->
<mat-stepper *ngIf="questionsCompleted" orientation="vertical" [linear]="isLinear"
(selectionChange)="onStepChange($event)" [selectedIndex]="currentStep">
<mat-stepper orientation="vertical" [linear]="isLinear" (selectionChange)="onStepChange($event)"
[selectedIndex]="currentStep">
<!-- 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>
<app-application [isEditMode]="isEditMode" (applicationIdCreated)="onApplicationDetailCreated($event)">
<app-application [isEditMode]="isEditMode" (headerIdCreated)="onApplicationDetailCreated($event)">
</app-application>
</mat-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>
<app-holder (completed)="onHolderSelectionSaved($event)">
<app-holder (completed)="onHolderSelectionSaved($event)" [headerid]="headerid">
</app-holder>
</mat-step>
@ -66,44 +23,45 @@
<mat-step *ngIf="applicationType === 'new'" [completed]="stepsCompleted.goodsSection"
[editable]="stepsCompleted.applicationDetail">
<ng-template matStepLabel>Goods Section</ng-template>
<app-goods (completed)="onGoodsSectionSaved($event)">
<app-goods (completed)="onGoodsSectionSaved($event)" [headerid]="headerid">
</app-goods>
</mat-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">
<ng-template matStepLabel>Travel Plan</ng-template>
<!-- <app-travel-plan (completed)="onTravelPlanSaved($event)">
</app-travel-plan> -->
<app-travel-plan (completed)="onTravelPlanSaved($event)" [headerid]="headerid">
</app-travel-plan>
</mat-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>
<!-- <app-insurance (completed)="onInsuranceSaved($event)">
</app-insurance> -->
<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)">
</app-shipping> -->
<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-method (completed)="onDeliveryMethodSaved($event)">
</app-delivery-method> -->
<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)">
</app-payment> -->
<app-payment (completed)="onPaymentSaved($event)" [headerid]="headerid">
</app-payment>
</mat-step>
</mat-stepper>
</div>

View File

@ -1,51 +1,11 @@
.client-carnet-container {
// padding: 24px;
// max-width: 1200px;
// margin: 0 auto;
// .client-carnet-container {
// // padding: 24px;
// // max-width: 1200px;
// // margin: 0 auto;
.questions-section {
// padding: 20px;
// border-radius: 8px;
// margin-bottom: 24px;
// h2 {
// margin-bottom: 24px;
// color: #3f51b5;
// .actions {
// margin-top: 24px;
// display: flex;
// justify-content: flex-start;
// }
.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 { 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';
@Component({
selector: 'app-add-carnet',
imports: [AngularMaterialModule, CommonModule, ReactiveFormsModule,
ApplicationComponent, HolderComponent, GoodsComponent],
ApplicationComponent, HolderComponent, GoodsComponent, TravelPlanComponent, InsuranceComponent,
ShippingComponent, DeliveryComponent, PaymentComponent],
templateUrl: './add-carnet.component.html',
styleUrl: './add-carnet.component.scss'
})
export class AddCarnetComponent {
currentStep = 0;
questionsCompleted = false;
isLinear = true;
applicationType: 'new' | 'additional' | 'duplicate' | 'extend' | null = null;
applicationType: 'new' | 'additional' | 'duplicate' | 'extend' | null = 'new';
isEditMode = false;
applicationid: 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]
});
headerid: number = 0;
// Track completion of each step
stepsCompleted = {
@ -44,30 +39,12 @@ export class AddCarnetComponent {
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 {
this.currentStep = event.selectedIndex;
}
onApplicationDetailCreated(applicationid: number): void {
this.applicationid = applicationid;
onApplicationDetailCreated(headerid: number): void {
this.headerid = this.headerid;
this.stepsCompleted.applicationDetail = true;
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 { AngularMaterialModule } from '../../shared/module/angular-material.module';
import { CommonModule } from '@angular/common';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-application',
@ -16,9 +17,10 @@ import { CommonModule } from '@angular/common';
})
export class ApplicationComponent {
@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;
isLoading = false;
@ -30,31 +32,33 @@ export class ApplicationComponent {
private applicationDetailService = inject(ApplicationDetailService);
private notificationService = inject(NotificationService);
private errorHandler = inject(ApiErrorHandlerService);
private route = inject(ActivatedRoute);
constructor() {
this.applicationDetailsForm = this.createForm();
}
ngOnInit(): void {
// this.spidCreated.emit(this.spid?.toString());
this.applicationName = this.route.snapshot.queryParamMap.get('applicationName') || '';
// Patch edit form data
if (this.applicationid > 0) {
this.isLoading = true;
this.applicationDetailService.getApplicationDetailsById(this.applicationid).subscribe({
next: (applicationDetail: ApplicationDetail) => {
if (applicationDetail?.applicationId > 0) {
this.patchFormData(applicationDetail);
}
this.isLoading = false;
},
error: (error: any) => {
let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load application details');
this.notificationService.showError(errorMessage);
this.isLoading = false;
console.error('Error loading application details:', error);
}
});
if (this.applicationName) {
this.patchFormData();
// this.applicationDetailService.getApplicationDetailsById(this.headerid).subscribe({
// next: (applicationDetail: ApplicationDetail) => {
// if (applicationDetail?.applicationId > 0) {
// this.patchFormData(applicationDetail);
// }
// this.isLoading = false;
// },
// error: (error: any) => {
// let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load application details');
// this.notificationService.showError(errorMessage);
// 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({
name: data.name,
name: this.applicationName,
});
if (this.isEditMode) {
@ -93,12 +97,12 @@ export class ApplicationComponent {
const applicationDetailData: ApplicationDetail = this.applicationDetailsForm.value;
if (!this.isEditMode && this.applicationid == 0) {
if (!this.isEditMode && this.headerid == 0) {
this.isLoading = true;
this.applicationDetailService.createApplicationDetails(applicationDetailData).subscribe({
next: (applicationData: any) => {
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.disableSaveButton = true;
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({
selector: 'app-delivery',
@ -7,5 +7,7 @@ import { Component } from '@angular/core';
styleUrl: './delivery.component.scss'
})
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({
selector: 'app-edit-carnet',
imports: [],
imports: [AngularMaterialModule, CommonModule, ReactiveFormsModule,
ApplicationComponent, HolderComponent, GoodsComponent, TravelPlanComponent, InsuranceComponent,
ShippingComponent, DeliveryComponent, PaymentComponent],
templateUrl: './edit-carnet.component.html',
styleUrl: './edit-carnet.component.scss'
})
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'
})
export class GoodsComponent {
@Input() applicationid: number = 0;
@Input() headerid: number = 0;
@Output() completed = new EventEmitter<boolean>();
}

View File

@ -7,6 +7,6 @@ import { Component, EventEmitter, Input, Output } from '@angular/core';
styleUrl: './holder.component.scss'
})
export class HolderComponent {
@Input() applicationid: number = 0;
@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({
selector: 'app-insurance',
@ -7,5 +7,6 @@ import { Component } from '@angular/core';
styleUrl: './insurance.component.scss'
})
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({
selector: 'app-payment',
@ -7,5 +7,6 @@ import { Component } from '@angular/core';
styleUrl: './payment.component.scss'
})
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({
selector: 'app-shipping',
@ -7,5 +7,6 @@ import { Component } from '@angular/core';
styleUrl: './shipping.component.scss'
})
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({
selector: 'app-travel-plan',
@ -7,5 +7,6 @@ import { Component } from '@angular/core';
styleUrl: './travel-plan.component.scss'
})
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()
export class AuthInterceptor implements HttpInterceptor {
private excludedUrls = ['/register', '/forgot-password'];
private isRefreshing = false;
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>> {
// 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({
withCredentials: true
});

View File

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

View File

@ -15,29 +15,37 @@ export class ApplicationDetailService {
private http = inject(HttpClient);
private userService = inject(UserService);
getApplicationDetailsById(id: number): ApplicationDetail | any {
return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetPreparerByClientid/${this.userService.getUserSpid()}/${id}`).pipe(
filter(response => response.length > 0),
map(response => this.mapToApplicationDetail(response?.[0])));
}
// getApplicationDetailsById(id: number): ApplicationDetail | any {
// return this.http.get<any[]>(`${this.apiUrl}/${this.apiDb}/GetPreparerByClientid/${this.userService.getUserSpid()}/${id}`).pipe(
// filter(response => response.length > 0),
// map(response => this.mapToApplicationDetail(response?.[0])));
// }
private mapToApplicationDetail(applicationDetails: any): ApplicationDetail {
return {
clientid: applicationDetails.CLIENTID,
spid: applicationDetails.SPID,
name: applicationDetails.P_APPLICATIONNAME,
applicationId: applicationDetails.P_HEADERID
};
}
// private mapToApplicationDetail(applicationDetails: any): ApplicationDetail {
// return {
// clientid: applicationDetails.CLIENTID,
// spid: applicationDetails.SPID,
// name: applicationDetails.P_APPLICATIONNAME,
// applicationId: applicationDetails.P_HEADERID
// };
// }
createApplicationDetails(data: ApplicationDetail): Observable<any> {
const userDetails = this.userService.getUserDetails();
if (!userDetails || !userDetails.userDetails) {
throw new Error('User details are not available');
}
const applicationDetails = {
P_SPID: this.userService.getUserSpid(),
P_CLIENTID: this.userService.getUserClientid(),
P_SPID: userDetails.userDetails.spid,
P_CLIENTID: userDetails.userDetails.clientid,
P_LOCATIONID: userDetails.userDetails.locationid,
P_APPLICATIONNAME: data.name,
P_ORDERTYPE: 'ORIGINAL',
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 { Observable } from 'rxjs';
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 apiDb = environment.apiDb;
private spid: number = 0;
private clientid: number = 0;
userDetailsSignal = signal<User>({});
private readonly USER_EMAIL_KEY = 'CurrentUserEmail';
@ -98,23 +95,18 @@ export class UserService {
}
getUserSpid(): number {
if (this.spid === 0) {
const userDetails = this.getUserDetails();
if (userDetails && userDetails.userDetails && userDetails.userDetails.spid) {
this.spid = userDetails.userDetails.spid;
}
}
return this.spid;
return userDetails?.userDetails?.spid ?? 0;
}
getUserClientid(): number {
if (this.clientid === 0) {
const userDetails = this.getUserDetails();
if (userDetails && userDetails.userDetails && userDetails.userDetails.clientid) {
this.clientid = userDetails.userDetails.clientid;
return userDetails?.userDetails?.clientid ?? 0;
}
}
return this.clientid;
getUserLocationid(): number {
const userDetails = this.getUserDetails();
return userDetails?.userDetails?.locationid ?? 0;
}
private mapToUser(data: any): User {
@ -141,6 +133,7 @@ export class UserService {
return {
spid: userDetails.SPID,
clientid: userDetails.CLIENTID,
locationid: userDetails.LOCATIONID,
urlKey: userDetails.ENCURLKEY,
logoName: userDetails.LOGONAME,
themeName: userDetails.THEMENAME

View File

@ -37,7 +37,8 @@ export class HomeService {
issueDate: item.ISSUEDATE || null,
expiryDate: item.EXPDATE || null,
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 { CarnetStatus } from '../../core/models/carnet-status';
import { NavigationService } from '../../core/services/common/navigation.service';
import { UserService } from '../../core/services/common/user.service';
@Component({
selector: 'app-chart',
@ -18,6 +19,7 @@ export class ChartComponent {
@Output() carnetStatusData = new EventEmitter<any>();
private navigationService = inject(NavigationService);
private userService = inject(UserService);
navigateToManageProvider(spid: number): void {
this.navigationService.navigate(['service-provider', spid]);
@ -98,8 +100,8 @@ export class ChartComponent {
});
return {
title: provider.Service_Provider_Name,
spid: provider.SPID,
title: provider.Service_Provider_Name ?? provider.Preparer_Name,
spid: provider.SPID ?? this.userService.getUserSpid(),
data: {
labels: labels,
datasets: [{

View File

@ -1,17 +1,48 @@
<div class="dashboard-container">
<div class="content">
<div class="quick-actions">
<button mat-raised-button (click)="navigateTo('add-carnet')" color="accent">
<mat-icon>add</mat-icon> Add New Carnet
</button>
</div>
<div class="app-container">
<!-- Chart Section -->
<div class="chart-section">
<app-chart [chartData]="carnetData" [carnetStatuses]="carnetStatuses"
(carnetStatusData)="onCarnetStatusClick($event)"></app-chart>
</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 -->
<div class="carnet-details-section">
<div class="loading-shade" *ngIf="isLoading">
@ -79,7 +110,7 @@
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>Actions</th>
<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'"
[hidden]="getCarnetStatusLabel(item.carnetStatus) === 'Pending'" matTooltip="Edit">
<mat-icon>edit</mat-icon>

View File

@ -1,12 +1,42 @@
.dashboard-container {
padding: 24px;
// padding: 24px;
.quick-actions {
// .quick-actions {
// display: flex;
// justify-content: end;
// button {
// margin-bottom: 16px;
// }
// }
.app-container {
display: flex;
justify-content: end;
gap: 16px;
box-sizing: border-box;
button {
margin-bottom: 16px;
.chart-section {
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 { NotificationService } from '../core/services/common/notification.service';
import { NavigationService } from '../core/services/common/navigation.service';
import { MatRadioChange } from '@angular/material/radio';
@Component({
selector: 'app-home',
@ -111,6 +112,29 @@ export class HomeComponent {
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 {
this.navigationService.navigate([route]);
}

View File

@ -22,7 +22,7 @@
</mat-form-field>
<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>
<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 { User } from '../core/models/user';
import { ThemeService } from '../core/services/theme.service';
import { RouterModule } from '@angular/router';
@Component({
selector: 'app-login',
imports: [ReactiveFormsModule, CommonModule, AngularMaterialModule],
imports: [ReactiveFormsModule, CommonModule, AngularMaterialModule, RouterModule],
templateUrl: './login.component.html',
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');
}
}