Compare commits
2 Commits
39521f5e76
...
4673bb36a3
| Author | SHA1 | Date | |
|---|---|---|---|
| 4673bb36a3 | |||
| 4b9914e5eb |
14
public/mockApi/master-data/employees.json
Normal file
14
public/mockApi/master-data/employees.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
[
|
||||||
|
{ "id": "EMP001", "name": "Alice Williams", "designationId": "DSG001", "designationName": "Software Engineer", "deptId": "DEP001", "deptName": "IT", "branchId": "BR001", "branchName": "Bengaluru", "companyId": "CMP001", "companyName": "CLRI", "status": "Active" },
|
||||||
|
{ "id": "EMP002", "name": "Bob Smith", "designationId": "DSG002", "designationName": "Senior Software Engineer", "deptId": "DEP001", "deptName": "IT", "branchId": "BR001", "branchName": "Bengaluru", "companyId": "CMP001", "companyName": "CLRI", "status": "Active" },
|
||||||
|
{ "id": "EMP003", "name": "Charlie Brown", "designationId": "DSG004", "designationName": "Trainer", "deptId": "DEP002", "deptName": "Training", "branchId": "BR002", "branchName": "Pune", "companyId": "CMP001", "companyName": "CLRI", "status": "Disabled" },
|
||||||
|
{ "id": "EMP004", "name": "Diana Prince", "designationId": "DSG006", "designationName": "Data Analyst", "deptId": "DEP003", "deptName": "Analytics", "branchId": "BR001", "branchName": "Bengaluru", "companyId": "CMP001", "companyName": "CLRI", "status": "Active" },
|
||||||
|
{ "id": "EMP005", "name": "Evan Wright", "designationId": "DSG008", "designationName": "QA Tester", "deptId": "DEP006", "deptName": "Software Dev", "branchId": "BR004", "branchName": "Mumbai", "companyId": "CMP002", "companyName": "TechNova Solutions", "status": "Active" },
|
||||||
|
{ "id": "EMP006", "name": "Fiona Gallagher", "designationId": "DSG010", "designationName": "HR Executive", "deptId": "DEP008", "deptName": "HR", "branchId": "BR006", "branchName": "Chennai", "companyId": "CMP003", "companyName": "Infotech Ltd", "status": "Active" },
|
||||||
|
{ "id": "EMP007", "name": "George Costanza", "designationId": "DSG007", "designationName": "Placement Officer", "deptId": "DEP004", "deptName": "Placement", "branchId": "BR001", "branchName": "Bengaluru", "companyId": "CMP001", "companyName": "CLRI", "status": "Active" },
|
||||||
|
{ "id": "EMP008", "name": "Hannah Abbott", "designationId": "DSG012", "designationName": "Support Engineer", "deptId": "DEP010", "deptName": "Support", "branchId": "BR008", "branchName": "Ahmedabad", "companyId": "CMP004", "companyName": "Global Systems", "status": "Disabled" },
|
||||||
|
{ "id": "EMP009", "name": "Ian Malcolm", "designationId": "DSG005", "designationName": "Lead Trainer", "deptId": "DEP002", "deptName": "Training", "branchId": "BR002", "branchName": "Pune", "companyId": "CMP001", "companyName": "CLRI", "status": "Active" },
|
||||||
|
{ "id": "EMP010", "name": "Jane Porter", "designationId": "DSG009", "designationName": "Project Manager", "deptId": "DEP006", "deptName": "Software Dev", "branchId": "BR004", "branchName": "Mumbai", "companyId": "CMP002", "companyName": "TechNova Solutions", "status": "Active" },
|
||||||
|
{ "id": "EMP011", "name": "Kevin Hart", "designationId": "DSG001", "designationName": "Software Engineer", "deptId": "DEP001", "deptName": "IT", "branchId": "BR001", "branchName": "Bengaluru", "companyId": "CMP001", "companyName": "CLRI", "status": "Active" },
|
||||||
|
{ "id": "EMP012", "name": "Laura Croft", "designationId": "DSG006", "designationName": "Data Analyst", "deptId": "DEP003", "deptName": "Analytics", "branchId": "BR001", "branchName": "Bengaluru", "companyId": "CMP001", "companyName": "CLRI", "status": "Active" }
|
||||||
|
]
|
||||||
23
src/features/employee-management/api/useEmployeeData.ts
Normal file
23
src/features/employee-management/api/useEmployeeData.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { fetchMockData } from '../../../api/client';
|
||||||
|
|
||||||
|
export interface Employee {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
designationId: string;
|
||||||
|
designationName: string;
|
||||||
|
deptId: string;
|
||||||
|
deptName: string;
|
||||||
|
branchId: string;
|
||||||
|
branchName: string;
|
||||||
|
companyId: string;
|
||||||
|
companyName: string;
|
||||||
|
status: 'Active' | 'Disabled';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useEmployees = () => {
|
||||||
|
return useQuery<Employee[]>({
|
||||||
|
queryKey: ['employees'],
|
||||||
|
queryFn: () => fetchMockData<Employee[]>('master-data/employees.json'),
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -0,0 +1,157 @@
|
|||||||
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
|
import { X } from 'lucide-react';
|
||||||
|
import type { Employee } from '../../api/useEmployeeData';
|
||||||
|
import type { Company } from '../../api/useCompanyData';
|
||||||
|
import type { Branch } from '../../api/useBranchData';
|
||||||
|
import type { Department } from '../../api/useDepartmentData';
|
||||||
|
import type { Designation } from '../../api/useDesignationData';
|
||||||
|
|
||||||
|
interface EmployeeFormModalProps {
|
||||||
|
employee: Employee | null;
|
||||||
|
companies: Company[];
|
||||||
|
branches: Branch[];
|
||||||
|
departments: Department[];
|
||||||
|
designations: Designation[];
|
||||||
|
onClose: () => void;
|
||||||
|
onSave: (data: Omit<Employee, 'id'>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EmployeeFormModal({ employee, companies, branches, departments, designations, onClose, onSave }: EmployeeFormModalProps) {
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
const [companyId, setCompanyId] = useState('');
|
||||||
|
const [branchId, setBranchId] = useState('');
|
||||||
|
const [deptId, setDeptId] = useState('');
|
||||||
|
const [designationId, setDesignationId] = useState('');
|
||||||
|
const [status, setStatus] = useState<'Active' | 'Disabled'>('Active');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (employee) {
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
|
setName(employee.name);
|
||||||
|
setCompanyId(employee.companyId);
|
||||||
|
setBranchId(employee.branchId);
|
||||||
|
setDeptId(employee.deptId);
|
||||||
|
setDesignationId(employee.designationId);
|
||||||
|
setStatus(employee.status);
|
||||||
|
} else {
|
||||||
|
setName('');
|
||||||
|
setCompanyId(companies.length > 0 ? companies[0].id : '');
|
||||||
|
setBranchId('');
|
||||||
|
setDeptId('');
|
||||||
|
setDesignationId('');
|
||||||
|
setStatus('Active');
|
||||||
|
}
|
||||||
|
}, [employee, companies]);
|
||||||
|
|
||||||
|
const filteredBranches = useMemo(() => branches.filter(b => b.companyId === companyId), [branches, companyId]);
|
||||||
|
const filteredDepartments = useMemo(() => departments.filter(d => d.branchId === branchId), [departments, branchId]);
|
||||||
|
const filteredDesignations = useMemo(() => designations.filter(d => d.deptId === deptId), [designations, deptId]);
|
||||||
|
|
||||||
|
const handleCompanyChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
setCompanyId(e.target.value);
|
||||||
|
setBranchId('');
|
||||||
|
setDeptId('');
|
||||||
|
setDesignationId('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBranchChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
setBranchId(e.target.value);
|
||||||
|
setDeptId('');
|
||||||
|
setDesignationId('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeptChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
setDeptId(e.target.value);
|
||||||
|
setDesignationId('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const selectedCompany = companies.find(c => c.id === companyId);
|
||||||
|
const selectedBranch = branches.find(b => b.id === branchId);
|
||||||
|
const selectedDept = departments.find(d => d.id === deptId);
|
||||||
|
const selectedDesg = designations.find(d => d.id === designationId);
|
||||||
|
|
||||||
|
onSave({
|
||||||
|
name,
|
||||||
|
companyId,
|
||||||
|
companyName: selectedCompany?.name || '',
|
||||||
|
branchId,
|
||||||
|
branchName: selectedBranch?.name || '',
|
||||||
|
deptId,
|
||||||
|
deptName: selectedDept?.name || '',
|
||||||
|
designationId,
|
||||||
|
designationName: selectedDesg?.name || '',
|
||||||
|
status,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const inputClass = "w-full px-3 py-2 border border-app-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary text-text-secondary bg-app-card";
|
||||||
|
const labelClass = "block text-sm font-medium text-text-secondary mb-1";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||||
|
<div className="bg-app-card rounded-lg shadow-xl w-full max-w-md p-6 relative max-h-[90vh] overflow-y-auto">
|
||||||
|
<button onClick={onClose} className="absolute top-4 right-4 text-text-muted hover:text-text-primary">
|
||||||
|
<X size={24} />
|
||||||
|
</button>
|
||||||
|
<h3 className="text-xl font-bold text-text-primary mb-6">
|
||||||
|
{employee ? 'Edit Employee' : 'Add New Employee'}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Employee Name</label>
|
||||||
|
<input type="text" value={name} onChange={(e) => setName(e.target.value)} required className={inputClass} placeholder="Enter employee name..." />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Company</label>
|
||||||
|
<select value={companyId} onChange={handleCompanyChange} required className={inputClass}>
|
||||||
|
{companies.length === 0 ? <option value="">No companies available</option> : companies.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Branch</label>
|
||||||
|
<select value={branchId} onChange={handleBranchChange} required disabled={!companyId || filteredBranches.length === 0} className={`${inputClass} disabled:opacity-50 disabled:cursor-not-allowed`}>
|
||||||
|
<option value="">Select Branch</option>
|
||||||
|
{filteredBranches.map(b => <option key={b.id} value={b.id}>{b.name}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Department</label>
|
||||||
|
<select value={deptId} onChange={handleDeptChange} required disabled={!branchId || filteredDepartments.length === 0} className={`${inputClass} disabled:opacity-50 disabled:cursor-not-allowed`}>
|
||||||
|
<option value="">Select Department</option>
|
||||||
|
{filteredDepartments.map(d => <option key={d.id} value={d.id}>{d.name}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Designation</label>
|
||||||
|
<select value={designationId} onChange={(e) => setDesignationId(e.target.value)} required disabled={!deptId || filteredDesignations.length === 0} className={`${inputClass} disabled:opacity-50 disabled:cursor-not-allowed`}>
|
||||||
|
<option value="">Select Designation</option>
|
||||||
|
{filteredDesignations.map(d => <option key={d.id} value={d.id}>{d.name}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Status</label>
|
||||||
|
<select value={status} onChange={(e) => setStatus(e.target.value as 'Active' | 'Disabled')} className={inputClass}>
|
||||||
|
<option value="Active">Active</option>
|
||||||
|
<option value="Disabled">Disabled</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end space-x-3 mt-6">
|
||||||
|
<button type="button" onClick={onClose} className="px-4 py-2 bg-app-muted text-text-primary rounded-lg hover:bg-app-border transition-colors text-sm font-medium">Cancel</button>
|
||||||
|
<button type="submit" disabled={!designationId} className="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-hover transition-colors text-sm font-medium disabled:opacity-50 disabled:cursor-not-allowed">
|
||||||
|
{employee ? 'Update' : 'Add'} Employee
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,195 @@
|
|||||||
|
import { useState, useMemo } from 'react';
|
||||||
|
import { ArrowUpDown, ArrowUp, ArrowDown, Search, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Eye, Pencil, Trash2 } from 'lucide-react'; // Added Eye
|
||||||
|
import type { Employee } from '../../api/useEmployeeData';
|
||||||
|
|
||||||
|
interface EmployeeTableProps {
|
||||||
|
records: Employee[];
|
||||||
|
onView: (employee: Employee) => void; // Added onView
|
||||||
|
onEdit: (employee: Employee) => void;
|
||||||
|
onDelete: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type SortKey = 'id' | 'name' | 'designationName' | 'deptName' | 'branchName' | 'companyName' | 'status';
|
||||||
|
type SortDirection = 'ascending' | 'descending';
|
||||||
|
|
||||||
|
const getStatusClass = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'Active': return 'bg-present-100 text-present-700';
|
||||||
|
case 'Disabled': return 'bg-absent-100 text-absent-700';
|
||||||
|
default: return 'bg-app-muted text-text-secondary';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function EmployeeTable({ records, onView, onEdit, onDelete }: EmployeeTableProps) {
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [pageSize, setPageSize] = useState(10);
|
||||||
|
|
||||||
|
const [sortConfig, setSortConfig] = useState<{ key: SortKey; direction: SortDirection }>({
|
||||||
|
key: 'name', direction: 'ascending'
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredData = useMemo(() => {
|
||||||
|
if (!searchQuery) return records;
|
||||||
|
return records.filter(record =>
|
||||||
|
record.id.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
record.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
record.designationName.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
record.deptName.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
record.branchName.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
record.companyName.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
record.status.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
|
);
|
||||||
|
}, [records, searchQuery]);
|
||||||
|
|
||||||
|
const sortedData = useMemo(() => {
|
||||||
|
const sortableData = [...filteredData];
|
||||||
|
sortableData.sort((a, b) => {
|
||||||
|
if (a[sortConfig.key] < b[sortConfig.key]) return sortConfig.direction === 'ascending' ? -1 : 1;
|
||||||
|
if (a[sortConfig.key] > b[sortConfig.key]) return sortConfig.direction === 'ascending' ? 1 : -1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
return sortableData;
|
||||||
|
}, [filteredData, sortConfig]);
|
||||||
|
|
||||||
|
const totalPages = Math.ceil(sortedData.length / pageSize);
|
||||||
|
const isPaginationEnabled = sortedData.length > pageSize;
|
||||||
|
|
||||||
|
const paginatedData = useMemo(() => {
|
||||||
|
const startIndex = (currentPage - 1) * pageSize;
|
||||||
|
return sortedData.slice(startIndex, startIndex + pageSize);
|
||||||
|
}, [sortedData, currentPage, pageSize]);
|
||||||
|
|
||||||
|
const pageNumbers = useMemo(() => {
|
||||||
|
const pages = [];
|
||||||
|
const maxVisiblePages = 5;
|
||||||
|
let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2));
|
||||||
|
let endPage = startPage + maxVisiblePages - 1;
|
||||||
|
if (endPage > totalPages) {
|
||||||
|
endPage = totalPages;
|
||||||
|
startPage = Math.max(1, endPage - maxVisiblePages + 1);
|
||||||
|
}
|
||||||
|
for (let i = startPage; i <= endPage; i++) pages.push(i);
|
||||||
|
return pages;
|
||||||
|
}, [currentPage, totalPages]);
|
||||||
|
|
||||||
|
const requestSort = (key: SortKey) => {
|
||||||
|
let direction: SortDirection = 'ascending';
|
||||||
|
if (sortConfig.key === key && sortConfig.direction === 'ascending') direction = 'descending';
|
||||||
|
setSortConfig({ key, direction });
|
||||||
|
setCurrentPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePageSizeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
setPageSize(Number(e.target.value));
|
||||||
|
setCurrentPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSortIcon = (key: SortKey) => {
|
||||||
|
if (sortConfig.key !== key) return <ArrowUpDown size={14} className="ml-1 text-text-light" />;
|
||||||
|
if (sortConfig.direction === 'ascending') return <ArrowUp size={14} className="ml-1 text-primary" />;
|
||||||
|
return <ArrowDown size={14} className="ml-1 text-primary" />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatHeader = (key: string) => {
|
||||||
|
if (key === 'id') return 'Emp ID';
|
||||||
|
if (key === 'name') return 'Employee Name';
|
||||||
|
if (key === 'designationName') return 'Designation';
|
||||||
|
if (key === 'deptName') return 'Department';
|
||||||
|
if (key === 'branchName') return 'Branch';
|
||||||
|
if (key === 'companyName') return 'Company';
|
||||||
|
return key.charAt(0).toUpperCase() + key.slice(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-app-card p-6 rounded-lg shadow-sm">
|
||||||
|
<div className="flex flex-col md:flex-row md:items-center justify-between mb-6 gap-4">
|
||||||
|
<h2 className="text-lg font-semibold text-text-primary">Employees List</h2>
|
||||||
|
<div className="relative w-full md:w-64">
|
||||||
|
<Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-light" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search id, name, role..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => { setSearchQuery(e.target.value); setCurrentPage(1); }}
|
||||||
|
className="pl-9 pr-3 py-2 border border-app-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary text-text-secondary bg-app-card w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="min-w-full divide-y divide-app-border">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{(['id', 'name', 'designationName', 'deptName', 'branchName', 'companyName', 'status'] as SortKey[]).map((key) => (
|
||||||
|
<th key={key} onClick={() => requestSort(key)} className="px-6 py-3 text-left text-xs font-semibold text-text-muted uppercase tracking-wider cursor-pointer hover:bg-app-muted transition-colors">
|
||||||
|
<div className="flex items-center">
|
||||||
|
{formatHeader(key)} {getSortIcon(key)}
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-semibold text-text-muted uppercase tracking-wider">Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-app-border">
|
||||||
|
{paginatedData.length > 0 ? (
|
||||||
|
paginatedData.map((record) => (
|
||||||
|
<tr key={record.id} className="hover:bg-app-muted transition-colors">
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-primary">{record.id}</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.name}</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.designationName}</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.deptName}</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.branchName}</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.companyName}</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm">
|
||||||
|
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusClass(record.status)}`}>{record.status}</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm flex space-x-3">
|
||||||
|
<button onClick={() => onView(record)} className="text-primary hover:text-primary-hover flex items-center">
|
||||||
|
<Eye size={16} className="mr-1" /> View
|
||||||
|
</button>
|
||||||
|
<button onClick={() => onEdit(record)} className="text-primary hover:text-primary-hover flex items-center">
|
||||||
|
<Pencil size={16} className="mr-1" /> Edit
|
||||||
|
</button>
|
||||||
|
<button onClick={() => onDelete(record.id)} className="text-absent-500 hover:text-absent-700 flex items-center">
|
||||||
|
<Trash2 size={16} className="mr-1" /> Delete
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={8} className="px-6 py-8 text-center text-sm text-text-muted">No employees found.</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col md:flex-row md:items-center justify-between mt-4 pt-4 border-t border-app-border gap-4">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className="text-sm text-text-muted">Show</span>
|
||||||
|
<select value={pageSize} onChange={handlePageSizeChange} className="px-2 py-1 border border-app-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary text-text-secondary bg-app-card">
|
||||||
|
<option value={10}>10</option>
|
||||||
|
<option value={15}>15</option>
|
||||||
|
<option value={20}>20</option>
|
||||||
|
</select>
|
||||||
|
<span className="text-sm text-text-muted">entries</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isPaginationEnabled && totalPages > 1 && (
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
<span className="text-sm text-text-muted mr-2 hidden sm:inline">Page {currentPage} of {totalPages}</span>
|
||||||
|
<button onClick={() => setCurrentPage(1)} disabled={currentPage === 1} className="p-2 border border-app-border rounded-lg text-text-secondary hover:bg-app-muted disabled:opacity-50 disabled:cursor-not-allowed transition-colors"><ChevronsLeft size={16} /></button>
|
||||||
|
<button onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))} disabled={currentPage === 1} className="p-2 border border-app-border rounded-lg text-text-secondary hover:bg-app-muted disabled:opacity-50 disabled:cursor-not-allowed transition-colors"><ChevronLeft size={16} /></button>
|
||||||
|
{pageNumbers.map(num => (
|
||||||
|
<button key={num} onClick={() => setCurrentPage(num)} className={`w-8 h-8 flex items-center justify-center border rounded-lg text-sm transition-colors ${currentPage === num ? 'bg-primary text-white border-primary' : 'border-app-border text-text-secondary hover:bg-app-muted'}`}>{num}</button>
|
||||||
|
))}
|
||||||
|
<button onClick={() => setCurrentPage(prev => Math.min(prev + 1, totalPages))} disabled={currentPage === totalPages} className="p-2 border border-app-border rounded-lg text-text-secondary hover:bg-app-muted disabled:opacity-50 disabled:cursor-not-allowed transition-colors"><ChevronRight size={16} /></button>
|
||||||
|
<button onClick={() => setCurrentPage(totalPages)} disabled={currentPage === totalPages} className="p-2 border border-app-border rounded-lg text-text-secondary hover:bg-app-muted disabled:opacity-50 disabled:cursor-not-allowed transition-colors"><ChevronsRight size={16} /></button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,66 @@
|
|||||||
|
import { X } from 'lucide-react';
|
||||||
|
import type { Employee } from '../../api/useEmployeeData';
|
||||||
|
|
||||||
|
interface EmployeeViewModalProps {
|
||||||
|
employee: Employee;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusClass = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'Active': return 'bg-present-100 text-present-700';
|
||||||
|
case 'Disabled': return 'bg-absent-100 text-absent-700';
|
||||||
|
default: return 'bg-app-muted text-text-secondary';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function EmployeeViewModal({ employee, onClose }: EmployeeViewModalProps) {
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||||
|
<div className="bg-app-card rounded-lg shadow-xl w-full max-w-md p-6 relative">
|
||||||
|
<button onClick={onClose} className="absolute top-4 right-4 text-text-muted hover:text-text-primary">
|
||||||
|
<X size={24} />
|
||||||
|
</button>
|
||||||
|
<h3 className="text-xl font-bold text-text-primary mb-6">Employee Details</h3>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Employee ID moved here to match other fields */}
|
||||||
|
<div className="flex justify-between border-b border-app-border pb-2">
|
||||||
|
<span className="text-sm text-text-muted">Employee ID</span>
|
||||||
|
<span className="text-sm font-medium text-text-primary">{employee.id}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between border-b border-app-border pb-2">
|
||||||
|
<span className="text-sm text-text-muted">Employee Name</span>
|
||||||
|
<span className="text-sm font-medium text-text-primary">{employee.name}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between border-b border-app-border pb-2">
|
||||||
|
<span className="text-sm text-text-muted">Company</span>
|
||||||
|
<span className="text-sm font-medium text-text-primary">{employee.companyName}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between border-b border-app-border pb-2">
|
||||||
|
<span className="text-sm text-text-muted">Branch</span>
|
||||||
|
<span className="text-sm font-medium text-text-primary">{employee.branchName}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between border-b border-app-border pb-2">
|
||||||
|
<span className="text-sm text-text-muted">Department</span>
|
||||||
|
<span className="text-sm font-medium text-text-primary">{employee.deptName}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between border-b border-app-border pb-2">
|
||||||
|
<span className="text-sm text-text-muted">Designation</span>
|
||||||
|
<span className="text-sm font-medium text-text-primary">{employee.designationName}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between border-b border-app-border pb-2">
|
||||||
|
<span className="text-sm text-text-muted">Status</span>
|
||||||
|
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusClass(employee.status)}`}>{employee.status}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end mt-6">
|
||||||
|
<button onClick={onClose} className="px-4 py-2 bg-app-muted text-text-primary rounded-lg hover:bg-app-border transition-colors text-sm font-medium">
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
136
src/features/employee-management/pages/ManageEmployees.tsx
Normal file
136
src/features/employee-management/pages/ManageEmployees.tsx
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import { useState, useMemo } from 'react';
|
||||||
|
import { Users, Plus } from 'lucide-react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import Loader from '../../../components/ui/Loader';
|
||||||
|
import ErrorState from '../../../components/ui/ErrorState';
|
||||||
|
import ConfirmationModal from '../../../components/ui/ConfirmationModal';
|
||||||
|
import EmployeeTable from '../components/employee/EmployeeTable';
|
||||||
|
import EmployeeFormModal from '../components/employee/EmployeeFormModal';
|
||||||
|
import EmployeeViewModal from '../components/employee/EmployeeViewModal'; // New Import
|
||||||
|
import { useEmployees, type Employee } from '../api/useEmployeeData';
|
||||||
|
import { useCompanies } from '../api/useCompanyData';
|
||||||
|
import { useBranches } from '../api/useBranchData';
|
||||||
|
import { useDepartments } from '../api/useDepartmentData';
|
||||||
|
import { useDesignations } from '../api/useDesignationData';
|
||||||
|
|
||||||
|
export default function ManageEmployees() {
|
||||||
|
const { data: apiRecords, isLoading: empLoading, isError: empError } = useEmployees();
|
||||||
|
const { data: companies, isLoading: compLoading, isError: compError } = useCompanies();
|
||||||
|
const { data: branches, isLoading: branchLoading, isError: branchError } = useBranches();
|
||||||
|
const { data: departments, isLoading: depLoading, isError: depError } = useDepartments();
|
||||||
|
const { data: designations, isLoading: dsgLoading, isError: dsgError } = useDesignations();
|
||||||
|
|
||||||
|
const [localRecords, setLocalRecords] = useState<Employee[]>([]);
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const [editingEmployee, setEditingEmployee] = useState<Employee | null>(null);
|
||||||
|
const [viewEmployee, setViewEmployee] = useState<Employee | null>(null); // New state for viewing
|
||||||
|
const [deleteId, setDeleteId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useMemo(() => {
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-render
|
||||||
|
if (apiRecords) setLocalRecords(apiRecords);
|
||||||
|
}, [apiRecords]);
|
||||||
|
|
||||||
|
const handleAddClick = () => {
|
||||||
|
setEditingEmployee(null);
|
||||||
|
setIsModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditClick = (employee: Employee) => {
|
||||||
|
setEditingEmployee(employee);
|
||||||
|
setIsModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleViewClick = (employee: Employee) => {
|
||||||
|
setViewEmployee(employee);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteClick = (id: string) => {
|
||||||
|
setDeleteId(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmDelete = () => {
|
||||||
|
if (deleteId) {
|
||||||
|
setLocalRecords(prev => prev.filter(e => e.id !== deleteId));
|
||||||
|
toast.success("Employee deleted successfully.");
|
||||||
|
setDeleteId(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = (data: Omit<Employee, 'id'>) => {
|
||||||
|
if (editingEmployee) {
|
||||||
|
setLocalRecords(prev => prev.map(e => e.id === editingEmployee.id ? { ...e, ...data } : e));
|
||||||
|
toast.success("Employee updated successfully.");
|
||||||
|
} else {
|
||||||
|
const newId = `EMP${String(localRecords.length + 1).padStart(3, '0')}`;
|
||||||
|
setLocalRecords(prev => [...prev, { id: newId, ...data }]);
|
||||||
|
toast.success("Employee added successfully.");
|
||||||
|
}
|
||||||
|
setIsModalOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isLoading = empLoading || compLoading || branchLoading || depLoading || dsgLoading;
|
||||||
|
const isError = empError || compError || branchError || depError || dsgError;
|
||||||
|
|
||||||
|
if (isLoading) return <Loader message="Loading employees data..." />;
|
||||||
|
if (isError) return <ErrorState message="Failed to load employees data." />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="bg-app-card p-6 rounded-lg shadow-sm flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div className="p-3 bg-primary/10 rounded-full">
|
||||||
|
<Users className="text-primary" size={28} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-bold text-text-primary">Manage Employees</h2>
|
||||||
|
<p className="text-sm text-text-muted">Add, edit, or disable employee profiles.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleAddClick}
|
||||||
|
className="flex items-center justify-center px-4 py-2 bg-action text-white rounded-lg hover:bg-action-hover transition-colors font-medium"
|
||||||
|
>
|
||||||
|
<Plus size={18} className="mr-2" />
|
||||||
|
Add Employee
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<EmployeeTable
|
||||||
|
records={localRecords}
|
||||||
|
onView={handleViewClick}
|
||||||
|
onEdit={handleEditClick}
|
||||||
|
onDelete={handleDeleteClick}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isModalOpen && (
|
||||||
|
<EmployeeFormModal
|
||||||
|
employee={editingEmployee}
|
||||||
|
companies={companies || []}
|
||||||
|
branches={branches || []}
|
||||||
|
departments={departments || []}
|
||||||
|
designations={designations || []}
|
||||||
|
onClose={() => setIsModalOpen(false)}
|
||||||
|
onSave={handleSave}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* View Modal */}
|
||||||
|
{viewEmployee && (
|
||||||
|
<EmployeeViewModal
|
||||||
|
employee={viewEmployee}
|
||||||
|
onClose={() => setViewEmployee(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{deleteId && (
|
||||||
|
<ConfirmationModal
|
||||||
|
title="Delete Employee?"
|
||||||
|
message="Are you sure you want to delete this employee? This action cannot be undone."
|
||||||
|
onConfirm={confirmDelete}
|
||||||
|
onClose={() => setDeleteId(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -3,14 +3,8 @@ import MasterDataDashboard from '../pages/MasterDataDashboard';
|
|||||||
import ManageCompanies from '../pages/ManageCompanies';
|
import ManageCompanies from '../pages/ManageCompanies';
|
||||||
import ManageBranches from '../pages/ManageBranches';
|
import ManageBranches from '../pages/ManageBranches';
|
||||||
import ManageDepartments from '../pages/ManageDepartments';
|
import ManageDepartments from '../pages/ManageDepartments';
|
||||||
import ManageDesignations from '../pages/ManageDesignations'; // New Import
|
import ManageDesignations from '../pages/ManageDesignations';
|
||||||
|
import ManageEmployees from '../pages/ManageEmployees'; // New Import
|
||||||
const Placeholder = ({ title }: { title: string }) => (
|
|
||||||
<div className="bg-app-card p-6 rounded-lg shadow-sm">
|
|
||||||
<h2 className="text-xl font-bold text-text-primary">{title}</h2>
|
|
||||||
<p className="text-text-muted mt-2">{title} UI will be implemented here.</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default function MasterDataRouter() {
|
export default function MasterDataRouter() {
|
||||||
return (
|
return (
|
||||||
@ -20,8 +14,8 @@ export default function MasterDataRouter() {
|
|||||||
<Route path="/company" element={<ManageCompanies />} />
|
<Route path="/company" element={<ManageCompanies />} />
|
||||||
<Route path="/branches" element={<ManageBranches />} />
|
<Route path="/branches" element={<ManageBranches />} />
|
||||||
<Route path="/departments" element={<ManageDepartments />} />
|
<Route path="/departments" element={<ManageDepartments />} />
|
||||||
<Route path="/designations" element={<ManageDesignations />} /> {/* Updated Route */}
|
<Route path="/designations" element={<ManageDesignations />} />
|
||||||
<Route path="/employees" element={<Placeholder title="Manage Employees" />} />
|
<Route path="/employees" element={<ManageEmployees />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user