This commit is contained in:
2026-05-21 22:23:19 +05:30
parent 26478608b1
commit 8537c653c1
68 changed files with 3644 additions and 333 deletions

1
frontend/proxy.conf.json Normal file
View File

@@ -0,0 +1 @@
{}

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -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: [

View File

@@ -0,0 +1,21 @@
<div class="card m-0">
<p-menubar [model]="items" styleClass="text-sm">
<ng-template #start>
<img src="assets/images/logo.png" class="w-3rem cursor-pointer" (click)="gotToDashboard()" pTooltip="Go to Dashboard" showDelay="500" tooltipPosition="bottom" />
</ng-template>
<ng-template #end>
<div class="flex align-items-center justify-content-center gap-2">
<div class="flex flex-column line-height-1">
<span class="text-primary-700 font-medium">
{{ companyName }}
</span>
<span class="text-500 text-xs mt-1 text-right">
{{ name }} | {{ branchName }}
</span>
</div>
<p-menu #menu [model]="profileItems" [popup]="true" />
<p-button (click)="menu.toggle($event)" icon="pi pi-user" [rounded]="true" severity="info" />
</div>
</ng-template>
</p-menubar>
</div>

View File

@@ -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']);
}
}
}

View File

@@ -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)
);
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}

View File

@@ -0,0 +1,50 @@
<div class="grid align-content-center justify-content-center login-page">
<div>
<form [formGroup]="loginForm" (ngSubmit)="onLogin()">
<p-card styleClass="login-card shadow-3 w-30rem">
<ng-template pTemplate="header">
<div class="flex flex-column align-items-center gap-3 w-full mt-3">
<img src="assets/images/logo.png" class="w-5rem" />
<span class="text-xl font-bold whitespace-nowrap">
User Login
</span>
</div>
</ng-template>
<div class="mt-2">
<p-inputgroup>
<p-inputgroup-addon>
<i class="pi pi-user"></i>
</p-inputgroup-addon>
<p-floatlabel variant="on">
<input pInputText formControlName="username"/>
<label for="on_label" class="z-1">Username</label>
</p-floatlabel>
</p-inputgroup>
</div>
<div class="mt-3">
<p-inputgroup>
<p-inputgroup-addon>
<i class="pi pi-ellipsis-h"></i>
</p-inputgroup-addon>
<p-floatlabel variant="on">
<p-password
class="w-full"
formControlName="password"
[feedback]="false"
[toggleMask]="true"
[style]="{ width: '100%' }"
></p-password>
<label for="on_label" class="z-1">Password</label>
</p-floatlabel>
</p-inputgroup>
</div>
<div class="mt-4 mb-2 text-right">
<button pButton label="Login" icon="pi pi-sign-in" [disabled]="!loginForm.valid"></button>
</div>
<div class="mt-4 mb-2" *ngIf="message">
<p-message severity="error" variant="outlined" [text]="message"></p-message>
</div>
</p-card>
</form>
</div>
</div>

View File

@@ -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: `
<div class="login-container">
<p-card header="OCR Admin Login" [style]="{width: '360px'}" styleClass="p-card-shadow">
<div class="field">
<span class="p-float-label">
<input id="username" type="text" pInputText [(ngModel)]="username" class="w-full">
<label htmlFor="username">Username</label>
</span>
</div>
<div class="field mt-4">
<span class="p-float-label">
<input id="password" type="password" pInputText [(ngModel)]="password" class="w-full">
<label htmlFor="password">Password</label>
</span>
</div>
<div class="mt-4">
<p-button label="Sign In" icon="pi pi-user" styleClass="w-full" (onClick)="onLogin()"></p-button>
</div>
</p-card>
<p-toast></p-toast>
</div>
`,
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<ResponseDto>(`${environment.authService}/3z4mkell5g5aset/authenticate`, requestPayload).subscribe({
next: (response) => {
this.router.navigate(['/authorize']);
},
error: (err) => {
this.message = err.message;
}
});
}
}
}

View File

@@ -1 +0,0 @@
<p>login works!</p>

View File

@@ -1,11 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-login',
imports: [],
templateUrl: './login.html',
styleUrl: './login.scss',
})
export class Login {
}

View File

@@ -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;

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -0,0 +1,7 @@
export interface LocationDTO {
place_id?: string;
name?: string;
formatted_address?: string;
lat?: number;
lng?: number;
}

View File

@@ -0,0 +1,6 @@
export interface Request<T = any> {
scopes?: string[];
data: T;
target?: string;
compressed?: boolean;
}

View File

@@ -0,0 +1,6 @@
export interface ResponseDto {
statuscode : number;
message?: string;
description?: string;
data?: any;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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: `
<div class="card">
<h2>OCR Extraction</h2>
<!--
Note: "customUpload" mode in PrimeNG FileUpload requires "uploadHandler".
"mode='advanced'" gives the sleek UI.
-->
<p-fileUpload mode="advanced"
chooseLabel="Select PDF or Image"
uploadLabel="Extract Text"
@@ -44,15 +51,120 @@ import { ToastModule } from 'primeng/toast';
<p-progressBar mode="indeterminate" [style]="{'height': '6px'}"></p-progressBar>
</div>
<div class="mt-4" *ngIf="extractedText !== null">
<h3>Extracted Text Result:</h3>
<textarea pInputTextarea
[autoResize]="true"
[(ngModel)]="extractedText"
readonly
class="w-full"
style="min-height: 300px; width: 100%; border-color: #d1d5db; font-family: monospace;">
</textarea>
<div class="mt-4 grid" *ngIf="extractedText !== null">
<div class="col-12 md:col-6">
<h3>Extracted Text Result:</h3>
<textarea pInputTextarea
[autoResize]="true"
[(ngModel)]="extractedText"
readonly
class="w-full"
style="min-height: 300px; width: 100%; border-color: #d1d5db; font-family: monospace;">
</textarea>
<div class="mt-3">
<div class="flex flex-column gap-2 mb-3">
<label>AI Analysis Mode:</label>
<div class="flex align-items-center">
<p-radioButton name="model" value="text" [(ngModel)]="modelType" inputId="mod1"></p-radioButton>
<label for="mod1" class="ml-2">Text Analysis (Fast - Gemma)</label>
</div>
<div class="flex align-items-center">
<p-radioButton name="model" value="vision" [(ngModel)]="modelType" inputId="mod2"></p-radioButton>
<label for="mod2" class="ml-2">Vision Analysis (Accurate - Qwen)</label>
</div>
</div>
<p-button label="Process with AI"
icon="pi pi-bolt"
[loading]="aiLoading"
(onClick)="processWithAI()">
</p-button>
</div>
</div>
<div class="col-12 md:col-6" *ngIf="aiResult">
<div class="flex justify-content-between align-items-center">
<h3>AI Analysis Result:</h3>
<p-button label="Save & Verify" icon="pi pi-check" styleClass="p-button-success" [loading]="saveLoading" (onClick)="saveDocument()"></p-button>
</div>
<p-card class="mb-3">
<div class="grid">
<div class="col-6">
<label class="block text-sm font-bold mb-1">Vendor</label>
<input pInputText [(ngModel)]="aiResult.vendor_name" class="w-full" />
</div>
<div class="col-6">
<label class="block text-sm font-bold mb-1">Date</label>
<input pInputText [(ngModel)]="aiResult.date" class="w-full" />
</div>
<div class="col-6 mt-2">
<label class="block text-sm font-bold mb-1">Invoice #</label>
<input pInputText [(ngModel)]="aiResult.invoice_number" class="w-full" />
</div>
<div class="col-6 mt-2">
<label class="block text-sm font-bold mb-1">Total</label>
<input pInputText [(ngModel)]="aiResult.total_amount" class="w-full" />
</div>
</div>
</p-card>
<p-table [value]="aiResult.line_items" styleClass="p-datatable-sm" [scrollable]="true" scrollHeight="200px">
<ng-template pTemplate="header">
<tr>
<th>Description</th>
<th>Qty</th>
<th>Price</th>
<th>Total</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-item>
<tr>
<td pEditableColumn>
<p-cellEditor>
<ng-template pTemplate="input">
<input pInputText type="text" [(ngModel)]="item.description">
</ng-template>
<ng-template pTemplate="output">
{{item.description}}
</ng-template>
</p-cellEditor>
</td>
<td pEditableColumn>
<p-cellEditor>
<ng-template pTemplate="input">
<input pInputText type="text" [(ngModel)]="item.quantity">
</ng-template>
<ng-template pTemplate="output">
{{item.quantity}}
</ng-template>
</p-cellEditor>
</td>
<td pEditableColumn>
<p-cellEditor>
<ng-template pTemplate="input">
<input pInputText type="text" [(ngModel)]="item.unit_price">
</ng-template>
<ng-template pTemplate="output">
{{item.unit_price}}
</ng-template>
</p-cellEditor>
</td>
<td pEditableColumn>
<p-cellEditor>
<ng-template pTemplate="input">
<input pInputText type="text" [(ngModel)]="item.total">
</ng-template>
<ng-template pTemplate="output">
{{item.total}}
</ng-template>
</p-cellEditor>
</td>
</tr>
</ng-template>
</p-table>
</div>
</div>
<p-toast></p-toast>
</div>
@@ -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'});
}
});
}
}

View File

@@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DepartmentComponent } from './department.component';
describe('DepartmentComponent', () => {
let component: DepartmentComponent;
let fixture: ComponentFixture<DepartmentComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [DepartmentComponent]
}).compileComponents();
fixture = TestBed.createComponent(DepartmentComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -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) => {

View File

@@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DesignationComponent } from './designation.component';
describe('DesignationComponent', () => {
let component: DesignationComponent;
let fixture: ComponentFixture<DesignationComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [DesignationComponent]
}).compileComponents();
fixture = TestBed.createComponent(DesignationComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -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) => {

View File

@@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { EmployeeComponent } from './employee.component';
describe('EmployeeComponent', () => {
let component: EmployeeComponent;
let fixture: ComponentFixture<EmployeeComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [EmployeeComponent]
}).compileComponents();
fixture = TestBed.createComponent(EmployeeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -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) => {

View File

@@ -0,0 +1,5 @@
:host ::ng-deep .p-dialog .product-image {
width: 150px;
margin: 0 auto 2rem auto;
display: block;
}

View File

@@ -30,7 +30,7 @@
<h4 class="m-0">Manage Subsidiaries</h4>
<p-iconfield>
<p-inputicon class="pi pi-search" />
<input pInputText type="text" (input)="onSearch($event)" placeholder="Search..." />
<input pInputText type="text" (input)="dt.filterGlobal($any($event.target).value, 'contains')" placeholder="Search..." />
</p-iconfield>
</div>
</ng-template>

View File

@@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SubsidiaryComponent } from './subsidiary.component';
describe('SubsidiaryComponent', () => {
let component: SubsidiaryComponent;
let fixture: ComponentFixture<SubsidiaryComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [SubsidiaryComponent]
}).compileComponents();
fixture = TestBed.createComponent(SubsidiaryComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -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) => {

View File

@@ -0,0 +1,273 @@
<div class="card">
<p-toast />
<p-toolbar class="mb-6">
<ng-template #start>
<p-button label="New Verifier" icon="pi pi-plus" class="mr-2" (onClick)="openNew()" />
</ng-template>
<ng-template #end>
<p-button label="Export" icon="pi pi-upload" severity="secondary" (onClick)="exportCSV()" styleClass="mx-2" />
<p-button label="Refresh" icon="pi pi-refresh" severity="info" (onClick)="loadAllVerifiers()" />
</ng-template>
</p-toolbar>
<p-table
#dt
[value]="isLoading ? skeletonData : verifiers"
[rows]="5"
[columns]="cols"
[paginator]="true"
[globalFilterFields]="['fullName', 'mobileNo', 'emailId', 'verifierCode']"
[tableStyle]="{ 'min-width': '75rem' }"
[(selection)]="selectedVerifiers"
[rowHover]="true"
dataKey="id"
currentPageReportTemplate="Showing {first} to {last} of {totalRecords} entries"
[showCurrentPageReport]="true"
>
<ng-template #caption>
<div class="flex justify-content-between align-items-center">
<h4 class="m-0">Manage Verifiers</h4>
<p-iconfield>
<p-inputicon class="pi pi-search" />
<input pInputText type="text" (input)="dt.filterGlobal($any($event.target).value, 'contains')" placeholder="Search..." />
</p-iconfield>
</div>
</ng-template>
<ng-template #header>
<tr>
<th style="width: 3rem">#</th>
<th pSortableColumn="fullName">
<div class="flex items-center gap-2">
Full Name
<p-sortIcon field="fullName" />
</div>
</th>
<th pSortableColumn="verifierCode" style="min-width: 10rem">
<div class="flex items-center gap-2">
Verifier Code
<p-sortIcon field="verifierCode" />
</div>
</th>
<th pSortableColumn="verifierType" style="min-width: 10rem">
<div class="flex items-center gap-2">
Type
<p-sortIcon field="verifierType" />
</div>
</th>
<th pSortableColumn="mobileNo" style="min-width: 10rem">
<div class="flex items-center gap-2">
Mobile No
<p-sortIcon field="mobileNo" />
</div>
</th>
<th pSortableColumn="emailId" style="min-width: 10rem">
<div class="flex items-center gap-2">
Email
<p-sortIcon field="emailId" />
</div>
</th>
<th pSortableColumn="updatedAt" style="width: 13rem">
<div class="flex items-center gap-2">
Updated At
<p-sortIcon field="updatedAt" />
</div>
</th>
<th pSortableColumn="updatedUser" style="min-width: 12rem">
<div class="flex items-center gap-2">
Updated By
<p-sortIcon field="updatedUser" />
</div>
</th>
<th pSortableColumn="active" style="width: 4rem">
<div class="flex items-center gap-2">
Status
<p-sortIcon field="active" />
</div>
</th>
<th style="min-width: 8rem"></th>
</tr>
</ng-template>
<ng-template #body let-verifier let-rowIndex="rowIndex">
<tr *ngIf="!isLoading">
<td>{{ rowIndex + 1 }}</td>
<td>{{ verifier.fullName }}</td>
<td>{{ verifier.verifierCode }}</td>
<td>{{ verifier.verifierType }}</td>
<td>{{ verifier.mobileNo }}</td>
<td>{{ verifier.emailId }}</td>
<td>{{ verifier.updatedAt | date:'dd/MM/yyyy HH:mm:ss' }}</td>
<td>
<span *ngIf="verifier.updatedUser; else noUpdatedUser">
{{ verifier.updatedUser }}
</span>
<ng-template #noUpdatedUser>
<em class="text-500 ">Not Available</em>
</ng-template>
</td>
<td>
<p-tag [value]="verifier.active ? 'Active' : 'Inactive'" [severity]="getSeverity(verifier.active)" />
</td>
<td>
<p-button icon="pi pi-pencil" class="mr-2" [rounded]="true" [outlined]="true" (click)="editVerifier(verifier)" />
<p-button [icon]="verifier.active ? 'pi pi-ban' : 'pi pi-check'" [severity]="verifier.active ? 'danger' : 'success'" [rounded]="true" [outlined]="true" (click)="toggleActive(verifier)" />
</td>
</tr>
<tr *ngIf="isLoading">
<td><p-skeleton width="2rem" height="1.5rem"></p-skeleton></td>
<td><p-skeleton width="10rem" height="1.5rem"></p-skeleton></td>
<td><p-skeleton width="8rem" height="1.5rem"></p-skeleton></td>
<td><p-skeleton width="8rem" height="1.5rem"></p-skeleton></td>
<td><p-skeleton width="10rem" height="1.5rem"></p-skeleton></td>
<td><p-skeleton width="10rem" height="1.5rem"></p-skeleton></td>
<td><p-skeleton width="10rem" height="1.5rem"></p-skeleton></td>
<td><p-skeleton width="8rem" height="1.5rem"></p-skeleton></td>
<td><p-skeleton width="4rem" height="1.5rem"></p-skeleton></td>
<td><p-skeleton width="2rem" height="1.5rem"></p-skeleton></td>
</tr>
</ng-template>
</p-table>
<p-dialog [(visible)]="verifierDialog" [style]="{'width': '50vw'}" [breakpoints]="{ '960px': '75vw', '640px': '90vw' }" header="Verifier Details" [modal]="true">
<ng-template #content>
<form [formGroup]="verifierForm" (ngSubmit)="saveVerifier()" class="mt-2">
<div class="grid mt-0">
<!-- Verifier Type -->
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<p-select
id="verifierType"
formControlName="verifierType"
[options]="verifierTypes"
optionLabel="label"
optionValue="value"
fluid
appendTo="body"
[class.p-invalid]="isFieldInvalid('verifierType')"
pTooltip="{{ getErrorMessage('verifierType') }}"
tooltipPosition="top"
(onChange)="onVerifierTypeChange()"
/>
<label for="verifierType">Verifier Type <span class="text-red-500">*</span></label>
</p-floatlabel>
</div>
<!-- Verifier Code -->
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<input
id="verifierCode"
pInputText
formControlName="verifierCode"
fluid
[class.p-invalid]="isFieldInvalid('verifierCode')"
pTooltip="{{ getErrorMessage('verifierCode') }}"
tooltipPosition="top"
/>
<label for="verifierCode">Verifier Code <span class="text-red-500">*</span></label>
</p-floatlabel>
</div>
<!-- Full Name -->
<div class="col-12">
<p-floatlabel variant="on">
<p-autoComplete
*ngIf="verifierForm.get('verifierType')?.value === 'INTERNAL'"
id="fullName"
formControlName="fullName"
[suggestions]="suggestions"
optionLabel="fullName"
[scrollHeight]="'160px'"
(completeMethod)="searchEmployees($event)"
(onSelect)="onSelectEmployee($event)"
fluid
appendTo="body"
[class.p-invalid]="isFieldInvalid('fullName')"
pTooltip="{{ getErrorMessage('fullName') }}"
tooltipPosition="top"
/>
<input
*ngIf="verifierForm.get('verifierType')?.value !== 'INTERNAL'"
id="fullName"
pInputText
formControlName="fullName"
fluid
[class.p-invalid]="isFieldInvalid('fullName')"
pTooltip="{{ getErrorMessage('fullName') }}"
tooltipPosition="top"
/>
<label for="fullName">Full Name <span class="text-red-500">*</span></label>
</p-floatlabel>
</div>
<!-- Mobile No -->
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<input
id="mobileNo"
pInputText
formControlName="mobileNo"
fluid
[class.p-invalid]="isFieldInvalid('mobileNo')"
pTooltip="{{ getErrorMessage('mobileNo') }}"
tooltipPosition="top"
/>
<label for="mobileNo">Mobile No <span class="text-red-500">*</span></label>
</p-floatlabel>
</div>
<!-- Email ID -->
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<input
id="emailId"
pInputText
formControlName="emailId"
fluid
[class.p-invalid]="isFieldInvalid('emailId')"
pTooltip="{{ getErrorMessage('emailId') }}"
tooltipPosition="top"
/>
<label for="emailId">Email ID <span class="text-red-500">*</span></label>
</p-floatlabel>
</div>
<!-- Allocation Date -->
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<p-calendar
id="allocationDate"
formControlName="allocationDate"
dateFormat="dd/mm/yy"
[showIcon]="true"
fluid
appendTo="body"
/>
<label for="allocationDate">Allocation Date</label>
</p-floatlabel>
</div>
<!-- App Active & Photo Mandatory -->
<div class="col-12 md:col-6 flex flex-column md:flex-row md:items-center gap-2 md:gap-4 px-3 align-self-center md:mt-1">
<div class="flex items-center gap-2">
<p-checkbox formControlName="appActive" [binary]="true" inputId="appActive" />
<label for="appActive">App Active</label>
</div>
<div class="flex items-center gap-2">
<p-checkbox formControlName="photoMandatory" [binary]="true" inputId="photoMandatory" />
<label for="photoMandatory">Photo Mandatory</label>
</div>
</div>
</div>
</form>
</ng-template>
<ng-template #footer>
<p-button label="Cancel" icon="pi pi-times" text (click)="hideDialog()" />
<p-button label="Save" icon="pi pi-check" (click)="saveVerifier()" />
</ng-template>
</p-dialog>
<p-confirmDialog [style]="{ width: '450px' }" />
</div>

View File

@@ -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<string>();
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
});
}
});
}
}

View File

@@ -0,0 +1 @@

View File

@@ -1,11 +1,11 @@
<div class="card">
<p-toast />
<p-toolbar class="mb-6">
<ng-template pTemplate="start">
<ng-template #start>
<p-button label="New User" icon="pi pi-plus" class="mr-2" (onClick)="openNew()" />
</ng-template>
<ng-template pTemplate="end">
<ng-template #end>
<p-button label="Export" icon="pi pi-upload" severity="secondary" (onClick)="exportCSV()" styleClass="mx-2" />
<p-button label="Refresh" icon="pi pi-refresh" severity="info" (onClick)="loadAllUsers()" />
</ng-template>
@@ -25,16 +25,16 @@
currentPageReportTemplate="Showing {first} to {last} of {totalRecords} entries"
[showCurrentPageReport]="true"
>
<ng-template pTemplate="caption">
<ng-template #caption>
<div class="flex justify-content-between align-items-center">
<h4 class="m-0">Manage Users</h4>
<span class="p-input-icon-left">
<i class="pi pi-search"></i>
<p-iconfield>
<p-inputicon class="pi pi-search" />
<input pInputText type="text" (input)="onSearch($event)" placeholder="Search..." />
</span>
</p-iconfield>
</div>
</ng-template>
<ng-template pTemplate="header">
<ng-template #header>
<tr>
<th style="width: 3rem">#</th>
<th pSortableColumn="loginId">
@@ -82,7 +82,7 @@
<th style="min-width: 8rem"></th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-user let-rowIndex="rowIndex">
<ng-template #body let-user let-rowIndex="rowIndex">
<tr *ngIf="!isLoading">
<td>{{ rowIndex + 1 }}</td>
<td>{{ user.loginId }}</td>
@@ -130,27 +130,34 @@
</p-table>
<p-dialog [(visible)]="userDialog" [style]="{'width': '50vw'}" [breakpoints]="{ '960px': '75vw', '640px': '90vw' }" header="User Details" [modal]="true">
<ng-template pTemplate="content">
<ng-template #content>
<form [formGroup]="userForm" (ngSubmit)="saveUser()" class="mt-2">
<div class="grid mt-0">
<div class="col-12">
<span class="p-float-label">
<input
id="loginId"
pInputText
formControlName="loginId"
fluid
[readonly]="!!user"
[class.p-invalid]="isFieldInvalid('loginId')"
pTooltip="{{ getErrorMessage('loginId') }}"
tooltipPosition="top"
autofocus
/>
<p-floatlabel variant="on">
<p-iconfield>
<input
id="loginId"
pInputText
formControlName="loginId"
fluid
[readonly]="!!user"
[class.p-invalid]="isFieldInvalid('loginId') || loginIdTaken"
autofocus
(blur)="checkLoginId()"
(input)="onLoginIdInput()"
/>
<p-inputicon class="pi pi-spin pi-spinner" *ngIf="checkingLoginId" />
</p-iconfield>
<label for="loginId">Login ID <span class="text-red-500">*</span></label>
</span>
</p-floatlabel>
<div class="error-msg-container" [class.show-error]="loginIdTaken || isFieldInvalid('loginId')">
<small class="text-red-500 block" *ngIf="loginIdTaken">Login ID is already taken.</small>
<small class="text-red-500 block" *ngIf="isFieldInvalid('loginId') && !loginIdTaken">{{ getErrorMessage('loginId') }}</small>
</div>
</div>
<div class="col-12">
<span class="p-float-label">
<p-floatlabel variant="on">
<input
id="displayName"
pInputText
@@ -161,10 +168,13 @@
tooltipPosition="top"
/>
<label for="displayName">Display Name <span class="text-red-500">*</span></label>
</span>
</p-floatlabel>
<div class="error-msg-container" [class.show-error]="isFieldInvalid('displayName')">
<small class="text-red-500 block" *ngIf="isFieldInvalid('displayName')">{{ getErrorMessage('displayName') }}</small>
</div>
</div>
<div class="col-12">
<span class="p-float-label">
<p-floatlabel variant="on">
<p-autoComplete
id="employeeId"
formControlName="employeeId"
@@ -177,10 +187,10 @@
fluid
/>
<label for="employeeId">Employee ID</label>
</span>
</p-floatlabel>
</div>
<div class="col-12">
<span class="p-float-label">
<p-floatlabel variant="on">
<input
id="employeeName"
pInputText
@@ -189,10 +199,10 @@
readonly
/>
<label for="employeeName">Employee Name</label>
</span>
</p-floatlabel>
</div>
<div class="col-12">
<span class="p-float-label">
<p-floatlabel variant="on">
<input
id="fatherName"
pInputText
@@ -201,10 +211,10 @@
readonly
/>
<label for="fatherName">Father Name</label>
</span>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<span class="p-float-label">
<p-floatlabel variant="on">
<input
id="department"
pInputText
@@ -213,10 +223,10 @@
readonly
/>
<label for="department">Department</label>
</span>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<span class="p-float-label">
<p-floatlabel variant="on">
<input
id="designation"
pInputText
@@ -225,11 +235,11 @@
readonly
/>
<label for="designation">Designation</label>
</span>
</p-floatlabel>
</div>
<div class="col-12 mb-3">
<span class="p-float-label">
<p-dropdown
<p-floatlabel variant="on">
<p-select
id="status"
[options]="statuses"
formControlName="status"
@@ -241,22 +251,25 @@
tooltipPosition="top"
/>
<label for="status">Status <span class="text-red-500">*</span></label>
</span>
</p-floatlabel>
<div class="error-msg-container" [class.show-error]="isFieldInvalid('status')">
<small class="text-red-500 block" *ngIf="isFieldInvalid('status')">{{ getErrorMessage('status') }}</small>
</div>
</div>
</div>
<p-fieldset legend="User Roles" class="mt-4" [style]="{'background-color': 'rgba(243,243,243,0.5)'}">
<div class="grid mb-4 mt-2">
<div class="col-12 md:col-4">
<span class="p-float-label">
<p-dropdown [options]="branches" optionLabel="name" optionValue="id" fluid />
<p-floatlabel variant="on">
<p-select [(ngModel)]="selectedBranch" [ngModelOptions]="{standalone: true}" [options]="branches" optionLabel="branchName" optionValue="id" fluid appendTo="body" />
<label>Branch</label>
</span>
</p-floatlabel>
</div>
<div class="col-12 md:col-4">
<span class="p-float-label">
<p-dropdown [options]="userRolesOptions" optionLabel="name" optionValue="id" fluid />
<p-floatlabel variant="on">
<p-select [(ngModel)]="selectedRole" [ngModelOptions]="{standalone: true}" [options]="userRolesOptions" optionLabel="groupName" optionValue="id" fluid appendTo="body" />
<label>User Role</label>
</span>
</p-floatlabel>
</div>
<div class="col-12 md:col-4">
<p-button label="Add Role" icon="pi pi-plus" (onClick)="addRole()" />
@@ -279,6 +292,7 @@
<td>{{role.groupName}}</td>
<td>{{role.branchName}}</td>
<td>
<p-button [icon]="role.defaultRole ? 'pi pi-star-fill' : 'pi pi-star'" [severity]="role.defaultRole ? 'success' : 'secondary'" [rounded]="true" [outlined]="!role.defaultRole" (onClick)="setDefaultRole(role)" [pTooltip]="role.defaultRole ? 'Default Role' : 'Set as Default'" class="mr-2" />
<p-button [icon]="role.active ? 'pi pi-trash' : 'pi pi-plus'" [severity]="role.active ? 'danger' : 'warn'" [rounded]="true" [outlined]="true" (onClick)="toggleRoleActive(role)" [pTooltip]="role.active ? 'Remove Role' : 'Enable Role'" />
</td>
</tr>
@@ -293,7 +307,7 @@
</form>
</ng-template>
<ng-template pTemplate="footer">
<ng-template #footer>
<p-button label="Cancel" icon="pi pi-times" text (click)="hideDialog()" />
<p-button label="Save" icon="pi pi-check" (click)="saveUser()" />
</ng-template>

View File

@@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserComponent } from './user.component';
describe('UserComponent', () => {
let component: UserComponent;
let fixture: ComponentFixture<UserComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [UserComponent]
}).compileComponents();
fixture = TestBed.createComponent(UserComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -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<string>();
@@ -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);
}

View File

@@ -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: `<div class="p-5">
imports: [MenuComponent, RouterOutlet],
template: `<div>
<app-menu></app-menu>
<div class="dashboard-content">
<router-outlet></router-outlet>
</div>
</div>`
})
export class DashboardComponent {

View File

@@ -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;
}

View File

@@ -0,0 +1,38 @@
<div class="grid align-content-center justify-content-center login-page">
<div>
<form [formGroup]="authForm" (ngSubmit)="onAuthorize()">
<p-card styleClass="login-card shadow-3 w-30rem">
<ng-template pTemplate="header">
<div class="flex flex-column align-items-center gap-3 w-full mt-3">
<img src="assets/images/logo.png" class="w-5rem" />
<span class="text-xl font-bold whitespace-nowrap">
Select Branch
</span>
</div>
</ng-template>
<div class="mt-2">
<p-inputgroup>
<p-inputgroup-addon>
<i class="pi pi-building"></i>
</p-inputgroup-addon>
<p-select [options]="companies" optionLabel="companyName" optionValue="id" formControlName="companyId" [disabled]="isCompanyDisabled" placeholder="Select Company" styleClass="w-full"></p-select>
</p-inputgroup>
</div>
<div class="mt-3">
<p-inputgroup>
<p-inputgroup-addon>
<i class="pi pi-map-marker"></i>
</p-inputgroup-addon>
<p-select [options]="branches" optionLabel="branchName" optionValue="id" formControlName="branchId" [disabled]="!authForm.get('companyId')?.value" placeholder="Select Branch" styleClass="w-full"></p-select>
</p-inputgroup>
</div>
<div class="mt-4 mb-2 text-right">
<button pButton label="Confirm" icon="pi pi-sign-in" [disabled]="!authForm.valid"></button>
</div>
<div class="mt-4 mb-2" *ngIf="message">
<p-message severity="error" variant="outlined" [text]="message"></p-message>
</div>
</p-card>
</form>
</div>
</div>

View File

@@ -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();
}

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,167 @@
<div class="mb-4 p-8 pt-2 full-width align-content-center justify-content-center">
<form [formGroup]="profileForm" (ngSubmit)="onSave()" class="p-3 mt-5">
<p-card class="flex flex-column gap-6">
<div class="flex align-items-center gap-3 p-3 pb-0">
<i class="pi pi-user text-primary text-xl"></i>
<span class="font-semibold text-lg">User Details</span>
</div>
<div class="grid p-5 m-2 mt-0">
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<input
id="username"
pInputText
formControlName="username"
fluid
/>
<label for="username">Username</label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<input
id="displayName"
pInputText
formControlName="displayName"
fluid
/>
<label for="displayName">Display Name</label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<input
id="emailId"
pInputText
formControlName="emailId"
fluid
/>
<label for="emailId">Email ID</label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<input
id="roleName"
pInputText
formControlName="roleName"
fluid
/>
<label for="roleName">Role</label>
</p-floatlabel>
</div>
</div>
<p-divider />
<!-- Employment Details -->
<div class="flex align-items-center gap-3 p-3 pb-0">
<i class="pi pi-briefcase text-primary text-xl"></i>
<span class="font-semibold text-lg">Employment Details</span>
</div>
<div class="grid p-5 m-2 mt-0">
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<input
id="employeeId"
pInputText
formControlName="employeeId"
fluid
/>
<label for="employeeId">Employee ID</label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<input
id="joiningDate"
pInputText
formControlName="joiningDate"
fluid
/>
<label for="joiningDate">Joining Date</label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<input
id="department"
pInputText
formControlName="department"
fluid
/>
<label for="department">Department</label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<input
id="designation"
pInputText
formControlName="designation"
fluid
/>
<label for="designation">Designation</label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<input
id="fatherName"
pInputText
formControlName="fatherName"
fluid
/>
<label for="fatherName">Father Name</label>
</p-floatlabel>
</div>
<div class="col-12 md:col-3">
<p-floatlabel variant="on">
<input
id="gender"
pInputText
formControlName="gender"
fluid
/>
<label for="gender">Gender</label>
</p-floatlabel>
</div>
<div class="col-12 md:col-3">
<p-floatlabel variant="on">
<input
id="dob"
pInputText
formControlName="dob"
fluid
/>
<label for="dob">DOB</label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<input
id="contactNo"
pInputText
formControlName="contactNo"
fluid
/>
<label for="contactNo">Contact No.</label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<input
id="alternateContactNo"
pInputText
formControlName="alternateContactNo"
fluid
/>
<label for="alternateContactNo">Alternate Contact No.</label>
</p-floatlabel>
</div>
</div>
</p-card>
</form>
</div>

View File

@@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProfileComponent } from './profile.component';
describe('ProfileComponent', () => {
let component: ProfileComponent;
let fixture: ComponentFixture<ProfileComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ProfileComponent]
}).compileComponents();
fixture = TestBed.createComponent(ProfileComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -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<UserProfile>);
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{
}
}

View File

@@ -0,0 +1 @@
/* localities.component.css */

View File

@@ -0,0 +1,289 @@
<div class="card">
<p-toast />
<p-toolbar class="mb-6">
<ng-template #start>
<p-button label="Add Locality" icon="pi pi-plus" class="mr-2" (onClick)="openNewLocality()" />
<p-button label="Add Area Group" icon="pi pi-plus" severity="help" class="mr-2" (onClick)="openNewArea()" />
</ng-template>
<ng-template #end>
<p-button label="Export" icon="pi pi-upload" severity="secondary" (onClick)="exportCSV()" styleClass="mx-2" />
<p-button label="Refresh" icon="pi pi-refresh" severity="info" (onClick)="loadLocalities()" />
</ng-template>
</p-toolbar>
<p-table
#dt
[value]="isLoading ? skeletonData : localities"
[rows]="5"
[columns]="cols"
[paginator]="true"
[globalFilterFields]="['localityName', 'areaName', 'adminArea', 'pincode']"
[tableStyle]="{ 'min-width': '75rem' }"
[(selection)]="selectedLocalities"
[rowHover]="true"
dataKey="id"
currentPageReportTemplate="Showing {first} to {last} of {totalRecords} entries"
[showCurrentPageReport]="true"
>
<ng-template #caption>
<div class="flex justify-content-between align-items-center">
<h4 class="m-0">Manage Localities</h4>
<p-iconfield>
<p-inputicon class="pi pi-search" />
<input pInputText type="text" (input)="dt.filterGlobal($any($event.target).value, 'contains')" placeholder="Search..." />
</p-iconfield>
</div>
</ng-template>
<ng-template #header>
<tr>
<th style="width: 3rem">#</th>
<th pSortableColumn="localityName">
<div class="flex items-center gap-2">Locality Name <p-sortIcon field="localityName" /></div>
</th>
<th pSortableColumn="adminArea">
<div class="flex items-center gap-2">Admin Area <p-sortIcon field="adminArea" /></div>
</th>
<th pSortableColumn="pincode">
<div class="flex items-center gap-2">Pincode <p-sortIcon field="pincode" /></div>
</th>
<th pSortableColumn="areaName">
<div class="flex items-center gap-2">Area <p-sortIcon field="areaName" /></div>
</th>
<th pSortableColumn="updatedAt">
<div class="flex items-center gap-2">Updated At <p-sortIcon field="updatedAt" /></div>
</th>
<th pSortableColumn="updatedUser">
<div class="flex items-center gap-2">Updated By <p-sortIcon field="updatedUser" /></div>
</th>
<th pSortableColumn="active" style="width: 4rem">
<div class="flex items-center gap-2">Status <p-sortIcon field="active" /></div>
</th>
<th style="min-width: 8rem"></th>
</tr>
</ng-template>
<ng-template #body let-row let-rowIndex="rowIndex">
<tr *ngIf="!isLoading">
<td>{{ rowIndex + 1 }}</td>
<td>{{ row.localityName }}</td>
<td>{{ row.adminArea }}<span *ngIf="row.masterStateName">, {{ row.masterStateName }}</span></td>
<td>{{ row.pincode }}</td>
<td>{{ row.areaName }}</td>
<td>{{ row.updatedAt | date:'dd/MM/yyyy HH:mm:ss' }}</td>
<td><span *ngIf="row.updatedUser; else noUser">{{ row.updatedUser }}</span><ng-template #noUser><em>N/A</em></ng-template></td>
<td><p-tag [value]="row.active ? 'Active' : 'Inactive'" [severity]="getSeverity(row.active)" /></td>
<td>
<p-button icon="pi pi-pencil" class="mr-2" [rounded]="true" [outlined]="true" (click)="editLocality(row)" />
</td>
</tr>
<tr *ngIf="isLoading">
<td><p-skeleton width="2rem" height="1.5rem"></p-skeleton></td>
<td><p-skeleton width="10rem" height="1.5rem"></p-skeleton></td>
<td><p-skeleton width="8rem" height="1.5rem"></p-skeleton></td>
<td><p-skeleton width="8rem" height="1.5rem"></p-skeleton></td>
<td><p-skeleton width="6rem" height="1.5rem"></p-skeleton></td>
<td><p-skeleton width="10rem" height="1.5rem"></p-skeleton></td>
<td><p-skeleton width="8rem" height="1.5rem"></p-skeleton></td>
<td><p-skeleton width="4rem" height="1.5rem"></p-skeleton></td>
<td><p-skeleton width="2rem" height="1.5rem"></p-skeleton></td>
</tr>
</ng-template>
</p-table>
<!-- LOCALITY DIALOG -->
<p-dialog [(visible)]="localityDialog" [style]="{'width': '50vw'}" [breakpoints]="{ '960px': '75vw', '640px': '90vw' }" header="Locality Details" [modal]="true">
<ng-template #content>
<form [formGroup]="localityForm" class="mt-2">
<div class="grid mt-0">
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<p-select
id="areaId"
formControlName="areaId"
[options]="areas"
optionLabel="areaName"
optionValue="id"
[filter]="true"
filterBy="areaName"
fluid
appendTo="body"
[class.p-invalid]="isLocalityFieldInvalid('areaId')"
/>
<label for="areaId">Search Area <span class="text-red-500">*</span></label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<p-autoComplete id="cityAutoComplete" formControlName="cityAutoComplete" [suggestions]="citySuggestions" optionLabel="display" (completeMethod)="searchCities($event)" (onSelect)="onSelectCity($event)" fluid appendTo="body" [class.p-invalid]="isLocalityFieldInvalid('cityAutoComplete')" />
<label for="cityAutoComplete">Search City <span class="text-red-500">*</span></label>
</p-floatlabel>
</div>
<div class="col-12">
<p-floatlabel variant="on">
<input id="localityName" #localityNameInput pInputText formControlName="localityName" fluid [class.p-invalid]="isLocalityFieldInvalid('localityName')" />
<label for="localityName">Locality Name <span class="text-red-500">*</span></label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<input id="adminArea" pInputText formControlName="adminArea" fluid [class.p-invalid]="isLocalityFieldInvalid('adminArea')" />
<label for="adminArea">Area <span class="text-red-500">*</span></label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<p-select
id="localityTypeId"
formControlName="localityTypeId"
[options]="localityTypes"
optionLabel="type"
optionValue="id"
[filter]="true"
filterBy="type"
fluid
appendTo="body"
[class.p-invalid]="isLocalityFieldInvalid('localityTypeId')"
/>
<label for="localityTypeId">Locality Type <span class="text-red-500">*</span></label>
</p-floatlabel>
</div>
<div class="col-12">
<p-floatlabel variant="on">
<p-autoComplete id="olaLocalityAutoComplete" formControlName="olaLocalityAutoComplete" [suggestions]="olaSuggestions" optionLabel="name" (completeMethod)="searchOlaLocalities($event)" (onSelect)="onSelectOlaLocality($event)" fluid appendTo="body" />
<label for="olaLocalityAutoComplete">Tag Locality (Ola)</label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<input id="stateName" pInputText formControlName="stateName" fluid />
<label for="stateName">State</label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<input id="pincode" pInputText formControlName="pincode" fluid />
<label for="pincode">Pincode</label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<input id="latitude" pInputText formControlName="latitude" fluid />
<label for="latitude">Latitude</label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<input id="longitude" pInputText formControlName="longitude" fluid />
<label for="longitude">Longitude</label>
</p-floatlabel>
</div>
</div>
</form>
</ng-template>
<ng-template #footer>
<div class="flex justify-content-between w-full">
<div>
<p-button *ngIf="!localityForm.get('id')?.value" label="Clear" icon="pi pi-eraser" severity="secondary" text (click)="clearLocalityForm()" />
</div>
<div>
<p-button label="Cancel" icon="pi pi-times" text (click)="hideLocalityDialog()" />
<p-button label="Save" icon="pi pi-check" (click)="saveLocality()" />
</div>
</div>
</ng-template>
</p-dialog>
<!-- AREA DIALOG -->
<p-dialog [(visible)]="areaDialog" [style]="{'width': '50vw'}" [breakpoints]="{ '960px': '75vw', '640px': '90vw' }" header="Area Group Details" [modal]="true">
<ng-template #content>
<form [formGroup]="areaForm" class="mt-2">
<div class="grid mt-0">
<div class="col-12">
<p-floatlabel variant="on">
<p-autoComplete id="areaName" formControlName="areaName" [suggestions]="areaSuggestions" optionLabel="areaName" (completeMethod)="searchAreas($event)" (onSelect)="onSelectAreaGroup($event)" fluid appendTo="body" [class.p-invalid]="isAreaFieldInvalid('areaName')" />
<label for="areaName">Area Group Name (Search & Select or Type New) <span class="text-red-500">*</span></label>
</p-floatlabel>
</div>
<div class="col-12 md:col-4">
<p-floatlabel variant="on">
<p-select
id="groupAVerifierId"
formControlName="groupAVerifierId"
[options]="verifiers"
optionLabel="display"
optionValue="id"
[filter]="true"
filterBy="display"
fluid
appendTo="body"
[class.p-invalid]="isAreaFieldInvalid('groupAVerifierId')"
pTooltip="{{ getAreaErrorMessage('groupAVerifierId') }}"
tooltipPosition="top"
/>
<label for="groupAVerifierId">Group A Verifier <span class="text-red-500">*</span></label>
</p-floatlabel>
</div>
<div class="col-12 md:col-4">
<p-floatlabel variant="on">
<p-select
id="groupBVerifierId"
formControlName="groupBVerifierId"
[options]="verifiers"
optionLabel="display"
optionValue="id"
[filter]="true"
filterBy="display"
fluid
appendTo="body"
[class.p-invalid]="isAreaFieldInvalid('groupBVerifierId')"
pTooltip="{{ getAreaErrorMessage('groupBVerifierId') }}"
tooltipPosition="top"
/>
<label for="groupBVerifierId">Group B Verifier <span class="text-red-500">*</span></label>
</p-floatlabel>
</div>
<div class="col-12 md:col-4">
<p-floatlabel variant="on">
<p-select
id="groupCVerifierId"
formControlName="groupCVerifierId"
[options]="verifiers"
optionLabel="display"
optionValue="id"
[filter]="true"
filterBy="display"
fluid
appendTo="body"
[class.p-invalid]="isAreaFieldInvalid('groupCVerifierId')"
pTooltip="{{ getAreaErrorMessage('groupCVerifierId') }}"
tooltipPosition="top"
/>
<label for="groupCVerifierId">Group C Verifier <span class="text-red-500">*</span></label>
</p-floatlabel>
</div>
</div>
</form>
</ng-template>
<ng-template #footer>
<p-button label="Cancel" icon="pi pi-times" text (click)="hideAreaDialog()" />
<p-button label="Save" icon="pi pi-check" (click)="saveArea()" />
</ng-template>
</p-dialog>
<p-confirmDialog [style]="{ width: '450px' }" />
</div>

View File

@@ -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<string>();
private searchCitySubject = new Subject<string>();
private searchOlaSubject = new Subject<string>();
@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 });
}
});
}
}

View File

@@ -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<SubsidiaryDTO[]> {
return this.http.get(`${environment.accountService}/company/subsidiaries`).pipe(
map((response: ResponseDto) => response.data as SubsidiaryDTO[])
);
}
saveSubsidiary(subsidiary: SubsidiaryDTO): Observable<SubsidiaryDTO> {
const requestPayload: Request<SubsidiaryDTO> = {
data: subsidiary,
compressed: true,
target: 'cygnus.models.account.Subsidiary'
};
return this.http.post<ResponseDto>(`${environment.accountService}/company/subsidiaries`, requestPayload).pipe(
map((response: ResponseDto) => response.data as SubsidiaryDTO)
);
}
activateDeactivateSubsidiary(subsidiaryId: string, active: boolean): Observable<SubsidiaryDTO> {
const subsidiary: SubsidiaryDTO = ({
id: subsidiaryId,
active: active
});
const requestPayload: Request<SubsidiaryDTO> = {
scopes: [active ? "ACTIVATE" : "DEACTIVATE"],
data: subsidiary,
compressed: true,
target: 'cygnus.models.account.Subsidiary'
};
return this.http.post<ResponseDto>(`${environment.accountService}/company/subsidiaries`, requestPayload).pipe(
map((response: ResponseDto) => response.data as SubsidiaryDTO)
);
}
// Departments
getAllDepartments(): Observable<DepartmentDTO[]> {
return this.http.get(`${environment.accountService}/company/departments`).pipe(
map((response: ResponseDto) => response.data as DepartmentDTO[])
);
}
saveDepartment(department: DepartmentDTO): Observable<DepartmentDTO> {
const requestPayload: Request<DepartmentDTO> = {
data: department,
compressed: true,
target: 'cygnus.models.account.Department'
};
return this.http.post<ResponseDto>(`${environment.accountService}/company/departments`, requestPayload).pipe(
map((response: ResponseDto) => response.data as DepartmentDTO)
);
}
activateDeactivateDepartment(departmentId: string, active: boolean): Observable<DepartmentDTO> {
const department: DepartmentDTO = ({
id: departmentId,
active: active
} as DepartmentDTO);
const requestPayload: Request<DepartmentDTO> = {
scopes: [active ? "ACTIVATE" : "DEACTIVATE"],
data: department,
compressed: true,
target: 'cygnus.models.account.Department'
};
return this.http.post<ResponseDto>(`${environment.accountService}/company/departments`, requestPayload).pipe(
map((response: ResponseDto) => response.data as DepartmentDTO)
);
}
// Designations
getAllDesignations(): Observable<DesignationDTO[]> {
return this.http.get(`${environment.accountService}/company/designations`).pipe(
map((response: ResponseDto) => response.data as DesignationDTO[])
);
}
saveDesignation(designation: DesignationDTO): Observable<DesignationDTO> {
const requestPayload: Request<DesignationDTO> = {
data: designation,
compressed: true,
target: 'cygnus.models.account.Designation'
};
return this.http.post<ResponseDto>(`${environment.accountService}/company/designations`, requestPayload).pipe(
map((response: ResponseDto) => response.data as DesignationDTO)
);
}
activateDeactivateDesignation(designationId: string, active: boolean): Observable<DesignationDTO> {
const designation: DesignationDTO = ({
id: designationId,
active: active
} as DesignationDTO);
const requestPayload: Request<DesignationDTO> = {
scopes: [active ? "ACTIVATE" : "DEACTIVATE"],
data: designation,
compressed: true,
target: 'cygnus.models.account.Designation'
};
return this.http.post<ResponseDto>(`${environment.accountService}/company/designations`, requestPayload).pipe(
map((response: ResponseDto) => response.data as DesignationDTO)
);
}
// Employees
getAllEmployees(): Observable<EmployeeDTO[]> {
return this.http.get(`${environment.accountService}/company/employees`).pipe(
map((response: ResponseDto) => response.data as EmployeeDTO[])
);
}
saveEmployee(employee: EmployeeDTO): Observable<EmployeeDTO> {
const requestPayload: Request<EmployeeDTO> = {
data: employee,
compressed: true,
target: 'cygnus.models.account.Employee'
};
return this.http.post<ResponseDto>(`${environment.accountService}/company/employees`, requestPayload).pipe(
map((response: ResponseDto) => response.data as EmployeeDTO)
);
}
activateDeactivateEmployee(employeeId: string, active: boolean): Observable<EmployeeDTO> {
const employee: EmployeeDTO = ({
id: employeeId,
active: active
} as EmployeeDTO);
const requestPayload: Request<EmployeeDTO> = {
scopes: [active ? "ACTIVATE" : "DEACTIVATE"],
data: employee,
compressed: true,
target: 'cygnus.models.account.Employee'
};
return this.http.post<ResponseDto>(`${environment.accountService}/company/employees`, requestPayload).pipe(
map((response: ResponseDto) => response.data as EmployeeDTO)
);
}
searchEmployees(payload: Request): Observable<any[]> {
return this.http.post<ResponseDto>(`${environment.accountService}/company/employees/search`, payload).pipe(
map((response: ResponseDto) => response.data as any[])
);
}
// Verifiers
getAllVerifiers(): Observable<CompanyVerifierModel[]> {
return this.http.get(`${environment.accountService}/company/verifiers`).pipe(
map((response: ResponseDto) => response.data as CompanyVerifierModel[])
);
}
saveVerifier(verifier: CompanyVerifierModel): Observable<CompanyVerifierModel> {
const requestPayload: Request<CompanyVerifierModel> = {
data: verifier,
compressed: true,
target: 'cygnus.models.account.CompanyVerifier'
};
return this.http.post<ResponseDto>(`${environment.accountService}/company/verifiers`, requestPayload).pipe(
map((response: ResponseDto) => response.data as CompanyVerifierModel)
);
}
activateDeactivateVerifier(verifierId: string, active: boolean): Observable<CompanyVerifierModel> {
const verifier: any = ({
id: verifierId,
active: active
});
const requestPayload: Request<CompanyVerifierModel> = {
scopes: [active ? "ACTIVATE" : "DEACTIVATE"],
data: verifier,
compressed: true,
target: 'cygnus.models.account.CompanyVerifier'
};
return this.http.post<ResponseDto>(`${environment.accountService}/company/verifiers`, requestPayload).pipe(
map((response: ResponseDto) => response.data as CompanyVerifierModel)
);
}
searchVerifiers(payload: Request): Observable<any[]> {
return this.http.post<ResponseDto>(`${environment.accountService}/company/verifiers/search`, payload).pipe(
map((response: ResponseDto) => response.data as any[])
);
}
}

View File

@@ -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<UserDTO[]> {
return this.http.get(`${environment.userService}/users`).pipe(
map((response: ResponseDto) => response.data as UserDTO[])
);
}
searchUser(payload: Request<any>): Observable<UserDTO> {
return this.http.post<ResponseDto>(`${environment.userService}/users/search`, payload).pipe(
map((response: ResponseDto) => response.data as UserDTO)
);
}
getAllRoles(): Observable<any[]> {
return this.http.get(`${environment.userService}/roles`).pipe(
map((response: any) => response.data as any[])
);
}
getAllBranches(): Observable<any[]> {
return this.http.get(`${environment.userService}/branches`).pipe(
map((response: any) => response.data as any[])
);
}
saveUser(user: UserDTO): Observable<UserDTO> {
const requestPayload: Request<UserDTO> = {
data: user,
compressed: true,
target: 'cygnus.models.user.User'
};
return this.http.post<ResponseDto>(`${environment.userService}/users`, requestPayload).pipe(
map((response: ResponseDto) => response.data as UserDTO)
);
}
activateDeactivateUser(userId: string, active: boolean): Observable<UserDTO> {
const user: UserDTO = {
id: userId,
active: active
};
const requestPayload: Request<UserDTO> = {
scopes: [active ? "ACTIVATE" : "DEACTIVATE"],
data: user,
compressed: true,
target: 'cygnus.models.user.User'
};
return this.http.post<ResponseDto>(`${environment.userService}/users`, requestPayload).pipe(
map((response: ResponseDto) => response.data as UserDTO)
);
}
}

View File

@@ -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<T>(key: string, value: T): void {
try {
sessionStorage.setItem(key, JSON.stringify(value));
} catch (e) {
console.error(`Error saving ${key} to sessionStorage`, e);
}
}
getDetails<T>(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(['/']);
}
}

View File

@@ -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<ResponseDto> {
const options = {
headers,
withCredentials: true
};
return this.http.get<ResponseDto>(url, options).pipe(
catchError(this.handleError)
);
}
post<ResponseDto>(url: string, body: any, headers?: HttpHeaders): Observable<ResponseDto> {
const options = {
headers,
withCredentials: true
};
return this.http.post<ResponseDto>(url, body, options).pipe(
catchError(this.handleError)
);
}
put<T>(url: string, body: any, headers?: HttpHeaders): Observable<T> {
return this.http.put<T>(url, body, { headers }).pipe(
catchError(this.handleError)
);
}
delete<T>(url: string, headers?: HttpHeaders): Observable<T> {
return this.http.delete<T>(url, { headers }).pipe(
catchError(this.handleError)
);
}
private handleError(error: HttpErrorResponse) {
return throwError(() => error.error);
}
setHeaders(additionalHeaders: { [key: string]: string }): HttpHeaders {
return new HttpHeaders({
...additionalHeaders
});
}
}

View File

@@ -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<CityDTO[]> {
return this.http.post<ResponseDto>(`${environment.masterService}/cities-states/search`, payload).pipe(
map((response: ResponseDto) => response.data as CityDTO[])
);
}
}

View File

@@ -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<any> {
// 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)
);
}
}

View File

@@ -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<AreaDTO[]> {
return this.http.get(`${environment.toolsService}/areas`).pipe(
map((response: ResponseDto) => response.data as AreaDTO[])
);
}
saveArea(area: AreaDTO): Observable<AreaDTO> {
const requestPayload: Request<AreaDTO> = {
data: area,
compressed: false,
target: 'cygnus.models.tools.Area'
};
return this.http.post<ResponseDto>(`${environment.toolsService}/areas`, requestPayload).pipe(
map((response: ResponseDto) => response.data as AreaDTO)
);
}
searchAreas(payload: Request): Observable<any[]> {
return this.http.post<ResponseDto>(`${environment.toolsService}/areas/search`, payload).pipe(
map((response: ResponseDto) => response.data as any[])
);
}
// Localities
getAllLocalities(): Observable<LocalityDTO[]> {
return this.http.get(`${environment.toolsService}/localities`).pipe(
map((response: ResponseDto) => response.data as LocalityDTO[])
);
}
saveLocality(locality: LocalityDTO): Observable<LocalityDTO> {
const requestPayload: Request<LocalityDTO> = {
data: locality,
compressed: false,
target: 'cygnus.models.tools.Locality'
};
return this.http.post<ResponseDto>(`${environment.toolsService}/localities`, requestPayload).pipe(
map((response: ResponseDto) => response.data as LocalityDTO)
);
}
searchLocalities(payload: Request): Observable<any[]> {
return this.http.post<ResponseDto>(`${environment.toolsService}/localities/search`, payload).pipe(
map((response: ResponseDto) => response.data as any[])
);
}
// Locality Types
getAllLocalityTypes(): Observable<LocalityTypeDTO[]> {
return this.http.get(`${environment.toolsService}/locality-types`).pipe(
map((response: ResponseDto) => response.data as LocalityTypeDTO[])
);
}
}

View File

@@ -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<string> {
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<any> {
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));
}
}

View File

@@ -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<CryptoKey> {
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<string> {
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)));
}
}

View File

@@ -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';
}
}

View File

@@ -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`
};

View File

@@ -3,7 +3,7 @@
{
"compileOnSave": false,
"compilerOptions": {
"strict": true,
"strict": false,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,