Compare commits
3 Commits
303c7beffd
...
a24f0e9cbf
| Author | SHA1 | Date | |
|---|---|---|---|
| a24f0e9cbf | |||
| 592c0e24f9 | |||
| 31fb8bba85 |
6
public/mockApi/admin_attendance.json
Normal file
6
public/mockApi/admin_attendance.json
Normal file
@ -0,0 +1,6 @@
|
||||
[
|
||||
{ "date": "2026-07-01", "timeIn": "08:50 AM", "timeOut": "06:00 PM", "loggedHours": "9h 10m", "status": "Present" },
|
||||
{ "date": "2026-07-02", "timeIn": "09:00 AM", "timeOut": "06:00 PM", "loggedHours": "9h 00m", "status": "Present" },
|
||||
{ "date": "2026-07-07", "timeIn": "-", "timeOut": "-", "loggedHours": "-", "status": "Holiday" },
|
||||
{ "date": "2026-07-15", "timeIn": "-", "timeOut": "-", "loggedHours": "-", "status": "Absent" }
|
||||
]
|
||||
7
public/mockApi/manager_attendance.json
Normal file
7
public/mockApi/manager_attendance.json
Normal file
@ -0,0 +1,7 @@
|
||||
[
|
||||
{ "date": "2026-07-01", "timeIn": "09:05 AM", "timeOut": "06:15 PM", "loggedHours": "9h 10m", "status": "Present" },
|
||||
{ "date": "2026-07-02", "timeIn": "10:30 AM", "timeOut": "06:30 PM", "loggedHours": "8h 00m", "status": "Late" },
|
||||
{ "date": "2026-07-03", "timeIn": "-", "timeOut": "-", "loggedHours": "-", "status": "Leave" },
|
||||
{ "date": "2026-07-04", "timeIn": "09:00 AM", "timeOut": "05:05 PM", "loggedHours": "8h 05m", "status": "Present" },
|
||||
{ "date": "2026-07-08", "timeIn": "09:15 AM", "timeOut": "02:00 PM", "loggedHours": "4h 45m", "status": "Half Day" }
|
||||
]
|
||||
5
public/mockApi/superadmin_attendance.json
Normal file
5
public/mockApi/superadmin_attendance.json
Normal file
@ -0,0 +1,5 @@
|
||||
[
|
||||
{ "date": "2026-07-01", "timeIn": "09:30 AM", "timeOut": "06:30 PM", "loggedHours": "9h 00m", "status": "Late" },
|
||||
{ "date": "2026-07-03", "timeIn": "08:45 AM", "timeOut": "05:30 PM", "loggedHours": "8h 45m", "status": "WFH" },
|
||||
{ "date": "2026-07-10", "timeIn": "09:00 AM", "timeOut": "-", "loggedHours": "-", "status": "Mispunch" }
|
||||
]
|
||||
@ -7,4 +7,4 @@ export const fetchMockData = async <T>(endpoint: string, delay = 500): Promise<T
|
||||
throw new Error(`API Error: Failed to fetch ${endpoint}`);
|
||||
}
|
||||
return response.json() as Promise<T>;
|
||||
};
|
||||
};
|
||||
|
||||
@ -3,15 +3,15 @@ import { Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { useIsFetching } from '@tanstack/react-query';
|
||||
import Sidebar from './Sidebar';
|
||||
import Navbar from './Navbar';
|
||||
import DashboardRouter from '../../features/routes/DashboardRouter';
|
||||
import AttendanceRouter from '../../features/routes/AttendanceRouter';
|
||||
import LeaveRouter from '../../features/routes/LeaveRouter';
|
||||
import ApprovalRouter from '../../features/routes/ApprovalRouter';
|
||||
import AttendanceSummaryRouter from '../../features/routes/AttendanceSummaryRouter';
|
||||
import LeaveSummaryRouter from '../../features/routes/LeaveSummaryRouter';
|
||||
import ManageEmployeesRouter from '../../features/routes/ManageEmployeesRouter';
|
||||
import ManageAdminsRouter from '../../features/routes/ManageAdminsRouter';
|
||||
import PolicyRouter from '../../features/routes/PolicyRouter';
|
||||
import DashboardRouter from '../../features/dashboard/routes/DashboardRouter';
|
||||
import AttendanceRouter from '../../features/attendance/routes/AttendanceRouter';
|
||||
import LeaveRouter from '../../features/leave/routes/LeaveRouter';
|
||||
import ApprovalRouter from '../../features/leave/routes/ApprovalRouter';
|
||||
import AttendanceSummaryRouter from '../../features/attendance/routes/AttendanceSummaryRouter';
|
||||
import LeaveSummaryRouter from '../../features/leave/routes/LeaveSummaryRouter';
|
||||
import ManageEmployeesRouter from '../../features/dashboard/routes/ManageEmployeesRouter';
|
||||
import ManageAdminsRouter from '../../features/dashboard/routes/ManageAdminsRouter';
|
||||
import PolicyRouter from '../../features/dashboard/routes/PolicyRouter';
|
||||
|
||||
export default function MainLayout() {
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
|
||||
29
src/features/attendance/api/useAttendanceData.ts
Normal file
29
src/features/attendance/api/useAttendanceData.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { fetchMockData } from '../../../api/client';
|
||||
import type { RootState } from '../../../store/store';
|
||||
import type { UserRole } from '../../../store/roleSlice';
|
||||
import type { AttendanceRecord } from '../../../types/attendance';
|
||||
|
||||
// 1. Define endpoints per role (Easy to swap to real API URLs later)
|
||||
const ATTENDANCE_ENDPOINTS: Record<UserRole, string> = {
|
||||
employee: 'employee_attendance',
|
||||
manager: 'manager_attendance',
|
||||
admin: 'admin_attendance',
|
||||
superadmin: 'superadmin_attendance',
|
||||
|
||||
// Future real backend example:
|
||||
// employee: '/api/v1/attendance/me',
|
||||
// manager: '/api/v1/attendance/team',
|
||||
};
|
||||
|
||||
// 2. Create a single dynamic hook
|
||||
export const useAttendanceData = () => {
|
||||
const role = useSelector((state: RootState) => state.role.currentRole);
|
||||
|
||||
return useQuery<AttendanceRecord[]>({
|
||||
// Cache key includes the role so React Query caches them separately
|
||||
queryKey: ['attendanceRecords', role],
|
||||
queryFn: () => fetchMockData<AttendanceRecord[]>(ATTENDANCE_ENDPOINTS[role]),
|
||||
});
|
||||
};
|
||||
@ -1,6 +1,6 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { ArrowUpDown, ArrowUp, ArrowDown, Calendar, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';
|
||||
import type { AttendanceRecord, SortKey } from '../types/attendance';
|
||||
import type { AttendanceRecord, SortKey } from '../../../types/attendance';
|
||||
|
||||
type SortDirection = 'ascending' | 'descending';
|
||||
|
||||
44
src/features/attendance/pages/AdminAttendance.tsx
Normal file
44
src/features/attendance/pages/AdminAttendance.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
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(() => {
|
||||
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 Calendar from '../../../components/Calendar';
|
||||
import AttendanceRecordsTable from '../../../components/AttendanceRecordsTable';
|
||||
import DailySummary from '../../../components/DailySummary';
|
||||
import Loader from '../../../components/ui/Loader'; // <-- Changed from FadeLoader to Loader
|
||||
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 './hooks/useAttendanceData';
|
||||
import { useAttendanceData } from '../api/useAttendanceData';
|
||||
|
||||
const getTodayDate = () => {
|
||||
const today = new Date();
|
||||
44
src/features/attendance/pages/ManagerAttendance.tsx
Normal file
44
src/features/attendance/pages/ManagerAttendance.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
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(() => {
|
||||
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>
|
||||
);
|
||||
}
|
||||
44
src/features/attendance/pages/SuperAdminAttendance.tsx
Normal file
44
src/features/attendance/pages/SuperAdminAttendance.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
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(() => {
|
||||
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>
|
||||
);
|
||||
}
|
||||
18
src/features/attendance/routes/AttendanceRouter.tsx
Normal file
18
src/features/attendance/routes/AttendanceRouter.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
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';
|
||||
|
||||
export default function AttendanceRouter() {
|
||||
const role = useSelector((state: RootState) => state.role.currentRole);
|
||||
|
||||
switch (role) {
|
||||
case 'employee': return <EmployeeAttendance />;
|
||||
case 'manager': return <ManagerAttendance />;
|
||||
case 'admin': return <AdminAttendance />;
|
||||
case 'superadmin': return <SuperAdminAttendance />;
|
||||
default: return <div>Unauthorized</div>;
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,8 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import type { RootState } from '../../store/store';
|
||||
import EmployeeDashboard from '../roles/employee/EmployeeDashboard';
|
||||
import ManagerDashboard from '../roles/manager/ManagerDashboard';
|
||||
import AdminDashboard from '../roles/admin/AdminDashboard';
|
||||
import type { RootState } from '../../../store/store';
|
||||
import EmployeeDashboard from '../pages/EmployeeDashboard';
|
||||
import ManagerDashboard from '../pages/ManagerDashboard';
|
||||
import AdminDashboard from '../pages/AdminDashboard';
|
||||
|
||||
export default function DashboardRouter() {
|
||||
const role = useSelector((state: RootState) => state.role.currentRole);
|
||||
@ -1,5 +1,5 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import type { RootState } from '../../store/store';
|
||||
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>;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import type { RootState } from '../../store/store';
|
||||
import type { RootState } from '../../../store/store';
|
||||
|
||||
const EmployeeLeave = () => <div className="bg-app-card p-6 rounded-lg shadow"><h2 className="text-2xl font-bold text-primary">My Leaves</h2><p className="mt-2 text-text-muted">Apply for leave and view your leave history.</p></div>;
|
||||
const ManagerLeave = () => <div className="bg-app-card p-6 rounded-lg shadow"><h2 className="text-2xl font-bold text-primary">Team Leave Calendar</h2><p className="mt-2 text-text-muted">See who is on leave in your team.</p></div>;
|
||||
@ -11,7 +11,7 @@ export default function LeaveRouter() {
|
||||
switch (role) {
|
||||
case 'employee': return <EmployeeLeave />;
|
||||
case 'manager': return <ManagerLeave />;
|
||||
case 'admin':
|
||||
case 'admin': return <ManagerLeave />;
|
||||
case 'superadmin': return <AdminLeave />;
|
||||
default: return <div>Unauthorized</div>;
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { fetchMockData } from '../../../../api/client';
|
||||
import type { AttendanceRecord } from '../../../../types/attendance';
|
||||
|
||||
export const useAttendanceData = () => {
|
||||
return useQuery<AttendanceRecord[]>({
|
||||
queryKey: ['attendanceRecords', 'employee'],
|
||||
queryFn: () => fetchMockData<AttendanceRecord[]>('attendance'),
|
||||
});
|
||||
};
|
||||
@ -1,17 +0,0 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import type { RootState } from '../../store/store';
|
||||
import EmployeeAttendance from '../roles/employee/EmployeeAttendance';
|
||||
// import ManagerAttendance from '../../roles/manager/ManagerAttendance';
|
||||
// import AdminAttendance from '../roles/admin/AdminAttendance';
|
||||
|
||||
export default function AttendanceRouter() {
|
||||
const role = useSelector((state: RootState) => state.role.currentRole);
|
||||
|
||||
switch (role) {
|
||||
case 'employee': return <EmployeeAttendance />;
|
||||
// case 'manager': return <ManagerAttendance />;
|
||||
// case 'admin':
|
||||
// case 'superadmin': return <AdminAttendance />;
|
||||
default: return <div>Unauthorized</div>;
|
||||
}
|
||||
}
|
||||
@ -4,6 +4,7 @@ export type AttendanceStatus = 'Present' | 'WFH' | 'Absent' | 'Leave' | 'Mispunc
|
||||
|
||||
export interface AttendanceRecord {
|
||||
date: string;
|
||||
employeeName?: string; // Optional for employee view, required for admin/manager
|
||||
timeIn: string;
|
||||
timeOut: string;
|
||||
loggedHours: string;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user