Compare commits

...

2 Commits

22 changed files with 843 additions and 36 deletions

View File

@ -0,0 +1,222 @@
[
{
"id": "MA005",
"employeeName": "Michael Brown",
"leaveType": "Casual Leave",
"applicationDate": "2026-07-16",
"fromDate": "2026-07-28",
"toDate": "2026-07-29",
"leaveDays": 2,
"status": "Pending",
"reason": "Attending a family wedding."
},
{
"id": "MA006",
"employeeName": "Emily Davis",
"leaveType": "Optional Holiday",
"applicationDate": "2026-07-17",
"fromDate": "2026-08-01",
"toDate": "2026-08-01",
"leaveDays": 1,
"status": "Pending",
"reason": "Festival celebration."
},
{
"id": "MA007",
"employeeName": "Chris Wilson",
"leaveType": "Casual Leave",
"applicationDate": "2026-07-18",
"fromDate": "2026-08-03",
"toDate": "2026-08-04",
"leaveDays": 2,
"status": "Approved",
"reason": "Personal commitments."
},
{
"id": "MA008",
"employeeName": "Sophia Johnson",
"leaveType": "Optional Holiday",
"applicationDate": "2026-07-19",
"fromDate": "2026-08-05",
"toDate": "2026-08-05",
"leaveDays": 1,
"status": "Rejected",
"reason": "Festival observance."
},
{
"id": "MA009",
"employeeName": "Daniel Lee",
"leaveType": "Casual Leave",
"applicationDate": "2026-07-20",
"fromDate": "2026-08-06",
"toDate": "2026-08-08",
"leaveDays": 3,
"status": "Pending",
"reason": "Family emergency."
},
{
"id": "MA010",
"employeeName": "Olivia Martin",
"leaveType": "Optional Holiday",
"applicationDate": "2026-07-21",
"fromDate": "2026-08-10",
"toDate": "2026-08-10",
"leaveDays": 1,
"status": "Approved",
"reason": "Cultural celebration."
},
{
"id": "MA011",
"employeeName": "James Taylor",
"leaveType": "Casual Leave",
"applicationDate": "2026-07-22",
"fromDate": "2026-08-12",
"toDate": "2026-08-13",
"leaveDays": 2,
"status": "Pending",
"reason": "Home relocation."
},
{
"id": "MA012",
"employeeName": "Emma Garcia",
"leaveType": "Optional Holiday",
"applicationDate": "2026-07-23",
"fromDate": "2026-08-14",
"toDate": "2026-08-14",
"leaveDays": 1,
"status": "Pending",
"reason": "Religious observance."
},
{
"id": "MA013",
"employeeName": "William Harris",
"leaveType": "Casual Leave",
"applicationDate": "2026-07-24",
"fromDate": "2026-08-17",
"toDate": "2026-08-18",
"leaveDays": 2,
"status": "Approved",
"reason": "Health checkup."
},
{
"id": "MA014",
"employeeName": "Ava Clark",
"leaveType": "Optional Holiday",
"applicationDate": "2026-07-25",
"fromDate": "2026-08-19",
"toDate": "2026-08-19",
"leaveDays": 1,
"status": "Pending",
"reason": "Community event."
},
{
"id": "MA015",
"employeeName": "Benjamin Lewis",
"leaveType": "Casual Leave",
"applicationDate": "2026-07-26",
"fromDate": "2026-08-20",
"toDate": "2026-08-22",
"leaveDays": 3,
"status": "Rejected",
"reason": "Vacation with family."
},
{
"id": "MA016",
"employeeName": "Mia Walker",
"leaveType": "Optional Holiday",
"applicationDate": "2026-07-27",
"fromDate": "2026-08-24",
"toDate": "2026-08-24",
"leaveDays": 1,
"status": "Approved",
"reason": "Festival celebration."
},
{
"id": "MA017",
"employeeName": "Ethan Hall",
"leaveType": "Casual Leave",
"applicationDate": "2026-07-28",
"fromDate": "2026-08-25",
"toDate": "2026-08-26",
"leaveDays": 2,
"status": "Pending",
"reason": "Personal travel."
},
{
"id": "MA018",
"employeeName": "Charlotte Allen",
"leaveType": "Optional Holiday",
"applicationDate": "2026-07-29",
"fromDate": "2026-08-27",
"toDate": "2026-08-27",
"leaveDays": 1,
"status": "Pending",
"reason": "Special occasion."
},
{
"id": "MA019",
"employeeName": "Alexander Young",
"leaveType": "Casual Leave",
"applicationDate": "2026-07-30",
"fromDate": "2026-08-28",
"toDate": "2026-08-30",
"leaveDays": 3,
"status": "Approved",
"reason": "Family gathering."
},
{
"id": "MA020",
"employeeName": "Amelia King",
"leaveType": "Optional Holiday",
"applicationDate": "2026-07-31",
"fromDate": "2026-09-01",
"toDate": "2026-09-01",
"leaveDays": 1,
"status": "Pending",
"reason": "Festival leave."
},
{
"id": "MA021",
"employeeName": "Matthew Scott",
"leaveType": "Casual Leave",
"applicationDate": "2026-08-01",
"fromDate": "2026-09-02",
"toDate": "2026-09-03",
"leaveDays": 2,
"status": "Rejected",
"reason": "Personal errands."
},
{
"id": "MA022",
"employeeName": "Harper Green",
"leaveType": "Optional Holiday",
"applicationDate": "2026-08-02",
"fromDate": "2026-09-04",
"toDate": "2026-09-04",
"leaveDays": 1,
"status": "Approved",
"reason": "Religious holiday."
},
{
"id": "MA023",
"employeeName": "David Baker",
"leaveType": "Casual Leave",
"applicationDate": "2026-08-03",
"fromDate": "2026-09-07",
"toDate": "2026-09-08",
"leaveDays": 2,
"status": "Pending",
"reason": "Attending a seminar."
},
{
"id": "MA024",
"employeeName": "Ella Adams",
"leaveType": "Optional Holiday",
"applicationDate": "2026-08-04",
"fromDate": "2026-09-09",
"toDate": "2026-09-09",
"leaveDays": 1,
"status": "Pending",
"reason": "Festival observance."
}
]

View File

@ -0,0 +1,45 @@
import { useState, useMemo } from 'react';
import Calendar from '../../components/Calendar';
import AttendanceRecordsTable from '../../components/AttendanceRecordsTable';
import DailySummary from '../../components/DailySummary';
import Loader from '../../../../components/ui/Loader';
import ErrorState from '../../../../components/ui/ErrorState';
import { useAttendanceData } from '../../api/useAttendanceData';
const getTodayDate = () => {
const today = new Date();
return `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
};
export default function AdminAttendance() {
const [selectedDate, setSelectedDate] = useState(getTodayDate());
const { data: attendanceRecords, isLoading, isError } = useAttendanceData();
const calendarData = useMemo(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const map: Record<string, any> = {};
if (attendanceRecords) {
attendanceRecords.forEach(record => {
map[record.date] = {
status: record.status, login: record.timeIn, logout: record.timeOut, hours: record.loggedHours
};
});
}
return map;
}, [attendanceRecords]);
if (isLoading) return <Loader message="Loading my attendance..." />;
if (isError) return <ErrorState message="Failed to load attendance data." />;
return (
<div className="flex flex-col gap-6">
<div className="flex flex-col lg:flex-row gap-6">
<div className="flex-1">
<Calendar data={calendarData} selectedDate={selectedDate} onDateSelect={setSelectedDate} />
</div>
<DailySummary selectedDate={selectedDate} data={calendarData[selectedDate] || null} />
</div>
{attendanceRecords && <AttendanceRecordsTable records={attendanceRecords} title="My Attendance Records" />}
</div>
);
}

View File

@ -1,10 +1,10 @@
import { useState, useMemo } from 'react';
import Calendar from '../components/Calendar';
import AttendanceRecordsTable from '../components/AttendanceRecordsTable';
import DailySummary from '../components/DailySummary';
import Loader from '../../../components/ui/Loader';
import ErrorState from '../../../components/ui/ErrorState';
import { useAttendanceData } from '../api/useAttendanceData';
import Calendar from '../../components/Calendar';
import AttendanceRecordsTable from '../../components/AttendanceRecordsTable';
import DailySummary from '../../components/DailySummary';
import Loader from '../../../../components/ui/Loader';
import ErrorState from '../../../../components/ui/ErrorState';
import { useAttendanceData } from '../../api/useAttendanceData';
const getTodayDate = () => {
const today = new Date();

View File

@ -0,0 +1,45 @@
import { useState, useMemo } from 'react';
import Calendar from '../../components/Calendar';
import AttendanceRecordsTable from '../../components/AttendanceRecordsTable';
import DailySummary from '../../components/DailySummary';
import Loader from '../../../../components/ui/Loader';
import ErrorState from '../../../../components/ui/ErrorState';
import { useAttendanceData } from '../../api/useAttendanceData';
const getTodayDate = () => {
const today = new Date();
return `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
};
export default function ManagerAttendance() {
const [selectedDate, setSelectedDate] = useState(getTodayDate());
const { data: attendanceRecords, isLoading, isError } = useAttendanceData();
const calendarData = useMemo(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const map: Record<string, any> = {};
if (attendanceRecords) {
attendanceRecords.forEach(record => {
map[record.date] = {
status: record.status, login: record.timeIn, logout: record.timeOut, hours: record.loggedHours
};
});
}
return map;
}, [attendanceRecords]);
if (isLoading) return <Loader message="Loading my attendance..." />;
if (isError) return <ErrorState message="Failed to load attendance data." />;
return (
<div className="flex flex-col gap-6">
<div className="flex flex-col lg:flex-row gap-6">
<div className="flex-1">
<Calendar data={calendarData} selectedDate={selectedDate} onDateSelect={setSelectedDate} />
</div>
<DailySummary selectedDate={selectedDate} data={calendarData[selectedDate] || null} />
</div>
{attendanceRecords && <AttendanceRecordsTable records={attendanceRecords} title="My Attendance Records" />}
</div>
);
}

View File

@ -0,0 +1,45 @@
import { useState, useMemo } from 'react';
import Calendar from '../../components/Calendar';
import AttendanceRecordsTable from '../../components/AttendanceRecordsTable';
import DailySummary from '../../components/DailySummary';
import Loader from '../../../../components/ui/Loader';
import ErrorState from '../../../../components/ui/ErrorState';
import { useAttendanceData } from '../../api/useAttendanceData';
const getTodayDate = () => {
const today = new Date();
return `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
};
export default function SuperAdminAttendance() {
const [selectedDate, setSelectedDate] = useState(getTodayDate());
const { data: attendanceRecords, isLoading, isError } = useAttendanceData();
const calendarData = useMemo(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const map: Record<string, any> = {};
if (attendanceRecords) {
attendanceRecords.forEach(record => {
map[record.date] = {
status: record.status, login: record.timeIn, logout: record.timeOut, hours: record.loggedHours
};
});
}
return map;
}, [attendanceRecords]);
if (isLoading) return <Loader message="Loading my attendance..." />;
if (isError) return <ErrorState message="Failed to load attendance data." />;
return (
<div className="flex flex-col gap-6">
<div className="flex flex-col lg:flex-row gap-6">
<div className="flex-1">
<Calendar data={calendarData} selectedDate={selectedDate} onDateSelect={setSelectedDate} />
</div>
<DailySummary selectedDate={selectedDate} data={calendarData[selectedDate] || null} />
</div>
{attendanceRecords && <AttendanceRecordsTable records={attendanceRecords} title="My Attendance Records" />}
</div>
);
}

View File

@ -1,9 +1,9 @@
import { useSelector } from 'react-redux';
import type { RootState } from '../../../store/store';
import EmployeeAttendance from '../pages/EmployeeAttendance';
import ManagerAttendance from '../pages/ManagerAttendance';
import AdminAttendance from '../pages/AdminAttendance';
import SuperAdminAttendance from '../pages/SuperAdminAttendance';
import EmployeeAttendance from '../pages/Attendance/EmployeeAttendance';
import ManagerAttendance from '../pages/Attendance/ManagerAttendance';
import AdminAttendance from '../pages/Attendance/AdminAttendance';
import SuperAdminAttendance from '../pages/Attendance/SuperAdminAttendance';
export default function AttendanceRouter() {
const role = useSelector((state: RootState) => state.role.currentRole);

View File

@ -3,7 +3,7 @@ import { useSelector } from 'react-redux';
import { fetchMockData, postMockData } from '../../../api/client';
import type { RootState } from '../../../store/store';
import type { UserRole } from '../../../store/roleSlice';
import type { LeaveSummary, LeaveRequestPayload, LeaveHistoryRecord } from '../types/leave';
import type { LeaveSummary, LeaveRequestPayload, LeaveHistoryRecord, ManagerLeaveApproval, ApprovalStatus } from '../types/leave';
const LEAVE_ENDPOINTS: Record<UserRole, { summary: string, history: string }> = {
employee: {
@ -55,3 +55,29 @@ export const useLeaveHistory = () => {
queryFn: () => fetchMockData<LeaveHistoryRecord[]>(LEAVE_ENDPOINTS[role].history),
});
};
export const useManagerApprovals = () => {
return useQuery<ManagerLeaveApproval[]>({
queryKey: ['leaveApprovals', 'manager'],
// Fix: Point to the exact path where you created the JSON file
queryFn: () => fetchMockData<ManagerLeaveApproval[]>('approvals/manager_pending_leaves.json'),
});
};
export const useUpdateLeaveApproval = () => {
const queryClient = useQueryClient();
return useMutation({
// In the future: axios.put('/api/v1/leave/approvals/:id', { status })
mutationFn: async ({ id, status }: { id: string; status: ApprovalStatus }) => {
await new Promise((resolve) => setTimeout(resolve, 500)); // simulate network
console.log(`[Mock PUT] Updated leave ${id} to ${status}`);
return { id, status };
},
onSuccess: () => {
// Refetch the list to show the updated status
queryClient.invalidateQueries({ queryKey: ['leaveApprovals', 'manager'] });
},
});
};

View File

@ -0,0 +1,93 @@
import { X, Check, Ban } from 'lucide-react';
import type { ManagerLeaveApproval, ApprovalStatus } from '../../types/leave';
interface ApprovalModalProps {
record: ManagerLeaveApproval;
onClose: () => void;
onAction: (id: string, status: ApprovalStatus) => void;
}
const getStatusClass = (status: string) => {
switch (status) {
case 'Approved': return 'bg-present-100 text-present-700';
case 'Pending': return 'bg-late-100 text-late-700';
case 'Rejected': return 'bg-absent-100 text-absent-700';
default: return 'bg-app-muted text-text-secondary';
}
};
export default function ApprovalModal({ record, onClose, onAction }: ApprovalModalProps) {
// Format dates to be highly readable (e.g., 15 Jul 2026)
const formatReadableDate = (dateStr: string) => {
return new Date(dateStr + 'T00:00:00').toLocaleDateString('en-US', { day: '2-digit', month: 'short', year: 'numeric' });
};
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-app-card rounded-lg shadow-xl w-full max-w-md p-6 relative">
<button onClick={onClose} className="absolute top-4 right-4 text-text-muted hover:text-text-primary">
<X size={24} />
</button>
<h3 className="text-xl font-bold text-text-primary mb-1">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="flex justify-between border-b border-app-border pb-2">
<span className="text-sm text-text-muted">Employee Name</span>
<span className="text-sm font-medium text-text-primary">{record.employeeName}</span>
</div>
<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 font-medium text-text-primary">{record.leaveType}</span>
</div>
<div className="flex justify-between border-b border-app-border pb-2">
<span className="text-sm text-text-muted">Application Date</span>
<span className="text-sm font-medium text-text-primary">{formatReadableDate(record.applicationDate)}</span>
</div>
{/* Updated Readable Duration */}
<div className="flex justify-between border-b border-app-border pb-2">
<span className="text-sm text-text-muted">Duration</span>
<span className="text-sm font-medium text-text-primary text-right">
{formatReadableDate(record.fromDate)} to {formatReadableDate(record.toDate)}<br />
<span className="text-text-muted">({record.leaveDays} Days)</span>
</span>
</div>
<div className="flex justify-between border-b border-app-border pb-2">
<span className="text-sm text-text-muted">Current Status</span>
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusClass(record.status)}`}>{record.status}</span>
</div>
<div>
<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}</p>
</div>
</div>
{/* Action Buttons */}
<div className="flex justify-end space-x-3 mt-6">
{record.status === 'Pending' ? (
<>
<button
onClick={() => onAction(record.id, 'Rejected')}
className="flex items-center px-4 py-2 bg-absent-500 text-white rounded-lg hover:bg-absent-600 transition-colors text-sm font-medium"
>
<Ban size={16} className="mr-2" /> Reject
</button>
<button
onClick={() => onAction(record.id, 'Approved')}
className="flex items-center px-4 py-2 bg-present-600 text-white rounded-lg hover:bg-present-700 transition-colors text-sm font-medium"
>
<Check size={16} className="mr-2" /> Approve
</button>
</>
) : (
<button onClick={onClose} className="px-4 py-2 bg-app-muted text-text-primary rounded-lg hover:bg-app-border transition-colors text-sm font-medium">
Close
</button>
)}
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,197 @@
import { useState, useMemo } from 'react';
import { ArrowUpDown, ArrowUp, ArrowDown, Eye, Search, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';
import type { ManagerLeaveApproval, ApprovalStatus } from '../../types/leave';
import ApprovalModal from './ApprovalModal';
interface ApprovalTableProps {
records: ManagerLeaveApproval[];
onAction: (id: string, status: ApprovalStatus) => void;
}
type SortKey = 'employeeName' | 'leaveType' | 'applicationDate' | 'leaveDays' | 'status';
type SortDirection = 'ascending' | 'descending';
const getStatusClass = (status: string) => {
switch (status) {
case 'Approved': return 'bg-present-100 text-present-700';
case 'Pending': return 'bg-late-100 text-late-700';
case 'Rejected': return 'bg-absent-100 text-absent-700';
default: return 'bg-app-muted text-text-secondary';
}
};
export default function ApprovalTable({ records, onAction }: ApprovalTableProps) {
const [viewRecord, setViewRecord] = useState<ManagerLeaveApproval | null>(null);
const [searchQuery, setSearchQuery] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10); // Default 10 entries per page
// Default sort: applicationDate descending (last applied first)
const [sortConfig, setSortConfig] = useState<{ key: SortKey; direction: SortDirection }>({
key: 'applicationDate',
direction: 'descending'
});
// Filter data based on search query
const filteredData = useMemo(() => {
if (!searchQuery) return records;
return records.filter(record =>
record.employeeName.toLowerCase().includes(searchQuery.toLowerCase()) ||
record.leaveType.toLowerCase().includes(searchQuery.toLowerCase()) ||
record.status.toLowerCase().includes(searchQuery.toLowerCase())
);
}, [records, searchQuery]);
// Sort filtered data
const sortedData = useMemo(() => {
const sortableData = [...filteredData];
sortableData.sort((a, b) => {
if (a[sortConfig.key] < b[sortConfig.key]) return sortConfig.direction === 'ascending' ? -1 : 1;
if (a[sortConfig.key] > b[sortConfig.key]) return sortConfig.direction === 'ascending' ? 1 : -1;
return 0;
});
return sortableData;
}, [filteredData, sortConfig]);
// Pagination Logic
const totalPages = Math.ceil(sortedData.length / pageSize);
const isPaginationEnabled = sortedData.length > pageSize;
const paginatedData = useMemo(() => {
const startIndex = (currentPage - 1) * pageSize;
return sortedData.slice(startIndex, startIndex + pageSize);
}, [sortedData, currentPage, pageSize]);
const pageNumbers = useMemo(() => {
const pages = [];
const maxVisiblePages = 5;
let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2));
let endPage = startPage + maxVisiblePages - 1;
if (endPage > totalPages) {
endPage = totalPages;
startPage = Math.max(1, endPage - maxVisiblePages + 1);
}
for (let i = startPage; i <= endPage; i++) pages.push(i);
return pages;
}, [currentPage, totalPages]);
const requestSort = (key: SortKey) => {
let direction: SortDirection = 'ascending';
if (sortConfig.key === key && sortConfig.direction === 'ascending') direction = 'descending';
setSortConfig({ key, direction });
setCurrentPage(1);
};
const handlePageSizeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setPageSize(Number(e.target.value));
setCurrentPage(1);
};
const getSortIcon = (key: SortKey) => {
if (sortConfig.key !== key) return <ArrowUpDown size={14} className="ml-1 text-text-light" />;
if (sortConfig.direction === 'ascending') return <ArrowUp size={14} className="ml-1 text-primary" />;
return <ArrowDown size={14} className="ml-1 text-primary" />;
};
return (
<div className="bg-app-card p-6 rounded-lg shadow-sm mt-6">
<div className="flex flex-col md:flex-row md:items-center justify-between mb-6 gap-4">
<h2 className="text-lg font-semibold text-text-primary">Pending Leave Approvals</h2>
{/* Search Input */}
<div className="relative w-full md:w-64">
<Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-light" />
<input
type="text"
placeholder="Search name, type, status..."
value={searchQuery}
onChange={(e) => { setSearchQuery(e.target.value); setCurrentPage(1); }}
className="pl-9 pr-3 py-2 border border-app-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary text-text-secondary bg-app-card w-full"
/>
</div>
</div>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-app-border">
<thead>
<tr>
{(['employeeName', 'leaveType', 'applicationDate', 'leaveDays', 'status'] as SortKey[]).map((key) => (
<th
key={key}
onClick={() => requestSort(key)}
className="px-6 py-3 text-left text-xs font-semibold text-text-muted uppercase tracking-wider cursor-pointer hover:bg-app-muted transition-colors"
>
<div className="flex items-center">
{key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())} {getSortIcon(key)}
</div>
</th>
))}
<th className="px-6 py-3 text-left text-xs font-semibold text-text-muted uppercase tracking-wider">Action</th>
</tr>
</thead>
<tbody className="divide-y divide-app-border">
{paginatedData.length > 0 ? (
paginatedData.map((record) => (
<tr key={record.id} className="hover:bg-app-muted transition-colors">
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-primary">{record.employeeName}</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">{new Date(record.applicationDate + 'T00:00:00').toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' })}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-text-primary">{record.leaveDays}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm">
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusClass(record.status)}`}>{record.status}</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm">
<button onClick={() => setViewRecord(record)} className="text-primary hover:text-primary-hover flex items-center">
<Eye size={16} className="mr-1" /> View
</button>
</td>
</tr>
))
) : (
<tr>
<td colSpan={6} className="px-6 py-8 text-center text-sm text-text-muted">No matching leave requests found.</td>
</tr>
)}
</tbody>
</table>
</div>
{/* Footer: Pagination & Page Size */}
<div className="flex flex-col md:flex-row md:items-center justify-between mt-4 pt-4 border-t border-app-border gap-4">
<div className="flex items-center space-x-2">
<span className="text-sm text-text-muted">Show</span>
<select value={pageSize} onChange={handlePageSizeChange} className="px-2 py-1 border border-app-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary text-text-secondary bg-app-card">
<option value={10}>10</option>
<option value={15}>15</option>
<option value={20}>20</option>
</select>
<span className="text-sm text-text-muted">entries</span>
</div>
{isPaginationEnabled && totalPages > 1 && (
<div className="flex items-center space-x-1">
<span className="text-sm text-text-muted mr-2 hidden sm:inline">Page {currentPage} of {totalPages}</span>
<button onClick={() => setCurrentPage(1)} disabled={currentPage === 1} className="p-2 border border-app-border rounded-lg text-text-secondary hover:bg-app-muted disabled:opacity-50 disabled:cursor-not-allowed transition-colors"><ChevronsLeft size={16} /></button>
<button onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))} disabled={currentPage === 1} className="p-2 border border-app-border rounded-lg text-text-secondary hover:bg-app-muted disabled:opacity-50 disabled:cursor-not-allowed transition-colors"><ChevronLeft size={16} /></button>
{pageNumbers.map(num => (
<button key={num} onClick={() => setCurrentPage(num)} className={`w-8 h-8 flex items-center justify-center border rounded-lg text-sm transition-colors ${currentPage === num ? 'bg-primary text-white border-primary' : 'border-app-border text-text-secondary hover:bg-app-muted'}`}>{num}</button>
))}
<button onClick={() => setCurrentPage(prev => Math.min(prev + 1, totalPages))} disabled={currentPage === totalPages} className="p-2 border border-app-border rounded-lg text-text-secondary hover:bg-app-muted disabled:opacity-50 disabled:cursor-not-allowed transition-colors"><ChevronRight size={16} /></button>
<button onClick={() => setCurrentPage(totalPages)} disabled={currentPage === totalPages} className="p-2 border border-app-border rounded-lg text-text-secondary hover:bg-app-muted disabled:opacity-50 disabled:cursor-not-allowed transition-colors"><ChevronsRight size={16} /></button>
</div>
)}
</div>
{viewRecord && (
<ApprovalModal
record={viewRecord}
onClose={() => setViewRecord(null)}
onAction={(id, status) => {
onAction(id, status);
setViewRecord(null); // Close modal immediately on action
}}
/>
)}
</div>
);
}

View File

@ -1,7 +1,7 @@
import { useState, useMemo } from 'react';
import { toast } from 'sonner';
import { useApplyLeave } from '../api/useLeaveData';
import type { LeaveSummary, LeaveRequestPayload, DayMode } from '../types/leave';
import { useApplyLeave } from '../../api/useLeaveData';
import type { LeaveSummary, LeaveRequestPayload, DayMode } from '../../types/leave';
interface ApplyLeaveFormProps {
summaryData: LeaveSummary[];

View File

@ -1,7 +1,7 @@
import { useState, useMemo } from 'react';
import { ArrowUpDown, ArrowUp, ArrowDown, Calendar, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Eye, Ban } from 'lucide-react';
import { toast } from 'sonner';
import type { LeaveHistoryRecord, LeaveSortKey } from '../types/leave';
import type { LeaveHistoryRecord, LeaveSortKey } from '../../types/leave';
import ViewLeaveModal from './ViewLeaveModal';
import WithdrawLeaveModal from './WithdrawLeaveModal';

View File

@ -1,4 +1,4 @@
import type { LeaveSummary } from '../types/leave';
import type { LeaveSummary } from '../../types/leave';
interface LeaveSummaryTableProps {
data: LeaveSummary[];

View File

@ -1,5 +1,5 @@
import { X } from 'lucide-react';
import type { LeaveHistoryRecord } from '../types/leave';
import type { LeaveHistoryRecord } from '../../types/leave';
interface ViewLeaveModalProps {
record: LeaveHistoryRecord;

View File

@ -1,5 +1,5 @@
import { AlertTriangle } from 'lucide-react';
import type { LeaveHistoryRecord } from '../types/leave';
import type { LeaveHistoryRecord } from '../../types/leave';
interface WithdrawLeaveModalProps {
record: LeaveHistoryRecord;

View File

@ -0,0 +1,30 @@
import { toast } from 'sonner';
import Loader from '../../../../components/ui/Loader';
import ErrorState from '../../../../components/ui/ErrorState';
import ApprovalTable from '../../components/Approval/ApprovalTable';
import { useManagerApprovals, useUpdateLeaveApproval } from '../../api/useLeaveData';
export default function ManagerApproval() {
const { data: approvals, isLoading, isError } = useManagerApprovals();
const { mutate } = useUpdateLeaveApproval();
const handleAction = (id: string, status: 'Approved' | 'Rejected') => {
mutate({ id, status }, {
onSuccess: () => {
toast.success(`Leave request ${status.toLowerCase()} successfully.`);
},
onError: () => {
toast.error("Failed to update leave status. Please try again.");
}
});
};
if (isLoading) return <Loader message="Loading pending approvals..." />;
if (isError) return <ErrorState message="Failed to load pending approvals." />;
return (
<div>
{approvals && <ApprovalTable records={approvals} onAction={handleAction} />}
</div>
);
}

View File

@ -0,0 +1,29 @@
import Loader from '../../../../components/ui/Loader';
import ErrorState from '../../../../components/ui/ErrorState';
import LeaveSummaryTable from '../../components/Leave/LeaveSummaryTable';
import ApplyLeaveForm from '../../components/Leave/ApplyLeaveForm';
import LeaveHistoryTable from '../../components/Leave/LeaveHistoryTable';
import { useLeaveSummary, useLeaveHistory } from '../../api/useLeaveData';
export default function AdminLeave() {
const { data: summaryData, isLoading: summaryLoading, isError: summaryError } = useLeaveSummary();
const { data: historyData, isLoading: historyLoading, isError: historyError } = useLeaveHistory();
if (summaryLoading || historyLoading) return <Loader message="Loading leave data..." />;
if (summaryError || historyError) return <ErrorState message="Failed to load leave data." />;
return (
<div className="flex flex-col gap-6">
{/* Top Section: Summary & Apply Form */}
<div className="flex flex-col lg:flex-row gap-6">
{summaryData && <LeaveSummaryTable data={summaryData} />}
{summaryData && <ApplyLeaveForm summaryData={summaryData} />}
</div>
{/* Bottom Section: Leave History Table */}
{historyData && <LeaveHistoryTable records={historyData} />}
</div>
);
}

View File

@ -1,9 +1,9 @@
import Loader from '../../../components/ui/Loader';
import ErrorState from '../../../components/ui/ErrorState';
import LeaveSummaryTable from '../components/LeaveSummaryTable';
import ApplyLeaveForm from '../components/ApplyLeaveForm';
import LeaveHistoryTable from '../components/LeaveHistoryTable';
import { useLeaveSummary, useLeaveHistory } from '../api/useLeaveData';
import Loader from '../../../../components/ui/Loader';
import ErrorState from '../../../../components/ui/ErrorState';
import LeaveSummaryTable from '../../components/Leave/LeaveSummaryTable';
import ApplyLeaveForm from '../../components/Leave/ApplyLeaveForm';
import LeaveHistoryTable from '../../components/Leave/LeaveHistoryTable';
import { useLeaveSummary, useLeaveHistory } from '../../api/useLeaveData';
export default function EmployeeLeave() {
const { data: summaryData, isLoading: summaryLoading, isError: summaryError } = useLeaveSummary();

View File

@ -0,0 +1,29 @@
import Loader from '../../../../components/ui/Loader';
import ErrorState from '../../../../components/ui/ErrorState';
import LeaveSummaryTable from '../../components/Leave/LeaveSummaryTable';
import ApplyLeaveForm from '../../components/Leave/ApplyLeaveForm';
import LeaveHistoryTable from '../../components/Leave/LeaveHistoryTable';
import { useLeaveSummary, useLeaveHistory } from '../../api/useLeaveData';
export default function ManagerLeave() {
const { data: summaryData, isLoading: summaryLoading, isError: summaryError } = useLeaveSummary();
const { data: historyData, isLoading: historyLoading, isError: historyError } = useLeaveHistory();
if (summaryLoading || historyLoading) return <Loader message="Loading leave data..." />;
if (summaryError || historyError) return <ErrorState message="Failed to load leave data." />;
return (
<div className="flex flex-col gap-6">
{/* Top Section: Summary & Apply Form */}
<div className="flex flex-col lg:flex-row gap-6">
{summaryData && <LeaveSummaryTable data={summaryData} />}
{summaryData && <ApplyLeaveForm summaryData={summaryData} />}
</div>
{/* Bottom Section: Leave History Table */}
{historyData && <LeaveHistoryTable records={historyData} />}
</div>
);
}

View File

@ -0,0 +1,29 @@
import Loader from '../../../../components/ui/Loader';
import ErrorState from '../../../../components/ui/ErrorState';
import LeaveSummaryTable from '../../components/Leave/LeaveSummaryTable';
import ApplyLeaveForm from '../../components/Leave/ApplyLeaveForm';
import LeaveHistoryTable from '../../components/Leave/LeaveHistoryTable';
import { useLeaveSummary, useLeaveHistory } from '../../api/useLeaveData';
export default function SuperAdminLeaveLeave() {
const { data: summaryData, isLoading: summaryLoading, isError: summaryError } = useLeaveSummary();
const { data: historyData, isLoading: historyLoading, isError: historyError } = useLeaveHistory();
if (summaryLoading || historyLoading) return <Loader message="Loading leave data..." />;
if (summaryError || historyError) return <ErrorState message="Failed to load leave data." />;
return (
<div className="flex flex-col gap-6">
{/* Top Section: Summary & Apply Form */}
<div className="flex flex-col lg:flex-row gap-6">
{summaryData && <LeaveSummaryTable data={summaryData} />}
{summaryData && <ApplyLeaveForm summaryData={summaryData} />}
</div>
{/* Bottom Section: Leave History Table */}
{historyData && <LeaveHistoryTable records={historyData} />}
</div>
);
}

View File

@ -1,7 +1,6 @@
import { useSelector } from 'react-redux';
import type { RootState } from '../../../store/store';
const ManagerApproval = () => <div className="bg-app-card p-6 rounded-lg shadow"><h2 className="text-2xl font-bold text-primary">Pending Approvals</h2><p className="mt-2 text-text-muted">Approve or reject team leave and regularization requests.</p></div>;
import ManagerApproval from '../pages/Approval/ManagerApproval';
export default function ApprovalRouter() {
const role = useSelector((state: RootState) => state.role.currentRole);

View File

@ -1,6 +1,9 @@
import { useSelector } from 'react-redux';
import type { RootState } from '../../../store/store';
import EmployeeLeave from '../pages/EmployeeLeave';
import EmployeeLeave from '../pages/Leave/EmployeeLeave';
import ManagerLeave from '../pages/Leave/ManagerLeave';
import AdminLeave from '../pages/Leave/AdminLeave';
import SuperAdminLeaveLeave from '../pages/Leave/SuperAdminLeave';
export default function LeaveRouter() {
const role = useSelector((state: RootState) => state.role.currentRole);
@ -8,12 +11,10 @@ export default function LeaveRouter() {
// Since the hooks dynamically fetch based on role,
// the EmployeeLeave component works perfectly for all roles!
switch (role) {
case 'employee':
case 'manager':
case 'admin':
case 'superadmin':
return <EmployeeLeave />;
default:
return <div>Unauthorized</div>;
case 'employee': return <EmployeeLeave />;
case 'manager': return <ManagerLeave />;
case 'admin': return <AdminLeave />;
case 'superadmin': return <SuperAdminLeaveLeave />;
default: return <div>Unauthorized</div>;
}
}

View File

@ -34,3 +34,20 @@ export interface LeaveHistoryRecord {
}
export type LeaveSortKey = keyof LeaveHistoryRecord;
// Add these to your existing types/leave.ts
export type ApprovalStatus = 'Pending' | 'Approved' | 'Rejected';
export interface ManagerLeaveApproval {
id: string;
employeeName: string;
leaveType: string;
applicationDate: string;
fromDate: string;
toDate: string;
leaveDays: number;
status: ApprovalStatus;
reason: string;
}