Compare commits
No commits in common. "f4b426cfeb9986198d9fae268d605e492cd64d24" and "aaaab5cda74878012e24fe3d241a5b282aef15dd" have entirely different histories.
f4b426cfeb
...
aaaab5cda7
@ -1,222 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"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."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,10 +1,10 @@
|
|||||||
import { useState, useMemo } from 'react';
|
import { useState, useMemo } from 'react';
|
||||||
import Calendar from '../../components/Calendar';
|
import Calendar from '../components/Calendar';
|
||||||
import AttendanceRecordsTable from '../../components/AttendanceRecordsTable';
|
import AttendanceRecordsTable from '../components/AttendanceRecordsTable';
|
||||||
import DailySummary from '../../components/DailySummary';
|
import DailySummary from '../components/DailySummary';
|
||||||
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 { useAttendanceData } from '../../api/useAttendanceData';
|
import { useAttendanceData } from '../api/useAttendanceData';
|
||||||
|
|
||||||
const getTodayDate = () => {
|
const getTodayDate = () => {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
@ -1,9 +1,9 @@
|
|||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import type { RootState } from '../../../store/store';
|
import type { RootState } from '../../../store/store';
|
||||||
import EmployeeAttendance from '../pages/Attendance/EmployeeAttendance';
|
import EmployeeAttendance from '../pages/EmployeeAttendance';
|
||||||
import ManagerAttendance from '../pages/Attendance/ManagerAttendance';
|
import ManagerAttendance from '../pages/ManagerAttendance';
|
||||||
import AdminAttendance from '../pages/Attendance/AdminAttendance';
|
import AdminAttendance from '../pages/AdminAttendance';
|
||||||
import SuperAdminAttendance from '../pages/Attendance/SuperAdminAttendance';
|
import SuperAdminAttendance from '../pages/SuperAdminAttendance';
|
||||||
|
|
||||||
export default function AttendanceRouter() {
|
export default function AttendanceRouter() {
|
||||||
const role = useSelector((state: RootState) => state.role.currentRole);
|
const role = useSelector((state: RootState) => state.role.currentRole);
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { useSelector } from 'react-redux';
|
|||||||
import { fetchMockData, postMockData } from '../../../api/client';
|
import { fetchMockData, postMockData } from '../../../api/client';
|
||||||
import type { RootState } from '../../../store/store';
|
import type { RootState } from '../../../store/store';
|
||||||
import type { UserRole } from '../../../store/roleSlice';
|
import type { UserRole } from '../../../store/roleSlice';
|
||||||
import type { LeaveSummary, LeaveRequestPayload, LeaveHistoryRecord, ManagerLeaveApproval, ApprovalStatus } from '../types/leave';
|
import type { LeaveSummary, LeaveRequestPayload, LeaveHistoryRecord } from '../types/leave';
|
||||||
|
|
||||||
const LEAVE_ENDPOINTS: Record<UserRole, { summary: string, history: string }> = {
|
const LEAVE_ENDPOINTS: Record<UserRole, { summary: string, history: string }> = {
|
||||||
employee: {
|
employee: {
|
||||||
@ -55,29 +55,3 @@ export const useLeaveHistory = () => {
|
|||||||
queryFn: () => fetchMockData<LeaveHistoryRecord[]>(LEAVE_ENDPOINTS[role].history),
|
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'] });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|||||||
@ -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[];
|
||||||
@ -1,93 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,197 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { useState, useMemo } from 'react';
|
import { useState, useMemo } from 'react';
|
||||||
import { ArrowUpDown, ArrowUp, ArrowDown, Calendar, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Eye, Ban } from 'lucide-react';
|
import { ArrowUpDown, ArrowUp, ArrowDown, Calendar, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Eye, Ban } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import type { LeaveHistoryRecord, LeaveSortKey } from '../../types/leave';
|
import type { LeaveHistoryRecord, LeaveSortKey } from '../types/leave';
|
||||||
import ViewLeaveModal from './ViewLeaveModal';
|
import ViewLeaveModal from './ViewLeaveModal';
|
||||||
import WithdrawLeaveModal from './WithdrawLeaveModal';
|
import WithdrawLeaveModal from './WithdrawLeaveModal';
|
||||||
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import type { LeaveSummary } from '../../types/leave';
|
import type { LeaveSummary } from '../types/leave';
|
||||||
|
|
||||||
interface LeaveSummaryTableProps {
|
interface LeaveSummaryTableProps {
|
||||||
data: LeaveSummary[];
|
data: LeaveSummary[];
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { X } from 'lucide-react';
|
import { X } from 'lucide-react';
|
||||||
import type { LeaveHistoryRecord } from '../../types/leave';
|
import type { LeaveHistoryRecord } from '../types/leave';
|
||||||
|
|
||||||
interface ViewLeaveModalProps {
|
interface ViewLeaveModalProps {
|
||||||
record: LeaveHistoryRecord;
|
record: LeaveHistoryRecord;
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { AlertTriangle } from 'lucide-react';
|
import { AlertTriangle } from 'lucide-react';
|
||||||
import type { LeaveHistoryRecord } from '../../types/leave';
|
import type { LeaveHistoryRecord } from '../types/leave';
|
||||||
|
|
||||||
interface WithdrawLeaveModalProps {
|
interface WithdrawLeaveModalProps {
|
||||||
record: LeaveHistoryRecord;
|
record: LeaveHistoryRecord;
|
||||||
@ -1,30 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,9 +1,9 @@
|
|||||||
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/LeaveSummaryTable';
|
import LeaveSummaryTable from '../components/LeaveSummaryTable';
|
||||||
import ApplyLeaveForm from '../../components/Leave/ApplyLeaveForm';
|
import ApplyLeaveForm from '../components/ApplyLeaveForm';
|
||||||
import LeaveHistoryTable from '../../components/Leave/LeaveHistoryTable';
|
import LeaveHistoryTable from '../components/LeaveHistoryTable';
|
||||||
import { useLeaveSummary, useLeaveHistory } from '../../api/useLeaveData';
|
import { useLeaveSummary, useLeaveHistory } from '../api/useLeaveData';
|
||||||
|
|
||||||
export default function EmployeeLeave() {
|
export default function EmployeeLeave() {
|
||||||
const { data: summaryData, isLoading: summaryLoading, isError: summaryError } = useLeaveSummary();
|
const { data: summaryData, isLoading: summaryLoading, isError: summaryError } = useLeaveSummary();
|
||||||
@ -1,29 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import type { RootState } from '../../../store/store';
|
import type { RootState } from '../../../store/store';
|
||||||
import ManagerApproval from '../pages/Approval/ManagerApproval';
|
|
||||||
|
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>;
|
||||||
|
|
||||||
export default function ApprovalRouter() {
|
export default function ApprovalRouter() {
|
||||||
const role = useSelector((state: RootState) => state.role.currentRole);
|
const role = useSelector((state: RootState) => state.role.currentRole);
|
||||||
|
|||||||
@ -1,9 +1,6 @@
|
|||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import type { RootState } from '../../../store/store';
|
import type { RootState } from '../../../store/store';
|
||||||
import EmployeeLeave from '../pages/Leave/EmployeeLeave';
|
import EmployeeLeave from '../pages/EmployeeLeave';
|
||||||
import ManagerLeave from '../pages/Leave/ManagerLeave';
|
|
||||||
import AdminLeave from '../pages/Leave/AdminLeave';
|
|
||||||
import SuperAdminLeaveLeave from '../pages/Leave/SuperAdminLeave';
|
|
||||||
|
|
||||||
export default function LeaveRouter() {
|
export default function LeaveRouter() {
|
||||||
const role = useSelector((state: RootState) => state.role.currentRole);
|
const role = useSelector((state: RootState) => state.role.currentRole);
|
||||||
@ -11,10 +8,12 @@ export default function LeaveRouter() {
|
|||||||
// Since the hooks dynamically fetch based on role,
|
// Since the hooks dynamically fetch based on role,
|
||||||
// the EmployeeLeave component works perfectly for all roles!
|
// the EmployeeLeave component works perfectly for all roles!
|
||||||
switch (role) {
|
switch (role) {
|
||||||
case 'employee': return <EmployeeLeave />;
|
case 'employee':
|
||||||
case 'manager': return <ManagerLeave />;
|
case 'manager':
|
||||||
case 'admin': return <AdminLeave />;
|
case 'admin':
|
||||||
case 'superadmin': return <SuperAdminLeaveLeave />;
|
case 'superadmin':
|
||||||
default: return <div>Unauthorized</div>;
|
return <EmployeeLeave />;
|
||||||
|
default:
|
||||||
|
return <div>Unauthorized</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -34,20 +34,3 @@ export interface LeaveHistoryRecord {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type LeaveSortKey = keyof 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user