Compare commits
No commits in common. "31fb8bba85cab0cb6cb6ab2b6f84077d3f556c82" and "1433c241e539663faa3f5643b3cbf8bdde4904e4" have entirely different histories.
31fb8bba85
...
1433c241e5
22
Dockerfile
22
Dockerfile
@ -1,22 +0,0 @@
|
|||||||
# 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;"]
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
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
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
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
11
package-lock.json
generated
@ -18,7 +18,6 @@
|
|||||||
"react-dom": "^19.2.7",
|
"react-dom": "^19.2.7",
|
||||||
"react-redux": "^9.3.0",
|
"react-redux": "^9.3.0",
|
||||||
"react-router-dom": "^7.18.1",
|
"react-router-dom": "^7.18.1",
|
||||||
"react-spinners": "^0.17.0",
|
|
||||||
"sonner": "^2.0.7"
|
"sonner": "^2.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -2880,16 +2879,6 @@
|
|||||||
"react-dom": ">=18"
|
"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": {
|
"node_modules/redux": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||||
|
|||||||
@ -20,7 +20,6 @@
|
|||||||
"react-dom": "^19.2.7",
|
"react-dom": "^19.2.7",
|
||||||
"react-redux": "^9.3.0",
|
"react-redux": "^9.3.0",
|
||||||
"react-router-dom": "^7.18.1",
|
"react-router-dom": "^7.18.1",
|
||||||
"react-spinners": "^0.17.0",
|
|
||||||
"sonner": "^2.0.7"
|
"sonner": "^2.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@ -12,11 +12,5 @@
|
|||||||
{ "date": "2026-07-17", "timeIn": "09:15 AM", "timeOut": "-", "loggedHours": "-", "status": "Mispunch" },
|
{ "date": "2026-07-17", "timeIn": "09:15 AM", "timeOut": "-", "loggedHours": "-", "status": "Mispunch" },
|
||||||
{ "date": "2026-07-20", "timeIn": "-", "timeOut": "-", "loggedHours": "-", "status": "Holiday" },
|
{ "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-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" }
|
|
||||||
]
|
]
|
||||||
@ -2,8 +2,8 @@ import { Provider } from 'react-redux';
|
|||||||
import { store } from './store/store';
|
import { store } from './store/store';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { Toaster } from 'sonner';
|
import { Toaster } from 'sonner';
|
||||||
import MainLayout from './components/layout/MainLayout';
|
import MainLayout from './components/MainLayout';
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router-dom'; // <-- Import BrowserRouter
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
@ -11,6 +11,7 @@ function App() {
|
|||||||
return (
|
return (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
|
{/* Wrap MainLayout in BrowserRouter */}
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<MainLayout />
|
<MainLayout />
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
|||||||
@ -1,10 +0,0 @@
|
|||||||
// 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>;
|
|
||||||
};
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { useState, useMemo } from 'react';
|
import { useState, useMemo } from 'react';
|
||||||
import { ArrowUpDown, ArrowUp, ArrowDown, Calendar, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';
|
import { ArrowUpDown, ArrowUp, ArrowDown, Calendar } from 'lucide-react';
|
||||||
import type { AttendanceRecord, SortKey } from '../types/attendance';
|
import type { AttendanceRecord, SortKey } from '../types/attendance';
|
||||||
|
|
||||||
type SortDirection = 'ascending' | 'descending';
|
type SortDirection = 'ascending' | 'descending';
|
||||||
@ -11,38 +11,6 @@ interface AttendanceRecordsTableProps {
|
|||||||
initialToDate?: string;
|
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({
|
export default function AttendanceRecordsTable({
|
||||||
records,
|
records,
|
||||||
title = "Attendance Records",
|
title = "Attendance Records",
|
||||||
@ -50,23 +18,19 @@ export default function AttendanceRecordsTable({
|
|||||||
initialToDate
|
initialToDate
|
||||||
}: AttendanceRecordsTableProps) {
|
}: AttendanceRecordsTableProps) {
|
||||||
|
|
||||||
// Default to TODAY'S DATE if no initial dates are provided
|
// Default to current month if no initial dates are provided
|
||||||
const todayStr = formatDate(new Date());
|
const currentYear = new Date().getFullYear();
|
||||||
const defaultFrom = initialFromDate || todayStr;
|
const currentMonth = new Date().getMonth();
|
||||||
const defaultTo = initialToDate || todayStr;
|
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];
|
||||||
|
|
||||||
const [fromDate, setFromDate] = useState(defaultFrom);
|
const [fromDate, setFromDate] = useState(defaultFrom);
|
||||||
const [toDate, setToDate] = useState(defaultTo);
|
const [toDate, setToDate] = useState(defaultTo);
|
||||||
|
|
||||||
const [sortConfig, setSortConfig] = useState<{ key: SortKey; direction: SortDirection }>({
|
const [sortConfig, setSortConfig] = useState<{ key: SortKey; direction: SortDirection }>({
|
||||||
key: 'date',
|
key: 'date',
|
||||||
direction: 'ascending'
|
direction: 'descending'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Pagination State
|
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
|
||||||
const [pageSize, setPageSize] = useState(5); // Default to 5 entries per page
|
|
||||||
|
|
||||||
const filteredData = useMemo(() => {
|
const filteredData = useMemo(() => {
|
||||||
return records.filter((record) => record.date >= fromDate && record.date <= toDate);
|
return records.filter((record) => record.date >= fromDate && record.date <= toDate);
|
||||||
}, [records, fromDate, toDate]);
|
}, [records, fromDate, toDate]);
|
||||||
@ -74,16 +38,6 @@ export default function AttendanceRecordsTable({
|
|||||||
const sortedData = useMemo(() => {
|
const sortedData = useMemo(() => {
|
||||||
const sortableData = [...filteredData];
|
const sortableData = [...filteredData];
|
||||||
sortableData.sort((a, b) => {
|
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;
|
||||||
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 0;
|
||||||
@ -91,58 +45,10 @@ export default function AttendanceRecordsTable({
|
|||||||
return sortableData;
|
return sortableData;
|
||||||
}, [filteredData, sortConfig]);
|
}, [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) => {
|
const requestSort = (key: SortKey) => {
|
||||||
let direction: SortDirection = 'ascending';
|
let direction: SortDirection = 'ascending';
|
||||||
if (sortConfig.key === key && sortConfig.direction === 'ascending') direction = 'descending';
|
if (sortConfig.key === key && sortConfig.direction === 'ascending') direction = 'descending';
|
||||||
setSortConfig({ key, direction });
|
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) => {
|
const getSortIcon = (key: SortKey) => {
|
||||||
@ -176,7 +82,7 @@ export default function AttendanceRecordsTable({
|
|||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
value={fromDate}
|
value={fromDate}
|
||||||
onChange={handleFromDateChange}
|
onChange={(e) => setFromDate(e.target.value)}
|
||||||
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"
|
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>
|
</div>
|
||||||
@ -186,7 +92,7 @@ export default function AttendanceRecordsTable({
|
|||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
value={toDate}
|
value={toDate}
|
||||||
onChange={handleToDateChange}
|
onChange={(e) => setToDate(e.target.value)}
|
||||||
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"
|
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>
|
</div>
|
||||||
@ -204,15 +110,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"
|
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">
|
<div className="flex items-center">
|
||||||
{key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())} {getSortIcon(key)}
|
{key.replace(/([A-Z])/g, ' $1').replace(/^./, (str: string) => str.toUpperCase())} {getSortIcon(key)}
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-app-border">
|
<tbody className="divide-y divide-app-border">
|
||||||
{paginatedData.length > 0 ? (
|
{sortedData.length > 0 ? (
|
||||||
paginatedData.map((record, index) => (
|
sortedData.map((record, index) => (
|
||||||
<tr key={index} className="hover:bg-app-muted transition-colors">
|
<tr key={index} className="hover:bg-app-muted transition-colors">
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-primary">
|
<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' })}
|
{new Date(record.date + 'T00:00:00').toLocaleDateString('en-US', { day: '2-digit', month: 'short', year: 'numeric' })}
|
||||||
@ -237,74 +143,6 @@ export default function AttendanceRecordsTable({
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
20
src/components/ErrorState.tsx
Normal file
20
src/components/ErrorState.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
22
src/components/Loader.tsx
Normal file
22
src/components/Loader.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,37 +1,25 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Routes, Route, Navigate } from 'react-router-dom';
|
import { Routes, Route, Navigate } from 'react-router-dom';
|
||||||
import { useIsFetching } from '@tanstack/react-query';
|
|
||||||
import Sidebar from './Sidebar';
|
import Sidebar from './Sidebar';
|
||||||
import Navbar from './Navbar';
|
import Navbar from './Navbar';
|
||||||
import DashboardRouter from '../../features/routes/DashboardRouter';
|
import DashboardRouter from '../features/dashboard/DashboardRouter';
|
||||||
import AttendanceRouter from '../../features/routes/AttendanceRouter';
|
import AttendanceRouter from '../features/attendance/AttendanceRouter';
|
||||||
import LeaveRouter from '../../features/routes/LeaveRouter';
|
import LeaveRouter from '../features/leave/LeaveRouter';
|
||||||
import ApprovalRouter from '../../features/routes/ApprovalRouter';
|
import ApprovalRouter from '../features/approval/ApprovalRouter';
|
||||||
import AttendanceSummaryRouter from '../../features/routes/AttendanceSummaryRouter';
|
import AttendanceSummaryRouter from '../features/summary/AttendanceSummaryRouter';
|
||||||
import LeaveSummaryRouter from '../../features/routes/LeaveSummaryRouter';
|
import LeaveSummaryRouter from '../features/summary/LeaveSummaryRouter';
|
||||||
import ManageEmployeesRouter from '../../features/routes/ManageEmployeesRouter';
|
import ManageEmployeesRouter from '../features/manage/ManageEmployeesRouter';
|
||||||
import ManageAdminsRouter from '../../features/routes/ManageAdminsRouter';
|
import ManageAdminsRouter from '../features/manage/ManageAdminsRouter';
|
||||||
import PolicyRouter from '../../features/routes/PolicyRouter';
|
import PolicyRouter from '../features/policy/PolicyRouter';
|
||||||
|
|
||||||
export default function MainLayout() {
|
export default function MainLayout() {
|
||||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||||
|
|
||||||
// Returns the number of queries currently fetching data globally
|
|
||||||
const isFetching = useIsFetching();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen bg-app-bg overflow-hidden">
|
<div className="flex h-screen bg-app-bg overflow-hidden">
|
||||||
<Sidebar isCollapsed={isCollapsed} setIsCollapsed={setIsCollapsed} />
|
<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 />
|
<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">
|
<main className="flex-1 overflow-y-auto p-6">
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/dashboard" element={<DashboardRouter />} />
|
<Route path="/dashboard" element={<DashboardRouter />} />
|
||||||
@ -43,8 +31,6 @@ export default function MainLayout() {
|
|||||||
<Route path="/manage-employees" element={<ManageEmployeesRouter />} />
|
<Route path="/manage-employees" element={<ManageEmployeesRouter />} />
|
||||||
<Route path="/manage-admins" element={<ManageAdminsRouter />} />
|
<Route path="/manage-admins" element={<ManageAdminsRouter />} />
|
||||||
<Route path="/policies" element={<PolicyRouter />} />
|
<Route path="/policies" element={<PolicyRouter />} />
|
||||||
|
|
||||||
{/* Redirect to dashboard if no route matches */}
|
|
||||||
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</main>
|
</main>
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import type { RootState } from '../../store/store';
|
import type { RootState } from '../store/store';
|
||||||
import { setRole, type UserRole } from '../../store/roleSlice';
|
import { setRole, type UserRole } from '../store/roleSlice';
|
||||||
import { Search, UserCircle, ChevronDown, X, ArrowRight } from 'lucide-react';
|
import { Search, UserCircle, ChevronDown, X, ArrowRight } from 'lucide-react';
|
||||||
import { useState, useRef, useEffect } from 'react';
|
import { useState, useRef, useEffect } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
@ -163,8 +163,8 @@ export default function Navbar() {
|
|||||||
setDropdownOpen(false);
|
setDropdownOpen(false);
|
||||||
}}
|
}}
|
||||||
className={`block w-full text-left px-4 py-2 text-sm capitalize hover:bg-app-muted ${currentRole === role
|
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'
|
? 'bg-primary/10 text-primary font-medium'
|
||||||
: 'text-text-secondary'
|
: 'text-text-secondary'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{role}
|
{role}
|
||||||
@ -1,6 +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 { getSidebarItems } from '../../types/sidebarConfig';
|
import { getSidebarItems } from '../types/sidebarConfig';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
LayoutDashboard, CalendarCheck, Plane, CheckCircle, BarChart3, ClipboardList,
|
LayoutDashboard, CalendarCheck, Plane, CheckCircle, BarChart3, ClipboardList,
|
||||||
@ -59,8 +59,8 @@ export default function Sidebar({ isCollapsed, setIsCollapsed }: SidebarProps) {
|
|||||||
<Link
|
<Link
|
||||||
to={item.path}
|
to={item.path}
|
||||||
className={`flex items-center px-4 py-3 transition-colors duration-200 ${isCollapsed ? 'justify-center' : ''} ${isActive
|
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'
|
? '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'
|
: 'text-blue-100 hover:bg-sidebar-hover hover:text-white border-l-4 border-transparent'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{Icon && <Icon size={20} className="flex-shrink-0" />}
|
{Icon && <Icon size={20} className="flex-shrink-0" />}
|
||||||
@ -1,23 +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 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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
18
src/features/attendance/AttendanceRouter.tsx
Normal file
18
src/features/attendance/AttendanceRouter.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
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>;
|
||||||
|
}
|
||||||
|
}
|
||||||
78
src/features/attendance/EmployeeAttendance.tsx
Normal file
78
src/features/attendance/EmployeeAttendance.tsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
18
src/features/attendance/useAttendanceData.ts
Normal file
18
src/features/attendance/useAttendanceData.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
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();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -3,7 +3,7 @@ export default function AdminDashboard() {
|
|||||||
<div className="bg-app-card rounded-lg shadow-sm p-6">
|
<div className="bg-app-card rounded-lg shadow-sm p-6">
|
||||||
<h2 className="text-2xl font-bold text-primary">Admin / SuperAdmin Dashboard</h2>
|
<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>
|
<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="mt-6 grid grid-cols-4 gap-4">
|
||||||
<div className="p-4 border rounded-lg bg-app-muted">
|
<div className="p-4 border rounded-lg bg-app-muted">
|
||||||
<h3 className="font-semibold text-sm text-text-muted">Total Employees</h3>
|
<h3 className="font-semibold text-sm text-text-muted">Total Employees</h3>
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import type { RootState } from '../../store/store';
|
import type { RootState } from '../../store/store';
|
||||||
import EmployeeDashboard from '../roles/employee/EmployeeDashboard';
|
import EmployeeDashboard from './EmployeeDashboard';
|
||||||
import ManagerDashboard from '../roles/manager/ManagerDashboard';
|
import ManagerDashboard from './ManagerDashboard';
|
||||||
import AdminDashboard from '../roles/admin/AdminDashboard';
|
import AdminDashboard from './AdminDashboard';
|
||||||
|
|
||||||
export default function DashboardRouter() {
|
export default function DashboardRouter() {
|
||||||
const role = useSelector((state: RootState) => state.role.currentRole);
|
const role = useSelector((state: RootState) => state.role.currentRole);
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import type { RootState } from '../../../store/store';
|
import type { RootState } from '../../store/store';
|
||||||
|
|
||||||
export default function ManageAdminsRouter() {
|
export default function ManageAdminsRouter() {
|
||||||
const role = useSelector((state: RootState) => state.role.currentRole);
|
const role = useSelector((state: RootState) => state.role.currentRole);
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import type { RootState } from '../../../store/store';
|
import type { RootState } from '../../store/store';
|
||||||
|
|
||||||
export default function ManageEmployeesRouter() {
|
export default function ManageEmployeesRouter() {
|
||||||
const role = useSelector((state: RootState) => state.role.currentRole);
|
const role = useSelector((state: RootState) => state.role.currentRole);
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import type { RootState } from '../../../store/store';
|
import type { RootState } from '../../store/store';
|
||||||
|
|
||||||
export default function PolicyRouter() {
|
export default function PolicyRouter() {
|
||||||
const role = useSelector((state: RootState) => state.role.currentRole);
|
const role = useSelector((state: RootState) => state.role.currentRole);
|
||||||
@ -1,46 +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'; // <-- 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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { useSelector } from 'react-redux';
|
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>;
|
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>;
|
||||||
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { useSelector } from 'react-redux';
|
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>;
|
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>;
|
||||||
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
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;
|
|
||||||
Loading…
x
Reference in New Issue
Block a user