+
`
})
-export class AdminLayoutComponent implements OnInit {
- items: MenuItem[] = [];
-
- constructor(private auth: AuthService) {}
-
- ngOnInit() {
- this.items = [
- {
- label: 'Home',
- icon: 'pi pi-home',
- routerLink: '/'
- },
- {
- label: 'OCR Module',
- icon: 'pi pi-search',
- items: [
- {
- label: 'Upload',
- icon: 'pi pi-upload',
- routerLink: '/ocr'
- },
- {
- label: 'History',
- icon: 'pi pi-history'
- }
- ]
-
- },
- {
- label: 'Mailbox',
- icon: 'pi pi-envelope',
- routerLink: '/mailbox'
- },
- {
- label: 'Settings',
- icon: 'pi pi-cog',
- items: [
- {
- label: 'Profile',
- icon: 'pi pi-user'
- },
- {
- label: 'Preferences',
- icon: 'pi pi-sliders-h'
- }
- ]
- }
- ];
- }
-
- logout() {
- this.auth.logout();
- }
+export class AdminLayoutComponent {
+ constructor() {}
}
diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts
deleted file mode 100644
index cd86a1e..0000000
--- a/frontend/src/app/app-routing.module.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { NgModule } from '@angular/core';
-import { RouterModule, Routes } from '@angular/router';
-import { LoginComponent } from './login/login.component';
-import { AdminLayoutComponent } from './admin-layout/admin-layout.component';
-import { OcrComponent } from './ocr/ocr.component';
-import { AuthGuard } from './auth.guard';
-
-const routes: Routes = [
- { path: 'login', component: LoginComponent },
- {
- path: '',
- component: AdminLayoutComponent,
- canActivate: [AuthGuard],
- children: [
- { path: '', redirectTo: 'ocr', pathMatch: 'full' },
- { path: 'ocr', component: OcrComponent }
- ]
- },
- { path: '**', redirectTo: '' }
-];
-
-@NgModule({
- imports: [RouterModule.forRoot(routes)],
- exports: [RouterModule]
-})
-export class AppRoutingModule { }
diff --git a/frontend/src/app/app.config.ts b/frontend/src/app/app.config.ts
index 9118a55..000e680 100644
--- a/frontend/src/app/app.config.ts
+++ b/frontend/src/app/app.config.ts
@@ -1,7 +1,8 @@
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
-import { provideHttpClient } from '@angular/common/http';
+import { provideHttpClient, withInterceptors } from '@angular/common/http';
+import { AuthInterceptor } from './interceptors/auth.interceptor';
import { routes } from './app.routes';
@@ -10,6 +11,6 @@ export const appConfig: ApplicationConfig = {
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideAnimationsAsync(),
- provideHttpClient()
+ provideHttpClient(withInterceptors([AuthInterceptor]))
]
};
diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts
index 8199137..c614c62 100644
--- a/frontend/src/app/app.routes.ts
+++ b/frontend/src/app/app.routes.ts
@@ -3,18 +3,40 @@ import { LoginComponent } from './login/login.component';
import { AdminLayoutComponent } from './admin-layout/admin-layout.component';
import { OcrComponent } from './ocr/ocr.component';
import { MailboxComponent } from './mailbox/mailbox.component';
-import { AuthGuard } from './auth.guard';
+
+import { AuthorizeComponent } from './pages/session/auth/authorize.component';
+import { AuthorizeGuard } from './interceptors/authorize.guard';
+import { ProfileComponent } from './pages/session/profile/profile.component';
+import { SubsidiaryComponent } from './pages/account/company/subsidiary/subsidiary.component';
+import { DepartmentComponent } from './pages/account/company/department/department.component';
+import { DesignationComponent } from './pages/account/company/desigation/designation.component';
+import { EmployeeComponent } from './pages/account/company/employee/employee.component';
+import { UserComponent } from './pages/account/user/user.component';
export const routes: Routes = [
- { path: 'login', component: LoginComponent },
+ { path: '', component: LoginComponent },
+ { path: 'authorize', component: AuthorizeComponent, canActivate: [AuthorizeGuard]},
{
- path: '',
+ path: 'dashboard',
component: AdminLayoutComponent,
- canActivate: [AuthGuard],
+ canActivate: [AuthorizeGuard],
+ canActivateChild: [AuthorizeGuard],
children: [
{ path: '', redirectTo: 'ocr', pathMatch: 'full' },
{ path: 'ocr', component: OcrComponent },
- { path: 'mailbox', component: MailboxComponent }
+ { path: 'mailbox', component: MailboxComponent },
+ { path: 'user',
+ canActivate: [AuthorizeGuard],
+ canActivateChild: [AuthorizeGuard],
+ children: [
+ { path: 'profile', component: ProfileComponent },
+ { path: 'subsidiaries', component: SubsidiaryComponent },
+ { path: 'departments', component: DepartmentComponent },
+ { path: 'designations', component: DesignationComponent },
+ { path: 'employees', component: EmployeeComponent },
+ { path: 'users', component: UserComponent }
+ ]
+ }
]
},
{ path: '**', redirectTo: '' }
diff --git a/frontend/src/app/auth.guard.ts b/frontend/src/app/auth.guard.ts
deleted file mode 100644
index d070bb4..0000000
--- a/frontend/src/app/auth.guard.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { Injectable } from '@angular/core';
-import { CanActivate, Router } from '@angular/router';
-import { AuthService } from './auth.service';
-
-@Injectable({
- providedIn: 'root'
-})
-export class AuthGuard implements CanActivate {
- constructor(private authService: AuthService, private router: Router) {}
-
- canActivate(): boolean {
- if (this.authService.isLoggedIn()) {
- return true;
- } else {
- this.router.navigate(['/login']);
- return false;
- }
- }
-}
diff --git a/frontend/src/app/auth.service.ts b/frontend/src/app/auth.service.ts
deleted file mode 100644
index b03639f..0000000
--- a/frontend/src/app/auth.service.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { Injectable } from '@angular/core';
-import { HttpClient } from '@angular/common/http';
-import { Router } from '@angular/router';
-import { Observable, of, tap } from 'rxjs';
-
-@Injectable({
- providedIn: 'root'
-})
-export class AuthService {
- private apiUrl = '/ocrb/api';
-
- constructor(private http: HttpClient, private router: Router) { }
-
- login(username: string, password: string): Observable
{
- return this.http.post(`${this.apiUrl}/login`, { username, password }).pipe(
- tap((res: any) => {
- if (res.token) {
- localStorage.setItem('token', res.token);
- }
- })
- );
- }
-
- logout() {
- localStorage.removeItem('token');
- this.router.navigate(['/login']);
- }
-
- isLoggedIn(): boolean {
- return !!localStorage.getItem('token');
- }
-}
diff --git a/frontend/src/app/fragments/menu/menu.component.html b/frontend/src/app/fragments/menu/menu.component.html
new file mode 100644
index 0000000..e4083be
--- /dev/null
+++ b/frontend/src/app/fragments/menu/menu.component.html
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+ {{ companyName }}
+
+
+ {{ name }} | {{ branchName }}
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/app/fragments/menu/menu.component.ts b/frontend/src/app/fragments/menu/menu.component.ts
new file mode 100644
index 0000000..07575d0
--- /dev/null
+++ b/frontend/src/app/fragments/menu/menu.component.ts
@@ -0,0 +1,114 @@
+import { Component, Input, OnInit } from '@angular/core';
+import { Router } from '@angular/router';
+import { MenuItem } from 'primeng/api';
+import { MenubarModule } from 'primeng/menubar';
+import { HttpService } from '../../services/http.service';
+import { Request } from '../../models/request.model';
+import { AvatarModule } from 'primeng/avatar';
+import { MenuModule } from 'primeng/menu';
+import { ButtonModule } from 'primeng/button';
+import { InputTextModule } from 'primeng/inputtext';
+import { SessionService } from '../../services/commons/session.service';
+import { EncryptionService } from '../../services/utilities/encryption.service';
+import { TooltipModule } from 'primeng/tooltip';
+import { environment } from '../../../environments/environment';
+import { CommonModule } from '@angular/common';
+
+@Component({
+ selector: 'app-menu',
+ standalone: true,
+ imports: [CommonModule, MenubarModule, AvatarModule, ButtonModule, MenuModule, InputTextModule, TooltipModule],
+ templateUrl: './menu.component.html'
+})
+export class MenuComponent implements OnInit {
+ items: MenuItem[] = [];
+ profileItems: MenuItem[] | undefined;
+ companyName: string = '';
+ branchName: string = '';
+ roleName: string = '';
+ name: string = '';
+ constructor(private router: Router, private http: HttpService, private session: SessionService, private enc: EncryptionService) { }
+ ngOnInit() {
+ if (typeof window !== 'undefined' && sessionStorage) {
+ let menuString = this.session.getItem("nav");
+ if (menuString) {
+ this.enc.decrypt(menuString).then(decrypted => {
+ if (decrypted) {
+ this.items = JSON.parse(decrypted).map((m: any) => this.mapMenu(m));
+ console.log(this.items);
+ }
+ });
+ }
+ // SessionService now returns parsed objects
+ const companies = this.session.getItem("companies");
+ const userDetails = this.session.getItem('userDetails');
+
+ this.name = userDetails ? userDetails.displayName : '';
+ this.companyName = (companies && companies.length > 0) ? companies[0].companyName : '';
+ this.branchName = (companies && companies.length > 0) ? companies[0].branches[0].branchCode : '';
+ this.roleName = (companies && companies.length > 0) ? companies[0].branches[0].roles[0].groupName : '';
+ }
+ this.profileItems = [
+ {
+ label: this.roleName,
+ items: [
+ {
+ label: 'Details',
+ icon: 'pi pi-id-card',
+ command: () => {
+ this.router.navigateByUrl(`/dashboard/user/profile`);
+ }
+ },
+ {
+ label: 'Sign Out',
+ icon: 'pi pi-sign-out',
+ command: () => {
+ this.logout();
+ }
+ }
+ ]
+ }
+ ];
+ }
+
+
+ logout() {
+ const requestPayload: Request = {
+ data: '',
+ compressed: false
+ };
+ this.http.post(environment.authService + '/signout', requestPayload).subscribe({
+ next: () => {
+ sessionStorage.clear();
+ this.router.navigate(['/']);
+ },
+ error: (err) => {
+ console.error('Logout failed', err);
+ }
+ });
+ }
+
+ mapMenu(menu: any): MenuItem {
+ const hasChildren = Array.isArray(menu.items) && menu.items.length > 0;
+
+ return {
+ label: menu.label,
+ icon: menu.icon,
+ routerLink: !hasChildren && menu.route !== '#'
+ ? `/dashboard/user/${menu.route}`
+ : undefined,
+ command: !hasChildren && menu.route !== '#'
+ ? () => {
+ this.router.navigateByUrl(`/dashboard/user/${menu.route}`);
+ }
+ : undefined,
+ items: hasChildren
+ ? menu.items.map((c: any) => this.mapMenu(c))
+ : undefined
+ };
+ }
+
+ gotToDashboard() {
+ this.router.navigate(['/']);
+ }
+}
diff --git a/frontend/src/app/interceptors/auth.interceptor.ts b/frontend/src/app/interceptors/auth.interceptor.ts
new file mode 100644
index 0000000..1a87534
--- /dev/null
+++ b/frontend/src/app/interceptors/auth.interceptor.ts
@@ -0,0 +1,139 @@
+import { SessionService } from './../services/commons/session.service';
+import {
+ HttpInterceptorFn,
+ HttpRequest,
+ HttpHandlerFn,
+ HttpEvent,
+ HttpResponse,
+ HttpErrorResponse,
+ HttpHeaders
+} from '@angular/common/http';
+import { Observable, tap, catchError, throwError, from, switchMap } from 'rxjs';
+import { EncryptionService } from '../services/utilities/encryption.service';
+import { inject } from '@angular/core';
+import { ResponseDto } from '../models/response.dto';
+
+export const AuthInterceptor: HttpInterceptorFn = (
+ req: HttpRequest,
+ next: HttpHandlerFn
+): Observable> => {
+
+ const encryptionService = inject(EncryptionService);
+ const sessionService = inject(SessionService);
+
+ /* -----------------------------------------
+ * 1️⃣ Always attach Authorization header
+ * ----------------------------------------- */
+ // OCR uses sessionStorage for token now (migrated previously)
+ const token = sessionStorage.getItem('token');
+
+ let reqHeaders = req.headers.set('Content-Type', 'application/json');
+
+ if (token) {
+ reqHeaders = reqHeaders.set(
+ 'Authorization',
+ `Bearer ${token}`
+ );
+ }
+
+ /* -----------------------------------------
+ * 2️⃣ Handle encrypted (compressed) requests
+ * ----------------------------------------- */
+ if (
+ req.body &&
+ typeof req.body === 'object' &&
+ req.body.compressed
+ ) {
+ return from(encryptionService.encrypt(req.body.data)).pipe(
+ switchMap(encryptedBody => {
+
+ const payload = {
+ scopes: req.body.scopes ?? [],
+ data: encryptedBody,
+ target: req.body.target ?? null,
+ compressed: true
+ };
+
+ const modifiedReq = req.clone({
+ body: payload,
+ headers: reqHeaders
+ });
+
+ // Use 'any' type for event to avoid strict type mismatch if ResponseDto structure varies
+ return next(modifiedReq).pipe(
+ tap((event: any) => handleResponse(event, sessionService, encryptionService)),
+ catchError((error: HttpErrorResponse) =>
+ throwError(() => error)
+ )
+ );
+ })
+ );
+ }
+
+ /* -----------------------------------------
+ * 3️⃣ Non-encrypted requests (GET included)
+ * ----------------------------------------- */
+ const modifiedReq = req.clone({
+ headers: reqHeaders
+ });
+
+ return next(modifiedReq).pipe(
+ tap((event: any) => handleResponse(event, sessionService, encryptionService)),
+ catchError((error: HttpErrorResponse) =>
+ throwError(() => error)
+ )
+ );
+};
+
+function handleResponse(
+ event: HttpEvent,
+ sessionService: SessionService,
+ encryptionService: EncryptionService
+): void {
+
+ if (!(event instanceof HttpResponse) || !event.ok) {
+ return;
+ }
+
+ // Handle Authenticate Response
+ if (event.url?.endsWith('/authenticate')) {
+ const response = event.body as ResponseDto;
+ if (response?.data?.token) {
+ sessionStorage.setItem('token', response.data.token);
+ }
+ if (response?.data?.companies) {
+ sessionService.setItem('companies', response.data.companies);
+ }
+ }
+
+ // Handle Authorize Response
+ if (event.url?.endsWith('/authorize')) {
+ const response = event.body as ResponseDto;
+ // Clearing session might be too aggressive if we just logged in, but following source logic
+ // sessionService.clear();
+
+ if (response?.data?.token) {
+ sessionStorage.setItem('token', response.data.token);
+ }
+ if (response?.data?.refreshToken) {
+ sessionService.setItem('refreshToken', response.data.refreshToken);
+ }
+ if (response?.data?.userDetails) {
+ sessionService.setItem('userDetails', response.data.userDetails);
+ }
+
+ if (response?.data?.data) {
+ encryptionService.encrypt(response.data.data)
+ .then(encryptedNav => {
+ sessionService.setItem('nav', encryptedNav);
+ });
+ }
+
+ if (response?.data?.companies) {
+ sessionService.setItem(
+ 'companies',
+ response.data.companies
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/app/interceptors/authorize.guard.ts b/frontend/src/app/interceptors/authorize.guard.ts
new file mode 100644
index 0000000..0789af8
--- /dev/null
+++ b/frontend/src/app/interceptors/authorize.guard.ts
@@ -0,0 +1,40 @@
+import { Injectable, inject, PLATFORM_ID } from '@angular/core';
+import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, Router, RouterStateSnapshot } from '@angular/router';
+import { isPlatformBrowser } from '@angular/common';
+
+@Injectable({ providedIn: 'root' })
+export class AuthorizeGuard implements CanActivate, CanActivateChild {
+
+ private platformId = inject(PLATFORM_ID);
+ private router = inject(Router);
+
+ private checkAuth(): boolean {
+ if (!isPlatformBrowser(this.platformId)) {
+ return false;
+ }
+
+ const token = sessionStorage.getItem('token');
+
+ if (!token) {
+ sessionStorage.clear();
+ this.router.navigate(['/']);
+ return false;
+ }
+
+ return true;
+ }
+
+ canActivate(
+ route: ActivatedRouteSnapshot,
+ state: RouterStateSnapshot
+ ): boolean {
+ return this.checkAuth();
+ }
+
+ canActivateChild(
+ childRoute: ActivatedRouteSnapshot,
+ state: RouterStateSnapshot
+ ): boolean {
+ return this.checkAuth();
+ }
+}
diff --git a/frontend/src/app/login/login.component.css b/frontend/src/app/login/login.component.css
new file mode 100644
index 0000000..fe96441
--- /dev/null
+++ b/frontend/src/app/login/login.component.css
@@ -0,0 +1,42 @@
+::ng-deep .p-password-input {
+ border-top-left-radius: 0 !important;
+ border-bottom-left-radius: 0 !important;
+}
+
+.login-page {
+ height: 99vh;
+}
+
+::ng-deep .p-card {
+ position: relative;
+ border-radius: 24px !important;
+ background: white;
+ padding: 1rem;
+ overflow: hidden;
+}
+
+/* Gradient border */
+::ng-deep .p-card::before {
+ content: "";
+ position: absolute;
+ inset: 0;
+ padding: 3px; /* border thickness */
+ border-radius: 24px;
+ filter: drop-shadow(0 0 12px rgba(91, 185, 138, 0.35));
+ background: linear-gradient(
+ 180deg,
+ #5bb98a 0%,
+ rgba(91, 185, 138, 0.6) 40%,
+ rgba(91, 185, 138, 0.15) 70%,
+ transparent 100%
+ );
+
+ /* Mask trick = border only */
+ -webkit-mask:
+ linear-gradient(#fff 0 0) content-box,
+ linear-gradient(#fff 0 0);
+ -webkit-mask-composite: xor;
+ mask-composite: exclude;
+
+ pointer-events: none;
+}
diff --git a/frontend/src/app/login/login.component.html b/frontend/src/app/login/login.component.html
new file mode 100644
index 0000000..61006ed
--- /dev/null
+++ b/frontend/src/app/login/login.component.html
@@ -0,0 +1,54 @@
+
diff --git a/frontend/src/app/login/login.component.ts b/frontend/src/app/login/login.component.ts
index 221bde1..0081f4b 100644
--- a/frontend/src/app/login/login.component.ts
+++ b/frontend/src/app/login/login.component.ts
@@ -1,77 +1,67 @@
-import { Component } from '@angular/core';
-import { CommonModule } from '@angular/common';
+import { CommonModule} from '@angular/common';
import { FormsModule } from '@angular/forms';
-import { AuthService } from '../auth.service';
-import { Router } from '@angular/router';
-import { MessageService } from 'primeng/api';
-
-// PrimeNG Imports
-import { CardModule } from 'primeng/card';
-import { InputTextModule } from 'primeng/inputtext';
+import { Component } from '@angular/core';
import { ButtonModule } from 'primeng/button';
-import { ToastModule } from 'primeng/toast';
+import { InputTextModule } from 'primeng/inputtext';
+import { CardModule } from 'primeng/card';
+import { PasswordModule } from 'primeng/password';
+import { KeyFilterModule } from 'primeng/keyfilter';
+import { FormBuilder, FormGroup, Validators, ReactiveFormsModule} from '@angular/forms';
+import { Request } from '../models/request.model';
+import { HttpService } from '../services/http.service';
+import { MessageModule } from 'primeng/message';
+import { Router } from '@angular/router';
+import { environment } from '../../environments/environment';
+import { BadgeModule } from "primeng/badge";
@Component({
selector: 'app-login',
+ templateUrl: './login.component.html',
standalone: true,
+ styleUrl: './login.component.css',
imports: [
CommonModule,
FormsModule,
+ ReactiveFormsModule,
CardModule,
- InputTextModule,
ButtonModule,
- ToastModule
- ],
- providers: [MessageService],
- template: `
-
- `,
- styles: [`
- .login-container {
- display: flex;
- justify-content: center;
- align-items: center;
- height: 100vh;
- background: var(--surface-card);
- }
- .w-full { width: 100%; }
- .mt-4 { margin-top: 1.5rem; }
- /* PrimeNG handles the rest */
- `]
+ InputTextModule,
+ PasswordModule,
+ KeyFilterModule,
+ MessageModule,
+ BadgeModule
+ ]
})
export class LoginComponent {
- username = '';
- password = '';
+ loginForm: FormGroup;
+ message?: string;
- constructor(private auth: AuthService, private router: Router, private messageService: MessageService) {}
-
- onLogin() {
- this.auth.login(this.username, this.password).subscribe({
- next: () => {
- this.router.navigate(['/']);
- },
- error: () => {
- this.messageService.add({severity:'error', summary:'Error', detail:'Invalid Credentials'});
- }
+ constructor(private fb: FormBuilder, private http: HttpService, private router: Router) {
+ this.loginForm = this.fb.group({
+ username: ['', Validators.required],
+ password: ['', Validators.required]
});
}
+
+ onLogin() {
+ this.message = '';
+ if (this.loginForm.valid) {
+ const requestPayload: Request = {
+ data: this.loginForm.value,
+ compressed: true,
+ target: 'models.auth.Login'
+ };
+ // Matching cygnus-ui endpoint structure
+ this.http.post(`${environment.authService}/3z4mkell5g5aset/authenticate`, requestPayload).subscribe({
+ next: (response) => {
+ // Token is handled by AuthInterceptor now for 'authenticate' endpoint
+ // But we can double check or just navigate
+ this.router.navigate(['/authorize']);
+ },
+ error: (err) => {
+ this.message = err.message || 'Login Failed';
+ }
+ });
+ }
+ }
}
diff --git a/frontend/src/app/models/account.model.ts b/frontend/src/app/models/account.model.ts
new file mode 100644
index 0000000..ecc426e
--- /dev/null
+++ b/frontend/src/app/models/account.model.ts
@@ -0,0 +1,91 @@
+export class SubsidiaryDTO {
+ id?: string;
+ companyId?: string;
+ companyName?: string;
+ code?: string;
+ name?: string;
+ officeNo?: string;
+ street?: string;
+ locality?: string;
+ cityId?: string;
+ stateId?: string;
+ cityName?: string;
+ stateName?: string;
+ pinCode?: string;
+ emailId?: string;
+ contactNo?: string;
+ contactPerson?: string;
+ panNo?: string;
+ cinNo?: string;
+ msmeNo?: string;
+ updatedAt?: Date;
+ updatedBy?: string;
+ updatedUser?: string;
+ active?: boolean;
+
+ constructor(init?: Partial) {
+ Object.assign(this, init);
+ if (init?.updatedAt) {
+ this.updatedAt = new Date(init.updatedAt);
+ }
+ }
+}
+
+export interface DepartmentDTO {
+ id: string;
+ companyId: string;
+ department: string;
+ parentDepartment?: string;
+ parentDepartmentName?: string;
+ createdUser: string;
+ updatedAt: string;
+ updatedUser: string;
+ active: boolean;
+}
+
+export interface DesignationDTO {
+ id: string;
+ companyId: string;
+ departmentId: string;
+ departmentName: string;
+ designation: string;
+ createdUser: string;
+ updatedAt: string;
+ updatedUser: string;
+ active: boolean;
+ hod: boolean;
+ payGrade?: string;
+}
+
+export interface EmployeeDTO {
+ id: string;
+ subsidiaryId: string;
+ departmentId: string;
+ designationId: string;
+ subsidiaryName: string;
+ department: string;
+ designation: string;
+ employeeId?: string;
+ joiningDate?: string;
+ fullName: string;
+ fatherName?: string;
+ gender?: string;
+ dob?: string;
+ residenceAddress?: string;
+ residenceCityId?: string;
+ residenceStateId?: string;
+ residenceCityName?: string;
+ residenceStateName?: string;
+ permanentAddress?: string;
+ permanentCityId?: string;
+ permanentStateId?: string;
+ permanentCityName?: string;
+ permanentStateName?: string;
+ contactNo: string;
+ alternateNo?: string;
+ emailId: string;
+ createdUser: string;
+ updatedAt: string;
+ updatedUser: string;
+ active: boolean;
+}
\ No newline at end of file
diff --git a/frontend/src/app/models/masters/masters.ts b/frontend/src/app/models/masters/masters.ts
new file mode 100644
index 0000000..9d93f0e
--- /dev/null
+++ b/frontend/src/app/models/masters/masters.ts
@@ -0,0 +1,16 @@
+export interface CityDTO {
+ id?:string,
+ stateId?:string,
+ stateName?:string,
+ cityName?:string,
+ stateCode?:string,
+ gstCode?:string
+}
+
+export interface SearchDTO {
+ module?:string,
+ searchBy?:string,
+ searchValue?:string,
+ offset?:number,
+ limit?:number
+}
\ No newline at end of file
diff --git a/frontend/src/app/models/masters/member/memberlist.ts b/frontend/src/app/models/masters/member/memberlist.ts
new file mode 100644
index 0000000..d823bf6
--- /dev/null
+++ b/frontend/src/app/models/masters/member/memberlist.ts
@@ -0,0 +1,16 @@
+export interface MemberList {
+ id:string,
+ memberno:string,
+ membername:string,
+ fathername:string,
+ mobileno?:string,
+ membershipdate?:Date,
+ emailid?:string,
+ dob?:Date,
+ age?:number,
+ gender?:string,
+ status:string
+ isactive:boolean,
+ updatedat:Date,
+ updatedby:string
+}
diff --git a/frontend/src/app/models/request.model.ts b/frontend/src/app/models/request.model.ts
new file mode 100644
index 0000000..0f44bda
--- /dev/null
+++ b/frontend/src/app/models/request.model.ts
@@ -0,0 +1,6 @@
+export interface Request {
+ scopes?: string[];
+ data: T;
+ target?: string;
+ compressed?: boolean;
+}
diff --git a/frontend/src/app/models/response.dto.ts b/frontend/src/app/models/response.dto.ts
new file mode 100644
index 0000000..f3ce991
--- /dev/null
+++ b/frontend/src/app/models/response.dto.ts
@@ -0,0 +1,6 @@
+export interface ResponseDto {
+ statuscode : number;
+ message?: string;
+ description?: string;
+ data?: any;
+}
diff --git a/frontend/src/app/models/session.model.ts b/frontend/src/app/models/session.model.ts
new file mode 100644
index 0000000..07687cf
--- /dev/null
+++ b/frontend/src/app/models/session.model.ts
@@ -0,0 +1,35 @@
+export interface Company {
+ id?: string;
+ companyName?: string;
+ companyCode?: string;
+ branches?: Branch[];
+}
+
+export interface Branch {
+ id?: string;
+ branchName?: string;
+ branchCode?: string;
+ roles?: Role[];
+}
+
+export interface Role {
+ roleName?: string;
+ groupName?: string;
+ defaultRole?: boolean;
+}
+
+export interface UserProfile {
+ username?: string;
+ employeeId?: string;
+ joiningDate?: Date;
+ displayName?: string;
+ department?: string;
+ designation?: string;
+ name?: string;
+ fatherName?: string;
+ gender?: string;
+ dob?: Date;
+ contactNo?: string;
+ alternateContactNo?: string;
+ emailId?: string;
+}
diff --git a/frontend/src/app/models/user.model.ts b/frontend/src/app/models/user.model.ts
new file mode 100644
index 0000000..8589f36
--- /dev/null
+++ b/frontend/src/app/models/user.model.ts
@@ -0,0 +1,29 @@
+export interface UserDTO {
+ id?: string;
+ loginId?: string;
+ loginPassword?: string;
+ displayName?: string;
+ fkEmployeeId?: string;
+ employeeId?: string;
+ employeeName?: string;
+ fatherName?: string;
+ department?: string;
+ designation?: string;
+ status?: string;
+ updatedAt?: string;
+ updatedUser?: string;
+ active?: boolean;
+ userRoles?: UserRoleDTO[];
+}
+
+export interface UserRoleDTO {
+ id?: string;
+ roleId?: string;
+ branchId?: string;
+ roleName?: string;
+ groupName?: string;
+ branchName?: string;
+ updatedAt?: string;
+ updatedUser?: string;
+ active?: boolean;
+}
\ No newline at end of file
diff --git a/frontend/src/app/pages/account/company/department/department.component.css b/frontend/src/app/pages/account/company/department/department.component.css
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/frontend/src/app/pages/account/company/department/department.component.css
@@ -0,0 +1 @@
+
diff --git a/frontend/src/app/pages/account/company/department/department.component.html b/frontend/src/app/pages/account/company/department/department.component.html
new file mode 100644
index 0000000..12efa7b
--- /dev/null
+++ b/frontend/src/app/pages/account/company/department/department.component.html
@@ -0,0 +1,162 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Manage Departments
+
+
+
+
+
+
+
+
+ #
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ rowIndex + 1 }}
+ {{ department.department }}
+
+
+ {{ department.parentDepartmentName }}
+
+
+
+ Not Available
+
+
+ {{ department.updatedAt | date:'dd/MM/yyyy HH:mm:ss' }}
+
+
+ {{ department.updatedUser }}
+
+
+
+ Not Available
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/app/pages/account/company/department/department.component.spec.ts b/frontend/src/app/pages/account/company/department/department.component.spec.ts
new file mode 100644
index 0000000..57045f3
--- /dev/null
+++ b/frontend/src/app/pages/account/company/department/department.component.spec.ts
@@ -0,0 +1,21 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { DepartmentComponent } from './department.component';
+
+describe('DepartmentComponent', () => {
+ let component: DepartmentComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [DepartmentComponent]
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(DepartmentComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/frontend/src/app/pages/account/company/department/department.component.ts b/frontend/src/app/pages/account/company/department/department.component.ts
new file mode 100644
index 0000000..5b50835
--- /dev/null
+++ b/frontend/src/app/pages/account/company/department/department.component.ts
@@ -0,0 +1,283 @@
+import { DepartmentDTO } from './../../../../models/account.model';
+import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core';
+import { ConfirmationService, MessageService } from 'primeng/api';
+import { TableModule, Table } from 'primeng/table';
+import { DialogModule } from 'primeng/dialog';
+import { RippleModule } from 'primeng/ripple';
+import { ButtonModule } from 'primeng/button';
+import { ToastModule } from 'primeng/toast';
+import { ToolbarModule } from 'primeng/toolbar';
+import { ConfirmDialogModule } from 'primeng/confirmdialog';
+import { InputTextModule } from 'primeng/inputtext';
+import { InputTextareaModule } from 'primeng/inputtextarea';
+import { CommonModule } from '@angular/common';
+import { FileUploadModule } from 'primeng/fileupload';
+import { DropdownModule } from 'primeng/dropdown';
+import { TagModule } from 'primeng/tag';
+import { RadioButtonModule } from 'primeng/radiobutton';
+import { RatingModule } from 'primeng/rating';
+import { SkeletonModule } from 'primeng/skeleton';
+import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
+import { InputNumberModule } from 'primeng/inputnumber';
+import { IconFieldModule } from 'primeng/iconfield';
+import { InputIconModule } from 'primeng/inputicon';
+import { CompanyService } from '../../../../services/account/company/company.service';
+import { ValidationService } from '../../../../services/utilities/validation.service';
+import { TooltipModule } from 'primeng/tooltip';
+import { AutoCompleteModule } from 'primeng/autocomplete';
+
+interface Column {
+ field: string;
+ header: string;
+ customExportHeader?: string;
+}
+
+interface ExportColumn {
+ title: string;
+ dataKey: string;
+}
+
+@Component({
+ selector: 'app-department',
+ templateUrl: './department.component.html',
+ standalone: true,
+ imports: [
+ CommonModule, FormsModule, ReactiveFormsModule,
+ TableModule, DialogModule, ButtonModule, ToastModule, ToolbarModule, ConfirmDialogModule,
+ InputTextModule, InputTextareaModule, FileUploadModule, DropdownModule, TagModule,
+ RadioButtonModule, RatingModule, SkeletonModule, InputNumberModule, IconFieldModule,
+ InputIconModule, TooltipModule, AutoCompleteModule, RippleModule
+ ],
+ providers: [MessageService, ConfirmationService],
+ styleUrl: './department.component.css'
+})
+export class DepartmentComponent implements OnInit{
+ departmentForm: FormGroup;
+ departmentDialog: boolean = false;
+ departments!: DepartmentDTO[];
+
+ department: DepartmentDTO | undefined;
+
+ selectedDepartments!: DepartmentDTO[] | null;
+
+ submitted: boolean = false;
+
+ isLoading: boolean = true;
+
+ skeletonData: any[] = Array(10).fill({});
+
+ statuses!: any[];
+
+ departmentOptions: any[] = [];
+
+ @ViewChild('dt') dt!: Table;
+
+ cols!: Column[];
+
+ exportColumns!: ExportColumn[];
+
+ constructor(
+ private companyService: CompanyService,
+ private messageService: MessageService,
+ private confirmationService: ConfirmationService,
+ private cd: ChangeDetectorRef,
+ private fb: FormBuilder,
+ ) {
+
+ this.departmentForm = this.fb.group({
+ id: [{ value: '', disabled: true }],
+ department: [{ value: '', disabled: false }, [Validators.required, ValidationService.nameValidator()]],
+ parentDepartment: [{ value: '', disabled: false }],
+ parentDepartmentName: [{ value: '', disabled: true }],
+ active: [{ value: true, disabled: false }]
+ });
+ }
+
+ exportCSV() {
+ this.dt.exportCSV();
+ }
+
+ ngOnInit() {
+ this.loadAllDepartments();
+ }
+
+ loadAllDepartments() {
+ this.isLoading = true;
+ this.companyService.getAllDepartments().subscribe({
+ next: (data) => {
+ this.isLoading = false;
+ this.departments = data;
+ this.departmentOptions = [
+ { label: 'None', value: '' },
+ ...this.departments.map(d => ({ label: d.department, value: d.id }))
+ ];
+ this.cd.markForCheck();
+ },
+ error: (err) => {
+ this.isLoading = false;
+ console.error(err);
+ }
+ });
+
+ this.statuses = [
+ { label: 'Active', value: true },
+ { label: 'Inactive', value: false }
+ ];
+
+ this.cols = [
+ { field: 'department', header: 'Department', customExportHeader: 'Department' },
+ { field: 'parentDepartmentName', header: 'Parent Department' },
+ { field: 'updatedAt', header: 'Last Updated At' },
+ { field: 'updatedUser', header: 'Last Updated By' }
+ ];
+
+ this.exportColumns = this.cols.map((col) => ({ title: col.header, dataKey: col.field }));
+ }
+
+ openNew() {
+ this.department = undefined;
+ this.departmentForm.reset();
+ this.submitted = false;
+ this.departmentDialog = true;
+ }
+
+ editDepartment(department: DepartmentDTO) {
+ const parentDepartment = this.departmentOptions.find(dep => dep.label === department.parentDepartmentName);
+ this.departmentForm.reset();
+ this.department = { ...department };
+ this.department.parentDepartment = parentDepartment?.value ?? '';
+ this.departmentForm.patchValue(this.department);
+ this.departmentDialog = true;
+ }
+
+
+ hideDialog() {
+ this.departmentDialog = false;
+ this.submitted = false;
+ }
+
+ toggleActive(department: DepartmentDTO) {
+ const isActivating = !department.active;
+ this.confirmationService.confirm({
+ message: `Are you sure you want to ${isActivating ? 'activate' : 'deactivate'} ` + department.department + '?',
+ header: 'Confirm',
+ icon: 'pi pi-exclamation-triangle',
+ rejectButtonStyleClass: 'p-button-text p-button-secondary',
+ acceptButtonStyleClass: isActivating ? 'p-button-success' : 'p-button-danger',
+ accept: () => {
+ this.companyService.activateDeactivateDepartment(department.id, isActivating).subscribe({
+ next: (updatedDepartment) => {
+ department.active = updatedDepartment.active;
+ this.departments = [...this.departments];
+ this.messageService.add({
+ severity: 'success',
+ summary: 'Successful',
+ detail: `Department ${isActivating ? 'Activated' : 'Deactivated'}`,
+ life: 3000
+ });
+ },
+ error: (err) => {
+ console.error('Error toggling department active status', err);
+ this.messageService.add({
+ severity: 'error',
+ summary: 'Error',
+ detail: 'Failed to update department status',
+ life: 3000
+ });
+ }
+ });
+ }
+ });
+ }
+
+ getSeverity(status: boolean) {
+ switch (status) {
+ case true:
+ return 'success';
+ case false:
+ return 'warning';
+ }
+ }
+
+ getErrorMessage(fieldName: string): string {
+ const control = this.departmentForm.get(fieldName);
+ return control ? ValidationService.getErrorMessage(control, fieldName) : '';
+ }
+
+ isFieldInvalid(fieldName: string): boolean {
+ const control = this.departmentForm.get(fieldName);
+ return !!(control && control.invalid && (control.dirty || control.touched || this.submitted));
+ }
+
+ saveDepartment() {
+ this.submitted = true;
+
+ if (this.departmentForm.invalid) {
+ return;
+ }
+
+ const departmentData = this.departmentForm.getRawValue() as DepartmentDTO;
+
+ // ✅ Normalize null → empty array
+ const departments = this.departments ?? [];
+
+ // 🔍 Duplicate check (case-insensitive)
+ const existing = departments.some(dep =>
+ dep.department?.trim().toLowerCase() === departmentData.department?.trim().toLowerCase() &&
+ dep.id !== departmentData.id
+ );
+
+ if (existing) {
+ this.messageService.add({
+ severity: 'error',
+ summary: 'Validation Error',
+ detail: 'Department name already exists',
+ life: 3000
+ });
+ return;
+ }
+
+ this.companyService.saveDepartment(departmentData).subscribe({
+ next: (savedDepartment) => {
+
+ const index = departmentData.id
+ ? departments.findIndex(dep => dep.id === departmentData.id)
+ : -1;
+
+ if (index !== -1) {
+ // ✅ UPDATE
+ departments[index] = savedDepartment;
+ } else {
+ // ✅ CREATE
+ departments.push(savedDepartment);
+ }
+
+ // ✅ Reassign once (change detection + null safety)
+ this.departments = [...departments];
+
+ this.messageService.add({
+ severity: 'success',
+ summary: 'Successful',
+ detail: index !== -1
+ ? 'Department Updated'
+ : 'Department Created',
+ life: 3000
+ });
+
+ this.departmentDialog = false;
+ this.department = undefined;
+ this.departmentForm.reset();
+ this.submitted = false;
+ },
+ error: (err) => {
+ console.error('Error saving department', err);
+ this.messageService.add({
+ severity: 'error',
+ summary: 'Error',
+ detail: 'Failed to save department',
+ life: 3000
+ });
+ }
+ });
+ }
+}
diff --git a/frontend/src/app/pages/account/company/desigation/designation.component.css b/frontend/src/app/pages/account/company/desigation/designation.component.css
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/frontend/src/app/pages/account/company/desigation/designation.component.css
@@ -0,0 +1 @@
+
diff --git a/frontend/src/app/pages/account/company/desigation/designation.component.html b/frontend/src/app/pages/account/company/desigation/designation.component.html
new file mode 100644
index 0000000..7a0728d
--- /dev/null
+++ b/frontend/src/app/pages/account/company/desigation/designation.component.html
@@ -0,0 +1,183 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Manage Designations
+
+
+
+
+
+
+
+
+ #
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ rowIndex + 1 }}
+ {{ designation.designation }}
+ {{ designation.departmentName }}
+ {{ designation.hod ? 'Yes' : 'No' }}
+ {{ designation.payGrade || 'N/A' }}
+ {{ designation.updatedAt | date:'dd/MM/yyyy HH:mm:ss' }}
+
+
+ {{ designation.updatedUser }}
+
+
+
+ Not Available
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/app/pages/account/company/desigation/designation.component.spec.ts b/frontend/src/app/pages/account/company/desigation/designation.component.spec.ts
new file mode 100644
index 0000000..de59e48
--- /dev/null
+++ b/frontend/src/app/pages/account/company/desigation/designation.component.spec.ts
@@ -0,0 +1,21 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { DesignationComponent } from './designation.component';
+
+describe('DesignationComponent', () => {
+ let component: DesignationComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [DesignationComponent]
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(DesignationComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/frontend/src/app/pages/account/company/desigation/designation.component.ts b/frontend/src/app/pages/account/company/desigation/designation.component.ts
new file mode 100644
index 0000000..18e6ca4
--- /dev/null
+++ b/frontend/src/app/pages/account/company/desigation/designation.component.ts
@@ -0,0 +1,295 @@
+import { DesignationDTO, DepartmentDTO } from './../../../../models/account.model';
+import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core';
+import { ConfirmationService, MessageService } from 'primeng/api';
+import { TableModule, Table } from 'primeng/table';
+import { DialogModule } from 'primeng/dialog';
+import { RippleModule } from 'primeng/ripple';
+import { ButtonModule } from 'primeng/button';
+import { ToastModule } from 'primeng/toast';
+import { ToolbarModule } from 'primeng/toolbar';
+import { ConfirmDialogModule } from 'primeng/confirmdialog';
+import { InputTextModule } from 'primeng/inputtext';
+import { InputTextareaModule } from 'primeng/inputtextarea';
+import { CommonModule } from '@angular/common';
+import { FileUploadModule } from 'primeng/fileupload';
+import { DropdownModule } from 'primeng/dropdown';
+import { TagModule } from 'primeng/tag';
+import { RadioButtonModule } from 'primeng/radiobutton';
+import { RatingModule } from 'primeng/rating';
+import { SkeletonModule } from 'primeng/skeleton';
+import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
+import { InputNumberModule } from 'primeng/inputnumber';
+import { CompanyService } from '../../../../services/account/company/company.service';
+import { ValidationService } from '../../../../services/utilities/validation.service';
+import { TooltipModule } from 'primeng/tooltip';
+import { CheckboxModule } from 'primeng/checkbox';
+import { AutoCompleteModule } from 'primeng/autocomplete';
+
+interface Column {
+ field: string;
+ header: string;
+ customExportHeader?: string;
+}
+
+interface ExportColumn {
+ title: string;
+ dataKey: string;
+}
+
+@Component({
+ selector: 'app-designation',
+ templateUrl: './designation.component.html',
+ standalone: true,
+ imports: [
+ CommonModule, FormsModule, ReactiveFormsModule,
+ TableModule, DialogModule, ButtonModule, ToastModule, ToolbarModule, ConfirmDialogModule,
+ InputTextModule, InputTextareaModule, FileUploadModule, DropdownModule, TagModule,
+ RadioButtonModule, RatingModule, SkeletonModule, InputNumberModule,
+ TooltipModule, AutoCompleteModule, RippleModule, CheckboxModule
+ ],
+ providers: [MessageService, ConfirmationService],
+ styleUrl: './designation.component.css'
+})
+export class DesignationComponent implements OnInit{
+ designationForm: FormGroup;
+ designationDialog: boolean = false;
+ designations!: DesignationDTO[];
+
+ designation: DesignationDTO | undefined;
+
+ selectedDesignations!: DesignationDTO[] | null;
+
+ submitted: boolean = false;
+
+ isLoading: boolean = true;
+
+ skeletonData: any[] = Array(10).fill({});
+
+ departments: DepartmentDTO[] = [];
+
+ statuses!: any[];
+
+ @ViewChild('dt') dt!: Table;
+
+ cols!: Column[];
+
+ exportColumns!: ExportColumn[];
+
+ constructor(
+ private companyService: CompanyService,
+ private messageService: MessageService,
+ private confirmationService: ConfirmationService,
+ private cd: ChangeDetectorRef,
+ private fb: FormBuilder,
+ ) {
+
+ this.designationForm = this.fb.group({
+ id: [{ value: '', disabled: true }],
+ designation: [{ value: '', disabled: false }, [Validators.required, ValidationService.nameValidator()]],
+ departmentId: [{ value: null, disabled: false }, [Validators.required]],
+ hod: [{ value: false, disabled: false }],
+ active: [{ value: true, disabled: false }]
+ });
+ }
+
+ exportCSV() {
+ this.dt.exportCSV();
+ }
+
+ ngOnInit() {
+ this.loadAllDesignations();
+ this.loadDepartments();
+ }
+
+ loadDepartments() {
+ this.companyService.getAllDepartments().subscribe({
+ next: (data) => {
+ this.departments = data.filter(d => d.active);
+ this.cd.markForCheck();
+ },
+ error: (err) => {
+ console.error('Error loading departments', err);
+ }
+ });
+ }
+
+ loadAllDesignations() {
+ this.isLoading = true;
+ this.companyService.getAllDesignations().subscribe({
+ next: (data) => {
+ this.isLoading = false;
+ this.designations = data;
+ this.cd.markForCheck();
+ },
+ error: (err) => {
+ this.isLoading = false;
+ console.error(err);
+ }
+ });
+
+ this.statuses = [
+ { label: 'Active', value: true },
+ { label: 'Inactive', value: false }
+ ];
+
+ this.cols = [
+ { field: 'designation', header: 'Designation', customExportHeader: 'Designation' },
+ { field: 'departmentName', header: 'Department' },
+ { field: 'hod', header: 'HOD' },
+ { field: 'updatedAt', header: 'Last Updated At' },
+ { field: 'updatedUser', header: 'Last Updated By' }
+ ];
+
+ this.exportColumns = this.cols.map((col) => ({ title: col.header, dataKey: col.field }));
+ }
+
+ openNew() {
+ this.designation = undefined;
+ this.designationForm.reset();
+ this.submitted = false;
+ this.designationDialog = true;
+ }
+
+ editDesignation(designation: DesignationDTO) {
+ const parentDepartment = this.departments.find(dep => dep.department === designation.departmentName);
+ console.log(parentDepartment);
+ this.designationForm.reset();
+ this.designation = { ...designation };
+ this.designation.departmentId = parentDepartment?.id ?? '';
+ console.log(this.designation);
+ this.designationForm.patchValue(this.designation);
+ this.designationDialog = true;
+ }
+
+
+ hideDialog() {
+ this.designationDialog = false;
+ this.submitted = false;
+ }
+
+ toggleActive(designation: DesignationDTO) {
+ const isActivating = !designation.active;
+ this.confirmationService.confirm({
+ message: `Are you sure you want to ${isActivating ? 'activate' : 'deactivate'} ` + designation.designation + '?',
+ header: 'Confirm',
+ icon: 'pi pi-exclamation-triangle',
+ rejectButtonStyleClass: 'p-button-text p-button-secondary',
+ acceptButtonStyleClass: isActivating ? 'p-button-success' : 'p-button-danger',
+ accept: () => {
+ this.companyService.activateDeactivateDesignation(designation.id, isActivating).subscribe({
+ next: (updatedDesignation) => {
+ designation.active = updatedDesignation.active;
+ this.designations = [...this.designations];
+ this.messageService.add({
+ severity: 'success',
+ summary: 'Successful',
+ detail: `Designation ${isActivating ? 'Activated' : 'Deactivated'}`,
+ life: 3000
+ });
+ },
+ error: (err) => {
+ console.error('Error toggling designation active status', err);
+ this.messageService.add({
+ severity: 'error',
+ summary: 'Error',
+ detail: 'Failed to update designation status',
+ life: 3000
+ });
+ }
+ });
+ }
+ });
+ }
+
+ getSeverity(status: boolean) {
+ switch (status) {
+ case true:
+ return 'success';
+ case false:
+ return 'warning';
+ }
+ }
+
+ getErrorMessage(fieldName: string): string {
+ const control = this.designationForm.get(fieldName);
+ return control ? ValidationService.getErrorMessage(control, fieldName) : '';
+ }
+
+ isFieldInvalid(fieldName: string): boolean {
+ const control = this.designationForm.get(fieldName);
+ return !!(control && control.invalid && (control.dirty || control.touched || this.submitted));
+ }
+
+ saveDesignation() {
+ this.submitted = true;
+
+ if (this.designationForm.invalid) {
+ return;
+ }
+
+ const designationData = this.designationForm.getRawValue() as DesignationDTO;
+
+ // ✅ Normalize null → empty array
+ const designations = this.designations ?? [];
+
+ // 🔍 Duplicate check (case-insensitive, department-specific)
+ const existing = designations.some(des =>
+ des.designation?.trim().toLowerCase() === designationData.designation?.trim().toLowerCase() &&
+ des.departmentId === designationData.departmentId &&
+ des.id !== designationData.id
+ );
+
+ if (existing) {
+ this.messageService.add({
+ severity: 'error',
+ summary: 'Validation Error',
+ detail: 'Designation already exists in this department',
+ life: 3000
+ });
+ return;
+ }
+
+ this.companyService.saveDesignation(designationData).subscribe({
+ next: (savedDesignation) => {
+
+ const index = designationData.id
+ ? designations.findIndex(des => des.id === designationData.id)
+ : -1;
+
+ if (index !== -1) {
+ // ✅ UPDATE
+ designations[index] = savedDesignation;
+ } else {
+ // ✅ CREATE
+ designations.push(savedDesignation);
+ }
+
+ // ✅ Reassign once (change detection + null safety)
+ this.designations = [...designations];
+
+ this.messageService.add({
+ severity: 'success',
+ summary: 'Successful',
+ detail: index !== -1
+ ? 'Designation Updated'
+ : 'Designation Created',
+ life: 3000
+ });
+
+ this.designationDialog = false;
+ this.designation = undefined;
+ this.designationForm.reset();
+ this.submitted = false;
+ },
+ error: (err) => {
+ console.error('Error saving designation', err);
+ this.messageService.add({
+ severity: 'error',
+ summary: 'Error',
+ detail: 'Failed to save designation',
+ life: 3000
+ });
+ }
+ });
+ }
+}
diff --git a/frontend/src/app/pages/account/company/employee/employee.component.css b/frontend/src/app/pages/account/company/employee/employee.component.css
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/frontend/src/app/pages/account/company/employee/employee.component.css
@@ -0,0 +1 @@
+
diff --git a/frontend/src/app/pages/account/company/employee/employee.component.html b/frontend/src/app/pages/account/company/employee/employee.component.html
new file mode 100644
index 0000000..3a8c54a
--- /dev/null
+++ b/frontend/src/app/pages/account/company/employee/employee.component.html
@@ -0,0 +1,411 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Manage Employees
+
+
+
+
+
+
+
+
+ #
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ rowIndex + 1 }}
+ {{ employee.fullName }}
+ {{ employee.subsidiaryName }}
+ {{ employee.department }}
+ {{ employee.designation }}
+ {{ employee.contactNo }}
+ {{ employee.emailId }}
+ {{ employee.updatedAt | date:'dd/MM/yyyy HH:mm:ss' }}
+
+
+ {{ employee.updatedUser }}
+
+
+
+ Not Available
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/app/pages/account/company/employee/employee.component.spec.ts b/frontend/src/app/pages/account/company/employee/employee.component.spec.ts
new file mode 100644
index 0000000..6f30928
--- /dev/null
+++ b/frontend/src/app/pages/account/company/employee/employee.component.spec.ts
@@ -0,0 +1,21 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { EmployeeComponent } from './employee.component';
+
+describe('EmployeeComponent', () => {
+ let component: EmployeeComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [EmployeeComponent]
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(EmployeeComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/frontend/src/app/pages/account/company/employee/employee.component.ts b/frontend/src/app/pages/account/company/employee/employee.component.ts
new file mode 100644
index 0000000..6ee2ee2
--- /dev/null
+++ b/frontend/src/app/pages/account/company/employee/employee.component.ts
@@ -0,0 +1,429 @@
+import { EmployeeDTO, SubsidiaryDTO, DepartmentDTO, DesignationDTO } from './../../../../models/account.model';
+import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core';
+import { ConfirmationService, MessageService } from 'primeng/api';
+import { TableModule, Table } from 'primeng/table';
+import { DialogModule } from 'primeng/dialog';
+import { RippleModule } from 'primeng/ripple';
+import { ButtonModule } from 'primeng/button';
+import { ToastModule } from 'primeng/toast';
+import { ToolbarModule } from 'primeng/toolbar';
+import { ConfirmDialogModule } from 'primeng/confirmdialog';
+import { InputTextModule } from 'primeng/inputtext';
+import { InputTextareaModule } from 'primeng/inputtextarea';
+import { CommonModule } from '@angular/common';
+import { FileUploadModule } from 'primeng/fileupload';
+import { DropdownModule } from 'primeng/dropdown';
+import { TagModule } from 'primeng/tag';
+import { RadioButtonModule } from 'primeng/radiobutton';
+import { RatingModule } from 'primeng/rating';
+import { SkeletonModule } from 'primeng/skeleton';
+import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
+import { InputNumberModule } from 'primeng/inputnumber';
+import { IconFieldModule } from 'primeng/iconfield';
+import { InputIconModule } from 'primeng/inputicon';
+import { CompanyService } from '../../../../services/account/company/company.service';
+import { ValidationService } from '../../../../services/utilities/validation.service';
+import { TooltipModule } from 'primeng/tooltip';
+import { AutoCompleteModule } from 'primeng/autocomplete';
+import { CalendarModule } from 'primeng/calendar';
+import { MasterService } from '../../../../services/masters/master.service';
+import { CityDTO, SearchDTO } from '../../../../models/masters/masters';
+import { debounceTime, Subject } from 'rxjs';
+import { Request } from '../../../../models/request.model';
+
+interface Column {
+ field: string;
+ header: string;
+ customExportHeader?: string;
+}
+
+interface ExportColumn {
+ title: string;
+ dataKey: string;
+}
+
+@Component({
+ selector: 'app-employee',
+ templateUrl: './employee.component.html',
+ standalone: true,
+ imports: [
+ CommonModule, FormsModule, ReactiveFormsModule,
+ TableModule, DialogModule, ButtonModule, ToastModule, ToolbarModule, ConfirmDialogModule,
+ InputTextModule, InputTextareaModule, FileUploadModule, DropdownModule, TagModule,
+ RadioButtonModule, RatingModule, SkeletonModule, InputNumberModule, IconFieldModule,
+ InputIconModule, TooltipModule, AutoCompleteModule, RippleModule, CalendarModule
+ ],
+ providers: [MessageService, ConfirmationService],
+ styleUrl: './employee.component.css'
+})
+export class EmployeeComponent implements OnInit{
+ employeeForm: FormGroup;
+ employeeDialog: boolean = false;
+ employees!: EmployeeDTO[];
+
+ employee: EmployeeDTO | undefined;
+
+ selectedEmployees!: EmployeeDTO[] | null;
+
+ submitted: boolean = false;
+
+ isLoading: boolean = true;
+
+ skeletonData: any[] = Array(10).fill({});
+
+ subsidiaries: SubsidiaryDTO[] = [];
+
+ departments: DepartmentDTO[] = [];
+
+ designations: DesignationDTO[] = [];
+
+ suggestions: CityDTO[] = [];
+
+ private searchSubject = new Subject();
+
+ genders: any[] = [
+ { label: 'Male', value: 'Male' },
+ { label: 'Female', value: 'Female' },
+ { label: 'Other', value: 'Other' }
+ ];
+
+ statuses!: any[];
+
+ @ViewChild('dt') dt!: Table;
+
+ cols!: Column[];
+
+ exportColumns!: ExportColumn[];
+
+ constructor(
+ private companyService: CompanyService,
+ private masterService: MasterService,
+ private messageService: MessageService,
+ private confirmationService: ConfirmationService,
+ private cd: ChangeDetectorRef,
+ private fb: FormBuilder,
+ ) {
+
+ this.employeeForm = this.fb.group({
+ id: [{ value: '', disabled: true }],
+ subsidiaryId: [{ value: null, disabled: false }, [Validators.required]],
+ departmentId: [{ value: null, disabled: false }, [Validators.required]],
+ designationId: [{ value: null, disabled: false }, [Validators.required]],
+ joiningDate: [{ value: '', disabled: false }],
+ employeeId: [{ value: '', disabled: false }, [Validators.required]],
+ fullName: [{ value: '', disabled: false }, [Validators.required, ValidationService.nameValidator()]],
+ gender: [{ value: '', disabled: false }, [Validators.required]],
+ dob: [{ value: '', disabled: false }, [Validators.required]],
+ contactNo: [{ value: '', disabled: false }, [Validators.required, ValidationService.mobileValidator()]],
+ alternateNo: [{ value: '', disabled: false }, [ValidationService.mobileValidator()]],
+ emailId: [{ value: '', disabled: false }, [Validators.required, ValidationService.emailValidator()]],
+ residenceAddress: [{ value: '', disabled: false }],
+ residenceCityId: [{ value: '', disabled: true }],
+ residenceStateId: [{ value: '', disabled: true }],
+ residenceCityName: [{ value: '', disabled: false }],
+ residenceStateName: [{ value: '', disabled: true }],
+ permanentAddress: [{ value: '', disabled: false }],
+ permanentCityId: [{ value: '', disabled: true }],
+ permanentStateId: [{ value: '', disabled: true }],
+ permanentCityName: [{ value: '', disabled: false }],
+ permanentStateName: [{ value: '', disabled: true }],
+ active: [{ value: true, disabled: false }]
+ });
+ }
+
+ exportCSV() {
+ this.dt.exportCSV();
+ }
+
+ ngOnInit() {
+ this.loadAllEmployees();
+ this.loadSubsidiaries();
+ this.loadDepartments();
+ this.loadDesignations();
+ }
+
+ loadSubsidiaries() {
+ this.companyService.getAllSubsidiaries().subscribe({
+ next: (data) => {
+ this.subsidiaries = data.filter(s => s.active);
+ this.cd.markForCheck();
+ },
+ error: (err) => {
+ console.error('Error loading subsidiaries', err);
+ }
+ });
+ }
+
+ loadDepartments() {
+ this.companyService.getAllDepartments().subscribe({
+ next: (data) => {
+ this.departments = data.filter(d => d.active);
+ this.cd.markForCheck();
+ },
+ error: (err) => {
+ console.error('Error loading departments', err);
+ }
+ });
+ }
+
+ loadDesignations() {
+ this.companyService.getAllDesignations().subscribe({
+ next: (data) => {
+ this.designations = data.filter(d => d.active);
+ this.cd.markForCheck();
+ },
+ error: (err) => {
+ console.error('Error loading designations', err);
+ }
+ });
+ }
+
+ searchCities(event: any) {
+ const query = event.query;
+ this.searchSubject.next(query);
+ }
+
+ onSelectCity(event: any, type: 'permanent' | 'residence') {
+ const city = event.value as CityDTO;
+ if (type === 'permanent') {
+ this.employeeForm.patchValue({
+ permanentCityId: city.id,
+ permanentStateId: city.stateId,
+ permanentCityName: city.cityName,
+ permanentStateName: city.stateName
+ });
+ } else {
+ this.employeeForm.patchValue({
+ residenceCityId: city.id,
+ residenceStateId: city.stateId,
+ residenceCityName: city.cityName,
+ residenceStateName: city.stateName
+ });
+ }
+ }
+
+ loadAllEmployees() {
+ this.isLoading = true;
+ this.companyService.getAllEmployees().subscribe({
+ next: (data) => {
+ this.isLoading = false;
+ this.employees = data;
+ this.cd.markForCheck();
+ },
+ error: (err) => {
+ this.isLoading = false;
+ console.error(err);
+ }
+ });
+
+ this.statuses = [
+ { label: 'Active', value: true },
+ { label: 'Inactive', value: false }
+ ];
+
+ this.cols = [
+ { field: 'fullName', header: 'Full Name', customExportHeader: 'Full Name' },
+ { field: 'subsidiaryName', header: 'Subsidiary' },
+ { field: 'department', header: 'Department' },
+ { field: 'designation', header: 'Designation' },
+ { field: 'contactNo', header: 'Contact No' },
+ { field: 'emailId', header: 'Email' },
+ { field: 'updatedAt', header: 'Last Updated At' },
+ { field: 'updatedUser', header: 'Last Updated By' }
+ ];
+
+ this.exportColumns = this.cols.map((col) => ({ title: col.header, dataKey: col.field }));
+
+ this.searchSubject.pipe(debounceTime(300)).subscribe(query => {
+ if (query && query.length >= 2) {
+ const requestPayload: Request = {
+ data: {
+ searchBy: 'CITY',
+ searchValue: query
+ },
+ compressed: true,
+ target: 'models.commons.Search'
+ };
+ this.masterService.searchCityStates(requestPayload).subscribe({
+ next: (cities) => {
+ this.suggestions = cities.map(city => ({
+ ...city,
+ display: `${city.cityName}, ${city.stateName}`
+ }));
+ this.cd.markForCheck();
+ },
+ error: (err) => {
+ console.error('Error searching cities', err);
+ this.suggestions = [];
+ }
+ });
+ } else {
+ this.suggestions = [];
+ }
+ });
+ }
+
+ openNew() {
+ this.employee = undefined;
+ this.employeeForm.reset();
+ this.submitted = false;
+ this.employeeDialog = true;
+ }
+
+ editEmployee(employee: EmployeeDTO) {
+ const subsidiary = this.subsidiaries.find(sub => sub.name === employee.subsidiaryName);
+ const department = this.departments.find(dep => dep.department === employee.department);
+ const designation = this.designations.find(des => des.designation === employee.designation);
+
+ this.employeeForm.reset();
+ this.employee = { ...employee };
+ this.employee.subsidiaryId = subsidiary?.id ?? '';
+ this.employee.departmentId = department?.id ?? '';
+ this.employee.designationId = designation?.id ?? '';
+
+ // Prepare data for patching, converting dates
+ const employeeToPatch = { ...this.employee };
+ if (employeeToPatch.joiningDate) {
+ (employeeToPatch as any).joiningDate = new Date(employeeToPatch.joiningDate);
+ }
+ if (employeeToPatch.dob) {
+ (employeeToPatch as any).dob = new Date(employeeToPatch.dob);
+ }
+
+ this.employeeForm.patchValue(employeeToPatch);
+ this.employeeDialog = true;
+ console.log(this.employeeForm.getRawValue());
+ }
+
+
+ hideDialog() {
+ this.employeeDialog = false;
+ this.submitted = false;
+ }
+
+ toggleActive(employee: EmployeeDTO) {
+ const isActivating = !employee.active;
+ this.confirmationService.confirm({
+ message: `Are you sure you want to ${isActivating ? 'activate' : 'deactivate'} ` + employee.fullName + '?',
+ header: 'Confirm',
+ icon: 'pi pi-exclamation-triangle',
+ rejectButtonStyleClass: 'p-button-text p-button-secondary',
+ acceptButtonStyleClass: isActivating ? 'p-button-success' : 'p-button-danger',
+ accept: () => {
+ this.companyService.activateDeactivateEmployee(employee.id, isActivating).subscribe({
+ next: (updatedEmployee) => {
+ employee.active = updatedEmployee.active;
+ this.employees = [...this.employees];
+ this.messageService.add({
+ severity: 'success',
+ summary: 'Successful',
+ detail: `Employee ${isActivating ? 'Activated' : 'Deactivated'}`,
+ life: 3000
+ });
+ },
+ error: (err) => {
+ console.error('Error toggling employee active status', err);
+ this.messageService.add({
+ severity: 'error',
+ summary: 'Error',
+ detail: 'Failed to update employee status',
+ life: 3000
+ });
+ }
+ });
+ }
+ });
+ }
+
+ getSeverity(status: boolean) {
+ switch (status) {
+ case true:
+ return 'success';
+ case false:
+ return 'warning';
+ }
+ }
+
+ getErrorMessage(fieldName: string): string {
+ const control = this.employeeForm.get(fieldName);
+ return control ? ValidationService.getErrorMessage(control, fieldName) : '';
+ }
+
+ isFieldInvalid(fieldName: string): boolean {
+ const control = this.employeeForm.get(fieldName);
+ return !!(control && control.invalid && (control.dirty || control.touched || this.submitted));
+ }
+
+ saveEmployee() {
+ this.submitted = true;
+
+ if (this.employeeForm.invalid) {
+ return;
+ }
+
+ const employeeData = this.employeeForm.getRawValue() as EmployeeDTO;
+
+ // ✅ Normalize null → empty array
+ const employees = this.employees ?? [];
+
+ // 🔍 Duplicate check (case-insensitive)
+ const existing = employees.some(emp => emp.id !== employeeData.id && (
+ emp.fullName?.trim().toLowerCase() === employeeData.fullName?.trim().toLowerCase() ||
+ emp.contactNo?.trim() === employeeData.contactNo?.trim() ||
+ emp.emailId?.trim().toLowerCase() === employeeData.emailId?.trim().toLowerCase()
+ ));
+
+ if (existing) {
+ this.messageService.add({
+ severity: 'error',
+ summary: 'Validation Error',
+ detail: 'Employee with same name, contact or email already exists',
+ life: 3000
+ });
+ return;
+ }
+
+ this.companyService.saveEmployee(employeeData).subscribe({
+ next: (savedEmployee) => {
+
+ const index = employeeData.id
+ ? employees.findIndex(emp => emp.id === employeeData.id)
+ : -1;
+
+ if (index !== -1) {
+ // ✅ UPDATE
+ employees[index] = savedEmployee;
+ } else {
+ // ✅ CREATE
+ employees.push(savedEmployee);
+ }
+
+ // ✅ Reassign once (change detection + null safety)
+ this.employees = [...employees];
+
+ this.messageService.add({
+ severity: 'success',
+ summary: 'Successful',
+ detail: index !== -1
+ ? 'Employee Updated'
+ : 'Employee Created',
+ life: 3000
+ });
+
+ this.employeeDialog = false;
+ this.employee = undefined;
+ this.employeeForm.reset();
+ this.submitted = false;
+ },
+ error: (err) => {
+ console.error('Error saving employee', err);
+ this.messageService.add({
+ severity: 'error',
+ summary: 'Error',
+ detail: 'Failed to save employee',
+ life: 3000
+ });
+ }
+ });
+ }
+}
diff --git a/frontend/src/app/pages/account/company/subsidiary/subsidiary.component.css b/frontend/src/app/pages/account/company/subsidiary/subsidiary.component.css
new file mode 100644
index 0000000..463526a
--- /dev/null
+++ b/frontend/src/app/pages/account/company/subsidiary/subsidiary.component.css
@@ -0,0 +1,5 @@
+:host ::ng-deep .p-dialog .product-image {
+ width: 150px;
+ margin: 0 auto 2rem auto;
+ display: block;
+}
\ No newline at end of file
diff --git a/frontend/src/app/pages/account/company/subsidiary/subsidiary.component.html b/frontend/src/app/pages/account/company/subsidiary/subsidiary.component.html
new file mode 100644
index 0000000..0023a1a
--- /dev/null
+++ b/frontend/src/app/pages/account/company/subsidiary/subsidiary.component.html
@@ -0,0 +1,322 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Manage Subsidiaries
+
+
+
+
+
+
+
+
+ #
+ Code
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ rowIndex + 1 }}
+ {{ subsidiary.code }}
+ {{ subsidiary.name }}
+
+
+ {{ subsidiary.emailId }}
+
+
+
+ Not Available
+
+
+ {{ subsidiary.updatedAt | date:'dd/MM/yyyy HH:mm:ss' }}
+
+
+ {{ subsidiary.updatedUser }}
+
+
+
+ Not Available
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/app/pages/account/company/subsidiary/subsidiary.component.spec.ts b/frontend/src/app/pages/account/company/subsidiary/subsidiary.component.spec.ts
new file mode 100644
index 0000000..ac76031
--- /dev/null
+++ b/frontend/src/app/pages/account/company/subsidiary/subsidiary.component.spec.ts
@@ -0,0 +1,21 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { SubsidiaryComponent } from './subsidiary.component';
+
+describe('SubsidiaryComponent', () => {
+ let component: SubsidiaryComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [SubsidiaryComponent]
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(SubsidiaryComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/frontend/src/app/pages/account/company/subsidiary/subsidiary.component.ts b/frontend/src/app/pages/account/company/subsidiary/subsidiary.component.ts
new file mode 100644
index 0000000..2de8ff9
--- /dev/null
+++ b/frontend/src/app/pages/account/company/subsidiary/subsidiary.component.ts
@@ -0,0 +1,329 @@
+import { SearchDTO } from './../../../../models/masters/masters';
+import { SubsidiaryDTO } from './../../../../models/account.model';
+import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core';
+import { ConfirmationService, MessageService } from 'primeng/api';
+import { TableModule, Table } from 'primeng/table';
+import { DialogModule } from 'primeng/dialog';
+import { RippleModule } from 'primeng/ripple';
+import { ButtonModule } from 'primeng/button';
+import { ToastModule } from 'primeng/toast';
+import { ToolbarModule } from 'primeng/toolbar';
+import { ConfirmDialogModule } from 'primeng/confirmdialog';
+import { InputTextModule } from 'primeng/inputtext';
+import { InputTextareaModule } from 'primeng/inputtextarea';
+import { CommonModule } from '@angular/common';
+import { FileUploadModule } from 'primeng/fileupload';
+import { DropdownModule } from 'primeng/dropdown';
+import { TagModule } from 'primeng/tag';
+import { RadioButtonModule } from 'primeng/radiobutton';
+import { RatingModule } from 'primeng/rating';
+import { SkeletonModule } from 'primeng/skeleton';
+import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
+import { InputNumberModule } from 'primeng/inputnumber';
+import { IconFieldModule } from 'primeng/iconfield';
+import { InputIconModule } from 'primeng/inputicon';
+import { CompanyService } from '../../../../services/account/company/company.service';
+import { ValidationService } from '../../../../services/utilities/validation.service';
+import { TooltipModule } from 'primeng/tooltip';
+import { AutoCompleteModule } from 'primeng/autocomplete';
+import { MasterService } from '../../../../services/masters/master.service';
+import { CityDTO } from '../../../../models/masters/masters';
+import { debounceTime, Subject } from 'rxjs';
+import { Request } from '../../../../models/request.model';
+
+interface Column {
+ field: string;
+ header: string;
+ customExportHeader?: string;
+}
+
+interface ExportColumn {
+ title: string;
+ dataKey: string;
+}
+
+@Component({
+ selector: 'app-subsidiary',
+ templateUrl: './subsidiary.component.html',
+ standalone: true,
+ imports: [
+ CommonModule, FormsModule, ReactiveFormsModule,
+ TableModule, DialogModule, ButtonModule, ToastModule, ToolbarModule, ConfirmDialogModule,
+ InputTextModule, InputTextareaModule, FileUploadModule, DropdownModule, TagModule,
+ RadioButtonModule, RatingModule, SkeletonModule, InputNumberModule, IconFieldModule,
+ InputIconModule, TooltipModule, AutoCompleteModule, RippleModule
+ ],
+ providers: [MessageService, ConfirmationService],
+ styleUrl: './subsidiary.component.css'
+})
+export class SubsidiaryComponent implements OnInit{
+ subsidiaryForm: FormGroup;
+ subsidiaryDialog: boolean = false;
+ subsidiaries!: SubsidiaryDTO[];
+
+ subsidiary!: SubsidiaryDTO;
+
+ selectedSubsidiaries!: SubsidiaryDTO[] | null;
+
+ submitted: boolean = false;
+
+ isLoading: boolean = true;
+
+ skeletonData: any[] = Array(10).fill({});
+
+ suggestions: CityDTO[] = [];
+
+ private searchSubject = new Subject();
+
+ statuses!: any[];
+
+ @ViewChild('dt') dt!: Table;
+
+ cols!: Column[];
+
+ exportColumns!: ExportColumn[];
+
+ constructor(
+ private subsidiaryService: CompanyService,
+ private masterService: MasterService,
+ private messageService: MessageService,
+ private confirmationService: ConfirmationService,
+ private cd: ChangeDetectorRef,
+ private fb: FormBuilder,
+ ) {
+
+ this.subsidiaryForm = this.fb.group({
+ id: [{ value: '', disabled: true }],
+ cityId: [{ value: null, disabled: true }],
+ code: [{ value: '', disabled: false }, [Validators.required, ValidationService.codeValidator()]],
+ name: [{ value: '', disabled: false }, [Validators.required, ValidationService.nameValidator()]],
+ officeNo: [{ value: '', disabled: false }],
+ street: [{ value: '', disabled: false }],
+ locality: [{ value: '', disabled: false }],
+ cityName: [{ value: '', disabled: false }],
+ stateName: [{ value: '', disabled: true }],
+ pinCode: [{ value: '', disabled: false }, [ValidationService.pincodeValidator()]],
+ emailId: [{ value: '', disabled: false }, [ValidationService.emailValidator()]],
+ contactNo: [{ value: '', disabled: false }, [ValidationService.mobileValidator()]],
+ contactPerson: [{ value: '', disabled: false }, [ValidationService.contactPersonValidator()]],
+ panNo: [{ value: '', disabled: false }, [ValidationService.panValidator()]],
+ cinNo: [{ value: '', disabled: false }, [ValidationService.cinValidator()]],
+ msmeNo: [{ value: '', disabled: false }, [ValidationService.msmeValidator()]],
+ stateId: [{ value: '', disabled: true }],
+ stateCode: [{ value: '', disabled: true }],
+ gstCode: [{ value: '', disabled: true }]
+ });
+ }
+
+ exportCSV() {
+ this.dt.exportCSV();
+ }
+
+ onSearch(event: Event) {
+ const input = event.target as HTMLInputElement;
+ this.dt.filterGlobal(input.value, 'contains');
+ }
+
+ searchCities(event: any) {
+ const query = event.query;
+ this.searchSubject.next(query);
+ }
+
+ onSelectCity(event: any) {
+ const city = event.value as CityDTO;
+ this.subsidiaryForm.patchValue({
+ cityId: city.id,
+ stateId: city.stateId,
+ cityName: city.cityName,
+ stateName: city.stateName,
+ stateCode: city.stateCode,
+ gstCode: city.gstCode
+ });
+ }
+
+ ngOnInit() {
+ this.loadAllSubsidiaries();
+ }
+
+ loadAllSubsidiaries() {
+ this.isLoading = true;
+ this.subsidiaryService.getAllSubsidiaries().subscribe({
+ next: (data) => {
+ this.isLoading = false;
+ this.subsidiaries = data;
+ this.cd.markForCheck();
+ },
+ error: (err) => {
+ this.isLoading = false;
+ console.error(err);
+ }
+ });
+
+ this.statuses = [
+ { label: 'Active', value: true },
+ { label: 'Inactive', value: false }
+ ];
+
+ this.cols = [
+ { field: 'code', header: 'Code', customExportHeader: 'Code' },
+ { field: 'name', header: 'Name' },
+ { field: 'officeNo', header: 'Office No / Building / Floor' },
+ { field: 'street', header: 'Street / Road' },
+ { field: 'locality', header: 'Locality' },
+ { field: 'cityName', header: 'City' },
+ { field: 'emailId', header: 'Email' },
+ { field: 'contactNo', header: 'Contact No' },
+ { field: 'contactPerson', header: 'Contact Person' },
+ { field: 'panNo', header: 'PAN No.' },
+ { field: 'cinNo', header: 'CIN No.' },
+ { field: 'msmeNo', header: 'MSME No.' },
+ { field: 'updatedAt', header: 'Last Updated At' },
+ { field: 'updatedUser', header: 'Last Updated By' }
+ ];
+
+ this.exportColumns = this.cols.map((col) => ({ title: col.header, dataKey: col.field }));
+
+ this.searchSubject.pipe(debounceTime(300)).subscribe(query => {
+ if (query && query.length >= 2) {
+ const requestPayload: Request = {
+ data: {
+ searchBy: 'CITY',
+ searchValue: query
+ },
+ compressed: true,
+ target: 'models.commons.Search'
+ };
+ this.masterService.searchCityStates(requestPayload).subscribe({
+ next: (cities) => {
+ this.suggestions = cities.map(city => ({
+ ...city,
+ display: `${city.cityName}, ${city.stateName}`
+ }));
+ this.cd.markForCheck();
+ },
+ error: (err) => {
+ console.error('Error searching cities', err);
+ this.suggestions = [];
+ }
+ });
+ } else {
+ this.suggestions = [];
+ }
+ });
+ }
+
+ openNew() {
+ this.subsidiary = {};
+ this.subsidiaryForm.reset();
+ this.submitted = false;
+ this.subsidiaryDialog = true;
+ }
+
+ editSubsidiary(subsidiary: SubsidiaryDTO) {
+ this.subsidiaryForm.reset();
+ this.subsidiary = { ...subsidiary };
+ this.subsidiaryForm.patchValue(subsidiary);
+ this.subsidiaryDialog = true;
+ console.log(this.subsidiaryForm.getRawValue());
+ }
+
+
+ hideDialog() {
+ this.subsidiaryDialog = false;
+ this.submitted = false;
+ }
+
+ toggleActive(subsidiary: SubsidiaryDTO) {
+ const isActivating = !subsidiary.active;
+ this.confirmationService.confirm({
+ message: `Are you sure you want to ${isActivating ? 'activate' : 'deactivate'} ` + subsidiary.name + '?',
+ header: 'Confirm',
+ icon: 'pi pi-exclamation-triangle',
+ rejectButtonStyleClass: 'p-button-text p-button-secondary',
+ acceptButtonStyleClass: isActivating ? 'p-button-success' : 'p-button-danger',
+ accept: () => {
+ this.subsidiaryService.activateDeactivateSubsidiary(subsidiary.id?? '', isActivating).subscribe({
+ next: (updatedSubsidiary) => {
+ subsidiary.active = updatedSubsidiary.active;
+ this.subsidiaries = [...this.subsidiaries];
+ this.messageService.add({
+ severity: 'success',
+ summary: 'Successful',
+ detail: `Subsidiary ${isActivating ? 'Activated' : 'Deactivated'}`,
+ life: 3000
+ });
+ },
+ error: (err) => {
+ console.error('Error toggling subsidiary active status', err);
+ this.messageService.add({
+ severity: 'error',
+ summary: 'Error',
+ detail: 'Failed to update subsidiary status',
+ life: 3000
+ });
+ }
+ });
+ }
+ });
+ }
+
+ getSeverity(status: boolean) {
+ switch (status) {
+ case true:
+ return 'success';
+ case false:
+ return 'warning';
+ }
+ }
+
+ getErrorMessage(fieldName: string): string {
+ const control = this.subsidiaryForm.get(fieldName);
+ return control ? ValidationService.getErrorMessage(control, fieldName) : '';
+ }
+
+ isFieldInvalid(fieldName: string): boolean {
+ const control = this.subsidiaryForm.get(fieldName);
+ return !!(control && control.invalid && (control.dirty || control.touched || this.submitted));
+ }
+
+ saveSubsidiary() {
+ this.submitted = true;
+
+ if (this.subsidiaryForm.valid) {
+ const subsidiaryData = this.subsidiaryForm.getRawValue();
+ const existing = this.subsidiaries.find(sub => sub.id !== subsidiaryData.id && (sub.code === subsidiaryData.code || sub.name === subsidiaryData.name));
+ if (existing) {
+ this.messageService.add({
+ severity: 'error',
+ summary: 'Validation Error',
+ detail: 'Subsidiary code or name already exists',
+ life: 3000
+ });
+ return;
+ }
+ this.subsidiaryService.saveSubsidiary(subsidiaryData).subscribe({
+ next: (newSubsidiary) => {
+ this.subsidiaries.push(newSubsidiary);
+ this.subsidiaries = [...this.subsidiaries];
+ this.messageService.add({
+ severity: 'success',
+ summary: 'Successful',
+ detail: 'Subsidiary Created',
+ life: 3000
+ });
+ this.subsidiaryDialog = false;
+ this.subsidiary = {};
+ },
+ error: (err) => {
+ console.error('Error saving subsidiary', err);
+ this.messageService.add({
+ severity: 'error',
+ summary: 'Error',
+ detail: 'Failed to save subsidiary',
+ life: 3000
+ });
+ }
+ });
+ }
+ }
+}
diff --git a/frontend/src/app/pages/account/user/user.component.css b/frontend/src/app/pages/account/user/user.component.css
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/frontend/src/app/pages/account/user/user.component.css
@@ -0,0 +1 @@
+
diff --git a/frontend/src/app/pages/account/user/user.component.html b/frontend/src/app/pages/account/user/user.component.html
new file mode 100644
index 0000000..c0540ea
--- /dev/null
+++ b/frontend/src/app/pages/account/user/user.component.html
@@ -0,0 +1,303 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Manage Users
+
+
+
+
+
+
+
+
+ #
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ rowIndex + 1 }}
+ {{ user.loginId }}
+ {{ user.displayName }}
+
+
+ {{ user.employeeId }}
+
+
+ Not Available
+
+
+
+
+
+ {{ user.updatedAt | date:'dd/MM/yyyy HH:mm:ss' }}
+
+
+ {{ user.updatedUser }}
+
+
+ Not Available
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/app/pages/account/user/user.component.spec.ts b/frontend/src/app/pages/account/user/user.component.spec.ts
new file mode 100644
index 0000000..56317f0
--- /dev/null
+++ b/frontend/src/app/pages/account/user/user.component.spec.ts
@@ -0,0 +1,21 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { UserComponent } from './user.component';
+
+describe('UserComponent', () => {
+ let component: UserComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [UserComponent]
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(UserComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/frontend/src/app/pages/account/user/user.component.ts b/frontend/src/app/pages/account/user/user.component.ts
new file mode 100644
index 0000000..07b88d4
--- /dev/null
+++ b/frontend/src/app/pages/account/user/user.component.ts
@@ -0,0 +1,362 @@
+import { UserDTO, UserRoleDTO } from '../../../models/user.model';
+import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core';
+import { ConfirmationService, MessageService } from 'primeng/api';
+import { TableModule, Table } from 'primeng/table';
+import { DialogModule } from 'primeng/dialog';
+import { RippleModule } from 'primeng/ripple';
+import { ButtonModule } from 'primeng/button';
+import { ToastModule } from 'primeng/toast';
+import { ToolbarModule } from 'primeng/toolbar';
+import { ConfirmDialogModule } from 'primeng/confirmdialog';
+import { InputTextModule } from 'primeng/inputtext';
+import { InputTextareaModule } from 'primeng/inputtextarea';
+import { CommonModule } from '@angular/common';
+import { FileUploadModule } from 'primeng/fileupload';
+import { DropdownModule } from 'primeng/dropdown';
+import { TagModule } from 'primeng/tag';
+import { RadioButtonModule } from 'primeng/radiobutton';
+import { RatingModule } from 'primeng/rating';
+import { SkeletonModule } from 'primeng/skeleton';
+import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
+import { InputNumberModule } from 'primeng/inputnumber';
+import { UserService } from '../../../services/account/user/user.service';
+import { CompanyService } from '../../../services/account/company/company.service';
+import { ValidationService } from '../../../services/utilities/validation.service';
+import { TooltipModule } from 'primeng/tooltip';
+import { AutoCompleteModule } from 'primeng/autocomplete';
+import { FieldsetModule } from 'primeng/fieldset';
+import { debounceTime, Subject } from 'rxjs';
+
+interface Column {
+ field: string;
+ header: string;
+ customExportHeader?: string;
+}
+
+interface ExportColumn {
+ title: string;
+ dataKey: string;
+}
+
+@Component({
+ selector: 'app-user',
+ templateUrl: './user.component.html',
+ standalone: true,
+ imports: [
+ CommonModule, FormsModule, ReactiveFormsModule,
+ TableModule, DialogModule, ButtonModule, ToastModule, ToolbarModule, ConfirmDialogModule,
+ InputTextModule, InputTextareaModule, FileUploadModule, DropdownModule, TagModule,
+ RadioButtonModule, RatingModule, SkeletonModule, InputNumberModule,
+ TooltipModule, AutoCompleteModule, FieldsetModule, RippleModule
+ ],
+ providers: [MessageService, ConfirmationService],
+ styleUrl: './user.component.css'
+})
+export class UserComponent implements OnInit{
+ userForm: FormGroup;
+ userDialog: boolean = false;
+ users!: UserDTO[];
+
+ user: UserDTO | undefined;
+
+ selectedUsers!: UserDTO[] | null;
+
+ submitted: boolean = false;
+
+ isLoading: boolean = true;
+
+ skeletonData: any[] = Array(10).fill({});
+
+ userRoles: UserRoleDTO[] = [];
+
+ branches: any[] = [];
+
+ userRolesOptions: any[] = [];
+
+ suggestions: any[] = [];
+
+ private searchSubject = new Subject();
+
+
+ statuses!: any[];
+
+ @ViewChild('dt') dt!: Table;
+
+ cols!: Column[];
+
+ exportColumns!: ExportColumn[];
+
+ constructor(
+ private userService: UserService,
+ private companyService: CompanyService,
+ private messageService: MessageService,
+ private confirmationService: ConfirmationService,
+ private cd: ChangeDetectorRef,
+ private fb: FormBuilder,
+ ) {
+
+ this.userForm = this.fb.group({
+ id: [{ value: '', disabled: true }],
+ loginId: [{ value: '', disabled: false }, [Validators.required]],
+ displayName: [{ value: '', disabled: false }, [Validators.required, ValidationService.nameValidator()]],
+ fkEmployeeId: [{ value: '', disabled: false }],
+ employeeId: [{ value: '', disabled: false }],
+ employeeName: [{ value: '', disabled: false }],
+ fatherName: [{ value: '', disabled: false }],
+ department: [{ value: '', disabled: false }],
+ designation: [{ value: '', disabled: false }],
+ status: [{ value: 'Active', disabled: false }, [Validators.required]],
+ active: [{ value: true, disabled: false }]
+ });
+ }
+
+ exportCSV() {
+ this.dt.exportCSV();
+ }
+
+ onSearch(event: Event) {
+ const input = event.target as HTMLInputElement;
+ this.dt.filterGlobal(input.value, 'contains');
+ }
+
+ ngOnInit() {
+ this.loadAllUsers();
+ }
+
+ loadAllUsers() {
+ this.isLoading = true;
+ this.userService.getAllUsers().subscribe({
+ next: (data) => {
+ this.isLoading = false;
+ this.users = data;
+ this.cd.markForCheck();
+ },
+ error: (err) => {
+ this.isLoading = false;
+ console.error(err);
+ }
+ });
+
+ this.statuses = [
+ { label: 'Active', value: 'Active' },
+ { label: 'Inactive', value: 'Inactive' }
+ ];
+
+ this.cols = [
+ { field: 'loginId', header: 'Login ID', customExportHeader: 'Login ID' },
+ { field: 'displayName', header: 'Display Name' },
+ { field: 'employeeId', header: 'Employee ID' },
+ { field: 'status', header: 'Status' },
+ { field: 'updatedAt', header: 'Last Updated At' },
+ { field: 'updatedUser', header: 'Last Updated By' }
+ ];
+
+ this.exportColumns = this.cols.map((col) => ({ title: col.header, dataKey: col.field }));
+
+ this.searchSubject.pipe(debounceTime(300)).subscribe(query => {
+ if (query && query.length >= 2) {
+ const requestPayload = {
+ data: {
+ searchBy: 'NAME',
+ searchValue: query
+ },
+ compressed: true,
+ target: 'models.commons.Search'
+ };
+ this.companyService.searchEmployees(requestPayload).subscribe({
+ next: (employees) => {
+ this.suggestions = employees.map(emp => ({
+ ...emp,
+ display: `${emp.employeeId} - ${emp.fullName}`
+ }));
+ this.cd.markForCheck();
+ },
+ error: (err) => {
+ console.error('Error searching employees', err);
+ this.suggestions = [];
+ }
+ });
+ } else {
+ this.suggestions = [];
+ }
+ });
+ }
+
+ openNew() {
+ this.user = undefined;
+ this.userForm.reset();
+ this.userRoles = [];
+ this.submitted = false;
+ this.userDialog = true;
+ }
+
+ editUser(user: UserDTO) {
+ this.userForm.reset();
+ this.user = { ...user };
+ this.userRoles = [...(user.userRoles || [])];
+ this.userForm.patchValue(user);
+ this.userDialog = true;
+ console.log(this.userForm.getRawValue());
+ }
+
+ hideDialog() {
+ this.userDialog = false;
+ this.submitted = false;
+ }
+
+ toggleActive(user: UserDTO) {
+ const isActivating = !user.active;
+ this.confirmationService.confirm({
+ message: `Are you sure you want to ${isActivating ? 'activate' : 'deactivate'} ` + user.displayName + '?',
+ header: 'Confirm',
+ icon: 'pi pi-exclamation-triangle',
+ rejectButtonStyleClass: 'p-button-text p-button-secondary',
+ acceptButtonStyleClass: isActivating ? 'p-button-success' : 'p-button-danger',
+ accept: () => {
+ this.userService.activateDeactivateUser(user.id!, isActivating).subscribe({
+ next: (updatedUser) => {
+ user.active = updatedUser.active;
+ this.users = [...this.users];
+ this.messageService.add({
+ severity: 'success',
+ summary: 'Successful',
+ detail: `User ${isActivating ? 'Activated' : 'Deactivated'}`,
+ life: 3000
+ });
+ },
+ error: (err) => {
+ console.error('Error toggling user active status', err);
+ this.messageService.add({
+ severity: 'error',
+ summary: 'Error',
+ detail: 'Failed to update user status',
+ life: 3000
+ });
+ }
+ });
+ }
+ });
+ }
+
+ getSeverity(status: string) {
+ switch (status) {
+ case 'Active':
+ return 'success';
+ case 'Inactive':
+ return 'warning';
+ }
+ return 'info';
+ }
+
+ getErrorMessage(fieldName: string): string {
+ const control = this.userForm.get(fieldName);
+ return control ? ValidationService.getErrorMessage(control, fieldName) : '';
+ }
+
+ isFieldInvalid(fieldName: string): boolean {
+ const control = this.userForm.get(fieldName);
+ return !!(control && control.invalid && (control.dirty || control.touched || this.submitted));
+ }
+
+ addRole() {
+ // for now, do nothing, since selects empty
+ }
+
+ toggleRoleActive(role: UserRoleDTO) {
+ role.active = role.active === false ? true : false;
+ }
+
+ onSelectEmployee(event: any) {
+ const emp = event.value;
+ const patch: any = {
+ fkEmployeeId: emp.id,
+ employeeId: emp.employeeId,
+ employeeName: emp.fullName,
+ fatherName: emp.fatherName,
+ department: emp.department,
+ designation: emp.designation
+ };
+ if (!this.userForm.get('displayName')!.value) {
+ patch.displayName = emp.fullName;
+ }
+ this.userForm.patchValue(patch);
+ console.log(this.userForm);
+ }
+
+ searchEmployees(event: any) {
+ this.searchSubject.next(event.query);
+ }
+
+
+ saveUser() {
+ this.submitted = true;
+
+ if (this.userForm.invalid) {
+ return;
+ }
+
+ const userData = this.userForm.getRawValue() as UserDTO;
+ userData.userRoles = this.userRoles;
+
+ // Normalize null → empty array
+ const users = this.users ?? [];
+
+ // Duplicate check (case-insensitive)
+ const existing = users.some(u => u.id !== userData.id && (
+ u.loginId?.trim().toLowerCase() === userData.loginId?.trim().toLowerCase()
+ ));
+
+ if (existing) {
+ this.messageService.add({
+ severity: 'error',
+ summary: 'Validation Error',
+ detail: 'User with same login ID already exists',
+ life: 3000
+ });
+ return;
+ }
+
+ this.userService.saveUser(userData).subscribe({
+ next: (savedUser) => {
+
+ const index = userData.id
+ ? users.findIndex(u => u.id === userData.id)
+ : -1;
+
+ if (index !== -1) {
+ // UPDATE
+ users[index] = savedUser;
+ } else {
+ // CREATE
+ users.push(savedUser);
+ }
+
+ // Reassign once (change detection + null safety)
+ this.users = [...users];
+
+ this.messageService.add({
+ severity: 'success',
+ summary: 'Successful',
+ detail: index !== -1
+ ? 'User Updated'
+ : 'User Created',
+ life: 3000
+ });
+
+ this.userDialog = false;
+ this.user = undefined;
+ this.userForm.reset();
+ this.submitted = false;
+ },
+ error: (err) => {
+ console.error('Error saving user', err);
+ this.messageService.add({
+ severity: 'error',
+ summary: 'Error',
+ detail: 'Failed to save user',
+ life: 3000
+ });
+ }
+ });
+ }
+}
diff --git a/frontend/src/app/pages/dashboard/dashboard.component.ts b/frontend/src/app/pages/dashboard/dashboard.component.ts
new file mode 100644
index 0000000..8e6f11d
--- /dev/null
+++ b/frontend/src/app/pages/dashboard/dashboard.component.ts
@@ -0,0 +1,17 @@
+import { Component } from '@angular/core';
+import { MenuComponent } from "../../fragments/menu/menu.component";
+import { RouterOutlet } from '@angular/router';
+
+@Component({
+ selector: 'app-dashboard',
+ imports: [MenuComponent, RouterOutlet],
+ template: ``
+})
+export class DashboardComponent {
+ constructor(){}
+}
diff --git a/frontend/src/app/pages/session/auth/authorize.component.css b/frontend/src/app/pages/session/auth/authorize.component.css
new file mode 100644
index 0000000..0372f99
--- /dev/null
+++ b/frontend/src/app/pages/session/auth/authorize.component.css
@@ -0,0 +1,38 @@
+.login-page {
+ height: 99vh;
+}
+
+::ng-deep .p-card {
+ position: relative;
+ border-radius: 24px !important;
+ background: white;
+ padding: 1rem;
+ overflow: hidden;
+}
+
+/* Gradient border */
+::ng-deep .p-card::before {
+ content: "";
+ position: absolute;
+ inset: 0;
+ padding: 3px; /* border thickness */
+ border-radius: 24px;
+ filter: drop-shadow(0 0 12px rgba(91, 185, 138, 0.35));
+ background: linear-gradient(
+ 180deg,
+ #ffb455 0%,
+ rgba(255, 180, 85, 0.6) 40%,
+ rgba(255, 180, 85, 0.15) 70%,
+ transparent 100%
+ );
+
+ /* Mask trick = border only */
+ -webkit-mask:
+ linear-gradient(#fff 0 0) content-box,
+ linear-gradient(#fff 0 0);
+ -webkit-mask-composite: xor;
+ mask-composite: exclude;
+
+ pointer-events: none;
+}
+
diff --git a/frontend/src/app/pages/session/auth/authorize.component.html b/frontend/src/app/pages/session/auth/authorize.component.html
new file mode 100644
index 0000000..6d37b8a
--- /dev/null
+++ b/frontend/src/app/pages/session/auth/authorize.component.html
@@ -0,0 +1,38 @@
+
diff --git a/frontend/src/app/pages/session/auth/authorize.component.ts b/frontend/src/app/pages/session/auth/authorize.component.ts
new file mode 100644
index 0000000..11bc2af
--- /dev/null
+++ b/frontend/src/app/pages/session/auth/authorize.component.ts
@@ -0,0 +1,106 @@
+import { Component, OnInit } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule, ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
+import { ButtonModule } from 'primeng/button';
+import { InputTextModule } from 'primeng/inputtext';
+import { CardModule } from 'primeng/card';
+import { InputGroupModule } from 'primeng/inputgroup';
+import { InputGroupAddonModule } from 'primeng/inputgroupaddon';
+import { PasswordModule } from 'primeng/password';
+import { KeyFilterModule } from 'primeng/keyfilter';
+import { DropdownModule } from 'primeng/dropdown';
+import { MessageModule } from 'primeng/message';
+import { Router } from '@angular/router';
+import { SessionService } from './../../../services/commons/session.service';
+import { HttpService } from '../../../services/http.service';
+import { environment } from '../../../../environments/environment';
+import { Company, Branch } from '../../../models/session.model';
+import { Request } from '../../../models/request.model';
+import { ResponseDto } from '../../../models/response.dto';
+
+@Component({
+ selector: 'app-authorize',
+ standalone: true,
+ imports: [
+ CommonModule,
+ FormsModule,
+ ReactiveFormsModule,
+ CardModule,
+ ButtonModule,
+ InputTextModule,
+ PasswordModule,
+ DropdownModule,
+ KeyFilterModule,
+ MessageModule
+ ],
+ templateUrl: './authorize.component.html',
+ styleUrl: './authorize.component.css'
+})
+export class AuthorizeComponent implements OnInit {
+ companies: Company[] = [];
+ branches: Branch[] = [];
+ isCompanyDisabled: boolean = false;
+ authForm: FormGroup;
+ message?: string;
+
+ constructor(private fb: FormBuilder, private http: HttpService, private router: Router, private sessionService: SessionService) {
+ this.authForm = this.fb.group({
+ companyId: ['', Validators.required],
+ branchId: ['', Validators.required]
+ });
+ }
+
+ ngOnInit(){
+ const companyBranchRoles = this.sessionService.getItem('companies');
+
+ if (!companyBranchRoles) {
+ this.sessionService.logout();
+ return;
+ }
+ sessionStorage.removeItem('companies');
+
+ try {
+ this.companies = companyBranchRoles;
+ } catch (e) {
+ this.sessionService.logout();
+ }
+
+ if (this.companies.length === 1) {
+ this.authForm.patchValue({ companyId: this.companies[0].id });
+ this.isCompanyDisabled = true;
+ }
+
+ this.updateBranches();
+ this.authForm.get('companyId')?.valueChanges.subscribe(() => {
+ this.updateBranches();
+ });
+ }
+
+ private updateBranches(): void {
+ const companyId = this.authForm.get('companyId')?.value;
+ const selectedCompany = this.companies.find(c => c.id === companyId);
+ this.branches = selectedCompany?.branches || [];
+ this.authForm.patchValue({ branchId: '' });
+ }
+
+ onAuthorize() {
+ this.message = '';
+ if (this.authForm.valid) {
+ const requestPayload: Request = {
+ data: this.authForm.get('branchId')?.value
+ };
+
+ this.http.post(`${environment.authService}/3z4mkell5g5aset/authorize`, requestPayload).subscribe({
+ next: (response) => {
+ this.router.navigate(['/dashboard']);
+ },
+ error: (err) => {
+ this.message = err.message;
+ }
+ });
+ } else {
+ alert('Please fill out the form correctly');
+ }
+ }
+
+}
diff --git a/frontend/src/app/pages/session/profile/profile.component.css b/frontend/src/app/pages/session/profile/profile.component.css
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/frontend/src/app/pages/session/profile/profile.component.css
@@ -0,0 +1 @@
+
diff --git a/frontend/src/app/pages/session/profile/profile.component.html b/frontend/src/app/pages/session/profile/profile.component.html
new file mode 100644
index 0000000..ff14145
--- /dev/null
+++ b/frontend/src/app/pages/session/profile/profile.component.html
@@ -0,0 +1,167 @@
+
+
diff --git a/frontend/src/app/pages/session/profile/profile.component.spec.ts b/frontend/src/app/pages/session/profile/profile.component.spec.ts
new file mode 100644
index 0000000..658617a
--- /dev/null
+++ b/frontend/src/app/pages/session/profile/profile.component.spec.ts
@@ -0,0 +1,21 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ProfileComponent } from './profile.component';
+
+describe('ProfileComponent', () => {
+ let component: ProfileComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [ProfileComponent]
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(ProfileComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/frontend/src/app/pages/session/profile/profile.component.ts b/frontend/src/app/pages/session/profile/profile.component.ts
new file mode 100644
index 0000000..ae00c5c
--- /dev/null
+++ b/frontend/src/app/pages/session/profile/profile.component.ts
@@ -0,0 +1,61 @@
+import { Component, OnInit } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { InputTextModule } from 'primeng/inputtext';
+import { DividerModule } from 'primeng/divider';
+import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { SessionService } from '../../../services/commons/session.service';
+import { CardModule } from "primeng/card";
+import { UserProfile } from '../../../models/session.model';
+
+@Component({
+ selector: 'app-profile',
+ templateUrl: './profile.component.html',
+ styleUrl: './profile.component.css',
+ standalone: true,
+ imports: [CommonModule, ReactiveFormsModule, FormsModule, InputTextModule, DividerModule, CardModule]
+})
+export class ProfileComponent implements OnInit {
+ userProfile: UserProfile;
+ profileForm: FormGroup;
+ constructor(private fb: FormBuilder, private sessionService: SessionService){
+ this.userProfile = {};
+ this.profileForm = this.fb.group({
+ username: [{ value: null, disabled: true }],
+ roleName: [{ value: null, disabled: true }],
+ employeeId: [{ value: null, disabled: true }],
+ joiningDate: [{ value: null, disabled: true }],
+ displayName: [null],
+ department: [{ value: null, disabled: true }],
+ designation: [{ value: null, disabled: true }],
+ name: [{ value: null, disabled: true }],
+ fatherName: [{ value: null, disabled: true }],
+ gender: [{ value: null, disabled: true }],
+ dob: [{ value: null, disabled: true }],
+ contactNo: [{ value: null, disabled: true }],
+ alternateContactNo: [null],
+ emailId: [null]
+ });
+ }
+
+ ngOnInit(): void {
+ const userDetails = this.sessionService.getItem('userDetails');
+ const companies = this.sessionService.getItem("companies") ? JSON.parse(this.sessionService.getItem("companies")) : '';
+ this.userProfile = userDetails ? JSON.parse(userDetails) : {};
+ //One liner approach commented out for later user
+ //this.profileForm.patchValue(this.userProfile as Partial);
+ this.profileForm.patchValue({
+ ...this.userProfile,
+ roleName: companies ? companies[0].branches[0].roles[0].groupName : '',
+ joiningdate: this.userProfile.joiningDate
+ ? new Date(this.userProfile.joiningDate)
+ : null,
+ dob: this.userProfile.dob
+ ? new Date(this.userProfile.dob)
+ : null
+ });
+ }
+
+ onSave() : void{
+
+ }
+}
diff --git a/frontend/src/app/services/account/company/company.service.ts b/frontend/src/app/services/account/company/company.service.ts
new file mode 100644
index 0000000..24b9d6c
--- /dev/null
+++ b/frontend/src/app/services/account/company/company.service.ts
@@ -0,0 +1,159 @@
+import { Request } from './../../../models/request.model';
+import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
+import { environment } from '../../../../environments/environment';
+import { HttpService } from '../../http.service';
+
+import { SubsidiaryDTO, DepartmentDTO, DesignationDTO, EmployeeDTO } from '../../../models/account.model';
+import { ResponseDto } from '../../../models/response.dto';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class CompanyService {
+
+ constructor(private http: HttpService) {}
+
+ // Subsidiaries
+ getAllSubsidiaries(): Observable {
+ return this.http.get(`${environment.accountService}/company/subsidiaries`).pipe(
+ map((response: ResponseDto) => response.data as SubsidiaryDTO[])
+ );
+ }
+
+ saveSubsidiary(subsidiary: SubsidiaryDTO): Observable {
+ const requestPayload: Request = {
+ data: subsidiary,
+ compressed: true,
+ target: 'cygnus.models.account.Subsidiary'
+ };
+ return this.http.post(`${environment.accountService}/company/subsidiaries`, requestPayload).pipe(
+ map((response: ResponseDto) => response.data as SubsidiaryDTO)
+ );
+ }
+
+ activateDeactivateSubsidiary(subsidiaryId: string, active: boolean): Observable {
+ const subsidiary: SubsidiaryDTO = ({
+ id: subsidiaryId,
+ active: active
+ });
+ const requestPayload: Request = {
+ scopes: [active ? "ACTIVATE" : "DEACTIVATE"],
+ data: subsidiary,
+ compressed: true,
+ target: 'cygnus.models.account.Subsidiary'
+ };
+ return this.http.post(`${environment.accountService}/company/subsidiaries`, requestPayload).pipe(
+ map((response: ResponseDto) => response.data as SubsidiaryDTO)
+ );
+ }
+
+ // Departments
+ getAllDepartments(): Observable {
+ return this.http.get(`${environment.accountService}/company/departments`).pipe(
+ map((response: ResponseDto) => response.data as DepartmentDTO[])
+ );
+ }
+
+ saveDepartment(department: DepartmentDTO): Observable {
+ const requestPayload: Request = {
+ data: department,
+ compressed: true,
+ target: 'cygnus.models.account.Department'
+ };
+ return this.http.post(`${environment.accountService}/company/departments`, requestPayload).pipe(
+ map((response: ResponseDto) => response.data as DepartmentDTO)
+ );
+ }
+
+ activateDeactivateDepartment(departmentId: string, active: boolean): Observable {
+ const department: DepartmentDTO = ({
+ id: departmentId,
+ active: active
+ } as DepartmentDTO);
+ const requestPayload: Request = {
+ scopes: [active ? "ACTIVATE" : "DEACTIVATE"],
+ data: department,
+ compressed: true,
+ target: 'cygnus.models.account.Department'
+ };
+ return this.http.post(`${environment.accountService}/company/departments`, requestPayload).pipe(
+ map((response: ResponseDto) => response.data as DepartmentDTO)
+ );
+ }
+
+ // Designations
+ getAllDesignations(): Observable {
+ return this.http.get(`${environment.accountService}/company/designations`).pipe(
+ map((response: ResponseDto) => response.data as DesignationDTO[])
+ );
+ }
+
+ saveDesignation(designation: DesignationDTO): Observable {
+ const requestPayload: Request = {
+ data: designation,
+ compressed: true,
+ target: 'cygnus.models.account.Designation'
+ };
+ return this.http.post(`${environment.accountService}/company/designations`, requestPayload).pipe(
+ map((response: ResponseDto) => response.data as DesignationDTO)
+ );
+ }
+
+ activateDeactivateDesignation(designationId: string, active: boolean): Observable {
+ const designation: DesignationDTO = ({
+ id: designationId,
+ active: active
+ } as DesignationDTO);
+ const requestPayload: Request = {
+ scopes: [active ? "ACTIVATE" : "DEACTIVATE"],
+ data: designation,
+ compressed: true,
+ target: 'cygnus.models.account.Designation'
+ };
+ return this.http.post(`${environment.accountService}/company/designations`, requestPayload).pipe(
+ map((response: ResponseDto) => response.data as DesignationDTO)
+ );
+ }
+
+ // Employees
+ getAllEmployees(): Observable {
+ return this.http.get(`${environment.accountService}/company/employees`).pipe(
+ map((response: ResponseDto) => response.data as EmployeeDTO[])
+ );
+ }
+
+ saveEmployee(employee: EmployeeDTO): Observable {
+ const requestPayload: Request = {
+ data: employee,
+ compressed: true,
+ target: 'cygnus.models.account.Employee'
+ };
+ return this.http.post(`${environment.accountService}/company/employees`, requestPayload).pipe(
+ map((response: ResponseDto) => response.data as EmployeeDTO)
+ );
+ }
+
+ activateDeactivateEmployee(employeeId: string, active: boolean): Observable {
+ const employee: EmployeeDTO = ({
+ id: employeeId,
+ active: active
+ } as EmployeeDTO);
+ const requestPayload: Request = {
+ scopes: [active ? "ACTIVATE" : "DEACTIVATE"],
+ data: employee,
+ compressed: true,
+ target: 'cygnus.models.account.Employee'
+ };
+ return this.http.post(`${environment.accountService}/company/employees`, requestPayload).pipe(
+ map((response: ResponseDto) => response.data as EmployeeDTO)
+ );
+ }
+
+ searchEmployees(payload: Request): Observable {
+ return this.http.post(`${environment.accountService}/company/employees/search`, payload).pipe(
+ map((response: ResponseDto) => response.data as any[])
+ );
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/app/services/account/user/user.service.ts b/frontend/src/app/services/account/user/user.service.ts
new file mode 100644
index 0000000..d38b554
--- /dev/null
+++ b/frontend/src/app/services/account/user/user.service.ts
@@ -0,0 +1,49 @@
+import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
+import { environment } from '../../../../environments/environment';
+import { HttpService } from '../../http.service';
+import { UserDTO } from '../../../models/user.model';
+import { ResponseDto } from '../../../models/response.dto';
+import { Request } from '../../../models/request.model';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class UserService {
+
+ constructor(private http: HttpService) { }
+
+ getAllUsers(): Observable {
+ return this.http.get(`${environment.userService}/users`).pipe(
+ map((response: ResponseDto) => response.data as UserDTO[])
+ );
+ }
+
+ saveUser(user: UserDTO): Observable {
+ const requestPayload: Request = {
+ data: user,
+ compressed: true,
+ target: 'cygnus.models.account.User'
+ };
+ return this.http.post(`${environment.userService}/users`, requestPayload).pipe(
+ map((response: ResponseDto) => response.data as UserDTO)
+ );
+ }
+
+ activateDeactivateUser(userId: string, active: boolean): Observable {
+ const user: UserDTO = {
+ id: userId,
+ active: active
+ };
+ const requestPayload: Request = {
+ scopes: [active ? "ACTIVATE" : "DEACTIVATE"],
+ data: user,
+ compressed: true,
+ target: 'cygnus.models.account.User'
+ };
+ return this.http.post(`${environment.userService}/users`, requestPayload).pipe(
+ map((response: ResponseDto) => response.data as UserDTO)
+ );
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/app/services/commons/session.service.ts b/frontend/src/app/services/commons/session.service.ts
new file mode 100644
index 0000000..7a4ef2b
--- /dev/null
+++ b/frontend/src/app/services/commons/session.service.ts
@@ -0,0 +1,51 @@
+import { Injectable } from '@angular/core';
+import { Router } from '@angular/router';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class SessionService {
+
+ constructor(private router: Router) { }
+
+ setItem(key: string, value: T): void {
+ try {
+ sessionStorage.setItem(key, JSON.stringify(value));
+ } catch (e) {
+ console.error(`Error saving ${key} to sessionStorage`, e);
+ }
+ }
+
+ getDetails(key: string): T | null {
+ const details = this.getItem('details');
+ if (details === null) return null;
+ const value = details[key];
+ if (value === null) return null;
+ else return value;
+ }
+
+ getItem(key: string): any | null {
+ const json = sessionStorage.getItem(key);
+ if (json === null) return null;
+
+ try {
+ return JSON.parse(json);
+ } catch (e) {
+ console.warn(`Error parsing JSON for key ${key}`, e);
+ return null;
+ }
+ }
+
+ removeItem(key: string): void {
+ sessionStorage.removeItem(key);
+ }
+
+ clear(): void {
+ sessionStorage.clear();
+ }
+
+ logout(): void {
+ sessionStorage.clear();
+ this.router.navigate(['/']);
+ }
+}
diff --git a/frontend/src/app/services/http.service.ts b/frontend/src/app/services/http.service.ts
new file mode 100644
index 0000000..5b4f125
--- /dev/null
+++ b/frontend/src/app/services/http.service.ts
@@ -0,0 +1,56 @@
+import { Injectable } from '@angular/core';
+import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
+import { Observable, throwError } from 'rxjs';
+import { catchError } from 'rxjs/operators';
+import { ResponseDto } from '../models/response.dto';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class HttpService {
+
+ constructor(private http: HttpClient) {
+ }
+
+ get(url: string, headers?: HttpHeaders): Observable {
+ const options = {
+ headers,
+ withCredentials: true
+ };
+ return this.http.get(url, options).pipe(
+ catchError(this.handleError)
+ );
+ }
+
+ post(url: string, body: any, headers?: HttpHeaders): Observable {
+ const options = {
+ headers,
+ withCredentials: true
+ };
+ return this.http.post(url, body, options).pipe(
+ catchError(this.handleError)
+ );
+ }
+
+ put(url: string, body: any, headers?: HttpHeaders): Observable {
+ return this.http.put(url, body, { headers }).pipe(
+ catchError(this.handleError)
+ );
+ }
+
+ delete(url: string, headers?: HttpHeaders): Observable {
+ return this.http.delete(url, { headers }).pipe(
+ catchError(this.handleError)
+ );
+ }
+
+ private handleError(error: HttpErrorResponse) {
+ return throwError(() => error.error);
+ }
+
+ setHeaders(additionalHeaders: { [key: string]: string }): HttpHeaders {
+ return new HttpHeaders({
+ ...additionalHeaders
+ });
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/app/services/masters/master.service.ts b/frontend/src/app/services/masters/master.service.ts
new file mode 100644
index 0000000..f9f283f
--- /dev/null
+++ b/frontend/src/app/services/masters/master.service.ts
@@ -0,0 +1,24 @@
+import { Injectable } from '@angular/core';
+import { HttpService } from '../http.service';
+import { CityDTO } from '../../models/masters/masters';
+import { map, Observable } from 'rxjs';
+import { environment } from '../../../environments/environment';
+import { ResponseDto } from '../../models/response.dto';
+import { Request } from '../../models/request.model';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class MasterService {
+ private readonly baseUrl = '/master';
+
+ constructor(private http: HttpService) {}
+
+ searchCityStates(payload: Request): Observable {
+ return this.http.post(`${environment.masterService}/cities-states/search`, payload).pipe(
+ map((response: ResponseDto) => response.data as CityDTO[])
+ );
+ }
+
+
+}
diff --git a/frontend/src/app/services/utilities/encryption.service.ts b/frontend/src/app/services/utilities/encryption.service.ts
new file mode 100644
index 0000000..ecae54e
--- /dev/null
+++ b/frontend/src/app/services/utilities/encryption.service.ts
@@ -0,0 +1,61 @@
+import { Injectable } from '@angular/core';
+import { environment } from '../../../environments/environment';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class EncryptionService {
+
+ constructor() {}
+
+ async encrypt(payload: any): Promise {
+ const encoder = new TextEncoder();
+ const iv = crypto.getRandomValues(new Uint8Array(12));
+ const jsonString = JSON.stringify(payload);
+
+ const keyBytes = Uint8Array.from(atob(environment.encryptionKey), c => c.charCodeAt(0));
+ const cryptoKey = await crypto.subtle.importKey(
+ "raw",
+ keyBytes,
+ { name: "AES-GCM" },
+ false,
+ ["encrypt"]
+ );
+
+ const encrypted = await crypto.subtle.encrypt(
+ { name: "AES-GCM", iv },
+ cryptoKey,
+ encoder.encode(jsonString)
+ );
+
+ const combined = new Uint8Array(iv.length + encrypted.byteLength);
+ combined.set(iv, 0);
+ combined.set(new Uint8Array(encrypted), iv.length);
+
+ return btoa(String.fromCharCode(...combined));
+ }
+
+ async decrypt(base64Cipher: string): Promise {
+ const data = Uint8Array.from(atob(base64Cipher), c => c.charCodeAt(0));
+ const iv = data.slice(0, 12);
+ const ciphertext = data.slice(12);
+
+ const keyBytes = Uint8Array.from(atob(environment.encryptionKey), c => c.charCodeAt(0));
+ const cryptoKey = await crypto.subtle.importKey(
+ "raw",
+ keyBytes,
+ { name: "AES-GCM" },
+ false,
+ ["decrypt"]
+ );
+
+ const decrypted = await crypto.subtle.decrypt(
+ { name: "AES-GCM", iv },
+ cryptoKey,
+ ciphertext
+ );
+
+ const decoder = new TextDecoder();
+ return JSON.parse(decoder.decode(decrypted));
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/app/services/utilities/rsa.service.ts b/frontend/src/app/services/utilities/rsa.service.ts
new file mode 100644
index 0000000..a88ff21
--- /dev/null
+++ b/frontend/src/app/services/utilities/rsa.service.ts
@@ -0,0 +1,37 @@
+import { Injectable } from '@angular/core';
+import { environment } from '../../../environments/environment';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class RsaService {
+ constructor() {}
+
+ private async importPublicKey(pemKey: string): Promise {
+ const pem = pemKey
+ .replace('-----BEGIN PUBLIC KEY-----', '')
+ .replace('-----END PUBLIC KEY-----', '')
+ .replace(/\s+/g, '');
+ const binaryDer = Uint8Array.from(atob(pem), c => c.charCodeAt(0));
+
+ return crypto.subtle.importKey(
+ 'spki',
+ binaryDer.buffer,
+ {
+ name: 'RSA-OAEP',
+ hash: 'SHA-256',
+ },
+ false,
+ ['encrypt']
+ );
+ }
+
+ async encrypt(plaintext: string): Promise {
+ const key = await this.importPublicKey(environment.rsaPublicKey);
+ const encoded = new TextEncoder().encode(plaintext);
+
+ const encrypted = await crypto.subtle.encrypt({ name: 'RSA-OAEP' }, key, encoded);
+
+ return btoa(String.fromCharCode(...new Uint8Array(encrypted)));
+ }
+}
diff --git a/frontend/src/app/services/utilities/validation.service.ts b/frontend/src/app/services/utilities/validation.service.ts
new file mode 100644
index 0000000..b8dea27
--- /dev/null
+++ b/frontend/src/app/services/utilities/validation.service.ts
@@ -0,0 +1,161 @@
+import { Injectable } from '@angular/core';
+import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class ValidationService {
+
+ // Regex patterns
+ static readonly PATTERNS = {
+ EMAIL: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
+ INDIAN_MOBILE: /^[6-9]\d{9}$/,
+ PINCODE: /^\d{6}$/,
+ PAN: /^[A-Z]{5}[0-9]{4}[A-Z]{1}$/,
+ CIN: /^[UL][0-9]{5}[A-Z]{2}[0-9]{4}[A-Z]{3}[0-9]{6}$/,
+ MSME: /^UDYAM-[A-Z]{2}-\d{7}$/,
+ ALPHANUMERIC: /^[a-zA-Z0-9]+$/,
+ ALPHABET_WITH_SPACES: /^[a-zA-Z0-9\s.&'-]+$/,
+ CODE: /^[A-Z0-9]{2,10}$/
+ };
+
+ // Error messages
+ static readonly ERROR_MESSAGES = {
+ REQUIRED: 'This field is required',
+ EMAIL_INVALID: 'Please enter a valid email address',
+ MOBILE_INVALID: 'Please enter a valid 10-digit mobile number starting with 6-9',
+ PINCODE_INVALID: 'Please enter a valid 6-digit pincode',
+ PAN_INVALID: 'Please enter a valid PAN number (e.g., ABCDE1234F)',
+ CIN_INVALID: 'Please enter a valid CIN number',
+ MSME_INVALID: 'Please enter a valid MSME number (e.g., UDYAM-XX-XXXXXXX)',
+ CODE_INVALID: 'Code must be 2-10 alphanumeric characters',
+ NAME_INVALID: 'Name must be at least 2 characters and contain only letters, numbers, spaces, dots, ampersands, apostrophes, and hyphens',
+ CONTACT_PERSON_INVALID: 'Contact person name must be at least 2 characters',
+ MIN_LENGTH: (min: number) => `Minimum ${min} characters required`,
+ MAX_LENGTH: (max: number) => `Maximum ${max} characters allowed`
+ };
+
+ // Custom validators
+ static emailValidator(): ValidatorFn {
+ return (control: AbstractControl): ValidationErrors | null => {
+ if (!control.value) return null;
+ const isValid = this.PATTERNS.EMAIL.test(control.value);
+ return isValid ? null : { invalidEmail: true };
+ };
+ }
+
+ static mobileValidator(): ValidatorFn {
+ return (control: AbstractControl): ValidationErrors | null => {
+ if (!control.value) return null;
+ const isValid = this.PATTERNS.INDIAN_MOBILE.test(control.value);
+ return isValid ? null : { invalidMobile: true };
+ };
+ }
+
+ static pincodeValidator(): ValidatorFn {
+ return (control: AbstractControl): ValidationErrors | null => {
+ if (!control.value) return null;
+ const isValid = this.PATTERNS.PINCODE.test(control.value);
+ return isValid ? null : { invalidPincode: true };
+ };
+ }
+
+ static panValidator(): ValidatorFn {
+ return (control: AbstractControl): ValidationErrors | null => {
+ if (!control.value) return null;
+ const isValid = this.PATTERNS.PAN.test(control.value);
+ return isValid ? null : { invalidPan: true };
+ };
+ }
+
+ static cinValidator(): ValidatorFn {
+ return (control: AbstractControl): ValidationErrors | null => {
+ if (!control.value) return null;
+ const isValid = this.PATTERNS.CIN.test(control.value);
+ return isValid ? null : { invalidCin: true };
+ };
+ }
+
+ static msmeValidator(): ValidatorFn {
+ return (control: AbstractControl): ValidationErrors | null => {
+ if (!control.value) return null;
+ const isValid = this.PATTERNS.MSME.test(control.value);
+ return isValid ? null : { invalidMsme: true };
+ };
+ }
+
+ static codeValidator(): ValidatorFn {
+ return (control: AbstractControl): ValidationErrors | null => {
+ if (!control.value) return null;
+ const isValid = this.PATTERNS.CODE.test(control.value);
+ return isValid ? null : { invalidCode: true };
+ };
+ }
+
+ static nameValidator(): ValidatorFn {
+ return (control: AbstractControl): ValidationErrors | null => {
+ if (!control.value) return null;
+ if (control.value.length < 2) return { minLength: { requiredLength: 2, actualLength: control.value.length } };
+ const isValid = this.PATTERNS.ALPHABET_WITH_SPACES.test(control.value);
+ return isValid ? null : { invalidName: true };
+ };
+ }
+
+ static contactPersonValidator(): ValidatorFn {
+ return (control: AbstractControl): ValidationErrors | null => {
+ if (!control.value) return null;
+ return control.value.length >= 2 ? null : { minLength: { requiredLength: 2, actualLength: control.value.length } };
+ };
+ }
+
+ // Get error message for a control
+ static getErrorMessage(control: AbstractControl, fieldName: string): string {
+ if (!control.errors) return '';
+
+ if (control.errors['required']) {
+ return this.ERROR_MESSAGES.REQUIRED;
+ }
+
+ if (control.errors['invalidEmail']) {
+ return this.ERROR_MESSAGES.EMAIL_INVALID;
+ }
+
+ if (control.errors['invalidMobile']) {
+ return this.ERROR_MESSAGES.MOBILE_INVALID;
+ }
+
+ if (control.errors['invalidPincode']) {
+ return this.ERROR_MESSAGES.PINCODE_INVALID;
+ }
+
+ if (control.errors['invalidPan']) {
+ return this.ERROR_MESSAGES.PAN_INVALID;
+ }
+
+ if (control.errors['invalidCin']) {
+ return this.ERROR_MESSAGES.CIN_INVALID;
+ }
+
+ if (control.errors['invalidMsme']) {
+ return this.ERROR_MESSAGES.MSME_INVALID;
+ }
+
+ if (control.errors['invalidCode']) {
+ return this.ERROR_MESSAGES.CODE_INVALID;
+ }
+
+ if (control.errors['invalidName']) {
+ return this.ERROR_MESSAGES.NAME_INVALID;
+ }
+
+ if (control.errors['minlength']) {
+ return this.ERROR_MESSAGES.MIN_LENGTH(control.errors['minlength'].requiredLength);
+ }
+
+ if (control.errors['maxlength']) {
+ return this.ERROR_MESSAGES.MAX_LENGTH(control.errors['maxlength'].requiredLength);
+ }
+
+ return 'Invalid input';
+ }
+}
diff --git a/frontend/src/environments/environment.prod.ts b/frontend/src/environments/environment.prod.ts
new file mode 100644
index 0000000..a4206e1
--- /dev/null
+++ b/frontend/src/environments/environment.prod.ts
@@ -0,0 +1,6 @@
+export const environment = {
+ production: true,
+ encryptionKey: btoa('1234567890123456'),
+ baseUrl: 'https://society.nsbinfotech.com/app/api/v1',
+ rsaPublicKey: `MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq3RFV/f6ybsOF2m7NBLPUTMBq9b0frJG1HdIDYmrD9Wr1/aGBxTSJwq8IHFlatNpBF3OlJv9uEOybWMM1vXli4IgsuPPmcTOZsQ/O/9UGyBSL6apevNCw6pC1oa0MVLaN6COMAhDr+ri/PYiPQUcYsjDqghmAghMk99umHGUihz/oY/qgxzO+Q9cqePmjpH5c5RaXGBrOQxKoPlm7Uj6MqAfBhLC360VbcMot4XDoV+VeQXMzH0o6e870jdClsLOq1VsCA27jVvafj+HwaJ15ny9UWWilDuS/X8Sd7v+Rmd+qNezi6ROcglyaisXwKfeTWM8/o7HiUco2fL230+jEQIDAQAB`
+};
\ No newline at end of file
diff --git a/frontend/src/environments/environment.ts b/frontend/src/environments/environment.ts
new file mode 100644
index 0000000..631208a
--- /dev/null
+++ b/frontend/src/environments/environment.ts
@@ -0,0 +1,9 @@
+export const environment = {
+ production: false,
+ encryptionKey: btoa('1234567890123456'),
+ authService: 'http://192.168.0.111:2001/cygnus/app/api/v1',
+ accountService: 'http://localhost:1701/cygnus/app/api/v1/account',
+ userService: 'http://localhost:1701/cygnus/app/api/v1/user',
+ masterService: 'http://localhost:1702/cygnus/app/api/v1/master',
+ rsaPublicKey: `MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq3RFV/f6ybsOF2m7NBLPUTMBq9b0frJG1HdIDYmrD9Wr1/aGBxTSJwq8IHFlatNpBF3OlJv9uEOybWMM1vXli4IgsuPPmcTOZsQ/O/9UGyBSL6apevNCw6pC1oa0MVLaN6COMAhDr+ri/PYiPQUcYsjDqghmAghMk99umHGUihz/oY/qgxzO+Q9cqePmjpH5c5RaXGBrOQxKoPlm7Uj6MqAfBhLC360VbcMot4XDoV+VeQXMzH0o6e870jdClsLOq1VsCA27jVvafj+HwaJ15ny9UWWilDuS/X8Sd7v+Rmd+qNezi6ROcglyaisXwKfeTWM8/o7HiUco2fL230+jEQIDAQAB`
+};
\ No newline at end of file
diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss
index ed3cf92..a82a241 100644
--- a/frontend/src/styles.scss
+++ b/frontend/src/styles.scss
@@ -1,17 +1,240 @@
-/* @import "primeng/resources/themes/aura-light-noir/theme.css"; */
-/* @import "primeng/resources/primeng.min.css"; */
-/* PrimeNG v18+ handles themes differently (often via Tailwind or presets).
- For this prototype, we will rely on default component styles or add a CDN link if needed for quick styling.
- However, PrimeIcons is still valid. */
-/* Quill Editor Styles */
+/* Global Styles from Cygnus-UI */
+@import "primeicons/primeicons.css";
+/* PrimeFlex is loaded via angular.json */
+@import "primeng/resources/themes/aura-light-green/theme.css";
+@import "primeng/resources/primeng.css";
@import "quill/dist/quill.core.css";
@import "quill/dist/quill.snow.css";
-@import "primeicons/primeicons.css";
+@layer primeng, primeng-overrides;
-html, body {
- margin: 0;
- font-family: var(--font-family);
- background-color: var(--surface-ground);
- height: 100%;
+@font-face {
+ font-family: 'Roboto';
+ src: url('/assets/fonts/roboto/Roboto-Thin.ttf') format('truetype');
+ font-weight: 100;
+ font-style: normal;
}
+
+@font-face {
+ font-family: 'Roboto';
+ src: url('/assets/fonts/roboto/Roboto-ThinItalic.ttf') format('truetype');
+ font-weight: 100;
+ font-style: italic;
+}
+
+@font-face {
+ font-family: 'Roboto';
+ src: url('/assets/fonts/roboto/Roboto-Light.ttf') format('truetype');
+ font-weight: 300;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Roboto';
+ src: url('/assets/fonts/roboto/Roboto-LightItalic.ttf') format('truetype');
+ font-weight: 300;
+ font-style: italic;
+}
+
+@font-face {
+ font-family: 'Roboto';
+ src: url('/assets/fonts/roboto/Roboto-Regular.ttf') format('truetype');
+ font-weight: 400;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Roboto';
+ src: url('/assets/fonts/roboto/Roboto-Italic.ttf') format('truetype');
+ font-weight: 400;
+ font-style: italic;
+}
+
+@font-face {
+ font-family: 'Roboto';
+ src: url('/assets/fonts/roboto/Roboto-Medium.ttf') format('truetype');
+ font-weight: 500;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Roboto';
+ src: url('/assets/fonts/roboto/Roboto-MediumItalic.ttf') format('truetype');
+ font-weight: 500;
+ font-style: italic;
+}
+
+@font-face {
+ font-family: 'Roboto';
+ src: url('/assets/fonts/roboto/Roboto-SemiBold.ttf') format('truetype');
+ font-weight: 600;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Roboto';
+ src: url('/assets/fonts/roboto/Roboto-SemiBoldItalic.ttf') format('truetype');
+ font-weight: 600;
+ font-style: italic;
+}
+
+@font-face {
+ font-family: 'Roboto';
+ src: url('/assets/fonts/roboto/Roboto-Bold.ttf') format('truetype');
+ font-weight: 700;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Roboto';
+ src: url('/assets/fonts/roboto/Roboto-BoldItalic.ttf') format('truetype');
+ font-weight: 700;
+ font-style: italic;
+}
+
+@font-face {
+ font-family: 'Roboto';
+ src: url('/assets/fonts/roboto/Roboto-ExtraBold.ttf') format('truetype');
+ font-weight: 800;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Roboto';
+ src: url('/assets/fonts/roboto/Roboto-ExtraBoldItalic.ttf') format('truetype');
+ font-weight: 800;
+ font-style: italic;
+}
+
+@font-face {
+ font-family: 'Roboto';
+ src: url('/assets/fonts/roboto/Roboto-Black.ttf') format('truetype');
+ font-weight: 900;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Roboto';
+ src: url('/assets/fonts/roboto/Roboto-BlackItalic.ttf') format('truetype');
+ font-weight: 900;
+ font-style: italic;
+}
+
+body {
+ font-family: 'Roboto', sans-serif;
+ background-color: rgba(226,232,240,0.4);
+ margin: 0px 2px;
+}
+
+.fs-07{ font-size: 0.7rem; }
+.fs-08{ font-size: 0.8rem; }
+.fs-09{ font-size: 0.9rem; }
+.fs-1{ font-size: 1rem; }
+.fs-11{ font-size: 1.1rem; }
+.fs-12{ font-size: 1.2rem; }
+.fs-13{ font-size: 1.3rem; }
+.fs-14{ font-size: 1.4rem; }
+.fs-15{ font-size: 1.5rem; }
+
+
+/* Overrides */
+.p-message .p-message-text {
+ font-size: 0.8rem;
+}
+.p-message-content {
+ justify-content: center;
+}
+
+.p-float-label label{
+ font-size: 0.9rem;
+ font-weight: normal !important;
+}
+@layer primeng-overrides {
+ /* Compact table styles */
+ .p-datatable .p-datatable-tbody > tr > td {
+ font-size: 0.875rem;
+ padding: 0.4rem 0.5rem !important;
+ }
+
+ .p-datatable .p-datatable-thead > tr > th {
+ font-size: 0.875rem;
+ padding: 0.75rem 0.75rem !important;
+ background-color: rgba(241, 245, 249, 1);
+ }
+
+ .p-autocomplete-item {
+ font-size: 0.875rem;
+ }
+}
+
+.p-fieldset .p-fieldset-legend {
+ /*background: var(--p-orange-500);*/
+ background: rgba(245,115,22,0.9);
+ color: white;
+}
+
+/* Fluid Support for compatibility with Cygnus-UI (PrimeNG v19 syntax) */
+input[fluid], textarea[fluid] {
+ width: 100%;
+}
+
+p-dropdown[fluid] {
+ width: 100%;
+ display: block;
+}
+p-dropdown[fluid] .p-dropdown {
+ width: 100%;
+}
+
+p-autoComplete[fluid] {
+ width: 100%;
+ display: block;
+}
+p-autoComplete[fluid] .p-autocomplete {
+ width: 100%;
+}
+p-autoComplete[fluid] .p-autocomplete-multiple-container {
+ width: 100%;
+}
+
+p-calendar[fluid] {
+ width: 100%;
+ display: block;
+}
+p-calendar[fluid] .p-calendar {
+ width: 100%;
+}
+p-calendar[fluid] .p-inputtext {
+ width: 100%;
+}
+
+p-inputNumber[fluid] {
+ width: 100%;
+ display: block;
+}
+p-inputNumber[fluid] .p-inputnumber {
+ width: 100%;
+}
+p-inputNumber[fluid] .p-inputtext {
+ width: 100%;
+}
+
+p-password[fluid] {
+ width: 100%;
+ display: block;
+}
+p-password[fluid] .p-password {
+ width: 100%;
+}
+p-password[fluid] .p-inputtext {
+ width: 100%;
+}
+
+p-multiSelect[fluid] {
+ width: 100%;
+ display: block;
+}
+p-multiSelect[fluid] .p-multiselect {
+ width: 100%;
+}
+
diff --git a/ocr-session-api-dev.env b/ocr-session-api-dev.env
new file mode 100644
index 0000000..f998f24
--- /dev/null
+++ b/ocr-session-api-dev.env
@@ -0,0 +1,15 @@
+SERVER_PORT=1700
+DB_URL=jdbc:postgresql://postgres-db:5432/ocr_subscribers
+DB_USER=postgres
+DB_PASSWORD="M@tr!x#149@dm!N"
+REDIS_HOST=172.18.0.5
+REDIS_PORT=6379
+REDIS_PASSWORD=R3di5@2025
+MAX_REQUEST_SIZE=40MB
+MULTIPART_REQUEST_SIZE=20MB
+MULTIPART_FILE_SIZE=5MB
+MINIO_URL=http://103.83.146.41:9800
+MINIO_KEY=admin
+MINIO_SECRET=admin@123
+MINIO_BUCKET=cygnus-dev
+MINIO_ENABLED=true
\ No newline at end of file