Compare commits

...

2 Commits

34 changed files with 444 additions and 199 deletions

22
Dockerfile Normal file
View File

@ -0,0 +1,22 @@
# Stage 1: Build the React application
FROM node:20-alpine as builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# Stage 2: Serve the app with Nginx
FROM nginx:alpine
# Remove the default Nginx static assets and config
RUN rm -rf /usr/share/nginx/html/* /etc/nginx/conf.d/default.conf
# Copy the build output from the builder stage
COPY --from=builder /app/dist /usr/share/nginx/html
# Copy the custom Nginx configuration file
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

20
docker-compose.yml Normal file
View File

@ -0,0 +1,20 @@
version: '3.8'
services:
hrms-frontend:
build:
context: .
dockerfile: Dockerfile
container_name: hrms-frontend
ports:
- "8080:80"
volumes:
# Volume mount the Nginx config file
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
restart: unless-stopped
networks:
- hrms-network
networks:
hrms-network:
driver: bridge

19
nginx/default.conf Normal file
View File

@ -0,0 +1,19 @@
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
# This is crucial for React Router to work on page refresh
try_files $uri $uri/ /index.html;
}
# Optional: Cache static assets for better performance
location ~* \.(?:css|js|jpg|jpeg|png|gif|ico|svg|woff|woff2)$ {
root /usr/share/nginx/html;
expires 1y;
add_header Cache-Control "public";
access_log off;
}
}

11
package-lock.json generated
View File

@ -18,6 +18,7 @@
"react-dom": "^19.2.7",
"react-redux": "^9.3.0",
"react-router-dom": "^7.18.1",
"react-spinners": "^0.17.0",
"sonner": "^2.0.7"
},
"devDependencies": {
@ -2879,6 +2880,16 @@
"react-dom": ">=18"
}
},
"node_modules/react-spinners": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.17.0.tgz",
"integrity": "sha512-L/8HTylaBmIWwQzIjMq+0vyaRXuoAevzWoD35wKpNTxxtYXWZp+xtgkfD7Y4WItuX0YvdxMPU79+7VhhmbmuTQ==",
"license": "MIT",
"peerDependencies": {
"react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/redux": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",

View File

@ -20,6 +20,7 @@
"react-dom": "^19.2.7",
"react-redux": "^9.3.0",
"react-router-dom": "^7.18.1",
"react-spinners": "^0.17.0",
"sonner": "^2.0.7"
},
"devDependencies": {

View File

@ -12,5 +12,11 @@
{ "date": "2026-07-17", "timeIn": "09:15 AM", "timeOut": "-", "loggedHours": "-", "status": "Mispunch" },
{ "date": "2026-07-20", "timeIn": "-", "timeOut": "-", "loggedHours": "-", "status": "Holiday" },
{ "date": "2026-07-21", "timeIn": "09:00 AM", "timeOut": "06:00 PM", "loggedHours": "9h 00m", "status": "Present" },
{ "date": "2026-07-26", "timeIn": "-", "timeOut": "-", "loggedHours": "-", "status": "Holiday" }
{ "date": "2026-07-26", "timeIn": "-", "timeOut": "-", "loggedHours": "-", "status": "Holiday" },
{ "date": "2026-07-27", "timeIn": "08:50 AM", "timeOut": "06:00 PM", "loggedHours": "9h 10m", "status": "WFH" },
{ "date": "2026-07-28", "timeIn": "08:50 AM", "timeOut": "06:00 PM", "loggedHours": "9h 10m", "status": "Present" },
{ "date": "2026-07-29","timeIn": "08:50 AM", "timeOut": "06:00 PM", "loggedHours": "9h 10m", "status": "Present" },
{ "date": "2026-07-30", "timeIn": "08:50 AM", "timeOut": "06:00 PM", "loggedHours": "9h 10m", "status": "Present" },
{ "date": "2026-07-31", "timeIn": "08:50 AM", "timeOut": "06:00 PM", "loggedHours": "9h 10m","status": "Present" },
{ "date": "2026-08-01", "timeIn": "08:50 AM", "timeOut": "06:00 PM", "loggedHours": "9h 10m", "status": "Present" }
]

View File

@ -2,8 +2,8 @@ import { Provider } from 'react-redux';
import { store } from './store/store';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Toaster } from 'sonner';
import MainLayout from './components/MainLayout';
import { BrowserRouter } from 'react-router-dom'; // <-- Import BrowserRouter
import MainLayout from './components/layout/MainLayout';
import { BrowserRouter } from 'react-router-dom';
const queryClient = new QueryClient();
@ -11,7 +11,6 @@ function App() {
return (
<Provider store={store}>
<QueryClientProvider client={queryClient}>
{/* Wrap MainLayout in BrowserRouter */}
<BrowserRouter>
<MainLayout />
</BrowserRouter>

10
src/api/client.ts Normal file
View File

@ -0,0 +1,10 @@
// Simulates network latency and API fetching from public/mockApi/
export const fetchMockData = async <T>(endpoint: string, delay = 500): Promise<T> => {
await new Promise((resolve) => setTimeout(resolve, delay));
const response = await fetch(`/mockApi/${endpoint}.json`);
if (!response.ok) {
throw new Error(`API Error: Failed to fetch ${endpoint}`);
}
return response.json() as Promise<T>;
};

View File

@ -1,5 +1,5 @@
import { useState, useMemo } from 'react';
import { ArrowUpDown, ArrowUp, ArrowDown, Calendar } from 'lucide-react';
import { ArrowUpDown, ArrowUp, ArrowDown, Calendar, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';
import type { AttendanceRecord, SortKey } from '../types/attendance';
type SortDirection = 'ascending' | 'descending';
@ -11,6 +11,38 @@ interface AttendanceRecordsTableProps {
initialToDate?: string;
}
// Helper to format date to YYYY-MM-DD safely without timezone shifts
const formatDate = (date: Date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
// Helper to calculate days difference between two dates
const getDaysDifference = (start: string, end: string) => {
const startDate = new Date(start + 'T00:00:00');
const endDate = new Date(end + 'T00:00:00');
const diffTime = Math.abs(endDate.getTime() - startDate.getTime());
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1; // +1 to include start date
return diffDays;
};
// Helper to define sort priority for statuses
const getStatusPriority = (status: string) => {
const priorities: Record<string, number> = {
'Present': 1,
'Absent': 2,
'Late': 3,
'Half Day': 4,
'WFH': 5,
'Leave': 6,
'Mispunch': 7,
'Holiday': 8
};
return priorities[status] || 99; // Unknown statuses go to the end
};
export default function AttendanceRecordsTable({
records,
title = "Attendance Records",
@ -18,19 +50,23 @@ export default function AttendanceRecordsTable({
initialToDate
}: AttendanceRecordsTableProps) {
// Default to current month if no initial dates are provided
const currentYear = new Date().getFullYear();
const currentMonth = new Date().getMonth();
const defaultFrom = initialFromDate || new Date(currentYear, currentMonth, 1).toISOString().split('T')[0];
const defaultTo = initialToDate || new Date(currentYear, currentMonth + 1, 0).toISOString().split('T')[0];
// Default to TODAY'S DATE if no initial dates are provided
const todayStr = formatDate(new Date());
const defaultFrom = initialFromDate || todayStr;
const defaultTo = initialToDate || todayStr;
const [fromDate, setFromDate] = useState(defaultFrom);
const [toDate, setToDate] = useState(defaultTo);
const [sortConfig, setSortConfig] = useState<{ key: SortKey; direction: SortDirection }>({
key: 'date',
direction: 'descending'
direction: 'ascending'
});
// Pagination State
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(5); // Default to 5 entries per page
const filteredData = useMemo(() => {
return records.filter((record) => record.date >= fromDate && record.date <= toDate);
}, [records, fromDate, toDate]);
@ -38,6 +74,16 @@ export default function AttendanceRecordsTable({
const sortedData = useMemo(() => {
const sortableData = [...filteredData];
sortableData.sort((a, b) => {
// Custom sorting logic for the 'status' column
if (sortConfig.key === 'status') {
const priorityA = getStatusPriority(a.status);
const priorityB = getStatusPriority(b.status);
if (priorityA < priorityB) return sortConfig.direction === 'ascending' ? -1 : 1;
if (priorityA > priorityB) return sortConfig.direction === 'ascending' ? 1 : -1;
return 0;
}
// Default sorting for other columns (dates, strings)
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;
@ -45,10 +91,58 @@ export default function AttendanceRecordsTable({
return sortableData;
}, [filteredData, sortConfig]);
// Calculate if pagination should be enabled
const daysDifference = getDaysDifference(fromDate, toDate);
// Enable pagination if range > 7 days OR if records exceed the selected page size
const isPaginationEnabled = daysDifference > 7 || sortedData.length > pageSize;
const totalPages = isPaginationEnabled ? Math.ceil(sortedData.length / pageSize) : 1;
// Slice data for current page if pagination is enabled
const paginatedData = useMemo(() => {
if (!isPaginationEnabled) return sortedData;
const startIndex = (currentPage - 1) * pageSize;
return sortedData.slice(startIndex, startIndex + pageSize);
}, [sortedData, currentPage, isPaginationEnabled, pageSize]);
// Calculate which page numbers to show (e.g., 1, 2, 3, 4, 5)
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); // Reset page on sort
};
const handleFromDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFromDate(e.target.value);
setCurrentPage(1); // Reset page on filter change
};
const handleToDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setToDate(e.target.value);
setCurrentPage(1); // Reset page on filter change
};
const handlePageSizeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setPageSize(Number(e.target.value));
setCurrentPage(1); // Reset to page 1 when page size changes
};
const getSortIcon = (key: SortKey) => {
@ -82,7 +176,7 @@ export default function AttendanceRecordsTable({
<input
type="date"
value={fromDate}
onChange={(e) => setFromDate(e.target.value)}
onChange={handleFromDateChange}
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"
/>
</div>
@ -92,7 +186,7 @@ export default function AttendanceRecordsTable({
<input
type="date"
value={toDate}
onChange={(e) => setToDate(e.target.value)}
onChange={handleToDateChange}
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"
/>
</div>
@ -110,15 +204,15 @@ export default function AttendanceRecordsTable({
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: string) => str.toUpperCase())} {getSortIcon(key)}
{key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())} {getSortIcon(key)}
</div>
</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-app-border">
{sortedData.length > 0 ? (
sortedData.map((record, index) => (
{paginatedData.length > 0 ? (
paginatedData.map((record, index) => (
<tr key={index} className="hover:bg-app-muted transition-colors">
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-primary">
{new Date(record.date + 'T00:00:00').toLocaleDateString('en-US', { day: '2-digit', month: 'short', year: 'numeric' })}
@ -143,6 +237,74 @@ export default function AttendanceRecordsTable({
</tbody>
</table>
</div>
{/* Footer: Entries per page & Pagination Controls */}
<div className="flex flex-col md:flex-row md:items-center justify-between mt-4 pt-4 border-t border-app-border gap-4">
{/* Show Entries Dropdown */}
<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={5}>5</option>
<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>
{/* Pagination Buttons */}
{isPaginationEnabled && totalPages > 1 && (
<div className="flex items-center space-x-1">
<span className="text-sm text-text-muted mr-2 hidden sm:inline">
Page {currentPage} of {totalPages}
</span>
<button
onClick={() => setCurrentPage(1)}
disabled={currentPage === 1}
className="p-2 border border-app-border rounded-lg text-text-secondary hover:bg-app-muted disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<ChevronsLeft size={16} />
</button>
<button
onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
disabled={currentPage === 1}
className="p-2 border border-app-border rounded-lg text-text-secondary hover:bg-app-muted disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<ChevronLeft size={16} />
</button>
{pageNumbers.map(num => (
<button
key={num}
onClick={() => setCurrentPage(num)}
className={`w-8 h-8 flex items-center justify-center border rounded-lg text-sm transition-colors ${currentPage === num
? 'bg-primary text-white border-primary'
: 'border-app-border text-text-secondary hover:bg-app-muted'
}`}
>
{num}
</button>
))}
<button
onClick={() => setCurrentPage(prev => Math.min(prev + 1, totalPages))}
disabled={currentPage === totalPages}
className="p-2 border border-app-border rounded-lg text-text-secondary hover:bg-app-muted disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<ChevronRight size={16} />
</button>
<button
onClick={() => setCurrentPage(totalPages)}
disabled={currentPage === totalPages}
className="p-2 border border-app-border rounded-lg text-text-secondary hover:bg-app-muted disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<ChevronsRight size={16} />
</button>
</div>
)}
</div>
</div>
);
}

View File

@ -1,20 +0,0 @@
import { AlertCircle } from 'lucide-react';
interface ErrorStateProps {
message?: string;
fullScreen?: boolean;
}
export default function ErrorState({
message = 'Something went wrong. Please try again later.',
fullScreen = false
}: ErrorStateProps) {
return (
<div className={`flex items-center justify-center bg-app-card rounded-lg shadow-sm text-absent-500 ${
fullScreen ? 'h-screen w-screen fixed top-0 left-0 z-50' : 'h-64 w-full'
}`}>
<AlertCircle size={24} />
<span className="ml-3">{message}</span>
</div>
);
}

View File

@ -1,22 +0,0 @@
import { Loader2 } from 'lucide-react';
interface LoaderProps {
message?: string;
size?: number;
fullScreen?: boolean;
}
export default function Loader({
message = 'Loading...',
size = 32,
fullScreen = false
}: LoaderProps) {
return (
<div className={`flex flex-col items-center justify-center bg-app-card rounded-lg shadow-sm ${
fullScreen ? 'h-screen w-screen fixed top-0 left-0 z-50' : 'h-64 w-full'
}`}>
<Loader2 className="animate-spin text-primary" size={size} />
{message && <span className="mt-3 text-text-muted">{message}</span>}
</div>
);
}

View File

@ -1,25 +1,37 @@
import { useState } from 'react';
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/dashboard/DashboardRouter';
import AttendanceRouter from '../features/attendance/AttendanceRouter';
import LeaveRouter from '../features/leave/LeaveRouter';
import ApprovalRouter from '../features/approval/ApprovalRouter';
import AttendanceSummaryRouter from '../features/summary/AttendanceSummaryRouter';
import LeaveSummaryRouter from '../features/summary/LeaveSummaryRouter';
import ManageEmployeesRouter from '../features/manage/ManageEmployeesRouter';
import ManageAdminsRouter from '../features/manage/ManageAdminsRouter';
import PolicyRouter from '../features/policy/PolicyRouter';
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';
export default function MainLayout() {
const [isCollapsed, setIsCollapsed] = useState(false);
// Returns the number of queries currently fetching data globally
const isFetching = useIsFetching();
return (
<div className="flex h-screen bg-app-bg overflow-hidden">
<Sidebar isCollapsed={isCollapsed} setIsCollapsed={setIsCollapsed} />
<div className="flex-1 flex flex-col">
{/* Added 'relative' and 'overflow-hidden' here to contain the loading bar */}
<div className="flex-1 flex flex-col relative overflow-hidden">
<Navbar />
{/* Global Top Loading Bar - now safely constrained to this container */}
{isFetching > 0 && (
<div className="absolute top-16 left-0 right-0 h-1 bg-primary z-50 animate-pulse" />
)}
<main className="flex-1 overflow-y-auto p-6">
<Routes>
<Route path="/dashboard" element={<DashboardRouter />} />
@ -31,6 +43,8 @@ export default function MainLayout() {
<Route path="/manage-employees" element={<ManageEmployeesRouter />} />
<Route path="/manage-admins" element={<ManageAdminsRouter />} />
<Route path="/policies" element={<PolicyRouter />} />
{/* Redirect to dashboard if no route matches */}
<Route path="*" element={<Navigate to="/dashboard" replace />} />
</Routes>
</main>

View File

@ -1,6 +1,6 @@
import { useDispatch, useSelector } from 'react-redux';
import type { RootState } from '../store/store';
import { setRole, type UserRole } from '../store/roleSlice';
import type { RootState } from '../../store/store';
import { setRole, type UserRole } from '../../store/roleSlice';
import { Search, UserCircle, ChevronDown, X, ArrowRight } from 'lucide-react';
import { useState, useRef, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
@ -163,8 +163,8 @@ export default function Navbar() {
setDropdownOpen(false);
}}
className={`block w-full text-left px-4 py-2 text-sm capitalize hover:bg-app-muted ${currentRole === role
? 'bg-primary/10 text-primary font-medium'
: 'text-text-secondary'
? 'bg-primary/10 text-primary font-medium'
: 'text-text-secondary'
}`}
>
{role}

View File

@ -1,6 +1,6 @@
import { useSelector } from 'react-redux';
import type { RootState } from '../store/store';
import { getSidebarItems } from '../types/sidebarConfig';
import type { RootState } from '../../store/store';
import { getSidebarItems } from '../../types/sidebarConfig';
import { Link, useLocation } from 'react-router-dom';
import {
LayoutDashboard, CalendarCheck, Plane, CheckCircle, BarChart3, ClipboardList,
@ -59,8 +59,8 @@ export default function Sidebar({ isCollapsed, setIsCollapsed }: SidebarProps) {
<Link
to={item.path}
className={`flex items-center px-4 py-3 transition-colors duration-200 ${isCollapsed ? 'justify-center' : ''} ${isActive
? 'bg-sidebar-active text-white border-l-4 border-action'
: 'text-blue-100 hover:bg-sidebar-hover hover:text-white border-l-4 border-transparent'
? 'bg-sidebar-active text-white border-l-4 border-action'
: 'text-blue-100 hover:bg-sidebar-hover hover:text-white border-l-4 border-transparent'
}`}
>
{Icon && <Icon size={20} className="flex-shrink-0" />}

View File

@ -0,0 +1,23 @@
import { AlertCircle } from 'lucide-react';
interface ErrorStateProps {
message?: string;
fullScreen?: boolean;
}
export default function ErrorState({
message = 'Something went wrong. Please try again later.',
fullScreen = false
}: ErrorStateProps) {
return (
<div className={`flex flex-col items-center justify-center bg-app-card rounded-xl shadow-md border border-app-border p-8 text-absent-600 ${
fullScreen ? 'h-screen w-screen fixed top-0 left-0 z-50' : 'h-64 w-full'
}`}>
{/* Icon wrapped in a circular background to look like a card illustration */}
<div className="p-3 bg-absent-100 rounded-full mb-4">
<AlertCircle size={32} className="text-absent-500" />
</div>
<span className="text-sm font-medium text-text-secondary text-center max-w-xs">{message}</span>
</div>
);
}

View File

@ -0,0 +1,28 @@
import { FadeLoader } from 'react-spinners';
interface LoaderProps {
message?: string;
color?: string;
fullScreen?: boolean;
}
export default function Loader({
message = 'Loading...',
color = '#0A66C2', // Default to your primary brand color
fullScreen = false
}: LoaderProps) {
return (
<div className={`flex flex-col items-center justify-center bg-app-card rounded-xl shadow-md border border-app-border p-8 ${
fullScreen ? 'h-screen w-screen fixed top-0 left-0 z-50' : 'h-64 w-full'
}`}>
<FadeLoader
color={color}
height={15}
width={5}
radius={2}
margin={2}
/>
{message && <span className="mt-6 text-sm font-medium text-text-muted">{message}</span>}
</div>
);
}

View File

@ -1,18 +0,0 @@
import { useSelector } from 'react-redux';
import type { RootState } from '../../store/store';
import EmployeeAttendance from './EmployeeAttendance';
const ManagerAttendance = () => <div className="bg-app-card p-6 rounded-lg shadow"><h2 className="text-2xl font-bold text-primary">Team Attendance</h2><p className="mt-2 text-text-muted">View your team's daily attendance logs.</p></div>;
const AdminAttendance = () => <div className="bg-app-card p-6 rounded-lg shadow"><h2 className="text-2xl font-bold text-primary">Branch Attendance</h2><p className="mt-2 text-text-muted">Raw attendance logs for all employees.</p></div>;
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>;
}
}

View File

@ -1,78 +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/Loader';
import ErrorState from '../../components/ErrorState';
import { useAttendanceData } from './useAttendanceData';
// Helper to get current system date in YYYY-MM-DD format
const getTodayDate = () => {
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, '0');
const day = String(today.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
export default function EmployeeAttendance() {
// Initialize with the current system date
const [selectedDate, setSelectedDate] = useState(getTodayDate());
// Fetch data using TanStack Query
const { data: attendanceRecords, isLoading, isError } = useAttendanceData();
// Map array data to dictionary for the Calendar
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]);
const selectedDayData = calendarData[selectedDate] || null;
// 1. Loading State
if (isLoading) {
return <Loader message="Loading attendance data..." />;
}
// 2. Error State
if (isError) {
return <ErrorState message="Failed to load attendance data. Please try again later." />;
}
// 3. Success State
return (
<div className="flex flex-col gap-6">
{/* Top Section: Calendar and Daily Summary */}
<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={selectedDayData} />
</div>
{/* Bottom Section: Reusable Attendance Records Table */}
{/* Removed hardcoded dates so it defaults to the current system month */}
{attendanceRecords && (
<AttendanceRecordsTable
records={attendanceRecords}
title="My Attendance Records"
/>
)}
</div>
);
}

View File

@ -1,18 +0,0 @@
import { useQuery } from '@tanstack/react-query';
import type { AttendanceRecord } from '../../types/attendance';
export const useAttendanceData = () => {
return useQuery<AttendanceRecord[]>({
queryKey: ['attendanceRecords'],
queryFn: async () => {
// Simulate network delay (500ms)
await new Promise((resolve) => setTimeout(resolve, 500));
const response = await fetch('/mockApi/attendance.json');
if (!response.ok) {
throw new Error('Failed to fetch attendance data');
}
return response.json();
},
});
};

View File

@ -3,7 +3,7 @@ export default function AdminDashboard() {
<div className="bg-app-card rounded-lg shadow-sm p-6">
<h2 className="text-2xl font-bold text-primary">Admin / SuperAdmin Dashboard</h2>
<p className="text-text-muted mt-2">Global branch analytics and summaries.</p>
<div className="mt-6 grid grid-cols-4 gap-4">
<div className="p-4 border rounded-lg bg-app-muted">
<h3 className="font-semibold text-sm text-text-muted">Total Employees</h3>

View File

@ -0,0 +1,46 @@
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 ErrorState from '../../../components/ui/ErrorState';
import { useAttendanceData } from './hooks/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 EmployeeAttendance() {
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]);
// Use the global Loader component here
if (isLoading) return <Loader message="Loading attendance data..." />;
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,10 @@
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'),
});
};

View File

@ -0,0 +1,17 @@
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>;
}
}

View File

@ -1,5 +1,5 @@
import { useSelector } from 'react-redux';
import type { RootState } from '../../store/store';
import type { RootState } from '../../../store/store';
const AdminAttendanceSummary = () => <div className="bg-app-card p-6 rounded-lg shadow"><h2 className="text-2xl font-bold text-primary">Attendance Summary Report</h2><p className="mt-2 text-text-muted">Generate branch-wise attendance reports.</p></div>;

View File

@ -1,8 +1,8 @@
import { useSelector } from 'react-redux';
import type { RootState } from '../../store/store';
import EmployeeDashboard from './EmployeeDashboard';
import ManagerDashboard from './ManagerDashboard';
import AdminDashboard from './AdminDashboard';
import EmployeeDashboard from '../roles/employee/EmployeeDashboard';
import ManagerDashboard from '../roles/manager/ManagerDashboard';
import AdminDashboard from '../roles/admin/AdminDashboard';
export default function DashboardRouter() {
const role = useSelector((state: RootState) => state.role.currentRole);

View File

@ -1,5 +1,5 @@
import { useSelector } from 'react-redux';
import type { RootState } from '../../store/store';
import type { RootState } from '../../../store/store';
const AdminLeaveSummary = () => <div className="bg-app-card p-6 rounded-lg shadow"><h2 className="text-2xl font-bold text-primary">Leave Summary Report</h2><p className="mt-2 text-text-muted">Analyze leave trends across the organization.</p></div>;

View File

@ -1,5 +1,5 @@
import { useSelector } from 'react-redux';
import type { RootState } from '../../store/store';
import type { RootState } from '../../../store/store';
export default function ManageAdminsRouter() {
const role = useSelector((state: RootState) => state.role.currentRole);

View File

@ -1,5 +1,5 @@
import { useSelector } from 'react-redux';
import type { RootState } from '../../store/store';
import type { RootState } from '../../../store/store';
export default function ManageEmployeesRouter() {
const role = useSelector((state: RootState) => state.role.currentRole);

View File

@ -1,5 +1,5 @@
import { useSelector } from 'react-redux';
import type { RootState } from '../../store/store';
import type { RootState } from '../../../store/store';
export default function PolicyRouter() {
const role = useSelector((state: RootState) => state.role.currentRole);

13
src/types/index.ts Normal file
View File

@ -0,0 +1,13 @@
export type UserRole = 'employee' | 'manager' | 'admin' | 'superadmin';
export type AttendanceStatus = 'Present' | 'WFH' | 'Absent' | 'Leave' | 'Mispunch' | 'Holiday' | 'Late' | 'Half Day';
export interface AttendanceRecord {
date: string;
timeIn: string;
timeOut: string;
loggedHours: string;
status: AttendanceStatus;
}
export type SortKey = keyof AttendanceRecord;