diff --git a/package-lock.json b/package-lock.json index bfbadb3..bad5f70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,9 +23,10 @@ "chart.js": "^4.3.0", "express": "^4.18.2", "ng2-charts": "^8.0.0", - "ngx-cookie-service": "^19.1.2", + "ngx-cookie-service": "^20.0.1", "rxjs": "~7.8.0", "tslib": "^2.3.0", + "xlsx": "^0.18.5", "zone.js": "~0.15.0" }, "devDependencies": { @@ -3657,6 +3658,15 @@ "node": ">=0.4.0" } }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/agent-base": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", @@ -4131,6 +4141,19 @@ ], "license": "CC-BY-4.0" }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/chalk": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", @@ -4333,6 +4356,15 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -4533,6 +4565,18 @@ "node": ">= 0.10" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -5278,6 +5322,15 @@ "node": ">= 0.6" } }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -7309,16 +7362,16 @@ } }, "node_modules/ngx-cookie-service": { - "version": "19.1.2", - "resolved": "https://registry.npmjs.org/ngx-cookie-service/-/ngx-cookie-service-19.1.2.tgz", - "integrity": "sha512-4BFbc5BA7/ryuQVZUfDMHcjdgkHeW8obqfCCknoOEaplefjzlVvMQUx9Hywfjbc8POzZeZD2csS43Hs7rscJcA==", + "version": "20.0.1", + "resolved": "https://registry.npmjs.org/ngx-cookie-service/-/ngx-cookie-service-20.0.1.tgz", + "integrity": "sha512-XTPrW/5ihI3DvTljDj14E501fouHdiONCnd1SPhvqyHNHjvKECFWuTzOpcHrWl9X1ZOKfOG/uXW8G8fINcL9fQ==", "license": "MIT", "dependencies": { "tslib": "^2.8.0" }, "peerDependencies": { - "@angular/common": "^19.0.0", - "@angular/core": "^19.0.0" + "@angular/common": "^20.0.0", + "@angular/core": "^20.0.0" } }, "node_modules/node-addon-api": { @@ -8793,6 +8846,18 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/ssri": { "version": "12.0.0", "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", @@ -9487,6 +9552,24 @@ "which": "bin/which" } }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", @@ -9669,6 +9752,27 @@ "node": ">= 6" } }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 3948eee..e62eeb6 100644 --- a/package.json +++ b/package.json @@ -26,9 +26,10 @@ "chart.js": "^4.3.0", "express": "^4.18.2", "ng2-charts": "^8.0.0", - "ngx-cookie-service": "^19.1.2", + "ngx-cookie-service": "^20.0.1", "rxjs": "~7.8.0", "tslib": "^2.3.0", + "xlsx": "^0.18.5", "zone.js": "~0.15.0" }, "devDependencies": { diff --git a/src/app/app.routes.server.ts b/src/app/app.routes.server.ts index c967cf2..23350c9 100644 --- a/src/app/app.routes.server.ts +++ b/src/app/app.routes.server.ts @@ -1,6 +1,7 @@ import { RenderMode, ServerRoute } from '@angular/ssr'; export const serverRoutes: ServerRoute[] = [ + { path: 'forgot-password/:token', renderMode: RenderMode.Client }, { path: ':appId', renderMode: RenderMode.Client }, { path: ':appId/home', renderMode: RenderMode.Client }, { path: ':appId/usersettings', renderMode: RenderMode.Client }, diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 0636455..d36dc1c 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -1,27 +1,22 @@ import { Routes } from '@angular/router'; -import { LoginComponent } from './login/login.component'; import { AuthGuard } from './guards/auth.guard'; -import { NotFoundComponent } from './shared/components/not-found/not-found.component'; -import { HomeComponent } from './home/home.component'; import { AppIdGuard } from './guards/appid.guard'; -import { EditServiceProviderComponent } from './service-provider/edit/edit-service-provider.component'; -import { UserSettingsComponent } from './user-settings/user-settings.component'; -import { ManagePreparerComponent } from './preparer/manage/manage-preparer.component'; -import { EditPreparerComponent } from './preparer/edit/edit-preparer.component'; -import { AddPreparerComponent } from './preparer/add/add-preparer.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/:token', loadComponent: () => import('./forgot-password/forgot-password.component').then(m => m.ForgotPasswordComponent) }, + { path: 'forgot-password', loadComponent: () => import('./forgot-password/forgot-password.component').then(m => m.ForgotPasswordComponent) }, + { path: 'register', 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: 'service-provider/:id', component: EditServiceProviderComponent }, - { path: 'preparer', component: ManagePreparerComponent }, - { path: 'preparer/:id', component: EditPreparerComponent }, - { path: 'add-preparer', component: AddPreparerComponent }, + { 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: 'service-provider/:id', loadComponent: () => import('./service-provider/edit/edit-service-provider.component').then(m => m.EditServiceProviderComponent) }, + { path: 'preparer', loadComponent: () => import('./preparer/manage/manage-preparer.component').then(m => m.ManagePreparerComponent) }, + { path: 'preparer/:id', loadComponent: () => import('./preparer/edit/edit-preparer.component').then(m => m.EditPreparerComponent) }, + { path: 'add-preparer', loadComponent: () => import('./preparer/add/add-preparer.component').then(m => m.AddPreparerComponent) }, { path: '', redirectTo: 'home', pathMatch: 'full' } ], canActivate: [AuthGuard, AppIdGuard] diff --git a/src/app/core/interceptors/auth.interceptor.ts b/src/app/core/interceptors/auth.interceptor.ts index 575b438..aaa74ae 100644 --- a/src/app/core/interceptors/auth.interceptor.ts +++ b/src/app/core/interceptors/auth.interceptor.ts @@ -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 = new BehaviorSubject(null); @@ -17,6 +18,11 @@ export class AuthInterceptor implements HttpInterceptor { intercept(request: HttpRequest, next: HttpHandler): Observable> { + // Add withCredentials to all requests except excluded URLs + if (this.excludedUrls.some(url => request.url.includes(url))) { + return next.handle(request); + } + // Add withCredentials to all requests request = request.clone({ withCredentials: true diff --git a/src/app/core/models/preparer/contact.ts b/src/app/core/models/preparer/contact.ts index f40d6fc..3787659 100644 --- a/src/app/core/models/preparer/contact.ts +++ b/src/app/core/models/preparer/contact.ts @@ -2,6 +2,7 @@ export interface Contact { clientContactId: number; spid: number clientid: number; + locationid: number; defaultContact: boolean; firstName: string; lastName: string; diff --git a/src/app/core/models/user.ts b/src/app/core/models/user.ts index 837eb73..fb70884 100644 --- a/src/app/core/models/user.ts +++ b/src/app/core/models/user.ts @@ -13,6 +13,7 @@ export interface Menu { export interface UserDetail { spid: number; clientid: number; + locationid: number; urlKey: string; logoName: string; themeName: string; diff --git a/src/app/core/services/common/auth.service.ts b/src/app/core/services/common/auth.service.ts index 4128df0..1f34416 100644 --- a/src/app/core/services/common/auth.service.ts +++ b/src/app/core/services/common/auth.service.ts @@ -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,33 @@ export class AuthService { }); } } + + register(email: string, password: string): Observable { + + return this.http.post( + `${this.apiUrl}/register`, + { P_EMAILADDR: email, P_PASSWORD: password } + ); + } + + forgotPassword(email: string, password: string, token: string): Observable { + let headers = new HttpHeaders(); + + if (token) { + headers = headers.set('Authorization', `Bearer ${token}`); + } + + return this.http.put( + `${this.apiUrl}/forgot-password`, + { P_EMAILADDR: email, P_PASSWORD: password }, + { headers } + ); + } + + sendEmail(email: string, type: string): Observable { + return this.http.post( + `${this.apiUrl}/sendmail`, + { P_TO: email, P_MAIL_TYPE: type } + ); + } } diff --git a/src/app/core/services/common/user.service.ts b/src/app/core/services/common/user.service.ts index 759f18f..89972d9 100644 --- a/src/app/core/services/common/user.service.ts +++ b/src/app/core/services/common/user.service.ts @@ -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({}); 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; + const userDetails = this.getUserDetails(); + 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 this.clientid; + const userDetails = this.getUserDetails(); + return userDetails?.userDetails?.clientid ?? 0; + } + + 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 diff --git a/src/app/core/services/preparer/contact.service.ts b/src/app/core/services/preparer/contact.service.ts index dfbae4a..856b780 100644 --- a/src/app/core/services/preparer/contact.service.ts +++ b/src/app/core/services/preparer/contact.service.ts @@ -26,6 +26,7 @@ export class ContactService { clientContactId: contact.CLIENTCONTACTID, spid: contact.SPID, clientid: contact.CLIENTID, + locationid: contact.LOCATIONID, defaultContact: contact.DEFCONTACTFLAG === 'Y', firstName: contact.FIRSTNAME, lastName: contact.LASTNAME, @@ -45,10 +46,10 @@ export class ContactService { })); } - createContact(clientid: number, data: Contact): Observable { + createContact(data: Contact): Observable { const contact = { P_SPID: this.userService.getUserSpid(), - P_CLIENTID: clientid, + P_LOCATIONID: data.locationid, P_DEFCONTACTFLAG: data.defaultContact ? 'Y' : 'N', P_CONTACTSTABLE: [{ P_FIRSTNAME: data.firstName, @@ -70,6 +71,7 @@ export class ContactService { const contact = { P_SPID: this.userService.getUserSpid(), P_CLIENTCONTACTID: spContactId, + P_LOCATIONID: data.locationid, P_FIRSTNAME: data.firstName, P_LASTNAME: data.lastName, P_MIDDLEINITIAL: data.middleInitial, diff --git a/src/app/forgot-password/forgot-password.component.html b/src/app/forgot-password/forgot-password.component.html new file mode 100644 index 0000000..dfdd1b3 --- /dev/null +++ b/src/app/forgot-password/forgot-password.component.html @@ -0,0 +1,105 @@ +
+
+

+ {{forgotPasswordRequestCompleted ? 'Reset Password' : 'Forgot Password'}} +

+ +
+ + Email + + email + + Email is required + + + Please enter a valid email + + + + + + +
+ +
+ + Email + + email + + Email is required + + + Please enter a valid email + + + + + Password + + + + Password is required + + + Password must be at least 8 characters + + + Password must contain at least one uppercase letter, one lowercase letter, one number, and one + special character + + + + + Confirm Password + + + + Please confirm your password + + + Passwords do not match + + + + + + +
+
+ +
+

ATA Carnet

+

+ 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. +

+

+ 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. +

+
+
\ No newline at end of file diff --git a/src/app/forgot-password/forgot-password.component.scss b/src/app/forgot-password/forgot-password.component.scss new file mode 100644 index 0000000..4415baa --- /dev/null +++ b/src/app/forgot-password/forgot-password.component.scss @@ -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; + } + } +} \ No newline at end of file diff --git a/src/app/forgot-password/forgot-password.component.ts b/src/app/forgot-password/forgot-password.component.ts new file mode 100644 index 0000000..c82fb19 --- /dev/null +++ b/src/app/forgot-password/forgot-password.component.ts @@ -0,0 +1,126 @@ +import { Component, inject } from '@angular/core'; +import { FormGroup, FormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ApiErrorHandlerService } from '../core/services/common/api-error-handler.service'; +import { AuthService } from '../core/services/common/auth.service'; +import { NotificationService } from '../core/services/common/notification.service'; +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 { + forgotPasswordRequestCompleted: boolean = false; + forgotPasswordRequestForm: FormGroup; + forgotPasswordForm: FormGroup; + isLoading = false; + hidePassword = true; + token: string | null = null; + + private fb = inject(FormBuilder); + private authService = inject(AuthService); + private router = inject(Router); + private notificationService = inject(NotificationService); + private errorHandler = inject(ApiErrorHandlerService); + private themeService = inject(ThemeService); + private route = inject(ActivatedRoute); + + constructor() { + this.forgotPasswordRequestForm = this.fb.group({ + email: ['', [Validators.required, Validators.email]] + }); + + this.forgotPasswordForm = this.fb.group({ + email: ['', [Validators.required, Validators.email]], + password: ['', [Validators.required, Validators.minLength(8), + Validators.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]+$/) + ]], + confirmPassword: ['', [Validators.required]] + }, { validator: this.passwordMatchValidator }); + + // // Get token from URL if present + this.token = this.route.snapshot.paramMap.get('token'); + + // Check if token is null or empty + if (this.token && this.token.trim() !== '') { + this.forgotPasswordRequestCompleted = true; + } + } + + ngOnInit(): void { + this.themeService.setTheme('default'); + } + + onRequest(): void { + if (this.forgotPasswordRequestForm.invalid) { + this.forgotPasswordRequestForm.markAllAsTouched(); + return; + } + + this.isLoading = true; + const { email } = this.forgotPasswordRequestForm.value; + + this.authService.sendEmail(email, 'FORGOT_PASSWORD').subscribe({ + next: () => { + this.notificationService.showSuccess('A mail has been sent successfully to your email to setup a new password.'); + this.isLoading = false; + }, + error: (error) => { + this.isLoading = false; + let errorMessage = this.errorHandler.handleApiError(error, `Request failed. Please try again.`); + this.notificationService.showError(errorMessage); + console.error('Forgot password request failed:', error); + } + }); + } + + onSubmit(): void { + if (this.forgotPasswordForm.invalid) { + this.forgotPasswordForm.markAllAsTouched(); + return; + } + + this.isLoading = true; + const { email, password } = this.forgotPasswordForm.value; + + this.authService.forgotPassword(email, password, this.token!).subscribe({ + next: () => { + this.notificationService.showSuccess('Password reset successfully'); + this.router.navigate(['/login']); + }, + error: (error) => { + this.isLoading = false; + let errorMessage = this.errorHandler.handleApiError(error, `Password request failed. Please try again.`); + this.notificationService.showError(errorMessage); + console.error('Forgot password request failed:', error); + } + }); + } + + private passwordMatchValidator(formGroup: FormGroup) { + const password = formGroup.get('password')?.value; + const confirmPassword = formGroup.get('confirmPassword')?.value; + return password === confirmPassword ? null : { mismatch: true }; + } + + get emailRequest() { + return this.forgotPasswordRequestForm.get('email'); + } + + get email() { + return this.forgotPasswordForm.get('email'); + } + + get password() { + return this.forgotPasswordForm.get('password'); + } + + get confirmPassword() { + return this.forgotPasswordForm.get('confirmPassword'); + } +} diff --git a/src/app/home/home.component.html b/src/app/home/home.component.html index 5aa4a5a..bf66771 100644 --- a/src/app/home/home.component.html +++ b/src/app/home/home.component.html @@ -8,7 +8,8 @@
- +
@@ -17,6 +18,13 @@ +
+ +
+
diff --git a/src/app/home/home.component.scss b/src/app/home/home.component.scss index d909936..8b48ece 100644 --- a/src/app/home/home.component.scss +++ b/src/app/home/home.component.scss @@ -10,6 +10,15 @@ } } + .table-actions { + display: flex; + justify-content: end; + + button { + margin-bottom: 16px; + } + } + .table-container { position: relative; overflow: auto; diff --git a/src/app/home/home.component.ts b/src/app/home/home.component.ts index 9eb5fa2..560d4d6 100644 --- a/src/app/home/home.component.ts +++ b/src/app/home/home.component.ts @@ -13,6 +13,7 @@ import { CarnetStatus } from '../core/models/carnet-status'; import { ApiErrorHandlerService } from '../core/services/common/api-error-handler.service'; import { CommonService } from '../core/services/common/common.service'; import { NotificationService } from '../core/services/common/notification.service'; +import * as XLSX from 'xlsx'; @Component({ selector: 'app-home', @@ -102,6 +103,40 @@ export class HomeComponent { } exportData() { + try { + // Prepare worksheet data + const worksheetData = this.prepareDownloadData(); + + // Create workbook and worksheet + const workbook = XLSX.utils.book_new(); + const worksheet = XLSX.utils.json_to_sheet(worksheetData); + + // Add worksheet to workbook + XLSX.utils.book_append_sheet(workbook, worksheet, 'Carnet Data'); + + // Generate Excel file + const fileName = `carnet_data_${new Date().toISOString()}.xlsx`; + XLSX.writeFile(workbook, fileName); + } catch (error) { + console.error('Error downloading goods items:', error); + this.notificationService.showError('Failed to download Excel file'); + } + } + + prepareDownloadData(): any[] { + return this.dataSource.data.map(item => ({ + 'Application Name': item.applicationName, + 'Holder Name': item.holderName, + 'Carnet Number': item.carnetNumber, + 'US Sets': item.usSets, + 'Foreign Sets': item.foreignSets, + 'Transit Sets': item.transitSets, + 'Carnet Value': item.carnetValue, + 'Issue Date': item.issueDate ? new Date(item.issueDate).toLocaleDateString() : '', + 'Expiry Date': item.expiryDate ? new Date(item.expiryDate).toLocaleDateString() : '', + 'Order Type': item.orderType, + 'Carnet Status': this.getCarnetStatusLabel(item.carnetStatus) + })); } getCarnetStatusLabel(value: string): string { diff --git a/src/app/login/login.component.html b/src/app/login/login.component.html index d234c5c..3edc5da 100644 --- a/src/app/login/login.component.html +++ b/src/app/login/login.component.html @@ -21,8 +21,12 @@ -
- Forgot your password? + + +
\ No newline at end of file diff --git a/src/app/preparer/contacts/contacts.component.html b/src/app/preparer/contacts/contacts.component.html index 3b0641b..782e1b0 100644 --- a/src/app/preparer/contacts/contacts.component.html +++ b/src/app/preparer/contacts/contacts.component.html @@ -15,6 +15,12 @@
+ + + + + + @@ -158,6 +164,18 @@ Maximum 100 characters allowed + + + Location + + + {{ location.name }} + + + + Location is required + +
diff --git a/src/app/preparer/contacts/contacts.component.ts b/src/app/preparer/contacts/contacts.component.ts index ef90cc1..db7dc6b 100644 --- a/src/app/preparer/contacts/contacts.component.ts +++ b/src/app/preparer/contacts/contacts.component.ts @@ -15,6 +15,8 @@ import { ApiErrorHandlerService } from '../../core/services/common/api-error-han import { Contact } from '../../core/models/preparer/contact'; import { ConfirmDialogComponent } from '../../shared/components/confirm-dialog/confirm-dialog.component'; import { ContactLogin } from '../../core/models/preparer/contact-login'; +import { LocationService } from '../../core/services/preparer/location.service'; +import { Location } from '../../core/models/preparer/location'; @Component({ selector: 'app-contacts', @@ -27,7 +29,7 @@ export class ContactsComponent { @ViewChild(MatPaginator) paginator!: MatPaginator; @ViewChild(MatSort) sort!: MatSort; - displayedColumns: string[] = ['firstName', 'lastName', 'title', 'phone', 'mobile', 'email', 'defaultContact', 'actions']; + displayedColumns: string[] = ['location', 'firstName', 'lastName', 'title', 'phone', 'mobile', 'email', 'defaultContact', 'actions']; dataSource = new MatTableDataSource(); contactForm: FormGroup; contactLoginForm: FormGroup; @@ -38,6 +40,8 @@ export class ContactsComponent { showLoginForm = false showInactiveContacts = false; contacts: Contact[] = []; + locations: Location[] = []; + contactReadOnlyFields: any = { lastChangedDate: null, lastChangedBy: null, @@ -58,6 +62,7 @@ export class ContactsComponent { private dialog = inject(MatDialog); private notificationService = inject(NotificationService); private errorHandler = inject(ApiErrorHandlerService); + private locationService = inject(LocationService); constructor() { this.contactForm = this.fb.group({ @@ -69,6 +74,7 @@ export class ContactsComponent { mobile: ['', [Validators.required, Validators.pattern(/^[0-9]{10,15}$/)]], fax: ['', [Validators.pattern(/^[0-9]{10,15}$/)]], email: ['', [Validators.required, Validators.email, Validators.maxLength(100)]], + location: ['', [Validators.required]], defaultContact: [false] }); @@ -78,7 +84,7 @@ export class ContactsComponent { } ngOnInit(): void { - this.loadContacts(); + this.loadLocations(); } ngAfterViewInit() { @@ -86,6 +92,23 @@ export class ContactsComponent { this.dataSource.sort = this.sort; } + loadLocations(): void { + this.isLoading = true; + this.locationService.getLocationsById(this.clientid).subscribe({ + next: (locations: Location[]) => { + this.locations = locations; + this.loadContacts(); + this.isLoading = false; + }, + error: (error: any) => { + let errorMessage = this.errorHandler.handleApiError(error, 'Failed to load locations'); + this.notificationService.showError(errorMessage); + this.isLoading = false; + console.error('Error loading locations:', error); + } + }); + } + loadContacts(): void { this.isLoading = true; @@ -126,7 +149,8 @@ export class ContactsComponent { phone: contact.phone, mobile: contact.mobile, fax: contact.fax, - email: contact.email + email: contact.email, + location: contact.locationid, // defaultContact: contact.defaultContact }); @@ -148,7 +172,7 @@ export class ContactsComponent { const saveObservable = this.isEditing && (this.currentContactId! > 0) ? this.contactService.updateContact(this.currentContactId!, contactData) - : this.contactService.createContact(this.clientid, contactData); + : this.contactService.createContact(contactData); saveObservable.subscribe({ next: () => { @@ -234,6 +258,11 @@ export class ContactsComponent { }); } + getLocationLabel(value: number): string { + const location = this.locations.find(l => l.locationid === value); + return location ? location.name : ''; + } + createLogin(contact: Contact): void { this.showForm = false; this.showLoginForm = true; diff --git a/src/app/preparer/edit/edit-preparer.component.html b/src/app/preparer/edit/edit-preparer.component.html index 86c8a3b..9dba714 100644 --- a/src/app/preparer/edit/edit-preparer.component.html +++ b/src/app/preparer/edit/edit-preparer.component.html @@ -13,12 +13,6 @@ - - - Contacts - - - @@ -26,4 +20,12 @@ + + + + Contacts + + + + \ No newline at end of file diff --git a/src/app/register/register.component.html b/src/app/register/register.component.html new file mode 100644 index 0000000..8a053e2 --- /dev/null +++ b/src/app/register/register.component.html @@ -0,0 +1,76 @@ +
+
+

User Registration

+ +
+ + Email + + email + + Email is required + + + Please enter a valid email + + + + + Password + + + + Password is required + + + Password must be at least 8 characters + + + Password must contain at least one uppercase letter, one lowercase letter, one number, and one + special character + + + + + Confirm Password + + + + Please confirm your password + + + Passwords do not match + + + + + + + + +
+ +
+

ATA Carnet

+

+ 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. +

+

+ 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. +

+
+
\ No newline at end of file diff --git a/src/app/register/register.component.scss b/src/app/register/register.component.scss new file mode 100644 index 0000000..41be398 --- /dev/null +++ b/src/app/register/register.component.scss @@ -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; + } + } +} \ No newline at end of file diff --git a/src/app/register/register.component.ts b/src/app/register/register.component.ts new file mode 100644 index 0000000..e7e2e55 --- /dev/null +++ b/src/app/register/register.component.ts @@ -0,0 +1,96 @@ +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, Validators.minLength(8), + Validators.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]+$/) + ]], + confirmPassword: ['', [Validators.required]] + }, { validator: this.passwordMatchValidator }); + + // // Get token from URL if present + // this.token = this.route.snapshot.paramMap.get('token'); + + // // Check if token is null or empty + // if (!this.token || this.token.trim() === '') { + // this.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).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); + } + }); + } + + private passwordMatchValidator(formGroup: FormGroup) { + const password = formGroup.get('password')?.value; + const confirmPassword = formGroup.get('confirmPassword')?.value; + return password === confirmPassword ? null : { mismatch: true }; + } + + get email() { + return this.registrationForm.get('email'); + } + + get password() { + return this.registrationForm.get('password'); + } + + get confirmPassword() { + return this.registrationForm.get('confirmPassword'); + } +}
Location Name{{ getLocationLabel(contact.locationid) }} First Name