From 26478608b1cf013a03d23e50832acd683bffcd5f Mon Sep 17 00:00:00 2001 From: Narayanan Madaswamy Date: Thu, 21 May 2026 21:33:37 +0530 Subject: [PATCH] commit --- backend/database.py | 62 ++- frontend/package-lock.json | 85 +++- frontend/package.json | 6 +- frontend/src/app/app.config.ts | 12 +- frontend/src/app/app.routes.ts | 49 +- .../src/app/fragments/menu/menu.component.ts | 111 +++++ .../src/app/interceptors/auth.interceptor.ts | 145 ++++++ frontend/src/app/models/account.model.ts | 129 +++++ frontend/src/app/ocr/ocr.component.ts | 200 +------- .../department/department.component.html | 162 +++++++ .../department/department.component.ts | 284 +++++++++++ .../desigation/designation.component.html | 183 ++++++++ .../desigation/designation.component.ts | 297 ++++++++++++ .../company/employee/employee.component.html | 407 ++++++++++++++++ .../company/employee/employee.component.ts | 440 ++++++++++++++++++ .../subsidiary/subsidiary.component.html | 322 +++++++++++++ .../subsidiary/subsidiary.component.ts | 327 +++++++++++++ .../pages/account/user/user.component.html | 303 ++++++++++++ .../app/pages/account/user/user.component.ts | 362 ++++++++++++++ .../pages/account/vendor/vendor.component.css | 14 + .../account/vendor/vendor.component.html | 419 +++++++++++++++++ .../pages/account/vendor/vendor.component.ts | 419 +++++++++++++++++ .../pages/dashboard/dashboard.component.ts | 11 + .../pages/session/auth/authorize.component.ts | 106 +++++ .../services/account/vendor/vendor.service.ts | 67 +++ frontend/src/environments/environment.ts | 9 + frontend/src/styles.scss | 202 +++++++- frontend_integration.md | 74 +++ 28 files changed, 4948 insertions(+), 259 deletions(-) create mode 100644 frontend/src/app/fragments/menu/menu.component.ts create mode 100644 frontend/src/app/interceptors/auth.interceptor.ts create mode 100644 frontend/src/app/models/account.model.ts create mode 100644 frontend/src/app/pages/account/company/department/department.component.html create mode 100644 frontend/src/app/pages/account/company/department/department.component.ts create mode 100644 frontend/src/app/pages/account/company/desigation/designation.component.html create mode 100644 frontend/src/app/pages/account/company/desigation/designation.component.ts create mode 100644 frontend/src/app/pages/account/company/employee/employee.component.html create mode 100644 frontend/src/app/pages/account/company/employee/employee.component.ts create mode 100644 frontend/src/app/pages/account/company/subsidiary/subsidiary.component.html create mode 100644 frontend/src/app/pages/account/company/subsidiary/subsidiary.component.ts create mode 100644 frontend/src/app/pages/account/user/user.component.html create mode 100644 frontend/src/app/pages/account/user/user.component.ts create mode 100644 frontend/src/app/pages/account/vendor/vendor.component.css create mode 100644 frontend/src/app/pages/account/vendor/vendor.component.html create mode 100644 frontend/src/app/pages/account/vendor/vendor.component.ts create mode 100644 frontend/src/app/pages/dashboard/dashboard.component.ts create mode 100644 frontend/src/app/pages/session/auth/authorize.component.ts create mode 100644 frontend/src/app/services/account/vendor/vendor.service.ts create mode 100644 frontend/src/environments/environment.ts create mode 100644 frontend_integration.md diff --git a/backend/database.py b/backend/database.py index bbd8d60..af155b9 100644 --- a/backend/database.py +++ b/backend/database.py @@ -1,20 +1,19 @@ import os 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.orm import sessionmaker, relationship -from sqlalchemy.dialects.postgresql import JSONB -from sqlalchemy.sql import func from dotenv import load_dotenv # Load environment variables load_dotenv() -DB_USER = os.getenv("DB_USER") -DB_PASSWORD = os.getenv("DB_PASSWORD") -DB_HOST = os.getenv("DB_HOST") -DB_PORT = os.getenv("DB_PORT") -DB_NAME = os.getenv("DB_NAME") +DB_USER = os.getenv("DB_USER", "postgres") +DB_PASSWORD = os.getenv("DB_PASSWORD", "M@tr!x#149@dm!N") +DB_HOST = os.getenv("DB_HOST", "192.168.0.111") +DB_PORT = os.getenv("DB_PORT", "7925") +DB_NAME = os.getenv("DB_NAME", "ocr") encoded_user = urllib.parse.quote_plus(DB_USER) encoded_password = urllib.parse.quote_plus(DB_PASSWORD) @@ -27,6 +26,26 @@ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) 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): __tablename__ = "emails" @@ -46,33 +65,10 @@ class Attachment(Base): email_id = Column(Integer, ForeignKey("emails.id")) filename = Column(String) content_type = Column(String) - file_path = Column(String, nullable=True) # Path to file on disk - file_content = Column(LargeBinary, nullable=True) # Stored in DB (for small files) - + file_content = Column(LargeBinary) + 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(): db = SessionLocal() try: diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6a48e3b..220b2fc 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -16,8 +16,10 @@ "@angular/forms": "^21.1.0", "@angular/platform-browser": "^21.1.0", "@angular/router": "^21.1.0", + "@primeng/themes": "^19.1.0", + "primeflex": "^4.0.0", "primeicons": "^7.0.0", - "primeng": "^17.18.0", + "primeng": "^19.1.0", "quill": "^2.0.3", "rxjs": "~7.8.0", "tslib": "^2.3.0", @@ -3001,6 +3003,37 @@ "license": "MIT", "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": { "version": "1.0.0-beta.58", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.58.tgz", @@ -6570,6 +6603,12 @@ "dev": true, "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": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/primeicons/-/primeicons-7.0.0.tgz", @@ -6577,19 +6616,45 @@ "license": "MIT" }, "node_modules/primeng": { - "version": "17.18.0", - "resolved": "https://registry.npmjs.org/primeng/-/primeng-17.18.0.tgz", - "integrity": "sha512-EcvU/0Ex9QoBR6g6db9fDTCTAmzokW70TV5Oroy2gdvXRr3eqlflnOBoArQsmxTaw1oxSsu68YVj3RvcKYWhTg==", - "license": "MIT", + "version": "19.1.4", + "resolved": "https://registry.npmjs.org/primeng/-/primeng-19.1.4.tgz", + "integrity": "sha512-l5l8SHTxopxxyyXZx1BvbQ11P7ndLv2Qp8H5k2/+OCi65jTZn4xmtrBDGDs7k2K5UMHSqAnGjBgtpbckyqQETg==", + "license": "SEE LICENSE IN LICENSE.md", "dependencies": { + "@primeuix/styled": "^0.3.2", + "@primeuix/utils": "^0.3.2", "tslib": "^2.3.0" }, "peerDependencies": { - "@angular/common": "^17.0.0 || ^18.0.0", - "@angular/core": "^17.0.0 || ^18.0.0", - "@angular/forms": "^17.0.0 || ^18.0.0", - "rxjs": "^6.0.0 || ^7.8.1", - "zone.js": "~0.14.0" + "@angular/animations": "^19.0.0", + "@angular/cdk": "^19.0.0", + "@angular/common": "^19.0.0", + "@angular/core": "^19.0.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": { diff --git a/frontend/package.json b/frontend/package.json index 679ab77..626ead1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "scripts": { "ng": "ng", - "start": "ng serve", + "start": "ng serve --proxy-config proxy.conf.json", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test" @@ -31,8 +31,10 @@ "@angular/forms": "^21.1.0", "@angular/platform-browser": "^21.1.0", "@angular/router": "^21.1.0", + "@primeng/themes": "^19.1.0", + "primeflex": "^4.0.0", "primeicons": "^7.0.0", - "primeng": "^17.18.0", + "primeng": "^19.1.0", "quill": "^2.0.3", "rxjs": "~7.8.0", "tslib": "^2.3.0", diff --git a/frontend/src/app/app.config.ts b/frontend/src/app/app.config.ts index 9118a55..98c83a9 100644 --- a/frontend/src/app/app.config.ts +++ b/frontend/src/app/app.config.ts @@ -1,7 +1,10 @@ import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; import { provideRouter } from '@angular/router'; 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'; @@ -10,6 +13,11 @@ export const appConfig: ApplicationConfig = { provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideAnimationsAsync(), - provideHttpClient() + providePrimeNG({ + theme: { + preset: Aura + } + }), + provideHttpClient(withInterceptors([AuthInterceptor])) ] }; diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index 8199137..1a6c481 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -3,19 +3,40 @@ import { LoginComponent } from './login/login.component'; import { AdminLayoutComponent } from './admin-layout/admin-layout.component'; import { OcrComponent } from './ocr/ocr.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 = [ - { path: 'login', component: LoginComponent }, - { - path: '', - component: AdminLayoutComponent, - canActivate: [AuthGuard], - children: [ - { path: '', redirectTo: 'ocr', pathMatch: 'full' }, - { path: 'ocr', component: OcrComponent }, - { path: 'mailbox', component: MailboxComponent } - ] - }, - { path: '**', redirectTo: '' } -]; + { path: '', component: LoginComponent }, + { path: 'authorize', component: AuthorizeComponent, canActivate: [AuthorizeGuard]}, + { path: 'user', + component: AdminLayoutComponent, + canActivate: [AuthorizeGuard], + canActivateChild: [AuthorizeGuard], + children: [ + { path: 'profile', component: ProfileComponent }, + { path: 'mailbox', component: MailboxComponent }, + { path: 'ocr', component: OcrComponent } + ] + }, + { 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 } + ] + } +]; \ No newline at end of file diff --git a/frontend/src/app/fragments/menu/menu.component.ts b/frontend/src/app/fragments/menu/menu.component.ts new file mode 100644 index 0000000..926a6fa --- /dev/null +++ b/frontend/src/app/fragments/menu/menu.component.ts @@ -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']); + } +} \ No newline at end of file diff --git a/frontend/src/app/interceptors/auth.interceptor.ts b/frontend/src/app/interceptors/auth.interceptor.ts new file mode 100644 index 0000000..cd5148d --- /dev/null +++ b/frontend/src/app/interceptors/auth.interceptor.ts @@ -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, + next: HttpHandlerFn +): Observable> => { + + 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, + 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 + ); + } + } +} \ No newline at end of file diff --git a/frontend/src/app/models/account.model.ts b/frontend/src/app/models/account.model.ts new file mode 100644 index 0000000..5ddd8fd --- /dev/null +++ b/frontend/src/app/models/account.model.ts @@ -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) { + 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[]; +} \ No newline at end of file diff --git a/frontend/src/app/ocr/ocr.component.ts b/frontend/src/app/ocr/ocr.component.ts index a5b23fa..a1cbe68 100644 --- a/frontend/src/app/ocr/ocr.component.ts +++ b/frontend/src/app/ocr/ocr.component.ts @@ -7,13 +7,8 @@ import { MessageService } from 'primeng/api'; // PrimeNG import { FileUploadModule } from 'primeng/fileupload'; import { ProgressBarModule } from 'primeng/progressbar'; -import { InputTextareaModule } from 'primeng/inputtextarea'; +import { TextareaModule } from 'primeng/textarea'; import { ToastModule } from 'primeng/toast'; -import { ButtonModule } from 'primeng/button'; -import { TableModule } from 'primeng/table'; -import { CardModule } from 'primeng/card'; -import { RadioButtonModule } from 'primeng/radiobutton'; -import { InputTextModule } from 'primeng/inputtext'; @Component({ selector: 'app-ocr', @@ -23,19 +18,17 @@ import { InputTextModule } from 'primeng/inputtext'; FormsModule, FileUploadModule, ProgressBarModule, - InputTextareaModule, - ToastModule, - ButtonModule, - TableModule, - CardModule, - RadioButtonModule, - InputTextModule + TextareaModule, + ToastModule ], providers: [MessageService], template: `

OCR Extraction

- +
-
-
-

Extracted Text Result:

- - -
-
- -
- - -
-
- - -
-
- - - -
-
- -
-
-

AI Analysis Result:

- -
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- - - - - Description - Qty - Price - Total - - - - - - - - - - - {{item.description}} - - - - - - - - - - {{item.quantity}} - - - - - - - - - - {{item.unit_price}} - - - - - - - - - - {{item.total}} - - - - - - -
+
+

Extracted Text Result:

+
@@ -177,27 +65,16 @@ import { InputTextModule } from 'primeng/inputtext'; export class OcrComponent { extractedText: string | null = null; loading: boolean = false; - - aiLoading: boolean = false; - saveLoading: boolean = false; - aiResult: any = null; - - // Hybrid AI Props - modelType: string = 'text'; - filePath: string | null = null; constructor(private ocrService: OcrService, private messageService: MessageService) {} onUpload(event: any) { this.loading = true; - this.aiResult = null; // Reset AI result on new upload - this.filePath = null; const file = event.files[0]; this.ocrService.extractText(file).subscribe({ next: (res) => { this.extractedText = res.text; - this.filePath = res.file_path; this.loading = false; this.messageService.add({severity:'success', summary:'Success', detail:'Text Extracted Successfully'}); }, @@ -211,50 +88,5 @@ export class OcrComponent { onClear() { this.extractedText = null; - this.aiResult = null; - this.filePath = null; - } - - processWithAI() { - if (!this.extractedText) return; - - this.aiLoading = true; - // Pass text, filePath, and modelType - this.ocrService.extractWithAI(this.extractedText, this.filePath, this.modelType).subscribe({ - next: (res) => { - this.aiResult = res; - this.aiLoading = false; - this.messageService.add({severity:'success', summary:'AI Processing Complete', detail:'Data Extracted'}); - }, - error: (err) => { - console.error(err); - this.aiLoading = false; - this.messageService.add({severity:'error', summary:'AI Error', detail:'Could not process with AI'}); - } - }); - } - - saveDocument() { - if (!this.aiResult || !this.filePath) return; - - this.saveLoading = true; - const payload = { - vendor_name: this.aiResult.vendor_name || 'Unknown Vendor', - file_path: this.filePath, - model_type: this.modelType, - data: this.aiResult - }; - - this.ocrService.saveDocument(payload).subscribe({ - next: (res) => { - this.saveLoading = false; - this.messageService.add({severity:'success', summary:'Saved & Verified', detail:'Document and rules saved'}); - }, - error: (err) => { - console.error(err); - this.saveLoading = false; - this.messageService.add({severity:'error', summary:'Save Error', detail:'Failed to save document'}); - } - }); } } diff --git a/frontend/src/app/pages/account/company/department/department.component.html b/frontend/src/app/pages/account/company/department/department.component.html new file mode 100644 index 0000000..59f3d7d --- /dev/null +++ b/frontend/src/app/pages/account/company/department/department.component.html @@ -0,0 +1,162 @@ +
+ + + + + + + + + + + + + + +
+

Manage Departments

+ + + + +
+
+ + + # + +
+ Department + +
+ + +
+ Parent Department + +
+ + +
+ Updated At + +
+ + +
+ Updated By + +
+ + +
+ Status + +
+ + + +
+ + + {{ rowIndex + 1 }} + {{ department.department }} + + + {{ department.parentDepartmentName }} + + + + Not Available + + + {{ department.updatedAt | date:'dd/MM/yyyy HH:mm:ss' }} + + + {{ department.updatedUser }} + + + + Not Available + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+
+ + + + +
+
+ + + + +
+
+
+
+ + + + + +
+ + +
diff --git a/frontend/src/app/pages/account/company/department/department.component.ts b/frontend/src/app/pages/account/company/department/department.component.ts new file mode 100644 index 0000000..9842162 --- /dev/null +++ b/frontend/src/app/pages/account/company/department/department.component.ts @@ -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 + }); + } + }); + } +} diff --git a/frontend/src/app/pages/account/company/desigation/designation.component.html b/frontend/src/app/pages/account/company/desigation/designation.component.html new file mode 100644 index 0000000..5f58129 --- /dev/null +++ b/frontend/src/app/pages/account/company/desigation/designation.component.html @@ -0,0 +1,183 @@ +
+ + + + + + + + + + + + + + +
+

Manage Designations

+ + + + +
+
+ + + # + +
+ Designation + +
+ + +
+ Department + +
+ + +
+ HOD + +
+ + +
+ Pay Grade + +
+ + +
+ Updated At + +
+ + +
+ Updated By + +
+ + +
+ Status + +
+ + + +
+ + + {{ rowIndex + 1 }} + {{ designation.designation }} + {{ designation.departmentName }} + {{ designation.hod ? 'Yes' : 'No' }} + {{ designation.payGrade || 'N/A' }} + {{ designation.updatedAt | date:'dd/MM/yyyy HH:mm:ss' }} + + + {{ designation.updatedUser }} + + + + Not Available + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+
+ + + + +
+
+ + + + +
+
+ +
+
+
+
+ + + + + +
+ + +
diff --git a/frontend/src/app/pages/account/company/desigation/designation.component.ts b/frontend/src/app/pages/account/company/desigation/designation.component.ts new file mode 100644 index 0000000..d445f85 --- /dev/null +++ b/frontend/src/app/pages/account/company/desigation/designation.component.ts @@ -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 + }); + } + }); + } +} diff --git a/frontend/src/app/pages/account/company/employee/employee.component.html b/frontend/src/app/pages/account/company/employee/employee.component.html new file mode 100644 index 0000000..bd9a213 --- /dev/null +++ b/frontend/src/app/pages/account/company/employee/employee.component.html @@ -0,0 +1,407 @@ +
+ + + + + + + + + + + + + + +
+

Manage Employees

+ + + + +
+
+ + + # + +
+ Full Name + +
+ + +
+ Subsidiary + +
+ + +
+ Department + +
+ + +
+ Designation + +
+ + +
+ Contact No + +
+ + +
+ Email + +
+ + +
+ Updated At + +
+ + +
+ Updated By + +
+ + +
+ Status + +
+ + + +
+ + + {{ rowIndex + 1 }} + {{ employee.fullName }} + {{ employee.subsidiaryName }} + {{ employee.department }} + {{ employee.designation }} + {{ employee.contactNo }} + {{ employee.emailId }} + {{ employee.updatedAt | date:'dd/MM/yyyy HH:mm:ss' }} + + + {{ employee.updatedUser }} + + + + Not Available + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+
Permanent Address
+ + + + +
+
+ + + + +
+
+ + + + +
+
+
Residence Address
+ + + + +
+
+ + + + +
+
+ + + + +
+
+
+
+ + + + + +
+ + +
diff --git a/frontend/src/app/pages/account/company/employee/employee.component.ts b/frontend/src/app/pages/account/company/employee/employee.component.ts new file mode 100644 index 0000000..9e94371 --- /dev/null +++ b/frontend/src/app/pages/account/company/employee/employee.component.ts @@ -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(); + + 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 = { + 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 + }); + } + }); + } +} diff --git a/frontend/src/app/pages/account/company/subsidiary/subsidiary.component.html b/frontend/src/app/pages/account/company/subsidiary/subsidiary.component.html new file mode 100644 index 0000000..f3d8a88 --- /dev/null +++ b/frontend/src/app/pages/account/company/subsidiary/subsidiary.component.html @@ -0,0 +1,322 @@ +
+ + + + + + + + + + + + + + +
+

Manage Subsidiaries

+ + + + +
+
+ + + # + Code + +
+ Name + +
+ + +
+ Email + +
+ + +
+ Updated At + +
+ + +
+ Updated By + +
+ + +
+ Status + +
+ + + +
+ + + {{ rowIndex + 1 }} + {{ subsidiary.code }} + {{ subsidiary.name }} + + + {{ subsidiary.emailId }} + + + + Not Available + + + {{ subsidiary.updatedAt | date:'dd/MM/yyyy HH:mm:ss' }} + + + {{ subsidiary.updatedUser }} + + + + Not Available + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+
+
+ + + + + +
+ + +
+ diff --git a/frontend/src/app/pages/account/company/subsidiary/subsidiary.component.ts b/frontend/src/app/pages/account/company/subsidiary/subsidiary.component.ts new file mode 100644 index 0000000..78b4630 --- /dev/null +++ b/frontend/src/app/pages/account/company/subsidiary/subsidiary.component.ts @@ -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(); + + 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 = { + 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 + }); + } + }); + } + } +} diff --git a/frontend/src/app/pages/account/user/user.component.html b/frontend/src/app/pages/account/user/user.component.html new file mode 100644 index 0000000..d05f647 --- /dev/null +++ b/frontend/src/app/pages/account/user/user.component.html @@ -0,0 +1,303 @@ +
+ + + + + + + + + + + + + + +
+

Manage Users

+ + + + +
+
+ + + # + +
+ Login ID + +
+ + +
+ Display Name + +
+ + +
+ Employee ID + +
+ + +
+ Status + +
+ + +
+ Updated At + +
+ + +
+ Updated By + +
+ + +
+ Active + +
+ + + +
+ + + {{ rowIndex + 1 }} + {{ user.loginId }} + {{ user.displayName }} + + + {{ user.employeeId }} + + + Not Available + + + + + + {{ user.updatedAt | date:'dd/MM/yyyy HH:mm:ss' }} + + + {{ user.updatedUser }} + + + Not Available + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ +
+
+ + + + +
+
+ + + + +
+
+ +
+
+ + + + # + Role Name + Group Name + Branch Name + Action + + + + + {{ rowIndex + 1 }} + {{role.roleName}} + {{role.groupName}} + {{role.branchName}} + + + + + + + + No roles assigned. + + + +
+
+
+ + + + + +
+ + +
diff --git a/frontend/src/app/pages/account/user/user.component.ts b/frontend/src/app/pages/account/user/user.component.ts new file mode 100644 index 0000000..283fb40 --- /dev/null +++ b/frontend/src/app/pages/account/user/user.component.ts @@ -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(); + + + 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 + }); + } + }); + } +} diff --git a/frontend/src/app/pages/account/vendor/vendor.component.css b/frontend/src/app/pages/account/vendor/vendor.component.css new file mode 100644 index 0000000..cffb4bb --- /dev/null +++ b/frontend/src/app/pages/account/vendor/vendor.component.css @@ -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; +} diff --git a/frontend/src/app/pages/account/vendor/vendor.component.html b/frontend/src/app/pages/account/vendor/vendor.component.html new file mode 100644 index 0000000..09434e0 --- /dev/null +++ b/frontend/src/app/pages/account/vendor/vendor.component.html @@ -0,0 +1,419 @@ +
+ + + + + + + + + + + + + + +
+

Manage Vendors

+ + + + +
+
+ + + # + Code + +
+ Name + +
+ + +
+ PAN No. + +
+ + +
+ MSME No. + +
+ + +
+ Updated At + +
+ + +
+ Updated By + +
+ + +
+ Status + +
+ + + +
+ + + {{ rowIndex + 1 }} + {{ vendor.code }} + {{ vendor.name }} + {{ vendor.panNo || '-' }} + {{ vendor.msmeNo || '-' }} + {{ vendor.updatedAt | date:'dd/MM/yyyy HH:mm:ss' }} + + + {{ vendor.updatedUser }} + + + Not Available + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+
+
+ +
+
+
Vendor Branches
+ +
+ + + + + Code + Name + City + State + Contact No + Actions + + + + + {{ branch.branchCode }} + {{ branch.branchName }} + {{ branch.cityName }} + {{ branch.stateName }} + {{ branch.contactNo }} + + + + + + + + + No branches added yet. + + + +
+ +
+ + + + + +
+ + + + +
+
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+
+
+ + + + +
+ + +
diff --git a/frontend/src/app/pages/account/vendor/vendor.component.ts b/frontend/src/app/pages/account/vendor/vendor.component.ts new file mode 100644 index 0000000..59b37bd --- /dev/null +++ b/frontend/src/app/pages/account/vendor/vendor.component.ts @@ -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(); + + @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(); + + 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 = { + 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, 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; + } +} diff --git a/frontend/src/app/pages/dashboard/dashboard.component.ts b/frontend/src/app/pages/dashboard/dashboard.component.ts new file mode 100644 index 0000000..2a57170 --- /dev/null +++ b/frontend/src/app/pages/dashboard/dashboard.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-dashboard', + imports: [], + template: `
+
` +}) +export class DashboardComponent { + constructor(){} +} diff --git a/frontend/src/app/pages/session/auth/authorize.component.ts b/frontend/src/app/pages/session/auth/authorize.component.ts new file mode 100644 index 0000000..5e49dfe --- /dev/null +++ b/frontend/src/app/pages/session/auth/authorize.component.ts @@ -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(`${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'); + } + } + +} diff --git a/frontend/src/app/services/account/vendor/vendor.service.ts b/frontend/src/app/services/account/vendor/vendor.service.ts new file mode 100644 index 0000000..f610157 --- /dev/null +++ b/frontend/src/app/services/account/vendor/vendor.service.ts @@ -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 { + return this.http.get(this.apiUrl); + } + + saveVendor(vendor: VendorDTO): Observable { + return this.http.post(this.apiUrl, vendor); + } + + searchVendors(searchDTO: SearchDTO): Observable { + return this.http.post(`${this.apiUrl}/search`, searchDTO); + } + + getVendorBranches(vendorId: string): Observable { + return this.http.get(`${this.apiUrl}/${vendorId}/branches`); + } + + saveBranch(branch: VendorBranchDTO): Observable { + return this.http.post(`${this.apiUrl}/${branch.fkVendorId}/branches`, branch); + } + + deleteBranch(vendorId: string, branchId: string): Observable { + return this.http.delete(`${this.apiUrl}/${vendorId}/branches/${branchId}`); + } + + activateDeactivateVendor(id: string, active: boolean): Observable { + // 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(`${this.apiUrl}/${id}/activate-deactivate`, { active }); + } +} diff --git a/frontend/src/environments/environment.ts b/frontend/src/environments/environment.ts new file mode 100644 index 0000000..a2396c0 --- /dev/null +++ b/frontend/src/environments/environment.ts @@ -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` +}; \ No newline at end of file diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index ed3cf92..dd146dc 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -1,17 +1,191 @@ -/* @import "primeng/resources/themes/aura-light-noir/theme.css"; */ -/* @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"; - +/* Global Styles from Cygnus-UI */ @import "primeicons/primeicons.css"; +@import 'primeflex/primeflex.css'; -html, body { - margin: 0; - font-family: var(--font-family); - background-color: var(--surface-ground); - height: 100%; + +@layer primeng, primeng-overrides; + +@font-face { + 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; +} + diff --git a/frontend_integration.md b/frontend_integration.md new file mode 100644 index 0000000..10e0934 --- /dev/null +++ b/frontend_integration.md @@ -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`