Compare commits

..

2 Commits

60 changed files with 749 additions and 822 deletions

View File

@ -4,7 +4,7 @@ server {
location / { location / {
root /usr/share/nginx/html; root /usr/share/nginx/html;
index index.html index.htm; index index.html index.html;
# This is crucial for React Router to work on page refresh # This is crucial for React Router to work on page refresh
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html;
} }

View File

@ -14,7 +14,7 @@ import {
const iconMap: Record<string, any> = { const iconMap: Record<string, any> = {
LayoutDashboard, CalendarCheck, Plane, CheckCircle, BarChart3, ClipboardList, LayoutDashboard, CalendarCheck, Plane, CheckCircle, BarChart3, ClipboardList,
Users, ShieldUser, ScrollText, FileText, CalendarDays, ClipboardCheck, Users, ShieldUser, ScrollText, FileText, CalendarDays, ClipboardCheck,
Database, Building, Network, Briefcase, Tag, ClipboardType, ClipboardMinus, History // ADDED History Database, Building, Network, Briefcase, Tag, ClipboardType, ClipboardMinus, History
}; };
export default function Sidebar({ isCollapsed, setIsCollapsed }: { isCollapsed: boolean; setIsCollapsed: (v: boolean) => void }) { export default function Sidebar({ isCollapsed, setIsCollapsed }: { isCollapsed: boolean; setIsCollapsed: (v: boolean) => void }) {
@ -139,7 +139,7 @@ export default function Sidebar({ isCollapsed, setIsCollapsed }: { isCollapsed:
<img <img
src="/logo.gif" src="/logo.gif"
alt="Company Logo" alt="Company Logo"
className="h-14 w-5xl max-w-75 cursor-pointer " className="h-14 w-4xl max-w-75 cursor-pointer "
/> />
</Link> </Link>
)} )}

View File

@ -41,7 +41,7 @@ export const getSidebarItems = (role: UserRole): NavItem[] => {
{ name: 'Attendance Summary', icon: 'CalendarCheck', path: '/attendance' }, { name: 'Attendance Summary', icon: 'CalendarCheck', path: '/attendance' },
{ name: 'Daily Report', icon: 'CalendarDays', path: '/attendance-summary/daily' }, { name: 'Daily Report', icon: 'CalendarDays', path: '/attendance-summary/daily' },
{ name: 'Monthly Report', icon: 'FileText', path: '/attendance-summary/monthly' }, { name: 'Monthly Report', icon: 'FileText', path: '/attendance-summary/monthly' },
{ name: 'Regularization History', icon: 'History', path: '/attendance-summary/regularization-history' } , { name: 'Regularization History', icon: 'History', path: '/attendance-summary/regularization-history' },
...(role === 'hrmanager' ? [{ name: 'Regularization', icon: 'ClipboardCheck', path: '/attendance-summary/regularization' }] : []) ...(role === 'hrmanager' ? [{ name: 'Regularization', icon: 'ClipboardCheck', path: '/attendance-summary/regularization' }] : [])
] ]
}; };

View File

@ -1,6 +1,6 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiClient } from '../../../api/client'; import { apiClient } from '../../../api/client';
import type { AttendanceRecord, AttendanceStatus, AttendanceSummary, DashboardMetrics, DailyReportRecord, AdminReportRecord } from '../types/attendance'; import type { AttendanceRecord, AttendanceStatus, AttendanceSummary, DashboardMetrics, DailyReportRecord, AdminReportRecord, DailyReportFilters, AdminReportFilters, RegularizationPayload, RegularizationRecord } from '../types/attendance';
// Helper to map backend statuses to frontend statuses // Helper to map backend statuses to frontend statuses
const mapStatus = (backendStatus: string): AttendanceStatus => { const mapStatus = (backendStatus: string): AttendanceStatus => {
@ -15,7 +15,6 @@ const mapStatus = (backendStatus: string): AttendanceStatus => {
} }
}; };
// 1. Fetch Employee Range Report
export const useEmployeeRangeReport = (employeeId: number | null, fromDate: string, toDate: string) => { export const useEmployeeRangeReport = (employeeId: number | null, fromDate: string, toDate: string) => {
return useQuery<AttendanceRecord[]>({ return useQuery<AttendanceRecord[]>({
queryKey: ['employeeRangeReport', employeeId, fromDate, toDate], queryKey: ['employeeRangeReport', employeeId, fromDate, toDate],
@ -24,6 +23,7 @@ export const useEmployeeRangeReport = (employeeId: number | null, fromDate: stri
params: { employee_id: employeeId, from_date: fromDate, to_date: toDate } params: { employee_id: employeeId, from_date: fromDate, to_date: toDate }
}); });
const history = response.data.history || []; const history = response.data.history || [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return history.map((h: any) => ({ return history.map((h: any) => ({
date: h.workDate, date: h.workDate,
checkIn: h.checkIn ? h.checkIn.substring(0, 5) : '-', checkIn: h.checkIn ? h.checkIn.substring(0, 5) : '-',
@ -37,7 +37,7 @@ export const useEmployeeRangeReport = (employeeId: number | null, fromDate: stri
}); });
}; };
// 2. Fetch Employee Summary
export const useAttendanceSummary = (employeeId: number, startDate: string, endDate: string) => { export const useAttendanceSummary = (employeeId: number, startDate: string, endDate: string) => {
return useQuery<AttendanceSummary>({ return useQuery<AttendanceSummary>({
queryKey: ['attendanceSummary', employeeId, startDate, endDate], queryKey: ['attendanceSummary', employeeId, startDate, endDate],
@ -46,6 +46,7 @@ export const useAttendanceSummary = (employeeId: number, startDate: string, endD
params: { employee_id: employeeId, start_date: startDate, end_date: endDate } params: { employee_id: employeeId, start_date: startDate, end_date: endDate }
}); });
const data = response.data; const data = response.data;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const history: AttendanceRecord[] = data.history.map((h: any) => ({ const history: AttendanceRecord[] = data.history.map((h: any) => ({
date: h.workDate, date: h.workDate,
checkIn: h.checkIn ? h.checkIn.substring(0, 5) : '-', checkIn: h.checkIn ? h.checkIn.substring(0, 5) : '-',
@ -60,7 +61,6 @@ export const useAttendanceSummary = (employeeId: number, startDate: string, endD
}); });
}; };
// 3. Fetch Dashboard Metrics
export const useDashboardMetrics = (date: string, companyId: string, branchId: string, departmentId: string) => { export const useDashboardMetrics = (date: string, companyId: string, branchId: string, departmentId: string) => {
return useQuery<DashboardMetrics>({ return useQuery<DashboardMetrics>({
queryKey: ['dashboardMetrics', date, companyId, branchId, departmentId], queryKey: ['dashboardMetrics', date, companyId, branchId, departmentId],
@ -84,16 +84,6 @@ export const useDashboardMetrics = (date: string, companyId: string, branchId: s
}); });
}; };
// 4. Fetch Daily Report
export interface DailyReportFilters {
date: string;
companyId: string;
branchId: string;
departmentId: string;
designationId: string;
employeeName: string;
employeeCode: string;
}
export const useDailyReport = (filters: DailyReportFilters) => { export const useDailyReport = (filters: DailyReportFilters) => {
return useQuery<DailyReportRecord[]>({ return useQuery<DailyReportRecord[]>({
@ -116,17 +106,6 @@ export const useDailyReport = (filters: DailyReportFilters) => {
}); });
}; };
// 5. Fetch Admin Range Report
export interface AdminReportFilters {
fromDate: string;
toDate: string;
companyId: string;
branchId: string;
departmentId: string;
designation: string;
employeeName: string;
employeeCode: string;
}
export const useAdminReport = (filters: AdminReportFilters) => { export const useAdminReport = (filters: AdminReportFilters) => {
return useQuery<AdminReportRecord[]>({ return useQuery<AdminReportRecord[]>({
@ -150,16 +129,6 @@ export const useAdminReport = (filters: AdminReportFilters) => {
}); });
}; };
// 6. Create Regularization Request
export interface RegularizationPayload {
attendance_id: number;
employee_id: number;
regularization_type: string;
target_date: string;
requested_check_in: string;
requested_check_out: string;
reason: string;
}
export const useCreateRegularization = () => { export const useCreateRegularization = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@ -176,20 +145,6 @@ export const useCreateRegularization = () => {
}); });
}; };
// 7. Fetch HR Manager Regularizations
export interface RegularizationRecord {
id: string;
empId: string;
empName: string;
date: string;
regularizationType: string;
reqCheckIn: string;
reqCheckOut: string;
reason: string;
status: 'Pending' | 'Approved' | 'Rejected';
branchName: string;
reviewerName?: string;
}
export const useHRManagerRegularizations = () => { export const useHRManagerRegularizations = () => {
return useQuery<RegularizationRecord[]>({ return useQuery<RegularizationRecord[]>({
@ -198,6 +153,7 @@ export const useHRManagerRegularizations = () => {
const response = await apiClient.get('/api/ams/attendance/regularize/pending'); const response = await apiClient.get('/api/ams/attendance/regularize/pending');
const rawData = response.data.data || []; const rawData = response.data.data || [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return rawData.map((r: any) => { return rawData.map((r: any) => {
const targetDate = r.targetDate ? new Date(r.targetDate) : new Date(); const targetDate = r.targetDate ? new Date(r.targetDate) : new Date();
const reqCheckInDate = r.requestedCheckIn ? new Date(r.requestedCheckIn) : new Date(); const reqCheckInDate = r.requestedCheckIn ? new Date(r.requestedCheckIn) : new Date();
@ -224,7 +180,6 @@ export const useHRManagerRegularizations = () => {
}); });
}; };
// 8. Review Regularization Request (Approve/Reject)
export const useReviewRegularization = () => { export const useReviewRegularization = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
@ -250,6 +205,7 @@ export const useRegularizationHistory = () => {
const response = await apiClient.get('/api/ams/attendance/regularize/history'); const response = await apiClient.get('/api/ams/attendance/regularize/history');
const rawData = response.data.data || []; const rawData = response.data.data || [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return rawData.map((r: any) => { return rawData.map((r: any) => {
const targetDate = r.targetDate ? new Date(r.targetDate) : new Date(); const targetDate = r.targetDate ? new Date(r.targetDate) : new Date();
const reqCheckInDate = r.requestedCheckIn ? new Date(r.requestedCheckIn) : new Date(); const reqCheckInDate = r.requestedCheckIn ? new Date(r.requestedCheckIn) : new Date();
@ -275,11 +231,4 @@ export const useRegularizationHistory = () => {
}); });
}, },
}); });
}; };
// Update the interface
// Update the useHRManagerRegularizations hook mapping

View File

@ -19,7 +19,6 @@ interface DailySummaryProps {
export default function DailySummary({ selectedDate, data, employeeId }: DailySummaryProps) { export default function DailySummary({ selectedDate, data, employeeId }: DailySummaryProps) {
const selectedData = data || { status: 'No Data', login: '-', logout: '-', hours: '-', attendanceId: 0 }; const selectedData = data || { status: 'No Data', login: '-', logout: '-', hours: '-', attendanceId: 0 };
// Updated condition to include Half Day
const canRegularize = selectedData.status === 'Mispunch' || selectedData.status === 'Half Day'; const canRegularize = selectedData.status === 'Mispunch' || selectedData.status === 'Half Day';
const [isApplied, setIsApplied] = useState(false); const [isApplied, setIsApplied] = useState(false);
@ -27,6 +26,7 @@ export default function DailySummary({ selectedDate, data, employeeId }: DailySu
const createRegMutation = useCreateRegularization(); const createRegMutation = useCreateRegularization();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const handleApplyRegularization = (payload: any) => { const handleApplyRegularization = (payload: any) => {
createRegMutation.mutate(payload, { createRegMutation.mutate(payload, {
onSuccess: () => { onSuccess: () => {
@ -34,6 +34,7 @@ export default function DailySummary({ selectedDate, data, employeeId }: DailySu
setShowModal(false); setShowModal(false);
toast.success("Regularization request submitted successfully."); toast.success("Regularization request submitted successfully.");
}, },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onError: (error: any) => { onError: (error: any) => {
toast.error(error?.response?.data?.message || "Failed to submit request."); toast.error(error?.response?.data?.message || "Failed to submit request.");
} }
@ -117,7 +118,7 @@ export default function DailySummary({ selectedDate, data, employeeId }: DailySu
selectedDate={selectedDate} selectedDate={selectedDate}
attendanceId={selectedData.attendanceId} attendanceId={selectedData.attendanceId}
employeeId={employeeId} employeeId={employeeId}
status={selectedData.status} // Pass the status to the modal status={selectedData.status}
onClose={() => setShowModal(false)} onClose={() => setShowModal(false)}
onApply={handleApplyRegularization} onApply={handleApplyRegularization}
/> />

View File

@ -5,8 +5,9 @@ interface RegularizationModalProps {
selectedDate: string; selectedDate: string;
attendanceId: number; attendanceId: number;
employeeId: number; employeeId: number;
status: string; // Added status prop status: string;
onClose: () => void; onClose: () => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onApply: (payload: any) => void; onApply: (payload: any) => void;
} }
@ -27,7 +28,7 @@ export default function RegularizationModal({ selectedDate, attendanceId, employ
const payload = { const payload = {
attendance_id: attendanceId, attendance_id: attendanceId,
employee_id: employeeId, employee_id: employeeId,
regularization_type: backendType, // Dynamic type regularization_type: backendType,
target_date: selectedDate, target_date: selectedDate,
requested_check_in: `${selectedDate} ${regData.checkIn}:00`, requested_check_in: `${selectedDate} ${regData.checkIn}:00`,
requested_check_out: `${selectedDate} ${regData.checkOut}:00`, requested_check_out: `${selectedDate} ${regData.checkOut}:00`,

View File

@ -4,7 +4,7 @@ import { useCompanies } from '../../../masterData/api/useCompanyData';
import { useBranches } from '../../../masterData/api/useBranchData'; import { useBranches } from '../../../masterData/api/useBranchData';
import { useDepartments } from '../../../masterData/api/useDepartmentData'; import { useDepartments } from '../../../masterData/api/useDepartmentData';
import { useJobRoles } from '../../../masterData/api/useJobRoleData'; import { useJobRoles } from '../../../masterData/api/useJobRoleData';
import type { DailyReportFilters } from '../../api/useAttendanceData'; import type { DailyReportFilters } from '../../types/attendance';
interface DailyReportFiltersProps { interface DailyReportFiltersProps {
filters: DailyReportFilters; filters: DailyReportFilters;

View File

@ -0,0 +1,138 @@
// src/features/attendance/components/summary/DailyReportTable.tsx
import { useState, useMemo } from 'react';
import { Loader2, ChevronsLeft, ChevronLeft, ChevronRight, ChevronsRight } from 'lucide-react';
import type { DailyReportRecord } from '../../types/attendance';
interface DailyReportTableProps {
records: DailyReportRecord[];
date: string;
isLoading?: boolean;
}
const getStatusClass = (status: string) => {
switch (status) {
case 'FULL_DAY': return 'bg-present-100 text-present-700';
case 'LATE': return 'bg-late-100 text-late-700';
case 'ABSENT': return 'bg-absent-100 text-absent-700';
case 'MISPUNCH': return 'bg-mispunch-100 text-mispunch-700';
case 'HALF_DAY': return 'bg-halfday-100 text-halfday-700';
default: return 'bg-app-muted text-text-secondary';
}
};
export default function DailyReportTable({ records, date, isLoading = false }: DailyReportTableProps) {
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(20); // Default 20 entries
const totalPages = Math.ceil(records.length / pageSize);
const paginatedData = useMemo(() => {
const startIndex = (currentPage - 1) * pageSize;
return records.slice(startIndex, startIndex + pageSize);
}, [records, 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 handlePageSizeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setPageSize(Number(e.target.value));
setCurrentPage(1);
};
const formattedDate = new Date(date + 'T00:00:00').toLocaleDateString('en-GB', { day: '2-digit', month: 'long', year: 'numeric' });
return (
<div className="bg-app-card p-6 rounded-lg shadow-sm mt-6 overflow-x-auto">
<h3 className="text-lg font-semibold text-text-primary mb-4">
Attendance Report for {formattedDate} ({records.length})
</h3>
<table className="min-w-full divide-y divide-app-border">
<thead>
<tr>
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Emp CODE</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Name</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Company</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Branch</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Department</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Job Role</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Check-In</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Check-Out</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Hours</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Status</th>
</tr>
</thead>
<tbody className="divide-y divide-app-border">
{isLoading ? (
<tr>
<td colSpan={10} className="px-4 py-8 text-center text-sm text-text-muted">
<Loader2 size={20} className="animate-spin mx-auto text-primary" />
</td>
</tr>
) : paginatedData.length > 0 ? (
paginatedData.map((record, index) => (
<tr key={index} className="hover:bg-app-muted transition-colors">
<td className="px-4 py-3 text-sm text-text-primary">{record.employeeCode}</td>
<td className="px-4 py-3 text-sm text-text-secondary">{record.fullName}</td>
<td className="px-4 py-3 text-sm text-text-secondary">{record.companyName || '-'}</td>
<td className="px-4 py-3 text-sm text-text-secondary">{record.branchName || '-'}</td>
<td className="px-4 py-3 text-sm text-text-secondary">{record.departmentName || '-'}</td>
<td className="px-4 py-3 text-sm text-text-secondary">{record.designation || '-'}</td>
<td className="px-4 py-3 text-sm text-text-secondary">{record.checkIn ? record.checkIn.substring(0, 5) : '-'}</td>
<td className="px-4 py-3 text-sm text-text-secondary">{record.checkOut ? record.checkOut.substring(0, 5) : '-'}</td>
<td className="px-4 py-3 text-sm text-text-secondary">{record.workedHours}</td>
<td className="px-4 py-3 text-sm">
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusClass(record.status)}`}>
{record.status.replace('_', ' ')}
</span>
</td>
</tr>
))
) : (
<tr>
<td colSpan={10} className="px-4 py-8 text-center text-sm text-text-muted">
No attendance records found for this date.
</td>
</tr>
)}
</tbody>
</table>
{/* Pagination Controls */}
<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={20}>20</option>
<option value={50}>50</option>
<option value={100}>100</option>
</select>
<span className="text-sm text-text-muted">entries</span>
</div>
{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>
);
}

View File

@ -4,7 +4,7 @@ import { useCompanies } from '../../../masterData/api/useCompanyData';
import { useBranches } from '../../../masterData/api/useBranchData'; import { useBranches } from '../../../masterData/api/useBranchData';
import { useDepartments } from '../../../masterData/api/useDepartmentData'; import { useDepartments } from '../../../masterData/api/useDepartmentData';
import { useJobRoles } from '../../../masterData/api/useJobRoleData'; import { useJobRoles } from '../../../masterData/api/useJobRoleData';
import type { AdminReportFilters } from '../../api/useAttendanceData'; import type { AdminReportFilters } from '../../types/attendance';
interface MonthlyReportFiltersProps { interface MonthlyReportFiltersProps {
filters: AdminReportFilters; filters: AdminReportFilters;

View File

@ -0,0 +1,124 @@
import { useState, useMemo } from 'react';
import { Loader2, ChevronsLeft, ChevronLeft, ChevronRight, ChevronsRight } from 'lucide-react';
import type { AdminReportRecord } from '../../types/attendance';
interface MonthlyReportTableProps {
records: AdminReportRecord[];
fromDate: string;
toDate: string;
isLoading?: boolean;
}
export default function MonthlyReportTable({ records, fromDate, toDate, isLoading = false }: MonthlyReportTableProps) {
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(20);
const totalPages = Math.ceil(records.length / pageSize);
const paginatedData = useMemo(() => {
const startIndex = (currentPage - 1) * pageSize;
return records.slice(startIndex, startIndex + pageSize);
}, [records, 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 handlePageSizeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setPageSize(Number(e.target.value));
setCurrentPage(1);
};
const formattedFromDate = new Date(fromDate + 'T00:00:00').toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' });
const formattedToDate = new Date(toDate + 'T00:00:00').toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' });
return (
<div className="bg-app-card p-6 rounded-lg shadow-sm mt-6 overflow-x-auto">
<h3 className="text-lg font-semibold text-text-primary mb-4">
Monthly Attendance Report from {formattedFromDate} to {formattedToDate} ({records.length})
</h3>
<table className="min-w-full divide-y divide-app-border">
<thead>
<tr>
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Emp CODE</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Name</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Company</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Branch</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Department</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Job Role</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Full Days</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Half Days</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Late Arrivals</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Mispunches</th>
</tr>
</thead>
<tbody className="divide-y divide-app-border">
{isLoading ? (
<tr>
<td colSpan={10} className="px-4 py-8 text-center text-sm text-text-muted">
<Loader2 size={20} className="animate-spin mx-auto text-primary" />
</td>
</tr>
) : paginatedData.length > 0 ? (
paginatedData.map((record, index) => (
<tr key={index} className="hover:bg-app-muted transition-colors">
<td className="px-4 py-3 text-sm text-text-primary">{record.employeeCode}</td>
<td className="px-4 py-3 text-sm text-text-secondary">{record.firstName} {record.lastName}</td>
<td className="px-4 py-3 text-sm text-text-secondary">{record.companyName || '-'}</td>
<td className="px-4 py-3 text-sm text-text-secondary">{record.branchName || '-'}</td>
<td className="px-4 py-3 text-sm text-text-secondary">{record.departmentName || '-'}</td>
<td className="px-4 py-3 text-sm text-text-secondary">{record.designation || '-'}</td>
<td className="px-4 py-3 text-sm text-text-secondary">{record.rangeMetrics?.fullDays || 0}</td>
<td className="px-4 py-3 text-sm text-text-secondary">{record.rangeMetrics?.halfDays || 0}</td>
<td className="px-4 py-3 text-sm text-text-secondary">{record.rangeMetrics?.lateArrivals || 0}</td>
<td className="px-4 py-3 text-sm text-text-secondary">{record.rangeMetrics?.mispunches || 0}</td>
</tr>
))
) : (
<tr>
<td colSpan={10} className="px-4 py-8 text-center text-sm text-text-muted">
No attendance records found for this period.
</td>
</tr>
)}
</tbody>
</table>
{/* Pagination Controls */}
<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={20}>20</option>
<option value={50}>50</option>
<option value={100}>100</option>
</select>
<span className="text-sm text-text-muted">entries</span>
</div>
{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>
);
}

View File

@ -1,5 +1,5 @@
import { X } from 'lucide-react'; import { X } from 'lucide-react';
import type { RegularizationRecord } from '../../api/useAttendanceData'; import type { RegularizationRecord } from '../../types/attendance';
interface RegularizationHistoryModalProps { interface RegularizationHistoryModalProps {
record: RegularizationRecord; record: RegularizationRecord;
@ -22,10 +22,13 @@ export default function RegularizationHistoryModal({ record, onClose }: Regulari
<button onClick={onClose} className="absolute top-4 right-4 text-text-muted hover:text-text-primary"> <button onClick={onClose} className="absolute top-4 right-4 text-text-muted hover:text-text-primary">
<X size={24} /> <X size={24} />
</button> </button>
<h3 className="text-xl font-bold text-text-primary mb-1">Regularization Details</h3> <h3 className="text-xl font-bold text-text-primary mb-6">Regularization Details</h3>
<p className="text-sm text-text-muted mb-6">Request ID: {record.id}</p>
<div className="space-y-4"> <div className="space-y-4">
<div className="flex justify-between border-b border-app-border pb-2">
<span className="text-sm text-text-muted">Request ID</span>
<span className="text-sm font-medium text-text-primary">{record.id}</span>
</div>
<div className="flex justify-between border-b border-app-border pb-2"> <div className="flex justify-between border-b border-app-border pb-2">
<span className="text-sm text-text-muted">Emp Id</span> <span className="text-sm text-text-muted">Emp Id</span>
<span className="text-sm font-medium text-text-primary">{record.empId}</span> <span className="text-sm font-medium text-text-primary">{record.empId}</span>

View File

@ -1,7 +1,7 @@
import { useState, useMemo } from 'react'; import { useState, useMemo } from 'react';
import { ArrowUpDown, ArrowUp, ArrowDown, Eye, Search, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react'; import { ArrowUpDown, ArrowUp, ArrowDown, Eye, Search, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';
import type { RegularizationRecord } from '../../api/useAttendanceData'; import type { RegularizationRecord } from '../../types/attendance';
import RegularizationHistoryModal from './RegularizationHistoryModal'; // NEW IMPORT import RegularizationHistoryModal from './RegularizationHistoryModal';
interface RegularizationHistoryTableProps { interface RegularizationHistoryTableProps {
records: RegularizationRecord[]; records: RegularizationRecord[];

View File

@ -1,5 +1,5 @@
import { X, Check, Ban } from 'lucide-react'; import { X, Check, Ban } from 'lucide-react';
import type { RegularizationRecord } from '../../api/useAttendanceData'; import type { RegularizationRecord } from '../../types/attendance';
interface RegularizationModalProps { interface RegularizationModalProps {
record: RegularizationRecord; record: RegularizationRecord;
@ -23,10 +23,13 @@ export default function RegularizationModal({ record, onClose, onAction }: Regul
<button onClick={onClose} className="absolute top-4 right-4 text-text-muted hover:text-text-primary"> <button onClick={onClose} className="absolute top-4 right-4 text-text-muted hover:text-text-primary">
<X size={24} /> <X size={24} />
</button> </button>
<h3 className="text-xl font-bold text-text-primary mb-1">Regularization Request</h3> <h3 className="text-xl font-bold text-text-primary mb-6">Regularization Request</h3>
<p className="text-sm text-text-muted mb-6">Request ID: {record.id}</p>
<div className="space-y-4"> <div className="space-y-4">
<div className="flex justify-between border-b border-app-border pb-2">
<span className="text-sm text-text-muted">Request ID</span>
<span className="text-sm font-medium text-text-primary">{record.id}</span>
</div>
<div className="flex justify-between border-b border-app-border pb-2"> <div className="flex justify-between border-b border-app-border pb-2">
<span className="text-sm text-text-muted">Emp Id</span> <span className="text-sm text-text-muted">Emp Id</span>
<span className="text-sm font-medium text-text-primary">{record.empId}</span> <span className="text-sm font-medium text-text-primary">{record.empId}</span>

View File

@ -1,6 +1,6 @@
import { useState, useMemo } from 'react'; import { useState, useMemo } from 'react';
import { ArrowUpDown, ArrowUp, ArrowDown, Eye, Search, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react'; import { ArrowUpDown, ArrowUp, ArrowDown, Eye, Search, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';
import type { RegularizationRecord } from '../../api/useAttendanceData'; import type { RegularizationRecord } from '../../types/attendance';
import RegularizationModal from './RegularizationModal'; import RegularizationModal from './RegularizationModal';
interface RegularizationTableProps { interface RegularizationTableProps {
@ -8,7 +8,6 @@ interface RegularizationTableProps {
onAction: (id: string, status: 'Approved' | 'Rejected') => void; onAction: (id: string, status: 'Approved' | 'Rejected') => void;
} }
// Updated SortKey to include branchName and regularizationType
type SortKey = 'empId' | 'empName' | 'branchName' | 'date' | 'regularizationType' | 'reqCheckIn' | 'reqCheckOut' | 'reason' | 'status'; type SortKey = 'empId' | 'empName' | 'branchName' | 'date' | 'regularizationType' | 'reqCheckIn' | 'reqCheckOut' | 'reason' | 'status';
type SortDirection = 'ascending' | 'descending'; type SortDirection = 'ascending' | 'descending';

View File

@ -59,7 +59,7 @@ export default function AttendanceDashboard() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Professional Grid Filter Bar */} {/* Grid Filter Bar */}
<div className="bg-app-card p-4 rounded-lg shadow-sm grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"> <div className="bg-app-card p-4 rounded-lg shadow-sm grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="flex flex-col"> <div className="flex flex-col">
<label className="text-xs text-text-muted mb-1">Date</label> <label className="text-xs text-text-muted mb-1">Date</label>
@ -70,7 +70,7 @@ export default function AttendanceDashboard() {
className="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" className="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"
/> />
</div> </div>
{/* Company Dropdown */} {/* Company Dropdown */}
<div className="flex flex-col"> <div className="flex flex-col">
<label className="text-xs text-text-muted mb-1">Company</label> <label className="text-xs text-text-muted mb-1">Company</label>

View File

@ -2,7 +2,9 @@ import { useState } from 'react';
import { FileText, Download, Loader2 } from 'lucide-react'; import { FileText, Download, Loader2 } from 'lucide-react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import DailyReportFilters from '../components/summary/DailyReportFilters'; import DailyReportFilters from '../components/summary/DailyReportFilters';
import { useDailyReport, type DailyReportFilters as FilterValues } from '../api/useAttendanceData'; import DailyReportTable from '../components/summary/DailyReportTable';
import { useDailyReport } from '../api/useAttendanceData';
import { type DailyReportFilters as FilterValues } from '../types/attendance';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import type { RootState } from '../../../store/store'; import type { RootState } from '../../../store/store';
@ -37,7 +39,7 @@ export default function DailyReport() {
designation: filters.designationId, designation: filters.designationId,
employee_name: filters.employeeName, employee_name: filters.employeeName,
employee_code: filters.employeeCode, employee_code: filters.employeeCode,
file_type: downloadFormat, // Pass format to backend file_type: downloadFormat,
}).toString(); }).toString();
const response = await fetch(`/api/ams/attendance/daily-report/export?${queryParams}`, { const response = await fetch(`/api/ams/attendance/daily-report/export?${queryParams}`, {
@ -49,38 +51,28 @@ export default function DailyReport() {
}); });
if (!response.ok) throw new Error('Failed to download report'); if (!response.ok) throw new Error('Failed to download report');
const blob = await response.blob(); const blob = await response.blob();
const url = window.URL.createObjectURL(blob); const url = window.URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement('a');
a.href = url; a.href = url;
const contentDisposition = response.headers.get('Content-Disposition'); const contentDisposition = response.headers.get('Content-Disposition');
const fileName = contentDisposition ? contentDisposition.split('filename=')[1].replace(/"/g, '') : `Daily_Report.${downloadFormat === 'excel' ? 'xlsx' : 'pdf'}`; const fileName = contentDisposition ? contentDisposition.split('filename=')[1].replace(/"/g, '') : `Daily_Report.${downloadFormat === 'excel' ? 'xlsx' : 'pdf'}`;
a.download = fileName; a.download = fileName;
document.body.appendChild(a); document.body.appendChild(a);
a.click(); a.click();
document.body.removeChild(a); document.body.removeChild(a);
window.URL.revokeObjectURL(url); window.URL.revokeObjectURL(url);
toast.success('Report downloaded successfully.'); toast.success('Report downloaded successfully.');
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) { } catch (error) {
toast.error('Failed to download report.'); toast.error('Failed to download report.');
} }
}; };
const getStatusClass = (status: string) => {
switch (status) {
case 'FULL_DAY': return 'bg-present-100 text-present-700';
case 'LATE': return 'bg-late-100 text-late-700';
case 'ABSENT': return 'bg-absent-100 text-absent-700';
case 'MISPUNCH': return 'bg-mispunch-100 text-mispunch-700';
case 'HALF_DAY': return 'bg-halfday-100 text-halfday-700';
default: return 'bg-app-muted text-text-secondary';
}
};
return ( return (
<div className="space-y-6"> <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="bg-app-card p-6 rounded-lg shadow-sm flex flex-col md:flex-row md:items-center justify-between gap-4">
@ -93,11 +85,10 @@ export default function DailyReport() {
<p className="text-sm text-text-muted">View daily attendance logs based on filters.</p> <p className="text-sm text-text-muted">View daily attendance logs based on filters.</p>
</div> </div>
</div> </div>
{/* Download Controls */}
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<select <select
value={downloadFormat} value={downloadFormat}
onChange={(e) => setDownloadFormat(e.target.value as 'excel' | 'pdf')} onChange={(e) => setDownloadFormat(e.target.value as 'excel' | 'pdf')}
className="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 h-[38px]" className="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 h-[38px]"
> >
@ -117,62 +108,11 @@ export default function DailyReport() {
<DailyReportFilters filters={filters} onFilterChange={setFilters} /> <DailyReportFilters filters={filters} onFilterChange={setFilters} />
{/* Data Table */} <DailyReportTable
<div className="bg-app-card p-6 rounded-lg shadow-sm overflow-x-auto"> records={reportData || []}
<h3 className="text-lg font-semibold text-text-primary mb-4"> date={filters.date}
Results for {new Date(filters.date + 'T00:00:00').toLocaleDateString('en-GB', { day: '2-digit', month: 'long', year: 'numeric' })} ({reportData?.length || 0}) isLoading={isLoading || isFetching}
</h3> />
<table className="min-w-full divide-y divide-app-border">
<thead>
<tr>
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Emp CODE</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Name</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Status</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Check-In</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Check-Out</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Worked Hours</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Company</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Branch</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Department</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Job Role</th>
</tr>
</thead>
<tbody className="divide-y divide-app-border">
{isLoading ? (
<tr>
<td colSpan={10} className="px-4 py-8 text-center text-sm text-text-muted">
<Loader2 size={20} className="animate-spin mx-auto text-primary" />
</td>
</tr>
) : reportData && reportData.length > 0 ? (
reportData.map((record, index) => (
<tr key={index} className="hover:bg-app-muted transition-colors">
<td className="px-4 py-3 text-sm text-text-primary">{record.employeeCode}</td>
<td className="px-4 py-3 text-sm text-text-secondary">{record.fullName}</td>
<td className="px-4 py-3 text-sm">
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusClass(record.status)}`}>
{record.status.replace('_', ' ')}
</span>
</td>
<td className="px-4 py-3 text-sm text-text-secondary">{record.checkIn ? record.checkIn.substring(0, 5) : '-'}</td>
<td className="px-4 py-3 text-sm text-text-secondary">{record.checkOut ? record.checkOut.substring(0, 5) : '-'}</td>
<td className="px-4 py-3 text-sm text-text-secondary">{record.workedHours}</td>
<td className="px-4 py-3 text-sm text-text-secondary">{record.companyName || '-'}</td>
<td className="px-4 py-3 text-sm text-text-secondary">{record.branchName || '-'}</td>
<td className="px-4 py-3 text-sm text-text-secondary">{record.departmentName || '-'}</td>
<td className="px-4 py-3 text-sm text-text-secondary">{record.designation || '-'}</td>
</tr>
))
) : (
<tr>
<td colSpan={10} className="px-4 py-8 text-center text-sm text-text-muted">
No attendance records found for this date.
</td>
</tr>
)}
</tbody>
</table>
</div>
</div> </div>
); );
} }

View File

@ -14,6 +14,7 @@ export default function HRManagerRegularization() {
onSuccess: () => { onSuccess: () => {
toast.success(`Regularization request ${status.toLowerCase()} successfully.`); toast.success(`Regularization request ${status.toLowerCase()} successfully.`);
}, },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onError: (error: any) => { onError: (error: any) => {
toast.error(error?.response?.data?.error || "Failed to update regularization status."); toast.error(error?.response?.data?.error || "Failed to update regularization status.");
} }

View File

@ -2,7 +2,9 @@ import { useState } from 'react';
import { Download, FileText, Loader2 } from 'lucide-react'; import { Download, FileText, Loader2 } from 'lucide-react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import MonthlyReportFilters from '../components/summary/MonthlyReportFilters'; import MonthlyReportFilters from '../components/summary/MonthlyReportFilters';
import { useAdminReport, type AdminReportFilters } from '../api/useAttendanceData'; import MonthlyReportTable from '../components/summary/MonthlyReportTable';
import { type AdminReportFilters } from '../types/attendance';
import { useAdminReport } from '../api/useAttendanceData';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import type { RootState } from '../../../store/store'; import type { RootState } from '../../../store/store';
@ -56,22 +58,23 @@ export default function MonthlyReport() {
}); });
if (!response.ok) throw new Error('Failed to download report'); if (!response.ok) throw new Error('Failed to download report');
const blob = await response.blob(); const blob = await response.blob();
const url = window.URL.createObjectURL(blob); const url = window.URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement('a');
a.href = url; a.href = url;
const contentDisposition = response.headers.get('Content-Disposition'); const contentDisposition = response.headers.get('Content-Disposition');
const fileName = contentDisposition ? contentDisposition.split('filename=')[1].replace(/"/g, '') : `Monthly_Report.${downloadFormat === 'excel' ? 'xlsx' : 'pdf'}`; const fileName = contentDisposition ? contentDisposition.split('filename=')[1].replace(/"/g, '') : `Monthly_Report.${downloadFormat === 'excel' ? 'xlsx' : 'pdf'}`;
a.download = fileName; a.download = fileName;
document.body.appendChild(a); document.body.appendChild(a);
a.click(); a.click();
document.body.removeChild(a); document.body.removeChild(a);
window.URL.revokeObjectURL(url); window.URL.revokeObjectURL(url);
toast.success('Report downloaded successfully.'); toast.success('Report downloaded successfully.');
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) { } catch (error) {
toast.error('Failed to download report.'); toast.error('Failed to download report.');
} }
@ -89,10 +92,10 @@ export default function MonthlyReport() {
<p className="text-sm text-text-muted">View monthly attendance metrics based on filters.</p> <p className="text-sm text-text-muted">View monthly attendance metrics based on filters.</p>
</div> </div>
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<select <select
value={downloadFormat} value={downloadFormat}
onChange={(e) => setDownloadFormat(e.target.value as 'excel' | 'pdf')} onChange={(e) => setDownloadFormat(e.target.value as 'excel' | 'pdf')}
className="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 h-[38px]" className="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 h-[38px]"
> >
@ -112,53 +115,12 @@ export default function MonthlyReport() {
<MonthlyReportFilters filters={filters} onFilterChange={setFilters} /> <MonthlyReportFilters filters={filters} onFilterChange={setFilters} />
<div className="bg-app-card p-6 rounded-lg shadow-sm overflow-x-auto"> <MonthlyReportTable
<h3 className="text-lg font-semibold text-text-primary mb-4"> records={reportData || []}
Filtered Results ({reportData?.length || 0}) fromDate={filters.fromDate}
</h3> toDate={filters.toDate}
<table className="min-w-full divide-y divide-app-border"> isLoading={isLoading || isFetching}
<thead> />
<tr>
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Emp CODE</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Name</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Branch</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Department</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Full Days</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Half Days</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Late Arrivals</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Mispunches</th>
</tr>
</thead>
<tbody className="divide-y divide-app-border">
{isLoading ? (
<tr>
<td colSpan={8} className="px-4 py-8 text-center text-sm text-text-muted">
<Loader2 size={20} className="animate-spin mx-auto text-primary" />
</td>
</tr>
) : reportData && reportData.length > 0 ? (
reportData.map((record, index) => (
<tr key={index} className="hover:bg-app-muted transition-colors">
<td className="px-4 py-3 text-sm text-text-primary">{record.employeeCode}</td>
<td className="px-4 py-3 text-sm text-text-secondary">{record.firstName} {record.lastName}</td>
<td className="px-4 py-3 text-sm text-text-secondary">{record.branchName || '-'}</td>
<td className="px-4 py-3 text-sm text-text-secondary">{record.departmentName || '-'}</td>
<td className="px-4 py-3 text-sm text-text-secondary">{record.rangeMetrics?.fullDays || 0}</td>
<td className="px-4 py-3 text-sm text-text-secondary">{record.rangeMetrics?.halfDays || 0}</td>
<td className="px-4 py-3 text-sm text-text-secondary">{record.rangeMetrics?.lateArrivals || 0}</td>
<td className="px-4 py-3 text-sm text-text-secondary">{record.rangeMetrics?.mispunches || 0}</td>
</tr>
))
) : (
<tr>
<td colSpan={8} className="px-4 py-8 text-center text-sm text-text-muted">
No attendance records found for this period.
</td>
</tr>
)}
</tbody>
</table>
</div>
</div> </div>
); );
} }

View File

@ -51,8 +51,10 @@ export interface AdminReportRecord {
employeeCode: string; employeeCode: string;
firstName: string; firstName: string;
lastName: string; lastName: string;
companyName: string;
branchName: string; branchName: string;
departmentName: string; departmentName: string;
designation: string;
rangeMetrics: { rangeMetrics: {
fullDays: number; fullDays: number;
halfDays: number; halfDays: number;
@ -60,3 +62,49 @@ export interface AdminReportRecord {
lateArrivals: number; lateArrivals: number;
}; };
} }
export interface DailyReportFilters {
date: string;
companyId: string;
branchId: string;
departmentId: string;
designationId: string;
employeeName: string;
employeeCode: string;
}
export interface AdminReportFilters {
fromDate: string;
toDate: string;
companyId: string;
branchId: string;
departmentId: string;
designation: string;
employeeName: string;
employeeCode: string;
}
export interface RegularizationPayload {
attendance_id: number;
employee_id: number;
regularization_type: string;
target_date: string;
requested_check_in: string;
requested_check_out: string;
reason: string;
}
export interface RegularizationRecord {
id: string;
empId: string;
empName: string;
date: string;
regularizationType: string;
reqCheckIn: string;
reqCheckOut: string;
reason: string;
status: 'Pending' | 'Approved' | 'Rejected';
branchName: string;
reviewerName?: string;
}

View File

@ -1,86 +1,8 @@
import { Building, Network, Users, ShieldUser, ArrowRight, ScrollText } from 'lucide-react';
import { Link } from 'react-router-dom';
export default function DirectorDashboard() { export default function DirectorDashboard() {
const stats = [
{ title: 'Total Companies', value: '3', icon: Building, color: 'text-primary', bg: 'bg-primary/10' },
{ title: 'Total Branches', value: '12', icon: Network, color: 'text-primary', bg: 'bg-primary/10' },
{ title: 'Global Employees', value: '450', icon: Users, color: 'text-present-700', bg: 'bg-present-100' },
{ title: 'Total HRManagers', value: '5', icon: ShieldUser, color: 'text-late-700', bg: 'bg-late-100' },
];
const companies = [
{ name: 'CLRI', employees: 245, status: 'Active' },
{ name: 'TechNova Solutions', employees: 120, status: 'Active' },
{ name: 'Infotech Ltd', employees: 85, status: 'Active' },
];
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="bg-app-card p-6 rounded-lg shadow-sm"> <div className="bg-app-card p-6 rounded-lg shadow-sm">
<h2 className="text-2xl font-bold text-text-primary">Welcome back, Director!</h2> <h2 className="text-2xl font-bold text-text-primary">Welcome back, Director!</h2>
<p className="text-text-muted mt-1">Here is the global organizational overview.</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{stats.map((stat, index) => (
<div key={index} className="bg-app-card p-6 rounded-lg shadow-sm flex items-center justify-between transition-transform hover:scale-[1.02]">
<div>
<p className="text-sm text-text-muted font-medium">{stat.title}</p>
<p className={`text-2xl font-bold mt-2 ${stat.color}`}>{stat.value}</p>
</div>
<div className={`p-3 rounded-full ${stat.bg}`}>
<stat.icon className={stat.color} size={28} />
</div>
</div>
))}
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-app-card p-6 rounded-lg shadow-sm">
<h3 className="text-lg font-semibold text-text-primary mb-4">Company Overview</h3>
<div className="space-y-3">
{companies.map((c, idx) => (
<div key={idx} className="flex items-center justify-between p-3 border border-app-border rounded-lg">
<div className="flex items-center">
<Building size={18} className="text-text-light mr-3" />
<span className="text-sm font-medium text-text-primary">{c.name}</span>
</div>
<div className="flex items-center space-x-4">
<span className="text-sm text-text-secondary">{c.employees} Employees</span>
<span className="px-2 py-1 rounded-full text-xs font-medium bg-present-100 text-present-700">{c.status}</span>
</div>
</div>
))}
</div>
</div>
<div className="bg-app-card p-6 rounded-lg shadow-sm">
<h3 className="text-lg font-semibold text-text-primary mb-4">Director Actions</h3>
<div className="space-y-3">
<Link to="/manage-hrmanagers" className="flex items-center justify-between p-4 border border-app-border rounded-lg hover:bg-app-muted transition-colors">
<div className="flex items-center">
<ShieldUser size={20} className="text-primary mr-3" />
<span className="text-sm font-medium text-text-primary">Manage HR Managers</span>
</div>
<ArrowRight size={18} className="text-text-light" />
</Link>
<Link to="/policies" className="flex items-center justify-between p-4 border border-app-border rounded-lg hover:bg-app-muted transition-colors">
<div className="flex items-center">
<ScrollText size={20} className="text-primary mr-3" />
<span className="text-sm font-medium text-text-primary">Policy and Rules</span>
</div>
<ArrowRight size={18} className="text-text-light" />
</Link>
<Link to="/master-data/company" className="flex items-center justify-between p-4 border border-app-border rounded-lg hover:bg-app-muted transition-colors">
<div className="flex items-center">
<Building size={20} className="text-primary mr-3" />
<span className="text-sm font-medium text-text-primary">Manage Master Data</span>
</div>
<ArrowRight size={18} className="text-text-light" />
</Link>
</div>
</div>
</div> </div>
</div> </div>
); );

View File

@ -1,48 +1,8 @@
import { CalendarCheck, Plane, Clock } from 'lucide-react';
import { Link } from 'react-router-dom';
export default function EmployeeDashboard() { export default function EmployeeDashboard() {
const stats = [
{ title: 'Attendance Status', value: 'Present', icon: CalendarCheck, color: 'text-present-700', bg: 'bg-present-100' },
{ title: 'Leave Balance', value: '12 Days', icon: Plane, color: 'text-primary', bg: 'bg-primary/10' },
{ title: 'Check-In Time', value: '09:05 AM', icon: Clock, color: 'text-late-700', bg: 'bg-late-100' },
];
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="bg-app-card p-6 rounded-lg shadow-sm"> <div className="bg-app-card p-6 rounded-lg shadow-sm">
<h2 className="text-2xl font-bold text-text-primary">Welcome back, Employee!</h2> <h2 className="text-2xl font-bold text-text-primary">Welcome back, Employee!</h2>
<p className="text-text-muted mt-1">Here is your attendance and leave summary for today.</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{stats.map((stat, index) => (
<div key={index} className="bg-app-card p-6 rounded-lg shadow-sm flex items-center justify-between transition-transform hover:scale-[1.02]">
<div>
<p className="text-sm text-text-muted font-medium">{stat.title}</p>
<p className={`text-2xl font-bold mt-2 ${stat.color}`}>{stat.value}</p>
</div>
<div className={`p-3 rounded-full ${stat.bg}`}>
<stat.icon className={stat.color} size={28} />
</div>
</div>
))}
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-app-card p-6 rounded-lg shadow-sm">
<h3 className="text-lg font-semibold text-text-primary mb-4">Quick Actions</h3>
<div className="grid grid-cols-2 gap-4">
<Link to="/leave" className="flex items-center p-4 border border-app-border rounded-lg hover:bg-app-muted transition-colors">
<Plane size={20} className="text-primary mr-3" />
<span className="text-sm font-medium text-text-primary">Check Leave Balance</span>
</Link>
<Link to="/attendance" className="flex items-center p-4 border border-app-border rounded-lg hover:bg-app-muted transition-colors">
<CalendarCheck size={20} className="text-present-700 mr-3" />
<span className="text-sm font-medium text-text-primary">View Attendance</span>
</Link>
</div>
</div>
</div> </div>
</div> </div>
); );

View File

@ -1,58 +1,8 @@
import { Users, CalendarCheck, ClipboardCheck, Plane, Building } from 'lucide-react';
import { Link } from 'react-router-dom';
export default function HRManagerDashboard() { export default function HRManagerDashboard() {
const stats = [
{ title: 'Total Employees', value: '245', icon: Users, color: 'text-primary', bg: 'bg-primary/10' },
{ title: 'Present Today', value: '198', icon: CalendarCheck, color: 'text-present-700', bg: 'bg-present-100' },
{ title: 'Pending Regularizations', value: '4', icon: ClipboardCheck, color: 'text-late-700', bg: 'bg-late-100' },
{ title: 'On Leave', value: '12', icon: Plane, color: 'text-absent-700', bg: 'bg-absent-100' },
];
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="bg-app-card p-6 rounded-lg shadow-sm"> <div className="bg-app-card p-6 rounded-lg shadow-sm">
<h2 className="text-2xl font-bold text-text-primary">Welcome back, HRManager!</h2> <h2 className="text-2xl font-bold text-text-primary">Welcome back, HRManager!</h2>
<p className="text-text-muted mt-1">Here is the organizational overview for today.</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{stats.map((stat, index) => (
<div key={index} className="bg-app-card p-6 rounded-lg shadow-sm flex items-center justify-between transition-transform hover:scale-[1.02]">
<div>
<p className="text-sm text-text-muted font-medium">{stat.title}</p>
<p className={`text-2xl font-bold mt-2 ${stat.color}`}>{stat.value}</p>
</div>
<div className={`p-3 rounded-full ${stat.bg}`}>
<stat.icon className={stat.color} size={28} />
</div>
</div>
))}
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-app-card p-6 rounded-lg shadow-sm">
<h3 className="text-lg font-semibold text-text-primary mb-4">Quick Links</h3>
<div className="grid grid-cols-2 gap-4">
<Link to="/master-data/employees" className="flex items-center p-4 border border-app-border rounded-lg hover:bg-app-muted transition-colors">
<Users size={20} className="text-primary mr-3" />
<span className="text-sm font-medium text-text-primary">Manage Employees</span>
</Link>
<Link to="/attendance-summary/dashboard" className="flex items-center p-4 border border-app-border rounded-lg hover:bg-app-muted transition-colors">
<CalendarCheck size={20} className="text-present-700 mr-3" />
<span className="text-sm font-medium text-text-primary">Attendance Summary</span>
</Link>
<Link to="/leave-summary/applications" className="flex items-center p-4 border border-app-border rounded-lg hover:bg-app-muted transition-colors">
<ClipboardCheck size={20} className="text-late-700 mr-3" />
<span className="text-sm font-medium text-text-primary">Leave Applications</span>
</Link>
<Link to="/master-data/company" className="flex items-center p-4 border border-app-border rounded-lg hover:bg-app-muted transition-colors">
<Building size={20} className="text-primary mr-3" />
<span className="text-sm font-medium text-text-primary">Master Data</span>
</Link>
</div>
</div>
</div> </div>
</div> </div>
); );

View File

@ -1,61 +1,8 @@
import { CheckCircle, Users, Plane, ArrowRight } from 'lucide-react';
import { Link } from 'react-router-dom';
export default function ManagerDashboard() { export default function ManagerDashboard() {
const stats = [
{ title: 'Pending Approvals', value: '5', icon: CheckCircle, color: 'text-late-700', bg: 'bg-late-100' },
{ title: 'Team Present Today', value: '18 / 20', icon: Users, color: 'text-present-700', bg: 'bg-present-100' },
{ title: 'Team On Leave', value: '2', icon: Plane, color: 'text-absent-700', bg: 'bg-absent-100' },
];
const recentApprovals = [
{ id: 'LA001', name: 'Alice Williams', type: 'Casual Leave', days: 2 },
{ id: 'LA002', name: 'Bob Smith', type: 'Sick Leave', days: 1 },
{ id: 'LA004', name: 'Diana Prince', type: 'Casual Leave', days: 3 },
];
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="bg-app-card p-6 rounded-lg shadow-sm"> <div className="bg-app-card p-6 rounded-lg shadow-sm">
<h2 className="text-2xl font-bold text-text-primary">Welcome back, Manager!</h2> <h2 className="text-2xl font-bold text-text-primary">Welcome back, Manager!</h2>
<p className="text-text-muted mt-1">Here is your team overview for today.</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{stats.map((stat, index) => (
<div key={index} className="bg-app-card p-6 rounded-lg shadow-sm flex items-center justify-between transition-transform hover:scale-[1.02]">
<div>
<p className="text-sm text-text-muted font-medium">{stat.title}</p>
<p className={`text-2xl font-bold mt-2 ${stat.color}`}>{stat.value}</p>
</div>
<div className={`p-3 rounded-full ${stat.bg}`}>
<stat.icon className={stat.color} size={28} />
</div>
</div>
))}
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-app-card p-6 rounded-lg shadow-sm">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-semibold text-text-primary">Pending Leave Requests</h3>
<Link to="/approval" className="text-primary text-sm font-medium hover:text-primary-hover flex items-center">
View All <ArrowRight size={14} className="ml-1" />
</Link>
</div>
<div className="space-y-3">
{recentApprovals.map((req) => (
<div key={req.id} className="flex items-center justify-between p-3 border border-app-border rounded-lg">
<div>
<p className="text-sm font-medium text-text-primary">{req.name}</p>
<p className="text-xs text-text-muted">{req.type} &bull; {req.days} Day(s)</p>
</div>
<Link to="/approval" className="px-3 py-1 bg-primary/10 text-primary text-xs font-medium rounded-full">Review</Link>
</div>
))}
</div>
</div>
</div> </div>
</div> </div>
); );

View File

@ -1,7 +1,6 @@
// src/features/leave/api/useLeaveData.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { apiClient, fetchMockData } from '../../../api/client'; import { apiClient } from '../../../api/client';
import type { RootState } from '../../../store/store'; import type { RootState } from '../../../store/store';
import type { import type {
LeaveSummary, LeaveSummary,
@ -12,15 +11,9 @@ import type {
LeaveApplicationSummary, LeaveApplicationSummary,
LeaveBalanceRecord LeaveBalanceRecord
} from '../types/leave'; } from '../types/leave';
// Add this import at the top of the file
import { useEmployeeById } from '../../masterData/api/useEmployeeData'; import { useEmployeeById } from '../../masterData/api/useEmployeeData';
// ==========================================
// 1. ADMIN SETUP & CONFIGURATION
// ==========================================
// Create Leave Type
export const useCreateLeaveType = () => { export const useCreateLeaveType = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
@ -34,8 +27,8 @@ export const useCreateLeaveType = () => {
}); });
}; };
// Get Leave Types
export const useLeaveTypes = (companyId: number = 1) => { export const useLeaveTypes = (companyId: number = 1) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return useQuery<any[]>({ return useQuery<any[]>({
queryKey: ['leaveTypes', companyId], queryKey: ['leaveTypes', companyId],
queryFn: async () => { queryFn: async () => {
@ -45,10 +38,11 @@ export const useLeaveTypes = (companyId: number = 1) => {
}); });
}; };
// Create Policy Rule
export const useCreatePolicyRule = () => { export const useCreatePolicyRule = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
mutationFn: async (data: any) => { mutationFn: async (data: any) => {
const response = await apiClient.post('/api/lms/config/rules', data); const response = await apiClient.post('/api/lms/config/rules', data);
return response.data; return response.data;
@ -59,10 +53,11 @@ export const useCreatePolicyRule = () => {
}); });
}; };
// Create Company Holiday (Bulk)
export const useCreateCompanyHoliday = () => { export const useCreateCompanyHoliday = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
mutationFn: async (holidays: any[]) => { mutationFn: async (holidays: any[]) => {
const response = await apiClient.post('/api/lms/config/holidays', holidays); const response = await apiClient.post('/api/lms/config/holidays', holidays);
return response.data; return response.data;
@ -73,7 +68,6 @@ export const useCreateCompanyHoliday = () => {
}); });
}; };
// Update Work Settings
export const useUpdateWorkSettings = () => { export const useUpdateWorkSettings = () => {
return useMutation({ return useMutation({
mutationFn: async (data: { company_id: number; branch_id: number; weekly_off_days: number[]; effective_from: string }) => { mutationFn: async (data: { company_id: number; branch_id: number; weekly_off_days: number[]; effective_from: string }) => {
@ -83,12 +77,6 @@ export const useUpdateWorkSettings = () => {
}); });
}; };
// ==========================================
// 2. EMPLOYEE ACTIONS
// ==========================================
// Get Leave Balances (Summary)
export const useLeaveSummary = () => { export const useLeaveSummary = () => {
return useQuery<LeaveSummary[]>({ return useQuery<LeaveSummary[]>({
queryKey: ['leaveSummary'], queryKey: ['leaveSummary'],
@ -97,10 +85,9 @@ export const useLeaveSummary = () => {
params: { year: new Date().getFullYear() } params: { year: new Date().getFullYear() }
}); });
// Map backend response to frontend LeaveSummary interface // eslint-disable-next-line @typescript-eslint/no-explicit-any
// Inside useLeaveSummary
return (response.data.balances || []).map((b: any) => ({ return (response.data.balances || []).map((b: any) => ({
leaveTypeId: b.leaveTypeId, // Ensure this is mapped! leaveTypeId: b.leaveTypeId,
leaveType: b.leaveTypeName, leaveType: b.leaveTypeName,
credited: b.grantedDays, credited: b.grantedDays,
utilized: b.usedDays, utilized: b.usedDays,
@ -111,10 +98,6 @@ export const useLeaveSummary = () => {
}); });
}; };
// ... (keep other hooks)
// Apply for Leave
export const useApplyLeave = () => { export const useApplyLeave = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const employeeId = useSelector((state: RootState) => Number(state.role.mockUserId)); const employeeId = useSelector((state: RootState) => Number(state.role.mockUserId));
@ -128,12 +111,11 @@ export const useApplyLeave = () => {
throw new Error("User profile is still loading. Please try again in a moment."); throw new Error("User profile is still loading. Please try again in a moment.");
} }
// All values are now fully dynamic based on the user's real data
const reqPayload = { const reqPayload = {
employee_id: employeeId, employee_id: employeeId,
leave_type_id: payload.leaveTypeId, leave_type_id: payload.leaveTypeId,
company_id: empDetail.companyId, // Dynamic from EMS company_id: empDetail.companyId,
branch_id: empDetail.branchId, // Dynamic from EMS branch_id: empDetail.branchId,
date_from: payload.fromDate, date_from: payload.fromDate,
date_to: payload.toDate, date_to: payload.toDate,
is_half_day: payload.leaveDays === 0.5, is_half_day: payload.leaveDays === 0.5,
@ -149,9 +131,6 @@ export const useApplyLeave = () => {
}); });
}; };
// ... (keep the rest of the file)
// Get Leave History
export const useLeaveHistory = () => { export const useLeaveHistory = () => {
return useQuery<LeaveHistoryRecord[]>({ return useQuery<LeaveHistoryRecord[]>({
queryKey: ['leaveHistory'], queryKey: ['leaveHistory'],
@ -160,11 +139,11 @@ export const useLeaveHistory = () => {
params: { year: new Date().getFullYear() } params: { year: new Date().getFullYear() }
}); });
// Map backend response to frontend LeaveHistoryRecord interface // eslint-disable-next-line @typescript-eslint/no-explicit-any
return (response.data.history || []).map((h: any) => ({ return (response.data.history || []).map((h: any) => ({
id: String(h.applicationId), id: String(h.applicationId),
leaveType: h.leaveTypeName, leaveType: h.leaveTypeName,
applicationDate: new Date().toISOString().split('T')[0], // Backend doesn't return applicationDate yet applicationDate: new Date().toISOString().split('T')[0],
fromDate: h.dateFrom, fromDate: h.dateFrom,
toDate: h.dateTo, toDate: h.dateTo,
leaveDays: h.numberOfDays, leaveDays: h.numberOfDays,
@ -175,8 +154,9 @@ export const useLeaveHistory = () => {
}); });
}; };
// Get Valid Optional Holidays
export const useValidOptionalHolidays = () => { export const useValidOptionalHolidays = () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return useQuery<any[]>({ return useQuery<any[]>({
queryKey: ['optionalHolidays'], queryKey: ['optionalHolidays'],
queryFn: async () => { queryFn: async () => {
@ -187,18 +167,13 @@ export const useValidOptionalHolidays = () => {
}; };
// ==========================================
// 3. MANAGER ACTIONS
// ==========================================
// Get Pending Leaves for Team (Generic for Manager, HR, Director)
export const usePendingApprovals = () => { export const usePendingApprovals = () => {
return useQuery<ManagerLeaveApproval[]>({ return useQuery<ManagerLeaveApproval[]>({
queryKey: ['pendingApprovals'], queryKey: ['pendingApprovals'],
queryFn: async () => { queryFn: async () => {
const response = await apiClient.get('/api/lms/manager/pending'); const response = await apiClient.get('/api/lms/manager/pending');
// Map backend response to frontend ManagerLeaveApproval interface // eslint-disable-next-line @typescript-eslint/no-explicit-any
return (response.data.data || []).map((p: any) => ({ return (response.data.data || []).map((p: any) => ({
id: String(p.applicationId), id: String(p.applicationId),
employeeId: p.employeeCode || String(p.employeeId), employeeId: p.employeeCode || String(p.employeeId),
@ -215,12 +190,11 @@ export const usePendingApprovals = () => {
}); });
}; };
// Aliases for role-specific hooks to keep existing components working
export const useManagerApprovals = usePendingApprovals; export const useManagerApprovals = usePendingApprovals;
export const useHRManagerApprovals = usePendingApprovals; export const useHRManagerApprovals = usePendingApprovals;
export const useDirectorApprovals = usePendingApprovals; export const useDirectorApprovals = usePendingApprovals;
// Approve / Reject Leave
export const useUpdateLeaveApproval = () => { export const useUpdateLeaveApproval = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
@ -237,10 +211,6 @@ export const useUpdateLeaveApproval = () => {
}; };
// ==========================================
// 4. REPORTS & MOCK FALLBACKS
// ==========================================
// Get Ledger Report (OB-CB) // Get Ledger Report (OB-CB)
export const useLeaveBalance = (params: { month: number; year: number; companyId: number }, enabled: boolean) => { export const useLeaveBalance = (params: { month: number; year: number; companyId: number }, enabled: boolean) => {
return useQuery<LeaveBalanceRecord[]>({ return useQuery<LeaveBalanceRecord[]>({
@ -253,7 +223,7 @@ export const useLeaveBalance = (params: { month: number; year: number; companyId
const rawData = response.data.data || []; const rawData = response.data.data || [];
const employeeMap: Record<string, LeaveBalanceRecord> = {}; const employeeMap: Record<string, LeaveBalanceRecord> = {};
// Group raw data by employeeId // eslint-disable-next-line @typescript-eslint/no-explicit-any
rawData.forEach((item: any) => { rawData.forEach((item: any) => {
if (!employeeMap[item.employeeId]) { if (!employeeMap[item.employeeId]) {
employeeMap[item.employeeId] = { employeeMap[item.employeeId] = {
@ -282,26 +252,26 @@ export const useLeaveBalance = (params: { month: number; year: number; companyId
}); });
}; };
// Get Leave Applications (Real API for HR/Director)
export const useLeaveApplications = () => { export const useLeaveApplications = () => {
return useQuery<LeaveApplicationSummary[]>({ return useQuery<LeaveApplicationSummary[]>({
queryKey: ['leaveApplications'], queryKey: ['leaveApplications'],
queryFn: async () => { queryFn: async () => {
const response = await apiClient.get('/api/lms/hr/applications'); const response = await apiClient.get('/api/lms/hr/applications');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (response.data.data || []).map((p: any) => ({ return (response.data.data || []).map((p: any) => ({
id: String(p.applicationId), id: String(p.applicationId),
employeeId: p.employeeCode, employeeId: p.employeeCode,
employeeName: `${p.firstName} ${p.lastName}`.trim(), employeeName: `${p.firstName} ${p.lastName}`.trim(),
jobRole: p.jobTitle, // Mapped from jobTitle jobRole: p.jobTitle,
department: p.departmentName, department: p.departmentName,
managerName: p.managerName, // Mapped from backend managerName: p.managerName,
leaveType: p.leaveType, leaveType: p.leaveType,
fromDate: p.dateFrom, fromDate: p.dateFrom,
toDate: p.dateTo, toDate: p.dateTo,
leaveDays: p.numberOfDays, leaveDays: p.numberOfDays,
status: p.status, status: p.status,
reason: p.reason, // Mapped from backend reason: p.reason,
})); }));
}, },
}); });

View File

@ -21,7 +21,7 @@ export default function ApprovalModal({ record, onClose, onAction }: ApprovalMod
return new Date(dateStr + 'T00:00:00').toLocaleDateString('en-US', { day: '2-digit', month: 'short', year: 'numeric' }); return new Date(dateStr + 'T00:00:00').toLocaleDateString('en-US', { day: '2-digit', month: 'short', year: 'numeric' });
}; };
// Check if the status is pending (case-insensitive) // Check if the status is pending
const isPending = record.status.toUpperCase().includes('PENDING'); const isPending = record.status.toUpperCase().includes('PENDING');
return ( return (
@ -30,10 +30,13 @@ export default function ApprovalModal({ record, onClose, onAction }: ApprovalMod
<button onClick={onClose} className="absolute top-4 right-4 text-text-muted hover:text-text-primary"> <button onClick={onClose} className="absolute top-4 right-4 text-text-muted hover:text-text-primary">
<X size={24} /> <X size={24} />
</button> </button>
<h3 className="text-xl font-bold text-text-primary mb-1">Leave Application Details</h3> <h3 className="text-xl font-bold text-text-primary mb-6">Leave Application Details</h3>
<p className="text-sm text-text-muted mb-6">Application ID: {record.id}</p>
<div className="space-y-4"> <div className="space-y-4">
<div className="flex justify-between border-b border-app-border pb-2">
<span className="text-sm text-text-muted">Application ID</span>
<span className="text-sm font-medium text-text-primary">{record.id}</span>
</div>
<div className="flex justify-between border-b border-app-border pb-2"> <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 text-text-muted">Employee ID</span>
<span className="text-sm font-medium text-text-primary">{record.employeeId}</span> <span className="text-sm font-medium text-text-primary">{record.employeeId}</span>

View File

@ -1,7 +1,7 @@
import { useState, useMemo } from 'react'; import { useState, useMemo } from 'react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useApplyLeave } from '../../api/useLeaveData'; import { useApplyLeave } from '../../../api/useLeaveData';
import type { LeaveSummary, LeaveRequestPayload, DayMode } from '../../types/leave'; import type { LeaveSummary, LeaveRequestPayload, DayMode } from '../../../types/leave';
interface ApplyLeaveFormProps { interface ApplyLeaveFormProps {
summaryData: LeaveSummary[]; summaryData: LeaveSummary[];
@ -33,7 +33,7 @@ const calculateResumptionDate = (to: string) => {
export default function ApplyLeaveForm({ summaryData }: ApplyLeaveFormProps) { export default function ApplyLeaveForm({ summaryData }: ApplyLeaveFormProps) {
const { mutate, isPending } = useApplyLeave(); const { mutate, isPending } = useApplyLeave();
const [showForm, setShowForm] = useState(false); const [showForm, setShowForm] = useState(false);
// Track both ID (for API) and Name (for UI display) // Track both ID (for API) and Name (for UI display)
const [selectedLeaveTypeId, setSelectedLeaveTypeId] = useState<number | ''>(''); const [selectedLeaveTypeId, setSelectedLeaveTypeId] = useState<number | ''>('');
const [selectedLeaveTypeName, setSelectedLeaveTypeName] = useState(''); const [selectedLeaveTypeName, setSelectedLeaveTypeName] = useState('');
@ -77,14 +77,14 @@ export default function ApplyLeaveForm({ summaryData }: ApplyLeaveFormProps) {
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<label className="block text-sm font-medium text-text-secondary mb-1">Leave Type</label> <label className="block text-sm font-medium text-text-secondary mb-1">Leave Type</label>
<select <select
value={selectedLeaveTypeId} value={selectedLeaveTypeId}
onChange={(e) => { onChange={(e) => {
const id = Number(e.target.value); const id = Number(e.target.value);
setSelectedLeaveTypeId(id); setSelectedLeaveTypeId(id);
const selected = summaryData.find(l => l.leaveTypeId === id); const selected = summaryData.find(l => l.leaveTypeId === id);
setSelectedLeaveTypeName(selected?.leaveType || ''); setSelectedLeaveTypeName(selected?.leaveType || '');
}} }}
className="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" className="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"
> >
<option value="">Select Leave Type</option> <option value="">Select Leave Type</option>

View File

@ -43,8 +43,8 @@ export default function LeaveApplicationModal({ record, onClose }: LeaveApplicat
<span className="text-sm font-medium text-text-primary">{record.employeeName}</span> <span className="text-sm font-medium text-text-primary">{record.employeeName}</span>
</div> </div>
<div className="flex justify-between border-b border-app-border pb-2"> <div className="flex justify-between border-b border-app-border pb-2">
<span className="text-sm text-text-muted">Job Role</span> {/* Changed */} <span className="text-sm text-text-muted">Job Role</span>
<span className="text-sm font-medium text-text-primary">{record.jobRole}</span> {/* Changed */} <span className="text-sm font-medium text-text-primary">{record.jobRole}</span>
</div> </div>
<div className="flex justify-between border-b border-app-border pb-2"> <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 text-text-muted">Department</span>
@ -69,7 +69,6 @@ export default function LeaveApplicationModal({ record, onClose }: LeaveApplicat
<span className="text-sm text-text-muted">Status</span> <span className="text-sm text-text-muted">Status</span>
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusClass(record.status)}`}>{record.status}</span> <span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusClass(record.status)}`}>{record.status}</span>
</div> </div>
{/* Added Reason Field */}
<div className="border-b border-app-border pb-2"> <div className="border-b border-app-border pb-2">
<span className="text-sm text-text-muted block mb-1">Reason</span> <span className="text-sm text-text-muted block mb-1">Reason</span>
<p className="text-sm text-text-secondary bg-app-muted p-3 rounded-lg">{record.reason || 'No reason provided.'}</p> <p className="text-sm text-text-secondary bg-app-muted p-3 rounded-lg">{record.reason || 'No reason provided.'}</p>

View File

@ -7,8 +7,6 @@ interface ViewLeaveModalProps {
onWithdraw: (record: LeaveHistoryRecord) => void; onWithdraw: (record: LeaveHistoryRecord) => void;
} }
// Inside ViewLeaveModal.tsx
const getStatusClass = (status: string) => { const getStatusClass = (status: string) => {
const normalizedStatus = status.toUpperCase(); const normalizedStatus = status.toUpperCase();
if (normalizedStatus.includes('APPROVED')) return 'bg-present-100 text-present-700'; if (normalizedStatus.includes('APPROVED')) return 'bg-present-100 text-present-700';
@ -25,10 +23,13 @@ export default function ViewLeaveModal({ record, onClose, onWithdraw }: ViewLeav
<button onClick={onClose} className="absolute top-4 right-4 text-text-muted hover:text-text-primary"> <button onClick={onClose} className="absolute top-4 right-4 text-text-muted hover:text-text-primary">
<X size={24} /> <X size={24} />
</button> </button>
<h3 className="text-xl font-bold text-text-primary mb-1">Leave Details</h3> <h3 className="text-xl font-bold text-text-primary mb-6">Leave Details</h3>
<p className="text-sm text-text-muted mb-6">Application ID: {record.id}</p>
<div className="space-y-4"> <div className="space-y-4">
<div className="flex justify-between border-b border-app-border pb-2">
<span className="text-sm text-text-muted">Application ID</span>
<span className="text-sm font-medium text-text-primary">{record.id}</span>
</div>
<div className="flex justify-between border-b border-app-border pb-2"> <div className="flex justify-between border-b border-app-border pb-2">
<span className="text-sm text-text-muted">Leave Type</span> <span className="text-sm text-text-muted">Leave Type</span>
<span className="text-sm font-medium text-text-primary">{record.leaveType}</span> <span className="text-sm font-medium text-text-primary">{record.leaveType}</span>
@ -52,7 +53,6 @@ export default function ViewLeaveModal({ record, onClose, onWithdraw }: ViewLeav
</div> </div>
<div className="flex justify-end space-x-3 mt-6"> <div className="flex justify-end space-x-3 mt-6">
{/* Update this condition to check uppercase PENDING */}
{record.status.toUpperCase() === 'PENDING' && ( {record.status.toUpperCase() === 'PENDING' && (
<button onClick={() => onWithdraw(record)} className="px-4 py-2 bg-absent-500 text-white rounded-lg hover:bg-absent-600 transition-colors text-sm font-medium"> <button onClick={() => onWithdraw(record)} className="px-4 py-2 bg-absent-500 text-white rounded-lg hover:bg-absent-600 transition-colors text-sm font-medium">
Withdraw Application Withdraw Application

View File

@ -7,7 +7,6 @@ interface LeaveApplicationsTableProps {
records: LeaveApplicationSummary[]; records: LeaveApplicationSummary[];
} }
// Updated to use jobRole instead of designation
type SortKey = 'id' | 'employeeId' | 'employeeName' | 'jobRole' | 'department' | 'managerName' | 'leaveType' | 'leaveDays' | 'status'; type SortKey = 'id' | 'employeeId' | 'employeeName' | 'jobRole' | 'department' | 'managerName' | 'leaveType' | 'leaveDays' | 'status';
type SortDirection = 'ascending' | 'descending'; type SortDirection = 'ascending' | 'descending';
@ -36,7 +35,7 @@ export default function LeaveApplicationsTable({ records }: LeaveApplicationsTab
record.id.toLowerCase().includes(searchQuery.toLowerCase()) || record.id.toLowerCase().includes(searchQuery.toLowerCase()) ||
record.employeeId.toLowerCase().includes(searchQuery.toLowerCase()) || record.employeeId.toLowerCase().includes(searchQuery.toLowerCase()) ||
record.employeeName.toLowerCase().includes(searchQuery.toLowerCase()) || record.employeeName.toLowerCase().includes(searchQuery.toLowerCase()) ||
record.jobRole.toLowerCase().includes(searchQuery.toLowerCase()) || // Changed record.jobRole.toLowerCase().includes(searchQuery.toLowerCase()) ||
record.department.toLowerCase().includes(searchQuery.toLowerCase()) || record.department.toLowerCase().includes(searchQuery.toLowerCase()) ||
record.managerName.toLowerCase().includes(searchQuery.toLowerCase()) || record.managerName.toLowerCase().includes(searchQuery.toLowerCase()) ||
record.leaveType.toLowerCase().includes(searchQuery.toLowerCase()) || record.leaveType.toLowerCase().includes(searchQuery.toLowerCase()) ||
@ -97,7 +96,7 @@ export default function LeaveApplicationsTable({ records }: LeaveApplicationsTab
if (key === 'id') return 'App ID'; if (key === 'id') return 'App ID';
if (key === 'employeeId') return 'Emp CODE'; if (key === 'employeeId') return 'Emp CODE';
if (key === 'employeeName') return 'Employee Name'; if (key === 'employeeName') return 'Employee Name';
if (key === 'jobRole') return 'Job Role'; // Changed if (key === 'jobRole') return 'Job Role';
if (key === 'managerName') return 'Manager Name'; if (key === 'managerName') return 'Manager Name';
if (key === 'leaveType') return 'Leave Type'; if (key === 'leaveType') return 'Leave Type';
if (key === 'leaveDays') return 'Leave Days'; if (key === 'leaveDays') return 'Leave Days';
@ -141,7 +140,7 @@ export default function LeaveApplicationsTable({ records }: LeaveApplicationsTab
<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-primary">{record.id}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.employeeId}</td> <td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.employeeId}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.employeeName}</td> <td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.employeeName}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.jobRole}</td> {/* Changed */} <td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.jobRole}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.department}</td> <td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.department}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.managerName}</td> <td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.managerName}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.leaveType}</td> <td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.leaveType}</td>

View File

@ -44,7 +44,7 @@ export default function LeaveHistoryTable({ records }: LeaveHistoryTableProps) {
// If a filter date is selected, check if the leave period overlaps with the selected range // If a filter date is selected, check if the leave period overlaps with the selected range
const matchesFrom = fromDate ? record.toDate >= fromDate : true; const matchesFrom = fromDate ? record.toDate >= fromDate : true;
const matchesTo = toDate ? record.fromDate <= toDate : true; const matchesTo = toDate ? record.fromDate <= toDate : true;
return matchesFrom && matchesTo; return matchesFrom && matchesTo;
}); });
}, [combinedRecords, fromDate, toDate]); }, [combinedRecords, fromDate, toDate]);

View File

@ -11,7 +11,7 @@ import { useCompanies } from '../../masterData/api/useCompanyData';
export default function BalanceReport() { export default function BalanceReport() {
const today = new Date(); const today = new Date();
const [filters, setFilters] = useState<BalanceFilterValues>({ const [filters, setFilters] = useState<BalanceFilterValues>({
companyId: '1', // Default to company 1 companyId: '1',
month: today.getMonth() + 1, month: today.getMonth() + 1,
year: today.getFullYear(), year: today.getFullYear(),
}); });

View File

@ -1,13 +1,12 @@
// src/features/leave/pages/leave/MyLeave.tsx
import Loader from '../../../components/ui/Loader'; import Loader from '../../../components/ui/Loader';
import ErrorState from '../../../components/ui/ErrorState'; import ErrorState from '../../../components/ui/ErrorState';
import LeaveSummaryTable from '../components/leave/table/LeaveSummaryTable'; import LeaveSummaryTable from '../components/leave/table/LeaveSummaryTable';
import ApplyLeaveForm from '../components/leave/ApplyLeaveForm'; import ApplyLeaveForm from '../components/leave/form/ApplyLeaveForm';
import LeaveHistoryTable from '../components/leave/table/LeaveHistoryTable'; import LeaveHistoryTable from '../components/leave/table/LeaveHistoryTable';
import { useLeaveSummary, useLeaveHistory } from '../api/useLeaveData'; import { useLeaveSummary, useLeaveHistory } from '../api/useLeaveData';
export default function MyLeave() { export default function MyLeave() {
// The hooks automatically use the Mock SSO headers injected by the Axios interceptor! // The hooks automatically use the Mock SSO headers injected by the Axios interceptor
const { data: summaryData, isLoading: summaryLoading, isError: summaryError } = useLeaveSummary(); const { data: summaryData, isLoading: summaryLoading, isError: summaryError } = useLeaveSummary();
const { data: historyData, isLoading: historyLoading, isError: historyError } = useLeaveHistory(); const { data: historyData, isLoading: historyLoading, isError: historyError } = useLeaveHistory();

View File

@ -56,7 +56,7 @@ export interface LeaveApplicationSummary {
id: string; id: string;
employeeId: string; employeeId: string;
employeeName: string; employeeName: string;
jobRole: string; // Changed from designation jobRole: string;
department: string; department: string;
managerName: string; managerName: string;
leaveType: string; leaveType: string;
@ -64,7 +64,7 @@ export interface LeaveApplicationSummary {
toDate: string; toDate: string;
leaveDays: number; leaveDays: number;
status: string; status: string;
reason: string; // Added reason reason: string;
} }
export interface LeaveBalanceRecord { export interface LeaveBalanceRecord {

View File

@ -1,18 +1,8 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiClient } from '../../../api/client'; import { apiClient } from '../../../api/client';
import { toast } from 'sonner'; import { toast } from 'sonner';
import type { Branch } from '../types/masterData';
export interface Branch {
id: number;
code: string;
name: string;
companyId: number | null;
companyCode: string;
companyName: string;
status: 'Active' | 'Disabled';
}
// 1. GET: Fetch all branches
export const useBranches = () => { export const useBranches = () => {
return useQuery<Branch[]>({ return useQuery<Branch[]>({
queryKey: ['branches'], queryKey: ['branches'],
@ -21,19 +11,18 @@ export const useBranches = () => {
const rawData = response.data.data || []; const rawData = response.data.data || [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
return rawData.map((b: any) => ({ return rawData.map((b: any) => ({
id: b.branchId, id: b.branchId,
code: b.branchCode || b.code, code: b.branchCode || b.code,
name: b.branchName, name: b.branchName,
companyId: b.companyId || null, companyId: b.companyId || null,
companyCode: b.companyCode || '', companyCode: b.companyCode || '',
companyName: b.companyName || '', companyName: b.companyName || '',
status: b.isActive ? 'Active' : 'Disabled', status: b.isActive ? 'Active' : 'Disabled',
})); }));
}, },
}); });
}; };
// 2. POST: Create a branch
export const useCreateBranch = () => { export const useCreateBranch = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
@ -53,7 +42,6 @@ export const useCreateBranch = () => {
}); });
}; };
// 3. PUT: Update a branch
export const useUpdateBranch = () => { export const useUpdateBranch = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
@ -61,7 +49,7 @@ export const useUpdateBranch = () => {
const payload = { const payload = {
branchName: name, branchName: name,
companyId: companyId, companyId: companyId,
code: code, code: code,
isActive: status === 'Active', isActive: status === 'Active',
}; };
const response = await apiClient.put(`/api/ems/branches/${id}`, payload); const response = await apiClient.put(`/api/ems/branches/${id}`, payload);
@ -74,7 +62,6 @@ export const useUpdateBranch = () => {
}); });
}; };
// 4. DELETE: Delete a branch
export const useDeleteBranch = () => { export const useDeleteBranch = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({

View File

@ -1,22 +1,16 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiClient } from '../../../api/client'; import { apiClient } from '../../../api/client';
import { toast } from 'sonner'; import { toast } from 'sonner';
import type { Company } from '../types/masterData';
export interface Company {
id: number;
code: string;
name: string;
status: 'Active' | 'Disabled';
}
// 1. GET: Fetch all companies
export const useCompanies = () => { export const useCompanies = () => {
return useQuery<Company[]>({ return useQuery<Company[]>({
queryKey: ['companies'], queryKey: ['companies'],
queryFn: async () => { queryFn: async () => {
const response = await apiClient.get('/api/ems/companies'); const response = await apiClient.get('/api/ems/companies');
const rawData = response.data.data || []; const rawData = response.data.data || [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return rawData.map((c: any) => ({ return rawData.map((c: any) => ({
id: c.companyId, id: c.companyId,
code: c.companyCode, code: c.companyCode,
@ -27,7 +21,6 @@ export const useCompanies = () => {
}); });
}; };
// 2. POST: Create a company
export const useCreateCompany = () => { export const useCreateCompany = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
@ -44,13 +37,14 @@ export const useCreateCompany = () => {
queryClient.invalidateQueries({ queryKey: ['companies'] }); queryClient.invalidateQueries({ queryKey: ['companies'] });
toast.success('Company created successfully'); toast.success('Company created successfully');
}, },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onError: (error: any) => { onError: (error: any) => {
toast.error(error?.response?.data?.message || 'Failed to create company'); toast.error(error?.response?.data?.message || 'Failed to create company');
} }
}); });
}; };
// 3. PUT: Update a company
export const useUpdateCompany = () => { export const useUpdateCompany = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
@ -70,7 +64,6 @@ export const useUpdateCompany = () => {
}); });
}; };
// 4. DELETE: Delete a company
export const useDeleteCompany = () => { export const useDeleteCompany = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({

View File

@ -1,19 +1,8 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiClient } from '../../../api/client'; import { apiClient } from '../../../api/client';
import { toast } from 'sonner'; import { toast } from 'sonner';
import type { Department } from '../types/masterData';
export interface Department {
id: number;
code: string;
name: string;
branchId: number | null;
branchName: string;
companyId: number | null;
companyName: string;
status: 'Active' | 'Disabled';
}
// 1. GET: Fetch all departments
export const useDepartments = () => { export const useDepartments = () => {
return useQuery<Department[]>({ return useQuery<Department[]>({
queryKey: ['departments'], queryKey: ['departments'],
@ -35,7 +24,6 @@ export const useDepartments = () => {
}); });
}; };
// 2. POST: Create a department
export const useCreateDepartment = () => { export const useCreateDepartment = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
@ -58,7 +46,6 @@ export const useCreateDepartment = () => {
}); });
}; };
// 3. PUT: Update a department
export const useUpdateDepartment = () => { export const useUpdateDepartment = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
@ -81,7 +68,6 @@ export const useUpdateDepartment = () => {
}); });
}; };
// 4. DELETE: Delete a department
export const useDeleteDepartment = () => { export const useDeleteDepartment = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({

View File

@ -1,69 +1,8 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiClient } from '../../../api/client'; import { apiClient } from '../../../api/client';
import { toast } from 'sonner'; import { toast } from 'sonner';
import type { Employee, EmployeeDetail, Manager } from '../types/masterData';
export interface Employee {
id: number;
code: string;
firstName: string;
lastName: string;
email: string;
jobId: number | null;
jobName: string;
deptId: number | null;
deptName: string;
branchId: number | null;
branchName: string;
companyId: number | null;
companyName: string;
status: 'Active' | 'Disabled';
}
export interface EmployeeDetail {
employeeId: number;
employeeCode: string;
isActive: boolean;
firstName: string;
lastName: string;
dob: string;
gender: string;
personalEmail: string;
personalPhone: string;
workEmail: string;
dateJoining: string;
probationDays: number;
contractStatus: string;
salaryStructureId: number;
department: string;
departmentId: number;
jobName: string;
jobId: number;
managerFirstName: string;
managerLastName: string;
managerEmployeeCode: string;
addresses: Array<{
type: string;
doorNumber: string;
landmark: string;
line: string;
pincode: string;
district: string;
state: string;
}>;
companyName: string;
companyId: number;
branchName: string;
branchId: number;
}
export interface Manager {
id: number;
code: string;
firstName: string;
lastName: string;
}
// 1. GET: Fetch all employees (List view)
export const useEmployees = () => { export const useEmployees = () => {
return useQuery<Employee[]>({ return useQuery<Employee[]>({
queryKey: ['employees'], queryKey: ['employees'],
@ -91,7 +30,6 @@ export const useEmployees = () => {
}); });
}; };
// 1.5. GET: Fetch single employee by ID
export const useEmployeeById = (id: number | null) => { export const useEmployeeById = (id: number | null) => {
return useQuery<EmployeeDetail>({ return useQuery<EmployeeDetail>({
queryKey: ['employee', id], queryKey: ['employee', id],
@ -99,11 +37,10 @@ export const useEmployeeById = (id: number | null) => {
const response = await apiClient.get(`/api/ems/employees/${id}`); const response = await apiClient.get(`/api/ems/employees/${id}`);
return response.data.data as EmployeeDetail; return response.data.data as EmployeeDetail;
}, },
enabled: !!id, enabled: !!id,
}); });
}; };
// 1.6. GET: Fetch Managers by Department ID
export const useManagers = (deptId: number | null) => { export const useManagers = (deptId: number | null) => {
return useQuery<Manager[]>({ return useQuery<Manager[]>({
queryKey: ['managers', deptId], queryKey: ['managers', deptId],
@ -123,10 +60,10 @@ export const useManagers = (deptId: number | null) => {
}); });
}; };
// 2. POST: Create an employee
export const useCreateEmployee = () => { export const useCreateEmployee = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
mutationFn: async (data: any) => { mutationFn: async (data: any) => {
const payload = { const payload = {
firstName: data.firstName, firstName: data.firstName,
@ -135,7 +72,7 @@ export const useCreateEmployee = () => {
gender: data.gender, gender: data.gender,
personalEmail: data.personalEmail, personalEmail: data.personalEmail,
personalPhone: data.personalPhone, personalPhone: data.personalPhone,
address: data.address, address: data.address,
companyId: data.companyId, companyId: data.companyId,
branchId: data.branchId, branchId: data.branchId,
departmentId: data.deptId, departmentId: data.deptId,
@ -151,6 +88,7 @@ export const useCreateEmployee = () => {
try { try {
const response = await apiClient.post('/api/ems/employees', payload); const response = await apiClient.post('/api/ems/employees', payload);
return response.data; return response.data;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) { } catch (error: any) {
const backendError = error?.response?.data?.message || error?.response?.data?.error || "Unknown backend error"; const backendError = error?.response?.data?.message || error?.response?.data?.error || "Unknown backend error";
throw new Error(backendError, { cause: error }); throw new Error(backendError, { cause: error });
@ -163,12 +101,13 @@ export const useCreateEmployee = () => {
}); });
}; };
// 3. PUT: Update an employee profile data
export const useUpdateEmployee = () => { export const useUpdateEmployee = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
mutationFn: async (data: any) => { mutationFn: async (data: any) => {
// Removed isActive from here, status is handled by PATCH endpoint // status is handled by PATCH endpoint
const payload = { const payload = {
firstName: data.firstName, firstName: data.firstName,
lastName: data.lastName, lastName: data.lastName,
@ -176,7 +115,7 @@ export const useUpdateEmployee = () => {
gender: data.gender, gender: data.gender,
personalEmail: data.personalEmail, personalEmail: data.personalEmail,
personalPhone: data.personalPhone, personalPhone: data.personalPhone,
address: data.address, address: data.address,
companyId: data.companyId, companyId: data.companyId,
branchId: data.branchId, branchId: data.branchId,
departmentId: data.deptId, departmentId: data.deptId,
@ -189,6 +128,7 @@ export const useUpdateEmployee = () => {
try { try {
const response = await apiClient.put(`/api/ems/employees/${data.id}`, payload); const response = await apiClient.put(`/api/ems/employees/${data.id}`, payload);
return response.data; return response.data;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) { } catch (error: any) {
const backendError = error?.response?.data?.message || error?.response?.data?.error || "Unknown backend error"; const backendError = error?.response?.data?.message || error?.response?.data?.error || "Unknown backend error";
throw new Error(backendError, { cause: error }); throw new Error(backendError, { cause: error });
@ -203,7 +143,7 @@ export const useUpdateEmployee = () => {
}); });
}; };
// 4. PATCH: Update Employee Status (Active/Disabled)
export const useUpdateEmployeeStatus = () => { export const useUpdateEmployeeStatus = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
@ -211,6 +151,7 @@ export const useUpdateEmployeeStatus = () => {
try { try {
const response = await apiClient.patch(`/api/ems/employees/${id}/status`, { isActive }); const response = await apiClient.patch(`/api/ems/employees/${id}/status`, { isActive });
return response.data; return response.data;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) { } catch (error: any) {
const backendError = error?.response?.data?.message || error?.response?.data?.error || "Unknown backend error"; const backendError = error?.response?.data?.message || error?.response?.data?.error || "Unknown backend error";
throw new Error(backendError, { cause: error }); throw new Error(backendError, { cause: error });

View File

@ -1,10 +1,6 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { fetchMockData } from '../../../api/client'; import { fetchMockData } from '../../../api/client';
import type { Employee } from './useEmployeeData'; import type { HRManagerEmployee } from '../types/masterData';
export interface HRManagerEmployee extends Employee {
name: any;
isHRManager: boolean;
}
export const useHRManagerEmployees = () => { export const useHRManagerEmployees = () => {
return useQuery<HRManagerEmployee[]>({ return useQuery<HRManagerEmployee[]>({

View File

@ -1,22 +1,8 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiClient } from '../../../api/client'; import { apiClient } from '../../../api/client';
import { toast } from 'sonner'; import { toast } from 'sonner';
import type { JobRole } from '../types/masterData';
export interface JobRole {
id: number;
code: string;
name: string;
description: string;
deptId: number | null;
deptName: string;
branchId: number | null;
branchName: string;
companyId: number | null;
companyName: string;
status: 'Active' | 'Disabled';
}
// 1. GET: Fetch all job roles
export const useJobRoles = () => { export const useJobRoles = () => {
return useQuery<JobRole[]>({ return useQuery<JobRole[]>({
queryKey: ['jobRoles'], queryKey: ['jobRoles'],
@ -28,8 +14,8 @@ export const useJobRoles = () => {
id: d.jobId, id: d.jobId,
code: d.jobCode || '', code: d.jobCode || '',
name: d.jobName || '', name: d.jobName || '',
description: d.description || '', description: d.description || '',
deptId: d.departmentId || null, deptId: d.departmentId || null,
deptName: d.departmentName || '', deptName: d.departmentName || '',
branchId: d.branchId || null, branchId: d.branchId || null,
branchName: d.branchName || '', branchName: d.branchName || '',
@ -41,15 +27,14 @@ export const useJobRoles = () => {
}); });
}; };
// 2. POST: Create a job role
export const useCreateJobRole = () => { export const useCreateJobRole = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: async (data: { name: string; description: string; deptId: number; status: 'Active' | 'Disabled' }) => { mutationFn: async (data: { name: string; description: string; deptId: number; status: 'Active' | 'Disabled' }) => {
const payload = { const payload = {
departmentId: data.deptId, departmentId: data.deptId,
title: data.name, title: data.name,
description: data.description, description: data.description,
isActive: data.status === 'Active', isActive: data.status === 'Active',
}; };
const response = await apiClient.post('/api/ems/jobs', payload); const response = await apiClient.post('/api/ems/jobs', payload);
@ -62,7 +47,6 @@ export const useCreateJobRole = () => {
}); });
}; };
// 3. PUT: Update a job role
export const useUpdateJobRole = () => { export const useUpdateJobRole = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
@ -70,7 +54,7 @@ export const useUpdateJobRole = () => {
const payload = { const payload = {
departmentId: deptId, departmentId: deptId,
title: name, title: name,
description: description, description: description,
isActive: status === 'Active', isActive: status === 'Active',
}; };
const response = await apiClient.put(`/api/ems/jobs/${id}`, payload); const response = await apiClient.put(`/api/ems/jobs/${id}`, payload);
@ -83,7 +67,6 @@ export const useUpdateJobRole = () => {
}); });
}; };
// 4. DELETE: Delete a job role
export const useDeleteJobRole = () => { export const useDeleteJobRole = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({

View File

@ -1,13 +1,6 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { apiClient } from '../../../api/client'; import { apiClient } from '../../../api/client';
import type { MasterDataSummary } from '../types/masterData';
export interface MasterDataSummary {
totalCompanies: number;
totalBranches: number;
totalDepartments: number;
totalJobRoles: number;
totalEmployees: number;
}
export const useMasterDataSummary = () => { export const useMasterDataSummary = () => {
return useQuery<MasterDataSummary>({ return useQuery<MasterDataSummary>({
@ -15,7 +8,7 @@ export const useMasterDataSummary = () => {
queryFn: async () => { queryFn: async () => {
const response = await apiClient.get('/api/ems/dashboard/metrics'); const response = await apiClient.get('/api/ems/dashboard/metrics');
const d = response.data.data; const d = response.data.data;
return { return {
totalCompanies: d.total_companies ?? d.totalCompanies ?? 0, totalCompanies: d.total_companies ?? d.totalCompanies ?? 0,
totalBranches: d.total_branches ?? d.totalBranches ?? 0, totalBranches: d.total_branches ?? d.totalBranches ?? 0,

View File

@ -1,22 +1,10 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { fetchMockData } from '../../../api/client'; import { fetchMockData } from '../../../api/client';
import type { CompanyPolicy } from '../types/masterData';
export interface Policy {
id: string;
title: string;
category: string;
description: string;
}
export interface CompanyPolicy {
companyId: string;
companyName: string;
policies: Policy[];
}
export const usePolicies = () => { export const usePolicies = () => {
return useQuery<CompanyPolicy[]>({ return useQuery<CompanyPolicy[]>({
queryKey: ['policies'], queryKey: ['policies'],
queryFn: () => fetchMockData<CompanyPolicy[]>('master-data/policies.json'), queryFn: () => fetchMockData<CompanyPolicy[]>('policies.json'),
}); });
}; };

View File

@ -1,7 +1,6 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { X } from 'lucide-react'; import { X } from 'lucide-react';
import type { Branch } from '../../api/useBranchData'; import type { Branch, Company } from '../../types/masterData';
import type { Company } from '../../api/useCompanyData';
interface BranchFormModalProps { interface BranchFormModalProps {
branch: Branch | null; branch: Branch | null;
@ -20,7 +19,7 @@ export default function BranchFormModal({ branch, companies, onClose, onSave }:
// eslint-disable-next-line react-hooks/set-state-in-effect // eslint-disable-next-line react-hooks/set-state-in-effect
setName(branch.name); setName(branch.name);
setStatus(branch.status); setStatus(branch.status);
// If backend GET provided companyId, use it. Otherwise, find it by companyCode. // If backend GET provided companyId, use it. Otherwise, find it by companyCode.
if (branch.companyId) { if (branch.companyId) {
setCompanyId(String(branch.companyId)); setCompanyId(String(branch.companyId));

View File

@ -1,11 +1,11 @@
import { useState, useMemo } from 'react'; import { useState, useMemo } from 'react';
import { ArrowUpDown, ArrowUp, ArrowDown, Search, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Pencil, Trash2 } from 'lucide-react'; import { ArrowUpDown, ArrowUp, ArrowDown, Search, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Pencil, Trash2 } from 'lucide-react';
import type { Branch } from '../../api/useBranchData'; import type { Branch } from '../../types/masterData';
interface BranchTableProps { interface BranchTableProps {
records: Branch[]; records: Branch[];
onEdit: (branch: Branch) => void; onEdit: (branch: Branch) => void;
onDelete: (branch: Branch) => void; onDelete: (branch: Branch) => void;
} }
type SortKey = 'code' | 'name' | 'companyName' | 'status'; type SortKey = 'code' | 'name' | 'companyName' | 'status';

View File

@ -1,9 +1,9 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { X } from 'lucide-react'; import { X } from 'lucide-react';
import type { Company } from '../../api/useCompanyData'; import type { Company } from '../../types/masterData';
interface CompanyFormModalProps { interface CompanyFormModalProps {
company: Company | null; // null means Add mode, object means Edit mode company: Company | null;
onClose: () => void; onClose: () => void;
onSave: (name: string, status: 'Active' | 'Disabled') => void; onSave: (name: string, status: 'Active' | 'Disabled') => void;
} }

View File

@ -1,6 +1,6 @@
import { useState, useMemo } from 'react'; import { useState, useMemo } from 'react';
import { ArrowUpDown, ArrowUp, ArrowDown, Search, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Pencil, Trash2 } from 'lucide-react'; import { ArrowUpDown, ArrowUp, ArrowDown, Search, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Pencil, Trash2 } from 'lucide-react';
import type { Company } from '../../api/useCompanyData'; import type { Company } from '../../types/masterData';
interface CompanyTableProps { interface CompanyTableProps {
records: Company[]; records: Company[];

View File

@ -1,8 +1,6 @@
import { useState, useEffect, useMemo } from 'react'; import { useState, useEffect, useMemo } from 'react';
import { X } from 'lucide-react'; import { X } from 'lucide-react';
import type { Department } from '../../api/useDepartmentData'; import type { Department, Company, Branch } from '../../types/masterData';
import type { Company } from '../../api/useCompanyData';
import type { Branch } from '../../api/useBranchData';
interface DepartmentFormModalProps { interface DepartmentFormModalProps {
department: Department | null; department: Department | null;
@ -63,7 +61,7 @@ export default function DepartmentFormModal({ department, companies, branches, o
alert("Please select a company and branch"); alert("Please select a company and branch");
return; return;
} }
onSave({ onSave({
name, name,
companyId: Number(companyId), companyId: Number(companyId),

View File

@ -1,14 +1,14 @@
import { useState, useMemo } from 'react'; import { useState, useMemo } from 'react';
import { ArrowUpDown, ArrowUp, ArrowDown, Search, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Pencil, Trash2 } from 'lucide-react'; import { ArrowUpDown, ArrowUp, ArrowDown, Search, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Pencil, Trash2 } from 'lucide-react';
import type { Department } from '../../api/useDepartmentData'; import type { Department } from '../../types/masterData';
interface DepartmentTableProps { interface DepartmentTableProps {
records: Department[]; records: Department[];
onEdit: (department: Department) => void; onEdit: (department: Department) => void;
onDelete: (department: Department) => void; onDelete: (department: Department) => void;
} }
// Removed 'id', added 'code'
type SortKey = 'code' | 'name' | 'branchName' | 'companyName' | 'status'; type SortKey = 'code' | 'name' | 'branchName' | 'companyName' | 'status';
type SortDirection = 'ascending' | 'descending'; type SortDirection = 'ascending' | 'descending';

View File

@ -1,11 +1,8 @@
import { useState, useEffect, useMemo } from 'react'; import { useState, useEffect, useMemo } from 'react';
import { X } from 'lucide-react'; import { X } from 'lucide-react';
import type { Employee } from '../../api/useEmployeeData'; import type { Employee, Company, Branch, Department, JobRole } from '../../types/masterData';
import { useEmployeeById, useManagers } from '../../api/useEmployeeData'; import { useEmployeeById, useManagers } from '../../api/useEmployeeData';
import type { Company } from '../../api/useCompanyData';
import type { Branch } from '../../api/useBranchData';
import type { Department } from '../../api/useDepartmentData';
import type { JobRole } from '../../api/useJobRoleData';
interface EmployeeFormModalProps { interface EmployeeFormModalProps {
employee: Employee | null; employee: Employee | null;
@ -14,6 +11,7 @@ interface EmployeeFormModalProps {
departments: Department[]; departments: Department[];
jobRoles: JobRole[]; jobRoles: JobRole[];
onClose: () => void; onClose: () => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onSave: (data: any) => void; onSave: (data: any) => void;
} }
@ -38,14 +36,14 @@ export default function EmployeeFormModal({ employee, companies, branches, depar
const [workEmail, setWorkEmail] = useState(''); const [workEmail, setWorkEmail] = useState('');
const [personalPhone, setPersonalPhone] = useState(''); const [personalPhone, setPersonalPhone] = useState('');
const [dateJoining, setDateJoining] = useState(''); const [dateJoining, setDateJoining] = useState('');
const [addrDoor, setAddrDoor] = useState(''); const [addrDoor, setAddrDoor] = useState('');
const [addrLandmark, setAddrLandmark] = useState(''); const [addrLandmark, setAddrLandmark] = useState('');
const [addrLine, setAddrLine] = useState(''); const [addrLine, setAddrLine] = useState('');
const [addrPincode, setAddrPincode] = useState(''); const [addrPincode, setAddrPincode] = useState('');
const [addrDistrict, setAddrDistrict] = useState(''); const [addrDistrict, setAddrDistrict] = useState('');
const [addrState, setAddrState] = useState(''); const [addrState, setAddrState] = useState('');
const [companyId, setCompanyId] = useState<string>(''); const [companyId, setCompanyId] = useState<string>('');
const [branchId, setBranchId] = useState<string>(''); const [branchId, setBranchId] = useState<string>('');
const [deptId, setDeptId] = useState<string>(''); const [deptId, setDeptId] = useState<string>('');
@ -60,8 +58,9 @@ export default function EmployeeFormModal({ employee, companies, branches, depar
useEffect(() => { useEffect(() => {
if (employee && empDetail && employee.id !== loadedEmpId) { if (employee && empDetail && employee.id !== loadedEmpId) {
// eslint-disable-next-line react-hooks/set-state-in-effect
setLoadedEmpId(employee.id); setLoadedEmpId(employee.id);
setFirstName(empDetail.firstName || ''); setFirstName(empDetail.firstName || '');
setLastName(empDetail.lastName || ''); setLastName(empDetail.lastName || '');
setDob(parseDate(empDetail.dob)); setDob(parseDate(empDetail.dob));
@ -84,7 +83,7 @@ export default function EmployeeFormModal({ employee, companies, branches, depar
setBranchId(String(empDetail.branchId || '')); setBranchId(String(empDetail.branchId || ''));
setDeptId(String(empDetail.departmentId || '')); setDeptId(String(empDetail.departmentId || ''));
setJobId(String(empDetail.jobId || '')); setJobId(String(empDetail.jobId || ''));
} }
else if (!employee && loadedEmpId !== null) { else if (!employee && loadedEmpId !== null) {
setLoadedEmpId(null); setLoadedEmpId(null);
setFirstName(''); setFirstName('');
@ -125,12 +124,12 @@ export default function EmployeeFormModal({ employee, companies, branches, depar
}, [empDetail, managers, employee, loadedEmpId]); }, [empDetail, managers, employee, loadedEmpId]);
// --- CASCADING DROPDOWN LOGIC (FILTERING BY NAME TO MATCH BACKEND RESPONSES) --- // --- CASCADING DROPDOWN LOGIC (FILTERING BY NAME TO MATCH BACKEND RESPONSES) ---
const filteredBranches = useMemo(() => { const filteredBranches = useMemo(() => {
if (!companyId) return []; if (!companyId) return [];
const selectedCompany = companies.find(c => String(c.id) === companyId); const selectedCompany = companies.find(c => String(c.id) === companyId);
if (!selectedCompany) return []; if (!selectedCompany) return [];
// Filter by companyName OR companyId just to be safe // Filter by companyName OR companyId
return branches.filter(b => b.companyName === selectedCompany.name || String(b.companyId) === companyId); return branches.filter(b => b.companyName === selectedCompany.name || String(b.companyId) === companyId);
}, [branches, companyId, companies]); }, [branches, companyId, companies]);
@ -166,7 +165,7 @@ export default function EmployeeFormModal({ employee, companies, branches, depar
const handleDeptChange = (e: React.ChangeEvent<HTMLSelectElement>) => { const handleDeptChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setDeptId(e.target.value); setDeptId(e.target.value);
setJobId(''); setJobId('');
setManagerId(''); setManagerId('');
}; };
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = (e: React.FormEvent) => {

View File

@ -1,6 +1,6 @@
import { useState, useMemo } from 'react'; import { useState, useMemo } from 'react';
import { ArrowUpDown, ArrowUp, ArrowDown, Search, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Eye } from 'lucide-react'; import { ArrowUpDown, ArrowUp, ArrowDown, Search, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Eye } from 'lucide-react';
import type { Employee } from '../../api/useEmployeeData'; import type { Employee } from '../../types/masterData';
interface EmployeeTableProps { interface EmployeeTableProps {
records: Employee[]; records: Employee[];

View File

@ -1,5 +1,5 @@
import { X, Pencil } from 'lucide-react'; import { X, Pencil } from 'lucide-react';
import type { Employee } from '../../api/useEmployeeData'; import type { Employee } from '../../types/masterData';
import { useEmployeeById } from '../../api/useEmployeeData'; import { useEmployeeById } from '../../api/useEmployeeData';
interface EmployeeViewModalProps { interface EmployeeViewModalProps {
@ -35,7 +35,7 @@ export default function EmployeeViewModal({ employee, onClose, onEdit }: Employe
<X size={24} /> <X size={24} />
</button> </button>
</div> </div>
<h3 className="text-xl font-bold text-text-primary mb-6">Employee Details</h3> <h3 className="text-xl font-bold text-text-primary mb-6">Employee Details</h3>
{isLoading || !empDetail ? ( {isLoading || !empDetail ? (

View File

@ -1,9 +1,6 @@
import { useState, useEffect, useMemo } from 'react'; import { useState, useEffect, useMemo } from 'react';
import { X, Pencil } from 'lucide-react'; import { X, Pencil } from 'lucide-react';
import type { JobRole } from '../../api/useJobRoleData'; import type { JobRole, Company, Branch, Department } from '../../types/masterData';
import type { Company } from '../../api/useCompanyData';
import type { Branch } from '../../api/useBranchData';
import type { Department } from '../../api/useDepartmentData';
interface JobRoleFormModalProps { interface JobRoleFormModalProps {
mode: 'view' | 'edit' | 'add'; mode: 'view' | 'edit' | 'add';
@ -13,7 +10,7 @@ interface JobRoleFormModalProps {
departments: Department[]; departments: Department[];
onClose: () => void; onClose: () => void;
onSave: (data: { name: string; description: string; companyId: number; branchId: number; deptId: number; status: 'Active' | 'Disabled' }) => void; onSave: (data: { name: string; description: string; companyId: number; branchId: number; deptId: number; status: 'Active' | 'Disabled' }) => void;
onEdit: () => void; onEdit: () => void;
} }
export default function JobRoleFormModal({ mode, jobRole, companies, branches, departments, onClose, onSave, onEdit }: JobRoleFormModalProps) { export default function JobRoleFormModal({ mode, jobRole, companies, branches, departments, onClose, onSave, onEdit }: JobRoleFormModalProps) {
@ -92,7 +89,7 @@ export default function JobRoleFormModal({ mode, jobRole, companies, branches, d
alert("Please select Company, Branch, and Department"); alert("Please select Company, Branch, and Department");
return; return;
} }
onSave({ onSave({
name, name,
description, description,
@ -119,7 +116,7 @@ export default function JobRoleFormModal({ mode, jobRole, companies, branches, d
<X size={24} /> <X size={24} />
</button> </button>
</div> </div>
<h3 className="text-xl font-bold text-text-primary mb-6"> <h3 className="text-xl font-bold text-text-primary mb-6">
{isViewMode ? 'View Job Role' : mode === 'edit' ? 'Edit Job Role' : 'Add New Job Role'} {isViewMode ? 'View Job Role' : mode === 'edit' ? 'Edit Job Role' : 'Add New Job Role'}
</h3> </h3>
@ -127,23 +124,23 @@ export default function JobRoleFormModal({ mode, jobRole, companies, branches, d
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
<div> <div>
<label className={labelClass}>Job Role Name</label> <label className={labelClass}>Job Role Name</label>
<input <input
type="text" type="text"
value={name} value={name}
onChange={(e) => setName(e.target.value)} onChange={(e) => setName(e.target.value)}
required required
disabled={isViewMode} disabled={isViewMode}
className={inputClass} className={inputClass}
placeholder="Enter job role name..." placeholder="Enter job role name..."
/> />
</div> </div>
<div> <div>
<label className={labelClass}>Company</label> <label className={labelClass}>Company</label>
<select <select
value={companyId} value={companyId}
onChange={handleCompanyChange} onChange={handleCompanyChange}
required required
disabled={isViewMode} disabled={isViewMode}
className={inputClass} className={inputClass}
> >
@ -153,11 +150,11 @@ export default function JobRoleFormModal({ mode, jobRole, companies, branches, d
<div> <div>
<label className={labelClass}>Branch</label> <label className={labelClass}>Branch</label>
<select <select
value={branchId} value={branchId}
onChange={handleBranchChange} onChange={handleBranchChange}
required required
disabled={isViewMode || !companyId || filteredBranches.length === 0} disabled={isViewMode || !companyId || filteredBranches.length === 0}
className={inputClass} className={inputClass}
> >
<option value="">Select Branch</option> <option value="">Select Branch</option>
@ -167,11 +164,11 @@ export default function JobRoleFormModal({ mode, jobRole, companies, branches, d
<div> <div>
<label className={labelClass}>Department</label> <label className={labelClass}>Department</label>
<select <select
value={deptId} value={deptId}
onChange={(e) => setDeptId(e.target.value)} onChange={(e) => setDeptId(e.target.value)}
required required
disabled={isViewMode || !branchId || filteredDepartments.length === 0} disabled={isViewMode || !branchId || filteredDepartments.length === 0}
className={inputClass} className={inputClass}
> >
<option value="">Select Department</option> <option value="">Select Department</option>
@ -181,21 +178,21 @@ export default function JobRoleFormModal({ mode, jobRole, companies, branches, d
<div> <div>
<label className={labelClass}>Description</label> <label className={labelClass}>Description</label>
<textarea <textarea
value={description} value={description}
onChange={(e) => setDescription(e.target.value)} onChange={(e) => setDescription(e.target.value)}
rows={4} rows={4}
disabled={isViewMode} disabled={isViewMode}
className={`${inputClass} resize-y`} className={`${inputClass} resize-y`}
placeholder="Enter job description..." placeholder="Enter job description..."
/> />
</div> </div>
<div> <div>
<label className={labelClass}>Status</label> <label className={labelClass}>Status</label>
<select <select
value={status} value={status}
onChange={(e) => setStatus(e.target.value as 'Active' | 'Disabled')} onChange={(e) => setStatus(e.target.value as 'Active' | 'Disabled')}
disabled={isViewMode} disabled={isViewMode}
className={inputClass} className={inputClass}
> >

View File

@ -1,6 +1,6 @@
import { useState, useMemo } from 'react'; import { useState, useMemo } from 'react';
import { ArrowUpDown, ArrowUp, ArrowDown, Search, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Eye, Trash2 } from 'lucide-react'; import { ArrowUpDown, ArrowUp, ArrowDown, Search, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Eye, Trash2 } from 'lucide-react';
import type { JobRole } from '../../api/useJobRoleData'; import type { JobRole } from '../../types/masterData';
interface JobRoleTableProps { interface JobRoleTableProps {
records: JobRole[]; records: JobRole[];

View File

@ -1,4 +1,3 @@
// src/features/employee-management/pages/ManageBranches.tsx
import { useState } from 'react'; import { useState } from 'react';
import { Network, Plus } from 'lucide-react'; import { Network, Plus } from 'lucide-react';
import { toast } from 'sonner'; import { toast } from 'sonner';
@ -7,8 +6,9 @@ import ErrorState from '../../../components/ui/ErrorState';
import ConfirmationModal from '../../../components/ui/ConfirmationModal'; import ConfirmationModal from '../../../components/ui/ConfirmationModal';
import BranchTable from '../components/branch/BranchTable'; import BranchTable from '../components/branch/BranchTable';
import BranchFormModal from '../components/branch/BranchFormModal'; import BranchFormModal from '../components/branch/BranchFormModal';
import { useBranches, useCreateBranch, useUpdateBranch, useDeleteBranch, type Branch } from '../api/useBranchData'; import { useBranches, useCreateBranch, useUpdateBranch, useDeleteBranch } from '../api/useBranchData';
import { useCompanies } from '../api/useCompanyData'; import { useCompanies } from '../api/useCompanyData';
import type { Branch } from '../types/masterData';
export default function ManageBranches() { export default function ManageBranches() {
const { data: branches, isLoading: branchesLoading, isError: branchesError } = useBranches(); const { data: branches, isLoading: branchesLoading, isError: branchesError } = useBranches();
@ -46,9 +46,10 @@ export default function ManageBranches() {
toast.success("Branch deleted successfully."); toast.success("Branch deleted successfully.");
setDeleteTarget(null); setDeleteTarget(null);
}, },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onError: (error: any) => { onError: (error: any) => {
toast.error(error.message || "Failed to delete branch."); toast.error(error.message || "Failed to delete branch.");
setDeleteTarget(null); setDeleteTarget(null);
}, },
}); });
} }
@ -57,18 +58,19 @@ export default function ManageBranches() {
const handleSave = (name: string, companyId: number, status: 'Active' | 'Disabled') => { const handleSave = (name: string, companyId: number, status: 'Active' | 'Disabled') => {
if (editingBranch) { if (editingBranch) {
updateMutation.mutate( updateMutation.mutate(
{ {
id: editingBranch.id, id: editingBranch.id,
code: editingBranch.code, code: editingBranch.code,
name, name,
companyId, companyId,
status status
}, },
{ {
onSuccess: () => { onSuccess: () => {
toast.success("Branch updated successfully."); toast.success("Branch updated successfully.");
setIsModalOpen(false); setIsModalOpen(false);
}, },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onError: (error: any) => { onError: (error: any) => {
toast.error(error.message || "Failed to update branch."); toast.error(error.message || "Failed to update branch.");
}, },
@ -82,6 +84,7 @@ export default function ManageBranches() {
toast.success("Branch added successfully."); toast.success("Branch added successfully.");
setIsModalOpen(false); setIsModalOpen(false);
}, },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onError: (error: any) => { onError: (error: any) => {
toast.error(error.message || "Failed to add branch."); toast.error(error.message || "Failed to add branch.");
}, },
@ -119,7 +122,7 @@ export default function ManageBranches() {
{isModalOpen && ( {isModalOpen && (
<BranchFormModal <BranchFormModal
branch={editingBranch} branch={editingBranch}
companies={activeCompanies} // Pass only active companies here companies={activeCompanies}
onClose={() => setIsModalOpen(false)} onClose={() => setIsModalOpen(false)}
onSave={handleSave} onSave={handleSave}
/> />

View File

@ -6,7 +6,8 @@ import ErrorState from '../../../components/ui/ErrorState';
import ConfirmationModal from '../../../components/ui/ConfirmationModal'; import ConfirmationModal from '../../../components/ui/ConfirmationModal';
import CompanyTable from '../components/company/CompanyTable'; import CompanyTable from '../components/company/CompanyTable';
import CompanyFormModal from '../components/company/CompanyFormModal'; import CompanyFormModal from '../components/company/CompanyFormModal';
import { useCompanies, useCreateCompany, useUpdateCompany, useDeleteCompany, type Company } from '../api/useCompanyData'; import { useCompanies, useCreateCompany, useUpdateCompany, useDeleteCompany } from '../api/useCompanyData';
import type { Company } from '../types/masterData';
export default function ManageCompanies() { export default function ManageCompanies() {
const { data: companies, isLoading, isError } = useCompanies(); const { data: companies, isLoading, isError } = useCompanies();
@ -39,6 +40,7 @@ export default function ManageCompanies() {
toast.success("Company deleted successfully."); toast.success("Company deleted successfully.");
setDeleteTarget(null); setDeleteTarget(null);
}, },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onError: (error: any) => { onError: (error: any) => {
// Display the specific backend error message // Display the specific backend error message
toast.error(error.message || "Failed to delete company."); toast.error(error.message || "Failed to delete company.");
@ -57,6 +59,7 @@ export default function ManageCompanies() {
toast.success("Company updated successfully."); toast.success("Company updated successfully.");
setIsModalOpen(false); setIsModalOpen(false);
}, },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onError: (error: any) => { onError: (error: any) => {
toast.error(error.message || "Failed to update company."); toast.error(error.message || "Failed to update company.");
}, },
@ -70,6 +73,7 @@ export default function ManageCompanies() {
toast.success("Company added successfully."); toast.success("Company added successfully.");
setIsModalOpen(false); setIsModalOpen(false);
}, },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onError: (error: any) => { onError: (error: any) => {
toast.error(error.message || "Failed to add company."); toast.error(error.message || "Failed to add company.");
}, },

View File

@ -6,9 +6,10 @@ import ErrorState from '../../../components/ui/ErrorState';
import ConfirmationModal from '../../../components/ui/ConfirmationModal'; import ConfirmationModal from '../../../components/ui/ConfirmationModal';
import DepartmentTable from '../components/department/DepartmentTable'; import DepartmentTable from '../components/department/DepartmentTable';
import DepartmentFormModal from '../components/department/DepartmentFormModal'; import DepartmentFormModal from '../components/department/DepartmentFormModal';
import { useDepartments, useCreateDepartment, useUpdateDepartment, useDeleteDepartment, type Department } from '../api/useDepartmentData'; import { useDepartments, useCreateDepartment, useUpdateDepartment, useDeleteDepartment } from '../api/useDepartmentData';
import { useCompanies } from '../api/useCompanyData'; import { useCompanies } from '../api/useCompanyData';
import { useBranches } from '../api/useBranchData'; import { useBranches } from '../api/useBranchData';
import type { Department } from '../types/masterData';
export default function ManageDepartments() { export default function ManageDepartments() {
const { data: departments, isLoading: depLoading, isError: depError } = useDepartments(); const { data: departments, isLoading: depLoading, isError: depError } = useDepartments();
@ -47,9 +48,10 @@ export default function ManageDepartments() {
toast.success("Department deleted successfully."); toast.success("Department deleted successfully.");
setDeleteTarget(null); setDeleteTarget(null);
}, },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onError: (error: any) => { onError: (error: any) => {
toast.error(error.message || "Failed to delete department."); toast.error(error.message || "Failed to delete department.");
setDeleteTarget(null); setDeleteTarget(null);
}, },
}); });
} }
@ -58,18 +60,19 @@ export default function ManageDepartments() {
const handleSave = (data: Omit<Department, 'id' | 'code' | 'companyName' | 'branchName'>) => { const handleSave = (data: Omit<Department, 'id' | 'code' | 'companyName' | 'branchName'>) => {
if (editingDepartment) { if (editingDepartment) {
updateMutation.mutate( updateMutation.mutate(
{ {
id: editingDepartment.id, id: editingDepartment.id,
name: data.name, name: data.name,
companyId: data.companyId as number, companyId: data.companyId as number,
branchId: data.branchId as number, branchId: data.branchId as number,
status: data.status status: data.status
}, },
{ {
onSuccess: () => { onSuccess: () => {
toast.success("Department updated successfully."); toast.success("Department updated successfully.");
setIsModalOpen(false); setIsModalOpen(false);
}, },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onError: (error: any) => { onError: (error: any) => {
toast.error(error.message || "Failed to update department."); toast.error(error.message || "Failed to update department.");
}, },
@ -77,17 +80,18 @@ export default function ManageDepartments() {
); );
} else { } else {
createMutation.mutate( createMutation.mutate(
{ {
name: data.name, name: data.name,
companyId: data.companyId as number, companyId: data.companyId as number,
branchId: data.branchId as number, branchId: data.branchId as number,
status: data.status status: data.status
}, },
{ {
onSuccess: () => { onSuccess: () => {
toast.success("Department added successfully."); toast.success("Department added successfully.");
setIsModalOpen(false); setIsModalOpen(false);
}, },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onError: (error: any) => { onError: (error: any) => {
toast.error(error.message || "Failed to add department."); toast.error(error.message || "Failed to add department.");
}, },

View File

@ -6,11 +6,12 @@ import ErrorState from '../../../components/ui/ErrorState';
import EmployeeTable from '../components/employee/EmployeeTable'; import EmployeeTable from '../components/employee/EmployeeTable';
import EmployeeFormModal from '../components/employee/EmployeeFormModal'; import EmployeeFormModal from '../components/employee/EmployeeFormModal';
import EmployeeViewModal from '../components/employee/EmployeeViewModal'; import EmployeeViewModal from '../components/employee/EmployeeViewModal';
import { useEmployees, useCreateEmployee, useUpdateEmployee, useUpdateEmployeeStatus, type Employee } from '../api/useEmployeeData'; import { useEmployees, useCreateEmployee, useUpdateEmployee, useUpdateEmployeeStatus } from '../api/useEmployeeData';
import { useCompanies } from '../api/useCompanyData'; import { useCompanies } from '../api/useCompanyData';
import { useBranches } from '../api/useBranchData'; import { useBranches } from '../api/useBranchData';
import { useDepartments } from '../api/useDepartmentData'; import { useDepartments } from '../api/useDepartmentData';
import { useJobRoles } from '../api/useJobRoleData'; import { useJobRoles } from '../api/useJobRoleData';
import type { Employee } from '../types/masterData';
export default function ManageEmployees() { export default function ManageEmployees() {
const { data: employees, isLoading: empLoading, isError: empError } = useEmployees(); const { data: employees, isLoading: empLoading, isError: empError } = useEmployees();
@ -42,11 +43,12 @@ export default function ManageEmployees() {
}; };
const handleEditFromView = (employee: Employee) => { const handleEditFromView = (employee: Employee) => {
setViewingEmployee(null); setViewingEmployee(null);
setEditingEmployee(employee); setEditingEmployee(employee);
setIsFormModalOpen(true); setIsFormModalOpen(true);
}; };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const handleSave = async (data: any) => { const handleSave = async (data: any) => {
if (editingEmployee) { if (editingEmployee) {
try { try {
@ -60,9 +62,10 @@ export default function ManageEmployees() {
// 2. Update the rest of the profile via PUT endpoint // 2. Update the rest of the profile via PUT endpoint
await updateMutation.mutateAsync({ ...data, id: editingEmployee.id }); await updateMutation.mutateAsync({ ...data, id: editingEmployee.id });
toast.success("Employee updated successfully."); toast.success("Employee updated successfully.");
setIsFormModalOpen(false); setIsFormModalOpen(false);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) { } catch (error: any) {
toast.error(error.message || "Failed to save employee data."); toast.error(error.message || "Failed to save employee data.");
} }
@ -72,6 +75,7 @@ export default function ManageEmployees() {
toast.success("Employee added successfully."); toast.success("Employee added successfully.");
setIsFormModalOpen(false); setIsFormModalOpen(false);
}, },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onError: (error: any) => { onError: (error: any) => {
toast.error(error.message || "Failed to add employee."); toast.error(error.message || "Failed to add employee.");
}, },

View File

@ -6,10 +6,11 @@ import ErrorState from '../../../components/ui/ErrorState';
import ConfirmationModal from '../../../components/ui/ConfirmationModal'; import ConfirmationModal from '../../../components/ui/ConfirmationModal';
import JobRoleTable from '../components/jobRole/JobRoleTable'; import JobRoleTable from '../components/jobRole/JobRoleTable';
import JobRoleFormModal from '../components/jobRole/JobRoleFormModal'; import JobRoleFormModal from '../components/jobRole/JobRoleFormModal';
import { useJobRoles, useCreateJobRole, useUpdateJobRole, useDeleteJobRole, type JobRole } from '../api/useJobRoleData'; import { useJobRoles, useCreateJobRole, useUpdateJobRole, useDeleteJobRole } from '../api/useJobRoleData';
import { useCompanies } from '../api/useCompanyData'; import { useCompanies } from '../api/useCompanyData';
import { useBranches } from '../api/useBranchData'; import { useBranches } from '../api/useBranchData';
import { useDepartments } from '../api/useDepartmentData'; import { useDepartments } from '../api/useDepartmentData';
import type { JobRole } from '../types/masterData';
type ModalMode = 'view' | 'edit' | 'add'; type ModalMode = 'view' | 'edit' | 'add';
@ -59,9 +60,10 @@ export default function ManageJobRoles() {
toast.success("Job Role deleted successfully."); toast.success("Job Role deleted successfully.");
setDeleteTarget(null); setDeleteTarget(null);
}, },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onError: (error: any) => { onError: (error: any) => {
toast.error(error.message || "Failed to delete job role."); toast.error(error.message || "Failed to delete job role.");
setDeleteTarget(null); setDeleteTarget(null);
}, },
}); });
} }
@ -70,18 +72,19 @@ export default function ManageJobRoles() {
const handleSave = (data: { name: string; description: string; companyId: number; branchId: number; deptId: number; status: 'Active' | 'Disabled' }) => { const handleSave = (data: { name: string; description: string; companyId: number; branchId: number; deptId: number; status: 'Active' | 'Disabled' }) => {
if (modalMode === 'edit' && editingJobRole) { if (modalMode === 'edit' && editingJobRole) {
updateMutation.mutate( updateMutation.mutate(
{ {
id: editingJobRole.id, id: editingJobRole.id,
name: data.name, name: data.name,
description: data.description, description: data.description,
deptId: data.deptId, deptId: data.deptId,
status: data.status status: data.status
}, },
{ {
onSuccess: () => { onSuccess: () => {
toast.success("Job Role updated successfully."); toast.success("Job Role updated successfully.");
setIsModalOpen(false); setIsModalOpen(false);
}, },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onError: (error: any) => { onError: (error: any) => {
toast.error(error.message || "Failed to update job role."); toast.error(error.message || "Failed to update job role.");
}, },
@ -89,17 +92,18 @@ export default function ManageJobRoles() {
); );
} else { } else {
createMutation.mutate( createMutation.mutate(
{ {
name: data.name, name: data.name,
description: data.description, description: data.description,
deptId: data.deptId, deptId: data.deptId,
status: data.status status: data.status
}, },
{ {
onSuccess: () => { onSuccess: () => {
toast.success("Job Role added successfully."); toast.success("Job Role added successfully.");
setIsModalOpen(false); setIsModalOpen(false);
}, },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onError: (error: any) => { onError: (error: any) => {
toast.error(error.message || "Failed to add job role."); toast.error(error.message || "Failed to add job role.");
}, },

View File

@ -1,7 +1,7 @@
import { Building, Network, Briefcase, Tag, Users } from 'lucide-react'; import { Building, Network, Briefcase, Tag, Users } from 'lucide-react';
import Loader from '../../../components/ui/Loader'; import Loader from '../../../components/ui/Loader';
import ErrorState from '../../../components/ui/ErrorState'; import ErrorState from '../../../components/ui/ErrorState';
import StatCard from '../../../components/ui/StatCard'; import StatCard from '../../../components/ui/StatCard';
import { useMasterDataSummary } from '../api/useMasterData'; import { useMasterDataSummary } from '../api/useMasterData';
export default function MasterDataDashboard() { export default function MasterDataDashboard() {
@ -18,40 +18,40 @@ export default function MasterDataDashboard() {
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<StatCard <StatCard
title="Total Companies" title="Total Companies"
value={summary?.totalCompanies || 0} value={summary?.totalCompanies || 0}
Icon={Building} Icon={Building}
colorClass="text-primary" colorClass="text-primary"
bgClass="bg-primary/10" bgClass="bg-primary/10"
/> />
<StatCard <StatCard
title="Total Branches" title="Total Branches"
value={summary?.totalBranches || 0} value={summary?.totalBranches || 0}
Icon={Network} Icon={Network}
colorClass="text-primary" colorClass="text-primary"
bgClass="bg-primary/10" bgClass="bg-primary/10"
/> />
<StatCard <StatCard
title="Total Departments" title="Total Departments"
value={summary?.totalDepartments || 0} value={summary?.totalDepartments || 0}
Icon={Briefcase} Icon={Briefcase}
colorClass="text-primary" colorClass="text-primary"
bgClass="bg-primary/10" bgClass="bg-primary/10"
/> />
<StatCard <StatCard
title="Total Job Roles" title="Total Job Roles"
value={summary?.totalJobRoles || 0} value={summary?.totalJobRoles || 0}
Icon={Tag} Icon={Tag}
colorClass="text-primary" colorClass="text-primary"
bgClass="bg-primary/10" bgClass="bg-primary/10"
/> />
<StatCard <StatCard
title="Total Employees" title="Total Employees"
value={summary?.totalEmployees || 0} value={summary?.totalEmployees || 0}
Icon={Users} Icon={Users}
colorClass="text-primary" colorClass="text-primary"
bgClass="bg-primary/10" bgClass="bg-primary/10"
/> />
</div> </div>
</div> </div>

View File

@ -0,0 +1,130 @@
export interface Branch {
id: number;
code: string;
name: string;
companyId: number | null;
companyCode: string;
companyName: string;
status: 'Active' | 'Disabled';
}
export interface Company {
id: number;
code: string;
name: string;
status: 'Active' | 'Disabled';
}
export interface Department {
id: number;
code: string;
name: string;
branchId: number | null;
branchName: string;
companyId: number | null;
companyName: string;
status: 'Active' | 'Disabled';
}
export interface Employee {
id: number;
code: string;
firstName: string;
lastName: string;
email: string;
jobId: number | null;
jobName: string;
deptId: number | null;
deptName: string;
branchId: number | null;
branchName: string;
companyId: number | null;
companyName: string;
status: 'Active' | 'Disabled';
}
export interface EmployeeDetail {
employeeId: number;
employeeCode: string;
isActive: boolean;
firstName: string;
lastName: string;
dob: string;
gender: string;
personalEmail: string;
personalPhone: string;
workEmail: string;
dateJoining: string;
probationDays: number;
contractStatus: string;
salaryStructureId: number;
department: string;
departmentId: number;
jobName: string;
jobId: number;
managerFirstName: string;
managerLastName: string;
managerEmployeeCode: string;
addresses: Array<{
type: string;
doorNumber: string;
landmark: string;
line: string;
pincode: string;
district: string;
state: string;
}>;
companyName: string;
companyId: number;
branchName: string;
branchId: number;
}
export interface Manager {
id: number;
code: string;
firstName: string;
lastName: string;
}
export interface HRManagerEmployee extends Employee {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
name: any;
isHRManager: boolean;
}
export interface JobRole {
id: number;
code: string;
name: string;
description: string;
deptId: number | null;
deptName: string;
branchId: number | null;
branchName: string;
companyId: number | null;
companyName: string;
status: 'Active' | 'Disabled';
}
export interface MasterDataSummary {
totalCompanies: number;
totalBranches: number;
totalDepartments: number;
totalJobRoles: number;
totalEmployees: number;
}
export interface Policy {
id: string;
title: string;
category: string;
description: string;
}
export interface CompanyPolicy {
companyId: string;
companyName: string;
policies: Policy[];
}