Compare commits
2 Commits
7fec4c2308
...
aaaab5cda7
| Author | SHA1 | Date | |
|---|---|---|---|
| aaaab5cda7 | |||
| a4cb8a37d9 |
@ -3,5 +3,6 @@
|
|||||||
{ "date": "2026-07-02", "timeIn": "10:30 AM", "timeOut": "06:30 PM", "loggedHours": "8h 00m", "status": "Late" },
|
{ "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-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-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" }
|
{ "date": "2026-07-08", "timeIn": "09:15 AM", "timeOut": "02:00 PM", "loggedHours": "4h 45m", "status": "Half Day" },
|
||||||
|
{ "date": "2026-07-17", "timeIn": "09:15 AM", "timeOut": "-", "loggedHours": "-", "status": "Mispunch" }
|
||||||
]
|
]
|
||||||
@ -11,36 +11,22 @@ 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
|
// Updated to handle empty strings safely
|
||||||
const getDaysDifference = (start: string, end: string) => {
|
const getDaysDifference = (start: string, end: string) => {
|
||||||
|
if (!start || !end) return 0;
|
||||||
const startDate = new Date(start + 'T00:00:00');
|
const startDate = new Date(start + 'T00:00:00');
|
||||||
const endDate = new Date(end + 'T00:00:00');
|
const endDate = new Date(end + 'T00:00:00');
|
||||||
const diffTime = Math.abs(endDate.getTime() - startDate.getTime());
|
const diffTime = Math.abs(endDate.getTime() - startDate.getTime());
|
||||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1; // +1 to include start date
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1;
|
||||||
return diffDays;
|
return diffDays;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper to define sort priority for statuses
|
|
||||||
const getStatusPriority = (status: string) => {
|
const getStatusPriority = (status: string) => {
|
||||||
const priorities: Record<string, number> = {
|
const priorities: Record<string, number> = {
|
||||||
'Present': 1,
|
'Present': 1, 'Absent': 2, 'Late': 3, 'Half Day': 4, 'WFH': 5, 'Leave': 6, 'Mispunch': 7, 'Holiday': 8
|
||||||
'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
|
return priorities[status] || 99;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function AttendanceRecordsTable({
|
export default function AttendanceRecordsTable({
|
||||||
@ -49,32 +35,29 @@ export default function AttendanceRecordsTable({
|
|||||||
initialFromDate,
|
initialFromDate,
|
||||||
initialToDate
|
initialToDate
|
||||||
}: AttendanceRecordsTableProps) {
|
}: AttendanceRecordsTableProps) {
|
||||||
|
// Default to empty strings to show placeholder in date inputs
|
||||||
// Default to TODAY'S DATE if no initial dates are provided
|
const [fromDate, setFromDate] = useState(initialFromDate || '');
|
||||||
const todayStr = formatDate(new Date());
|
const [toDate, setToDate] = useState(initialToDate || '');
|
||||||
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 }>({
|
const [sortConfig, setSortConfig] = useState<{ key: SortKey; direction: SortDirection }>({
|
||||||
key: 'date',
|
key: 'date', direction: 'ascending'
|
||||||
direction: 'ascending'
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Pagination State
|
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [pageSize, setPageSize] = useState(5); // Default to 5 entries per page
|
const [pageSize, setPageSize] = useState(5);
|
||||||
|
|
||||||
|
// Filter safely by checking if dates exist
|
||||||
const filteredData = useMemo(() => {
|
const filteredData = useMemo(() => {
|
||||||
return records.filter((record) => record.date >= fromDate && record.date <= toDate);
|
return records.filter((record) => {
|
||||||
|
const isAfterFrom = fromDate ? record.date >= fromDate : true;
|
||||||
|
const isBeforeTo = toDate ? record.date <= toDate : true;
|
||||||
|
return isAfterFrom && isBeforeTo;
|
||||||
|
});
|
||||||
}, [records, fromDate, toDate]);
|
}, [records, fromDate, toDate]);
|
||||||
|
|
||||||
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') {
|
if (sortConfig.key === 'status') {
|
||||||
const priorityA = getStatusPriority(a.status);
|
const priorityA = getStatusPriority(a.status);
|
||||||
const priorityB = getStatusPriority(b.status);
|
const priorityB = getStatusPriority(b.status);
|
||||||
@ -82,8 +65,6 @@ export default function AttendanceRecordsTable({
|
|||||||
if (priorityA > priorityB) return sortConfig.direction === 'ascending' ? 1 : -1;
|
if (priorityA > priorityB) return sortConfig.direction === 'ascending' ? 1 : -1;
|
||||||
return 0;
|
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,35 +72,26 @@ export default function AttendanceRecordsTable({
|
|||||||
return sortableData;
|
return sortableData;
|
||||||
}, [filteredData, sortConfig]);
|
}, [filteredData, sortConfig]);
|
||||||
|
|
||||||
// Calculate if pagination should be enabled
|
|
||||||
const daysDifference = getDaysDifference(fromDate, toDate);
|
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 isPaginationEnabled = daysDifference > 7 || sortedData.length > pageSize;
|
||||||
|
|
||||||
const totalPages = isPaginationEnabled ? Math.ceil(sortedData.length / pageSize) : 1;
|
const totalPages = isPaginationEnabled ? Math.ceil(sortedData.length / pageSize) : 1;
|
||||||
|
|
||||||
// Slice data for current page if pagination is enabled
|
|
||||||
const paginatedData = useMemo(() => {
|
const paginatedData = useMemo(() => {
|
||||||
if (!isPaginationEnabled) return sortedData;
|
if (!isPaginationEnabled) return sortedData;
|
||||||
const startIndex = (currentPage - 1) * pageSize;
|
const startIndex = (currentPage - 1) * pageSize;
|
||||||
return sortedData.slice(startIndex, startIndex + pageSize);
|
return sortedData.slice(startIndex, startIndex + pageSize);
|
||||||
}, [sortedData, currentPage, isPaginationEnabled, pageSize]);
|
}, [sortedData, currentPage, isPaginationEnabled, pageSize]);
|
||||||
|
|
||||||
// Calculate which page numbers to show (e.g., 1, 2, 3, 4, 5)
|
|
||||||
const pageNumbers = useMemo(() => {
|
const pageNumbers = useMemo(() => {
|
||||||
const pages = [];
|
const pages = [];
|
||||||
const maxVisiblePages = 5;
|
const maxVisiblePages = 5;
|
||||||
let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2));
|
let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2));
|
||||||
let endPage = startPage + maxVisiblePages - 1;
|
let endPage = startPage + maxVisiblePages - 1;
|
||||||
|
|
||||||
if (endPage > totalPages) {
|
if (endPage > totalPages) {
|
||||||
endPage = totalPages;
|
endPage = totalPages;
|
||||||
startPage = Math.max(1, endPage - maxVisiblePages + 1);
|
startPage = Math.max(1, endPage - maxVisiblePages + 1);
|
||||||
}
|
}
|
||||||
|
for (let i = startPage; i <= endPage; i++) pages.push(i);
|
||||||
for (let i = startPage; i <= endPage; i++) {
|
|
||||||
pages.push(i);
|
|
||||||
}
|
|
||||||
return pages;
|
return pages;
|
||||||
}, [currentPage, totalPages]);
|
}, [currentPage, totalPages]);
|
||||||
|
|
||||||
@ -127,23 +99,12 @@ export default function AttendanceRecordsTable({
|
|||||||
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
|
setCurrentPage(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFromDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFromDateChange = (e: React.ChangeEvent<HTMLInputElement>) => { setFromDate(e.target.value); setCurrentPage(1); };
|
||||||
setFromDate(e.target.value);
|
const handleToDateChange = (e: React.ChangeEvent<HTMLInputElement>) => { setToDate(e.target.value); setCurrentPage(1); };
|
||||||
setCurrentPage(1); // Reset page on filter change
|
const handlePageSizeChange = (e: React.ChangeEvent<HTMLSelectElement>) => { setPageSize(Number(e.target.value)); setCurrentPage(1); };
|
||||||
};
|
|
||||||
|
|
||||||
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) => {
|
||||||
if (sortConfig.key !== key) return <ArrowUpDown size={14} className="ml-1 text-text-light" />;
|
if (sortConfig.key !== key) return <ArrowUpDown size={14} className="ml-1 text-text-light" />;
|
||||||
@ -169,26 +130,15 @@ export default function AttendanceRecordsTable({
|
|||||||
<div className="bg-app-card p-6 rounded-lg shadow-sm mt-6">
|
<div className="bg-app-card p-6 rounded-lg shadow-sm mt-6">
|
||||||
<div className="flex flex-col md:flex-row md:items-center justify-between mb-6 gap-4">
|
<div className="flex flex-col md:flex-row md:items-center justify-between mb-6 gap-4">
|
||||||
<h2 className="text-lg font-semibold text-text-primary">{title}</h2>
|
<h2 className="text-lg font-semibold text-text-primary">{title}</h2>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Calendar size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-light" />
|
<Calendar size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-light" />
|
||||||
<input
|
<input type="date" value={fromDate} 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" />
|
||||||
type="date"
|
|
||||||
value={fromDate}
|
|
||||||
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>
|
</div>
|
||||||
<span className="text-text-muted text-sm">to</span>
|
<span className="text-text-muted text-sm">to</span>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Calendar size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-light" />
|
<Calendar size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-light" />
|
||||||
<input
|
<input type="date" value={toDate} 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" />
|
||||||
type="date"
|
|
||||||
value={toDate}
|
|
||||||
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -198,11 +148,7 @@ export default function AttendanceRecordsTable({
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
{(['date', 'timeIn', 'timeOut', 'loggedHours', 'status'] as SortKey[]).map((key) => (
|
{(['date', 'timeIn', 'timeOut', 'loggedHours', 'status'] as SortKey[]).map((key) => (
|
||||||
<th
|
<th key={key} onClick={() => requestSort(key)} className="px-6 py-3 text-left text-xs font-semibold text-text-muted uppercase tracking-wider cursor-pointer hover:bg-app-muted transition-colors">
|
||||||
key={key}
|
|
||||||
onClick={() => requestSort(key)}
|
|
||||||
className="px-6 py-3 text-left text-xs font-semibold text-text-muted uppercase tracking-wider cursor-pointer hover:bg-app-muted transition-colors"
|
|
||||||
>
|
|
||||||
<div className="flex items-center">
|
<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 => str.toUpperCase())} {getSortIcon(key)}
|
||||||
</div>
|
</div>
|
||||||
@ -215,39 +161,30 @@ export default function AttendanceRecordsTable({
|
|||||||
paginatedData.map((record, index) => (
|
paginatedData.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' })}
|
{/* Table cells format to dd-mm-yyyy */}
|
||||||
|
{new Date(record.date + 'T00:00:00').toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' })}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.timeIn}</td>
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.timeIn}</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.timeOut}</td>
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.timeOut}</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-text-primary">{record.loggedHours}</td>
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-text-primary">{record.loggedHours}</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm">
|
<td className="px-6 py-4 whitespace-nowrap text-sm">
|
||||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusClass(record.status)}`}>
|
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusClass(record.status)}`}>{record.status}</span>
|
||||||
{record.status}
|
|
||||||
</span>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={5} className="px-6 py-8 text-center text-sm text-text-muted">
|
<td colSpan={5} className="px-6 py-8 text-center text-sm text-text-muted">No records found for the selected period.</td>
|
||||||
No records found for the selected period.
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
</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">
|
<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">
|
<div className="flex items-center space-x-2">
|
||||||
<span className="text-sm text-text-muted">Show</span>
|
<span className="text-sm text-text-muted">Show</span>
|
||||||
<select
|
<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">
|
||||||
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={5}>5</option>
|
||||||
<option value={10}>10</option>
|
<option value={10}>10</option>
|
||||||
<option value={15}>15</option>
|
<option value={15}>15</option>
|
||||||
@ -256,52 +193,16 @@ export default function AttendanceRecordsTable({
|
|||||||
<span className="text-sm text-text-muted">entries</span>
|
<span className="text-sm text-text-muted">entries</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Pagination Buttons */}
|
|
||||||
{isPaginationEnabled && totalPages > 1 && (
|
{isPaginationEnabled && totalPages > 1 && (
|
||||||
<div className="flex items-center space-x-1">
|
<div className="flex items-center space-x-1">
|
||||||
<span className="text-sm text-text-muted mr-2 hidden sm:inline">
|
<span className="text-sm text-text-muted mr-2 hidden sm:inline">Page {currentPage} of {totalPages}</span>
|
||||||
Page {currentPage} of {totalPages}
|
<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>
|
||||||
</span>
|
<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>
|
||||||
<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 => (
|
{pageNumbers.map(num => (
|
||||||
<button
|
<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>
|
||||||
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
|
<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>
|
||||||
onClick={() => setCurrentPage(prev => Math.min(prev + 1, totalPages))}
|
<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>
|
||||||
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>
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
import { LogIn, LogOut, Clock } from 'lucide-react';
|
import { useState } from 'react';
|
||||||
|
import { LogIn, LogOut, Clock, AlertTriangle } from 'lucide-react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import RegularizationModal from './RegularizationModal';
|
||||||
|
|
||||||
interface DailySummaryProps {
|
interface DailySummaryProps {
|
||||||
selectedDate: string;
|
selectedDate: string;
|
||||||
@ -11,8 +14,18 @@ interface DailySummaryProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function DailySummary({ selectedDate, data }: DailySummaryProps) {
|
export default function DailySummary({ selectedDate, data }: DailySummaryProps) {
|
||||||
// Fallback if no data exists for the selected date
|
|
||||||
const selectedData = data || { status: 'No Data', login: '-', logout: '-', hours: '-' };
|
const selectedData = data || { status: 'No Data', login: '-', logout: '-', hours: '-' };
|
||||||
|
const isMispunch = selectedData.status === 'Mispunch';
|
||||||
|
|
||||||
|
const [isApplied, setIsApplied] = useState(false);
|
||||||
|
const [showModal, setShowModal] = useState(false);
|
||||||
|
|
||||||
|
const handleApplyRegularization = (regData: { checkIn: string; checkOut: string; reason: string }) => {
|
||||||
|
console.log("Regularization Data:", { date: selectedDate, ...regData });
|
||||||
|
setIsApplied(true);
|
||||||
|
setShowModal(false);
|
||||||
|
toast.success("Regularization request submitted successfully.");
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full lg:w-96 bg-app-card p-6 rounded-lg shadow-sm h-fit">
|
<div className="w-full lg:w-96 bg-app-card p-6 rounded-lg shadow-sm h-fit">
|
||||||
@ -40,9 +53,7 @@ export default function DailySummary({ selectedDate, data }: DailySummaryProps)
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between p-4 bg-app-muted rounded-lg">
|
<div className="flex items-center justify-between p-4 bg-app-muted rounded-lg">
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<div className="p-2 bg-present-100 rounded-lg">
|
<div className="p-2 bg-present-100 rounded-lg"><LogIn size={20} className="text-present-600" /></div>
|
||||||
<LogIn size={20} className="text-present-600" />
|
|
||||||
</div>
|
|
||||||
<span className="text-sm font-medium text-text-secondary">Check-In</span>
|
<span className="text-sm font-medium text-text-secondary">Check-In</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm font-bold text-text-primary">{selectedData.login}</span>
|
<span className="text-sm font-bold text-text-primary">{selectedData.login}</span>
|
||||||
@ -50,9 +61,7 @@ export default function DailySummary({ selectedDate, data }: DailySummaryProps)
|
|||||||
|
|
||||||
<div className="flex items-center justify-between p-4 bg-app-muted rounded-lg">
|
<div className="flex items-center justify-between p-4 bg-app-muted rounded-lg">
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<div className="p-2 bg-absent-100 rounded-lg">
|
<div className="p-2 bg-absent-100 rounded-lg"><LogOut size={20} className="text-absent-600" /></div>
|
||||||
<LogOut size={20} className="text-absent-600" />
|
|
||||||
</div>
|
|
||||||
<span className="text-sm font-medium text-text-secondary">Check-Out</span>
|
<span className="text-sm font-medium text-text-secondary">Check-Out</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm font-bold text-text-primary">{selectedData.logout}</span>
|
<span className="text-sm font-bold text-text-primary">{selectedData.logout}</span>
|
||||||
@ -60,14 +69,44 @@ export default function DailySummary({ selectedDate, data }: DailySummaryProps)
|
|||||||
|
|
||||||
<div className="flex items-center justify-between p-4 bg-primary/5 rounded-lg border border-primary/20">
|
<div className="flex items-center justify-between p-4 bg-primary/5 rounded-lg border border-primary/20">
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<div className="p-2 bg-today-100 rounded-lg">
|
<div className="p-2 bg-today-100 rounded-lg"><Clock size={20} className="text-primary" /></div>
|
||||||
<Clock size={20} className="text-primary" />
|
|
||||||
</div>
|
|
||||||
<span className="text-sm font-medium text-text-secondary">Total Hours</span>
|
<span className="text-sm font-medium text-text-secondary">Total Hours</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-base font-extrabold text-primary">{selectedData.hours}</span>
|
<span className="text-base font-extrabold text-primary">{selectedData.hours}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Conditional Regularization Button */}
|
||||||
|
{isMispunch && (
|
||||||
|
<div className="mt-6">
|
||||||
|
{isApplied ? (
|
||||||
|
<button
|
||||||
|
disabled
|
||||||
|
className="w-full flex items-center justify-center px-4 py-2 bg-app-muted text-text-light rounded-lg cursor-not-allowed text-sm font-medium"
|
||||||
|
>
|
||||||
|
<AlertTriangle size={16} className="mr-2" />
|
||||||
|
Applied for Regularization
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={() => setShowModal(true)}
|
||||||
|
className="w-full flex items-center justify-center px-4 py-2 bg-action text-white rounded-lg hover:bg-action-hover transition-colors text-sm font-medium"
|
||||||
|
>
|
||||||
|
<AlertTriangle size={16} className="mr-2" />
|
||||||
|
Request Regularization
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Render Separated Modal */}
|
||||||
|
{showModal && (
|
||||||
|
<RegularizationModal
|
||||||
|
selectedDate={selectedDate}
|
||||||
|
onClose={() => setShowModal(false)}
|
||||||
|
onApply={handleApplyRegularization}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
82
src/features/attendance/components/RegularizationModal.tsx
Normal file
82
src/features/attendance/components/RegularizationModal.tsx
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { X } from 'lucide-react';
|
||||||
|
|
||||||
|
interface RegularizationModalProps {
|
||||||
|
selectedDate: string;
|
||||||
|
onClose: () => void;
|
||||||
|
onApply: (data: { checkIn: string; checkOut: string; reason: string }) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RegularizationModal({ selectedDate, onClose, onApply }: RegularizationModalProps) {
|
||||||
|
const [regData, setRegData] = useState({ checkIn: '', checkOut: '', reason: '' });
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||||
|
setRegData(prev => ({ ...prev, [e.target.name]: e.target.value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onApply(regData);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||||
|
<div className="bg-app-card rounded-lg shadow-xl w-full max-w-md p-6 relative">
|
||||||
|
<button onClick={onClose} className="absolute top-4 right-4 text-text-muted hover:text-text-primary">
|
||||||
|
<X size={24} />
|
||||||
|
</button>
|
||||||
|
<h3 className="text-xl font-bold text-text-primary mb-1">Request Regularization</h3>
|
||||||
|
<p className="text-sm text-text-muted mb-6">
|
||||||
|
For Date: {new Date(selectedDate + 'T00:00:00').toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' })}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-text-secondary mb-1">Check-In Time</label>
|
||||||
|
<input
|
||||||
|
type="time"
|
||||||
|
name="checkIn"
|
||||||
|
value={regData.checkIn}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
className="w-full px-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>
|
||||||
|
<label className="block text-sm font-medium text-text-secondary mb-1">Check-Out Time</label>
|
||||||
|
<input
|
||||||
|
type="time"
|
||||||
|
name="checkOut"
|
||||||
|
value={regData.checkOut}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
className="w-full px-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>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-text-secondary mb-1">Reason</label>
|
||||||
|
<textarea
|
||||||
|
name="reason"
|
||||||
|
value={regData.reason}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
rows={3}
|
||||||
|
className="w-full px-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 resize-none"
|
||||||
|
placeholder="Brief reason for mispunch..."
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end space-x-3 mt-6">
|
||||||
|
<button type="button" onClick={onClose} className="px-4 py-2 bg-app-muted text-text-primary rounded-lg hover:bg-app-border transition-colors text-sm font-medium">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button type="submit" className="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-hover transition-colors text-sm font-medium">
|
||||||
|
Apply
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -16,6 +16,7 @@ export default function AdminAttendance() {
|
|||||||
const { data: attendanceRecords, isLoading, isError } = useAttendanceData();
|
const { data: attendanceRecords, isLoading, isError } = useAttendanceData();
|
||||||
|
|
||||||
const calendarData = useMemo(() => {
|
const calendarData = useMemo(() => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const map: Record<string, any> = {};
|
const map: Record<string, any> = {};
|
||||||
if (attendanceRecords) {
|
if (attendanceRecords) {
|
||||||
attendanceRecords.forEach(record => {
|
attendanceRecords.forEach(record => {
|
||||||
|
|||||||
@ -16,6 +16,7 @@ export default function ManagerAttendance() {
|
|||||||
const { data: attendanceRecords, isLoading, isError } = useAttendanceData();
|
const { data: attendanceRecords, isLoading, isError } = useAttendanceData();
|
||||||
|
|
||||||
const calendarData = useMemo(() => {
|
const calendarData = useMemo(() => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const map: Record<string, any> = {};
|
const map: Record<string, any> = {};
|
||||||
if (attendanceRecords) {
|
if (attendanceRecords) {
|
||||||
attendanceRecords.forEach(record => {
|
attendanceRecords.forEach(record => {
|
||||||
|
|||||||
@ -16,6 +16,7 @@ export default function SuperAdminAttendance() {
|
|||||||
const { data: attendanceRecords, isLoading, isError } = useAttendanceData();
|
const { data: attendanceRecords, isLoading, isError } = useAttendanceData();
|
||||||
|
|
||||||
const calendarData = useMemo(() => {
|
const calendarData = useMemo(() => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const map: Record<string, any> = {};
|
const map: Record<string, any> = {};
|
||||||
if (attendanceRecords) {
|
if (attendanceRecords) {
|
||||||
attendanceRecords.forEach(record => {
|
attendanceRecords.forEach(record => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user