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

View File

@@ -0,0 +1,162 @@
<div class="card">
<p-toast />
<p-toolbar class="mb-6">
<ng-template #start>
<p-button label="New Department" icon="pi pi-plus" class="mr-2" (onClick)="openNew()" />
</ng-template>
<ng-template #end>
<p-button label="Export" icon="pi pi-upload" severity="secondary" (onClick)="exportCSV()" styleClass="mx-2" />
<p-button label="Refresh" icon="pi pi-refresh" severity="info" (onClick)="loadAllDepartments()" />
</ng-template>
</p-toolbar>
<p-table
#dt
[value]="isLoading ? skeletonData : departments"
[rows]="5"
[columns]="cols"
[paginator]="true"
[globalFilterFields]="['department']"
[tableStyle]="{ 'min-width': '75rem' }"
[(selection)]="selectedDepartments"
[rowHover]="true"
dataKey="id"
currentPageReportTemplate="Showing {first} to {last} of {totalRecords} entries"
[showCurrentPageReport]="true"
>
<ng-template #caption>
<div class="flex justify-content-between align-items-center">
<h4 class="m-0">Manage Departments</h4>
<p-iconfield>
<p-inputicon class="pi pi-search" />
<input pInputText type="text" (input)="dt.filterGlobal($any($event.target).value, 'contains')" placeholder="Search..." />
</p-iconfield>
</div>
</ng-template>
<ng-template #header>
<tr>
<th style="width: 3rem">#</th>
<th pSortableColumn="department">
<div class="flex items-center gap-2">
Department
<p-sortIcon field="department" />
</div>
</th>
<th pSortableColumn="parentDepartmentName" style="min-width: 10rem">
<div class="flex items-center gap-2">
Parent Department
<p-sortIcon field="parentDepartmentName" />
</div>
</th>
<th pSortableColumn="updatedAt" style="width: 13rem">
<div class="flex items-center gap-2">
Updated At
<p-sortIcon field="updatedAt" />
</div>
</th>
<th pSortableColumn="updatedUser" style="min-width: 12rem">
<div class="flex items-center gap-2">
Updated By
<p-sortIcon field="updatedUser" />
</div>
</th>
<th pSortableColumn="active" style="width: 4rem">
<div class="flex items-center gap-2">
Status
<p-sortIcon field="active" />
</div>
</th>
<th style="min-width: 8rem"></th>
</tr>
</ng-template>
<ng-template #body let-department let-rowIndex="rowIndex">
<tr *ngIf="!isLoading">
<td>{{ rowIndex + 1 }}</td>
<td>{{ department.department }}</td>
<td>
<span *ngIf="department.parentDepartmentName; else noParent">
{{ department.parentDepartmentName }}
</span>
<ng-template #noParent>
<em class="text-500 ">Not Available</em>
</ng-template>
</td>
<td>{{ department.updatedAt | date:'dd/MM/yyyy HH:mm:ss' }}</td>
<td>
<span *ngIf="department.updatedUser; else noUpdatedUser">
{{ department.updatedUser }}
</span>
<ng-template #noUpdatedUser>
<em class="text-500 ">Not Available</em>
</ng-template>
</td>
<td>
<p-tag [value]="department.active ? 'Active' : 'Inactive'" [severity]="getSeverity(department.active)" />
</td>
<td>
<p-button icon="pi pi-pencil" class="mr-2" [rounded]="true" [outlined]="true" (click)="editDepartment(department)" />
<p-button [icon]="department.active ? 'pi pi-ban' : 'pi pi-check'" [severity]="department.active ? 'danger' : 'success'" [rounded]="true" [outlined]="true" (click)="toggleActive(department)" />
</td>
</tr>
<tr *ngIf="isLoading">
<td><p-skeleton width="2rem" height="1.5rem"></p-skeleton></td>
<td><p-skeleton width="10rem" height="1.5rem"></p-skeleton></td>
<td><p-skeleton width="8rem" height="1.5rem"></p-skeleton></td>
<td><p-skeleton width="10rem" height="1.5rem"></p-skeleton></td>
<td><p-skeleton width="8rem" height="1.5rem"></p-skeleton></td>
<td><p-skeleton width="4rem" height="1.5rem"></p-skeleton></td>
<td><p-skeleton width="2rem" height="1.5rem"></p-skeleton></td>
</tr>
</ng-template>
</p-table>
<p-dialog [(visible)]="departmentDialog" [style]="{'width': '50vw'}" [breakpoints]="{ '960px': '75vw', '640px': '90vw' }" header="Department Details" [modal]="true">
<ng-template #content>
<form [formGroup]="departmentForm" (ngSubmit)="saveDepartment()" class="mt-2">
<div class="grid mt-0">
<div class="col-12">
<p-floatlabel variant="on">
<input
id="department"
pInputText
formControlName="department"
fluid
[class.p-invalid]="isFieldInvalid('department')"
pTooltip="{{ getErrorMessage('department') }}"
tooltipPosition="top"
autofocus
/>
<label for="department">Department Name <span class="text-red-500">*</span></label>
</p-floatlabel>
</div>
<div class="col-12">
<p-floatlabel variant="on">
<p-select
id="parentDepartment"
[options]="departmentOptions"
formControlName="parentDepartment"
optionLabel="label"
optionValue="value"
[filter]="true"
filterBy="label"
fluid
appendTo="body"
/>
<label for="parentDepartment">Parent Department</label>
</p-floatlabel>
</div>
</div>
</form>
</ng-template>
<ng-template #footer>
<p-button label="Cancel" icon="pi pi-times" text (click)="hideDialog()" />
<p-button label="Save" icon="pi pi-check" (click)="saveDepartment()" />
</ng-template>
</p-dialog>
<p-confirmDialog [style]="{ width: '450px' }" />
</div>

View File

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

View File

@@ -0,0 +1,183 @@
<div class="card">
<p-toast />
<p-toolbar class="mb-6">
<ng-template #start>
<p-button label="New Designation" icon="pi pi-plus" class="mr-2" (onClick)="openNew()" />
</ng-template>
<ng-template #end>
<p-button label="Export" icon="pi pi-upload" severity="secondary" (onClick)="exportCSV()" styleClass="mx-2" />
<p-button label="Refresh" icon="pi pi-refresh" severity="info" (onClick)="loadAllDesignations()" />
</ng-template>
</p-toolbar>
<p-table
#dt
[value]="isLoading ? skeletonData : designations"
[rows]="5"
[columns]="cols"
[paginator]="true"
[globalFilterFields]="['designation']"
[tableStyle]="{ 'min-width': '75rem' }"
[(selection)]="selectedDesignations"
[rowHover]="true"
dataKey="id"
currentPageReportTemplate="Showing {first} to {last} of {totalRecords} entries"
[showCurrentPageReport]="true"
>
<ng-template #caption>
<div class="flex justify-content-between align-items-center">
<h4 class="m-0">Manage Designations</h4>
<p-iconfield>
<p-inputicon class="pi pi-search" />
<input pInputText type="text" (input)="dt.filterGlobal($any($event.target).value, 'contains')" placeholder="Search..." />
</p-iconfield>
</div>
</ng-template>
<ng-template #header>
<tr>
<th style="width: 3rem">#</th>
<th pSortableColumn="designation">
<div class="flex items-center gap-2">
Designation
<p-sortIcon field="designation" />
</div>
</th>
<th pSortableColumn="departmentName" style="min-width: 10rem">
<div class="flex items-center gap-2">
Department
<p-sortIcon field="departmentName" />
</div>
</th>
<th pSortableColumn="hod" style="width: 4rem">
<div class="flex items-center gap-2">
HOD
<p-sortIcon field="hod" />
</div>
</th>
<th pSortableColumn="payGrade" style="min-width: 8rem">
<div class="flex items-center gap-2">
Pay Grade
<p-sortIcon field="payGrade" />
</div>
</th>
<th pSortableColumn="updatedAt" style="width: 13rem">
<div class="flex items-center gap-2">
Updated At
<p-sortIcon field="updatedAt" />
</div>
</th>
<th pSortableColumn="updatedUser" style="min-width: 12rem">
<div class="flex items-center gap-2">
Updated By
<p-sortIcon field="updatedUser" />
</div>
</th>
<th pSortableColumn="active" style="width: 4rem">
<div class="flex items-center gap-2">
Status
<p-sortIcon field="active" />
</div>
</th>
<th style="min-width: 8rem"></th>
</tr>
</ng-template>
<ng-template #body let-designation let-rowIndex="rowIndex">
<tr *ngIf="!isLoading">
<td>{{ rowIndex + 1 }}</td>
<td>{{ designation.designation }}</td>
<td>{{ designation.departmentName }}</td>
<td>{{ designation.hod ? 'Yes' : 'No' }}</td>
<td>{{ designation.payGrade || 'N/A' }}</td>
<td>{{ designation.updatedAt | date:'dd/MM/yyyy HH:mm:ss' }}</td>
<td>
<span *ngIf="designation.updatedUser; else noUpdatedUser">
{{ designation.updatedUser }}
</span>
<ng-template #noUpdatedUser>
<em class="text-500 ">Not Available</em>
</ng-template>
</td>
<td>
<p-tag [value]="designation.active ? 'Active' : 'Inactive'" [severity]="getSeverity(designation.active)" />
</td>
<td>
<p-button icon="pi pi-pencil" class="mr-2" [rounded]="true" [outlined]="true" (click)="editDesignation(designation)" />
<p-button [icon]="designation.active ? 'pi pi-ban' : 'pi pi-check'" [severity]="designation.active ? 'danger' : 'success'" [rounded]="true" [outlined]="true" (click)="toggleActive(designation)" />
</td>
</tr>
<tr *ngIf="isLoading">
<td><p-skeleton width="2rem" height="1.5rem"></p-skeleton></td>
<td><p-skeleton width="10rem" height="1.5rem"></p-skeleton></td>
<td><p-skeleton width="8rem" height="1.5rem"></p-skeleton></td>
<td><p-skeleton width="4rem" height="1.5rem"></p-skeleton></td>
<td><p-skeleton width="6rem" height="1.5rem"></p-skeleton></td>
<td><p-skeleton width="10rem" height="1.5rem"></p-skeleton></td>
<td><p-skeleton width="8rem" height="1.5rem"></p-skeleton></td>
<td><p-skeleton width="4rem" height="1.5rem"></p-skeleton></td>
<td><p-skeleton width="2rem" height="1.5rem"></p-skeleton></td>
</tr>
</ng-template>
</p-table>
<p-dialog [(visible)]="designationDialog" [style]="{'width': '50vw'}" [breakpoints]="{ '960px': '75vw', '640px': '90vw' }" header="Designation Details" [modal]="true">
<ng-template #content>
<form [formGroup]="designationForm" (ngSubmit)="saveDesignation()" class="mt-2">
<div class="grid mt-0">
<div class="col-12">
<p-floatlabel variant="on">
<input
id="designation"
pInputText
formControlName="designation"
fluid
[class.p-invalid]="isFieldInvalid('designation')"
pTooltip="{{ getErrorMessage('designation') }}"
tooltipPosition="top"
autofocus
/>
<label for="designation">Designation <span class="text-red-500">*</span></label>
</p-floatlabel>
</div>
<div class="col-12">
<p-floatlabel variant="on">
<p-select
id="departmentId"
formControlName="departmentId"
[options]="departments"
optionLabel="department"
optionValue="id"
[filter]="true"
filterBy="department"
fluid
appendTo="body"
[class.p-invalid]="isFieldInvalid('departmentId')"
pTooltip="{{ getErrorMessage('departmentId') }}"
tooltipPosition="top"
/>
<label for="departmentId">Department <span class="text-red-500">*</span></label>
</p-floatlabel>
</div>
<div class="col-12">
<label for="hod" class="flex align-items-center">
<p-checkbox
id="hod"
formControlName="hod"
binary="true"
/>
<span class="ml-2">Is HOD</span>
</label>
</div>
</div>
</form>
</ng-template>
<ng-template #footer>
<p-button label="Cancel" icon="pi pi-times" text (click)="hideDialog()" />
<p-button label="Save" icon="pi pi-check" (click)="saveDesignation()" />
</ng-template>
</p-dialog>
<p-confirmDialog [style]="{ width: '450px' }" />
</div>

View File

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

View File

@@ -0,0 +1,407 @@
<div class="card">
<p-toast />
<p-toolbar class="mb-6">
<ng-template #start>
<p-button label="New Employee" icon="pi pi-plus" class="mr-2" (onClick)="openNew()" />
</ng-template>
<ng-template #end>
<p-button label="Export" icon="pi pi-upload" severity="secondary" (onClick)="exportCSV()" styleClass="mx-2" />
<p-button label="Refresh" icon="pi pi-refresh" severity="info" (onClick)="loadAllEmployees()" />
</ng-template>
</p-toolbar>
<p-table
#dt
[value]="isLoading ? skeletonData : employees"
[rows]="5"
[columns]="cols"
[paginator]="true"
[globalFilterFields]="['fullName', 'contactNo', 'emailId']"
[tableStyle]="{ 'min-width': '75rem' }"
[(selection)]="selectedEmployees"
[rowHover]="true"
dataKey="id"
currentPageReportTemplate="Showing {first} to {last} of {totalRecords} entries"
[showCurrentPageReport]="true"
>
<ng-template #caption>
<div class="flex justify-content-between align-items-center">
<h4 class="m-0">Manage Employees</h4>
<p-iconfield>
<p-inputicon class="pi pi-search" />
<input pInputText type="text" (input)="dt.filterGlobal($any($event.target).value, 'contains')" placeholder="Search..." />
</p-iconfield>
</div>
</ng-template>
<ng-template #header>
<tr>
<th style="width: 3rem">#</th>
<th pSortableColumn="fullName">
<div class="flex items-center gap-2">
Full Name
<p-sortIcon field="fullName" />
</div>
</th>
<th pSortableColumn="subsidiaryName" style="min-width: 10rem">
<div class="flex items-center gap-2">
Subsidiary
<p-sortIcon field="subsidiaryName" />
</div>
</th>
<th pSortableColumn="department" style="min-width: 10rem">
<div class="flex items-center gap-2">
Department
<p-sortIcon field="department" />
</div>
</th>
<th pSortableColumn="designation" style="min-width: 10rem">
<div class="flex items-center gap-2">
Designation
<p-sortIcon field="designation" />
</div>
</th>
<th pSortableColumn="contactNo" style="min-width: 10rem">
<div class="flex items-center gap-2">
Contact No
<p-sortIcon field="contactNo" />
</div>
</th>
<th pSortableColumn="emailId" style="min-width: 10rem">
<div class="flex items-center gap-2">
Email
<p-sortIcon field="emailId" />
</div>
</th>
<th pSortableColumn="updatedAt" style="width: 13rem">
<div class="flex items-center gap-2">
Updated At
<p-sortIcon field="updatedAt" />
</div>
</th>
<th pSortableColumn="updatedUser" style="min-width: 12rem">
<div class="flex items-center gap-2">
Updated By
<p-sortIcon field="updatedUser" />
</div>
</th>
<th pSortableColumn="active" style="width: 4rem">
<div class="flex items-center gap-2">
Status
<p-sortIcon field="active" />
</div>
</th>
<th style="min-width: 8rem"></th>
</tr>
</ng-template>
<ng-template #body let-employee let-rowIndex="rowIndex">
<tr *ngIf="!isLoading">
<td>{{ rowIndex + 1 }}</td>
<td>{{ employee.fullName }}</td>
<td>{{ employee.subsidiaryName }}</td>
<td>{{ employee.department }}</td>
<td>{{ employee.designation }}</td>
<td>{{ employee.contactNo }}</td>
<td>{{ employee.emailId }}</td>
<td>{{ employee.updatedAt | date:'dd/MM/yyyy HH:mm:ss' }}</td>
<td>
<span *ngIf="employee.updatedUser; else noUpdatedUser">
{{ employee.updatedUser }}
</span>
<ng-template #noUpdatedUser>
<em class="text-500 ">Not Available</em>
</ng-template>
</td>
<td>
<p-tag [value]="employee.active ? 'Active' : 'Inactive'" [severity]="getSeverity(employee.active)" />
</td>
<td>
<p-button icon="pi pi-pencil" class="mr-2" [rounded]="true" [outlined]="true" (click)="editEmployee(employee)" />
<p-button [icon]="employee.active ? 'pi pi-ban' : 'pi pi-check'" [severity]="employee.active ? 'danger' : 'success'" [rounded]="true" [outlined]="true" (click)="toggleActive(employee)" />
</td>
</tr>
<tr *ngIf="isLoading">
<td><p-skeleton width="2rem" height="1.5rem"></p-skeleton></td>
<td><p-skeleton width="10rem" height="1.5rem"></p-skeleton></td>
<td><p-skeleton width="8rem" height="1.5rem"></p-skeleton></td>
<td><p-skeleton width="8rem" height="1.5rem"></p-skeleton></td>
<td><p-skeleton width="8rem" height="1.5rem"></p-skeleton></td>
<td><p-skeleton width="10rem" height="1.5rem"></p-skeleton></td>
<td><p-skeleton width="10rem" height="1.5rem"></p-skeleton></td>
<td><p-skeleton width="10rem" height="1.5rem"></p-skeleton></td>
<td><p-skeleton width="8rem" height="1.5rem"></p-skeleton></td>
<td><p-skeleton width="4rem" height="1.5rem"></p-skeleton></td>
<td><p-skeleton width="2rem" height="1.5rem"></p-skeleton></td>
</tr>
</ng-template>
</p-table>
<p-dialog [(visible)]="employeeDialog" [style]="{'width': '50vw'}" [breakpoints]="{ '960px': '75vw', '640px': '90vw' }" header="Employee Details" [modal]="true">
<ng-template #content>
<form [formGroup]="employeeForm" (ngSubmit)="saveEmployee()" class="mt-2">
<div class="grid mt-0">
<div class="col-12">
<p-floatlabel variant="on">
<p-select
id="subsidiaryId"
formControlName="subsidiaryId"
[options]="subsidiaries"
optionLabel="name"
optionValue="id"
[filter]="true"
filterBy="name"
fluid
appendTo="body"
[class.p-invalid]="isFieldInvalid('subsidiaryId')"
pTooltip="{{ getErrorMessage('subsidiaryId') }}"
tooltipPosition="top"
autofocus
/>
<label for="subsidiaryId">Subsidiary <span class="text-red-500">*</span></label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<p-calendar
id="joiningDate"
formControlName="joiningDate"
dateFormat="dd/mm/yy"
[showIcon]="true"
fluid
appendTo="body"
/>
<label for="joiningDate">Joining Date</label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<input
id="employeeId"
pInputText
formControlName="employeeId"
fluid
[class.p-invalid]="isFieldInvalid('employeeId')"
pTooltip="{{ getErrorMessage('employeeId') }}"
tooltipPosition="top"
/>
<label for="employeeId">Employee ID <span class="text-red-500">*</span></label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<p-select
id="departmentId"
formControlName="departmentId"
[options]="departments"
optionLabel="department"
optionValue="id"
[filter]="true"
filterBy="department"
fluid
appendTo="body"
[class.p-invalid]="isFieldInvalid('departmentId')"
pTooltip="{{ getErrorMessage('departmentId') }}"
tooltipPosition="top"
/>
<label for="departmentId">Department <span class="text-red-500">*</span></label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<p-select
id="designationId"
formControlName="designationId"
[options]="designations"
optionLabel="designation"
optionValue="id"
[filter]="true"
filterBy="designation"
fluid
appendTo="body"
[class.p-invalid]="isFieldInvalid('designationId')"
pTooltip="{{ getErrorMessage('designationId') }}"
tooltipPosition="top"
/>
<label for="designationId">Designation <span class="text-red-500">*</span></label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<input
id="fullName"
pInputText
formControlName="fullName"
fluid
[class.p-invalid]="isFieldInvalid('fullName')"
pTooltip="{{ getErrorMessage('fullName') }}"
tooltipPosition="top"
/>
<label for="fullName">Full Name <span class="text-red-500">*</span></label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<input
id="contactNo"
pInputText
formControlName="contactNo"
fluid
[class.p-invalid]="isFieldInvalid('contactNo')"
pTooltip="{{ getErrorMessage('contactNo') }}"
tooltipPosition="top"
/>
<label for="contactNo">Contact No <span class="text-red-500">*</span></label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<input
id="emailId"
pInputText
formControlName="emailId"
fluid
[class.p-invalid]="isFieldInvalid('emailId')"
pTooltip="{{ getErrorMessage('emailId') }}"
tooltipPosition="top"
/>
<label for="emailId">Email ID <span class="text-red-500">*</span></label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<p-select
id="gender"
formControlName="gender"
[options]="genders"
optionLabel="label"
optionValue="value"
fluid
appendTo="body"
[class.p-invalid]="isFieldInvalid('gender')"
pTooltip="{{ getErrorMessage('gender') }}"
tooltipPosition="top"
/>
<label for="gender">Gender <span class="text-red-500">*</span></label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<p-calendar
id="dob"
formControlName="dob"
dateFormat="dd/mm/yy"
[showIcon]="true"
fluid
appendTo="body"
[class.p-invalid]="isFieldInvalid('dob')"
pTooltip="{{ getErrorMessage('dob') }}"
tooltipPosition="top"
/>
<label for="dob">Date of Birth <span class="text-red-500">*</span></label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<input
id="alternateNo"
pInputText
formControlName="alternateNo"
fluid
[class.p-invalid]="isFieldInvalid('alternateNo')"
pTooltip="{{ getErrorMessage('alternateNo') }}"
tooltipPosition="top"
/>
<label for="alternateNo">Alternate Contact No</label>
</p-floatlabel>
</div>
<div class="col-12">
<h5>Permanent Address</h5>
<p-floatlabel variant="on">
<input
id="permanentAddress"
pInputText
formControlName="permanentAddress"
fluid
/>
<label for="permanentAddress">Address</label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<p-autoComplete
id="permanentCityName"
formControlName="permanentCityName"
[suggestions]="suggestions"
optionLabel="display"
[scrollHeight]="'160px'"
appendTo="body"
(completeMethod)="searchCities($event)"
(onSelect)="onSelectCity($event, 'permanent')"
fluid
/>
<label for="permanentCityName">City</label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<input
id="permanentStateName"
pInputText
formControlName="permanentStateName"
fluid
/>
<label for="permanentStateName">State</label>
</p-floatlabel>
</div>
<div class="col-12">
<h5>Residence Address</h5>
<p-floatlabel variant="on">
<input
id="residenceAddress"
pInputText
formControlName="residenceAddress"
fluid
/>
<label for="residenceAddress">Address</label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<p-autoComplete
id="residenceCityName"
formControlName="residenceCityName"
[suggestions]="suggestions"
optionLabel="display"
[scrollHeight]="'160px'"
appendTo="body"
(completeMethod)="searchCities($event)"
(onSelect)="onSelectCity($event, 'residence')"
fluid
/>
<label for="residenceCityName">City</label>
</p-floatlabel>
</div>
<div class="col-12 md:col-6">
<p-floatlabel variant="on">
<input
id="residenceStateName"
pInputText
formControlName="residenceStateName"
fluid
/>
<label for="residenceStateName">State</label>
</p-floatlabel>
</div>
</div>
</form>
</ng-template>
<ng-template #footer>
<p-button label="Cancel" icon="pi pi-times" text (click)="hideDialog()" />
<p-button label="Save" icon="pi pi-check" (click)="saveEmployee()" />
</ng-template>
</p-dialog>
<p-confirmDialog [style]="{ width: '450px' }" />
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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