diff --git a/backend/uploads/Invoice For Jan-Feb-2026.pdf b/backend/uploads/Invoice For Jan-Feb-2026.pdf new file mode 100644 index 0000000..6c73b97 Binary files /dev/null and b/backend/uploads/Invoice For Jan-Feb-2026.pdf differ diff --git a/backend/uploads/Invoice For Jan-Feb-2026.pdf.jpg b/backend/uploads/Invoice For Jan-Feb-2026.pdf.jpg new file mode 100644 index 0000000..a1b8862 Binary files /dev/null and b/backend/uploads/Invoice For Jan-Feb-2026.pdf.jpg differ diff --git a/frontend/proxy.conf.json b/frontend/proxy.conf.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/frontend/proxy.conf.json @@ -0,0 +1 @@ +{} diff --git a/frontend/public/assets/images/logo.png b/frontend/public/assets/images/logo.png new file mode 100644 index 0000000..642e236 Binary files /dev/null and b/frontend/public/assets/images/logo.png differ diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index 1a6c481..49e8453 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -1,6 +1,6 @@ import { Routes } from '@angular/router'; import { LoginComponent } from './login/login.component'; -import { AdminLayoutComponent } from './admin-layout/admin-layout.component'; +import { DashboardComponent } from './pages/dashboard/dashboard.component'; import { OcrComponent } from './ocr/ocr.component'; import { MailboxComponent } from './mailbox/mailbox.component'; @@ -17,7 +17,7 @@ export const routes: Routes = [ { path: '', component: LoginComponent }, { path: 'authorize', component: AuthorizeComponent, canActivate: [AuthorizeGuard]}, { path: 'user', - component: AdminLayoutComponent, + component: DashboardComponent, canActivate: [AuthorizeGuard], canActivateChild: [AuthorizeGuard], children: [ @@ -27,7 +27,7 @@ export const routes: Routes = [ ] }, { path: 'account', - component: AdminLayoutComponent, + component: DashboardComponent, canActivate: [AuthorizeGuard], canActivateChild: [AuthorizeGuard], children: [ 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..8b07f83 --- /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 index 926a6fa..e315341 100644 --- a/frontend/src/app/fragments/menu/menu.component.ts +++ b/frontend/src/app/fragments/menu/menu.component.ts @@ -1,23 +1,22 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, Input } from '@angular/core'; import { Router } from '@angular/router'; import { MenuItem } from 'primeng/api'; -import { MenubarModule } from 'primeng/menubar'; +import { Menubar } 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 { Menu } 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], + imports: [Menubar, AvatarModule, ButtonModule, Menu, InputTextModule, TooltipModule], templateUrl: './menu.component.html' }) export class MenuComponent { @@ -106,6 +105,6 @@ export class MenuComponent { } gotToDashboard() { - this.router.navigate(['/user']); + this.router.navigate(['/account']); } -} \ No newline at end of file +} diff --git a/frontend/src/app/interceptors/auth.interceptor.ts b/frontend/src/app/interceptors/auth.interceptor.ts index cd5148d..1f9a086 100644 --- a/frontend/src/app/interceptors/auth.interceptor.ts +++ b/frontend/src/app/interceptors/auth.interceptor.ts @@ -24,15 +24,12 @@ export const AuthInterceptor: HttpInterceptorFn = ( /* ----------------------------------------- * 1️⃣ Always attach Authorization header * ----------------------------------------- */ - // OCR uses sessionStorage for token now (migrated previously) - const token = sessionStorage.getItem('token'); + const token = sessionService.getItem('token'); let reqHeaders = req.headers; - // Only set application/json if it's not FormData - // FormData requests need the browser to set Content-Type with boundary if (!(req.body instanceof FormData)) { - reqHeaders = reqHeaders.set('Content-Type', 'application/json'); + reqHeaders = reqHeaders.set('Content-Type', 'application/json'); } if (token) { @@ -65,9 +62,8 @@ export const AuthInterceptor: HttpInterceptorFn = ( 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)), + tap(event => handleResponse(event, sessionService, encryptionService)), catchError((error: HttpErrorResponse) => throwError(() => error) ) @@ -84,7 +80,7 @@ export const AuthInterceptor: HttpInterceptorFn = ( }); return next(modifiedReq).pipe( - tap((event: any) => handleResponse(event, sessionService, encryptionService)), + tap(event => handleResponse(event, sessionService, encryptionService)), catchError((error: HttpErrorResponse) => throwError(() => error) ) @@ -101,45 +97,33 @@ function handleResponse( return; } - // Handle Authenticate Response - if (event.url?.endsWith('/authenticate')) { + if (event.url?.endsWith('/3z4mkell5g5aset/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); - } + sessionService.setItem('token', response?.data?.token); + sessionService.setItem('companies', response?.data?.companies); } - // Handle Authorize Response - if (event.url?.endsWith('/authorize')) { + if (event.url?.endsWith('/3z4mkell5g5aset/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); - } + sessionService.clear(); + sessionService.setItem('token', response?.data?. token); + sessionService.setItem('refreshToken', response?.data?.refreshToken); + sessionService.setItem('userDetails', JSON.stringify(response?.data?.userDetails)); + encryptionService.encrypt(response?.data?.data) + .then(encryptedNav => { + sessionService.setItem('nav', encryptedNav); + }); + 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 + 'userDetails', + JSON.stringify(response.data.userDetails) ); } + + sessionService.setItem( + 'companies', + JSON.stringify(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..e83b32f --- /dev/null +++ b/frontend/src/app/login/login.component.css @@ -0,0 +1,43 @@ +::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..a1abfb7 --- /dev/null +++ b/frontend/src/app/login/login.component.html @@ -0,0 +1,50 @@ +
+
+
+ + +
+ + + User Login + +
+
+
+ + + + + + + + + +
+
+ + + + + + + + + +
+
+ +
+
+ +
+
+
+
+
diff --git a/frontend/src/app/login/login.component.ts b/frontend/src/app/login/login.component.ts index 8051b2c..f090337 100644 --- a/frontend/src/app/login/login.component.ts +++ b/frontend/src/app/login/login.component.ts @@ -1,76 +1,58 @@ -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 { Component } from '@angular/core'; +import { ButtonModule } from 'primeng/button';; import { InputTextModule } from 'primeng/inputtext'; -import { ButtonModule } from 'primeng/button'; -import { ToastModule } from 'primeng/toast'; +import { CardModule } from 'primeng/card'; +import { InputGroupModule } from 'primeng/inputgroup'; +import { InputGroupAddonModule } from 'primeng/inputgroupaddon'; +import { PasswordModule } from 'primeng/password'; +import { FloatLabelModule } from 'primeng/floatlabel'; +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 { StyleClassModule } from 'primeng/styleclass'; +import { Router } from '@angular/router'; +import { ResponseDto } from '../models/response.dto'; +import { BadgeModule } from "primeng/badge"; +import { environment } from '../../environments/environment'; @Component({ selector: 'app-login', - standalone: true, - imports: [ - CommonModule, - FormsModule, - 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; } - `] + templateUrl: './login.component.html', + styleUrls: ['./login.component.css'], + imports: [CommonModule, FormsModule, CardModule, ButtonModule, InputTextModule, InputGroupModule, InputGroupAddonModule, PasswordModule, + FloatLabelModule, ReactiveFormsModule, KeyFilterModule, MessageModule, StyleClassModule, 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' + }; + this.http.post(`${environment.authService}/3z4mkell5g5aset/authenticate`, requestPayload).subscribe({ + next: (response) => { + this.router.navigate(['/authorize']); + }, + error: (err) => { + this.message = err.message; + } + }); + } + } } diff --git a/frontend/src/app/login/login.html b/frontend/src/app/login/login.html deleted file mode 100644 index 147cfc4..0000000 --- a/frontend/src/app/login/login.html +++ /dev/null @@ -1 +0,0 @@ -

login works!

diff --git a/frontend/src/app/login/login.scss b/frontend/src/app/login/login.scss deleted file mode 100644 index e69de29..0000000 diff --git a/frontend/src/app/login/login.ts b/frontend/src/app/login/login.ts deleted file mode 100644 index 7888f76..0000000 --- a/frontend/src/app/login/login.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Component } from '@angular/core'; - -@Component({ - selector: 'app-login', - imports: [], - templateUrl: './login.html', - styleUrl: './login.scss', -}) -export class Login { - -} diff --git a/frontend/src/app/models/account.model.ts b/frontend/src/app/models/account.model.ts index 5ddd8fd..ac11b8d 100644 --- a/frontend/src/app/models/account.model.ts +++ b/frontend/src/app/models/account.model.ts @@ -43,6 +43,28 @@ export interface DepartmentDTO { active: boolean; } +export interface CompanyVerifierModel { + pkId?: number; + id?: string; + companyId: string; + employeeId?: string; + verifierCode: string; + verifierType: string; + fullName: string; + mobileNo: string; + emailId: string; + allocationDate?: string; + appActive: boolean; + photoMandatory: boolean; + createdAt?: string; + createdBy?: number; + createdUser?: string; + updatedAt?: string; + updatedBy?: number; + updatedUser?: string; + active: boolean; +} + export interface DesignationDTO { id: string; companyId: string; 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/ola.model.ts b/frontend/src/app/models/ola.model.ts new file mode 100644 index 0000000..a030247 --- /dev/null +++ b/frontend/src/app/models/ola.model.ts @@ -0,0 +1,7 @@ +export interface LocationDTO { + place_id?: string; + name?: string; + formatted_address?: string; + lat?: number; + lng?: number; +} 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/tools.model.ts b/frontend/src/app/models/tools.model.ts new file mode 100644 index 0000000..8a70ee4 --- /dev/null +++ b/frontend/src/app/models/tools.model.ts @@ -0,0 +1,52 @@ +export interface AreaDTO { + id?: string; + companyId?: string; + areaName?: string; + groupAVerifierId?: string; + groupBVerifierId?: string; + groupCVerifierId?: string; + groupAVerifierName?: string; + groupBVerifierName?: string; + groupCVerifierName?: string; + createdAt?: Date; + createdBy?: string; + createdUser?: string; + updatedAt?: Date; + updatedBy?: string; + updatedUser?: string; + active?: boolean; +} + +export interface LocalityDTO { + id?: string; + companyId?: string; + areaId?: string; + stateCityId?: string; + localityTypeId?: string; + olaPlaceId?: string; + olaLocality?: string; + localityName?: string; + adminArea?: string; + latitude?: number; + longitude?: number; + createdAt?: Date; + createdBy?: string; + createdUser?: string; + updatedAt?: Date; + updatedBy?: string; + updatedUser?: string; + active?: boolean; + stateName?: string; + pincode?: string; + areaName?: string; + localityTypeName?: string; + masterCityName?: string; + masterStateName?: string; +} + +export interface LocalityTypeDTO { + id?: string; + type?: string; + riskdetail?: string; + createdAt?: Date; +} diff --git a/frontend/src/app/models/user.model.ts b/frontend/src/app/models/user.model.ts new file mode 100644 index 0000000..4ccacc8 --- /dev/null +++ b/frontend/src/app/models/user.model.ts @@ -0,0 +1,30 @@ +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; + defaultRole?: boolean; + updatedAt?: string; + updatedUser?: string; + active?: boolean; +} \ No newline at end of file diff --git a/frontend/src/app/ocr/ocr.component.ts b/frontend/src/app/ocr/ocr.component.ts index a1cbe68..452d4ac 100644 --- a/frontend/src/app/ocr/ocr.component.ts +++ b/frontend/src/app/ocr/ocr.component.ts @@ -9,6 +9,11 @@ import { FileUploadModule } from 'primeng/fileupload'; import { ProgressBarModule } from 'primeng/progressbar'; import { TextareaModule } from 'primeng/textarea'; import { ToastModule } from 'primeng/toast'; +import { ButtonModule } from 'primeng/button'; +import { TableModule } from 'primeng/table'; +import { CardModule } from 'primeng/card'; +import { RadioButtonModule } from 'primeng/radiobutton'; +import { InputTextModule } from 'primeng/inputtext'; @Component({ selector: 'app-ocr', @@ -19,16 +24,18 @@ import { ToastModule } from 'primeng/toast'; FileUploadModule, ProgressBarModule, TextareaModule, - ToastModule + ToastModule, + ButtonModule, + TableModule, + CardModule, + RadioButtonModule, + InputTextModule ], providers: [MessageService], template: `

OCR Extraction

- +
-
-

Extracted Text Result:

- +
+
+

Extracted Text Result:

+ + +
+
+ +
+ + +
+
+ + +
+
+ + + +
+
+ +
+
+

AI Analysis Result:

+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + + + + Description + Qty + Price + Total + + + + + + + + + + + {{item.description}} + + + + + + + + + + {{item.quantity}} + + + + + + + + + + {{item.unit_price}} + + + + + + + + + + {{item.total}} + + + + + + +
@@ -65,16 +177,27 @@ import { ToastModule } from 'primeng/toast'; export class OcrComponent { extractedText: string | null = null; loading: boolean = false; + + aiLoading: boolean = false; + saveLoading: boolean = false; + aiResult: any = null; + + // Hybrid AI Props + modelType: string = 'text'; + filePath: string | null = null; constructor(private ocrService: OcrService, private messageService: MessageService) {} onUpload(event: any) { this.loading = true; + this.aiResult = null; // Reset AI result on new upload + this.filePath = null; const file = event.files[0]; this.ocrService.extractText(file).subscribe({ next: (res) => { this.extractedText = res.text; + this.filePath = res.file_path; this.loading = false; this.messageService.add({severity:'success', summary:'Success', detail:'Text Extracted Successfully'}); }, @@ -88,5 +211,50 @@ export class OcrComponent { onClear() { this.extractedText = null; + this.aiResult = null; + this.filePath = null; + } + + processWithAI() { + if (!this.extractedText) return; + + this.aiLoading = true; + // Pass text, filePath, and modelType + this.ocrService.extractWithAI(this.extractedText, this.filePath, this.modelType).subscribe({ + next: (res) => { + this.aiResult = res; + this.aiLoading = false; + this.messageService.add({severity:'success', summary:'AI Processing Complete', detail:'Data Extracted'}); + }, + error: (err) => { + console.error(err); + this.aiLoading = false; + this.messageService.add({severity:'error', summary:'AI Error', detail:'Could not process with AI'}); + } + }); + } + + saveDocument() { + if (!this.aiResult || !this.filePath) return; + + this.saveLoading = true; + const payload = { + vendor_name: this.aiResult.vendor_name || 'Unknown Vendor', + file_path: this.filePath, + model_type: this.modelType, + data: this.aiResult + }; + + this.ocrService.saveDocument(payload).subscribe({ + next: (res) => { + this.saveLoading = false; + this.messageService.add({severity:'success', summary:'Saved & Verified', detail:'Document and rules saved'}); + }, + error: (err) => { + console.error(err); + this.saveLoading = false; + this.messageService.add({severity:'error', summary:'Save Error', detail:'Failed to save document'}); + } + }); } } 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.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 index 9842162..814d844 100644 --- a/frontend/src/app/pages/account/company/department/department.component.ts +++ b/frontend/src/app/pages/account/company/department/department.component.ts @@ -1,31 +1,32 @@ 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 { TableModule } from 'primeng/table'; +import { Dialog } from 'primeng/dialog'; +import { Ripple } from 'primeng/ripple'; +import { ButtonModule, Button } from 'primeng/button'; import { ToastModule } from 'primeng/toast'; import { ToolbarModule } from 'primeng/toolbar'; -import { ConfirmDialogModule } from 'primeng/confirmdialog'; +import { ConfirmDialog } from 'primeng/confirmdialog'; import { InputTextModule } from 'primeng/inputtext'; import { TextareaModule } from 'primeng/textarea'; import { CommonModule } from '@angular/common'; -import { FileUploadModule } from 'primeng/fileupload'; +import { FileUpload } from 'primeng/fileupload'; import { SelectModule } from 'primeng/select'; -import { TagModule } from 'primeng/tag'; -import { RadioButtonModule } from 'primeng/radiobutton'; -import { RatingModule } from 'primeng/rating'; -import { SkeletonModule } from 'primeng/skeleton'; +import { Tag } from 'primeng/tag'; +import { RadioButton } from 'primeng/radiobutton'; +import { Rating } from 'primeng/rating'; +import { Skeleton } from 'primeng/skeleton'; import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; -import { InputNumberModule } from 'primeng/inputnumber'; +import { InputNumber } from 'primeng/inputnumber'; import { IconFieldModule } from 'primeng/iconfield'; import { InputIconModule } from 'primeng/inputicon'; +import { Table } from 'primeng/table'; +import { DropdownModule } from 'primeng/dropdown'; import { CompanyService } from '../../../../services/account/company/company.service'; +import { FloatLabelModule } from "primeng/floatlabel"; import { ValidationService } from '../../../../services/utilities/validation.service'; import { TooltipModule } from 'primeng/tooltip'; -import { AutoCompleteModule } from 'primeng/autocomplete'; -import { FloatLabelModule } from 'primeng/floatlabel'; interface Column { field: string; @@ -42,13 +43,7 @@ interface ExportColumn { selector: 'app-department', templateUrl: './department.component.html', standalone: true, - imports: [ - CommonModule, FormsModule, ReactiveFormsModule, - TableModule, DialogModule, ButtonModule, ToastModule, ToolbarModule, ConfirmDialogModule, - InputTextModule, TextareaModule, FileUploadModule, SelectModule, TagModule, - RadioButtonModule, RatingModule, SkeletonModule, InputNumberModule, IconFieldModule, - InputIconModule, TooltipModule, AutoCompleteModule, RippleModule, FloatLabelModule - ], + imports: [TableModule, Dialog, Ripple, SelectModule, ToastModule, ToolbarModule, ConfirmDialog, InputTextModule, TextareaModule, CommonModule, FileUpload, DropdownModule, Tag, RadioButton, Rating, InputTextModule, InputNumber, IconFieldModule, InputIconModule, Button, FloatLabelModule, FormsModule, ReactiveFormsModule, TooltipModule, Skeleton], providers: [MessageService, ConfirmationService], styleUrl: './department.component.css' }) @@ -163,8 +158,15 @@ export class DepartmentComponent implements OnInit{ 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', + rejectButtonProps: { + label: 'No', + severity: 'secondary', + variant: 'text' + }, + acceptButtonProps: { + severity: isActivating ? 'success' : 'danger', + label: 'Yes' + }, accept: () => { this.companyService.activateDeactivateDepartment(department.id, isActivating).subscribe({ next: (updatedDepartment) => { 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.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 index d445f85..cd8ebf0 100644 --- a/frontend/src/app/pages/account/company/desigation/designation.component.ts +++ b/frontend/src/app/pages/account/company/desigation/designation.component.ts @@ -1,31 +1,33 @@ 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 { TableModule } from 'primeng/table'; +import { Dialog } from 'primeng/dialog'; +import { Ripple } from 'primeng/ripple'; +import { ButtonModule, Button } from 'primeng/button'; import { ToastModule } from 'primeng/toast'; import { ToolbarModule } from 'primeng/toolbar'; -import { ConfirmDialogModule } from 'primeng/confirmdialog'; +import { ConfirmDialog } from 'primeng/confirmdialog'; +import { InputTextModule } from 'primeng/inputtext'; import { TextareaModule } from 'primeng/textarea'; import { CommonModule } from '@angular/common'; -import { FileUploadModule } from 'primeng/fileupload'; +import { FileUpload } from 'primeng/fileupload'; import { SelectModule } from 'primeng/select'; -import { TagModule } from 'primeng/tag'; -import { RadioButtonModule } from 'primeng/radiobutton'; -import { RatingModule } from 'primeng/rating'; -import { SkeletonModule } from 'primeng/skeleton'; +import { Tag } from 'primeng/tag'; +import { RadioButton } from 'primeng/radiobutton'; +import { Rating } from 'primeng/rating'; +import { Skeleton } from 'primeng/skeleton'; import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; -import { InputNumberModule } from 'primeng/inputnumber'; +import { InputNumber } from 'primeng/inputnumber'; import { IconFieldModule } from 'primeng/iconfield'; import { InputIconModule } from 'primeng/inputicon'; +import { Table } from 'primeng/table'; +import { DropdownModule } from 'primeng/dropdown'; import { CompanyService } from '../../../../services/account/company/company.service'; +import { FloatLabelModule } from "primeng/floatlabel"; import { ValidationService } from '../../../../services/utilities/validation.service'; import { TooltipModule } from 'primeng/tooltip'; import { CheckboxModule } from 'primeng/checkbox'; -import { AutoCompleteModule } from 'primeng/autocomplete'; -import { FloatLabelModule } from 'primeng/floatlabel'; interface Column { field: string; @@ -42,13 +44,7 @@ interface ExportColumn { selector: 'app-designation', templateUrl: './designation.component.html', standalone: true, - imports: [ - CommonModule, FormsModule, ReactiveFormsModule, - TableModule, DialogModule, ButtonModule, ToastModule, ToolbarModule, ConfirmDialogModule, - TextareaModule, FileUploadModule, SelectModule, TagModule, - RadioButtonModule, RatingModule, SkeletonModule, InputNumberModule, IconFieldModule, InputIconModule, - TooltipModule, AutoCompleteModule, RippleModule, CheckboxModule, FloatLabelModule - ], + imports: [TableModule, Dialog, Ripple, SelectModule, ToastModule, ToolbarModule, ConfirmDialog, InputTextModule, TextareaModule, CommonModule, FileUpload, DropdownModule, Tag, RadioButton, Rating, InputTextModule, InputNumber, IconFieldModule, InputIconModule, Button, FloatLabelModule, FormsModule, ReactiveFormsModule, TooltipModule, Skeleton, CheckboxModule], providers: [MessageService, ConfirmationService], styleUrl: './designation.component.css' }) @@ -175,8 +171,15 @@ export class DesignationComponent implements OnInit{ 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', + rejectButtonProps: { + label: 'No', + severity: 'secondary', + variant: 'text' + }, + acceptButtonProps: { + severity: isActivating ? 'success' : 'danger', + label: 'Yes' + }, accept: () => { this.companyService.activateDeactivateDesignation(designation.id, isActivating).subscribe({ next: (updatedDesignation) => { 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.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 index 9e94371..29e2eae 100644 --- a/frontend/src/app/pages/account/company/employee/employee.component.ts +++ b/frontend/src/app/pages/account/company/employee/employee.component.ts @@ -1,25 +1,30 @@ 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 { TableModule } from 'primeng/table'; +import { Dialog } from 'primeng/dialog'; +import { Ripple } from 'primeng/ripple'; +import { ButtonModule, Button } from 'primeng/button'; import { ToastModule } from 'primeng/toast'; import { ToolbarModule } from 'primeng/toolbar'; -import { ConfirmDialogModule } from 'primeng/confirmdialog'; +import { ConfirmDialog } from 'primeng/confirmdialog'; import { InputTextModule } from 'primeng/inputtext'; import { TextareaModule } from 'primeng/textarea'; import { CommonModule } from '@angular/common'; -import { FileUploadModule } from 'primeng/fileupload'; +import { FileUpload } from 'primeng/fileupload'; import { SelectModule } from 'primeng/select'; -import { TagModule } from 'primeng/tag'; -import { SkeletonModule } from 'primeng/skeleton'; +import { Tag } from 'primeng/tag'; +import { RadioButton } from 'primeng/radiobutton'; +import { Rating } from 'primeng/rating'; +import { Skeleton } from 'primeng/skeleton'; import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; -import { InputNumberModule } from 'primeng/inputnumber'; +import { InputNumber } from 'primeng/inputnumber'; import { IconFieldModule } from 'primeng/iconfield'; import { InputIconModule } from 'primeng/inputicon'; +import { Table } from 'primeng/table'; +import { DropdownModule } from 'primeng/dropdown'; import { CompanyService } from '../../../../services/account/company/company.service'; +import { FloatLabelModule } from "primeng/floatlabel"; import { ValidationService } from '../../../../services/utilities/validation.service'; import { TooltipModule } from 'primeng/tooltip'; import { AutoCompleteModule } from 'primeng/autocomplete'; @@ -28,7 +33,6 @@ 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'; -import { FloatLabelModule } from 'primeng/floatlabel'; interface Column { field: string; @@ -45,13 +49,7 @@ interface ExportColumn { selector: 'app-employee', templateUrl: './employee.component.html', standalone: true, - imports: [ - CommonModule, FormsModule, ReactiveFormsModule, - TableModule, DialogModule, ButtonModule, ToastModule, ToolbarModule, ConfirmDialogModule, - InputTextModule, TextareaModule, FileUploadModule, SelectModule, TagModule, - SkeletonModule, InputNumberModule, IconFieldModule, - InputIconModule, TooltipModule, AutoCompleteModule, RippleModule, CalendarModule, FloatLabelModule - ], + imports: [TableModule, Dialog, Ripple, SelectModule, ToastModule, ToolbarModule, ConfirmDialog, InputTextModule, TextareaModule, CommonModule, FileUpload, DropdownModule, Tag, RadioButton, Rating, InputTextModule, InputNumber, IconFieldModule, InputIconModule, Button, FloatLabelModule, FormsModule, ReactiveFormsModule, TooltipModule, Skeleton, AutoCompleteModule, CalendarModule], providers: [MessageService, ConfirmationService], styleUrl: './employee.component.css' }) @@ -306,8 +304,15 @@ export class EmployeeComponent implements OnInit{ 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', + rejectButtonProps: { + label: 'No', + severity: 'secondary', + variant: 'text' + }, + acceptButtonProps: { + severity: isActivating ? 'success' : 'danger', + label: 'Yes' + }, accept: () => { this.companyService.activateDeactivateEmployee(employee.id, isActivating).subscribe({ next: (updatedEmployee) => { 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 index f3d8a88..4e3e8de 100644 --- a/frontend/src/app/pages/account/company/subsidiary/subsidiary.component.html +++ b/frontend/src/app/pages/account/company/subsidiary/subsidiary.component.html @@ -30,7 +30,7 @@

Manage Subsidiaries

- + 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 index 78b4630..7af403d 100644 --- a/frontend/src/app/pages/account/company/subsidiary/subsidiary.component.ts +++ b/frontend/src/app/pages/account/company/subsidiary/subsidiary.component.ts @@ -1,31 +1,38 @@ +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 { TableModule } from 'primeng/table'; +import { Dialog } from 'primeng/dialog'; +import { Ripple } from 'primeng/ripple'; +import { ButtonModule, Button } from 'primeng/button'; import { ToastModule } from 'primeng/toast'; import { ToolbarModule } from 'primeng/toolbar'; -import { ConfirmDialogModule } from 'primeng/confirmdialog'; +import { ConfirmDialog } from 'primeng/confirmdialog'; import { InputTextModule } from 'primeng/inputtext'; import { TextareaModule } from 'primeng/textarea'; import { CommonModule } from '@angular/common'; +import { FileUpload } from 'primeng/fileupload'; import { SelectModule } from 'primeng/select'; -import { TagModule } from 'primeng/tag'; -import { SkeletonModule } from 'primeng/skeleton'; -import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; +import { Tag } from 'primeng/tag'; +import { RadioButton } from 'primeng/radiobutton'; +import { Rating } from 'primeng/rating'; +import { Skeleton } from 'primeng/skeleton'; +import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; +import { InputNumber } from 'primeng/inputnumber'; import { IconFieldModule } from 'primeng/iconfield'; import { InputIconModule } from 'primeng/inputicon'; +import { Table } from 'primeng/table'; +import { DropdownModule } from 'primeng/dropdown'; import { CompanyService } from '../../../../services/account/company/company.service'; +import { FloatLabelModule } from "primeng/floatlabel"; 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, SearchDTO } from '../../../../models/masters/masters'; +import { CityDTO } from '../../../../models/masters/masters'; import { debounceTime, Subject } from 'rxjs'; import { Request } from '../../../../models/request.model'; -import { FloatLabelModule } from 'primeng/floatlabel'; interface Column { field: string; @@ -38,19 +45,11 @@ interface ExportColumn { dataKey: string; } -import { FormsModule } from '@angular/forms'; - @Component({ selector: 'app-subsidiary', templateUrl: './subsidiary.component.html', standalone: true, - imports: [ - CommonModule, FormsModule, ReactiveFormsModule, - TableModule, DialogModule, ButtonModule, ToastModule, ToolbarModule, ConfirmDialogModule, - InputTextModule, TextareaModule, SelectModule, TagModule, - SkeletonModule, IconFieldModule, - InputIconModule, TooltipModule, AutoCompleteModule, RippleModule, FloatLabelModule - ], + imports: [TableModule, Dialog, Ripple, SelectModule, ToastModule, ToolbarModule, ConfirmDialog, InputTextModule, TextareaModule, CommonModule, FileUpload, DropdownModule, Tag, RadioButton, Rating, InputTextModule, InputNumber, IconFieldModule, InputIconModule, Button, FloatLabelModule, FormsModule, ReactiveFormsModule, TooltipModule, Skeleton, AutoCompleteModule], providers: [MessageService, ConfirmationService], styleUrl: './subsidiary.component.css' }) @@ -117,11 +116,6 @@ export class SubsidiaryComponent implements OnInit{ 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); @@ -237,8 +231,15 @@ export class SubsidiaryComponent implements OnInit{ 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', + rejectButtonProps: { + label: 'No', + severity: 'secondary', + variant: 'text' + }, + acceptButtonProps: { + severity: isActivating ? 'success' : 'danger', + label: 'Yes' + }, accept: () => { this.subsidiaryService.activateDeactivateSubsidiary(subsidiary.id?? '', isActivating).subscribe({ next: (updatedSubsidiary) => { diff --git a/frontend/src/app/pages/account/company/verifier/verifier.component.css b/frontend/src/app/pages/account/company/verifier/verifier.component.css new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/frontend/src/app/pages/account/company/verifier/verifier.component.css @@ -0,0 +1 @@ + diff --git a/frontend/src/app/pages/account/company/verifier/verifier.component.html b/frontend/src/app/pages/account/company/verifier/verifier.component.html new file mode 100644 index 0000000..e1e2b04 --- /dev/null +++ b/frontend/src/app/pages/account/company/verifier/verifier.component.html @@ -0,0 +1,273 @@ +
+ + + + + + + + + + + + + + +
+

Manage Verifiers

+ + + + +
+
+ + + # + +
+ Full Name + +
+ + +
+ Verifier Code + +
+ + +
+ Type + +
+ + +
+ Mobile No + +
+ + +
+ Email + +
+ + +
+ Updated At + +
+ + +
+ Updated By + +
+ + +
+ Status + +
+ + + +
+ + + {{ rowIndex + 1 }} + {{ verifier.fullName }} + {{ verifier.verifierCode }} + {{ verifier.verifierType }} + {{ verifier.mobileNo }} + {{ verifier.emailId }} + {{ verifier.updatedAt | date:'dd/MM/yyyy HH:mm:ss' }} + + + {{ verifier.updatedUser }} + + + + Not Available + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+ +
+ + + + +
+ + +
+ + + + +
+ + +
+ + + + + +
+ + +
+ + + + +
+ + +
+ + + + +
+ + +
+ + + + +
+ + +
+
+ + +
+
+ + +
+
+
+
+
+ + + + + +
+ + +
diff --git a/frontend/src/app/pages/account/company/verifier/verifier.component.ts b/frontend/src/app/pages/account/company/verifier/verifier.component.ts new file mode 100644 index 0000000..0dcd63f --- /dev/null +++ b/frontend/src/app/pages/account/company/verifier/verifier.component.ts @@ -0,0 +1,347 @@ +import { CompanyVerifierModel, EmployeeDTO } from './../../../../models/account.model'; +import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core'; +import { ConfirmationService, MessageService } from 'primeng/api'; +import { TableModule } from 'primeng/table'; +import { Dialog } from 'primeng/dialog'; +import { Button } from 'primeng/button'; +import { ToastModule } from 'primeng/toast'; +import { ToolbarModule } from 'primeng/toolbar'; +import { ConfirmDialog } from 'primeng/confirmdialog'; +import { InputTextModule } from 'primeng/inputtext'; +import { TextareaModule } from 'primeng/textarea'; +import { CommonModule } from '@angular/common'; +import { SelectModule } from 'primeng/select'; +import { Tag } from 'primeng/tag'; +import { Skeleton } from 'primeng/skeleton'; +import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; +import { IconFieldModule } from 'primeng/iconfield'; +import { InputIconModule } from 'primeng/inputicon'; +import { Table } from 'primeng/table'; +import { DropdownModule } from 'primeng/dropdown'; +import { CompanyService } from '../../../../services/account/company/company.service'; +import { FloatLabelModule } from "primeng/floatlabel"; +import { ValidationService } from '../../../../services/utilities/validation.service'; +import { TooltipModule } from 'primeng/tooltip'; +import { AutoCompleteModule } from 'primeng/autocomplete'; +import { CalendarModule } from 'primeng/calendar'; +import { CheckboxModule } from 'primeng/checkbox'; +import { Subject, debounceTime } from 'rxjs'; + +interface Column { + field: string; + header: string; + customExportHeader?: string; +} + +interface ExportColumn { + title: string; + dataKey: string; +} + +@Component({ + selector: 'app-verifier', + templateUrl: './verifier.component.html', + standalone: true, + imports: [TableModule, Dialog, SelectModule, ToastModule, ToolbarModule, ConfirmDialog, InputTextModule, TextareaModule, CommonModule, DropdownModule, Tag, InputTextModule, IconFieldModule, InputIconModule, Button, FloatLabelModule, FormsModule, ReactiveFormsModule, TooltipModule, Skeleton, AutoCompleteModule, CalendarModule, CheckboxModule], + providers: [MessageService, ConfirmationService], + styleUrl: './verifier.component.css' +}) +export class VerifierComponent implements OnInit{ + verifierForm: FormGroup; + verifierDialog: boolean = false; + verifiers!: CompanyVerifierModel[]; + + verifier: CompanyVerifierModel | undefined; + + selectedVerifiers!: CompanyVerifierModel[] | null; + + submitted: boolean = false; + + isLoading: boolean = true; + + skeletonData: any[] = Array(10).fill({}); + + suggestions: any[] = []; + + private searchSubject = new Subject(); + + verifierTypes: any[] = [ + { label: 'INTERNAL', value: 'INTERNAL' }, + { label: 'EXTERNAL', value: 'EXTERNAL' } + ]; + + @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.verifierForm = this.fb.group({ + id: [{ value: '', disabled: true }], + employeeId: [{ value: null, disabled: false }], + verifierCode: [{ value: '', disabled: false }, [Validators.required]], + verifierType: [{ value: 'INTERNAL', disabled: false }, [Validators.required]], + fullName: [{ value: '', disabled: false }, [Validators.required, ValidationService.nameValidator()]], + mobileNo: [{ value: '', disabled: false }, [Validators.required, ValidationService.mobileValidator()]], + emailId: [{ value: '', disabled: false }, [Validators.required, ValidationService.emailValidator()]], + allocationDate: [{ value: '', disabled: false }], + appActive: [{ value: true, disabled: false }], + photoMandatory: [{ value: false, disabled: false }], + active: [{ value: true, disabled: false }] + }); + } + + exportCSV() { + this.dt.exportCSV(); + } + + ngOnInit() { + this.loadAllVerifiers(); + 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 = []; + } + }); + } + + searchEmployees(event: any) { + this.searchSubject.next(event.query); + } + + onSelectEmployee(event: any) { + const emp = event.value; + const patchData: any = { + employeeId: emp.employeeId, + fullName: emp.fullName, + mobileNo: emp.contactNo, + emailId: emp.emailId + }; + + // Auto set allocation date only in add new mode + if (!this.verifier && emp.joiningDate) { + patchData.allocationDate = new Date(emp.joiningDate); + } + + this.verifierForm.patchValue(patchData); + } + + onVerifierTypeChange() { + this.suggestions = []; + this.verifierForm.patchValue({ + employeeId: null, + fullName: '', + mobileNo: '', + emailId: '' + }); + this.cd.markForCheck(); + } + + loadAllVerifiers() { + this.isLoading = true; + this.companyService.getAllVerifiers().subscribe({ + next: (data) => { + this.isLoading = false; + this.verifiers = data; + this.cd.markForCheck(); + }, + error: (err) => { + this.isLoading = false; + console.error(err); + } + }); + + this.cols = [ + { field: 'fullName', header: 'Full Name' }, + { field: 'verifierCode', header: 'Verifier Code' }, + { field: 'verifierType', header: 'Type' }, + { field: 'mobileNo', header: 'Mobile 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 })); + } + + openNew() { + this.verifier = undefined; + this.verifierForm.reset(); + this.verifierForm.patchValue({ + verifierType: 'INTERNAL', + appActive: true, + photoMandatory: false, + active: true + }); + this.submitted = false; + this.verifierDialog = true; + } + + editVerifier(verifier: CompanyVerifierModel) { + this.verifierForm.reset(); + this.verifier = { ...verifier }; + + const verifierToPatch = { ...this.verifier }; + if (verifierToPatch.allocationDate) { + (verifierToPatch as any).allocationDate = new Date(verifierToPatch.allocationDate); + } + + this.verifierForm.patchValue(verifierToPatch); + this.verifierDialog = true; + } + + hideDialog() { + this.verifierDialog = false; + this.submitted = false; + } + + toggleActive(verifier: CompanyVerifierModel) { + const isActivating = !verifier.active; + this.confirmationService.confirm({ + message: `Are you sure you want to ${isActivating ? 'activate' : 'deactivate'} ` + verifier.fullName + '?', + header: 'Confirm', + icon: 'pi pi-exclamation-triangle', + rejectButtonProps: { + label: 'No', + severity: 'secondary', + variant: 'text' + }, + acceptButtonProps: { + severity: isActivating ? 'success' : 'danger', + label: 'Yes' + }, + accept: () => { + if(verifier.id) { + this.companyService.activateDeactivateVerifier(verifier.id, isActivating).subscribe({ + next: (updatedVerifier) => { + verifier.active = updatedVerifier.active; + this.verifiers = [...this.verifiers]; + this.messageService.add({ + severity: 'success', + summary: 'Successful', + detail: `Verifier ${isActivating ? 'Activated' : 'Deactivated'}`, + life: 3000 + }); + }, + error: (err) => { + console.error('Error toggling verifier active status', err); + this.messageService.add({ + severity: 'error', + summary: 'Error', + detail: 'Failed to update verifier status', + life: 3000 + }); + } + }); + } + } + }); + } + + getSeverity(status: boolean) { + return status ? 'success' : 'warn'; + } + + getErrorMessage(fieldName: string): string { + const control = this.verifierForm.get(fieldName); + return control ? ValidationService.getErrorMessage(control, fieldName) : ''; + } + + isFieldInvalid(fieldName: string): boolean { + const control = this.verifierForm.get(fieldName); + return !!(control && control.invalid && (control.dirty || control.touched || this.submitted)); + } + + saveVerifier() { + this.submitted = true; + + if (this.verifierForm.invalid) { + return; + } + + const verifierData = this.verifierForm.getRawValue() as CompanyVerifierModel; + const verifiers = this.verifiers ?? []; + + // Duplicate check + const existing = verifiers.some(v => { + if (v.id === verifierData.id) return false; + return v.verifierCode?.trim().toLowerCase() === verifierData.verifierCode?.trim().toLowerCase(); + }); + + if (existing) { + this.messageService.add({ + severity: 'error', + summary: 'Validation Error', + detail: 'Verifier with same code already exists', + life: 3000 + }); + return; + } + + this.companyService.saveVerifier(verifierData).subscribe({ + next: (savedVerifier) => { + const index = verifierData.id + ? verifiers.findIndex(v => v.id === verifierData.id) + : -1; + + if (index !== -1) { + verifiers[index] = savedVerifier; + } else { + verifiers.push(savedVerifier); + } + + this.verifiers = [...verifiers]; + + this.messageService.add({ + severity: 'success', + summary: 'Successful', + detail: index !== -1 ? 'Verifier Updated' : 'Verifier Created', + life: 3000 + }); + + this.verifierDialog = false; + this.verifier = undefined; + this.verifierForm.reset(); + this.submitted = false; + }, + error: (err) => { + console.error('Error saving verifier', err); + this.messageService.add({ + severity: 'error', + summary: 'Error', + detail: 'Failed to save verifier', + 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 index d05f647..a352caf 100644 --- a/frontend/src/app/pages/account/user/user.component.html +++ b/frontend/src/app/pages/account/user/user.component.html @@ -1,11 +1,11 @@
- + - + @@ -25,16 +25,16 @@ currentPageReportTemplate="Showing {first} to {last} of {totalRecords} entries" [showCurrentPageReport]="true" > - +

Manage Users

- - + + - +
- + # @@ -82,7 +82,7 @@ - + {{ rowIndex + 1 }} {{ user.loginId }} @@ -130,27 +130,34 @@ - +
- - + + + + + - + +
+ Login ID is already taken. + {{ getErrorMessage('loginId') }} +
- + - + +
+ {{ getErrorMessage('displayName') }} +
- + - +
- + - +
- + - +
- + - +
- + - +
- - + - + +
+ {{ getErrorMessage('status') }} +
- - + + - +
- - + + - +
@@ -279,6 +292,7 @@ {{role.groupName}} {{role.branchName}} + @@ -293,7 +307,7 @@ - + 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 index 283fb40..eca90ed 100644 --- a/frontend/src/app/pages/account/user/user.component.ts +++ b/frontend/src/app/pages/account/user/user.component.ts @@ -1,26 +1,31 @@ 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 { TableModule } from 'primeng/table'; +import { Dialog } from 'primeng/dialog'; +import { Ripple } from 'primeng/ripple'; +import { ButtonModule, Button } from 'primeng/button'; import { ToastModule } from 'primeng/toast'; import { ToolbarModule } from 'primeng/toolbar'; -import { ConfirmDialogModule } from 'primeng/confirmdialog'; +import { ConfirmDialog } from 'primeng/confirmdialog'; import { InputTextModule } from 'primeng/inputtext'; import { TextareaModule } from 'primeng/textarea'; 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 { FileUpload } from 'primeng/fileupload'; +import { SelectModule } from 'primeng/select'; +import { Tag } from 'primeng/tag'; +import { RadioButton } from 'primeng/radiobutton'; +import { Rating } from 'primeng/rating'; +import { Skeleton } from 'primeng/skeleton'; import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; -import { InputNumberModule } from 'primeng/inputnumber'; +import { InputNumber } from 'primeng/inputnumber'; +import { IconFieldModule } from 'primeng/iconfield'; +import { InputIconModule } from 'primeng/inputicon'; +import { Table } from 'primeng/table'; +import { DropdownModule } from 'primeng/dropdown'; import { UserService } from '../../../services/account/user/user.service'; import { CompanyService } from '../../../services/account/company/company.service'; +import { FloatLabelModule } from "primeng/floatlabel"; import { ValidationService } from '../../../services/utilities/validation.service'; import { TooltipModule } from 'primeng/tooltip'; import { AutoCompleteModule } from 'primeng/autocomplete'; @@ -42,13 +47,7 @@ interface ExportColumn { selector: 'app-user', templateUrl: './user.component.html', standalone: true, - imports: [ - CommonModule, FormsModule, ReactiveFormsModule, - TableModule, DialogModule, ButtonModule, ToastModule, ToolbarModule, ConfirmDialogModule, - InputTextModule, TextareaModule, FileUploadModule, DropdownModule, TagModule, - RadioButtonModule, RatingModule, SkeletonModule, InputNumberModule, - TooltipModule, AutoCompleteModule, FieldsetModule, RippleModule - ], + imports: [TableModule, Dialog, SelectModule, ToastModule, ToolbarModule, ConfirmDialog, InputTextModule, CommonModule, Tag, IconFieldModule, InputIconModule, Button, FloatLabelModule, FormsModule, ReactiveFormsModule, TooltipModule, Skeleton, AutoCompleteModule, FieldsetModule], providers: [MessageService, ConfirmationService], styleUrl: './user.component.css' }) @@ -62,6 +61,8 @@ export class UserComponent implements OnInit{ selectedUsers!: UserDTO[] | null; submitted: boolean = false; + checkingLoginId: boolean = false; + loginIdTaken: boolean = false; isLoading: boolean = true; @@ -73,6 +74,9 @@ export class UserComponent implements OnInit{ userRolesOptions: any[] = []; + selectedBranch: string | undefined; + selectedRole: string | undefined; + suggestions: any[] = []; private searchSubject = new Subject(); @@ -121,6 +125,32 @@ export class UserComponent implements OnInit{ ngOnInit() { this.loadAllUsers(); + this.loadBranches(); + this.loadRoles(); + } + + loadRoles() { + this.userService.getAllRoles().subscribe({ + next: (data) => { + this.userRolesOptions = data; + this.cd.markForCheck(); + }, + error: (err) => { + console.error('Failed to load roles', err); + } + }); + } + + loadBranches() { + this.userService.getAllBranches().subscribe({ + next: (data) => { + this.branches = data; + this.cd.markForCheck(); + }, + error: (err) => { + console.error('Failed to load branches', err); + } + }); } loadAllUsers() { @@ -187,6 +217,10 @@ export class UserComponent implements OnInit{ this.userForm.reset(); this.userRoles = []; this.submitted = false; + this.loginIdTaken = false; + this.checkingLoginId = false; + this.selectedBranch = undefined; + this.selectedRole = undefined; this.userDialog = true; } @@ -194,6 +228,8 @@ export class UserComponent implements OnInit{ this.userForm.reset(); this.user = { ...user }; this.userRoles = [...(user.userRoles || [])]; + this.selectedBranch = undefined; + this.selectedRole = undefined; this.userForm.patchValue(user); this.userDialog = true; console.log(this.userForm.getRawValue()); @@ -202,6 +238,8 @@ export class UserComponent implements OnInit{ hideDialog() { this.userDialog = false; this.submitted = false; + this.loginIdTaken = false; + this.checkingLoginId = false; } toggleActive(user: UserDTO) { @@ -210,8 +248,15 @@ export class UserComponent implements OnInit{ 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', + rejectButtonProps: { + label: 'No', + severity: 'secondary', + variant: 'text' + }, + acceptButtonProps: { + severity: isActivating ? 'success' : 'danger', + label: 'Yes' + }, accept: () => { this.userService.activateDeactivateUser(user.id!, isActivating).subscribe({ next: (updatedUser) => { @@ -243,7 +288,7 @@ export class UserComponent implements OnInit{ case 'Active': return 'success'; case 'Inactive': - return 'warning'; + return 'warn'; } return 'info'; } @@ -258,14 +303,134 @@ export class UserComponent implements OnInit{ return !!(control && control.invalid && (control.dirty || control.touched || this.submitted)); } + checkLoginId() { + const loginIdValue = this.userForm.get('loginId')?.value; + + if (!loginIdValue) { + this.clearLoginIdTakenError(); + return; + } + + if (!!this.user) { + return; + } + + this.checkingLoginId = true; + this.loginIdTaken = false; + this.cd.markForCheck(); + + const requestPayload = { + data: { + searchBy: 'loginId', + searchValue: loginIdValue + }, + compressed: true, + target: 'models.commons.Search' + }; + + this.userService.searchUser(requestPayload).subscribe({ + next: (userDTO) => { + this.checkingLoginId = false; + if (userDTO && userDTO.loginId) { + this.loginIdTaken = true; + this.userForm.get('loginId')?.setErrors({ taken: true }); + } else { + this.loginIdTaken = false; + const loginIdControl = this.userForm.get('loginId'); + if (loginIdControl) { + const errors = loginIdControl.errors; + if (errors) { + delete errors['taken']; + loginIdControl.setErrors(Object.keys(errors).length ? errors : null); + } + } + } + this.cd.markForCheck(); + }, + error: (err) => { + this.checkingLoginId = false; + console.error('Error checking loginId', err); + this.cd.markForCheck(); + } + }); + } + + onLoginIdInput() { + if (this.loginIdTaken) { + this.clearLoginIdTakenError(); + } + } + + private clearLoginIdTakenError() { + this.loginIdTaken = false; + const loginIdControl = this.userForm.get('loginId'); + if (loginIdControl) { + const errors = loginIdControl.errors; + if (errors) { + delete errors['taken']; + loginIdControl.setErrors(Object.keys(errors).length ? errors : null); + } + } + } + addRole() { - // for now, do nothing, since selects empty + if (!this.selectedBranch || !this.selectedRole) { + this.messageService.add({ + severity: 'warn', + summary: 'Warning', + detail: 'Please select both Branch and User Role', + life: 3000 + }); + return; + } + + const branch = this.branches.find(b => b.id === this.selectedBranch); + const role = this.userRolesOptions.find(r => r.id === this.selectedRole); + + if (branch && role) { + const exists = this.userRoles.some(ur => ur.branchId === branch.id && ur.roleId === role.id); + if (exists) { + this.messageService.add({ + severity: 'warn', + summary: 'Warning', + detail: 'Role already assigned for this branch', + life: 3000 + }); + return; + } + + const newRole: UserRoleDTO = { + branchId: branch.id, + branchName: branch.branchName, + roleId: role.id, + roleName: role.roleName, + groupName: role.groupName, + active: true, + defaultRole: false + }; + this.userRoles.push(newRole); + + this.selectedBranch = undefined; + this.selectedRole = undefined; + } } toggleRoleActive(role: UserRoleDTO) { role.active = role.active === false ? true : false; } + setDefaultRole(role: UserRoleDTO) { + const isCurrentlyDefault = role.defaultRole; + this.userRoles.forEach(r => { + if (r.branchId === role.branchId) { + r.defaultRole = false; + } + }); + if (!isCurrentlyDefault) { + role.defaultRole = true; + } + } + onSelectEmployee(event: any) { const emp = event.value; const patch: any = { @@ -318,16 +483,21 @@ export class UserComponent implements OnInit{ this.userService.saveUser(userData).subscribe({ next: (savedUser) => { - const index = userData.id ? users.findIndex(u => u.id === userData.id) : -1; if (index !== -1) { // UPDATE + if (!savedUser.userRoles && userData.userRoles) { + savedUser.userRoles = userData.userRoles; + } users[index] = savedUser; } else { // CREATE + if (!savedUser.userRoles && userData.userRoles) { + savedUser.userRoles = userData.userRoles; + } users.push(savedUser); } diff --git a/frontend/src/app/pages/dashboard/dashboard.component.ts b/frontend/src/app/pages/dashboard/dashboard.component.ts index 2a57170..8e6f11d 100644 --- a/frontend/src/app/pages/dashboard/dashboard.component.ts +++ b/frontend/src/app/pages/dashboard/dashboard.component.ts @@ -1,9 +1,15 @@ import { Component } from '@angular/core'; +import { MenuComponent } from "../../fragments/menu/menu.component"; +import { RouterOutlet } from '@angular/router'; @Component({ selector: 'app-dashboard', - imports: [], - template: `
+ imports: [MenuComponent, RouterOutlet], + template: `
+ +
+ +
` }) export class DashboardComponent { 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..1dfaa5b --- /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 index 5e49dfe..f76af42 100644 --- a/frontend/src/app/pages/session/auth/authorize.component.ts +++ b/frontend/src/app/pages/session/auth/authorize.component.ts @@ -1,38 +1,30 @@ -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 { CommonModule} from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { Component, OnInit } from '@angular/core'; +import { ButtonModule } from 'primeng/button';; +import { InputTextModule } from 'primeng/inputtext'; +import { CardModule, Card } from 'primeng/card'; +import { InputGroupModule, InputGroup } from 'primeng/inputgroup'; +import { InputGroupAddonModule, InputGroupAddon } from 'primeng/inputgroupaddon'; +import { PasswordModule, Password } from 'primeng/password'; +import { FloatLabelModule, FloatLabel } from 'primeng/floatlabel'; +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 { StyleClassModule } from 'primeng/styleclass'; +import { Router } from '@angular/router'; import { ResponseDto } from '../../../models/response.dto'; +import { Company, Branch } from '../../../models/session.model'; +import { SelectModule } from 'primeng/select'; +import { environment } from '../../../../environments/environment'; @Component({ selector: 'app-authorize', - standalone: true, - imports: [ - CommonModule, - FormsModule, - ReactiveFormsModule, - CardModule, - ButtonModule, - InputTextModule, - PasswordModule, - DropdownModule, - KeyFilterModule, - MessageModule - ], + imports: [CommonModule, FormsModule, CardModule, ButtonModule, InputTextModule, InputGroupModule, InputGroupAddonModule, PasswordModule, + FloatLabelModule, ReactiveFormsModule, KeyFilterModule, MessageModule, StyleClassModule, SelectModule], templateUrl: './authorize.component.html', styleUrl: './authorize.component.css' }) @@ -51,7 +43,7 @@ export class AuthorizeComponent implements OnInit { } ngOnInit(){ - const companyBranchRoles = this.sessionService.getItem('companies'); + const companyBranchRoles = sessionStorage.getItem('companies'); if (!companyBranchRoles) { this.sessionService.logout(); @@ -60,7 +52,7 @@ export class AuthorizeComponent implements OnInit { sessionStorage.removeItem('companies'); try { - this.companies = companyBranchRoles; + this.companies = JSON.parse(companyBranchRoles); } catch (e) { this.sessionService.logout(); } 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..4112a40 --- /dev/null +++ b/frontend/src/app/pages/session/profile/profile.component.html @@ -0,0 +1,167 @@ +
+
+ +
+ + User Details +
+ +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + +
+ + Employment Details +
+ +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+
+
+
+ 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..3cdb91f --- /dev/null +++ b/frontend/src/app/pages/session/profile/profile.component.ts @@ -0,0 +1,60 @@ +import { UserProfile } from './../../../models/session.model'; +import { Component, OnInit } from '@angular/core'; +import { FloatLabelModule } from "primeng/floatlabel" +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"; + +@Component({ + selector: 'app-profile', + imports: [FloatLabelModule, InputTextModule, FormsModule, ReactiveFormsModule, CardModule, DividerModule], + templateUrl: './profile.component.html', + styleUrl: './profile.component.css' +}) +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/pages/tools/allocation/localities/localities.component.css b/frontend/src/app/pages/tools/allocation/localities/localities.component.css new file mode 100644 index 0000000..aec6235 --- /dev/null +++ b/frontend/src/app/pages/tools/allocation/localities/localities.component.css @@ -0,0 +1 @@ +/* localities.component.css */ diff --git a/frontend/src/app/pages/tools/allocation/localities/localities.component.html b/frontend/src/app/pages/tools/allocation/localities/localities.component.html new file mode 100644 index 0000000..8374135 --- /dev/null +++ b/frontend/src/app/pages/tools/allocation/localities/localities.component.html @@ -0,0 +1,289 @@ +
+ + + + + + + + + + + + + + + +
+

Manage Localities

+ + + + +
+
+ + + # + +
Locality Name
+ + +
Admin Area
+ + +
Pincode
+ + +
Area
+ + +
Updated At
+ + +
Updated By
+ + +
Status
+ + + +
+ + + {{ rowIndex + 1 }} + {{ row.localityName }} + {{ row.adminArea }}, {{ row.masterStateName }} + {{ row.pincode }} + {{ row.areaName }} + {{ row.updatedAt | date:'dd/MM/yyyy HH:mm:ss' }} + {{ row.updatedUser }}N/A + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+
+
+ +
+
+ +
+
+ + +
+
+
+
+ + + + + +
+
+
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+
+
+
+ + + + +
+ + + +
diff --git a/frontend/src/app/pages/tools/allocation/localities/localities.component.ts b/frontend/src/app/pages/tools/allocation/localities/localities.component.ts new file mode 100644 index 0000000..732c176 --- /dev/null +++ b/frontend/src/app/pages/tools/allocation/localities/localities.component.ts @@ -0,0 +1,542 @@ +import { ChangeDetectorRef, Component, ElementRef, OnInit, ViewChild } from '@angular/core'; +import { ConfirmationService, MessageService } from 'primeng/api'; +import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; +import { Table, TableModule } from 'primeng/table'; +import { Dialog } from 'primeng/dialog'; +import { Button } from 'primeng/button'; +import { ToastModule } from 'primeng/toast'; +import { ToolbarModule } from 'primeng/toolbar'; +import { ConfirmDialog } from 'primeng/confirmdialog'; +import { InputTextModule } from 'primeng/inputtext'; +import { TextareaModule } from 'primeng/textarea'; +import { CommonModule } from '@angular/common'; +import { SelectModule } from 'primeng/select'; +import { Tag } from 'primeng/tag'; +import { Skeleton } from 'primeng/skeleton'; +import { IconFieldModule } from 'primeng/iconfield'; +import { InputIconModule } from 'primeng/inputicon'; +import { DropdownModule } from 'primeng/dropdown'; +import { FloatLabelModule } from "primeng/floatlabel"; +import { ValidationService } from '../../../../services/utilities/validation.service'; +import { TooltipModule } from 'primeng/tooltip'; +import { AutoCompleteModule } from 'primeng/autocomplete'; +import { CheckboxModule } from 'primeng/checkbox'; +import { Subject, debounceTime } from 'rxjs'; + +import { AllocationService } from '../../../../services/tools/allocation/allocation.service'; +import { MasterService } from '../../../../services/masters/master.service'; +import { CompanyService } from '../../../../services/account/company/company.service'; +import { OlaService } from '../../../../services/ola/ola.service'; +import { LocalityDTO, AreaDTO, LocalityTypeDTO } from '../../../../models/tools.model'; +import { CityDTO } from '../../../../models/masters/masters'; +import { CompanyVerifierModel } from '../../../../models/account.model'; + +interface Column { + field: string; + header: string; + customExportHeader?: string; +} + +interface ExportColumn { + title: string; + dataKey: string; +} + +@Component({ + selector: 'app-localities', + templateUrl: './localities.component.html', + standalone: true, + imports: [TableModule, Dialog, SelectModule, ToastModule, ToolbarModule, ConfirmDialog, InputTextModule, TextareaModule, CommonModule, DropdownModule, Tag, InputTextModule, IconFieldModule, InputIconModule, Button, FloatLabelModule, FormsModule, ReactiveFormsModule, TooltipModule, Skeleton, AutoCompleteModule, CheckboxModule], + providers: [MessageService, ConfirmationService], + styleUrls: ['./localities.component.css'] +}) +export class LocalitiesComponent implements OnInit { + localityForm: FormGroup; + areaForm: FormGroup; + + localityDialog: boolean = false; + areaDialog: boolean = false; + + localities!: LocalityDTO[]; + locality: LocalityDTO | undefined; + + localityTypes: LocalityTypeDTO[] = []; + areas: AreaDTO[] = []; + + selectedLocalities!: LocalityDTO[] | null; + + submittedLocality: boolean = false; + submittedArea: boolean = false; + + isLoading: boolean = true; + skeletonData: any[] = Array(10).fill({}); + + // Suggestions + areaSuggestions: any[] = []; + citySuggestions: any[] = []; + olaSuggestions: any[] = []; + + // Verifiers for dropdowns + verifiers: CompanyVerifierModel[] = []; + + private searchAreaSubject = new Subject(); + private searchCitySubject = new Subject(); + private searchOlaSubject = new Subject(); + + @ViewChild('dt') dt!: Table; + @ViewChild('localityNameInput') localityNameInput!: ElementRef; + + cols!: Column[]; + exportColumns!: ExportColumn[]; + + constructor( + private allocationService: AllocationService, + private masterService: MasterService, + private companyService: CompanyService, + private olaService: OlaService, + private messageService: MessageService, + private confirmationService: ConfirmationService, + private cd: ChangeDetectorRef, + private fb: FormBuilder, + ) { + this.localityForm = this.fb.group({ + id: [{ value: '', disabled: true }], + areaId: [{ value: null, disabled: false }, Validators.required], + stateCityId: [{ value: null, disabled: false }, Validators.required], + cityAutoComplete: [{ value: null, disabled: false }, Validators.required], + localityTypeId: [{ value: null, disabled: false }, Validators.required], + localityName: [{ value: '', disabled: false }, Validators.required], + adminArea: [{ value: '', disabled: false }, Validators.required], + olaPlaceId: [{ value: null, disabled: false }], + olaLocalityAutoComplete: [{ value: null, disabled: false }], + olaLocality: [{ value: '', disabled: false }], + stateName: [{ value: '', disabled: false }], + pincode: [{ value: '', disabled: false }], + latitude: [{ value: null, disabled: true }], + longitude: [{ value: null, disabled: true }], + active: [{ value: true, disabled: false }] + }); + + this.areaForm = this.fb.group({ + id: [{ value: '', disabled: true }], + areaName: [{ value: '', disabled: false }, Validators.required], + + groupAVerifierId: [{ value: null, disabled: false }, Validators.required], + groupBVerifierId: [{ value: null, disabled: false }, Validators.required], + groupCVerifierId: [{ value: null, disabled: false }, Validators.required] + }); + } + + exportCSV() { + this.dt.exportCSV(); + } + + ngOnInit() { + this.loadLocalities(); + this.loadLocalityTypes(); + this.loadAreas(); + + this.searchAreaSubject.pipe(debounceTime(400)).subscribe(query => { + if (query && query.length >= 2) { + const req = { data: { searchBy: 'NAME', searchValue: query }, compressed: true, target: 'models.commons.Search' }; + this.allocationService.searchAreas(req).subscribe({ + next: (areas) => { + this.areaSuggestions = areas; + this.cd.markForCheck(); + }, + error: () => this.areaSuggestions = [] + }); + } else { this.areaSuggestions = []; } + }); + + this.searchCitySubject.pipe(debounceTime(300)).subscribe(query => { + if (query && query.length >= 2) { + const req: any = { + data: { searchBy: 'CITY', searchValue: query }, + compressed: true, + target: 'models.commons.Search' + }; + this.masterService.searchCityStates(req).subscribe({ + next: (cities) => { + this.citySuggestions = cities.map(city => ({ + ...city, + display: `${city.cityName}, ${city.stateName}` + })); + this.cd.markForCheck(); + }, + error: () => this.citySuggestions = [] + }); + } else { this.citySuggestions = []; } + }); + + this.searchOlaSubject.pipe(debounceTime(400)).subscribe(query => { + if (query && query.length >= 3) { + this.olaService.autocomplete(query).subscribe({ + next: (res) => { + let predictions = res; + if (res && res.predictions) { + predictions = res.predictions; + } + this.olaSuggestions = Array.isArray(predictions) ? predictions.map((p: any) => { + let displayName = p.structured_formatting?.main_text || ''; + const terms = p.terms || []; + const n = terms.length; + if (n >= 4) displayName += ', ' + terms[n - 4].value; + if (n >= 3) displayName += ', ' + terms[n - 3].value; + if (n >= 2) displayName += ', ' + terms[n - 2].value; + if (n >= 1) displayName += ', ' + terms[n - 1].value; + + return { + name: displayName || p.description, + raw: p, + place_id: p.place_id + }; + }) : []; + this.cd.markForCheck(); + }, + error: () => { + this.olaSuggestions = []; + } + }); + } else { this.olaSuggestions = []; } + }); + + this.areaForm.get('areaName')?.valueChanges.pipe(debounceTime(400)).subscribe(val => { + if(val == null) return; + const textToSearch = typeof val === 'string' ? val : val.areaName; + + if (textToSearch && textToSearch.trim().length >= 2) { + const req = { data: { searchBy: 'NAME', searchValue: textToSearch }, compressed: true, target: 'models.commons.Search' }; + this.allocationService.searchAreas(req).subscribe({ + next: (areas) => { + const exactMatch = areas.find(a => a.areaName?.toLowerCase().trim() === textToSearch.toLowerCase().trim()); + if (exactMatch) { + const vA = this.verifiers.find(v => v.fullName === exactMatch.groupAVerifierName); + const vB = this.verifiers.find(v => v.fullName === exactMatch.groupBVerifierName); + const vC = this.verifiers.find(v => v.fullName === exactMatch.groupCVerifierName); + + this.areaForm.patchValue({ + id: exactMatch.id, + groupAVerifierId: vA ? vA.id : null, + groupBVerifierId: vB ? vB.id : null, + groupCVerifierId: vC ? vC.id : null + }, { emitEvent: false }); + } else { + this.areaForm.patchValue({ id: '' }, { emitEvent: false }); + } + } + }); + } else { + this.areaForm.patchValue({ id: '' }, { emitEvent: false }); + } + }); + + this.loadVerifiers(); + } + + loadVerifiers() { + this.companyService.getAllVerifiers().subscribe({ + next: (data) => { + this.verifiers = data.map(v => ({ + ...v, + display: `${v.verifierCode} - ${v.fullName}` + })); + this.cd.markForCheck(); + }, + error: (err) => console.error('Error loading verifiers', err) + }); + } + + loadLocalityTypes() { + this.allocationService.getAllLocalityTypes().subscribe({ + next: (types) => { + this.localityTypes = types; + } + }); + } + + loadAreas() { + this.allocationService.getAllAreas().subscribe({ + next: (data) => { + this.areas = data; + this.cd.markForCheck(); + }, + error: (err) => console.error(err) + }); + } + + loadLocalities() { + this.isLoading = true; + this.allocationService.getAllLocalities().subscribe({ + next: (data) => { + this.isLoading = false; + this.localities = data; + this.cd.markForCheck(); + }, + error: (err) => { + this.isLoading = false; + console.error(err); + } + }); + + this.cols = [ + { field: 'localityName', header: 'Locality Name' }, + { field: 'adminArea', header: 'Admin Area' }, + { field: 'areaName', header: 'Area' }, + { field: 'pincode', header: 'Pincode' }, + { field: 'updatedAt', header: 'Last Updated At' }, + { field: 'updatedUser', header: 'Last Updated By' } + ]; + + this.exportColumns = this.cols.map((col) => ({ title: col.header, dataKey: col.field })); + } + + // AREA AUTOCOMPLETE (Used only by Area Dialog now) + searchAreas(event: any) { this.searchAreaSubject.next(event.query); } + + onSelectAreaGroup(event: any) { + const item = event.value; + const vA = this.verifiers.find(v => v.fullName === item.groupAVerifierName); + const vB = this.verifiers.find(v => v.fullName === item.groupBVerifierName); + const vC = this.verifiers.find(v => v.fullName === item.groupCVerifierName); + + this.areaForm.patchValue({ + id: item.id, + areaName: item.areaName, + groupAVerifierId: vA ? vA.id : null, + groupBVerifierId: vB ? vB.id : null, + groupCVerifierId: vC ? vC.id : null + }); + } + + // CITY AUTOCOMPLETE + searchCities(event: any) { this.searchCitySubject.next(event.query); } + onSelectCity(event: any) { + this.localityForm.patchValue({ stateCityId: event.value.id }); + } + + // OLA AUTOCOMPLETE + searchOlaLocalities(event: any) { this.searchOlaSubject.next(event.query); } + onSelectOlaLocality(event: any) { + const item = event.value; + const rawItem = item.raw || item; + let pincode = ''; + let state = ''; + if (rawItem && rawItem.terms && rawItem.terms.length > 0) { + const terms = rawItem.terms; + const n = terms.length; + if (n >= 2) pincode = terms[n - 2].value; + if (n >= 3) state = terms[n - 3].value; + } + + this.localityForm.patchValue({ + olaPlaceId: rawItem.place_id || item.place_id, + olaLocality: rawItem.structured_formatting?.main_text || item.name, + stateName: state, + pincode: pincode, + latitude: rawItem.geometry?.location?.lat || item.lat, + longitude: rawItem.geometry?.location?.lng || item.lng + }); + } + + // VERIFIER AUTOCOMPLETE + // Using p-select now, so these are no longer needed + // DIALOGS + openNewLocality() { + this.locality = undefined; + this.localityForm.reset(); + this.localityForm.patchValue({ active: true }); + this.submittedLocality = false; + this.localityDialog = true; + } + + editLocality(loc: LocalityDTO) { + this.localityForm.reset(); + this.locality = { ...loc }; + + // Setup autoComplete selections and dropdown matching + const patchData: any = { ...this.locality }; + + if (patchData.masterCityName && patchData.masterStateName) { + patchData.cityAutoComplete = { id: patchData.stateCityId, display: `${patchData.masterCityName}, ${patchData.masterStateName}` }; + } else { + patchData.cityAutoComplete = { id: patchData.stateCityId, display: 'Selected City' }; + } + + if (patchData.olaPlaceId) { + patchData.olaLocalityAutoComplete = { place_id: patchData.olaPlaceId, name: patchData.olaLocality }; + } + + const matchingArea = this.areas.find(a => a.areaName === patchData.areaName); + if (matchingArea) patchData.areaId = matchingArea.id; + + const matchingLocType = this.localityTypes.find(l => l.type === patchData.localityTypeName); + if (matchingLocType) patchData.localityTypeId = matchingLocType.id; + + this.localityForm.patchValue(patchData); + this.localityDialog = true; + } + + openNewArea() { + this.areaForm.reset(); + this.submittedArea = false; + this.areaDialog = true; + } + + hideLocalityDialog() { + this.localityDialog = false; + this.submittedLocality = false; + } + + clearLocalityForm() { + this.localityForm.patchValue({ + id: null, + localityTypeId: null, + adminArea: '', + olaLocalityAutoComplete: null, + olaLocality: '', + stateName: '', + pincode: '', + latitude: null, + longitude: null, + olaPlaceId: null, + localityName: '' + }); + this.submittedLocality = false; + + setTimeout(() => { + if (this.localityNameInput && this.localityNameInput.nativeElement) { + this.localityNameInput.nativeElement.focus(); + } + }, 100); + } + + hideAreaDialog() { + this.areaDialog = false; + this.submittedArea = false; + } + + getSeverity(status: boolean) { + return status ? 'success' : 'warn'; + } + + getLocalityErrorMessage(fieldName: string): string { + const control = this.localityForm.get(fieldName); + return control ? ValidationService.getErrorMessage(control, fieldName) : ''; + } + + isLocalityFieldInvalid(fieldName: string): boolean { + const control = this.localityForm.get(fieldName); + return !!(control && control.invalid && (control.dirty || control.touched || this.submittedLocality)); + } + + getAreaErrorMessage(fieldName: string): string { + const control = this.areaForm.get(fieldName); + return control ? ValidationService.getErrorMessage(control, fieldName) : ''; + } + + isAreaFieldInvalid(fieldName: string): boolean { + const control = this.areaForm.get(fieldName); + return !!(control && control.invalid && (control.dirty || control.touched || this.submittedArea)); + } + + validateUniqueLocality(localityName: string): boolean { + const existing = (this.localities ?? []).some(o => + o.id !== this.localityForm.get('id')?.value && + o.localityName?.toLowerCase().trim() === localityName.toLowerCase().trim() + ); + return !existing; + } + + saveLocality() { + this.submittedLocality = true; + + if (this.localityForm.invalid) { + return; + } + + const data = this.localityForm.getRawValue(); + + if (!this.validateUniqueLocality(data.localityName)) { + this.messageService.add({ severity: 'error', summary: 'Validation Error', detail: 'Locality Name already exists', life: 3000 }); + return; + } + + const locDto: LocalityDTO = { + id: data.id, + areaId: data.areaId, + stateCityId: data.stateCityId, + localityTypeId: data.localityTypeId, + localityName: data.localityName, + adminArea: data.adminArea, + olaPlaceId: data.olaPlaceId, + olaLocality: data.olaLocality, + latitude: data.latitude, + longitude: data.longitude, + stateName: data.stateName, + pincode: data.pincode, + active: data.active + }; + + this.allocationService.saveLocality(locDto).subscribe({ + next: (savedLoc) => { + const index = locDto.id ? (this.localities ?? []).findIndex(v => v.id === locDto.id) : -1; + if (index !== -1) { + this.localities[index] = savedLoc; + } else { + if (!this.localities) { + this.localities = []; + } + this.localities.push(savedLoc); + } + this.localities = [...this.localities]; + this.messageService.add({ severity: 'success', summary: 'Successful', detail: index !== -1 ? 'Locality Updated' : 'Locality Created', life: 3000 }); + + if (locDto.id) { + this.localityForm.reset(); + this.hideLocalityDialog(); + } else { + this.clearLocalityForm(); + } + }, + error: (err) => { + console.error(err); + this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Failed to save locality', life: 3000 }); + } + }); + } + + saveArea() { + this.submittedArea = true; + + if (this.areaForm.invalid) { + return; + } + + const data = this.areaForm.getRawValue(); + const extractedName = typeof data.areaName === 'string' ? data.areaName : data.areaName.areaName; + + const areaDto: AreaDTO = { + id: data.id, + areaName: extractedName, + groupAVerifierId: data.groupAVerifierId, + groupBVerifierId: data.groupBVerifierId, + groupCVerifierId: data.groupCVerifierId, + active: true + }; + + this.allocationService.saveArea(areaDto).subscribe({ + next: () => { + this.messageService.add({ severity: 'success', summary: 'Successful', detail: 'Area Group Created/Updated', life: 3000 }); + this.areaDialog = false; + this.areaForm.reset(); + this.submittedArea = false; + }, + error: (err) => { + console.error(err); + this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Failed to save Area Group', life: 3000 }); + } + }); + } + +} 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..c91c942 --- /dev/null +++ b/frontend/src/app/services/account/company/company.service.ts @@ -0,0 +1,199 @@ +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, CompanyVerifierModel } 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[]) + ); + } + + // Verifiers + getAllVerifiers(): Observable { + return this.http.get(`${environment.accountService}/company/verifiers`).pipe( + map((response: ResponseDto) => response.data as CompanyVerifierModel[]) + ); + } + + saveVerifier(verifier: CompanyVerifierModel): Observable { + const requestPayload: Request = { + data: verifier, + compressed: true, + target: 'cygnus.models.account.CompanyVerifier' + }; + return this.http.post(`${environment.accountService}/company/verifiers`, requestPayload).pipe( + map((response: ResponseDto) => response.data as CompanyVerifierModel) + ); + } + + activateDeactivateVerifier(verifierId: string, active: boolean): Observable { + const verifier: any = ({ + id: verifierId, + active: active + }); + const requestPayload: Request = { + scopes: [active ? "ACTIVATE" : "DEACTIVATE"], + data: verifier, + compressed: true, + target: 'cygnus.models.account.CompanyVerifier' + }; + return this.http.post(`${environment.accountService}/company/verifiers`, requestPayload).pipe( + map((response: ResponseDto) => response.data as CompanyVerifierModel) + ); + } + + searchVerifiers(payload: Request): Observable { + return this.http.post(`${environment.accountService}/company/verifiers/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..5dde5c6 --- /dev/null +++ b/frontend/src/app/services/account/user/user.service.ts @@ -0,0 +1,67 @@ +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[]) + ); + } + + searchUser(payload: Request): Observable { + return this.http.post(`${environment.userService}/users/search`, payload).pipe( + map((response: ResponseDto) => response.data as UserDTO) + ); + } + + getAllRoles(): Observable { + return this.http.get(`${environment.userService}/roles`).pipe( + map((response: any) => response.data as any[]) + ); + } + + getAllBranches(): Observable { + return this.http.get(`${environment.userService}/branches`).pipe( + map((response: any) => response.data as any[]) + ); + } + + saveUser(user: UserDTO): Observable { + const requestPayload: Request = { + data: user, + compressed: true, + target: 'cygnus.models.user.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.user.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/ola/ola.service.ts b/frontend/src/app/services/ola/ola.service.ts new file mode 100644 index 0000000..0c73517 --- /dev/null +++ b/frontend/src/app/services/ola/ola.service.ts @@ -0,0 +1,24 @@ +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 { ResponseDto } from '../../models/response.dto'; + +@Injectable({ + providedIn: 'root' +}) +export class OlaService { + + constructor(private http: HttpService) {} + + autocomplete(query: string): Observable { + // Assuming backend takes get parameter 'text' or something similar. + // Wait, the prompt says @GetMapping(AUTOCOMPLETE) on /ola/location/api/v1/autocomplete + // Let's pass query directly as a parameter like ?input=query + return this.http.get(`${environment.masterService}/ola-autocomplete?input=${encodeURIComponent(query)}`).pipe( + map((response: ResponseDto) => response.data) + ); + } + +} diff --git a/frontend/src/app/services/tools/allocation/allocation.service.ts b/frontend/src/app/services/tools/allocation/allocation.service.ts new file mode 100644 index 0000000..118c2fe --- /dev/null +++ b/frontend/src/app/services/tools/allocation/allocation.service.ts @@ -0,0 +1,73 @@ +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 { AreaDTO, LocalityDTO, LocalityTypeDTO } from '../../../models/tools.model'; +import { ResponseDto } from '../../../models/response.dto'; + +@Injectable({ + providedIn: 'root' +}) +export class AllocationService { + + constructor(private http: HttpService) {} + + // Areas + getAllAreas(): Observable { + return this.http.get(`${environment.toolsService}/areas`).pipe( + map((response: ResponseDto) => response.data as AreaDTO[]) + ); + } + + saveArea(area: AreaDTO): Observable { + const requestPayload: Request = { + data: area, + compressed: false, + target: 'cygnus.models.tools.Area' + }; + return this.http.post(`${environment.toolsService}/areas`, requestPayload).pipe( + map((response: ResponseDto) => response.data as AreaDTO) + ); + } + + searchAreas(payload: Request): Observable { + return this.http.post(`${environment.toolsService}/areas/search`, payload).pipe( + map((response: ResponseDto) => response.data as any[]) + ); + } + + // Localities + getAllLocalities(): Observable { + return this.http.get(`${environment.toolsService}/localities`).pipe( + map((response: ResponseDto) => response.data as LocalityDTO[]) + ); + } + + saveLocality(locality: LocalityDTO): Observable { + const requestPayload: Request = { + data: locality, + compressed: false, + target: 'cygnus.models.tools.Locality' + }; + return this.http.post(`${environment.toolsService}/localities`, requestPayload).pipe( + map((response: ResponseDto) => response.data as LocalityDTO) + ); + } + + searchLocalities(payload: Request): Observable { + return this.http.post(`${environment.toolsService}/localities/search`, payload).pipe( + map((response: ResponseDto) => response.data as any[]) + ); + } + + // Locality Types + getAllLocalityTypes(): Observable { + return this.http.get(`${environment.toolsService}/locality-types`).pipe( + map((response: ResponseDto) => response.data as LocalityTypeDTO[]) + ); + } + +} 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..2eacb14 --- /dev/null +++ b/frontend/src/app/services/utilities/validation.service.ts @@ -0,0 +1,167 @@ +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: (fieldName: string) => { + const readableName = fieldName.replace(/([A-Z])/g, ' $1').trim(); + const titleCaseName = readableName.charAt(0).toUpperCase() + readableName.slice(1); + // special cases for common acronyms + const finalName = titleCaseName.replace(/\bId\b/g, 'ID'); + return `${finalName} 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(fieldName); + } + + 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.ts b/frontend/src/environments/environment.ts index a2396c0..116e41b 100644 --- a/frontend/src/environments/environment.ts +++ b/frontend/src/environments/environment.ts @@ -5,5 +5,6 @@ export const environment = { 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', + toolsService: 'http://localhost:1703/cygnus/app/api/v1/tools', 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/tsconfig.json b/frontend/tsconfig.json index ad457fa..54beb76 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -3,7 +3,7 @@ { "compileOnSave": false, "compilerOptions": { - "strict": true, + "strict": false, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true,