diff --git a/index.html b/index.html index 748027e9..5bc1c55b 100644 --- a/index.html +++ b/index.html @@ -51,7 +51,7 @@ - + diff --git a/public/assets/js/main.js b/public/assets/js/main.js index 4ad0cfe9..cc356fa7 100644 --- a/public/assets/js/main.js +++ b/public/assets/js/main.js @@ -115,4 +115,38 @@ function Main () { // Auto update menu collapsed/expanded based on the themeConfig window.Helpers.setCollapsed(true, false); + + + + + // perfect scrolling + const verticalExample = document.getElementById('vertical-example'), + horizontalExample = document.getElementById('horizontal-example'), + horizVertExample = document.getElementById('both-scrollbars-example'); + + // Vertical Example + // -------------------------------------------------------------------- + if (verticalExample) { + new PerfectScrollbar(verticalExample, { + wheelPropagation: false + }); + } + + // Horizontal Example + // -------------------------------------------------------------------- + if (horizontalExample) { + new PerfectScrollbar(horizontalExample, { + wheelPropagation: false, + suppressScrollY: true + }); + } + + // Both vertical and Horizontal Example + // -------------------------------------------------------------------- + if (horizVertExample) { + new PerfectScrollbar(horizVertExample, { + wheelPropagation: false + }); + } + }; diff --git a/public/assets/vendor/css/core.css b/public/assets/vendor/css/core.css index d95f46c2..a9930ceb 100644 --- a/public/assets/vendor/css/core.css +++ b/public/assets/vendor/css/core.css @@ -454,7 +454,9 @@ table { caption-side: bottom; border-collapse: collapse; } - +.tr-group{ + background-color: var(--bs-body-bg); /* apply globale color for table row, where grouping datewise*/ +} caption { padding-top: 0.782rem; padding-bottom: 0.782rem; diff --git a/src/components/Activities/Attendance.jsx b/src/components/Activities/Attendance.jsx index 01ebc105..75979e7b 100644 --- a/src/components/Activities/Attendance.jsx +++ b/src/components/Activities/Attendance.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useCallback } from "react"; import moment from "moment"; import Avatar from "../common/Avatar"; import { convertShortTime } from "../../utils/dateUtils"; @@ -6,29 +6,41 @@ import RenderAttendanceStatus from "./RenderAttendanceStatus"; import usePagination from "../../hooks/usePagination"; import { useNavigate } from "react-router-dom"; import { ITEMS_PER_PAGE } from "../../utils/constants"; +import { useAttendance } from "../../hooks/useAttendance"; +import { useSelector } from "react-redux"; +import { useQueryClient } from "@tanstack/react-query"; +import eventBus from "../../services/eventBus"; -const Attendance = ({ - attendance, - getRole, - handleModalData, - setshowOnlyCheckout, - showOnlyCheckout, -}) => { +const Attendance = ({ getRole, handleModalData }) => { + const queryClient = useQueryClient(); const [loading, setLoading] = useState(false); const navigate = useNavigate(); const [todayDate, setTodayDate] = useState(new Date()); + const [ShowPending, setShowPending] = useState(false); + const selectedProject = useSelector( + (store) => store.localVariables.projectId + ); + const { + attendance, + loading: attLoading, + recall: attrecall, + isFetching + } = useAttendance(selectedProject); + const filteredAttendance = ShowPending + ? attendance?.filter( + (att) => att?.checkInTime !== null && att?.checkOutTime === null + ) + : attendance; - // Ensure attendance is an array - const attendanceList = Array.isArray(attendance) ? attendance : []; + const attendanceList = Array.isArray(filteredAttendance) + ? filteredAttendance + : []; - // Function to sort by first and last name const sortByName = (a, b) => { const nameA = (a.firstName + a.lastName).toLowerCase(); const nameB = (b.firstName + b.lastName).toLowerCase(); return nameA?.localeCompare(nameB); }; - - // Filter employees based on activity const group1 = attendanceList .filter((d) => d.activity === 1 || d.activity === 4) .sort(sortByName); @@ -37,30 +49,69 @@ const Attendance = ({ .sort(sortByName); const filteredData = [...group1, ...group2]; - const { currentPage, totalPages, currentItems, paginate } = usePagination( filteredData, ITEMS_PER_PAGE ); + + const handler = useCallback( + (msg) => { + if (selectedProject == msg.projectId) { + // const updatedAttendance = attendances.map((item) => + // item.employeeId === msg.response.employeeId + // ? { ...item, ...msg.response } + // : item + // ); + queryClient.setQueryData(["attendance", selectedProject], (oldData) => { + if (!oldData) { + queryClient.invalidateQueries({queryKey:["attendance"]}) + }; + return oldData.map((record) => + record.employeeId === msg.response.employeeId ? { ...record, ...msg.response } : record + ); + }); + } + }, + [selectedProject, attrecall] + ); + + const employeeHandler = useCallback( + (msg) => { + if (attendances.some((item) => item.employeeId == msg.employeeId)) { + attrecall(); + } + }, + [selectedProject, attendance] + ); + useEffect(() => { + eventBus.on("attendance", handler); + return () => eventBus.off("attendance", handler); + }, [handler]); + + useEffect(() => { + eventBus.on("employee", employeeHandler); + return () => eventBus.off("employee", employeeHandler); + }, [employeeHandler]); + return ( <> -
+
Date : {todayDate.toLocaleDateString("en-GB")} -
setshowOnlyCheckout(e.target.checked)} + disabled={isFetching} + checked={ShowPending} + onChange={(e) => setShowPending(e.target.checked)} />
- {attendance && attendance.length > 0 && ( + {Array.isArray(attendance) && attendance.length > 0 ? ( <> @@ -81,14 +132,13 @@ const Attendance = ({ {currentItems && currentItems .sort((a, b) => { - // If checkInTime exists, compare it, otherwise, treat null as earlier than a date const checkInA = a?.checkInTime ? new Date(a.checkInTime) : new Date(0); const checkInB = b?.checkInTime ? new Date(b.checkInTime) : new Date(0); - return checkInB - checkInA; // Sort in descending order of checkInTime + return checkInB - checkInA; }) .map((item) => ( @@ -138,7 +188,7 @@ const Attendance = ({ ))} {!attendance && ( - No employees assigned to the project + No employees assigned to the project! )}
@@ -189,6 +239,18 @@ const Attendance = ({ )} + ) : attLoading ? ( +
Loading...
+ ) : ( +
+ {Array.isArray(attendance) + ? "No employees assigned to the project" + : "Attendance data unavailable"} +
+ )} + + {currentItems?.length == 0 && attendance.length > 0 && ( +
No Pending Record Available !
)}
diff --git a/src/components/Activities/AttendcesLogs.jsx b/src/components/Activities/AttendcesLogs.jsx index 213897ed..76e01826 100644 --- a/src/components/Activities/AttendcesLogs.jsx +++ b/src/components/Activities/AttendcesLogs.jsx @@ -4,12 +4,14 @@ 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"; +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); @@ -28,7 +30,6 @@ const usePagination = (data, itemsPerPage) => { } }, [maxPage]); - // Ensure resetPage is returned by the hook const resetPage = useCallback(() => setCurrentPage(1), []); return { @@ -45,13 +46,28 @@ const AttendanceLog = ({ projectId, setshowOnlyCheckout, showOnlyCheckout, - searchQuery, // Prop for search query + searchQuery, }) => { - const [dateRange, setDateRange] = useState({ startDate: "", endDate: "" }); - const dispatch = useDispatch(); - const { data, loading, error } = useSelector((store) => store.attendanceLogs); - const [isRefreshing, setIsRefreshing] = useState(false); + 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); @@ -61,6 +77,7 @@ const AttendanceLog = ({ 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; }, []); @@ -84,6 +101,7 @@ const AttendanceLog = ({ return nameA.localeCompare(nameB); }, []); + // Effect to fetch attendance data when dateRange or projectId changes, or when refreshed useEffect(() => { const { startDate, endDate } = dateRange; dispatch( @@ -93,21 +111,26 @@ const AttendanceLog = ({ toDate: endDate, }) ); - setIsRefreshing(false); + // 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 - ? data.filter((item) => item.checkOutTime === null) - : data; + ? (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) => { - // Construct a full name from available parts, filtering out null/undefined const fullName = [att.firstName, att.middleName, att.lastName] - .filter(Boolean) // This removes null, undefined, or empty string parts + .filter(Boolean) .join(" ") .toLowerCase(); @@ -119,6 +142,7 @@ const AttendanceLog = ({ }); } + // 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); @@ -149,7 +173,10 @@ const AttendanceLog = ({ // Group by date const groupedByDate = sortedList.reduce((acc, item) => { - const date = (item.checkInTime || item.checkOutTime)?.split("T")[0]; + // 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); @@ -157,53 +184,62 @@ const AttendanceLog = ({ return acc; }, {}); - // Sort dates in descending order 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]); - }, [data, showOnlyCheckout, searchQuery, isSameDay, isBeforeToday, sortByName]); + }, [attendanceLogsData, showOnlyCheckout, searchQuery, isSameDay, isBeforeToday, sortByName]); const { currentPage, totalPages, currentItems: paginatedAttendances, paginate, - resetPage, // Destructure resetPage here + resetPage, } = usePagination(processedData, 20); - // Effect to reset pagination when search query changes + // Effect to reset pagination when search query or showOnlyCheckout changes useEffect(() => { resetPage(); - }, [searchQuery, resetPage]); // Add resetPage to dependencies + }, [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 checkIn = msg.response.checkInTime.substring(0, 10); + 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 && - startDate <= checkIn && - checkIn <= endDate + eventDate && + eventDate >= startDate && // Ensure eventDate is within the current range + eventDate <= endDate ) { - const updatedAttendance = data.map((item) => - item.id === msg.response.id - ? { ...item, ...msg.response } - : item + // Trigger a re-fetch of attendance data to get the latest state + dispatch( + fetchAttendanceData({ + projectId, + fromDate: startDate, + toDate: endDate, + }) ); - dispatch(setAttendanceData(updatedAttendance)); // Update Redux store } }, - [projectId, dateRange, data, dispatch] + [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; @@ -223,6 +259,11 @@ const AttendanceLog = ({ 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)} @@ -248,10 +290,10 @@ const AttendanceLog = ({
setIsRefreshing(true)} + onClick={handleRefreshClick} />
@@ -259,7 +301,12 @@ const AttendanceLog = ({ className="table-responsive text-nowrap" style={{ minHeight: "200px", display: 'flex' }} > - {processedData && processedData.length > 0 ? ( + {/* Conditional rendering for loading state */} + {(logsLoading || isRefreshing) ? ( +
+ Loading... +
+ ) : processedData && processedData.length > 0 ? ( @@ -278,96 +325,84 @@ const AttendanceLog = ({ - {(loading || isRefreshing) && ( - - - - )} - {!loading && - !isRefreshing && - 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; + {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( - - - - ); - } + if (!previousDate || currentDate !== previousDate) { acc.push( - - - - - - + ); - return acc; - }, [])} + } + acc.push( + + + + + + + + ); + return acc; + }, [])}
Loading...
- - {moment(currentDate).format("DD-MM-YYYY")} - -
- - - {moment( - attendance.checkInTime || attendance.checkOutTime - ).format("DD-MMM-YYYY")} - {convertShortTime(attendance.checkInTime)} - {attendance.checkOutTime - ? convertShortTime(attendance.checkOutTime) - : "--"} - - +
+ + {moment(currentDate).format("DD-MM-YYYY")} +
+ + + {moment( + attendance.checkInTime || attendance.checkOutTime + ).format("DD-MMM-YYYY")} + {convertShortTime(attendance.checkInTime)} + {attendance.checkOutTime + ? convertShortTime(attendance.checkOutTime) + : "--"} + + +
) : ( - !loading && - !isRefreshing && ( -
- No employee logs. -
- ) +
+ No employee logs. +
)}
- {!loading && !isRefreshing && processedData.length > 20 && ( + {!logsLoading && !isRefreshing && processedData.length > 20 && (