commit
This commit is contained in:
@@ -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:
|
||||||
|
|||||||
85
frontend/package-lock.json
generated
85
frontend/package-lock.json
generated
@@ -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": {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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]))
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
111
frontend/src/app/fragments/menu/menu.component.ts
Normal file
111
frontend/src/app/fragments/menu/menu.component.ts
Normal 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']);
|
||||||
|
}
|
||||||
|
}
|
||||||
145
frontend/src/app/interceptors/auth.interceptor.ts
Normal file
145
frontend/src/app/interceptors/auth.interceptor.ts
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
129
frontend/src/app/models/account.model.ts
Normal file
129
frontend/src/app/models/account.model.ts
Normal 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[];
|
||||||
|
}
|
||||||
@@ -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'});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|
||||||
@@ -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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
303
frontend/src/app/pages/account/user/user.component.html
Normal file
303
frontend/src/app/pages/account/user/user.component.html
Normal 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>
|
||||||
362
frontend/src/app/pages/account/user/user.component.ts
Normal file
362
frontend/src/app/pages/account/user/user.component.ts
Normal 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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
14
frontend/src/app/pages/account/vendor/vendor.component.css
generated
vendored
Normal file
14
frontend/src/app/pages/account/vendor/vendor.component.css
generated
vendored
Normal 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;
|
||||||
|
}
|
||||||
419
frontend/src/app/pages/account/vendor/vendor.component.html
generated
vendored
Normal file
419
frontend/src/app/pages/account/vendor/vendor.component.html
generated
vendored
Normal 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>
|
||||||
419
frontend/src/app/pages/account/vendor/vendor.component.ts
generated
vendored
Normal file
419
frontend/src/app/pages/account/vendor/vendor.component.ts
generated
vendored
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
11
frontend/src/app/pages/dashboard/dashboard.component.ts
Normal file
11
frontend/src/app/pages/dashboard/dashboard.component.ts
Normal 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(){}
|
||||||
|
}
|
||||||
106
frontend/src/app/pages/session/auth/authorize.component.ts
Normal file
106
frontend/src/app/pages/session/auth/authorize.component.ts
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
67
frontend/src/app/services/account/vendor/vendor.service.ts
vendored
Normal file
67
frontend/src/app/services/account/vendor/vendor.service.ts
vendored
Normal 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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
9
frontend/src/environments/environment.ts
Normal file
9
frontend/src/environments/environment.ts
Normal 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`
|
||||||
|
};
|
||||||
@@ -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
74
frontend_integration.md
Normal 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`
|
||||||
Reference in New Issue
Block a user