Compare commits

..

2 Commits

7 changed files with 172 additions and 146 deletions

View File

@ -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" }
] ]

View File

@ -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>

View File

@ -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>
); );
} }

View 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>
);
}

View File

@ -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 => {

View File

@ -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 => {

View File

@ -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 => {