import React, { useEffect, useState, useMemo, useCallback } from "react"; import moment from "moment"; import Avatar from "../common/Avatar"; import { convertShortTime } from "../../utils/dateUtils"; import RenderAttendanceStatus from "./RenderAttendanceStatus"; import { useSelector, useDispatch } from "react-redux"; import { fetchAttendanceData, setAttendanceData } from "../../slices/apiSlice/attedanceLogsSlice"; // Make sure setAttendanceData is correctly imported import DateRangePicker from "../common/DateRangePicker"; import eventBus from "../../services/eventBus"; // Custom hook for pagination const usePagination = (data, itemsPerPage) => { const [currentPage, setCurrentPage] = useState(1); // Ensure data is an array before accessing length const totalItems = Array.isArray(data) ? data.length : 0; const maxPage = Math.ceil(totalItems / itemsPerPage); const currentItems = useMemo(() => { if (!Array.isArray(data) || data.length === 0) { return []; } const startIndex = (currentPage - 1) * itemsPerPage; const endIndex = startIndex + itemsPerPage; return data.slice(startIndex, endIndex); }, [data, currentPage, itemsPerPage]); const paginate = useCallback((pageNumber) => { if (pageNumber > 0 && pageNumber <= maxPage) { setCurrentPage(pageNumber); } }, [maxPage]); const resetPage = useCallback(() => setCurrentPage(1), []); return { currentPage, totalPages: maxPage, currentItems, paginate, resetPage, }; }; const AttendanceLog = ({ handleModalData, projectId, setshowOnlyCheckout, showOnlyCheckout, searchQuery, }) => { const selectedProject = useSelector( (store) => store.localVariables.projectId ); // Initialize date range with sensible defaults, e.g., last 7 days or current day const defaultEndDate = moment().format("YYYY-MM-DD"); const defaultStartDate = moment().subtract(6, 'days').format("YYYY-MM-DD"); // Last 7 days including today const [dateRange, setDateRange] = useState({ startDate: defaultStartDate, endDate: defaultEndDate }); const dispatch = useDispatch(); const { data: attendanceLogsData, loading: logsLoading, isFetching: logsFetching } = useSelector( (state) => state.attendanceLogs ); const [isRefreshing, setIsRefreshing] = useState(false); // Local state for refresh spinner // Memoize today and yesterday dates to prevent re-creation on every render const today = useMemo(() => { const d = new Date(); d.setHours(0, 0, 0, 0); return d; }, []); const yesterday = useMemo(() => { const d = new Date(); d.setDate(d.getDate() - 1); d.setHours(0, 0, 0, 0); // Set to start of day for accurate comparison return d; }, []); const isSameDay = useCallback((dateStr) => { if (!dateStr) return false; const d = new Date(dateStr); d.setHours(0, 0, 0, 0); return d.getTime() === today.getTime(); }, [today]); const isBeforeToday = useCallback((dateStr) => { if (!dateStr) return false; const d = new Date(dateStr); d.setHours(0, 0, 0, 0); return d.getTime() < today.getTime(); }, [today]); const sortByName = useCallback((a, b) => { const nameA = `${a.firstName || ""} ${a.lastName || ""}`.toLowerCase(); const nameB = `${b.firstName || ""} ${b.lastName || ""}`.toLowerCase(); return nameA.localeCompare(nameB); }, []); // Effect to fetch attendance data when dateRange or projectId changes, or when refreshed useEffect(() => { const { startDate, endDate } = dateRange; dispatch( fetchAttendanceData({ projectId, fromDate: startDate, toDate: endDate, }) ); // Reset refreshing state only after the dispatch, assuming fetchAttendanceData // will eventually update logsLoading/logsFetching const timer = setTimeout(() => { // Give Redux time to update setIsRefreshing(false); }, 500); // Small delay to show spinner for a bit longer return () => clearTimeout(timer); }, [dateRange, projectId, dispatch, isRefreshing]); const processedData = useMemo(() => { let filteredData = showOnlyCheckout ? (attendanceLogsData || []).filter((item) => item.checkOutTime === null) // Ensure attendanceLogsData is an array : (attendanceLogsData || []); // Ensure attendanceLogsData is an array // Apply search query filter if (searchQuery) { const lowerCaseSearchQuery = searchQuery.toLowerCase(); filteredData = filteredData.filter((att) => { const fullName = [att.firstName, att.middleName, att.lastName] .filter(Boolean) .join(" ") .toLowerCase(); return ( att.employeeName?.toLowerCase().includes(lowerCaseSearchQuery) || att.employeeId?.toLowerCase().includes(lowerCaseSearchQuery) || fullName.includes(lowerCaseSearchQuery) ); }); } // Grouping and sorting logic remains mostly the same, ensuring 'filteredData' is used const group1 = filteredData .filter((d) => d.activity === 1 && isSameDay(d.checkInTime)) .sort(sortByName); const group2 = filteredData .filter((d) => d.activity === 4 && isSameDay(d.checkOutTime)) .sort(sortByName); const group3 = filteredData .filter((d) => d.activity === 1 && isBeforeToday(d.checkInTime)) .sort(sortByName); const group4 = filteredData.filter( (d) => d.activity === 4 && isBeforeToday(d.checkOutTime) ); const group5 = filteredData .filter((d) => d.activity === 2 && isBeforeToday(d.checkOutTime)) .sort(sortByName); const group6 = filteredData .filter((d) => d.activity === 5) .sort(sortByName); const sortedList = [ ...group1, ...group2, ...group3, ...group4, ...group5, ...group6, ]; // Group by date const groupedByDate = sortedList.reduce((acc, item) => { // Use checkInTime for activity 1, and checkOutTime for others or if checkInTime is null const dateString = item.activity === 1 ? item.checkInTime : item.checkOutTime; const date = dateString ? moment(dateString).format("YYYY-MM-DD") : null; if (date) { acc[date] = acc[date] || []; acc[date].push(item); } return acc; }, {}); const sortedDates = Object.keys(groupedByDate).sort( (a, b) => new Date(b) - new Date(a) ); // Create the final sorted array return sortedDates.flatMap((date) => groupedByDate[date]); }, [attendanceLogsData, showOnlyCheckout, searchQuery, isSameDay, isBeforeToday, sortByName]); const { currentPage, totalPages, currentItems: paginatedAttendances, paginate, resetPage, } = usePagination(processedData, 20); // Effect to reset pagination when search query or showOnlyCheckout changes useEffect(() => { resetPage(); }, [searchQuery, showOnlyCheckout, resetPage]); // Handler for 'attendance_log' event from eventBus // This will now trigger a re-fetch of data const handler = useCallback( (msg) => { // Check if the event is relevant to the current project and date range const { startDate, endDate } = dateRange; const eventDate = (msg.response.checkInTime || msg.response.checkOutTime)?.substring(0, 10); // Only refetch if the event relates to the currently viewed project and date range if ( projectId === msg.projectId && eventDate && eventDate >= startDate && // Ensure eventDate is within the current range eventDate <= endDate ) { // Trigger a re-fetch of attendance data to get the latest state dispatch( fetchAttendanceData({ projectId, fromDate: startDate, toDate: endDate, }) ); } }, [projectId, dateRange, dispatch] ); useEffect(() => { eventBus.on("attendance_log", handler); return () => eventBus.off("attendance_log", handler); }, [handler]); // Handler for 'employee' event from eventBus (already triggers a refetch) const employeeHandler = useCallback( (msg) => { const { startDate, endDate } = dateRange; dispatch( fetchAttendanceData({ projectId, fromDate: startDate, toDate: endDate, }) ); }, [projectId, dateRange, dispatch] ); useEffect(() => { eventBus.on("employee", employeeHandler); return () => eventBus.off("employee", employeeHandler); }, [employeeHandler]); const handleRefreshClick = () => { setIsRefreshing(true); // Set refreshing state to true // The useEffect for fetching data will automatically trigger due to isRefreshing dependency }; return ( <>
setshowOnlyCheckout(e.target.checked)} />
{/* Conditional rendering for loading state */} {(logsLoading || isRefreshing) ? (
Loading...
) : processedData && processedData.length > 0 ? ( {paginatedAttendances.reduce((acc, attendance, index, arr) => { const currentDate = moment( attendance.checkInTime || attendance.checkOutTime ).format("YYYY-MM-DD"); const previousAttendance = arr[index - 1]; const previousDate = previousAttendance ? moment( previousAttendance.checkInTime || previousAttendance.checkOutTime ).format("YYYY-MM-DD") : null; if (!previousDate || currentDate !== previousDate) { acc.push( ); } acc.push( ); return acc; }, [])}
Name Date {" "} Check-In Check-Out Action
{moment(currentDate).format("DD-MM-YYYY")}
{moment( attendance.checkInTime || attendance.checkOutTime ).format("DD-MMM-YYYY")} {convertShortTime(attendance.checkInTime)} {attendance.checkOutTime ? convertShortTime(attendance.checkOutTime) : "--"}
) : (
No employee logs.
)}
{!logsLoading && !isRefreshing && processedData.length > 20 && ( )} ); }; export default AttendanceLog;