Merged
This commit is contained in:
BIN
backend/uploads/Invoice For Jan-Feb-2026.pdf
Normal file
BIN
backend/uploads/Invoice For Jan-Feb-2026.pdf
Normal file
Binary file not shown.
BIN
backend/uploads/Invoice For Jan-Feb-2026.pdf.jpg
Normal file
BIN
backend/uploads/Invoice For Jan-Feb-2026.pdf.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 228 KiB |
1
frontend/proxy.conf.json
Normal file
1
frontend/proxy.conf.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
BIN
frontend/public/assets/images/logo.png
Normal file
BIN
frontend/public/assets/images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -1,6 +1,6 @@
|
|||||||
import { Routes } from '@angular/router';
|
import { Routes } from '@angular/router';
|
||||||
import { LoginComponent } from './login/login.component';
|
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 { OcrComponent } from './ocr/ocr.component';
|
||||||
import { MailboxComponent } from './mailbox/mailbox.component';
|
import { MailboxComponent } from './mailbox/mailbox.component';
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ export const routes: Routes = [
|
|||||||
{ path: '', component: LoginComponent },
|
{ path: '', component: LoginComponent },
|
||||||
{ path: 'authorize', component: AuthorizeComponent, canActivate: [AuthorizeGuard]},
|
{ path: 'authorize', component: AuthorizeComponent, canActivate: [AuthorizeGuard]},
|
||||||
{ path: 'user',
|
{ path: 'user',
|
||||||
component: AdminLayoutComponent,
|
component: DashboardComponent,
|
||||||
canActivate: [AuthorizeGuard],
|
canActivate: [AuthorizeGuard],
|
||||||
canActivateChild: [AuthorizeGuard],
|
canActivateChild: [AuthorizeGuard],
|
||||||
children: [
|
children: [
|
||||||
@@ -27,7 +27,7 @@ export const routes: Routes = [
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{ path: 'account',
|
{ path: 'account',
|
||||||
component: AdminLayoutComponent,
|
component: DashboardComponent,
|
||||||
canActivate: [AuthorizeGuard],
|
canActivate: [AuthorizeGuard],
|
||||||
canActivateChild: [AuthorizeGuard],
|
canActivateChild: [AuthorizeGuard],
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
21
frontend/src/app/fragments/menu/menu.component.html
Normal file
21
frontend/src/app/fragments/menu/menu.component.html
Normal 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>
|
||||||
@@ -1,23 +1,22 @@
|
|||||||
import { Component, Input, OnInit } from '@angular/core';
|
import { Component, Input } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { MenuItem } from 'primeng/api';
|
import { MenuItem } from 'primeng/api';
|
||||||
import { MenubarModule } from 'primeng/menubar';
|
import { Menubar } from 'primeng/menubar';
|
||||||
import { HttpService } from '../../services/http.service';
|
import { HttpService } from '../../services/http.service';
|
||||||
import { Request } from '../../models/request.model';
|
import { Request } from '../../models/request.model';
|
||||||
import { AvatarModule } from 'primeng/avatar';
|
import { AvatarModule } from 'primeng/avatar';
|
||||||
import { MenuModule } from 'primeng/menu';
|
import { Menu } from 'primeng/menu';
|
||||||
import { ButtonModule } from 'primeng/button';
|
import { ButtonModule } from 'primeng/button';
|
||||||
import { InputTextModule } from 'primeng/inputtext';
|
import { InputTextModule } from 'primeng/inputtext';
|
||||||
import { SessionService } from '../../services/commons/session.service';
|
import { SessionService } from '../../services/commons/session.service';
|
||||||
import { EncryptionService } from '../../services/utilities/encryption.service';
|
import { EncryptionService } from '../../services/utilities/encryption.service';
|
||||||
import { TooltipModule } from 'primeng/tooltip';
|
import { TooltipModule } from 'primeng/tooltip';
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-menu',
|
selector: 'app-menu',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, MenubarModule, AvatarModule, ButtonModule, MenuModule, InputTextModule, TooltipModule],
|
imports: [Menubar, AvatarModule, ButtonModule, Menu, InputTextModule, TooltipModule],
|
||||||
templateUrl: './menu.component.html'
|
templateUrl: './menu.component.html'
|
||||||
})
|
})
|
||||||
export class MenuComponent {
|
export class MenuComponent {
|
||||||
@@ -106,6 +105,6 @@ export class MenuComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
gotToDashboard() {
|
gotToDashboard() {
|
||||||
this.router.navigate(['/user']);
|
this.router.navigate(['/account']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -24,13 +24,10 @@ export const AuthInterceptor: HttpInterceptorFn = (
|
|||||||
/* -----------------------------------------
|
/* -----------------------------------------
|
||||||
* 1️⃣ Always attach Authorization header
|
* 1️⃣ Always attach Authorization header
|
||||||
* ----------------------------------------- */
|
* ----------------------------------------- */
|
||||||
// OCR uses sessionStorage for token now (migrated previously)
|
const token = sessionService.getItem('token');
|
||||||
const token = sessionStorage.getItem('token');
|
|
||||||
|
|
||||||
let reqHeaders = req.headers;
|
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)) {
|
if (!(req.body instanceof FormData)) {
|
||||||
reqHeaders = reqHeaders.set('Content-Type', 'application/json');
|
reqHeaders = reqHeaders.set('Content-Type', 'application/json');
|
||||||
}
|
}
|
||||||
@@ -65,9 +62,8 @@ export const AuthInterceptor: HttpInterceptorFn = (
|
|||||||
headers: reqHeaders
|
headers: reqHeaders
|
||||||
});
|
});
|
||||||
|
|
||||||
// Use 'any' type for event to avoid strict type mismatch if ResponseDto structure varies
|
|
||||||
return next(modifiedReq).pipe(
|
return next(modifiedReq).pipe(
|
||||||
tap((event: any) => handleResponse(event, sessionService, encryptionService)),
|
tap(event => handleResponse(event, sessionService, encryptionService)),
|
||||||
catchError((error: HttpErrorResponse) =>
|
catchError((error: HttpErrorResponse) =>
|
||||||
throwError(() => error)
|
throwError(() => error)
|
||||||
)
|
)
|
||||||
@@ -84,7 +80,7 @@ export const AuthInterceptor: HttpInterceptorFn = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
return next(modifiedReq).pipe(
|
return next(modifiedReq).pipe(
|
||||||
tap((event: any) => handleResponse(event, sessionService, encryptionService)),
|
tap(event => handleResponse(event, sessionService, encryptionService)),
|
||||||
catchError((error: HttpErrorResponse) =>
|
catchError((error: HttpErrorResponse) =>
|
||||||
throwError(() => error)
|
throwError(() => error)
|
||||||
)
|
)
|
||||||
@@ -101,45 +97,33 @@ function handleResponse(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle Authenticate Response
|
if (event.url?.endsWith('/3z4mkell5g5aset/authenticate')) {
|
||||||
if (event.url?.endsWith('/authenticate')) {
|
|
||||||
const response = event.body as ResponseDto;
|
const response = event.body as ResponseDto;
|
||||||
if (response?.data?.token) {
|
sessionService.setItem('token', response?.data?.token);
|
||||||
sessionStorage.setItem('token', response.data.token);
|
sessionService.setItem('companies', response?.data?.companies);
|
||||||
}
|
|
||||||
if (response?.data?.companies) {
|
|
||||||
sessionService.setItem('companies', response.data.companies);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle Authorize Response
|
if (event.url?.endsWith('/3z4mkell5g5aset/authorize')) {
|
||||||
if (event.url?.endsWith('/authorize')) {
|
|
||||||
const response = event.body as ResponseDto;
|
const response = event.body as ResponseDto;
|
||||||
// Clearing session might be too aggressive if we just logged in, but following source logic
|
sessionService.clear();
|
||||||
// sessionService.clear();
|
sessionService.setItem('token', response?.data?. token);
|
||||||
|
sessionService.setItem('refreshToken', response?.data?.refreshToken);
|
||||||
if (response?.data?.token) {
|
sessionService.setItem('userDetails', JSON.stringify(response?.data?.userDetails));
|
||||||
sessionStorage.setItem('token', response.data.token);
|
encryptionService.encrypt(response?.data?.data)
|
||||||
}
|
|
||||||
if (response?.data?.refreshToken) {
|
|
||||||
sessionService.setItem('refreshToken', response.data.refreshToken);
|
|
||||||
}
|
|
||||||
if (response?.data?.userDetails) {
|
|
||||||
sessionService.setItem('userDetails', response.data.userDetails);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response?.data?.data) {
|
|
||||||
encryptionService.encrypt(response.data.data)
|
|
||||||
.then(encryptedNav => {
|
.then(encryptedNav => {
|
||||||
sessionService.setItem('nav', encryptedNav);
|
sessionService.setItem('nav', encryptedNav);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (response?.data?.companies) {
|
if (response?.data?.userDetails) {
|
||||||
sessionService.setItem(
|
sessionService.setItem(
|
||||||
'companies',
|
'userDetails',
|
||||||
response.data.companies
|
JSON.stringify(response.data.userDetails)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sessionService.setItem(
|
||||||
|
'companies',
|
||||||
|
JSON.stringify(response?.data?.companies)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
40
frontend/src/app/interceptors/authorize.guard.ts
Normal file
40
frontend/src/app/interceptors/authorize.guard.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
43
frontend/src/app/login/login.component.css
Normal file
43
frontend/src/app/login/login.component.css
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
50
frontend/src/app/login/login.component.html
Normal file
50
frontend/src/app/login/login.component.html
Normal 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>
|
||||||
@@ -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 { FormsModule } from '@angular/forms';
|
||||||
import { AuthService } from '../auth.service';
|
import { Component } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { ButtonModule } from 'primeng/button';;
|
||||||
import { MessageService } from 'primeng/api';
|
|
||||||
|
|
||||||
// PrimeNG Imports
|
|
||||||
import { CardModule } from 'primeng/card';
|
|
||||||
import { InputTextModule } from 'primeng/inputtext';
|
import { InputTextModule } from 'primeng/inputtext';
|
||||||
import { ButtonModule } from 'primeng/button';
|
import { CardModule } from 'primeng/card';
|
||||||
import { ToastModule } from 'primeng/toast';
|
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({
|
@Component({
|
||||||
selector: 'app-login',
|
selector: 'app-login',
|
||||||
standalone: true,
|
templateUrl: './login.component.html',
|
||||||
imports: [
|
styleUrls: ['./login.component.css'],
|
||||||
CommonModule,
|
imports: [CommonModule, FormsModule, CardModule, ButtonModule, InputTextModule, InputGroupModule, InputGroupAddonModule, PasswordModule,
|
||||||
FormsModule,
|
FloatLabelModule, ReactiveFormsModule, KeyFilterModule, MessageModule, StyleClassModule, BadgeModule]
|
||||||
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; }
|
|
||||||
`]
|
|
||||||
})
|
})
|
||||||
export class LoginComponent {
|
export class LoginComponent {
|
||||||
username = '';
|
loginForm: FormGroup;
|
||||||
password = '';
|
message?: string;
|
||||||
|
|
||||||
constructor(private auth: AuthService, private router: Router, private messageService: MessageService) {}
|
constructor(private fb: FormBuilder, private http: HttpService, private router: Router) {
|
||||||
|
this.loginForm = this.fb.group({
|
||||||
|
username: ['', Validators.required],
|
||||||
|
password: ['', Validators.required]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onLogin() {
|
onLogin() {
|
||||||
this.auth.login(this.username, this.password).subscribe({
|
this.message = '';
|
||||||
next: () => {
|
if (this.loginForm.valid) {
|
||||||
this.router.navigate(['/']);
|
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: () => {
|
error: (err) => {
|
||||||
this.messageService.add({severity:'error', summary:'Error', detail:'Invalid Credentials'});
|
this.message = err.message;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
<p>login works!</p>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-login',
|
|
||||||
imports: [],
|
|
||||||
templateUrl: './login.html',
|
|
||||||
styleUrl: './login.scss',
|
|
||||||
})
|
|
||||||
export class Login {
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -43,6 +43,28 @@ export interface DepartmentDTO {
|
|||||||
active: boolean;
|
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 {
|
export interface DesignationDTO {
|
||||||
id: string;
|
id: string;
|
||||||
companyId: string;
|
companyId: string;
|
||||||
|
|||||||
16
frontend/src/app/models/masters/masters.ts
Normal file
16
frontend/src/app/models/masters/masters.ts
Normal 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
|
||||||
|
}
|
||||||
16
frontend/src/app/models/masters/member/memberlist.ts
Normal file
16
frontend/src/app/models/masters/member/memberlist.ts
Normal 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
|
||||||
|
}
|
||||||
7
frontend/src/app/models/ola.model.ts
Normal file
7
frontend/src/app/models/ola.model.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export interface LocationDTO {
|
||||||
|
place_id?: string;
|
||||||
|
name?: string;
|
||||||
|
formatted_address?: string;
|
||||||
|
lat?: number;
|
||||||
|
lng?: number;
|
||||||
|
}
|
||||||
6
frontend/src/app/models/request.model.ts
Normal file
6
frontend/src/app/models/request.model.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export interface Request<T = any> {
|
||||||
|
scopes?: string[];
|
||||||
|
data: T;
|
||||||
|
target?: string;
|
||||||
|
compressed?: boolean;
|
||||||
|
}
|
||||||
6
frontend/src/app/models/response.dto.ts
Normal file
6
frontend/src/app/models/response.dto.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export interface ResponseDto {
|
||||||
|
statuscode : number;
|
||||||
|
message?: string;
|
||||||
|
description?: string;
|
||||||
|
data?: any;
|
||||||
|
}
|
||||||
35
frontend/src/app/models/session.model.ts
Normal file
35
frontend/src/app/models/session.model.ts
Normal 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;
|
||||||
|
}
|
||||||
52
frontend/src/app/models/tools.model.ts
Normal file
52
frontend/src/app/models/tools.model.ts
Normal 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;
|
||||||
|
}
|
||||||
30
frontend/src/app/models/user.model.ts
Normal file
30
frontend/src/app/models/user.model.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -9,6 +9,11 @@ import { FileUploadModule } from 'primeng/fileupload';
|
|||||||
import { ProgressBarModule } from 'primeng/progressbar';
|
import { ProgressBarModule } from 'primeng/progressbar';
|
||||||
import { TextareaModule } from 'primeng/textarea';
|
import { TextareaModule } from 'primeng/textarea';
|
||||||
import { ToastModule } from 'primeng/toast';
|
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({
|
@Component({
|
||||||
selector: 'app-ocr',
|
selector: 'app-ocr',
|
||||||
@@ -19,16 +24,18 @@ import { ToastModule } from 'primeng/toast';
|
|||||||
FileUploadModule,
|
FileUploadModule,
|
||||||
ProgressBarModule,
|
ProgressBarModule,
|
||||||
TextareaModule,
|
TextareaModule,
|
||||||
ToastModule
|
ToastModule,
|
||||||
|
ButtonModule,
|
||||||
|
TableModule,
|
||||||
|
CardModule,
|
||||||
|
RadioButtonModule,
|
||||||
|
InputTextModule
|
||||||
],
|
],
|
||||||
providers: [MessageService],
|
providers: [MessageService],
|
||||||
template: `
|
template: `
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>OCR Extraction</h2>
|
<h2>OCR Extraction</h2>
|
||||||
<!--
|
|
||||||
Note: "customUpload" mode in PrimeNG FileUpload requires "uploadHandler".
|
|
||||||
"mode='advanced'" gives the sleek UI.
|
|
||||||
-->
|
|
||||||
<p-fileUpload mode="advanced"
|
<p-fileUpload mode="advanced"
|
||||||
chooseLabel="Select PDF or Image"
|
chooseLabel="Select PDF or Image"
|
||||||
uploadLabel="Extract Text"
|
uploadLabel="Extract Text"
|
||||||
@@ -44,7 +51,8 @@ import { ToastModule } from 'primeng/toast';
|
|||||||
<p-progressBar mode="indeterminate" [style]="{'height': '6px'}"></p-progressBar>
|
<p-progressBar mode="indeterminate" [style]="{'height': '6px'}"></p-progressBar>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4" *ngIf="extractedText !== null">
|
<div class="mt-4 grid" *ngIf="extractedText !== null">
|
||||||
|
<div class="col-12 md:col-6">
|
||||||
<h3>Extracted Text Result:</h3>
|
<h3>Extracted Text Result:</h3>
|
||||||
<textarea pInputTextarea
|
<textarea pInputTextarea
|
||||||
[autoResize]="true"
|
[autoResize]="true"
|
||||||
@@ -53,6 +61,110 @@ import { ToastModule } from 'primeng/toast';
|
|||||||
class="w-full"
|
class="w-full"
|
||||||
style="min-height: 300px; width: 100%; border-color: #d1d5db; font-family: monospace;">
|
style="min-height: 300px; width: 100%; border-color: #d1d5db; font-family: monospace;">
|
||||||
</textarea>
|
</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>
|
</div>
|
||||||
<p-toast></p-toast>
|
<p-toast></p-toast>
|
||||||
</div>
|
</div>
|
||||||
@@ -66,15 +178,26 @@ export class OcrComponent {
|
|||||||
extractedText: string | null = null;
|
extractedText: string | null = null;
|
||||||
loading: boolean = false;
|
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) {}
|
constructor(private ocrService: OcrService, private messageService: MessageService) {}
|
||||||
|
|
||||||
onUpload(event: any) {
|
onUpload(event: any) {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
this.aiResult = null; // Reset AI result on new upload
|
||||||
|
this.filePath = null;
|
||||||
const file = event.files[0];
|
const file = event.files[0];
|
||||||
|
|
||||||
this.ocrService.extractText(file).subscribe({
|
this.ocrService.extractText(file).subscribe({
|
||||||
next: (res) => {
|
next: (res) => {
|
||||||
this.extractedText = res.text;
|
this.extractedText = res.text;
|
||||||
|
this.filePath = res.file_path;
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.messageService.add({severity:'success', summary:'Success', detail:'Text Extracted Successfully'});
|
this.messageService.add({severity:'success', summary:'Success', detail:'Text Extracted Successfully'});
|
||||||
},
|
},
|
||||||
@@ -88,5 +211,50 @@ export class OcrComponent {
|
|||||||
|
|
||||||
onClear() {
|
onClear() {
|
||||||
this.extractedText = null;
|
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'});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,31 +1,32 @@
|
|||||||
import { DepartmentDTO } from './../../../../models/account.model';
|
import { DepartmentDTO } from './../../../../models/account.model';
|
||||||
import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core';
|
||||||
import { ConfirmationService, MessageService } from 'primeng/api';
|
import { ConfirmationService, MessageService } from 'primeng/api';
|
||||||
import { TableModule, Table } from 'primeng/table';
|
import { TableModule } from 'primeng/table';
|
||||||
import { DialogModule } from 'primeng/dialog';
|
import { Dialog } from 'primeng/dialog';
|
||||||
import { RippleModule } from 'primeng/ripple';
|
import { Ripple } from 'primeng/ripple';
|
||||||
import { ButtonModule } from 'primeng/button';
|
import { ButtonModule, Button } from 'primeng/button';
|
||||||
import { ToastModule } from 'primeng/toast';
|
import { ToastModule } from 'primeng/toast';
|
||||||
import { ToolbarModule } from 'primeng/toolbar';
|
import { ToolbarModule } from 'primeng/toolbar';
|
||||||
import { ConfirmDialogModule } from 'primeng/confirmdialog';
|
import { ConfirmDialog } from 'primeng/confirmdialog';
|
||||||
import { InputTextModule } from 'primeng/inputtext';
|
import { InputTextModule } from 'primeng/inputtext';
|
||||||
import { TextareaModule } from 'primeng/textarea';
|
import { TextareaModule } from 'primeng/textarea';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FileUploadModule } from 'primeng/fileupload';
|
import { FileUpload } from 'primeng/fileupload';
|
||||||
import { SelectModule } from 'primeng/select';
|
import { SelectModule } from 'primeng/select';
|
||||||
import { TagModule } from 'primeng/tag';
|
import { Tag } from 'primeng/tag';
|
||||||
import { RadioButtonModule } from 'primeng/radiobutton';
|
import { RadioButton } from 'primeng/radiobutton';
|
||||||
import { RatingModule } from 'primeng/rating';
|
import { Rating } from 'primeng/rating';
|
||||||
import { SkeletonModule } from 'primeng/skeleton';
|
import { Skeleton } from 'primeng/skeleton';
|
||||||
import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
|
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 { IconFieldModule } from 'primeng/iconfield';
|
||||||
import { InputIconModule } from 'primeng/inputicon';
|
import { InputIconModule } from 'primeng/inputicon';
|
||||||
|
import { Table } from 'primeng/table';
|
||||||
|
import { DropdownModule } from 'primeng/dropdown';
|
||||||
import { CompanyService } from '../../../../services/account/company/company.service';
|
import { CompanyService } from '../../../../services/account/company/company.service';
|
||||||
|
import { FloatLabelModule } from "primeng/floatlabel";
|
||||||
import { ValidationService } from '../../../../services/utilities/validation.service';
|
import { ValidationService } from '../../../../services/utilities/validation.service';
|
||||||
import { TooltipModule } from 'primeng/tooltip';
|
import { TooltipModule } from 'primeng/tooltip';
|
||||||
import { AutoCompleteModule } from 'primeng/autocomplete';
|
|
||||||
import { FloatLabelModule } from 'primeng/floatlabel';
|
|
||||||
|
|
||||||
interface Column {
|
interface Column {
|
||||||
field: string;
|
field: string;
|
||||||
@@ -42,13 +43,7 @@ interface ExportColumn {
|
|||||||
selector: 'app-department',
|
selector: 'app-department',
|
||||||
templateUrl: './department.component.html',
|
templateUrl: './department.component.html',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
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],
|
||||||
CommonModule, FormsModule, ReactiveFormsModule,
|
|
||||||
TableModule, DialogModule, ButtonModule, ToastModule, ToolbarModule, ConfirmDialogModule,
|
|
||||||
InputTextModule, TextareaModule, FileUploadModule, SelectModule, TagModule,
|
|
||||||
RadioButtonModule, RatingModule, SkeletonModule, InputNumberModule, IconFieldModule,
|
|
||||||
InputIconModule, TooltipModule, AutoCompleteModule, RippleModule, FloatLabelModule
|
|
||||||
],
|
|
||||||
providers: [MessageService, ConfirmationService],
|
providers: [MessageService, ConfirmationService],
|
||||||
styleUrl: './department.component.css'
|
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 + '?',
|
message: `Are you sure you want to ${isActivating ? 'activate' : 'deactivate'} ` + department.department + '?',
|
||||||
header: 'Confirm',
|
header: 'Confirm',
|
||||||
icon: 'pi pi-exclamation-triangle',
|
icon: 'pi pi-exclamation-triangle',
|
||||||
rejectButtonStyleClass: 'p-button-text p-button-secondary',
|
rejectButtonProps: {
|
||||||
acceptButtonStyleClass: isActivating ? 'p-button-success' : 'p-button-danger',
|
label: 'No',
|
||||||
|
severity: 'secondary',
|
||||||
|
variant: 'text'
|
||||||
|
},
|
||||||
|
acceptButtonProps: {
|
||||||
|
severity: isActivating ? 'success' : 'danger',
|
||||||
|
label: 'Yes'
|
||||||
|
},
|
||||||
accept: () => {
|
accept: () => {
|
||||||
this.companyService.activateDeactivateDepartment(department.id, isActivating).subscribe({
|
this.companyService.activateDeactivateDepartment(department.id, isActivating).subscribe({
|
||||||
next: (updatedDepartment) => {
|
next: (updatedDepartment) => {
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,31 +1,33 @@
|
|||||||
import { DesignationDTO, DepartmentDTO } from './../../../../models/account.model';
|
import { DesignationDTO, DepartmentDTO } from './../../../../models/account.model';
|
||||||
import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core';
|
||||||
import { ConfirmationService, MessageService } from 'primeng/api';
|
import { ConfirmationService, MessageService } from 'primeng/api';
|
||||||
import { TableModule, Table } from 'primeng/table';
|
import { TableModule } from 'primeng/table';
|
||||||
import { DialogModule } from 'primeng/dialog';
|
import { Dialog } from 'primeng/dialog';
|
||||||
import { RippleModule } from 'primeng/ripple';
|
import { Ripple } from 'primeng/ripple';
|
||||||
import { ButtonModule } from 'primeng/button';
|
import { ButtonModule, Button } from 'primeng/button';
|
||||||
import { ToastModule } from 'primeng/toast';
|
import { ToastModule } from 'primeng/toast';
|
||||||
import { ToolbarModule } from 'primeng/toolbar';
|
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 { TextareaModule } from 'primeng/textarea';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FileUploadModule } from 'primeng/fileupload';
|
import { FileUpload } from 'primeng/fileupload';
|
||||||
import { SelectModule } from 'primeng/select';
|
import { SelectModule } from 'primeng/select';
|
||||||
import { TagModule } from 'primeng/tag';
|
import { Tag } from 'primeng/tag';
|
||||||
import { RadioButtonModule } from 'primeng/radiobutton';
|
import { RadioButton } from 'primeng/radiobutton';
|
||||||
import { RatingModule } from 'primeng/rating';
|
import { Rating } from 'primeng/rating';
|
||||||
import { SkeletonModule } from 'primeng/skeleton';
|
import { Skeleton } from 'primeng/skeleton';
|
||||||
import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
|
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 { IconFieldModule } from 'primeng/iconfield';
|
||||||
import { InputIconModule } from 'primeng/inputicon';
|
import { InputIconModule } from 'primeng/inputicon';
|
||||||
|
import { Table } from 'primeng/table';
|
||||||
|
import { DropdownModule } from 'primeng/dropdown';
|
||||||
import { CompanyService } from '../../../../services/account/company/company.service';
|
import { CompanyService } from '../../../../services/account/company/company.service';
|
||||||
|
import { FloatLabelModule } from "primeng/floatlabel";
|
||||||
import { ValidationService } from '../../../../services/utilities/validation.service';
|
import { ValidationService } from '../../../../services/utilities/validation.service';
|
||||||
import { TooltipModule } from 'primeng/tooltip';
|
import { TooltipModule } from 'primeng/tooltip';
|
||||||
import { CheckboxModule } from 'primeng/checkbox';
|
import { CheckboxModule } from 'primeng/checkbox';
|
||||||
import { AutoCompleteModule } from 'primeng/autocomplete';
|
|
||||||
import { FloatLabelModule } from 'primeng/floatlabel';
|
|
||||||
|
|
||||||
interface Column {
|
interface Column {
|
||||||
field: string;
|
field: string;
|
||||||
@@ -42,13 +44,7 @@ interface ExportColumn {
|
|||||||
selector: 'app-designation',
|
selector: 'app-designation',
|
||||||
templateUrl: './designation.component.html',
|
templateUrl: './designation.component.html',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
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],
|
||||||
CommonModule, FormsModule, ReactiveFormsModule,
|
|
||||||
TableModule, DialogModule, ButtonModule, ToastModule, ToolbarModule, ConfirmDialogModule,
|
|
||||||
TextareaModule, FileUploadModule, SelectModule, TagModule,
|
|
||||||
RadioButtonModule, RatingModule, SkeletonModule, InputNumberModule, IconFieldModule, InputIconModule,
|
|
||||||
TooltipModule, AutoCompleteModule, RippleModule, CheckboxModule, FloatLabelModule
|
|
||||||
],
|
|
||||||
providers: [MessageService, ConfirmationService],
|
providers: [MessageService, ConfirmationService],
|
||||||
styleUrl: './designation.component.css'
|
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 + '?',
|
message: `Are you sure you want to ${isActivating ? 'activate' : 'deactivate'} ` + designation.designation + '?',
|
||||||
header: 'Confirm',
|
header: 'Confirm',
|
||||||
icon: 'pi pi-exclamation-triangle',
|
icon: 'pi pi-exclamation-triangle',
|
||||||
rejectButtonStyleClass: 'p-button-text p-button-secondary',
|
rejectButtonProps: {
|
||||||
acceptButtonStyleClass: isActivating ? 'p-button-success' : 'p-button-danger',
|
label: 'No',
|
||||||
|
severity: 'secondary',
|
||||||
|
variant: 'text'
|
||||||
|
},
|
||||||
|
acceptButtonProps: {
|
||||||
|
severity: isActivating ? 'success' : 'danger',
|
||||||
|
label: 'Yes'
|
||||||
|
},
|
||||||
accept: () => {
|
accept: () => {
|
||||||
this.companyService.activateDeactivateDesignation(designation.id, isActivating).subscribe({
|
this.companyService.activateDeactivateDesignation(designation.id, isActivating).subscribe({
|
||||||
next: (updatedDesignation) => {
|
next: (updatedDesignation) => {
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,25 +1,30 @@
|
|||||||
import { EmployeeDTO, SubsidiaryDTO, DepartmentDTO, DesignationDTO } from './../../../../models/account.model';
|
import { EmployeeDTO, SubsidiaryDTO, DepartmentDTO, DesignationDTO } from './../../../../models/account.model';
|
||||||
import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core';
|
||||||
import { ConfirmationService, MessageService } from 'primeng/api';
|
import { ConfirmationService, MessageService } from 'primeng/api';
|
||||||
import { TableModule, Table } from 'primeng/table';
|
import { TableModule } from 'primeng/table';
|
||||||
import { DialogModule } from 'primeng/dialog';
|
import { Dialog } from 'primeng/dialog';
|
||||||
import { RippleModule } from 'primeng/ripple';
|
import { Ripple } from 'primeng/ripple';
|
||||||
import { ButtonModule } from 'primeng/button';
|
import { ButtonModule, Button } from 'primeng/button';
|
||||||
import { ToastModule } from 'primeng/toast';
|
import { ToastModule } from 'primeng/toast';
|
||||||
import { ToolbarModule } from 'primeng/toolbar';
|
import { ToolbarModule } from 'primeng/toolbar';
|
||||||
import { ConfirmDialogModule } from 'primeng/confirmdialog';
|
import { ConfirmDialog } from 'primeng/confirmdialog';
|
||||||
import { InputTextModule } from 'primeng/inputtext';
|
import { InputTextModule } from 'primeng/inputtext';
|
||||||
import { TextareaModule } from 'primeng/textarea';
|
import { TextareaModule } from 'primeng/textarea';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FileUploadModule } from 'primeng/fileupload';
|
import { FileUpload } from 'primeng/fileupload';
|
||||||
import { SelectModule } from 'primeng/select';
|
import { SelectModule } from 'primeng/select';
|
||||||
import { TagModule } from 'primeng/tag';
|
import { Tag } from 'primeng/tag';
|
||||||
import { SkeletonModule } from 'primeng/skeleton';
|
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 { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
import { InputNumberModule } from 'primeng/inputnumber';
|
import { InputNumber } from 'primeng/inputnumber';
|
||||||
import { IconFieldModule } from 'primeng/iconfield';
|
import { IconFieldModule } from 'primeng/iconfield';
|
||||||
import { InputIconModule } from 'primeng/inputicon';
|
import { InputIconModule } from 'primeng/inputicon';
|
||||||
|
import { Table } from 'primeng/table';
|
||||||
|
import { DropdownModule } from 'primeng/dropdown';
|
||||||
import { CompanyService } from '../../../../services/account/company/company.service';
|
import { CompanyService } from '../../../../services/account/company/company.service';
|
||||||
|
import { FloatLabelModule } from "primeng/floatlabel";
|
||||||
import { ValidationService } from '../../../../services/utilities/validation.service';
|
import { ValidationService } from '../../../../services/utilities/validation.service';
|
||||||
import { TooltipModule } from 'primeng/tooltip';
|
import { TooltipModule } from 'primeng/tooltip';
|
||||||
import { AutoCompleteModule } from 'primeng/autocomplete';
|
import { AutoCompleteModule } from 'primeng/autocomplete';
|
||||||
@@ -28,7 +33,6 @@ import { MasterService } from '../../../../services/masters/master.service';
|
|||||||
import { CityDTO, SearchDTO } from '../../../../models/masters/masters';
|
import { CityDTO, SearchDTO } from '../../../../models/masters/masters';
|
||||||
import { debounceTime, Subject } from 'rxjs';
|
import { debounceTime, Subject } from 'rxjs';
|
||||||
import { Request } from '../../../../models/request.model';
|
import { Request } from '../../../../models/request.model';
|
||||||
import { FloatLabelModule } from 'primeng/floatlabel';
|
|
||||||
|
|
||||||
interface Column {
|
interface Column {
|
||||||
field: string;
|
field: string;
|
||||||
@@ -45,13 +49,7 @@ interface ExportColumn {
|
|||||||
selector: 'app-employee',
|
selector: 'app-employee',
|
||||||
templateUrl: './employee.component.html',
|
templateUrl: './employee.component.html',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
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],
|
||||||
CommonModule, FormsModule, ReactiveFormsModule,
|
|
||||||
TableModule, DialogModule, ButtonModule, ToastModule, ToolbarModule, ConfirmDialogModule,
|
|
||||||
InputTextModule, TextareaModule, FileUploadModule, SelectModule, TagModule,
|
|
||||||
SkeletonModule, InputNumberModule, IconFieldModule,
|
|
||||||
InputIconModule, TooltipModule, AutoCompleteModule, RippleModule, CalendarModule, FloatLabelModule
|
|
||||||
],
|
|
||||||
providers: [MessageService, ConfirmationService],
|
providers: [MessageService, ConfirmationService],
|
||||||
styleUrl: './employee.component.css'
|
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 + '?',
|
message: `Are you sure you want to ${isActivating ? 'activate' : 'deactivate'} ` + employee.fullName + '?',
|
||||||
header: 'Confirm',
|
header: 'Confirm',
|
||||||
icon: 'pi pi-exclamation-triangle',
|
icon: 'pi pi-exclamation-triangle',
|
||||||
rejectButtonStyleClass: 'p-button-text p-button-secondary',
|
rejectButtonProps: {
|
||||||
acceptButtonStyleClass: isActivating ? 'p-button-success' : 'p-button-danger',
|
label: 'No',
|
||||||
|
severity: 'secondary',
|
||||||
|
variant: 'text'
|
||||||
|
},
|
||||||
|
acceptButtonProps: {
|
||||||
|
severity: isActivating ? 'success' : 'danger',
|
||||||
|
label: 'Yes'
|
||||||
|
},
|
||||||
accept: () => {
|
accept: () => {
|
||||||
this.companyService.activateDeactivateEmployee(employee.id, isActivating).subscribe({
|
this.companyService.activateDeactivateEmployee(employee.id, isActivating).subscribe({
|
||||||
next: (updatedEmployee) => {
|
next: (updatedEmployee) => {
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
:host ::ng-deep .p-dialog .product-image {
|
||||||
|
width: 150px;
|
||||||
|
margin: 0 auto 2rem auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
<h4 class="m-0">Manage Subsidiaries</h4>
|
<h4 class="m-0">Manage Subsidiaries</h4>
|
||||||
<p-iconfield>
|
<p-iconfield>
|
||||||
<p-inputicon class="pi pi-search" />
|
<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>
|
</p-iconfield>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,31 +1,38 @@
|
|||||||
|
import { SearchDTO } from './../../../../models/masters/masters';
|
||||||
import { SubsidiaryDTO } from './../../../../models/account.model';
|
import { SubsidiaryDTO } from './../../../../models/account.model';
|
||||||
import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core';
|
||||||
import { ConfirmationService, MessageService } from 'primeng/api';
|
import { ConfirmationService, MessageService } from 'primeng/api';
|
||||||
import { TableModule, Table } from 'primeng/table';
|
import { TableModule } from 'primeng/table';
|
||||||
import { DialogModule } from 'primeng/dialog';
|
import { Dialog } from 'primeng/dialog';
|
||||||
import { RippleModule } from 'primeng/ripple';
|
import { Ripple } from 'primeng/ripple';
|
||||||
import { ButtonModule } from 'primeng/button';
|
import { ButtonModule, Button } from 'primeng/button';
|
||||||
import { ToastModule } from 'primeng/toast';
|
import { ToastModule } from 'primeng/toast';
|
||||||
import { ToolbarModule } from 'primeng/toolbar';
|
import { ToolbarModule } from 'primeng/toolbar';
|
||||||
import { ConfirmDialogModule } from 'primeng/confirmdialog';
|
import { ConfirmDialog } from 'primeng/confirmdialog';
|
||||||
import { InputTextModule } from 'primeng/inputtext';
|
import { InputTextModule } from 'primeng/inputtext';
|
||||||
import { TextareaModule } from 'primeng/textarea';
|
import { TextareaModule } from 'primeng/textarea';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FileUpload } from 'primeng/fileupload';
|
||||||
import { SelectModule } from 'primeng/select';
|
import { SelectModule } from 'primeng/select';
|
||||||
import { TagModule } from 'primeng/tag';
|
import { Tag } from 'primeng/tag';
|
||||||
import { SkeletonModule } from 'primeng/skeleton';
|
import { RadioButton } from 'primeng/radiobutton';
|
||||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
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 { IconFieldModule } from 'primeng/iconfield';
|
||||||
import { InputIconModule } from 'primeng/inputicon';
|
import { InputIconModule } from 'primeng/inputicon';
|
||||||
|
import { Table } from 'primeng/table';
|
||||||
|
import { DropdownModule } from 'primeng/dropdown';
|
||||||
import { CompanyService } from '../../../../services/account/company/company.service';
|
import { CompanyService } from '../../../../services/account/company/company.service';
|
||||||
|
import { FloatLabelModule } from "primeng/floatlabel";
|
||||||
import { ValidationService } from '../../../../services/utilities/validation.service';
|
import { ValidationService } from '../../../../services/utilities/validation.service';
|
||||||
import { TooltipModule } from 'primeng/tooltip';
|
import { TooltipModule } from 'primeng/tooltip';
|
||||||
import { AutoCompleteModule } from 'primeng/autocomplete';
|
import { AutoCompleteModule } from 'primeng/autocomplete';
|
||||||
import { MasterService } from '../../../../services/masters/master.service';
|
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 { debounceTime, Subject } from 'rxjs';
|
||||||
import { Request } from '../../../../models/request.model';
|
import { Request } from '../../../../models/request.model';
|
||||||
import { FloatLabelModule } from 'primeng/floatlabel';
|
|
||||||
|
|
||||||
interface Column {
|
interface Column {
|
||||||
field: string;
|
field: string;
|
||||||
@@ -38,19 +45,11 @@ interface ExportColumn {
|
|||||||
dataKey: string;
|
dataKey: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
import { FormsModule } from '@angular/forms';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-subsidiary',
|
selector: 'app-subsidiary',
|
||||||
templateUrl: './subsidiary.component.html',
|
templateUrl: './subsidiary.component.html',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
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],
|
||||||
CommonModule, FormsModule, ReactiveFormsModule,
|
|
||||||
TableModule, DialogModule, ButtonModule, ToastModule, ToolbarModule, ConfirmDialogModule,
|
|
||||||
InputTextModule, TextareaModule, SelectModule, TagModule,
|
|
||||||
SkeletonModule, IconFieldModule,
|
|
||||||
InputIconModule, TooltipModule, AutoCompleteModule, RippleModule, FloatLabelModule
|
|
||||||
],
|
|
||||||
providers: [MessageService, ConfirmationService],
|
providers: [MessageService, ConfirmationService],
|
||||||
styleUrl: './subsidiary.component.css'
|
styleUrl: './subsidiary.component.css'
|
||||||
})
|
})
|
||||||
@@ -117,11 +116,6 @@ export class SubsidiaryComponent implements OnInit{
|
|||||||
this.dt.exportCSV();
|
this.dt.exportCSV();
|
||||||
}
|
}
|
||||||
|
|
||||||
onSearch(event: Event) {
|
|
||||||
const input = event.target as HTMLInputElement;
|
|
||||||
this.dt.filterGlobal(input.value, 'contains');
|
|
||||||
}
|
|
||||||
|
|
||||||
searchCities(event: any) {
|
searchCities(event: any) {
|
||||||
const query = event.query;
|
const query = event.query;
|
||||||
this.searchSubject.next(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 + '?',
|
message: `Are you sure you want to ${isActivating ? 'activate' : 'deactivate'} ` + subsidiary.name + '?',
|
||||||
header: 'Confirm',
|
header: 'Confirm',
|
||||||
icon: 'pi pi-exclamation-triangle',
|
icon: 'pi pi-exclamation-triangle',
|
||||||
rejectButtonStyleClass: 'p-button-text p-button-secondary',
|
rejectButtonProps: {
|
||||||
acceptButtonStyleClass: isActivating ? 'p-button-success' : 'p-button-danger',
|
label: 'No',
|
||||||
|
severity: 'secondary',
|
||||||
|
variant: 'text'
|
||||||
|
},
|
||||||
|
acceptButtonProps: {
|
||||||
|
severity: isActivating ? 'success' : 'danger',
|
||||||
|
label: 'Yes'
|
||||||
|
},
|
||||||
accept: () => {
|
accept: () => {
|
||||||
this.subsidiaryService.activateDeactivateSubsidiary(subsidiary.id?? '', isActivating).subscribe({
|
this.subsidiaryService.activateDeactivateSubsidiary(subsidiary.id?? '', isActivating).subscribe({
|
||||||
next: (updatedSubsidiary) => {
|
next: (updatedSubsidiary) => {
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -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>
|
||||||
@@ -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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
1
frontend/src/app/pages/account/user/user.component.css
Normal file
1
frontend/src/app/pages/account/user/user.component.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<p-toast />
|
<p-toast />
|
||||||
<p-toolbar class="mb-6">
|
<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()" />
|
<p-button label="New User" icon="pi pi-plus" class="mr-2" (onClick)="openNew()" />
|
||||||
</ng-template>
|
</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="Export" icon="pi pi-upload" severity="secondary" (onClick)="exportCSV()" styleClass="mx-2" />
|
||||||
<p-button label="Refresh" icon="pi pi-refresh" severity="info" (onClick)="loadAllUsers()" />
|
<p-button label="Refresh" icon="pi pi-refresh" severity="info" (onClick)="loadAllUsers()" />
|
||||||
</ng-template>
|
</ng-template>
|
||||||
@@ -25,16 +25,16 @@
|
|||||||
currentPageReportTemplate="Showing {first} to {last} of {totalRecords} entries"
|
currentPageReportTemplate="Showing {first} to {last} of {totalRecords} entries"
|
||||||
[showCurrentPageReport]="true"
|
[showCurrentPageReport]="true"
|
||||||
>
|
>
|
||||||
<ng-template pTemplate="caption">
|
<ng-template #caption>
|
||||||
<div class="flex justify-content-between align-items-center">
|
<div class="flex justify-content-between align-items-center">
|
||||||
<h4 class="m-0">Manage Users</h4>
|
<h4 class="m-0">Manage Users</h4>
|
||||||
<span class="p-input-icon-left">
|
<p-iconfield>
|
||||||
<i class="pi pi-search"></i>
|
<p-inputicon class="pi pi-search" />
|
||||||
<input pInputText type="text" (input)="onSearch($event)" placeholder="Search..." />
|
<input pInputText type="text" (input)="onSearch($event)" placeholder="Search..." />
|
||||||
</span>
|
</p-iconfield>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template pTemplate="header">
|
<ng-template #header>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width: 3rem">#</th>
|
<th style="width: 3rem">#</th>
|
||||||
<th pSortableColumn="loginId">
|
<th pSortableColumn="loginId">
|
||||||
@@ -82,7 +82,7 @@
|
|||||||
<th style="min-width: 8rem"></th>
|
<th style="min-width: 8rem"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template pTemplate="body" let-user let-rowIndex="rowIndex">
|
<ng-template #body let-user let-rowIndex="rowIndex">
|
||||||
<tr *ngIf="!isLoading">
|
<tr *ngIf="!isLoading">
|
||||||
<td>{{ rowIndex + 1 }}</td>
|
<td>{{ rowIndex + 1 }}</td>
|
||||||
<td>{{ user.loginId }}</td>
|
<td>{{ user.loginId }}</td>
|
||||||
@@ -130,27 +130,34 @@
|
|||||||
</p-table>
|
</p-table>
|
||||||
|
|
||||||
<p-dialog [(visible)]="userDialog" [style]="{'width': '50vw'}" [breakpoints]="{ '960px': '75vw', '640px': '90vw' }" header="User Details" [modal]="true">
|
<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">
|
<form [formGroup]="userForm" (ngSubmit)="saveUser()" class="mt-2">
|
||||||
<div class="grid mt-0">
|
<div class="grid mt-0">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<span class="p-float-label">
|
<p-floatlabel variant="on">
|
||||||
|
<p-iconfield>
|
||||||
<input
|
<input
|
||||||
id="loginId"
|
id="loginId"
|
||||||
pInputText
|
pInputText
|
||||||
formControlName="loginId"
|
formControlName="loginId"
|
||||||
fluid
|
fluid
|
||||||
[readonly]="!!user"
|
[readonly]="!!user"
|
||||||
[class.p-invalid]="isFieldInvalid('loginId')"
|
[class.p-invalid]="isFieldInvalid('loginId') || loginIdTaken"
|
||||||
pTooltip="{{ getErrorMessage('loginId') }}"
|
|
||||||
tooltipPosition="top"
|
|
||||||
autofocus
|
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>
|
<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>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<span class="p-float-label">
|
<p-floatlabel variant="on">
|
||||||
<input
|
<input
|
||||||
id="displayName"
|
id="displayName"
|
||||||
pInputText
|
pInputText
|
||||||
@@ -161,10 +168,13 @@
|
|||||||
tooltipPosition="top"
|
tooltipPosition="top"
|
||||||
/>
|
/>
|
||||||
<label for="displayName">Display Name <span class="text-red-500">*</span></label>
|
<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>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<span class="p-float-label">
|
<p-floatlabel variant="on">
|
||||||
<p-autoComplete
|
<p-autoComplete
|
||||||
id="employeeId"
|
id="employeeId"
|
||||||
formControlName="employeeId"
|
formControlName="employeeId"
|
||||||
@@ -177,10 +187,10 @@
|
|||||||
fluid
|
fluid
|
||||||
/>
|
/>
|
||||||
<label for="employeeId">Employee ID</label>
|
<label for="employeeId">Employee ID</label>
|
||||||
</span>
|
</p-floatlabel>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<span class="p-float-label">
|
<p-floatlabel variant="on">
|
||||||
<input
|
<input
|
||||||
id="employeeName"
|
id="employeeName"
|
||||||
pInputText
|
pInputText
|
||||||
@@ -189,10 +199,10 @@
|
|||||||
readonly
|
readonly
|
||||||
/>
|
/>
|
||||||
<label for="employeeName">Employee Name</label>
|
<label for="employeeName">Employee Name</label>
|
||||||
</span>
|
</p-floatlabel>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<span class="p-float-label">
|
<p-floatlabel variant="on">
|
||||||
<input
|
<input
|
||||||
id="fatherName"
|
id="fatherName"
|
||||||
pInputText
|
pInputText
|
||||||
@@ -201,10 +211,10 @@
|
|||||||
readonly
|
readonly
|
||||||
/>
|
/>
|
||||||
<label for="fatherName">Father Name</label>
|
<label for="fatherName">Father Name</label>
|
||||||
</span>
|
</p-floatlabel>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 md:col-6">
|
<div class="col-12 md:col-6">
|
||||||
<span class="p-float-label">
|
<p-floatlabel variant="on">
|
||||||
<input
|
<input
|
||||||
id="department"
|
id="department"
|
||||||
pInputText
|
pInputText
|
||||||
@@ -213,10 +223,10 @@
|
|||||||
readonly
|
readonly
|
||||||
/>
|
/>
|
||||||
<label for="department">Department</label>
|
<label for="department">Department</label>
|
||||||
</span>
|
</p-floatlabel>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 md:col-6">
|
<div class="col-12 md:col-6">
|
||||||
<span class="p-float-label">
|
<p-floatlabel variant="on">
|
||||||
<input
|
<input
|
||||||
id="designation"
|
id="designation"
|
||||||
pInputText
|
pInputText
|
||||||
@@ -225,11 +235,11 @@
|
|||||||
readonly
|
readonly
|
||||||
/>
|
/>
|
||||||
<label for="designation">Designation</label>
|
<label for="designation">Designation</label>
|
||||||
</span>
|
</p-floatlabel>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 mb-3">
|
<div class="col-12 mb-3">
|
||||||
<span class="p-float-label">
|
<p-floatlabel variant="on">
|
||||||
<p-dropdown
|
<p-select
|
||||||
id="status"
|
id="status"
|
||||||
[options]="statuses"
|
[options]="statuses"
|
||||||
formControlName="status"
|
formControlName="status"
|
||||||
@@ -241,22 +251,25 @@
|
|||||||
tooltipPosition="top"
|
tooltipPosition="top"
|
||||||
/>
|
/>
|
||||||
<label for="status">Status <span class="text-red-500">*</span></label>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<p-fieldset legend="User Roles" class="mt-4" [style]="{'background-color': 'rgba(243,243,243,0.5)'}">
|
<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="grid mb-4 mt-2">
|
||||||
<div class="col-12 md:col-4">
|
<div class="col-12 md:col-4">
|
||||||
<span class="p-float-label">
|
<p-floatlabel variant="on">
|
||||||
<p-dropdown [options]="branches" optionLabel="name" optionValue="id" fluid />
|
<p-select [(ngModel)]="selectedBranch" [ngModelOptions]="{standalone: true}" [options]="branches" optionLabel="branchName" optionValue="id" fluid appendTo="body" />
|
||||||
<label>Branch</label>
|
<label>Branch</label>
|
||||||
</span>
|
</p-floatlabel>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 md:col-4">
|
<div class="col-12 md:col-4">
|
||||||
<span class="p-float-label">
|
<p-floatlabel variant="on">
|
||||||
<p-dropdown [options]="userRolesOptions" optionLabel="name" optionValue="id" fluid />
|
<p-select [(ngModel)]="selectedRole" [ngModelOptions]="{standalone: true}" [options]="userRolesOptions" optionLabel="groupName" optionValue="id" fluid appendTo="body" />
|
||||||
<label>User Role</label>
|
<label>User Role</label>
|
||||||
</span>
|
</p-floatlabel>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 md:col-4">
|
<div class="col-12 md:col-4">
|
||||||
<p-button label="Add Role" icon="pi pi-plus" (onClick)="addRole()" />
|
<p-button label="Add Role" icon="pi pi-plus" (onClick)="addRole()" />
|
||||||
@@ -279,6 +292,7 @@
|
|||||||
<td>{{role.groupName}}</td>
|
<td>{{role.groupName}}</td>
|
||||||
<td>{{role.branchName}}</td>
|
<td>{{role.branchName}}</td>
|
||||||
<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'" />
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -293,7 +307,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template pTemplate="footer">
|
<ng-template #footer>
|
||||||
<p-button label="Cancel" icon="pi pi-times" text (click)="hideDialog()" />
|
<p-button label="Cancel" icon="pi pi-times" text (click)="hideDialog()" />
|
||||||
<p-button label="Save" icon="pi pi-check" (click)="saveUser()" />
|
<p-button label="Save" icon="pi pi-check" (click)="saveUser()" />
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|||||||
21
frontend/src/app/pages/account/user/user.component.spec.ts
Normal file
21
frontend/src/app/pages/account/user/user.component.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,26 +1,31 @@
|
|||||||
import { UserDTO, UserRoleDTO } from '../../../models/user.model';
|
import { UserDTO, UserRoleDTO } from '../../../models/user.model';
|
||||||
import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core';
|
||||||
import { ConfirmationService, MessageService } from 'primeng/api';
|
import { ConfirmationService, MessageService } from 'primeng/api';
|
||||||
import { TableModule, Table } from 'primeng/table';
|
import { TableModule } from 'primeng/table';
|
||||||
import { DialogModule } from 'primeng/dialog';
|
import { Dialog } from 'primeng/dialog';
|
||||||
import { RippleModule } from 'primeng/ripple';
|
import { Ripple } from 'primeng/ripple';
|
||||||
import { ButtonModule } from 'primeng/button';
|
import { ButtonModule, Button } from 'primeng/button';
|
||||||
import { ToastModule } from 'primeng/toast';
|
import { ToastModule } from 'primeng/toast';
|
||||||
import { ToolbarModule } from 'primeng/toolbar';
|
import { ToolbarModule } from 'primeng/toolbar';
|
||||||
import { ConfirmDialogModule } from 'primeng/confirmdialog';
|
import { ConfirmDialog } from 'primeng/confirmdialog';
|
||||||
import { InputTextModule } from 'primeng/inputtext';
|
import { InputTextModule } from 'primeng/inputtext';
|
||||||
import { TextareaModule } from 'primeng/textarea';
|
import { TextareaModule } from 'primeng/textarea';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FileUploadModule } from 'primeng/fileupload';
|
import { FileUpload } from 'primeng/fileupload';
|
||||||
import { DropdownModule } from 'primeng/dropdown';
|
import { SelectModule } from 'primeng/select';
|
||||||
import { TagModule } from 'primeng/tag';
|
import { Tag } from 'primeng/tag';
|
||||||
import { RadioButtonModule } from 'primeng/radiobutton';
|
import { RadioButton } from 'primeng/radiobutton';
|
||||||
import { RatingModule } from 'primeng/rating';
|
import { Rating } from 'primeng/rating';
|
||||||
import { SkeletonModule } from 'primeng/skeleton';
|
import { Skeleton } from 'primeng/skeleton';
|
||||||
import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
|
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 { UserService } from '../../../services/account/user/user.service';
|
||||||
import { CompanyService } from '../../../services/account/company/company.service';
|
import { CompanyService } from '../../../services/account/company/company.service';
|
||||||
|
import { FloatLabelModule } from "primeng/floatlabel";
|
||||||
import { ValidationService } from '../../../services/utilities/validation.service';
|
import { ValidationService } from '../../../services/utilities/validation.service';
|
||||||
import { TooltipModule } from 'primeng/tooltip';
|
import { TooltipModule } from 'primeng/tooltip';
|
||||||
import { AutoCompleteModule } from 'primeng/autocomplete';
|
import { AutoCompleteModule } from 'primeng/autocomplete';
|
||||||
@@ -42,13 +47,7 @@ interface ExportColumn {
|
|||||||
selector: 'app-user',
|
selector: 'app-user',
|
||||||
templateUrl: './user.component.html',
|
templateUrl: './user.component.html',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [TableModule, Dialog, SelectModule, ToastModule, ToolbarModule, ConfirmDialog, InputTextModule, CommonModule, Tag, IconFieldModule, InputIconModule, Button, FloatLabelModule, FormsModule, ReactiveFormsModule, TooltipModule, Skeleton, AutoCompleteModule, FieldsetModule],
|
||||||
CommonModule, FormsModule, ReactiveFormsModule,
|
|
||||||
TableModule, DialogModule, ButtonModule, ToastModule, ToolbarModule, ConfirmDialogModule,
|
|
||||||
InputTextModule, TextareaModule, FileUploadModule, DropdownModule, TagModule,
|
|
||||||
RadioButtonModule, RatingModule, SkeletonModule, InputNumberModule,
|
|
||||||
TooltipModule, AutoCompleteModule, FieldsetModule, RippleModule
|
|
||||||
],
|
|
||||||
providers: [MessageService, ConfirmationService],
|
providers: [MessageService, ConfirmationService],
|
||||||
styleUrl: './user.component.css'
|
styleUrl: './user.component.css'
|
||||||
})
|
})
|
||||||
@@ -62,6 +61,8 @@ export class UserComponent implements OnInit{
|
|||||||
selectedUsers!: UserDTO[] | null;
|
selectedUsers!: UserDTO[] | null;
|
||||||
|
|
||||||
submitted: boolean = false;
|
submitted: boolean = false;
|
||||||
|
checkingLoginId: boolean = false;
|
||||||
|
loginIdTaken: boolean = false;
|
||||||
|
|
||||||
isLoading: boolean = true;
|
isLoading: boolean = true;
|
||||||
|
|
||||||
@@ -73,6 +74,9 @@ export class UserComponent implements OnInit{
|
|||||||
|
|
||||||
userRolesOptions: any[] = [];
|
userRolesOptions: any[] = [];
|
||||||
|
|
||||||
|
selectedBranch: string | undefined;
|
||||||
|
selectedRole: string | undefined;
|
||||||
|
|
||||||
suggestions: any[] = [];
|
suggestions: any[] = [];
|
||||||
|
|
||||||
private searchSubject = new Subject<string>();
|
private searchSubject = new Subject<string>();
|
||||||
@@ -121,6 +125,32 @@ export class UserComponent implements OnInit{
|
|||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.loadAllUsers();
|
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() {
|
loadAllUsers() {
|
||||||
@@ -187,6 +217,10 @@ export class UserComponent implements OnInit{
|
|||||||
this.userForm.reset();
|
this.userForm.reset();
|
||||||
this.userRoles = [];
|
this.userRoles = [];
|
||||||
this.submitted = false;
|
this.submitted = false;
|
||||||
|
this.loginIdTaken = false;
|
||||||
|
this.checkingLoginId = false;
|
||||||
|
this.selectedBranch = undefined;
|
||||||
|
this.selectedRole = undefined;
|
||||||
this.userDialog = true;
|
this.userDialog = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,6 +228,8 @@ export class UserComponent implements OnInit{
|
|||||||
this.userForm.reset();
|
this.userForm.reset();
|
||||||
this.user = { ...user };
|
this.user = { ...user };
|
||||||
this.userRoles = [...(user.userRoles || [])];
|
this.userRoles = [...(user.userRoles || [])];
|
||||||
|
this.selectedBranch = undefined;
|
||||||
|
this.selectedRole = undefined;
|
||||||
this.userForm.patchValue(user);
|
this.userForm.patchValue(user);
|
||||||
this.userDialog = true;
|
this.userDialog = true;
|
||||||
console.log(this.userForm.getRawValue());
|
console.log(this.userForm.getRawValue());
|
||||||
@@ -202,6 +238,8 @@ export class UserComponent implements OnInit{
|
|||||||
hideDialog() {
|
hideDialog() {
|
||||||
this.userDialog = false;
|
this.userDialog = false;
|
||||||
this.submitted = false;
|
this.submitted = false;
|
||||||
|
this.loginIdTaken = false;
|
||||||
|
this.checkingLoginId = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleActive(user: UserDTO) {
|
toggleActive(user: UserDTO) {
|
||||||
@@ -210,8 +248,15 @@ export class UserComponent implements OnInit{
|
|||||||
message: `Are you sure you want to ${isActivating ? 'activate' : 'deactivate'} ` + user.displayName + '?',
|
message: `Are you sure you want to ${isActivating ? 'activate' : 'deactivate'} ` + user.displayName + '?',
|
||||||
header: 'Confirm',
|
header: 'Confirm',
|
||||||
icon: 'pi pi-exclamation-triangle',
|
icon: 'pi pi-exclamation-triangle',
|
||||||
rejectButtonStyleClass: 'p-button-text p-button-secondary',
|
rejectButtonProps: {
|
||||||
acceptButtonStyleClass: isActivating ? 'p-button-success' : 'p-button-danger',
|
label: 'No',
|
||||||
|
severity: 'secondary',
|
||||||
|
variant: 'text'
|
||||||
|
},
|
||||||
|
acceptButtonProps: {
|
||||||
|
severity: isActivating ? 'success' : 'danger',
|
||||||
|
label: 'Yes'
|
||||||
|
},
|
||||||
accept: () => {
|
accept: () => {
|
||||||
this.userService.activateDeactivateUser(user.id!, isActivating).subscribe({
|
this.userService.activateDeactivateUser(user.id!, isActivating).subscribe({
|
||||||
next: (updatedUser) => {
|
next: (updatedUser) => {
|
||||||
@@ -243,7 +288,7 @@ export class UserComponent implements OnInit{
|
|||||||
case 'Active':
|
case 'Active':
|
||||||
return 'success';
|
return 'success';
|
||||||
case 'Inactive':
|
case 'Inactive':
|
||||||
return 'warning';
|
return 'warn';
|
||||||
}
|
}
|
||||||
return 'info';
|
return 'info';
|
||||||
}
|
}
|
||||||
@@ -258,14 +303,134 @@ export class UserComponent implements OnInit{
|
|||||||
return !!(control && control.invalid && (control.dirty || control.touched || this.submitted));
|
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() {
|
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) {
|
toggleRoleActive(role: UserRoleDTO) {
|
||||||
role.active = role.active === false ? true : false;
|
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) {
|
onSelectEmployee(event: any) {
|
||||||
const emp = event.value;
|
const emp = event.value;
|
||||||
const patch: any = {
|
const patch: any = {
|
||||||
@@ -318,16 +483,21 @@ export class UserComponent implements OnInit{
|
|||||||
|
|
||||||
this.userService.saveUser(userData).subscribe({
|
this.userService.saveUser(userData).subscribe({
|
||||||
next: (savedUser) => {
|
next: (savedUser) => {
|
||||||
|
|
||||||
const index = userData.id
|
const index = userData.id
|
||||||
? users.findIndex(u => u.id === userData.id)
|
? users.findIndex(u => u.id === userData.id)
|
||||||
: -1;
|
: -1;
|
||||||
|
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
// UPDATE
|
// UPDATE
|
||||||
|
if (!savedUser.userRoles && userData.userRoles) {
|
||||||
|
savedUser.userRoles = userData.userRoles;
|
||||||
|
}
|
||||||
users[index] = savedUser;
|
users[index] = savedUser;
|
||||||
} else {
|
} else {
|
||||||
// CREATE
|
// CREATE
|
||||||
|
if (!savedUser.userRoles && userData.userRoles) {
|
||||||
|
savedUser.userRoles = userData.userRoles;
|
||||||
|
}
|
||||||
users.push(savedUser);
|
users.push(savedUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
|
import { MenuComponent } from "../../fragments/menu/menu.component";
|
||||||
|
import { RouterOutlet } from '@angular/router';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-dashboard',
|
selector: 'app-dashboard',
|
||||||
imports: [],
|
imports: [MenuComponent, RouterOutlet],
|
||||||
template: `<div class="p-5">
|
template: `<div>
|
||||||
|
<app-menu></app-menu>
|
||||||
|
<div class="dashboard-content">
|
||||||
|
<router-outlet></router-outlet>
|
||||||
|
</div>
|
||||||
</div>`
|
</div>`
|
||||||
})
|
})
|
||||||
export class DashboardComponent {
|
export class DashboardComponent {
|
||||||
|
|||||||
38
frontend/src/app/pages/session/auth/authorize.component.css
Normal file
38
frontend/src/app/pages/session/auth/authorize.component.css
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
38
frontend/src/app/pages/session/auth/authorize.component.html
Normal file
38
frontend/src/app/pages/session/auth/authorize.component.html
Normal 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>
|
||||||
@@ -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 { SessionService } from './../../../services/commons/session.service';
|
||||||
import { HttpService } from '../../../services/http.service';
|
import { CommonModule} from '@angular/common';
|
||||||
import { environment } from '../../../../environments/environment';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { Company, Branch } from '../../../models/session.model';
|
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 { 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 { ResponseDto } from '../../../models/response.dto';
|
||||||
|
import { Company, Branch } from '../../../models/session.model';
|
||||||
|
import { SelectModule } from 'primeng/select';
|
||||||
|
import { environment } from '../../../../environments/environment';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-authorize',
|
selector: 'app-authorize',
|
||||||
standalone: true,
|
imports: [CommonModule, FormsModule, CardModule, ButtonModule, InputTextModule, InputGroupModule, InputGroupAddonModule, PasswordModule,
|
||||||
imports: [
|
FloatLabelModule, ReactiveFormsModule, KeyFilterModule, MessageModule, StyleClassModule, SelectModule],
|
||||||
CommonModule,
|
|
||||||
FormsModule,
|
|
||||||
ReactiveFormsModule,
|
|
||||||
CardModule,
|
|
||||||
ButtonModule,
|
|
||||||
InputTextModule,
|
|
||||||
PasswordModule,
|
|
||||||
DropdownModule,
|
|
||||||
KeyFilterModule,
|
|
||||||
MessageModule
|
|
||||||
],
|
|
||||||
templateUrl: './authorize.component.html',
|
templateUrl: './authorize.component.html',
|
||||||
styleUrl: './authorize.component.css'
|
styleUrl: './authorize.component.css'
|
||||||
})
|
})
|
||||||
@@ -51,7 +43,7 @@ export class AuthorizeComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(){
|
ngOnInit(){
|
||||||
const companyBranchRoles = this.sessionService.getItem('companies');
|
const companyBranchRoles = sessionStorage.getItem('companies');
|
||||||
|
|
||||||
if (!companyBranchRoles) {
|
if (!companyBranchRoles) {
|
||||||
this.sessionService.logout();
|
this.sessionService.logout();
|
||||||
@@ -60,7 +52,7 @@ export class AuthorizeComponent implements OnInit {
|
|||||||
sessionStorage.removeItem('companies');
|
sessionStorage.removeItem('companies');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.companies = companyBranchRoles;
|
this.companies = JSON.parse(companyBranchRoles);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.sessionService.logout();
|
this.sessionService.logout();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
167
frontend/src/app/pages/session/profile/profile.component.html
Normal file
167
frontend/src/app/pages/session/profile/profile.component.html
Normal 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>
|
||||||
|
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
60
frontend/src/app/pages/session/profile/profile.component.ts
Normal file
60
frontend/src/app/pages/session/profile/profile.component.ts
Normal 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{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* localities.component.css */
|
||||||
@@ -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>
|
||||||
@@ -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 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
199
frontend/src/app/services/account/company/company.service.ts
Normal file
199
frontend/src/app/services/account/company/company.service.ts
Normal 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[])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
67
frontend/src/app/services/account/user/user.service.ts
Normal file
67
frontend/src/app/services/account/user/user.service.ts
Normal 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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
51
frontend/src/app/services/commons/session.service.ts
Normal file
51
frontend/src/app/services/commons/session.service.ts
Normal 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(['/']);
|
||||||
|
}
|
||||||
|
}
|
||||||
56
frontend/src/app/services/http.service.ts
Normal file
56
frontend/src/app/services/http.service.ts
Normal 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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
24
frontend/src/app/services/masters/master.service.ts
Normal file
24
frontend/src/app/services/masters/master.service.ts
Normal 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[])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
24
frontend/src/app/services/ola/ola.service.ts
Normal file
24
frontend/src/app/services/ola/ola.service.ts
Normal 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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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[])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
61
frontend/src/app/services/utilities/encryption.service.ts
Normal file
61
frontend/src/app/services/utilities/encryption.service.ts
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
37
frontend/src/app/services/utilities/rsa.service.ts
Normal file
37
frontend/src/app/services/utilities/rsa.service.ts
Normal 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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
167
frontend/src/app/services/utilities/validation.service.ts
Normal file
167
frontend/src/app/services/utilities/validation.service.ts
Normal 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';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,5 +5,6 @@ export const environment = {
|
|||||||
accountService: 'http://localhost:1701/cygnus/app/api/v1/account',
|
accountService: 'http://localhost:1701/cygnus/app/api/v1/account',
|
||||||
userService: 'http://localhost:1701/cygnus/app/api/v1/user',
|
userService: 'http://localhost:1701/cygnus/app/api/v1/user',
|
||||||
masterService: 'http://localhost:1702/cygnus/app/api/v1/master',
|
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`
|
rsaPublicKey: `MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq3RFV/f6ybsOF2m7NBLPUTMBq9b0frJG1HdIDYmrD9Wr1/aGBxTSJwq8IHFlatNpBF3OlJv9uEOybWMM1vXli4IgsuPPmcTOZsQ/O/9UGyBSL6apevNCw6pC1oa0MVLaN6COMAhDr+ri/PYiPQUcYsjDqghmAghMk99umHGUihz/oY/qgxzO+Q9cqePmjpH5c5RaXGBrOQxKoPlm7Uj6MqAfBhLC360VbcMot4XDoV+VeQXMzH0o6e870jdClsLOq1VsCA27jVvafj+HwaJ15ny9UWWilDuS/X8Sd7v+Rmd+qNezi6ROcglyaisXwKfeTWM8/o7HiUco2fL230+jEQIDAQAB`
|
||||||
};
|
};
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
{
|
{
|
||||||
"compileOnSave": false,
|
"compileOnSave": false,
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"strict": true,
|
"strict": false,
|
||||||
"noImplicitOverride": true,
|
"noImplicitOverride": true,
|
||||||
"noPropertyAccessFromIndexSignature": true,
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
"noImplicitReturns": true,
|
"noImplicitReturns": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user