This commit is contained in:
2026-05-21 21:33:37 +05:30
parent f50dd4692d
commit 26478608b1
28 changed files with 4948 additions and 259 deletions

View File

@@ -1,20 +1,19 @@
import os import os
import urllib.parse import urllib.parse
from sqlalchemy import create_engine, Column, Integer, String, Text, DateTime, Boolean, ForeignKey, LargeBinary from sqlalchemy import create_engine, Column, Integer, String, Text, DateTime, Boolean, ForeignKey, LargeBinary, Enum as SqlEnum
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, relationship from sqlalchemy.orm import sessionmaker, relationship
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.sql import func
from dotenv import load_dotenv from dotenv import load_dotenv
# Load environment variables # Load environment variables
load_dotenv() load_dotenv()
DB_USER = os.getenv("DB_USER") DB_USER = os.getenv("DB_USER", "postgres")
DB_PASSWORD = os.getenv("DB_PASSWORD") DB_PASSWORD = os.getenv("DB_PASSWORD", "M@tr!x#149@dm!N")
DB_HOST = os.getenv("DB_HOST") DB_HOST = os.getenv("DB_HOST", "192.168.0.111")
DB_PORT = os.getenv("DB_PORT") DB_PORT = os.getenv("DB_PORT", "7925")
DB_NAME = os.getenv("DB_NAME") DB_NAME = os.getenv("DB_NAME", "ocr")
encoded_user = urllib.parse.quote_plus(DB_USER) encoded_user = urllib.parse.quote_plus(DB_USER)
encoded_password = urllib.parse.quote_plus(DB_PASSWORD) encoded_password = urllib.parse.quote_plus(DB_PASSWORD)
@@ -27,6 +26,26 @@ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base() Base = declarative_base()
class Vendor(Base):
__tablename__ = "vendors"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, unique=True, index=True)
default_model = Column(String) # 'text' or 'vision'
created_at = Column(DateTime)
class Document(Base):
__tablename__ = "documents"
id = Column(Integer, primary_key=True, index=True)
vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=True)
filename = Column(String)
upload_date = Column(DateTime)
status = Column(String) # 'pending', 'verified'
processed_data = Column(JSONB) # Store the verified extraction results
vendor = relationship("Vendor")
class Email(Base): class Email(Base):
__tablename__ = "emails" __tablename__ = "emails"
@@ -46,33 +65,10 @@ class Attachment(Base):
email_id = Column(Integer, ForeignKey("emails.id")) email_id = Column(Integer, ForeignKey("emails.id"))
filename = Column(String) filename = Column(String)
content_type = Column(String) content_type = Column(String)
file_path = Column(String, nullable=True) # Path to file on disk file_content = Column(LargeBinary)
file_content = Column(LargeBinary, nullable=True) # Stored in DB (for small files)
email = relationship("Email", back_populates="attachments") email = relationship("Email", back_populates="attachments")
class Vendor(Base):
__tablename__ = "vendors"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, unique=True, index=True)
default_model = Column(String, default="text") # 'text' (Gemma) or 'vision' (Qwen)
created_at = Column(DateTime(timezone=True), server_default=func.now())
documents = relationship("Document", back_populates="vendor")
class Document(Base):
__tablename__ = "documents"
id = Column(Integer, primary_key=True, index=True)
vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=True)
filename = Column(String)
upload_date = Column(DateTime(timezone=True), server_default=func.now())
status = Column(String, default="pending") # pending, verified
processed_data = Column(JSONB) # The final verified JSON
vendor = relationship("Vendor", back_populates="documents")
def get_db(): def get_db():
db = SessionLocal() db = SessionLocal()
try: try:

View File

@@ -16,8 +16,10 @@
"@angular/forms": "^21.1.0", "@angular/forms": "^21.1.0",
"@angular/platform-browser": "^21.1.0", "@angular/platform-browser": "^21.1.0",
"@angular/router": "^21.1.0", "@angular/router": "^21.1.0",
"@primeng/themes": "^19.1.0",
"primeflex": "^4.0.0",
"primeicons": "^7.0.0", "primeicons": "^7.0.0",
"primeng": "^17.18.0", "primeng": "^19.1.0",
"quill": "^2.0.3", "quill": "^2.0.3",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",
"tslib": "^2.3.0", "tslib": "^2.3.0",
@@ -3001,6 +3003,37 @@
"license": "MIT", "license": "MIT",
"optional": true "optional": true
}, },
"node_modules/@primeng/themes": {
"version": "19.1.4",
"resolved": "https://registry.npmjs.org/@primeng/themes/-/themes-19.1.4.tgz",
"integrity": "sha512-Hze5bBTjsLzZXb20qsm9apsFuzpZzXiU+Ulj/7R+2fwMmcQk0XpkQS7V88fsFw6xsTD7+R+hgqr7Rzy0Gf+4dw==",
"deprecated": "Deprecated. This package is no longer maintained. Please migrate to @primeuix/themes: https://www.npmjs.com/package/@primeuix/themes",
"license": "SEE LICENSE IN LICENSE.md",
"dependencies": {
"@primeuix/styled": "^0.3.2"
}
},
"node_modules/@primeng/themes/node_modules/@primeuix/styled": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@primeuix/styled/-/styled-0.3.2.tgz",
"integrity": "sha512-ColZes0+/WKqH4ob2x8DyNYf1NENpe5ZguOvx5yCLxaP8EIMVhLjWLO/3umJiDnQU4XXMLkn2mMHHw+fhTX/mw==",
"license": "MIT",
"dependencies": {
"@primeuix/utils": "^0.3.2"
},
"engines": {
"node": ">=12.11.0"
}
},
"node_modules/@primeng/themes/node_modules/@primeuix/utils": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@primeuix/utils/-/utils-0.3.2.tgz",
"integrity": "sha512-B+nphqTQeq+i6JuICLdVWnDMjONome2sNz0xI65qIOyeB4EF12CoKRiCsxuZ5uKAkHi/0d1LqlQ9mIWRSdkavw==",
"license": "MIT",
"engines": {
"node": ">=12.11.0"
}
},
"node_modules/@rolldown/binding-android-arm64": { "node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.0-beta.58", "version": "1.0.0-beta.58",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.58.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.58.tgz",
@@ -6570,6 +6603,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/primeflex": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/primeflex/-/primeflex-4.0.0.tgz",
"integrity": "sha512-UOEZCRjR36+sm5bUpDhS1xbA068l9VC6y1aTNVqQPtXuKIdPTqAWHRUxj3mKAoPrQ9W373ooJJMgNVXfiaw04g==",
"license": "MIT"
},
"node_modules/primeicons": { "node_modules/primeicons": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/primeicons/-/primeicons-7.0.0.tgz", "resolved": "https://registry.npmjs.org/primeicons/-/primeicons-7.0.0.tgz",
@@ -6577,19 +6616,45 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/primeng": { "node_modules/primeng": {
"version": "17.18.0", "version": "19.1.4",
"resolved": "https://registry.npmjs.org/primeng/-/primeng-17.18.0.tgz", "resolved": "https://registry.npmjs.org/primeng/-/primeng-19.1.4.tgz",
"integrity": "sha512-EcvU/0Ex9QoBR6g6db9fDTCTAmzokW70TV5Oroy2gdvXRr3eqlflnOBoArQsmxTaw1oxSsu68YVj3RvcKYWhTg==", "integrity": "sha512-l5l8SHTxopxxyyXZx1BvbQ11P7ndLv2Qp8H5k2/+OCi65jTZn4xmtrBDGDs7k2K5UMHSqAnGjBgtpbckyqQETg==",
"license": "MIT", "license": "SEE LICENSE IN LICENSE.md",
"dependencies": { "dependencies": {
"@primeuix/styled": "^0.3.2",
"@primeuix/utils": "^0.3.2",
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
"peerDependencies": { "peerDependencies": {
"@angular/common": "^17.0.0 || ^18.0.0", "@angular/animations": "^19.0.0",
"@angular/core": "^17.0.0 || ^18.0.0", "@angular/cdk": "^19.0.0",
"@angular/forms": "^17.0.0 || ^18.0.0", "@angular/common": "^19.0.0",
"rxjs": "^6.0.0 || ^7.8.1", "@angular/core": "^19.0.0",
"zone.js": "~0.14.0" "@angular/forms": "^19.0.0",
"@angular/platform-browser": "^19.0.0",
"@angular/router": "^19.0.0",
"rxjs": "^6.0.0 || ^7.8.1"
}
},
"node_modules/primeng/node_modules/@primeuix/styled": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@primeuix/styled/-/styled-0.3.2.tgz",
"integrity": "sha512-ColZes0+/WKqH4ob2x8DyNYf1NENpe5ZguOvx5yCLxaP8EIMVhLjWLO/3umJiDnQU4XXMLkn2mMHHw+fhTX/mw==",
"license": "MIT",
"dependencies": {
"@primeuix/utils": "^0.3.2"
},
"engines": {
"node": ">=12.11.0"
}
},
"node_modules/primeng/node_modules/@primeuix/utils": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@primeuix/utils/-/utils-0.3.2.tgz",
"integrity": "sha512-B+nphqTQeq+i6JuICLdVWnDMjONome2sNz0xI65qIOyeB4EF12CoKRiCsxuZ5uKAkHi/0d1LqlQ9mIWRSdkavw==",
"license": "MIT",
"engines": {
"node": ">=12.11.0"
} }
}, },
"node_modules/proc-log": { "node_modules/proc-log": {

View File

@@ -3,7 +3,7 @@
"version": "0.0.0", "version": "0.0.0",
"scripts": { "scripts": {
"ng": "ng", "ng": "ng",
"start": "ng serve", "start": "ng serve --proxy-config proxy.conf.json",
"build": "ng build", "build": "ng build",
"watch": "ng build --watch --configuration development", "watch": "ng build --watch --configuration development",
"test": "ng test" "test": "ng test"
@@ -31,8 +31,10 @@
"@angular/forms": "^21.1.0", "@angular/forms": "^21.1.0",
"@angular/platform-browser": "^21.1.0", "@angular/platform-browser": "^21.1.0",
"@angular/router": "^21.1.0", "@angular/router": "^21.1.0",
"@primeng/themes": "^19.1.0",
"primeflex": "^4.0.0",
"primeicons": "^7.0.0", "primeicons": "^7.0.0",
"primeng": "^17.18.0", "primeng": "^19.1.0",
"quill": "^2.0.3", "quill": "^2.0.3",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",
"tslib": "^2.3.0", "tslib": "^2.3.0",

View File

@@ -1,7 +1,10 @@
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router'; import { provideRouter } from '@angular/router';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { provideHttpClient } from '@angular/common/http'; import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { AuthInterceptor } from './interceptors/auth.interceptor';
import { providePrimeNG } from 'primeng/config';
import Aura from '@primeng/themes/aura';
import { routes } from './app.routes'; import { routes } from './app.routes';
@@ -10,6 +13,11 @@ export const appConfig: ApplicationConfig = {
provideZoneChangeDetection({ eventCoalescing: true }), provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes), provideRouter(routes),
provideAnimationsAsync(), provideAnimationsAsync(),
provideHttpClient() providePrimeNG({
theme: {
preset: Aura
}
}),
provideHttpClient(withInterceptors([AuthInterceptor]))
] ]
}; };

View File

@@ -3,19 +3,40 @@ import { LoginComponent } from './login/login.component';
import { AdminLayoutComponent } from './admin-layout/admin-layout.component'; import { AdminLayoutComponent } from './admin-layout/admin-layout.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';
import { AuthGuard } from './auth.guard';
import { AuthorizeComponent } from './pages/session/auth/authorize.component';
import { AuthorizeGuard } from './interceptors/authorize.guard';
import { ProfileComponent } from './pages/session/profile/profile.component';
import { SubsidiaryComponent } from './pages/account/company/subsidiary/subsidiary.component';
import { DepartmentComponent } from './pages/account/company/department/department.component';
import { DesignationComponent } from './pages/account/company/desigation/designation.component';
import { EmployeeComponent } from './pages/account/company/employee/employee.component';
import { UserComponent } from './pages/account/user/user.component';
export const routes: Routes = [ export const routes: Routes = [
{ path: 'login', component: LoginComponent }, { path: '', component: LoginComponent },
{ { path: 'authorize', component: AuthorizeComponent, canActivate: [AuthorizeGuard]},
path: '', { path: 'user',
component: AdminLayoutComponent, component: AdminLayoutComponent,
canActivate: [AuthGuard], canActivate: [AuthorizeGuard],
children: [ canActivateChild: [AuthorizeGuard],
{ path: '', redirectTo: 'ocr', pathMatch: 'full' }, children: [
{ path: 'ocr', component: OcrComponent }, { path: 'profile', component: ProfileComponent },
{ path: 'mailbox', component: MailboxComponent } { path: 'mailbox', component: MailboxComponent },
] { path: 'ocr', component: OcrComponent }
}, ]
{ path: '**', redirectTo: '' } },
]; { path: 'account',
component: AdminLayoutComponent,
canActivate: [AuthorizeGuard],
canActivateChild: [AuthorizeGuard],
children: [
{ path: 'profile', component: ProfileComponent },
{ path: 'subsidiaries', component: SubsidiaryComponent },
{ path: 'departments', component: DepartmentComponent },
{ path: 'designations', component: DesignationComponent },
{ path: 'employees', component: EmployeeComponent },
{ path: 'users', component: UserComponent }
]
}
];

View File

@@ -0,0 +1,111 @@
import { Component, Input, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { MenuItem } from 'primeng/api';
import { MenubarModule } from 'primeng/menubar';
import { HttpService } from '../../services/http.service';
import { Request } from '../../models/request.model';
import { AvatarModule } from 'primeng/avatar';
import { MenuModule } from 'primeng/menu';
import { ButtonModule } from 'primeng/button';
import { InputTextModule } from 'primeng/inputtext';
import { SessionService } from '../../services/commons/session.service';
import { EncryptionService } from '../../services/utilities/encryption.service';
import { TooltipModule } from 'primeng/tooltip';
import { environment } from '../../../environments/environment';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-menu',
standalone: true,
imports: [CommonModule, MenubarModule, AvatarModule, ButtonModule, MenuModule, InputTextModule, TooltipModule],
templateUrl: './menu.component.html'
})
export class MenuComponent {
items: MenuItem[] = [];
profileItems: MenuItem[] | undefined;
companyName: string = '';
branchName: string = '';
roleName: string = '';
name: string = '';
constructor(private router: Router, private http: HttpService, private session: SessionService, private enc: EncryptionService) { }
ngOnInit() {
if (typeof window !== 'undefined' && sessionStorage) {
let menuString = this.session.getItem("nav");
if (menuString) {
this.enc.decrypt(menuString).then(decrypted => {
if (decrypted) {
this.items = JSON.parse(decrypted).map((m: any) => this.mapMenu(m));
console.log(this.items);
}
});
}
const companies = this.session.getItem("companies") ? JSON.parse(this.session.getItem("companies")) : '';
const userDetails = this.session.getItem('userDetails');
this.name = userDetails ? JSON.parse(userDetails).displayName : '';
this.companyName = companies ? companies[0].companyName : companies;
this.branchName = companies ? companies[0].branches[0].branchCode : '';
this.roleName = companies ? companies[0].branches[0].roles[0].groupName : ''
}
this.profileItems = [
{
label: this.roleName,
items: [
{
label: 'Details',
icon: 'pi pi-id-card',
command: () => {
this.router.navigateByUrl(`/user/profile`);
}
},
{
label: 'Sign Out',
icon: 'pi pi-sign-out',
command: () => {
this.logout();
}
}
]
}
];
}
logout() {
const requestPayload: Request = {
data: '',
compressed: false
};
this.http.post(environment.authService + '/signout', requestPayload).subscribe({
next: () => {
sessionStorage.clear();
this.router.navigate(['/']);
},
error: (err) => {
console.error('Logout failed', err);
}
});
}
mapMenu(menu: any): MenuItem {
const hasChildren = Array.isArray(menu.items) && menu.items.length > 0;
return {
label: menu.label,
icon: menu.icon,
routerLink: !hasChildren && menu.route !== '#'
? menu.route
: undefined,
command: !hasChildren && menu.route !== '#'
? () => {
this.router.navigateByUrl(menu.route);
}
: undefined,
items: hasChildren
? menu.items.map((c: any) => this.mapMenu(c))
: undefined // 🔥 THIS IS CRITICAL
};
}
gotToDashboard() {
this.router.navigate(['/user']);
}
}

View File

@@ -0,0 +1,145 @@
import { SessionService } from './../services/commons/session.service';
import {
HttpInterceptorFn,
HttpRequest,
HttpHandlerFn,
HttpEvent,
HttpResponse,
HttpErrorResponse,
HttpHeaders
} from '@angular/common/http';
import { Observable, tap, catchError, throwError, from, switchMap } from 'rxjs';
import { EncryptionService } from '../services/utilities/encryption.service';
import { inject } from '@angular/core';
import { ResponseDto } from '../models/response.dto';
export const AuthInterceptor: HttpInterceptorFn = (
req: HttpRequest<any>,
next: HttpHandlerFn
): Observable<HttpEvent<any>> => {
const encryptionService = inject(EncryptionService);
const sessionService = inject(SessionService);
/* -----------------------------------------
* 1⃣ Always attach Authorization header
* ----------------------------------------- */
// OCR uses sessionStorage for token now (migrated previously)
const token = sessionStorage.getItem('token');
let reqHeaders = req.headers;
// Only set application/json if it's not FormData
// FormData requests need the browser to set Content-Type with boundary
if (!(req.body instanceof FormData)) {
reqHeaders = reqHeaders.set('Content-Type', 'application/json');
}
if (token) {
reqHeaders = reqHeaders.set(
'Authorization',
`Bearer ${token}`
);
}
/* -----------------------------------------
* 2⃣ Handle encrypted (compressed) requests
* ----------------------------------------- */
if (
req.body &&
typeof req.body === 'object' &&
req.body.compressed
) {
return from(encryptionService.encrypt(req.body.data)).pipe(
switchMap(encryptedBody => {
const payload = {
scopes: req.body.scopes ?? [],
data: encryptedBody,
target: req.body.target ?? null,
compressed: true
};
const modifiedReq = req.clone({
body: payload,
headers: reqHeaders
});
// Use 'any' type for event to avoid strict type mismatch if ResponseDto structure varies
return next(modifiedReq).pipe(
tap((event: any) => handleResponse(event, sessionService, encryptionService)),
catchError((error: HttpErrorResponse) =>
throwError(() => error)
)
);
})
);
}
/* -----------------------------------------
* 3⃣ Non-encrypted requests (GET included)
* ----------------------------------------- */
const modifiedReq = req.clone({
headers: reqHeaders
});
return next(modifiedReq).pipe(
tap((event: any) => handleResponse(event, sessionService, encryptionService)),
catchError((error: HttpErrorResponse) =>
throwError(() => error)
)
);
};
function handleResponse(
event: HttpEvent<any>,
sessionService: SessionService,
encryptionService: EncryptionService
): void {
if (!(event instanceof HttpResponse) || !event.ok) {
return;
}
// Handle Authenticate Response
if (event.url?.endsWith('/authenticate')) {
const response = event.body as ResponseDto;
if (response?.data?.token) {
sessionStorage.setItem('token', response.data.token);
}
if (response?.data?.companies) {
sessionService.setItem('companies', response.data.companies);
}
}
// Handle Authorize Response
if (event.url?.endsWith('/authorize')) {
const response = event.body as ResponseDto;
// Clearing session might be too aggressive if we just logged in, but following source logic
// sessionService.clear();
if (response?.data?.token) {
sessionStorage.setItem('token', response.data.token);
}
if (response?.data?.refreshToken) {
sessionService.setItem('refreshToken', response.data.refreshToken);
}
if (response?.data?.userDetails) {
sessionService.setItem('userDetails', response.data.userDetails);
}
if (response?.data?.data) {
encryptionService.encrypt(response.data.data)
.then(encryptedNav => {
sessionService.setItem('nav', encryptedNav);
});
}
if (response?.data?.companies) {
sessionService.setItem(
'companies',
response.data.companies
);
}
}
}

View File

@@ -0,0 +1,129 @@
export class SubsidiaryDTO {
id?: string;
companyId?: string;
companyName?: string;
code?: string;
name?: string;
officeNo?: string;
street?: string;
locality?: string;
cityId?: string;
stateId?: string;
cityName?: string;
stateName?: string;
pinCode?: string;
emailId?: string;
contactNo?: string;
contactPerson?: string;
panNo?: string;
cinNo?: string;
msmeNo?: string;
updatedAt?: Date;
updatedBy?: string;
updatedUser?: string;
active?: boolean;
constructor(init?: Partial<SubsidiaryDTO>) {
Object.assign(this, init);
if (init?.updatedAt) {
this.updatedAt = new Date(init.updatedAt);
}
}
}
export interface DepartmentDTO {
id: string;
companyId: string;
department: string;
parentDepartment?: string;
parentDepartmentName?: string;
createdUser: string;
updatedAt: string;
updatedUser: string;
active: boolean;
}
export interface DesignationDTO {
id: string;
companyId: string;
departmentId: string;
departmentName: string;
designation: string;
createdUser: string;
updatedAt: string;
updatedUser: string;
active: boolean;
hod: boolean;
payGrade?: string;
}
export interface EmployeeDTO {
id: string;
subsidiaryId: string;
departmentId: string;
designationId: string;
subsidiaryName: string;
department: string;
designation: string;
employeeId?: string;
joiningDate?: string;
fullName: string;
fatherName?: string;
gender?: string;
dob?: string;
residenceAddress?: string;
residenceCityId?: string;
residenceStateId?: string;
residenceCityName?: string;
residenceStateName?: string;
permanentAddress?: string;
permanentCityId?: string;
permanentStateId?: string;
permanentCityName?: string;
permanentStateName?: string;
contactNo: string;
alternateNo?: string;
emailId: string;
createdUser: string;
updatedAt: string;
updatedUser: string;
active: boolean;
}
export interface VendorBranchDTO {
id?: string;
fkVendorId?: string;
branchCode: string;
branchName: string;
officeNo?: string;
street?: string;
locality?: string;
cityId?: string;
stateId?: string;
stateName?: string;
cityName?: string;
pinCode?: string;
emailId?: string;
contactNo?: string;
contactPerson?: string;
gstNo?: string;
createdUser?: string;
updatedAt?: string;
updatedUser?: string;
active: boolean;
}
export interface VendorDTO {
id?: string;
companyId?: string;
code: string;
name: string;
panNo?: string;
cinNo?: string;
msmeNo?: string;
createdUser?: string;
updatedAt?: string;
updatedUser?: string;
active: boolean;
branches?: VendorBranchDTO[];
}

View File

@@ -7,13 +7,8 @@ import { MessageService } from 'primeng/api';
// PrimeNG // PrimeNG
import { FileUploadModule } from 'primeng/fileupload'; import { FileUploadModule } from 'primeng/fileupload';
import { ProgressBarModule } from 'primeng/progressbar'; import { ProgressBarModule } from 'primeng/progressbar';
import { InputTextareaModule } from 'primeng/inputtextarea'; 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',
@@ -23,19 +18,17 @@ import { InputTextModule } from 'primeng/inputtext';
FormsModule, FormsModule,
FileUploadModule, FileUploadModule,
ProgressBarModule, ProgressBarModule,
InputTextareaModule, 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"
@@ -51,120 +44,15 @@ import { InputTextModule } from 'primeng/inputtext';
<p-progressBar mode="indeterminate" [style]="{'height': '6px'}"></p-progressBar> <p-progressBar mode="indeterminate" [style]="{'height': '6px'}"></p-progressBar>
</div> </div>
<div class="mt-4 grid" *ngIf="extractedText !== null"> <div class="mt-4" *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" [(ngModel)]="extractedText"
[(ngModel)]="extractedText" readonly
readonly 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>
@@ -177,27 +65,16 @@ import { InputTextModule } from 'primeng/inputtext';
export class OcrComponent { 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'});
}, },
@@ -211,50 +88,5 @@ 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'});
}
});
} }
} }

View File

@@ -0,0 +1,162 @@
<div class="card">
<p-toast />
<p-toolbar class="mb-6">
<ng-template #start>
<p-button label="New Department" 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)="loadAllDepartments()" />
</ng-template>
</p-toolbar>
<p-table
#dt
[value]="isLoading ? skeletonData : departments"
[rows]="5"
[columns]="cols"
[paginator]="true"
[globalFilterFields]="['department']"
[tableStyle]="{ 'min-width': '75rem' }"
[(selection)]="selectedDepartments"
[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 Departments</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="department">
<div class="flex items-center gap-2">
Department
<p-sortIcon field="department" />
</div>
</th>
<th pSortableColumn="parentDepartmentName" style="min-width: 10rem">
<div class="flex items-center gap-2">
Parent Department
<p-sortIcon field="parentDepartmentName" />
</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-department let-rowIndex="rowIndex">
<tr *ngIf="!isLoading">
<td>{{ rowIndex + 1 }}</td>
<td>{{ department.department }}</td>
<td>
<span *ngIf="department.parentDepartmentName; else noParent">
{{ department.parentDepartmentName }}
</span>
<ng-template #noParent>
<em class="text-500 ">Not Available</em>
</ng-template>
</td>
<td>{{ department.updatedAt | date:'dd/MM/yyyy HH:mm:ss' }}</td>
<td>
<span *ngIf="department.updatedUser; else noUpdatedUser">
{{ department.updatedUser }}
</span>
<ng-template #noUpdatedUser>
<em class="text-500 ">Not Available</em>
</ng-template>
</td>
<td>
<p-tag [value]="department.active ? 'Active' : 'Inactive'" [severity]="getSeverity(department.active)" />
</td>
<td>
<p-button icon="pi pi-pencil" class="mr-2" [rounded]="true" [outlined]="true" (click)="editDepartment(department)" />
<p-button [icon]="department.active ? 'pi pi-ban' : 'pi pi-check'" [severity]="department.active ? 'danger' : 'success'" [rounded]="true" [outlined]="true" (click)="toggleActive(department)" />
</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="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)]="departmentDialog" [style]="{'width': '50vw'}" [breakpoints]="{ '960px': '75vw', '640px': '90vw' }" header="Department Details" [modal]="true">
<ng-template #content>
<form [formGroup]="departmentForm" (ngSubmit)="saveDepartment()" class="mt-2">
<div class="grid mt-0">
<div class="col-12">
<p-floatlabel variant="on">
<input
id="department"
pInputText
formControlName="department"
fluid
[class.p-invalid]="isFieldInvalid('department')"
pTooltip="{{ getErrorMessage('department') }}"
tooltipPosition="top"
autofocus
/>
<label for="department">Department Name <span class="text-red-500">*</span></label>
</p-floatlabel>
</div>
<div class="col-12">
<p-floatlabel variant="on">
<p-select
id="parentDepartment"
[options]="departmentOptions"
formControlName="parentDepartment"
optionLabel="label"
optionValue="value"
[filter]="true"
filterBy="label"
fluid
appendTo="body"
/>
<label for="parentDepartment">Parent Department</label>
</p-floatlabel>
</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)="saveDepartment()" />
</ng-template>
</p-dialog>
<p-confirmDialog [style]="{ width: '450px' }" />
</div>

View File

@@ -0,0 +1,284 @@
import { DepartmentDTO } from './../../../../models/account.model';
import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core';
import { ConfirmationService, MessageService } from 'primeng/api';
import { TableModule, Table } from 'primeng/table';
import { DialogModule } from 'primeng/dialog';
import { RippleModule } from 'primeng/ripple';
import { ButtonModule } from 'primeng/button';
import { ToastModule } from 'primeng/toast';
import { ToolbarModule } from 'primeng/toolbar';
import { ConfirmDialogModule } from 'primeng/confirmdialog';
import { InputTextModule } from 'primeng/inputtext';
import { TextareaModule } from 'primeng/textarea';
import { CommonModule } from '@angular/common';
import { FileUploadModule } from 'primeng/fileupload';
import { SelectModule } from 'primeng/select';
import { TagModule } from 'primeng/tag';
import { RadioButtonModule } from 'primeng/radiobutton';
import { RatingModule } from 'primeng/rating';
import { SkeletonModule } from 'primeng/skeleton';
import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { InputNumberModule } from 'primeng/inputnumber';
import { IconFieldModule } from 'primeng/iconfield';
import { InputIconModule } from 'primeng/inputicon';
import { CompanyService } from '../../../../services/account/company/company.service';
import { ValidationService } from '../../../../services/utilities/validation.service';
import { TooltipModule } from 'primeng/tooltip';
import { AutoCompleteModule } from 'primeng/autocomplete';
import { FloatLabelModule } from 'primeng/floatlabel';
interface Column {
field: string;
header: string;
customExportHeader?: string;
}
interface ExportColumn {
title: string;
dataKey: string;
}
@Component({
selector: 'app-department',
templateUrl: './department.component.html',
standalone: true,
imports: [
CommonModule, FormsModule, ReactiveFormsModule,
TableModule, DialogModule, ButtonModule, ToastModule, ToolbarModule, ConfirmDialogModule,
InputTextModule, TextareaModule, FileUploadModule, SelectModule, TagModule,
RadioButtonModule, RatingModule, SkeletonModule, InputNumberModule, IconFieldModule,
InputIconModule, TooltipModule, AutoCompleteModule, RippleModule, FloatLabelModule
],
providers: [MessageService, ConfirmationService],
styleUrl: './department.component.css'
})
export class DepartmentComponent implements OnInit{
departmentForm: FormGroup;
departmentDialog: boolean = false;
departments!: DepartmentDTO[];
department: DepartmentDTO | undefined;
selectedDepartments!: DepartmentDTO[] | null;
submitted: boolean = false;
isLoading: boolean = true;
skeletonData: any[] = Array(10).fill({});
statuses!: any[];
departmentOptions: any[] = [];
@ViewChild('dt') dt!: Table;
cols!: Column[];
exportColumns!: ExportColumn[];
constructor(
private companyService: CompanyService,
private messageService: MessageService,
private confirmationService: ConfirmationService,
private cd: ChangeDetectorRef,
private fb: FormBuilder,
) {
this.departmentForm = this.fb.group({
id: [{ value: '', disabled: true }],
department: [{ value: '', disabled: false }, [Validators.required, ValidationService.nameValidator()]],
parentDepartment: [{ value: '', disabled: false }],
parentDepartmentName: [{ value: '', disabled: true }],
active: [{ value: true, disabled: false }]
});
}
exportCSV() {
this.dt.exportCSV();
}
ngOnInit() {
this.loadAllDepartments();
}
loadAllDepartments() {
this.isLoading = true;
this.companyService.getAllDepartments().subscribe({
next: (data) => {
this.isLoading = false;
this.departments = data;
this.departmentOptions = [
{ label: 'None', value: '' },
...this.departments.map(d => ({ label: d.department, value: d.id }))
];
this.cd.markForCheck();
},
error: (err) => {
this.isLoading = false;
console.error(err);
}
});
this.statuses = [
{ label: 'Active', value: true },
{ label: 'Inactive', value: false }
];
this.cols = [
{ field: 'department', header: 'Department', customExportHeader: 'Department' },
{ field: 'parentDepartmentName', header: 'Parent Department' },
{ field: 'updatedAt', header: 'Last Updated At' },
{ field: 'updatedUser', header: 'Last Updated By' }
];
this.exportColumns = this.cols.map((col) => ({ title: col.header, dataKey: col.field }));
}
openNew() {
this.department = undefined;
this.departmentForm.reset();
this.submitted = false;
this.departmentDialog = true;
}
editDepartment(department: DepartmentDTO) {
const parentDepartment = this.departmentOptions.find(dep => dep.label === department.parentDepartmentName);
this.departmentForm.reset();
this.department = { ...department };
this.department.parentDepartment = parentDepartment?.value ?? '';
this.departmentForm.patchValue(this.department);
this.departmentDialog = true;
}
hideDialog() {
this.departmentDialog = false;
this.submitted = false;
}
toggleActive(department: DepartmentDTO) {
const isActivating = !department.active;
this.confirmationService.confirm({
message: `Are you sure you want to ${isActivating ? 'activate' : 'deactivate'} ` + department.department + '?',
header: 'Confirm',
icon: 'pi pi-exclamation-triangle',
rejectButtonStyleClass: 'p-button-text p-button-secondary',
acceptButtonStyleClass: isActivating ? 'p-button-success' : 'p-button-danger',
accept: () => {
this.companyService.activateDeactivateDepartment(department.id, isActivating).subscribe({
next: (updatedDepartment) => {
department.active = updatedDepartment.active;
this.departments = [...this.departments];
this.messageService.add({
severity: 'success',
summary: 'Successful',
detail: `Department ${isActivating ? 'Activated' : 'Deactivated'}`,
life: 3000
});
},
error: (err) => {
console.error('Error toggling department active status', err);
this.messageService.add({
severity: 'error',
summary: 'Error',
detail: 'Failed to update department status',
life: 3000
});
}
});
}
});
}
getSeverity(status: boolean) {
switch (status) {
case true:
return 'success';
case false:
return 'warn';
}
}
getErrorMessage(fieldName: string): string {
const control = this.departmentForm.get(fieldName);
return control ? ValidationService.getErrorMessage(control, fieldName) : '';
}
isFieldInvalid(fieldName: string): boolean {
const control = this.departmentForm.get(fieldName);
return !!(control && control.invalid && (control.dirty || control.touched || this.submitted));
}
saveDepartment() {
this.submitted = true;
if (this.departmentForm.invalid) {
return;
}
const departmentData = this.departmentForm.getRawValue() as DepartmentDTO;
// ✅ Normalize null → empty array
const departments = this.departments ?? [];
// 🔍 Duplicate check (case-insensitive)
const existing = departments.some(dep =>
dep.department?.trim().toLowerCase() === departmentData.department?.trim().toLowerCase() &&
dep.id !== departmentData.id
);
if (existing) {
this.messageService.add({
severity: 'error',
summary: 'Validation Error',
detail: 'Department name already exists',
life: 3000
});
return;
}
this.companyService.saveDepartment(departmentData).subscribe({
next: (savedDepartment) => {
const index = departmentData.id
? departments.findIndex(dep => dep.id === departmentData.id)
: -1;
if (index !== -1) {
// ✅ UPDATE
departments[index] = savedDepartment;
} else {
// ✅ CREATE
departments.push(savedDepartment);
}
// ✅ Reassign once (change detection + null safety)
this.departments = [...departments];
this.messageService.add({
severity: 'success',
summary: 'Successful',
detail: index !== -1
? 'Department Updated'
: 'Department Created',
life: 3000
});
this.departmentDialog = false;
this.department = undefined;
this.departmentForm.reset();
this.submitted = false;
},
error: (err) => {
console.error('Error saving department', err);
this.messageService.add({
severity: 'error',
summary: 'Error',
detail: 'Failed to save department',
life: 3000
});
}
});
}
}

View File

@@ -0,0 +1,183 @@
<div class="card">
<p-toast />
<p-toolbar class="mb-6">
<ng-template #start>
<p-button label="New Designation" 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)="loadAllDesignations()" />
</ng-template>
</p-toolbar>
<p-table
#dt
[value]="isLoading ? skeletonData : designations"
[rows]="5"
[columns]="cols"
[paginator]="true"
[globalFilterFields]="['designation']"
[tableStyle]="{ 'min-width': '75rem' }"
[(selection)]="selectedDesignations"
[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 Designations</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="designation">
<div class="flex items-center gap-2">
Designation
<p-sortIcon field="designation" />
</div>
</th>
<th pSortableColumn="departmentName" style="min-width: 10rem">
<div class="flex items-center gap-2">
Department
<p-sortIcon field="departmentName" />
</div>
</th>
<th pSortableColumn="hod" style="width: 4rem">
<div class="flex items-center gap-2">
HOD
<p-sortIcon field="hod" />
</div>
</th>
<th pSortableColumn="payGrade" style="min-width: 8rem">
<div class="flex items-center gap-2">
Pay Grade
<p-sortIcon field="payGrade" />
</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-designation let-rowIndex="rowIndex">
<tr *ngIf="!isLoading">
<td>{{ rowIndex + 1 }}</td>
<td>{{ designation.designation }}</td>
<td>{{ designation.departmentName }}</td>
<td>{{ designation.hod ? 'Yes' : 'No' }}</td>
<td>{{ designation.payGrade || 'N/A' }}</td>
<td>{{ designation.updatedAt | date:'dd/MM/yyyy HH:mm:ss' }}</td>
<td>
<span *ngIf="designation.updatedUser; else noUpdatedUser">
{{ designation.updatedUser }}
</span>
<ng-template #noUpdatedUser>
<em class="text-500 ">Not Available</em>
</ng-template>
</td>
<td>
<p-tag [value]="designation.active ? 'Active' : 'Inactive'" [severity]="getSeverity(designation.active)" />
</td>
<td>
<p-button icon="pi pi-pencil" class="mr-2" [rounded]="true" [outlined]="true" (click)="editDesignation(designation)" />
<p-button [icon]="designation.active ? 'pi pi-ban' : 'pi pi-check'" [severity]="designation.active ? 'danger' : 'success'" [rounded]="true" [outlined]="true" (click)="toggleActive(designation)" />
</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="4rem" 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>
<p-dialog [(visible)]="designationDialog" [style]="{'width': '50vw'}" [breakpoints]="{ '960px': '75vw', '640px': '90vw' }" header="Designation Details" [modal]="true">
<ng-template #content>
<form [formGroup]="designationForm" (ngSubmit)="saveDesignation()" class="mt-2">
<div class="grid mt-0">
<div class="col-12">
<p-floatlabel variant="on">
<input
id="designation"
pInputText
formControlName="designation"
fluid
[class.p-invalid]="isFieldInvalid('designation')"
pTooltip="{{ getErrorMessage('designation') }}"
tooltipPosition="top"
autofocus
/>
<label for="designation">Designation <span class="text-red-500">*</span></label>
</p-floatlabel>
</div>
<div class="col-12">
<p-floatlabel variant="on">
<p-select
id="departmentId"
formControlName="departmentId"
[options]="departments"
optionLabel="department"
optionValue="id"
[filter]="true"
filterBy="department"
fluid
appendTo="body"
[class.p-invalid]="isFieldInvalid('departmentId')"
pTooltip="{{ getErrorMessage('departmentId') }}"
tooltipPosition="top"
/>
<label for="departmentId">Department <span class="text-red-500">*</span></label>
</p-floatlabel>
</div>
<div class="col-12">
<label for="hod" class="flex align-items-center">
<p-checkbox
id="hod"
formControlName="hod"
binary="true"
/>
<span class="ml-2">Is HOD</span>
</label>
</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)="saveDesignation()" />
</ng-template>
</p-dialog>
<p-confirmDialog [style]="{ width: '450px' }" />
</div>

View File

@@ -0,0 +1,297 @@
import { DesignationDTO, DepartmentDTO } from './../../../../models/account.model';
import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core';
import { ConfirmationService, MessageService } from 'primeng/api';
import { TableModule, Table } from 'primeng/table';
import { DialogModule } from 'primeng/dialog';
import { RippleModule } from 'primeng/ripple';
import { ButtonModule } from 'primeng/button';
import { ToastModule } from 'primeng/toast';
import { ToolbarModule } from 'primeng/toolbar';
import { ConfirmDialogModule } from 'primeng/confirmdialog';
import { TextareaModule } from 'primeng/textarea';
import { CommonModule } from '@angular/common';
import { FileUploadModule } from 'primeng/fileupload';
import { SelectModule } from 'primeng/select';
import { TagModule } from 'primeng/tag';
import { RadioButtonModule } from 'primeng/radiobutton';
import { RatingModule } from 'primeng/rating';
import { SkeletonModule } from 'primeng/skeleton';
import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { InputNumberModule } from 'primeng/inputnumber';
import { IconFieldModule } from 'primeng/iconfield';
import { InputIconModule } from 'primeng/inputicon';
import { CompanyService } from '../../../../services/account/company/company.service';
import { ValidationService } from '../../../../services/utilities/validation.service';
import { TooltipModule } from 'primeng/tooltip';
import { CheckboxModule } from 'primeng/checkbox';
import { AutoCompleteModule } from 'primeng/autocomplete';
import { FloatLabelModule } from 'primeng/floatlabel';
interface Column {
field: string;
header: string;
customExportHeader?: string;
}
interface ExportColumn {
title: string;
dataKey: string;
}
@Component({
selector: 'app-designation',
templateUrl: './designation.component.html',
standalone: true,
imports: [
CommonModule, FormsModule, ReactiveFormsModule,
TableModule, DialogModule, ButtonModule, ToastModule, ToolbarModule, ConfirmDialogModule,
TextareaModule, FileUploadModule, SelectModule, TagModule,
RadioButtonModule, RatingModule, SkeletonModule, InputNumberModule, IconFieldModule, InputIconModule,
TooltipModule, AutoCompleteModule, RippleModule, CheckboxModule, FloatLabelModule
],
providers: [MessageService, ConfirmationService],
styleUrl: './designation.component.css'
})
export class DesignationComponent implements OnInit{
designationForm: FormGroup;
designationDialog: boolean = false;
designations!: DesignationDTO[];
designation: DesignationDTO | undefined;
selectedDesignations!: DesignationDTO[] | null;
submitted: boolean = false;
isLoading: boolean = true;
skeletonData: any[] = Array(10).fill({});
departments: DepartmentDTO[] = [];
statuses!: any[];
@ViewChild('dt') dt!: Table;
cols!: Column[];
exportColumns!: ExportColumn[];
constructor(
private companyService: CompanyService,
private messageService: MessageService,
private confirmationService: ConfirmationService,
private cd: ChangeDetectorRef,
private fb: FormBuilder,
) {
this.designationForm = this.fb.group({
id: [{ value: '', disabled: true }],
designation: [{ value: '', disabled: false }, [Validators.required, ValidationService.nameValidator()]],
departmentId: [{ value: null, disabled: false }, [Validators.required]],
hod: [{ value: false, disabled: false }],
active: [{ value: true, disabled: false }]
});
}
exportCSV() {
this.dt.exportCSV();
}
ngOnInit() {
this.loadAllDesignations();
this.loadDepartments();
}
loadDepartments() {
this.companyService.getAllDepartments().subscribe({
next: (data) => {
this.departments = data.filter(d => d.active);
this.cd.markForCheck();
},
error: (err) => {
console.error('Error loading departments', err);
}
});
}
loadAllDesignations() {
this.isLoading = true;
this.companyService.getAllDesignations().subscribe({
next: (data) => {
this.isLoading = false;
this.designations = data;
this.cd.markForCheck();
},
error: (err) => {
this.isLoading = false;
console.error(err);
}
});
this.statuses = [
{ label: 'Active', value: true },
{ label: 'Inactive', value: false }
];
this.cols = [
{ field: 'designation', header: 'Designation', customExportHeader: 'Designation' },
{ field: 'departmentName', header: 'Department' },
{ field: 'hod', header: 'HOD' },
{ field: 'updatedAt', header: 'Last Updated At' },
{ field: 'updatedUser', header: 'Last Updated By' }
];
this.exportColumns = this.cols.map((col) => ({ title: col.header, dataKey: col.field }));
}
openNew() {
this.designation = undefined;
this.designationForm.reset();
this.submitted = false;
this.designationDialog = true;
}
editDesignation(designation: DesignationDTO) {
const parentDepartment = this.departments.find(dep => dep.department === designation.departmentName);
console.log(parentDepartment);
this.designationForm.reset();
this.designation = { ...designation };
this.designation.departmentId = parentDepartment?.id ?? '';
console.log(this.designation);
this.designationForm.patchValue(this.designation);
this.designationDialog = true;
}
hideDialog() {
this.designationDialog = false;
this.submitted = false;
}
toggleActive(designation: DesignationDTO) {
const isActivating = !designation.active;
this.confirmationService.confirm({
message: `Are you sure you want to ${isActivating ? 'activate' : 'deactivate'} ` + designation.designation + '?',
header: 'Confirm',
icon: 'pi pi-exclamation-triangle',
rejectButtonStyleClass: 'p-button-text p-button-secondary',
acceptButtonStyleClass: isActivating ? 'p-button-success' : 'p-button-danger',
accept: () => {
this.companyService.activateDeactivateDesignation(designation.id, isActivating).subscribe({
next: (updatedDesignation) => {
designation.active = updatedDesignation.active;
this.designations = [...this.designations];
this.messageService.add({
severity: 'success',
summary: 'Successful',
detail: `Designation ${isActivating ? 'Activated' : 'Deactivated'}`,
life: 3000
});
},
error: (err) => {
console.error('Error toggling designation active status', err);
this.messageService.add({
severity: 'error',
summary: 'Error',
detail: 'Failed to update designation status',
life: 3000
});
}
});
}
});
}
getSeverity(status: boolean) {
switch (status) {
case true:
return 'success';
case false:
return 'warn';
}
}
getErrorMessage(fieldName: string): string {
const control = this.designationForm.get(fieldName);
return control ? ValidationService.getErrorMessage(control, fieldName) : '';
}
isFieldInvalid(fieldName: string): boolean {
const control = this.designationForm.get(fieldName);
return !!(control && control.invalid && (control.dirty || control.touched || this.submitted));
}
saveDesignation() {
this.submitted = true;
if (this.designationForm.invalid) {
return;
}
const designationData = this.designationForm.getRawValue() as DesignationDTO;
// ✅ Normalize null → empty array
const designations = this.designations ?? [];
// 🔍 Duplicate check (case-insensitive, department-specific)
const existing = designations.some(des =>
des.designation?.trim().toLowerCase() === designationData.designation?.trim().toLowerCase() &&
des.departmentId === designationData.departmentId &&
des.id !== designationData.id
);
if (existing) {
this.messageService.add({
severity: 'error',
summary: 'Validation Error',
detail: 'Designation already exists in this department',
life: 3000
});
return;
}
this.companyService.saveDesignation(designationData).subscribe({
next: (savedDesignation) => {
const index = designationData.id
? designations.findIndex(des => des.id === designationData.id)
: -1;
if (index !== -1) {
// ✅ UPDATE
designations[index] = savedDesignation;
} else {
// ✅ CREATE
designations.push(savedDesignation);
}
// ✅ Reassign once (change detection + null safety)
this.designations = [...designations];
this.messageService.add({
severity: 'success',
summary: 'Successful',
detail: index !== -1
? 'Designation Updated'
: 'Designation Created',
life: 3000
});
this.designationDialog = false;
this.designation = undefined;
this.designationForm.reset();
this.submitted = false;
},
error: (err) => {
console.error('Error saving designation', err);
this.messageService.add({
severity: 'error',
summary: 'Error',
detail: 'Failed to save designation',
life: 3000
});
}
});
}
}

View File

@@ -0,0 +1,407 @@
<div class="card">
<p-toast />
<p-toolbar class="mb-6">
<ng-template #start>
<p-button label="New Employee" 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)="loadAllEmployees()" />
</ng-template>
</p-toolbar>
<p-table
#dt
[value]="isLoading ? skeletonData : employees"
[rows]="5"
[columns]="cols"
[paginator]="true"
[globalFilterFields]="['fullName', 'contactNo', 'emailId']"
[tableStyle]="{ 'min-width': '75rem' }"
[(selection)]="selectedEmployees"
[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 Employees</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="subsidiaryName" style="min-width: 10rem">
<div class="flex items-center gap-2">
Subsidiary
<p-sortIcon field="subsidiaryName" />
</div>
</th>
<th pSortableColumn="department" style="min-width: 10rem">
<div class="flex items-center gap-2">
Department
<p-sortIcon field="department" />
</div>
</th>
<th pSortableColumn="designation" style="min-width: 10rem">
<div class="flex items-center gap-2">
Designation
<p-sortIcon field="designation" />
</div>
</th>
<th pSortableColumn="contactNo" style="min-width: 10rem">
<div class="flex items-center gap-2">
Contact No
<p-sortIcon field="contactNo" />
</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-employee let-rowIndex="rowIndex">
<tr *ngIf="!isLoading">
<td>{{ rowIndex + 1 }}</td>
<td>{{ employee.fullName }}</td>
<td>{{ employee.subsidiaryName }}</td>
<td>{{ employee.department }}</td>
<td>{{ employee.designation }}</td>
<td>{{ employee.contactNo }}</td>
<td>{{ employee.emailId }}</td>
<td>{{ employee.updatedAt | date:'dd/MM/yyyy HH:mm:ss' }}</td>
<td>
<span *ngIf="employee.updatedUser; else noUpdatedUser">
{{ employee.updatedUser }}
</span>
<ng-template #noUpdatedUser>
<em class="text-500 ">Not Available</em>
</ng-template>
</td>
<td>
<p-tag [value]="employee.active ? 'Active' : 'Inactive'" [severity]="getSeverity(employee.active)" />
</td>
<td>
<p-button icon="pi pi-pencil" class="mr-2" [rounded]="true" [outlined]="true" (click)="editEmployee(employee)" />
<p-button [icon]="employee.active ? 'pi pi-ban' : 'pi pi-check'" [severity]="employee.active ? 'danger' : 'success'" [rounded]="true" [outlined]="true" (click)="toggleActive(employee)" />
</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="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)]="employeeDialog" [style]="{'width': '50vw'}" [breakpoints]="{ '960px': '75vw', '640px': '90vw' }" header="Employee Details" [modal]="true">
<ng-template #content>
<form [formGroup]="employeeForm" (ngSubmit)="saveEmployee()" class="mt-2">
<div class="grid mt-0">
<div class="col-12">
<p-floatlabel variant="on">
<p-select
id="subsidiaryId"
formControlName="subsidiaryId"
[options]="subsidiaries"
optionLabel="name"
optionValue="id"
[filter]="true"
filterBy="name"
fluid
appendTo="body"
[class.p-invalid]="isFieldInvalid('subsidiaryId')"
pTooltip="{{ getErrorMessage('subsidiaryId') }}"
tooltipPosition="top"
autofocus
/>
<label for="subsidiaryId">Subsidiary <span class="text-red-500">*</span></label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<p-calendar
id="joiningDate"
formControlName="joiningDate"
dateFormat="dd/mm/yy"
[showIcon]="true"
fluid
appendTo="body"
/>
<label for="joiningDate">Joining Date</label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<input
id="employeeId"
pInputText
formControlName="employeeId"
fluid
[class.p-invalid]="isFieldInvalid('employeeId')"
pTooltip="{{ getErrorMessage('employeeId') }}"
tooltipPosition="top"
/>
<label for="employeeId">Employee ID <span class="text-red-500">*</span></label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<p-select
id="departmentId"
formControlName="departmentId"
[options]="departments"
optionLabel="department"
optionValue="id"
[filter]="true"
filterBy="department"
fluid
appendTo="body"
[class.p-invalid]="isFieldInvalid('departmentId')"
pTooltip="{{ getErrorMessage('departmentId') }}"
tooltipPosition="top"
/>
<label for="departmentId">Department <span class="text-red-500">*</span></label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<p-select
id="designationId"
formControlName="designationId"
[options]="designations"
optionLabel="designation"
optionValue="id"
[filter]="true"
filterBy="designation"
fluid
appendTo="body"
[class.p-invalid]="isFieldInvalid('designationId')"
pTooltip="{{ getErrorMessage('designationId') }}"
tooltipPosition="top"
/>
<label for="designationId">Designation <span class="text-red-500">*</span></label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<input
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>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<input
id="contactNo"
pInputText
formControlName="contactNo"
fluid
[class.p-invalid]="isFieldInvalid('contactNo')"
pTooltip="{{ getErrorMessage('contactNo') }}"
tooltipPosition="top"
/>
<label for="contactNo">Contact No <span class="text-red-500">*</span></label>
</p-floatlabel>
</div>
<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>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<p-select
id="gender"
formControlName="gender"
[options]="genders"
optionLabel="label"
optionValue="value"
fluid
appendTo="body"
[class.p-invalid]="isFieldInvalid('gender')"
pTooltip="{{ getErrorMessage('gender') }}"
tooltipPosition="top"
/>
<label for="gender">Gender <span class="text-red-500">*</span></label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<p-calendar
id="dob"
formControlName="dob"
dateFormat="dd/mm/yy"
[showIcon]="true"
fluid
appendTo="body"
[class.p-invalid]="isFieldInvalid('dob')"
pTooltip="{{ getErrorMessage('dob') }}"
tooltipPosition="top"
/>
<label for="dob">Date of Birth <span class="text-red-500">*</span></label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<input
id="alternateNo"
pInputText
formControlName="alternateNo"
fluid
[class.p-invalid]="isFieldInvalid('alternateNo')"
pTooltip="{{ getErrorMessage('alternateNo') }}"
tooltipPosition="top"
/>
<label for="alternateNo">Alternate Contact No</label>
</p-floatlabel>
</div>
<div class="col-12">
<h5>Permanent Address</h5>
<p-floatlabel variant="on">
<input
id="permanentAddress"
pInputText
formControlName="permanentAddress"
fluid
/>
<label for="permanentAddress">Address</label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<p-autoComplete
id="permanentCityName"
formControlName="permanentCityName"
[suggestions]="suggestions"
optionLabel="display"
[scrollHeight]="'160px'"
appendTo="body"
(completeMethod)="searchCities($event)"
(onSelect)="onSelectCity($event, 'permanent')"
fluid
/>
<label for="permanentCityName">City</label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<input
id="permanentStateName"
pInputText
formControlName="permanentStateName"
fluid
/>
<label for="permanentStateName">State</label>
</p-floatlabel>
</div>
<div class="col-12">
<h5>Residence Address</h5>
<p-floatlabel variant="on">
<input
id="residenceAddress"
pInputText
formControlName="residenceAddress"
fluid
/>
<label for="residenceAddress">Address</label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<p-autoComplete
id="residenceCityName"
formControlName="residenceCityName"
[suggestions]="suggestions"
optionLabel="display"
[scrollHeight]="'160px'"
appendTo="body"
(completeMethod)="searchCities($event)"
(onSelect)="onSelectCity($event, 'residence')"
fluid
/>
<label for="residenceCityName">City</label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<input
id="residenceStateName"
pInputText
formControlName="residenceStateName"
fluid
/>
<label for="residenceStateName">State</label>
</p-floatlabel>
</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)="saveEmployee()" />
</ng-template>
</p-dialog>
<p-confirmDialog [style]="{ width: '450px' }" />
</div>

View File

@@ -0,0 +1,440 @@
import { EmployeeDTO, SubsidiaryDTO, DepartmentDTO, DesignationDTO } from './../../../../models/account.model';
import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core';
import { ConfirmationService, MessageService } from 'primeng/api';
import { TableModule, Table } from 'primeng/table';
import { DialogModule } from 'primeng/dialog';
import { RippleModule } from 'primeng/ripple';
import { ButtonModule } from 'primeng/button';
import { ToastModule } from 'primeng/toast';
import { ToolbarModule } from 'primeng/toolbar';
import { ConfirmDialogModule } from 'primeng/confirmdialog';
import { InputTextModule } from 'primeng/inputtext';
import { TextareaModule } from 'primeng/textarea';
import { CommonModule } from '@angular/common';
import { FileUploadModule } from 'primeng/fileupload';
import { SelectModule } from 'primeng/select';
import { TagModule } from 'primeng/tag';
import { SkeletonModule } from 'primeng/skeleton';
import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { InputNumberModule } from 'primeng/inputnumber';
import { IconFieldModule } from 'primeng/iconfield';
import { InputIconModule } from 'primeng/inputicon';
import { CompanyService } from '../../../../services/account/company/company.service';
import { ValidationService } from '../../../../services/utilities/validation.service';
import { TooltipModule } from 'primeng/tooltip';
import { AutoCompleteModule } from 'primeng/autocomplete';
import { CalendarModule } from 'primeng/calendar';
import { MasterService } from '../../../../services/masters/master.service';
import { CityDTO, SearchDTO } from '../../../../models/masters/masters';
import { debounceTime, Subject } from 'rxjs';
import { Request } from '../../../../models/request.model';
import { FloatLabelModule } from 'primeng/floatlabel';
interface Column {
field: string;
header: string;
customExportHeader?: string;
}
interface ExportColumn {
title: string;
dataKey: string;
}
@Component({
selector: 'app-employee',
templateUrl: './employee.component.html',
standalone: true,
imports: [
CommonModule, FormsModule, ReactiveFormsModule,
TableModule, DialogModule, ButtonModule, ToastModule, ToolbarModule, ConfirmDialogModule,
InputTextModule, TextareaModule, FileUploadModule, SelectModule, TagModule,
SkeletonModule, InputNumberModule, IconFieldModule,
InputIconModule, TooltipModule, AutoCompleteModule, RippleModule, CalendarModule, FloatLabelModule
],
providers: [MessageService, ConfirmationService],
styleUrl: './employee.component.css'
})
export class EmployeeComponent implements OnInit{
employeeForm: FormGroup;
employeeDialog: boolean = false;
employees!: EmployeeDTO[];
employee: EmployeeDTO | undefined;
selectedEmployees!: EmployeeDTO[] | null;
submitted: boolean = false;
isLoading: boolean = true;
skeletonData: any[] = Array(10).fill({});
subsidiaries: SubsidiaryDTO[] = [];
departments: DepartmentDTO[] = [];
designations: DesignationDTO[] = [];
suggestions: CityDTO[] = [];
private searchSubject = new Subject<string>();
genders: any[] = [
{ label: 'Male', value: 'Male' },
{ label: 'Female', value: 'Female' },
{ label: 'Other', value: 'Other' }
];
statuses!: any[];
@ViewChild('dt') dt!: Table;
cols!: Column[];
exportColumns!: ExportColumn[];
constructor(
private companyService: CompanyService,
private masterService: MasterService,
private messageService: MessageService,
private confirmationService: ConfirmationService,
private cd: ChangeDetectorRef,
private fb: FormBuilder,
) {
this.employeeForm = this.fb.group({
id: [{ value: '', disabled: true }],
subsidiaryId: [{ value: null, disabled: false }, [Validators.required]],
departmentId: [{ value: null, disabled: false }, [Validators.required]],
designationId: [{ value: null, disabled: false }, [Validators.required]],
joiningDate: [{ value: '', disabled: false }],
employeeId: [{ value: '', disabled: false }, [Validators.required]],
fullName: [{ value: '', disabled: false }, [Validators.required, ValidationService.nameValidator()]],
gender: [{ value: '', disabled: false }, [Validators.required]],
dob: [{ value: '', disabled: false }, [Validators.required]],
contactNo: [{ value: '', disabled: false }, [Validators.required, ValidationService.mobileValidator()]],
alternateNo: [{ value: '', disabled: false }, [ValidationService.mobileValidator()]],
emailId: [{ value: '', disabled: false }, [Validators.required, ValidationService.emailValidator()]],
residenceAddress: [{ value: '', disabled: false }],
residenceCityId: [{ value: '', disabled: true }],
residenceStateId: [{ value: '', disabled: true }],
residenceCityName: [{ value: '', disabled: false }],
residenceStateName: [{ value: '', disabled: true }],
permanentAddress: [{ value: '', disabled: false }],
permanentCityId: [{ value: '', disabled: true }],
permanentStateId: [{ value: '', disabled: true }],
permanentCityName: [{ value: '', disabled: false }],
permanentStateName: [{ value: '', disabled: true }],
active: [{ value: true, disabled: false }]
});
}
exportCSV() {
this.dt.exportCSV();
}
ngOnInit() {
this.loadAllEmployees();
this.loadSubsidiaries();
this.loadDepartments();
this.loadDesignations();
}
loadSubsidiaries() {
this.companyService.getAllSubsidiaries().subscribe({
next: (data) => {
this.subsidiaries = data.filter(s => s.active);
this.cd.markForCheck();
},
error: (err) => {
console.error('Error loading subsidiaries', err);
}
});
}
loadDepartments() {
this.companyService.getAllDepartments().subscribe({
next: (data) => {
this.departments = data.filter(d => d.active);
this.cd.markForCheck();
},
error: (err) => {
console.error('Error loading departments', err);
}
});
}
loadDesignations() {
this.companyService.getAllDesignations().subscribe({
next: (data) => {
this.designations = data.filter(d => d.active);
this.cd.markForCheck();
},
error: (err) => {
console.error('Error loading designations', err);
}
});
}
searchCities(event: any) {
const query = event.query;
this.searchSubject.next(query);
}
onSelectCity(event: any, type: 'permanent' | 'residence') {
const city = event.value as CityDTO;
if (type === 'permanent') {
this.employeeForm.patchValue({
permanentCityId: city.id,
permanentStateId: city.stateId,
permanentCityName: city.cityName,
permanentStateName: city.stateName
});
} else {
this.employeeForm.patchValue({
residenceCityId: city.id,
residenceStateId: city.stateId,
residenceCityName: city.cityName,
residenceStateName: city.stateName
});
}
}
loadAllEmployees() {
this.isLoading = true;
this.companyService.getAllEmployees().subscribe({
next: (data) => {
this.isLoading = false;
this.employees = data;
this.cd.markForCheck();
},
error: (err) => {
this.isLoading = false;
console.error(err);
}
});
this.statuses = [
{ label: 'Active', value: true },
{ label: 'Inactive', value: false }
];
this.cols = [
{ field: 'fullName', header: 'Full Name', customExportHeader: 'Full Name' },
{ field: 'subsidiaryName', header: 'Subsidiary' },
{ field: 'department', header: 'Department' },
{ field: 'designation', header: 'Designation' },
{ field: 'contactNo', header: 'Contact No' },
{ field: 'emailId', header: 'Email' },
{ field: 'updatedAt', header: 'Last Updated At' },
{ field: 'updatedUser', header: 'Last Updated By' }
];
this.exportColumns = this.cols.map((col) => ({ title: col.header, dataKey: col.field }));
this.searchSubject.pipe(debounceTime(300)).subscribe(query => {
if (query && query.length >= 2) {
const requestPayload: Request<SearchDTO> = {
data: {
searchBy: 'CITY',
searchValue: query
},
compressed: true,
target: 'models.commons.Search'
};
this.masterService.searchCityStates(requestPayload).subscribe({
next: (cities) => {
this.suggestions = cities.map(city => ({
...city,
display: `${city.cityName}, ${city.stateName}`
}));
this.cd.markForCheck();
},
error: (err) => {
console.error('Error searching cities', err);
this.suggestions = [];
}
});
} else {
this.suggestions = [];
}
});
}
openNew() {
this.employee = undefined;
this.employeeForm.reset();
this.submitted = false;
this.employeeDialog = true;
}
editEmployee(employee: EmployeeDTO) {
const subsidiary = this.subsidiaries.find(sub => sub.name === employee.subsidiaryName);
const department = this.departments.find(dep => dep.department === employee.department);
const designation = this.designations.find(des => des.designation === employee.designation);
this.employeeForm.reset();
this.employee = { ...employee };
this.employee.subsidiaryId = subsidiary?.id ?? '';
this.employee.departmentId = department?.id ?? '';
this.employee.designationId = designation?.id ?? '';
// Prepare data for patching, converting dates
const employeeToPatch = { ...this.employee };
if (employeeToPatch.joiningDate) {
(employeeToPatch as any).joiningDate = new Date(employeeToPatch.joiningDate);
}
if (employeeToPatch.dob) {
(employeeToPatch as any).dob = new Date(employeeToPatch.dob);
}
this.employeeForm.patchValue(employeeToPatch);
this.employeeDialog = true;
console.log(this.employeeForm.getRawValue());
}
hideDialog() {
this.employeeDialog = false;
this.submitted = false;
}
toggleActive(employee: EmployeeDTO) {
const isActivating = !employee.active;
this.confirmationService.confirm({
message: `Are you sure you want to ${isActivating ? 'activate' : 'deactivate'} ` + employee.fullName + '?',
header: 'Confirm',
icon: 'pi pi-exclamation-triangle',
rejectButtonStyleClass: 'p-button-text p-button-secondary',
acceptButtonStyleClass: isActivating ? 'p-button-success' : 'p-button-danger',
accept: () => {
this.companyService.activateDeactivateEmployee(employee.id, isActivating).subscribe({
next: (updatedEmployee) => {
employee.active = updatedEmployee.active;
this.employees = [...this.employees];
this.messageService.add({
severity: 'success',
summary: 'Successful',
detail: `Employee ${isActivating ? 'Activated' : 'Deactivated'}`,
life: 3000
});
},
error: (err) => {
console.error('Error toggling employee active status', err);
this.messageService.add({
severity: 'error',
summary: 'Error',
detail: 'Failed to update employee status',
life: 3000
});
}
});
}
});
}
getSeverity(status: boolean) {
switch (status) {
case true:
return 'success';
case false:
return 'warn';
}
}
getErrorMessage(fieldName: string): string {
const control = this.employeeForm.get(fieldName);
return control ? ValidationService.getErrorMessage(control, fieldName) : '';
}
isFieldInvalid(fieldName: string): boolean {
const control = this.employeeForm.get(fieldName);
return !!(control && control.invalid && (control.dirty || control.touched || this.submitted));
}
saveEmployee() {
this.submitted = true;
if (this.employeeForm.invalid) {
return;
}
const employeeData = this.employeeForm.getRawValue() as EmployeeDTO;
// ✅ Normalize null → empty array
const employees = this.employees ?? [];
// 🔍 Duplicate check
const existing = employees.some(emp => {
if (emp.id === employeeData.id) return false;
const sameName = emp.fullName?.trim().toLowerCase() === employeeData.fullName?.trim().toLowerCase();
const empDob = emp.dob ? new Date(emp.dob) : null;
if (empDob) empDob.setHours(0, 0, 0, 0);
const dataDob = employeeData.dob ? new Date(employeeData.dob) : null;
if (dataDob) dataDob.setHours(0, 0, 0, 0);
const sameDob = empDob && dataDob ? empDob.getTime() === dataDob.getTime() : empDob === dataDob;
const sameGender = emp.gender === employeeData.gender;
const sameSubsidiary = emp.subsidiaryId === employeeData.subsidiaryId;
return sameName && sameDob && sameGender && sameSubsidiary;
});
if (existing) {
this.messageService.add({
severity: 'error',
summary: 'Validation Error',
detail: 'Employee with same name, dob, gender and subsidiary already exists',
life: 3000
});
return;
}
this.companyService.saveEmployee(employeeData).subscribe({
next: (savedEmployee) => {
const index = employeeData.id
? employees.findIndex(emp => emp.id === employeeData.id)
: -1;
if (index !== -1) {
// ✅ UPDATE
employees[index] = savedEmployee;
} else {
// ✅ CREATE
employees.push(savedEmployee);
}
// ✅ Reassign once (change detection + null safety)
this.employees = [...employees];
this.messageService.add({
severity: 'success',
summary: 'Successful',
detail: index !== -1
? 'Employee Updated'
: 'Employee Created',
life: 3000
});
this.employeeDialog = false;
this.employee = undefined;
this.employeeForm.reset();
this.submitted = false;
},
error: (err) => {
console.error('Error saving employee', err);
this.messageService.add({
severity: 'error',
summary: 'Error',
detail: 'Failed to save employee',
life: 3000
});
}
});
}
}

View File

@@ -0,0 +1,322 @@
<div class="card">
<p-toast />
<p-toolbar class="mb-6">
<ng-template #start>
<p-button label="New Subsidiary" 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)="loadAllSubsidiaries()" />
</ng-template>
</p-toolbar>
<p-table
#dt
[value]="isLoading ? skeletonData : subsidiaries"
[rows]="5"
[columns]="cols"
[paginator]="true"
[globalFilterFields]="['name', 'code']"
[tableStyle]="{ 'min-width': '75rem' }"
[(selection)]="selectedSubsidiaries"
[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 Subsidiaries</h4>
<p-iconfield>
<p-inputicon class="pi pi-search" />
<input pInputText type="text" (input)="onSearch($event)" placeholder="Search..." />
</p-iconfield>
</div>
</ng-template>
<ng-template #header>
<tr>
<th style="width: 3rem">#</th>
<th style="min-width: 5rem">Code</th>
<th pSortableColumn="name">
<div class="flex items-center gap-2">
Name
<p-sortIcon field="name" />
</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-subsidiary let-rowIndex="rowIndex">
<tr *ngIf="!isLoading">
<td>{{ rowIndex + 1 }}</td>
<td style="width: 5rem">{{ subsidiary.code }}</td>
<td>{{ subsidiary.name }}</td>
<td>
<span *ngIf="subsidiary.emailId; else noEmail">
{{ subsidiary.emailId }}
</span>
<ng-template #noEmail>
<em class="text-500 ">Not Available</em>
</ng-template>
</td>
<td>{{ subsidiary.updatedAt | date:'dd/MM/yyyy HH:mm:ss' }}</td>
<td>
<span *ngIf="subsidiary.updatedUser; else noUpdatedUser">
{{ subsidiary.updatedUser }}
</span>
<ng-template #noUpdatedUser>
<em class="text-500 ">Not Available</em>
</ng-template>
</td>
<td>
<p-tag [value]="subsidiary.active ? 'Active' : 'Inactive'" [severity]="getSeverity(subsidiary.active)" />
</td>
<td>
<p-button icon="pi pi-pencil" class="mr-2" [rounded]="true" [outlined]="true" (click)="editSubsidiary(subsidiary)" />
<p-button [icon]="subsidiary.active ? 'pi pi-ban' : 'pi pi-check'" [severity]="subsidiary.active ? 'danger' : 'success'" [rounded]="true" [outlined]="true" (click)="toggleActive(subsidiary)" />
</td>
</tr>
<tr *ngIf="isLoading">
<td><p-skeleton width="2rem" height="1.5rem"></p-skeleton></td>
<td><p-skeleton width="4rem" 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="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)]="subsidiaryDialog" [style]="{'width': '50vw'}" [breakpoints]="{ '960px': '75vw', '640px': '90vw' }" header="Subsidiary Details" [modal]="true">
<ng-template #content>
<form [formGroup]="subsidiaryForm" (ngSubmit)="saveSubsidiary()" class="mt-2">
<div class="grid mt-0">
<div class="col-12 md:col-3">
<p-floatlabel variant="on">
<input
id="code"
pInputText
formControlName="code"
fluid
[class.p-invalid]="isFieldInvalid('code')"
pTooltip="{{ getErrorMessage('code') }}"
tooltipPosition="top"
autofocus
/>
<label for="code">Code <span class="text-red-500">*</span></label>
</p-floatlabel>
</div>
<div class="col-12 md:col-9">
<p-floatlabel variant="on">
<input
id="name"
pInputText
formControlName="name"
fluid
[class.p-invalid]="isFieldInvalid('name')"
pTooltip="{{ getErrorMessage('name') }}"
tooltipPosition="top"
/>
<label for="name">Subsidiary Name <span class="text-red-500">*</span></label>
</p-floatlabel>
</div>
<div class="col-12 md:col-12">
<p-floatlabel variant="on">
<input
id="officeNo"
pInputText
formControlName="officeNo"
fluid
/>
<label for="officeNo">Office No., Floor, Building</label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<input
id="street"
pInputText
formControlName="street"
fluid
/>
<label for="street">Street</label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<input
id="locality"
pInputText
formControlName="locality"
fluid
/>
<label for="locality">Locality</label>
</p-floatlabel>
</div>
<div class="col-12 md:col-4">
<p-floatlabel variant="on">
<p-autoComplete
id="cityName"
formControlName="cityName"
[suggestions]="suggestions"
optionLabel="display"
[scrollHeight]="'160px'"
appendTo="body"
(completeMethod)="searchCities($event)"
(onSelect)="onSelectCity($event)"
fluid
/>
<label for="cityName">City</label>
</p-floatlabel>
</div>
<div class="col-12 md:col-4">
<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-4">
<p-floatlabel variant="on">
<input
id="pinCode"
pInputText
formControlName="pinCode"
fluid
[class.p-invalid]="isFieldInvalid('pinCode')"
pTooltip="{{ getErrorMessage('pinCode') }}"
tooltipPosition="top"
/>
<label for="pinCode">Pincode</label>
</p-floatlabel>
</div>
<div class="col-12 md:col-4">
<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</label>
</p-floatlabel>
</div>
<div class="col-12 md:col-4">
<p-floatlabel variant="on">
<input
id="contactNo"
pInputText
formControlName="contactNo"
fluid
[class.p-invalid]="isFieldInvalid('contactNo')"
pTooltip="{{ getErrorMessage('contactNo') }}"
tooltipPosition="top"
/>
<label for="contactNo">Contact No.</label>
</p-floatlabel>
</div>
<div class="col-12 md:col-4">
<p-floatlabel variant="on">
<input
id="contactPerson"
pInputText
formControlName="contactPerson"
fluid
[class.p-invalid]="isFieldInvalid('contactPerson')"
pTooltip="{{ getErrorMessage('contactPerson') }}"
tooltipPosition="top"
/>
<label for="contactPerson">Contact Person</label>
</p-floatlabel>
</div>
<div class="col-12 md:col-4">
<p-floatlabel variant="on">
<input
id="panNo"
pInputText
formControlName="panNo"
fluid
[class.p-invalid]="isFieldInvalid('panNo')"
pTooltip="{{ getErrorMessage('panNo') }}"
tooltipPosition="top"
/>
<label for="panNo">PAN No.</label>
</p-floatlabel>
</div>
<div class="col-12 md:col-4">
<p-floatlabel variant="on">
<input
id="cinNo"
pInputText
formControlName="cinNo"
fluid
[class.p-invalid]="isFieldInvalid('cinNo')"
pTooltip="{{ getErrorMessage('cinNo') }}"
tooltipPosition="top"
/>
<label for="cinNo">CIN No.</label>
</p-floatlabel>
</div>
<div class="col-12 md:col-4">
<p-floatlabel variant="on">
<input
id="msmeNo"
pInputText
formControlName="msmeNo"
fluid
[class.p-invalid]="isFieldInvalid('msmeNo')"
pTooltip="{{ getErrorMessage('msmeNo') }}"
tooltipPosition="top"
/>
<label for="msmeNo">MSME No.</label>
</p-floatlabel>
</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)="saveSubsidiary()" />
</ng-template>
</p-dialog>
<p-confirmDialog [style]="{ width: '450px' }" />
</div>

View File

@@ -0,0 +1,327 @@
import { SubsidiaryDTO } from './../../../../models/account.model';
import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core';
import { ConfirmationService, MessageService } from 'primeng/api';
import { TableModule, Table } from 'primeng/table';
import { DialogModule } from 'primeng/dialog';
import { RippleModule } from 'primeng/ripple';
import { ButtonModule } from 'primeng/button';
import { ToastModule } from 'primeng/toast';
import { ToolbarModule } from 'primeng/toolbar';
import { ConfirmDialogModule } from 'primeng/confirmdialog';
import { InputTextModule } from 'primeng/inputtext';
import { TextareaModule } from 'primeng/textarea';
import { CommonModule } from '@angular/common';
import { SelectModule } from 'primeng/select';
import { TagModule } from 'primeng/tag';
import { SkeletonModule } from 'primeng/skeleton';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { IconFieldModule } from 'primeng/iconfield';
import { InputIconModule } from 'primeng/inputicon';
import { CompanyService } from '../../../../services/account/company/company.service';
import { ValidationService } from '../../../../services/utilities/validation.service';
import { TooltipModule } from 'primeng/tooltip';
import { AutoCompleteModule } from 'primeng/autocomplete';
import { MasterService } from '../../../../services/masters/master.service';
import { CityDTO, SearchDTO } from '../../../../models/masters/masters';
import { debounceTime, Subject } from 'rxjs';
import { Request } from '../../../../models/request.model';
import { FloatLabelModule } from 'primeng/floatlabel';
interface Column {
field: string;
header: string;
customExportHeader?: string;
}
interface ExportColumn {
title: string;
dataKey: string;
}
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-subsidiary',
templateUrl: './subsidiary.component.html',
standalone: true,
imports: [
CommonModule, FormsModule, ReactiveFormsModule,
TableModule, DialogModule, ButtonModule, ToastModule, ToolbarModule, ConfirmDialogModule,
InputTextModule, TextareaModule, SelectModule, TagModule,
SkeletonModule, IconFieldModule,
InputIconModule, TooltipModule, AutoCompleteModule, RippleModule, FloatLabelModule
],
providers: [MessageService, ConfirmationService],
styleUrl: './subsidiary.component.css'
})
export class SubsidiaryComponent implements OnInit{
subsidiaryForm: FormGroup;
subsidiaryDialog: boolean = false;
subsidiaries!: SubsidiaryDTO[];
subsidiary!: SubsidiaryDTO;
selectedSubsidiaries!: SubsidiaryDTO[] | null;
submitted: boolean = false;
isLoading: boolean = true;
skeletonData: any[] = Array(10).fill({});
suggestions: CityDTO[] = [];
private searchSubject = new Subject<string>();
statuses!: any[];
@ViewChild('dt') dt!: Table;
cols!: Column[];
exportColumns!: ExportColumn[];
constructor(
private subsidiaryService: CompanyService,
private masterService: MasterService,
private messageService: MessageService,
private confirmationService: ConfirmationService,
private cd: ChangeDetectorRef,
private fb: FormBuilder,
) {
this.subsidiaryForm = this.fb.group({
id: [{ value: '', disabled: true }],
cityId: [{ value: null, disabled: true }],
code: [{ value: '', disabled: false }, [Validators.required, ValidationService.codeValidator()]],
name: [{ value: '', disabled: false }, [Validators.required, ValidationService.nameValidator()]],
officeNo: [{ value: '', disabled: false }],
street: [{ value: '', disabled: false }],
locality: [{ value: '', disabled: false }],
cityName: [{ value: '', disabled: false }],
stateName: [{ value: '', disabled: true }],
pinCode: [{ value: '', disabled: false }, [ValidationService.pincodeValidator()]],
emailId: [{ value: '', disabled: false }, [ValidationService.emailValidator()]],
contactNo: [{ value: '', disabled: false }, [ValidationService.mobileValidator()]],
contactPerson: [{ value: '', disabled: false }, [ValidationService.contactPersonValidator()]],
panNo: [{ value: '', disabled: false }, [ValidationService.panValidator()]],
cinNo: [{ value: '', disabled: false }, [ValidationService.cinValidator()]],
msmeNo: [{ value: '', disabled: false }, [ValidationService.msmeValidator()]],
stateId: [{ value: '', disabled: true }],
stateCode: [{ value: '', disabled: true }],
gstCode: [{ value: '', disabled: true }]
});
}
exportCSV() {
this.dt.exportCSV();
}
onSearch(event: Event) {
const input = event.target as HTMLInputElement;
this.dt.filterGlobal(input.value, 'contains');
}
searchCities(event: any) {
const query = event.query;
this.searchSubject.next(query);
}
onSelectCity(event: any) {
const city = event.value as CityDTO;
this.subsidiaryForm.patchValue({
cityId: city.id,
stateId: city.stateId,
cityName: city.cityName,
stateName: city.stateName,
stateCode: city.stateCode,
gstCode: city.gstCode
});
}
ngOnInit() {
this.loadAllSubsidiaries();
}
loadAllSubsidiaries() {
this.isLoading = true;
this.subsidiaryService.getAllSubsidiaries().subscribe({
next: (data) => {
this.isLoading = false;
this.subsidiaries = data;
this.cd.markForCheck();
},
error: (err) => {
this.isLoading = false;
console.error(err);
}
});
this.statuses = [
{ label: 'Active', value: true },
{ label: 'Inactive', value: false }
];
this.cols = [
{ field: 'code', header: 'Code', customExportHeader: 'Code' },
{ field: 'name', header: 'Name' },
{ field: 'officeNo', header: 'Office No / Building / Floor' },
{ field: 'street', header: 'Street / Road' },
{ field: 'locality', header: 'Locality' },
{ field: 'cityName', header: 'City' },
{ field: 'emailId', header: 'Email' },
{ field: 'contactNo', header: 'Contact No' },
{ field: 'contactPerson', header: 'Contact Person' },
{ field: 'panNo', header: 'PAN No.' },
{ field: 'cinNo', header: 'CIN No.' },
{ field: 'msmeNo', header: 'MSME No.' },
{ field: 'updatedAt', header: 'Last Updated At' },
{ field: 'updatedUser', header: 'Last Updated By' }
];
this.exportColumns = this.cols.map((col) => ({ title: col.header, dataKey: col.field }));
this.searchSubject.pipe(debounceTime(300)).subscribe(query => {
if (query && query.length >= 2) {
const requestPayload: Request<SearchDTO> = {
data: {
searchBy: 'CITY',
searchValue: query
},
compressed: true,
target: 'models.commons.Search'
};
this.masterService.searchCityStates(requestPayload).subscribe({
next: (cities) => {
this.suggestions = cities.map(city => ({
...city,
display: `${city.cityName}, ${city.stateName}`
}));
this.cd.markForCheck();
},
error: (err) => {
console.error('Error searching cities', err);
this.suggestions = [];
}
});
} else {
this.suggestions = [];
}
});
}
openNew() {
this.subsidiary = {};
this.subsidiaryForm.reset();
this.submitted = false;
this.subsidiaryDialog = true;
}
editSubsidiary(subsidiary: SubsidiaryDTO) {
this.subsidiaryForm.reset();
this.subsidiary = { ...subsidiary };
this.subsidiaryForm.patchValue(subsidiary);
this.subsidiaryDialog = true;
console.log(this.subsidiaryForm.getRawValue());
}
hideDialog() {
this.subsidiaryDialog = false;
this.submitted = false;
}
toggleActive(subsidiary: SubsidiaryDTO) {
const isActivating = !subsidiary.active;
this.confirmationService.confirm({
message: `Are you sure you want to ${isActivating ? 'activate' : 'deactivate'} ` + subsidiary.name + '?',
header: 'Confirm',
icon: 'pi pi-exclamation-triangle',
rejectButtonStyleClass: 'p-button-text p-button-secondary',
acceptButtonStyleClass: isActivating ? 'p-button-success' : 'p-button-danger',
accept: () => {
this.subsidiaryService.activateDeactivateSubsidiary(subsidiary.id?? '', isActivating).subscribe({
next: (updatedSubsidiary) => {
subsidiary.active = updatedSubsidiary.active;
this.subsidiaries = [...this.subsidiaries];
this.messageService.add({
severity: 'success',
summary: 'Successful',
detail: `Subsidiary ${isActivating ? 'Activated' : 'Deactivated'}`,
life: 3000
});
},
error: (err) => {
console.error('Error toggling subsidiary active status', err);
this.messageService.add({
severity: 'error',
summary: 'Error',
detail: 'Failed to update subsidiary status',
life: 3000
});
}
});
}
});
}
getSeverity(status: boolean) {
switch (status) {
case true:
return 'success';
case false:
return 'warn';
}
}
getErrorMessage(fieldName: string): string {
const control = this.subsidiaryForm.get(fieldName);
return control ? ValidationService.getErrorMessage(control, fieldName) : '';
}
isFieldInvalid(fieldName: string): boolean {
const control = this.subsidiaryForm.get(fieldName);
return !!(control && control.invalid && (control.dirty || control.touched || this.submitted));
}
saveSubsidiary() {
this.submitted = true;
if (this.subsidiaryForm.valid) {
const subsidiaryData = this.subsidiaryForm.getRawValue();
const existing = this.subsidiaries.find(sub => sub.id !== subsidiaryData.id && (sub.code === subsidiaryData.code || sub.name === subsidiaryData.name));
if (existing) {
this.messageService.add({
severity: 'error',
summary: 'Validation Error',
detail: 'Subsidiary code or name already exists',
life: 3000
});
return;
}
this.subsidiaryService.saveSubsidiary(subsidiaryData).subscribe({
next: (newSubsidiary) => {
this.subsidiaries.push(newSubsidiary);
this.subsidiaries = [...this.subsidiaries];
this.messageService.add({
severity: 'success',
summary: 'Successful',
detail: 'Subsidiary Created',
life: 3000
});
this.subsidiaryDialog = false;
this.subsidiary = {};
},
error: (err) => {
console.error('Error saving subsidiary', err);
this.messageService.add({
severity: 'error',
summary: 'Error',
detail: 'Failed to save subsidiary',
life: 3000
});
}
});
}
}
}

View File

@@ -0,0 +1,303 @@
<div class="card">
<p-toast />
<p-toolbar class="mb-6">
<ng-template pTemplate="start">
<p-button label="New User" icon="pi pi-plus" class="mr-2" (onClick)="openNew()" />
</ng-template>
<ng-template pTemplate="end">
<p-button label="Export" icon="pi pi-upload" severity="secondary" (onClick)="exportCSV()" styleClass="mx-2" />
<p-button label="Refresh" icon="pi pi-refresh" severity="info" (onClick)="loadAllUsers()" />
</ng-template>
</p-toolbar>
<p-table
#dt
[value]="isLoading ? skeletonData : users"
[rows]="5"
[columns]="cols"
[paginator]="true"
[globalFilterFields]="['loginId', 'displayName', 'employeeId', 'status']"
[tableStyle]="{ 'min-width': '75rem' }"
[(selection)]="selectedUsers"
[rowHover]="true"
dataKey="id"
currentPageReportTemplate="Showing {first} to {last} of {totalRecords} entries"
[showCurrentPageReport]="true"
>
<ng-template pTemplate="caption">
<div class="flex justify-content-between align-items-center">
<h4 class="m-0">Manage Users</h4>
<span class="p-input-icon-left">
<i class="pi pi-search"></i>
<input pInputText type="text" (input)="onSearch($event)" placeholder="Search..." />
</span>
</div>
</ng-template>
<ng-template pTemplate="header">
<tr>
<th style="width: 3rem">#</th>
<th pSortableColumn="loginId">
<div class="flex items-center gap-2">
Login ID
<p-sortIcon field="loginId" />
</div>
</th>
<th pSortableColumn="displayName" style="min-width: 10rem">
<div class="flex items-center gap-2">
Display Name
<p-sortIcon field="displayName" />
</div>
</th>
<th pSortableColumn="employeeId" style="min-width: 10rem">
<div class="flex items-center gap-2">
Employee ID
<p-sortIcon field="employeeId" />
</div>
</th>
<th pSortableColumn="status" style="width: 13rem">
<div class="flex items-center gap-2">
Status
<p-sortIcon field="status" />
</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">
Active
<p-sortIcon field="active" />
</div>
</th>
<th style="min-width: 8rem"></th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-user let-rowIndex="rowIndex">
<tr *ngIf="!isLoading">
<td>{{ rowIndex + 1 }}</td>
<td>{{ user.loginId }}</td>
<td>{{ user.displayName }}</td>
<td>
<span *ngIf="user.employeeId; else noEmployeeId">
{{ user.employeeId }}
</span>
<ng-template #noEmployeeId>
<em class="text-500 ">Not Available</em>
</ng-template>
</td>
<td>
<p-tag [value]="user.status" [severity]="getSeverity(user.status)" />
</td>
<td>{{ user.updatedAt | date:'dd/MM/yyyy HH:mm:ss' }}</td>
<td>
<span *ngIf="user.updatedUser; else noUpdatedUser">
{{ user.updatedUser }}
</span>
<ng-template #noUpdatedUser>
<em class="text-500 ">Not Available</em>
</ng-template>
</td>
<td>
<p-tag [value]="user.active ? 'Active' : 'Inactive'" [severity]="getSeverity(user.active ? 'Active' : 'Inactive')" />
</td>
<td>
<p-button icon="pi pi-pencil" class="mr-2" [rounded]="true" [outlined]="true" (click)="editUser(user)" />
<p-button [icon]="user.active ? 'pi pi-ban' : 'pi pi-check'" [severity]="user.active ? 'danger' : 'success'" [rounded]="true" [outlined]="true" (click)="toggleActive(user)" />
</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="10rem" 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="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)]="userDialog" [style]="{'width': '50vw'}" [breakpoints]="{ '960px': '75vw', '640px': '90vw' }" header="User Details" [modal]="true">
<ng-template pTemplate="content">
<form [formGroup]="userForm" (ngSubmit)="saveUser()" class="mt-2">
<div class="grid mt-0">
<div class="col-12">
<span class="p-float-label">
<input
id="loginId"
pInputText
formControlName="loginId"
fluid
[readonly]="!!user"
[class.p-invalid]="isFieldInvalid('loginId')"
pTooltip="{{ getErrorMessage('loginId') }}"
tooltipPosition="top"
autofocus
/>
<label for="loginId">Login ID <span class="text-red-500">*</span></label>
</span>
</div>
<div class="col-12">
<span class="p-float-label">
<input
id="displayName"
pInputText
formControlName="displayName"
fluid
[class.p-invalid]="isFieldInvalid('displayName')"
pTooltip="{{ getErrorMessage('displayName') }}"
tooltipPosition="top"
/>
<label for="displayName">Display Name <span class="text-red-500">*</span></label>
</span>
</div>
<div class="col-12">
<span class="p-float-label">
<p-autoComplete
id="employeeId"
formControlName="employeeId"
[suggestions]="suggestions"
optionLabel="display"
[scrollHeight]="'160px'"
appendTo="body"
(completeMethod)="searchEmployees($event)"
(onSelect)="onSelectEmployee($event)"
fluid
/>
<label for="employeeId">Employee ID</label>
</span>
</div>
<div class="col-12">
<span class="p-float-label">
<input
id="employeeName"
pInputText
formControlName="employeeName"
fluid
readonly
/>
<label for="employeeName">Employee Name</label>
</span>
</div>
<div class="col-12">
<span class="p-float-label">
<input
id="fatherName"
pInputText
formControlName="fatherName"
fluid
readonly
/>
<label for="fatherName">Father Name</label>
</span>
</div>
<div class="col-12 md:col-6">
<span class="p-float-label">
<input
id="department"
pInputText
formControlName="department"
fluid
readonly
/>
<label for="department">Department</label>
</span>
</div>
<div class="col-12 md:col-6">
<span class="p-float-label">
<input
id="designation"
pInputText
formControlName="designation"
fluid
readonly
/>
<label for="designation">Designation</label>
</span>
</div>
<div class="col-12 mb-3">
<span class="p-float-label">
<p-dropdown
id="status"
[options]="statuses"
formControlName="status"
optionLabel="label"
optionValue="value"
fluid
[class.p-invalid]="isFieldInvalid('status')"
pTooltip="{{ getErrorMessage('status') }}"
tooltipPosition="top"
/>
<label for="status">Status <span class="text-red-500">*</span></label>
</span>
</div>
</div>
<p-fieldset legend="User Roles" class="mt-4" [style]="{'background-color': 'rgba(243,243,243,0.5)'}">
<div class="grid mb-4 mt-2">
<div class="col-12 md:col-4">
<span class="p-float-label">
<p-dropdown [options]="branches" optionLabel="name" optionValue="id" fluid />
<label>Branch</label>
</span>
</div>
<div class="col-12 md:col-4">
<span class="p-float-label">
<p-dropdown [options]="userRolesOptions" optionLabel="name" optionValue="id" fluid />
<label>User Role</label>
</span>
</div>
<div class="col-12 md:col-4">
<p-button label="Add Role" icon="pi pi-plus" (onClick)="addRole()" />
</div>
</div>
<p-table [value]="userRoles" styleClass="p-datatable-sm">
<ng-template pTemplate="header">
<tr>
<th style="width: 3rem">#</th>
<th>Role Name</th>
<th>Group Name</th>
<th>Branch Name</th>
<th>Action</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-role let-rowIndex="rowIndex">
<tr>
<td>{{ rowIndex + 1 }}</td>
<td>{{role.roleName}}</td>
<td>{{role.groupName}}</td>
<td>{{role.branchName}}</td>
<td>
<p-button [icon]="role.active ? 'pi pi-trash' : 'pi pi-plus'" [severity]="role.active ? 'danger' : 'warn'" [rounded]="true" [outlined]="true" (onClick)="toggleRoleActive(role)" [pTooltip]="role.active ? 'Remove Role' : 'Enable Role'" />
</td>
</tr>
</ng-template>
<ng-template pTemplate="emptymessage">
<tr>
<td colspan="5">No roles assigned.</td>
</tr>
</ng-template>
</p-table>
</p-fieldset>
</form>
</ng-template>
<ng-template pTemplate="footer">
<p-button label="Cancel" icon="pi pi-times" text (click)="hideDialog()" />
<p-button label="Save" icon="pi pi-check" (click)="saveUser()" />
</ng-template>
</p-dialog>
<p-confirmDialog [style]="{ width: '450px' }" />
</div>

View File

@@ -0,0 +1,362 @@
import { UserDTO, UserRoleDTO } from '../../../models/user.model';
import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core';
import { ConfirmationService, MessageService } from 'primeng/api';
import { TableModule, Table } from 'primeng/table';
import { DialogModule } from 'primeng/dialog';
import { RippleModule } from 'primeng/ripple';
import { ButtonModule } from 'primeng/button';
import { ToastModule } from 'primeng/toast';
import { ToolbarModule } from 'primeng/toolbar';
import { ConfirmDialogModule } from 'primeng/confirmdialog';
import { InputTextModule } from 'primeng/inputtext';
import { TextareaModule } from 'primeng/textarea';
import { CommonModule } from '@angular/common';
import { FileUploadModule } from 'primeng/fileupload';
import { DropdownModule } from 'primeng/dropdown';
import { TagModule } from 'primeng/tag';
import { RadioButtonModule } from 'primeng/radiobutton';
import { RatingModule } from 'primeng/rating';
import { SkeletonModule } from 'primeng/skeleton';
import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { InputNumberModule } from 'primeng/inputnumber';
import { UserService } from '../../../services/account/user/user.service';
import { CompanyService } from '../../../services/account/company/company.service';
import { ValidationService } from '../../../services/utilities/validation.service';
import { TooltipModule } from 'primeng/tooltip';
import { AutoCompleteModule } from 'primeng/autocomplete';
import { FieldsetModule } from 'primeng/fieldset';
import { debounceTime, Subject } from 'rxjs';
interface Column {
field: string;
header: string;
customExportHeader?: string;
}
interface ExportColumn {
title: string;
dataKey: string;
}
@Component({
selector: 'app-user',
templateUrl: './user.component.html',
standalone: true,
imports: [
CommonModule, FormsModule, ReactiveFormsModule,
TableModule, DialogModule, ButtonModule, ToastModule, ToolbarModule, ConfirmDialogModule,
InputTextModule, TextareaModule, FileUploadModule, DropdownModule, TagModule,
RadioButtonModule, RatingModule, SkeletonModule, InputNumberModule,
TooltipModule, AutoCompleteModule, FieldsetModule, RippleModule
],
providers: [MessageService, ConfirmationService],
styleUrl: './user.component.css'
})
export class UserComponent implements OnInit{
userForm: FormGroup;
userDialog: boolean = false;
users!: UserDTO[];
user: UserDTO | undefined;
selectedUsers!: UserDTO[] | null;
submitted: boolean = false;
isLoading: boolean = true;
skeletonData: any[] = Array(10).fill({});
userRoles: UserRoleDTO[] = [];
branches: any[] = [];
userRolesOptions: any[] = [];
suggestions: any[] = [];
private searchSubject = new Subject<string>();
statuses!: any[];
@ViewChild('dt') dt!: Table;
cols!: Column[];
exportColumns!: ExportColumn[];
constructor(
private userService: UserService,
private companyService: CompanyService,
private messageService: MessageService,
private confirmationService: ConfirmationService,
private cd: ChangeDetectorRef,
private fb: FormBuilder,
) {
this.userForm = this.fb.group({
id: [{ value: '', disabled: true }],
loginId: [{ value: '', disabled: false }, [Validators.required]],
displayName: [{ value: '', disabled: false }, [Validators.required, ValidationService.nameValidator()]],
fkEmployeeId: [{ value: '', disabled: false }],
employeeId: [{ value: '', disabled: false }],
employeeName: [{ value: '', disabled: false }],
fatherName: [{ value: '', disabled: false }],
department: [{ value: '', disabled: false }],
designation: [{ value: '', disabled: false }],
status: [{ value: 'Active', disabled: false }, [Validators.required]],
active: [{ value: true, disabled: false }]
});
}
exportCSV() {
this.dt.exportCSV();
}
onSearch(event: Event) {
const input = event.target as HTMLInputElement;
this.dt.filterGlobal(input.value, 'contains');
}
ngOnInit() {
this.loadAllUsers();
}
loadAllUsers() {
this.isLoading = true;
this.userService.getAllUsers().subscribe({
next: (data) => {
this.isLoading = false;
this.users = data;
this.cd.markForCheck();
},
error: (err) => {
this.isLoading = false;
console.error(err);
}
});
this.statuses = [
{ label: 'Active', value: 'Active' },
{ label: 'Inactive', value: 'Inactive' }
];
this.cols = [
{ field: 'loginId', header: 'Login ID', customExportHeader: 'Login ID' },
{ field: 'displayName', header: 'Display Name' },
{ field: 'employeeId', header: 'Employee ID' },
{ field: 'status', header: 'Status' },
{ field: 'updatedAt', header: 'Last Updated At' },
{ field: 'updatedUser', header: 'Last Updated By' }
];
this.exportColumns = this.cols.map((col) => ({ title: col.header, dataKey: col.field }));
this.searchSubject.pipe(debounceTime(300)).subscribe(query => {
if (query && query.length >= 2) {
const requestPayload = {
data: {
searchBy: 'NAME',
searchValue: query
},
compressed: true,
target: 'models.commons.Search'
};
this.companyService.searchEmployees(requestPayload).subscribe({
next: (employees) => {
this.suggestions = employees.map(emp => ({
...emp,
display: `${emp.employeeId} - ${emp.fullName}`
}));
this.cd.markForCheck();
},
error: (err) => {
console.error('Error searching employees', err);
this.suggestions = [];
}
});
} else {
this.suggestions = [];
}
});
}
openNew() {
this.user = undefined;
this.userForm.reset();
this.userRoles = [];
this.submitted = false;
this.userDialog = true;
}
editUser(user: UserDTO) {
this.userForm.reset();
this.user = { ...user };
this.userRoles = [...(user.userRoles || [])];
this.userForm.patchValue(user);
this.userDialog = true;
console.log(this.userForm.getRawValue());
}
hideDialog() {
this.userDialog = false;
this.submitted = false;
}
toggleActive(user: UserDTO) {
const isActivating = !user.active;
this.confirmationService.confirm({
message: `Are you sure you want to ${isActivating ? 'activate' : 'deactivate'} ` + user.displayName + '?',
header: 'Confirm',
icon: 'pi pi-exclamation-triangle',
rejectButtonStyleClass: 'p-button-text p-button-secondary',
acceptButtonStyleClass: isActivating ? 'p-button-success' : 'p-button-danger',
accept: () => {
this.userService.activateDeactivateUser(user.id!, isActivating).subscribe({
next: (updatedUser) => {
user.active = updatedUser.active;
this.users = [...this.users];
this.messageService.add({
severity: 'success',
summary: 'Successful',
detail: `User ${isActivating ? 'Activated' : 'Deactivated'}`,
life: 3000
});
},
error: (err) => {
console.error('Error toggling user active status', err);
this.messageService.add({
severity: 'error',
summary: 'Error',
detail: 'Failed to update user status',
life: 3000
});
}
});
}
});
}
getSeverity(status: string) {
switch (status) {
case 'Active':
return 'success';
case 'Inactive':
return 'warning';
}
return 'info';
}
getErrorMessage(fieldName: string): string {
const control = this.userForm.get(fieldName);
return control ? ValidationService.getErrorMessage(control, fieldName) : '';
}
isFieldInvalid(fieldName: string): boolean {
const control = this.userForm.get(fieldName);
return !!(control && control.invalid && (control.dirty || control.touched || this.submitted));
}
addRole() {
// for now, do nothing, since selects empty
}
toggleRoleActive(role: UserRoleDTO) {
role.active = role.active === false ? true : false;
}
onSelectEmployee(event: any) {
const emp = event.value;
const patch: any = {
fkEmployeeId: emp.id,
employeeId: emp.employeeId,
employeeName: emp.fullName,
fatherName: emp.fatherName,
department: emp.department,
designation: emp.designation
};
if (!this.userForm.get('displayName')!.value) {
patch.displayName = emp.fullName;
}
this.userForm.patchValue(patch);
console.log(this.userForm);
}
searchEmployees(event: any) {
this.searchSubject.next(event.query);
}
saveUser() {
this.submitted = true;
if (this.userForm.invalid) {
return;
}
const userData = this.userForm.getRawValue() as UserDTO;
userData.userRoles = this.userRoles;
// Normalize null → empty array
const users = this.users ?? [];
// Duplicate check (case-insensitive)
const existing = users.some(u => u.id !== userData.id && (
u.loginId?.trim().toLowerCase() === userData.loginId?.trim().toLowerCase()
));
if (existing) {
this.messageService.add({
severity: 'error',
summary: 'Validation Error',
detail: 'User with same login ID already exists',
life: 3000
});
return;
}
this.userService.saveUser(userData).subscribe({
next: (savedUser) => {
const index = userData.id
? users.findIndex(u => u.id === userData.id)
: -1;
if (index !== -1) {
// UPDATE
users[index] = savedUser;
} else {
// CREATE
users.push(savedUser);
}
// Reassign once (change detection + null safety)
this.users = [...users];
this.messageService.add({
severity: 'success',
summary: 'Successful',
detail: index !== -1
? 'User Updated'
: 'User Created',
life: 3000
});
this.userDialog = false;
this.user = undefined;
this.userForm.reset();
this.submitted = false;
},
error: (err) => {
console.error('Error saving user', err);
this.messageService.add({
severity: 'error',
summary: 'Error',
detail: 'Failed to save user',
life: 3000
});
}
});
}
}

View File

@@ -0,0 +1,14 @@
:host ::ng-deep .p-dialog .p-button {
min-width: 6rem;
}
:host ::ng-deep .p-datatable .p-datatable-header {
border-top: none;
background-color: transparent;
}
/* Branch specific styles if necessary */
:host ::ng-deep .p-fieldset .p-fieldset-legend {
font-size: 1rem;
padding: 0.5rem;
}

View File

@@ -0,0 +1,419 @@
<div class="card">
<p-toast />
<p-toolbar class="mb-6">
<ng-template #start>
<p-button label="New Vendor" 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)="loadAllVendors()" />
</ng-template>
</p-toolbar>
<p-table
#dt
[value]="isLoading ? skeletonData : vendors"
[rows]="10"
[columns]="cols"
[paginator]="true"
[globalFilterFields]="['name', 'code', 'panNo', 'msmeNo']"
[tableStyle]="{ 'min-width': '75rem' }"
[(selection)]="selectedVendors"
[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 Vendors</h4>
<p-iconfield>
<p-inputicon class="pi pi-search" />
<input pInputText type="text" (input)="onSearch($event)" placeholder="Search..." />
</p-iconfield>
</div>
</ng-template>
<ng-template #header>
<tr>
<th style="width: 3rem">#</th>
<th style="min-width: 5rem">Code</th>
<th pSortableColumn="name">
<div class="flex items-center gap-2">
Name
<p-sortIcon field="name" />
</div>
</th>
<th pSortableColumn="panNo" style="min-width: 10rem">
<div class="flex items-center gap-2">
PAN No.
<p-sortIcon field="panNo" />
</div>
</th>
<th pSortableColumn="msmeNo" style="min-width: 10rem">
<div class="flex items-center gap-2">
MSME No.
<p-sortIcon field="msmeNo" />
</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-vendor let-rowIndex="rowIndex">
<tr *ngIf="!isLoading">
<td>{{ rowIndex + 1 }}</td>
<td style="width: 5rem">{{ vendor.code }}</td>
<td>{{ vendor.name }}</td>
<td>{{ vendor.panNo || '-' }}</td>
<td>{{ vendor.msmeNo || '-' }}</td>
<td>{{ vendor.updatedAt | date:'dd/MM/yyyy HH:mm:ss' }}</td>
<td>
<span *ngIf="vendor.updatedUser; else noUpdatedUser">
{{ vendor.updatedUser }}
</span>
<ng-template #noUpdatedUser>
<em class="text-500 ">Not Available</em>
</ng-template>
</td>
<td>
<p-tag [value]="vendor.active ? 'Active' : 'Inactive'" [severity]="getSeverity(vendor.active)" />
</td>
<td>
<p-button icon="pi pi-pencil" class="mr-2" [rounded]="true" [outlined]="true" (click)="editVendor(vendor)" />
<p-button [icon]="vendor.active ? 'pi pi-ban' : 'pi pi-check'" [severity]="vendor.active ? 'danger' : 'success'" [rounded]="true" [outlined]="true" (click)="toggleActive(vendor)" />
</td>
</tr>
<tr *ngIf="isLoading">
<td><p-skeleton width="2rem" height="1.5rem"></p-skeleton></td>
<td><p-skeleton width="4rem" 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="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>
<!-- Vendor Dialog -->
<p-dialog [(visible)]="vendorDialog" [style]="{'width': '70vw'}" [breakpoints]="{ '960px': '85vw', '640px': '95vw' }" header="Vendor Details" [modal]="true">
<ng-template #content>
<div class="card p-3">
<form [formGroup]="vendorForm" class="mt-2">
<div class="grid mt-0">
<div class="col-12 md:col-3">
<p-floatlabel variant="on">
<input
id="code"
pInputText
formControlName="code"
fluid
[class.p-invalid]="isFieldInvalid('code')"
pTooltip="{{ getErrorMessage('code') }}"
tooltipPosition="top"
autofocus
/>
<label for="code">Code <span class="text-red-500">*</span></label>
</p-floatlabel>
</div>
<div class="col-12 md:col-9">
<p-floatlabel variant="on">
<input
id="name"
pInputText
formControlName="name"
fluid
[class.p-invalid]="isFieldInvalid('name')"
pTooltip="{{ getErrorMessage('name') }}"
tooltipPosition="top"
/>
<label for="name">Vendor Name <span class="text-red-500">*</span></label>
</p-floatlabel>
</div>
<div class="col-12 md:col-4">
<p-floatlabel variant="on">
<input
id="panNo"
pInputText
formControlName="panNo"
fluid
[class.p-invalid]="isFieldInvalid('panNo')"
pTooltip="{{ getErrorMessage('panNo') }}"
tooltipPosition="top"
/>
<label for="panNo">PAN No.</label>
</p-floatlabel>
</div>
<div class="col-12 md:col-4">
<p-floatlabel variant="on">
<input
id="cinNo"
pInputText
formControlName="cinNo"
fluid
[class.p-invalid]="isFieldInvalid('cinNo')"
pTooltip="{{ getErrorMessage('cinNo') }}"
tooltipPosition="top"
/>
<label for="cinNo">CIN No.</label>
</p-floatlabel>
</div>
<div class="col-12 md:col-4">
<p-floatlabel variant="on">
<input
id="msmeNo"
pInputText
formControlName="msmeNo"
fluid
[class.p-invalid]="isFieldInvalid('msmeNo')"
pTooltip="{{ getErrorMessage('msmeNo') }}"
tooltipPosition="top"
/>
<label for="msmeNo">MSME No.</label>
</p-floatlabel>
</div>
</div>
</form>
</div>
<div class="card mt-3 p-3">
<div class="flex justify-content-between align-items-center mb-3">
<h5 class="m-0">Vendor Branches</h5>
<p-button label="Add Branch" icon="pi pi-plus" size="small" (onClick)="openNewBranch()" />
</div>
<p-table [value]="currentVendorBranches" [scrollable]="true" scrollHeight="300px">
<ng-template #header>
<tr>
<th>Code</th>
<th>Name</th>
<th>City</th>
<th>State</th>
<th>Contact No</th>
<th>Actions</th>
</tr>
</ng-template>
<ng-template #body let-branch let-i="rowIndex">
<tr>
<td>{{ branch.branchCode }}</td>
<td>{{ branch.branchName }}</td>
<td>{{ branch.cityName }}</td>
<td>{{ branch.stateName }}</td>
<td>{{ branch.contactNo }}</td>
<td>
<p-button icon="pi pi-pencil" [text]="true" severity="info" (onClick)="editBranch(branch, i)" />
<p-button icon="pi pi-trash" [text]="true" severity="danger" (onClick)="deleteBranch(i)" />
</td>
</tr>
</ng-template>
<ng-template #emptymessage>
<tr>
<td colspan="6" class="text-center">No branches added yet.</td>
</tr>
</ng-template>
</p-table>
</div>
</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)="saveVendor()" />
</ng-template>
</p-dialog>
<!-- Branch Dialog -->
<p-dialog [(visible)]="branchDialog" [style]="{'width': '50vw'}" [breakpoints]="{ '960px': '75vw', '640px': '90vw' }" header="Branch Details" [modal]="true">
<ng-template #content>
<form [formGroup]="branchForm" class="mt-2" autocomplete="off">
<div class="grid mt-0">
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<input
id="branchCode"
pInputText
formControlName="branchCode"
fluid
[class.p-invalid]="isFieldInvalid('branchCode', branchForm, submittedBranch)"
pTooltip="{{ getErrorMessage('branchCode', branchForm) }}"
tooltipPosition="top"
autofocus
/>
<label for="branchCode">Branch Code <span class="text-red-500">*</span></label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<input
id="branchName"
pInputText
formControlName="branchName"
fluid
[class.p-invalid]="isFieldInvalid('branchName', branchForm, submittedBranch)"
pTooltip="{{ getErrorMessage('branchName', branchForm) }}"
tooltipPosition="top"
/>
<label for="branchName">Branch Name <span class="text-red-500">*</span></label>
</p-floatlabel>
</div>
<div class="col-12 md:col-12">
<p-floatlabel variant="on">
<input
id="officeNo"
pInputText
formControlName="officeNo"
fluid
/>
<label for="officeNo">Office No., Floor, Building</label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<input
id="street"
pInputText
formControlName="street"
fluid
/>
<label for="street">Street</label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<input
id="locality"
pInputText
formControlName="locality"
fluid
/>
<label for="locality">Locality</label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<p-autoComplete
id="cityName"
formControlName="cityName"
[suggestions]="branchSuggestions"
optionLabel="display"
[scrollHeight]="'160px'"
appendTo="body"
(completeMethod)="searchBranchCities($event)"
(onSelect)="onSelectBranchCity($event)"
fluid
/>
<label for="cityName">City</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
[class.p-invalid]="isFieldInvalid('pinCode', branchForm, submittedBranch)"
pTooltip="{{ getErrorMessage('pinCode', branchForm) }}"
tooltipPosition="top"
/>
<label for="pinCode">Pincode</label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<input
id="emailId"
pInputText
formControlName="emailId"
fluid
[class.p-invalid]="isFieldInvalid('emailId', branchForm, submittedBranch)"
pTooltip="{{ getErrorMessage('emailId', branchForm) }}"
tooltipPosition="top"
/>
<label for="emailId">Email ID</label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<input
id="contactNo"
pInputText
formControlName="contactNo"
fluid
[class.p-invalid]="isFieldInvalid('contactNo', branchForm, submittedBranch)"
pTooltip="{{ getErrorMessage('contactNo', branchForm) }}"
tooltipPosition="top"
/>
<label for="contactNo">Contact No.</label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<input
id="contactPerson"
pInputText
formControlName="contactPerson"
fluid
[class.p-invalid]="isFieldInvalid('contactPerson', branchForm, submittedBranch)"
pTooltip="{{ getErrorMessage('contactPerson', branchForm) }}"
tooltipPosition="top"
/>
<label for="contactPerson">Contact Person</label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<input
id="gstNo"
pInputText
formControlName="gstNo"
fluid
[class.p-invalid]="isFieldInvalid('gstNo', branchForm, submittedBranch)"
pTooltip="{{ getErrorMessage('gstNo', branchForm) || 'Example: 22AAAAA0000A1Z5' }}"
tooltipPosition="top"
/>
<label for="gstNo">GST No.</label>
</p-floatlabel>
</div>
</div>
</form>
</ng-template>
<ng-template #footer>
<p-button label="Cancel" icon="pi pi-times" text (click)="hideBranchDialog()" />
<p-button label="Save Branch" icon="pi pi-check" (click)="saveBranch()" />
</ng-template>
</p-dialog>
<p-confirmDialog [style]="{ width: '450px' }" />
</div>

View File

@@ -0,0 +1,419 @@
import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core';
import { ConfirmationService, MessageService } from 'primeng/api';
import { TableModule, Table } from 'primeng/table';
import { DialogModule } from 'primeng/dialog';
import { RippleModule } from 'primeng/ripple';
import { ButtonModule } from 'primeng/button';
import { ToastModule } from 'primeng/toast';
import { ToolbarModule } from 'primeng/toolbar';
import { ConfirmDialogModule } from 'primeng/confirmdialog';
import { InputTextModule } from 'primeng/inputtext';
import { TextareaModule } from 'primeng/textarea';
import { CommonModule } from '@angular/common';
import { SelectModule } from 'primeng/select';
import { TagModule } from 'primeng/tag';
import { SkeletonModule } from 'primeng/skeleton';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators, FormArray } from '@angular/forms';
import { IconFieldModule } from 'primeng/iconfield';
import { InputIconModule } from 'primeng/inputicon';
import { ValidationService } from '../../../services/utilities/validation.service';
import { TooltipModule } from 'primeng/tooltip';
import { AutoCompleteModule } from 'primeng/autocomplete';
import { MasterService } from '../../../services/masters/master.service';
import { CityDTO, SearchDTO } from '../../../models/masters/masters';
import { debounceTime, Subject } from 'rxjs';
import { Request } from '../../../models/request.model';
import { FloatLabelModule } from 'primeng/floatlabel';
import { VendorService } from '../../../services/account/vendor/vendor.service';
import { VendorDTO, VendorBranchDTO } from '../../../models/account.model';
import { TabViewModule } from 'primeng/tabview';
interface Column {
field: string;
header: string;
customExportHeader?: string;
}
interface ExportColumn {
title: string;
dataKey: string;
}
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-vendor',
templateUrl: './vendor.component.html',
standalone: true,
imports: [
CommonModule, FormsModule, ReactiveFormsModule,
TableModule, DialogModule, ButtonModule, ToastModule, ToolbarModule, ConfirmDialogModule,
InputTextModule, TextareaModule, SelectModule, TagModule,
SkeletonModule, IconFieldModule,
InputIconModule, TooltipModule, AutoCompleteModule, RippleModule, FloatLabelModule, TabViewModule
],
providers: [MessageService, ConfirmationService],
styleUrl: './vendor.component.css'
})
export class VendorComponent implements OnInit{
vendorForm: FormGroup;
vendorDialog: boolean = false;
vendors!: VendorDTO[];
vendor!: VendorDTO;
selectedVendors!: VendorDTO[] | null;
submitted: boolean = false;
isLoading: boolean = true;
skeletonData: any[] = Array(10).fill({});
suggestions: CityDTO[] = [];
private searchSubject = new Subject<string>();
@ViewChild('dt') dt!: Table;
cols!: Column[];
exportColumns!: ExportColumn[];
// Branch related
branchDialog: boolean = false;
branchForm: FormGroup;
submittedBranch: boolean = false;
currentVendorBranches: VendorBranchDTO[] = [];
editingBranchIndex: number = -1; // -1 means new branch
branchSuggestions: CityDTO[] = [];
private branchSearchSubject = new Subject<string>();
constructor(
private vendorService: VendorService,
private masterService: MasterService,
private messageService: MessageService,
private confirmationService: ConfirmationService,
private cd: ChangeDetectorRef,
private fb: FormBuilder,
) {
this.vendorForm = this.fb.group({
id: [{ value: '', disabled: true }],
code: [{ value: '', disabled: false }, [Validators.required, ValidationService.codeValidator()]],
name: [{ value: '', disabled: false }, [Validators.required, ValidationService.nameValidator()]],
panNo: [{ value: '', disabled: false }, [ValidationService.panValidator()]],
cinNo: [{ value: '', disabled: false }, [ValidationService.cinValidator()]],
msmeNo: [{ value: '', disabled: false }, [ValidationService.msmeValidator()]],
active: [true]
});
this.branchForm = this.fb.group({
id: [{ value: '', disabled: true }],
branchCode: [{ value: '', disabled: false }, [Validators.required]],
branchName: [{ value: '', disabled: false }, [Validators.required]],
officeNo: [{ value: '', disabled: false }],
street: [{ value: '', disabled: false }],
locality: [{ value: '', disabled: false }],
cityName: [{ value: '', disabled: false }],
stateName: [{ value: '', disabled: true }],
cityId: [{ value: null, disabled: true }],
stateId: [{ value: '', disabled: true }],
pinCode: [{ value: '', disabled: false }, [ValidationService.pincodeValidator()]],
emailId: [{ value: '', disabled: false }, [ValidationService.emailValidator()]],
contactNo: [{ value: '', disabled: false }, [ValidationService.mobileValidator()]],
contactPerson: [{ value: '', disabled: false }, [ValidationService.contactPersonValidator()]],
gstNo: [{ value: '', disabled: false }, [Validators.pattern('^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}$')]], // GST Validator if needed
active: [true]
});
}
exportCSV() {
this.dt.exportCSV();
}
onSearch(event: Event) {
const input = event.target as HTMLInputElement;
this.dt.filterGlobal(input.value, 'contains');
}
// Branch City Search
searchBranchCities(event: any) {
const query = event.query;
this.branchSearchSubject.next(query);
}
onSelectBranchCity(event: any) {
const city = event.value as CityDTO;
this.branchForm.patchValue({
cityId: city.id,
stateId: city.stateId,
cityName: city.cityName,
stateName: city.stateName
});
}
ngOnInit() {
this.loadAllVendors();
this.cols = [
{ field: 'code', header: 'Code', customExportHeader: 'Code' },
{ field: 'name', header: 'Name' },
{ field: 'panNo', header: 'PAN No.' },
{ field: 'cinNo', header: 'CIN No.' },
{ field: 'msmeNo', header: 'MSME No.' },
{ field: 'updatedAt', header: 'Last Updated At' },
{ field: 'updatedUser', header: 'Last Updated By' }
];
this.exportColumns = this.cols.map((col) => ({ title: col.header, dataKey: col.field }));
this.branchSearchSubject.pipe(debounceTime(300)).subscribe(query => {
if (query && query.length >= 2) {
const requestPayload: Request<SearchDTO> = {
data: {
searchBy: 'CITY',
searchValue: query
},
compressed: true,
target: 'models.commons.Search'
};
this.masterService.searchCityStates(requestPayload).subscribe({
next: (cities) => {
this.branchSuggestions = cities.map(city => ({
...city,
display: `${city.cityName}, ${city.stateName}`
}));
this.cd.markForCheck();
},
error: (err) => {
console.error('Error searching cities', err);
this.branchSuggestions = [];
}
});
} else {
this.branchSuggestions = [];
}
});
}
loadAllVendors() {
this.isLoading = true;
this.vendorService.getAllVendors().subscribe({
next: (data) => {
this.isLoading = false;
this.vendors = data;
this.cd.markForCheck();
},
error: (err) => {
this.isLoading = false;
console.error(err);
}
});
}
openNew() {
this.vendor = { code: '', name: '', active: true, branches: [] };
this.vendorForm.reset();
this.vendorForm.patchValue({ active: true });
this.currentVendorBranches = [];
this.submitted = false;
this.vendorDialog = true;
}
editVendor(vendor: VendorDTO) {
this.vendorForm.reset();
this.vendor = { ...vendor };
this.currentVendorBranches = vendor.branches ? [...vendor.branches] : [];
this.vendorForm.patchValue(vendor);
this.vendorDialog = true;
}
hideDialog() {
this.vendorDialog = false;
this.submitted = false;
}
toggleActive(vendor: VendorDTO) {
const isActivating = !vendor.active;
this.confirmationService.confirm({
message: `Are you sure you want to ${isActivating ? 'activate' : 'deactivate'} ` + vendor.name + '?',
header: 'Confirm',
icon: 'pi pi-exclamation-triangle',
rejectButtonStyleClass: 'p-button-text p-button-secondary',
acceptButtonStyleClass: isActivating ? 'p-button-success' : 'p-button-danger',
accept: () => {
// Assuming activateDeactivateVendor exists or use save
/*
this.vendorService.activateDeactivateVendor(vendor.id!, isActivating).subscribe({
next: (updated) => {
vendor.active = updated.active; // or isActivating
this.messageService.add({severity:'success', summary: 'Successful', detail: `Vendor ${isActivating ? 'Activated' : 'Deactivated'}`, life: 3000});
},
error: () => this.messageService.add({severity:'error', summary: 'Error', detail: 'Failed to update status', life: 3000})
});
*/
// Using save for now as placeholder if specific endpoint not confirmed, but typically exists.
// I will simulate success for UI if backend not fully ready or use save.
vendor.active = isActivating;
this.vendorService.saveVendor(vendor).subscribe({
next: (res) => {
vendor.active = res.active;
this.messageService.add({
severity: 'success',
summary: 'Successful',
detail: `Vendor ${isActivating ? 'Activated' : 'Deactivated'}`,
life: 3000
});
},
error: (err) => {
vendor.active = !isActivating; // Revert
this.messageService.add({
severity: 'error',
summary: 'Error',
detail: 'Failed to update status',
life: 3000
});
}
})
}
});
}
getSeverity(status: boolean) {
switch (status) {
case true:
return 'success';
case false:
return 'warn';
}
}
getErrorMessage(fieldName: string, form: FormGroup = this.vendorForm): string {
const control = form.get(fieldName);
return control ? ValidationService.getErrorMessage(control, fieldName) : '';
}
isFieldInvalid(fieldName: string, form: FormGroup = this.vendorForm, submitted: boolean = this.submitted): boolean {
const control = form.get(fieldName);
return !!(control && control.invalid && (control.dirty || control.touched || submitted));
}
saveVendor() {
this.submitted = true;
if (this.vendorForm.valid) {
const vendorData = this.vendorForm.getRawValue();
vendorData.branches = this.currentVendorBranches;
// Check duplicates in list (simple client side check)
const existing = this.vendors.find(v => v.id !== vendorData.id && (v.code === vendorData.code || v.name === vendorData.name));
if (existing) {
this.messageService.add({
severity: 'error',
summary: 'Validation Error',
detail: 'Vendor code or name already exists',
life: 3000
});
return;
}
this.vendorService.saveVendor(vendorData).subscribe({
next: (newVendor) => {
if (vendorData.id) {
const index = this.vendors.findIndex(v => v.id === newVendor.id);
if (index !== -1) {
this.vendors[index] = newVendor;
}
} else {
this.vendors.push(newVendor);
}
this.vendors = [...this.vendors];
this.messageService.add({
severity: 'success',
summary: 'Successful',
detail: 'Vendor Saved',
life: 3000
});
this.vendorDialog = false;
this.vendor = {} as any;
},
error: (err) => {
console.error('Error saving vendor', err);
this.messageService.add({
severity: 'error',
summary: 'Error',
detail: 'Failed to save vendor',
life: 3000
});
}
});
}
}
/// BRANCH METHODS ///
openNewBranch() {
this.editingBranchIndex = -1;
this.branchForm.reset();
this.branchForm.patchValue({ active: true });
this.submittedBranch = false;
this.branchDialog = true;
}
editBranch(branch: VendorBranchDTO, index: number) {
this.editingBranchIndex = index;
this.branchForm.reset();
this.branchForm.patchValue(branch); // Needs correct mapping, especially city object for autocomplete
// If cityId is present but cityName is not in form search object format, we might need to handle it.
// Autocomplete expects an object with 'display' if strictly typed? No, form value is usually the string or object depending on config.
// Here we patch values, if cityId/stateId are there, we display text in cityName/stateName.
// But the autocomplete uses 'cityName' field? No, <p-autoComplete> uses 'cityName' form control.
// If we pass a string to it, it shows string. If object, it shows field.
// Let's assume fetching vendor returns cityName string.
// We might need to manually set the object for autocomplete if we want it to look "selected".
// But for now, simple patch.
if (branch.cityName) {
// For display purpose in autocomplete if it expects object
this.branchForm.patchValue({
cityName: { cityName: branch.cityName, stateName: branch.stateName, display: `${branch.cityName}, ${branch.stateName}`, id: branch.cityId, stateId: branch.stateId }
});
}
this.branchDialog = true;
}
deleteBranch(index: number) {
this.currentVendorBranches.splice(index, 1);
}
saveBranch() {
this.submittedBranch = true;
if (this.branchForm.valid) {
const branchData = this.branchForm.getRawValue();
// Extract city/state from autocomplete object if needed
if (typeof branchData.cityName === 'object') {
branchData.cityId = branchData.cityName.id;
branchData.stateId = branchData.cityName.stateId;
branchData.stateName = branchData.cityName.stateName;
branchData.cityName = branchData.cityName.cityName;
}
if (this.editingBranchIndex === -1) {
this.currentVendorBranches.push(branchData);
} else {
this.currentVendorBranches[this.editingBranchIndex] = branchData;
}
this.branchDialog = false;
this.branchForm.reset();
}
}
hideBranchDialog() {
this.branchDialog = false;
this.submittedBranch = false;
}
}

View File

@@ -0,0 +1,11 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-dashboard',
imports: [],
template: `<div class="p-5">
</div>`
})
export class DashboardComponent {
constructor(){}
}

View File

@@ -0,0 +1,106 @@
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ButtonModule } from 'primeng/button';
import { InputTextModule } from 'primeng/inputtext';
import { CardModule } from 'primeng/card';
import { InputGroupModule } from 'primeng/inputgroup';
import { InputGroupAddonModule } from 'primeng/inputgroupaddon';
import { PasswordModule } from 'primeng/password';
import { KeyFilterModule } from 'primeng/keyfilter';
import { DropdownModule } from 'primeng/dropdown';
import { MessageModule } from 'primeng/message';
import { Router } from '@angular/router';
import { SessionService } from './../../../services/commons/session.service';
import { HttpService } from '../../../services/http.service';
import { environment } from '../../../../environments/environment';
import { Company, Branch } from '../../../models/session.model';
import { Request } from '../../../models/request.model';
import { ResponseDto } from '../../../models/response.dto';
@Component({
selector: 'app-authorize',
standalone: true,
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
CardModule,
ButtonModule,
InputTextModule,
PasswordModule,
DropdownModule,
KeyFilterModule,
MessageModule
],
templateUrl: './authorize.component.html',
styleUrl: './authorize.component.css'
})
export class AuthorizeComponent implements OnInit {
companies: Company[] = [];
branches: Branch[] = [];
isCompanyDisabled: boolean = false;
authForm: FormGroup;
message?: string;
constructor(private fb: FormBuilder, private http: HttpService, private router: Router, private sessionService: SessionService) {
this.authForm = this.fb.group({
companyId: ['', Validators.required],
branchId: ['', Validators.required]
});
}
ngOnInit(){
const companyBranchRoles = this.sessionService.getItem('companies');
if (!companyBranchRoles) {
this.sessionService.logout();
return;
}
sessionStorage.removeItem('companies');
try {
this.companies = companyBranchRoles;
} catch (e) {
this.sessionService.logout();
}
if (this.companies.length === 1) {
this.authForm.patchValue({ companyId: this.companies[0].id });
this.isCompanyDisabled = true;
}
this.updateBranches();
this.authForm.get('companyId')?.valueChanges.subscribe(() => {
this.updateBranches();
});
}
private updateBranches(): void {
const companyId = this.authForm.get('companyId')?.value;
const selectedCompany = this.companies.find(c => c.id === companyId);
this.branches = selectedCompany?.branches || [];
this.authForm.patchValue({ branchId: '' });
}
onAuthorize() {
this.message = '';
if (this.authForm.valid) {
const requestPayload: Request = {
data: this.authForm.get('branchId')?.value
};
this.http.post<ResponseDto>(`${environment.authService}/3z4mkell5g5aset/authorize`, requestPayload).subscribe({
next: (response) => {
this.router.navigate(['/user']);
},
error: (err) => {
this.message = err.message;
}
});
} else {
alert('Please fill out the form correctly');
}
}
}

View File

@@ -0,0 +1,67 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { VendorDTO, VendorBranchDTO } from '../../../models/account.model';
import { SearchDTO } from '../../../models/masters/masters';
import { Request } from '../../../models/request.model';
import { environment } from '../../../../environments/environment';
@Injectable({
providedIn: 'root'
})
export class VendorService {
private apiUrl = `${environment.accountService}/vendor/vendors`;
constructor(private http: HttpClient) { }
getAllVendors(): Observable<VendorDTO[]> {
return this.http.get<VendorDTO[]>(this.apiUrl);
}
saveVendor(vendor: VendorDTO): Observable<VendorDTO> {
return this.http.post<VendorDTO>(this.apiUrl, vendor);
}
searchVendors(searchDTO: SearchDTO): Observable<VendorDTO[]> {
return this.http.post<VendorDTO[]>(`${this.apiUrl}/search`, searchDTO);
}
getVendorBranches(vendorId: string): Observable<VendorBranchDTO[]> {
return this.http.get<VendorBranchDTO[]>(`${this.apiUrl}/${vendorId}/branches`);
}
saveBranch(branch: VendorBranchDTO): Observable<VendorBranchDTO> {
return this.http.post<VendorBranchDTO>(`${this.apiUrl}/${branch.fkVendorId}/branches`, branch);
}
deleteBranch(vendorId: string, branchId: string): Observable<void> {
return this.http.delete<void>(`${this.apiUrl}/${vendorId}/branches/${branchId}`);
}
activateDeactivateVendor(id: string, active: boolean): Observable<VendorDTO> {
// Assuming backend follows similar pattern where update is used for everything or specific endpoint exists.
// Based on Subsidiary example, toggleActive uses a similar approach or custom endpoint.
// If not specific endpoint, fetching, changing active, and saving might be the way,
// OR if backend supports partial update.
// Re-reading frontend_integration.md:
// Create or Update Vendor is POST /
// So to activate/deactivate, we might need to send the object with updated status.
// BUT subsidiary used: this.subsidiaryService.activateDeactivateSubsidiary(subsidiary.id, isActivating)
// Let's assume a similar endpoint might be needed or we use the saveVendor for now if logic is inside.
// Actually, SubsidiaryService (CompanyService) likely has a specific method.
// I will implemented based on common patterns, if specific endpoint is missing in docs, I'll use save with updated status logic in component,
// OR add a specific endpoint if I can infer it exists.
// Let's look at Subsidiary implementation again.
// Subsidiary component calls: this.subsidiaryService.activateDeactivateSubsidiary(subsidiary.id, isActivating)
// I should probably check CompanyService to see how it does it.
// For now, I'll add a method that effectively does a save (or consistent with backend if I knew).
// The integration doc didn't list a specific activate/deactivate endpoint, just Create/Update.
// So I will assume Update (POST /) handles it.
// But verify with Subsidiary service...
// API: POST /cygnus/app/api/v1/account/vendor/vendors
// I'll stick to saveVendor for now. If I need a specific one, I'll add it.
// Wait, let me check CompanyService to see what activateDeactivateSubsidiary does.
return this.http.post<VendorDTO>(`${this.apiUrl}/${id}/activate-deactivate`, { active });
}
}

View File

@@ -0,0 +1,9 @@
export const environment = {
production: false,
encryptionKey: btoa('1234567890123456'),
authService: 'http://localhost:1699/cygnus/app/api/v1',
accountService: 'http://localhost:1701/cygnus/app/api/v1/account',
userService: 'http://localhost:1701/cygnus/app/api/v1/user',
masterService: 'http://localhost:1702/cygnus/app/api/v1/master',
rsaPublicKey: `MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq3RFV/f6ybsOF2m7NBLPUTMBq9b0frJG1HdIDYmrD9Wr1/aGBxTSJwq8IHFlatNpBF3OlJv9uEOybWMM1vXli4IgsuPPmcTOZsQ/O/9UGyBSL6apevNCw6pC1oa0MVLaN6COMAhDr+ri/PYiPQUcYsjDqghmAghMk99umHGUihz/oY/qgxzO+Q9cqePmjpH5c5RaXGBrOQxKoPlm7Uj6MqAfBhLC360VbcMot4XDoV+VeQXMzH0o6e870jdClsLOq1VsCA27jVvafj+HwaJ15ny9UWWilDuS/X8Sd7v+Rmd+qNezi6ROcglyaisXwKfeTWM8/o7HiUco2fL230+jEQIDAQAB`
};

View File

@@ -1,17 +1,191 @@
/* @import "primeng/resources/themes/aura-light-noir/theme.css"; */ /* Global Styles from Cygnus-UI */
/* @import "primeng/resources/primeng.min.css"; */
/* PrimeNG v18+ handles themes differently (often via Tailwind or presets).
For this prototype, we will rely on default component styles or add a CDN link if needed for quick styling.
However, PrimeIcons is still valid. */
/* Quill Editor Styles */
@import "quill/dist/quill.core.css";
@import "quill/dist/quill.snow.css";
@import "primeicons/primeicons.css"; @import "primeicons/primeicons.css";
@import 'primeflex/primeflex.css';
html, body {
margin: 0; @layer primeng, primeng-overrides;
font-family: var(--font-family);
background-color: var(--surface-ground); @font-face {
height: 100%; font-family: 'Roboto';
src: url('/assets/fonts/roboto/Roboto-Thin.ttf') format('truetype');
font-weight: 100;
font-style: normal;
} }
@font-face {
font-family: 'Roboto';
src: url('/assets/fonts/roboto/Roboto-ThinItalic.ttf') format('truetype');
font-weight: 100;
font-style: italic;
}
@font-face {
font-family: 'Roboto';
src: url('/assets/fonts/roboto/Roboto-Light.ttf') format('truetype');
font-weight: 300;
font-style: normal;
}
@font-face {
font-family: 'Roboto';
src: url('/assets/fonts/roboto/Roboto-LightItalic.ttf') format('truetype');
font-weight: 300;
font-style: italic;
}
@font-face {
font-family: 'Roboto';
src: url('/assets/fonts/roboto/Roboto-Regular.ttf') format('truetype');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'Roboto';
src: url('/assets/fonts/roboto/Roboto-Italic.ttf') format('truetype');
font-weight: 400;
font-style: italic;
}
@font-face {
font-family: 'Roboto';
src: url('/assets/fonts/roboto/Roboto-Medium.ttf') format('truetype');
font-weight: 500;
font-style: normal;
}
@font-face {
font-family: 'Roboto';
src: url('/assets/fonts/roboto/Roboto-MediumItalic.ttf') format('truetype');
font-weight: 500;
font-style: italic;
}
@font-face {
font-family: 'Roboto';
src: url('/assets/fonts/roboto/Roboto-SemiBold.ttf') format('truetype');
font-weight: 600;
font-style: normal;
}
@font-face {
font-family: 'Roboto';
src: url('/assets/fonts/roboto/Roboto-SemiBoldItalic.ttf') format('truetype');
font-weight: 600;
font-style: italic;
}
@font-face {
font-family: 'Roboto';
src: url('/assets/fonts/roboto/Roboto-Bold.ttf') format('truetype');
font-weight: 700;
font-style: normal;
}
@font-face {
font-family: 'Roboto';
src: url('/assets/fonts/roboto/Roboto-BoldItalic.ttf') format('truetype');
font-weight: 700;
font-style: italic;
}
@font-face {
font-family: 'Roboto';
src: url('/assets/fonts/roboto/Roboto-ExtraBold.ttf') format('truetype');
font-weight: 800;
font-style: normal;
}
@font-face {
font-family: 'Roboto';
src: url('/assets/fonts/roboto/Roboto-ExtraBoldItalic.ttf') format('truetype');
font-weight: 800;
font-style: italic;
}
@font-face {
font-family: 'Roboto';
src: url('/assets/fonts/roboto/Roboto-Black.ttf') format('truetype');
font-weight: 900;
font-style: normal;
}
@font-face {
font-family: 'Roboto';
src: url('/assets/fonts/roboto/Roboto-BlackItalic.ttf') format('truetype');
font-weight: 900;
font-style: italic;
}
body {
font-family: 'Roboto', sans-serif;
background-color: rgba(226,232,240,0.4);
margin: 0px 2px;
}
.fs-07{
font-size: 0.7rem;
}
.fs-08{
font-size: 0.8rem;
}
.fs-09{
font-size: 0.9rem;
}
.fs-1{
font-size: 1rem;
}
.fs-11{
font-size: 1.1rem;
}
.fs-12{
font-size: 1.2rem;
}
.fs-13{
font-size: 1.3rem;
}
.fs-14{
font-size: 1.4rem;
}
.fs-15{
font-size: 1.5rem;
}
/* Overrides */
.p-message .p-message-text {
font-size: 0.8rem;
}
.p-message-content {
justify-content: center;
}
.p-floatlabel label{
font-size: 0.9rem;
font-weight: normal !important;
}
@layer primeng-overrides {
/* Compact table styles */
.p-datatable .p-datatable-tbody > tr > td {
font-size: 0.875rem;
padding: 0.4rem 0.5rem !important;
}
.p-datatable .p-datatable-thead > tr > th {
font-size: 0.875rem;
padding: 0.75rem 0.75rem !important;
background-color: rgba(241, 245, 249, 1);
}
.p-autocomplete-item {
font-size: 0.875rem;
}
}
.p-fieldset .p-fieldset-legend {
/*background: var(--p-orange-500);*/
background: rgba(245,115,22,0.9);
color: white;
}

74
frontend_integration.md Normal file
View File

@@ -0,0 +1,74 @@
# Vendor and Vendor Branch Integration Guide
This document outlines the API endpoints and TypeScript interfaces to help integrate the Vendor and Vendor Branch features into your frontend project.
## 1. API Endpoints
**Base URL**: `/cygnus/app/api/v1/account/vendor/vendors`
| Method | Endpoint | Description | Payload |
|---|---|---|---|
| POST | `/` | Create or Update Vendor | `VendorDTO` |
| GET | `/` | Get All Vendors | (Query Params) |
| POST | `/search` | Search Vendors | `SearchDTO` |
| GET | `/{id}/branches` | Get Branches for Vendor | - |
| POST | `/{id}/branches` | Create or Update Branch | `VendorBranchDTO` |
| DELETE | `/{id}/branches/{branchId}` | Delete Branch | - |
## 2. TypeScript Interfaces
### Vendor
```typescript
export interface VendorDTO {
id?: string;
companyId?: string; // Encrypted Company ID
code: string;
name: string;
panNo?: string;
cinNo?: string;
msmeNo?: string;
createdUser?: string;
updatedAt?: string; // ISO Date String
updatedUser?: string;
active: boolean;
branches?: VendorBranchDTO[];
}
```
### Vendor Branch
```typescript
export interface VendorBranchDTO {
id?: string;
fkVendorId?: string; // Encrypted Vendor ID
branchCode: string;
branchName: string;
officeNo?: string;
street?: string;
locality?: string;
cityId?: string; // Encrypted City ID
stateId?: string; // Encrypted State ID
stateName?: string;
cityName?: string;
pinCode?: string;
emailId?: string;
contactNo?: string;
contactPerson?: string;
gstNo?: string;
createdUser?: string;
updatedAt?: string; // ISO Date String
updatedUser?: string;
active: boolean;
}
```
## 3. Usage Notes
- **IDs**: All IDs (`id`, `fkVendorId`, `companyId`, `cityId`, `stateId`) are strings and represent encrypted values.
- **Dates**: `updatedAt` is returned as an ISO string.
- **Search**: The search endpoint uses `SearchDTO` (likely existing in your frontend common types) to handle filters and pagination.
## Context Transfer
To reference this backend implementation context in another conversation, mention the **Conversation ID**:
`496255c5-4ca5-4e1d-908f-c9d456a6dfb8`