register, forgot password, preparer contact and home page updates

This commit is contained in:
Cyril Joseph 2025-07-10 22:43:00 -03:00
parent 62cf41e34d
commit e67aa9a9cd
26 changed files with 901 additions and 69 deletions

116
package-lock.json generated
View File

@ -23,9 +23,10 @@
"chart.js": "^4.3.0", "chart.js": "^4.3.0",
"express": "^4.18.2", "express": "^4.18.2",
"ng2-charts": "^8.0.0", "ng2-charts": "^8.0.0",
"ngx-cookie-service": "^19.1.2", "ngx-cookie-service": "^20.0.1",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",
"tslib": "^2.3.0", "tslib": "^2.3.0",
"xlsx": "^0.18.5",
"zone.js": "~0.15.0" "zone.js": "~0.15.0"
}, },
"devDependencies": { "devDependencies": {
@ -3657,6 +3658,15 @@
"node": ">=0.4.0" "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": { "node_modules/agent-base": {
"version": "7.1.3", "version": "7.1.3",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
@ -4131,6 +4141,19 @@
], ],
"license": "CC-BY-4.0" "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": { "node_modules/chalk": {
"version": "5.4.1", "version": "5.4.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
@ -4333,6 +4356,15 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1" "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": { "node_modules/color-convert": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -4533,6 +4565,18 @@
"node": ">= 0.10" "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": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@ -5278,6 +5322,15 @@
"node": ">= 0.6" "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": { "node_modules/fresh": {
"version": "0.5.2", "version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
@ -7309,16 +7362,16 @@
} }
}, },
"node_modules/ngx-cookie-service": { "node_modules/ngx-cookie-service": {
"version": "19.1.2", "version": "20.0.1",
"resolved": "https://registry.npmjs.org/ngx-cookie-service/-/ngx-cookie-service-19.1.2.tgz", "resolved": "https://registry.npmjs.org/ngx-cookie-service/-/ngx-cookie-service-20.0.1.tgz",
"integrity": "sha512-4BFbc5BA7/ryuQVZUfDMHcjdgkHeW8obqfCCknoOEaplefjzlVvMQUx9Hywfjbc8POzZeZD2csS43Hs7rscJcA==", "integrity": "sha512-XTPrW/5ihI3DvTljDj14E501fouHdiONCnd1SPhvqyHNHjvKECFWuTzOpcHrWl9X1ZOKfOG/uXW8G8fINcL9fQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"tslib": "^2.8.0" "tslib": "^2.8.0"
}, },
"peerDependencies": { "peerDependencies": {
"@angular/common": "^19.0.0", "@angular/common": "^20.0.0",
"@angular/core": "^19.0.0" "@angular/core": "^20.0.0"
} }
}, },
"node_modules/node-addon-api": { "node_modules/node-addon-api": {
@ -8793,6 +8846,18 @@
"dev": true, "dev": true,
"license": "BSD-3-Clause" "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": { "node_modules/ssri": {
"version": "12.0.0", "version": "12.0.0",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz",
@ -9487,6 +9552,24 @@
"which": "bin/which" "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": { "node_modules/wrap-ansi": {
"version": "6.2.0", "version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
@ -9669,6 +9752,27 @@
"node": ">= 6" "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": { "node_modules/y18n": {
"version": "5.0.8", "version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",

View File

@ -26,9 +26,10 @@
"chart.js": "^4.3.0", "chart.js": "^4.3.0",
"express": "^4.18.2", "express": "^4.18.2",
"ng2-charts": "^8.0.0", "ng2-charts": "^8.0.0",
"ngx-cookie-service": "^19.1.2", "ngx-cookie-service": "^20.0.1",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",
"tslib": "^2.3.0", "tslib": "^2.3.0",
"xlsx": "^0.18.5",
"zone.js": "~0.15.0" "zone.js": "~0.15.0"
}, },
"devDependencies": { "devDependencies": {

View File

@ -1,6 +1,7 @@
import { RenderMode, ServerRoute } from '@angular/ssr'; import { RenderMode, ServerRoute } from '@angular/ssr';
export const serverRoutes: ServerRoute[] = [ export const serverRoutes: ServerRoute[] = [
{ path: 'forgot-password/:token', renderMode: RenderMode.Client },
{ path: ':appId', renderMode: RenderMode.Client }, { path: ':appId', renderMode: RenderMode.Client },
{ path: ':appId/home', renderMode: RenderMode.Client }, { path: ':appId/home', renderMode: RenderMode.Client },
{ path: ':appId/usersettings', renderMode: RenderMode.Client }, { path: ':appId/usersettings', renderMode: RenderMode.Client },

View File

@ -1,27 +1,22 @@
import { Routes } from '@angular/router'; import { Routes } from '@angular/router';
import { LoginComponent } from './login/login.component';
import { AuthGuard } from './guards/auth.guard'; 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 { 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 = [ 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/: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', 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: 'service-provider/:id', component: EditServiceProviderComponent }, { path: 'service-provider/:id', loadComponent: () => import('./service-provider/edit/edit-service-provider.component').then(m => m.EditServiceProviderComponent) },
{ path: 'preparer', component: ManagePreparerComponent }, { path: 'preparer', loadComponent: () => import('./preparer/manage/manage-preparer.component').then(m => m.ManagePreparerComponent) },
{ path: 'preparer/:id', component: EditPreparerComponent }, { path: 'preparer/:id', loadComponent: () => import('./preparer/edit/edit-preparer.component').then(m => m.EditPreparerComponent) },
{ path: 'add-preparer', component: AddPreparerComponent }, { path: 'add-preparer', loadComponent: () => import('./preparer/add/add-preparer.component').then(m => m.AddPreparerComponent) },
{ path: '', redirectTo: 'home', pathMatch: 'full' } { path: '', redirectTo: 'home', pathMatch: 'full' }
], ],
canActivate: [AuthGuard, AppIdGuard] canActivate: [AuthGuard, AppIdGuard]

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,6 +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 except excluded URLs
if (this.excludedUrls.some(url => request.url.includes(url))) {
return next.handle(request);
}
// Add withCredentials to all requests // Add withCredentials to all requests
request = request.clone({ request = request.clone({
withCredentials: true withCredentials: true

View File

@ -2,6 +2,7 @@ export interface Contact {
clientContactId: number; clientContactId: number;
spid: number spid: number
clientid: number; clientid: number;
locationid: number;
defaultContact: boolean; defaultContact: boolean;
firstName: string; firstName: string;
lastName: string; lastName: string;

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

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

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

@ -26,6 +26,7 @@ export class ContactService {
clientContactId: contact.CLIENTCONTACTID, clientContactId: contact.CLIENTCONTACTID,
spid: contact.SPID, spid: contact.SPID,
clientid: contact.CLIENTID, clientid: contact.CLIENTID,
locationid: contact.LOCATIONID,
defaultContact: contact.DEFCONTACTFLAG === 'Y', defaultContact: contact.DEFCONTACTFLAG === 'Y',
firstName: contact.FIRSTNAME, firstName: contact.FIRSTNAME,
lastName: contact.LASTNAME, lastName: contact.LASTNAME,
@ -45,10 +46,10 @@ export class ContactService {
})); }));
} }
createContact(clientid: number, data: Contact): Observable<any> { createContact(data: Contact): Observable<any> {
const contact = { const contact = {
P_SPID: this.userService.getUserSpid(), P_SPID: this.userService.getUserSpid(),
P_CLIENTID: clientid, P_LOCATIONID: data.locationid,
P_DEFCONTACTFLAG: data.defaultContact ? 'Y' : 'N', P_DEFCONTACTFLAG: data.defaultContact ? 'Y' : 'N',
P_CONTACTSTABLE: [{ P_CONTACTSTABLE: [{
P_FIRSTNAME: data.firstName, P_FIRSTNAME: data.firstName,
@ -70,6 +71,7 @@ export class ContactService {
const contact = { const contact = {
P_SPID: this.userService.getUserSpid(), P_SPID: this.userService.getUserSpid(),
P_CLIENTCONTACTID: spContactId, P_CLIENTCONTACTID: spContactId,
P_LOCATIONID: data.locationid,
P_FIRSTNAME: data.firstName, P_FIRSTNAME: data.firstName,
P_LASTNAME: data.lastName, P_LASTNAME: data.lastName,
P_MIDDLEINITIAL: data.middleInitial, P_MIDDLEINITIAL: data.middleInitial,

View File

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

View File

@ -8,7 +8,8 @@
<!-- Chart Section --> <!-- Chart Section -->
<div class="chart-section"> <div class="chart-section">
<app-chart [chartData]="carnetData" [carnetStatuses]="carnetStatuses" (carnetStatusData)="onCarnetStatusClick($event)"></app-chart> <app-chart [chartData]="carnetData" [carnetStatuses]="carnetStatuses"
(carnetStatusData)="onCarnetStatusClick($event)"></app-chart>
</div> </div>
<!-- Carnet Details Section --> <!-- Carnet Details Section -->
@ -17,6 +18,13 @@
<mat-spinner diameter="50"></mat-spinner> <mat-spinner diameter="50"></mat-spinner>
</div> </div>
<div class="table-actions">
<button mat-raised-button color="accent" (click)="exportData()" *ngIf="dataSource.data.length > 0"
matTooltip="Export to Excel" [disabled]="dataSource.data.length === 0">
<mat-icon>download</mat-icon> Export
</button>
</div>
<div class="table-container mat-elevation-z8" *ngIf="showTable"> <div class="table-container mat-elevation-z8" *ngIf="showTable">
<table mat-table [dataSource]="dataSource" matSort> <table mat-table [dataSource]="dataSource" matSort>
<ng-container matColumnDef="applicationName"> <ng-container matColumnDef="applicationName">

View File

@ -10,6 +10,15 @@
} }
} }
.table-actions {
display: flex;
justify-content: end;
button {
margin-bottom: 16px;
}
}
.table-container { .table-container {
position: relative; position: relative;
overflow: auto; overflow: auto;

View File

@ -13,6 +13,7 @@ import { CarnetStatus } from '../core/models/carnet-status';
import { ApiErrorHandlerService } from '../core/services/common/api-error-handler.service'; import { ApiErrorHandlerService } from '../core/services/common/api-error-handler.service';
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 * as XLSX from 'xlsx';
@Component({ @Component({
selector: 'app-home', selector: 'app-home',
@ -102,6 +103,40 @@ export class HomeComponent {
} }
exportData() { 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 { getCarnetStatusLabel(value: string): string {

View File

@ -21,8 +21,12 @@
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<div class="forgot-password"> <div class="additional-options">
<a href="#" class="forgot-link">Forgot your password?</a> <a href="#" routerLink="/forgot-password" class="additional-options-link">Forgot your password?</a>
</div>
<div class="additional-options">
<a href="#" routerLink="/register" class="additional-options-link">New User? Register</a>
</div> </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

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

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

@ -11,17 +11,8 @@
</app-basic-details> </app-basic-details>
</mat-step> </mat-step>
<!-- Contacts Step -->
<mat-step [completed]="contactsCompleted" [editable]="!!clientid && basicDetailsCompleted">
<ng-template matStepLabel>Contacts</ng-template>
<app-contacts *ngIf="clientid" [clientid]="clientid" (hasContacts)="onContactsSaved($event)"
[userPreferences]="userPreferences">
</app-contacts>
</mat-step>
<!-- Location Step --> <!-- Location Step -->
<mat-step [completed]="locationCompleted" [editable]="!!clientid && contactsCompleted" *ngIf="showLocation"> <mat-step [completed]="locationCompleted" [editable]="!!clientid && basicDetailsCompleted" *ngIf="showLocation">
<ng-template matStepLabel>Locations</ng-template> <ng-template matStepLabel>Locations</ng-template>
<app-location *ngIf="clientid" [clientid]="clientid" (hasLocations)="onLocationSaved($event)" <app-location *ngIf="clientid" [clientid]="clientid" (hasLocations)="onLocationSaved($event)"
@ -29,5 +20,14 @@
</app-location> </app-location>
</mat-step> </mat-step>
<!-- Contacts Step -->
<mat-step [completed]="contactsCompleted" [editable]="!!clientid && locationCompleted">
<ng-template matStepLabel>Contacts</ng-template>
<app-contacts *ngIf="clientid" [clientid]="clientid" (hasContacts)="onContactsSaved($event)"
[userPreferences]="userPreferences">
</app-contacts>
</mat-step>
</mat-stepper> </mat-stepper>
</div> </div>

View File

@ -15,6 +15,12 @@
</div> </div>
<table mat-table [dataSource]="dataSource" matSort> <table mat-table [dataSource]="dataSource" matSort>
<!-- Location Column -->
<ng-container matColumnDef="location">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Location Name</th>
<td mat-cell *matCellDef="let contact">{{ getLocationLabel(contact.locationid) }}</td>
</ng-container>
<!-- First Name Column --> <!-- First Name Column -->
<ng-container matColumnDef="firstName"> <ng-container matColumnDef="firstName">
<th mat-header-cell *matHeaderCellDef mat-sort-header>First Name</th> <th mat-header-cell *matHeaderCellDef mat-sort-header>First Name</th>
@ -158,6 +164,18 @@
Maximum 100 characters allowed Maximum 100 characters allowed
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline" class="location">
<mat-label>Location</mat-label>
<mat-select formControlName="location" required>
<mat-option *ngFor="let location of locations" [value]="location.locationid">
{{ location.name }}
</mat-option>
</mat-select>
<mat-error *ngIf="contactForm.get('location')?.errors?.['required']">
Location is required
</mat-error>
</mat-form-field>
</div> </div>
<div class="form-row"> <div class="form-row">

View File

@ -15,6 +15,8 @@ import { ApiErrorHandlerService } from '../../core/services/common/api-error-han
import { Contact } from '../../core/models/preparer/contact'; import { Contact } from '../../core/models/preparer/contact';
import { ConfirmDialogComponent } from '../../shared/components/confirm-dialog/confirm-dialog.component'; import { ConfirmDialogComponent } from '../../shared/components/confirm-dialog/confirm-dialog.component';
import { ContactLogin } from '../../core/models/preparer/contact-login'; import { ContactLogin } from '../../core/models/preparer/contact-login';
import { LocationService } from '../../core/services/preparer/location.service';
import { Location } from '../../core/models/preparer/location';
@Component({ @Component({
selector: 'app-contacts', selector: 'app-contacts',
@ -27,7 +29,7 @@ export class ContactsComponent {
@ViewChild(MatPaginator) paginator!: MatPaginator; @ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort; @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<any>(); dataSource = new MatTableDataSource<any>();
contactForm: FormGroup; contactForm: FormGroup;
contactLoginForm: FormGroup; contactLoginForm: FormGroup;
@ -38,6 +40,8 @@ export class ContactsComponent {
showLoginForm = false showLoginForm = false
showInactiveContacts = false; showInactiveContacts = false;
contacts: Contact[] = []; contacts: Contact[] = [];
locations: Location[] = [];
contactReadOnlyFields: any = { contactReadOnlyFields: any = {
lastChangedDate: null, lastChangedDate: null,
lastChangedBy: null, lastChangedBy: null,
@ -58,6 +62,7 @@ export class ContactsComponent {
private dialog = inject(MatDialog); private dialog = inject(MatDialog);
private notificationService = inject(NotificationService); private notificationService = inject(NotificationService);
private errorHandler = inject(ApiErrorHandlerService); private errorHandler = inject(ApiErrorHandlerService);
private locationService = inject(LocationService);
constructor() { constructor() {
this.contactForm = this.fb.group({ this.contactForm = this.fb.group({
@ -69,6 +74,7 @@ export class ContactsComponent {
mobile: ['', [Validators.required, Validators.pattern(/^[0-9]{10,15}$/)]], mobile: ['', [Validators.required, Validators.pattern(/^[0-9]{10,15}$/)]],
fax: ['', [Validators.pattern(/^[0-9]{10,15}$/)]], fax: ['', [Validators.pattern(/^[0-9]{10,15}$/)]],
email: ['', [Validators.required, Validators.email, Validators.maxLength(100)]], email: ['', [Validators.required, Validators.email, Validators.maxLength(100)]],
location: ['', [Validators.required]],
defaultContact: [false] defaultContact: [false]
}); });
@ -78,7 +84,7 @@ export class ContactsComponent {
} }
ngOnInit(): void { ngOnInit(): void {
this.loadContacts(); this.loadLocations();
} }
ngAfterViewInit() { ngAfterViewInit() {
@ -86,6 +92,23 @@ export class ContactsComponent {
this.dataSource.sort = this.sort; 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 { loadContacts(): void {
this.isLoading = true; this.isLoading = true;
@ -126,7 +149,8 @@ export class ContactsComponent {
phone: contact.phone, phone: contact.phone,
mobile: contact.mobile, mobile: contact.mobile,
fax: contact.fax, fax: contact.fax,
email: contact.email email: contact.email,
location: contact.locationid,
// defaultContact: contact.defaultContact // defaultContact: contact.defaultContact
}); });
@ -148,7 +172,7 @@ export class ContactsComponent {
const saveObservable = this.isEditing && (this.currentContactId! > 0) const saveObservable = this.isEditing && (this.currentContactId! > 0)
? this.contactService.updateContact(this.currentContactId!, contactData) ? this.contactService.updateContact(this.currentContactId!, contactData)
: this.contactService.createContact(this.clientid, contactData); : this.contactService.createContact(contactData);
saveObservable.subscribe({ saveObservable.subscribe({
next: () => { 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 { createLogin(contact: Contact): void {
this.showForm = false; this.showForm = false;
this.showLoginForm = true; this.showLoginForm = true;

View File

@ -13,12 +13,6 @@
<app-basic-details [clientid]="clientid" [isEditMode]="isEditMode" <app-basic-details [clientid]="clientid" [isEditMode]="isEditMode"
(clientName)="onClientNameUpdate($event)"></app-basic-details> (clientName)="onClientNameUpdate($event)"></app-basic-details>
</mat-expansion-panel> </mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title> Contacts </mat-panel-title>
</mat-expansion-panel-header>
<app-contacts [clientid]="clientid" [userPreferences]="userPreferences"></app-contacts>
</mat-expansion-panel>
<mat-expansion-panel> <mat-expansion-panel>
<mat-expansion-panel-header> <mat-expansion-panel-header>
@ -26,4 +20,12 @@
</mat-expansion-panel-header> </mat-expansion-panel-header>
<app-location [clientid]="clientid" [userPreferences]="userPreferences"></app-location> <app-location [clientid]="clientid" [userPreferences]="userPreferences"></app-location>
</mat-expansion-panel> </mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title> Contacts </mat-panel-title>
</mat-expansion-panel-header>
<app-contacts [clientid]="clientid" [userPreferences]="userPreferences"></app-contacts>
</mat-expansion-panel>
</mat-accordion> </mat-accordion>

View File

@ -0,0 +1,76 @@
<div class="registration-container">
<div class="registration-card">
<h3 class="subtitle">User Registration</h3>
<form [formGroup]="registrationForm" (ngSubmit)="onSubmit()" class="registration-form">
<mat-form-field appearance="outline">
<mat-label>Email</mat-label>
<input matInput formControlName="email" type="email" required>
<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-error *ngIf="password?.errors?.['minlength']">
Password must be at least 8 characters
</mat-error>
<mat-error *ngIf="password?.errors?.['pattern']">
Password must contain at least one uppercase letter, one lowercase letter, one number, and one
special character
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Confirm Password</mat-label>
<input matInput formControlName="confirmPassword" [type]="hidePassword ? 'password' : 'text'" required>
<button type="button" mat-icon-button matSuffix (click)="hidePassword = !hidePassword">
<mat-icon>{{ hidePassword ? 'visibility_off' : 'visibility' }}</mat-icon>
</button>
<mat-error *ngIf="confirmPassword?.errors?.['required']">
Please confirm your password
</mat-error>
<mat-error *ngIf="registrationForm.errors?.['mismatch'] && confirmPassword?.touched">
Passwords do not match
</mat-error>
</mat-form-field>
<div class="login">
<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,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');
}
}