diff --git a/src/components/DailyProgressRport/TaskReportFilterPanel.jsx b/src/components/DailyProgressRport/TaskReportFilterPanel.jsx new file mode 100644 index 00000000..0216a7e7 --- /dev/null +++ b/src/components/DailyProgressRport/TaskReportFilterPanel.jsx @@ -0,0 +1,11 @@ +import React from 'react' + +const TaskReportFilterPanel = () => { + return ( +
+ +
+ ) +} + +export default TaskReportFilterPanel diff --git a/src/components/DailyProgressRport/TaskReportList.jsx b/src/components/DailyProgressRport/TaskReportList.jsx new file mode 100644 index 00000000..254b1ddf --- /dev/null +++ b/src/components/DailyProgressRport/TaskReportList.jsx @@ -0,0 +1,292 @@ +import React, { useState, useEffect, useMemo } from "react"; +import { useTaskList } from "../../hooks/useTasks"; +import { useSelectedProject } from "../../slices/apiDataManager"; +import { useProjectName } from "../../hooks/useProjects"; +import DailyProgrssReport, { + useDailyProgrssContext, +} from "../../pages/DailyProgressReport/DailyProgrssReport"; +import { useDispatch } from "react-redux"; +import { setProjectId } from "../../slices/localVariablesSlice"; +import { + APPROVE_TASK, + ASSIGN_REPORT_TASK, + ITEMS_PER_PAGE, +} from "../../utils/constants"; +import { formatNumber, formatUTCToLocalTime } from "../../utils/dateUtils"; +import { useHasUserPermission } from "../../hooks/useHasUserPermission"; +import Pagination from "../common/Pagination"; +import { TaskReportListSkeleton } from "./TaskRepprtListSkeleton"; +import HoverPopup from "../common/HoverPopup"; + +const TaskReportList = () => { + const [currentPage, setCurrentPage] = useState(1); + const [filters, setFilters] = useState({ + selectedBuilding: "", + selectedFloors: [], + selectedActivities: [], + }); + const dispatch = useDispatch(); + const ApprovedTaskRights = useHasUserPermission(APPROVE_TASK); + const ReportTaskRights = useHasUserPermission(ASSIGN_REPORT_TASK); + + const { service, openModal, closeModal } = useDailyProgrssContext(); + const selectedProject = useSelectedProject(); + const { projectNames } = useProjectName(); + + const { data, isLoading, isError, error } = useTaskList( + selectedProject, + ITEMS_PER_PAGE, + currentPage, + service + ); + +const ProgrssReportColumn = [ + { + key: "activity", + label: "Activity", + getValue: (task) => task.workItem.activityMaster?.activityName || "N/A", + align: "text-start", + }, + { + key: "assigned", + label: "Total Assigned", + getValue: (task) => task.plannedTask ?? "N/A", + align: "text-start", + }, + { + key: "completed", + label: "Completed", + getValue: (task) => task.completedTask ?? "N/A", + align: "text-start", + }, + { + key: "assignAt", + label: "Assign Date", + getValue: (task) => + task.assignmentDate ? formatUTCToLocalTime(task.assignmentDate) : "N/A", + align: "text-start", + }, + { + key: "team", + label: "Team", + getValue: (task) => + task.teamMembers?.map((m) => `${m.firstName} ${m.lastName}`).join(", ") || + "N/A", + align: "text-start", + }, + ]; + + const paginate = (page) => { + if (page >= 1 && page <= (data?.totalPages ?? 1)) { + setCurrentPage(page); + } + }; + + useEffect(() => { + if (!selectedProject && projectNames.length > 0) { + dispatch(setProjectId(projectNames[0].id)); + } + }, [selectedProject, projectNames, dispatch]); + + useEffect(() => { + setFilters({ + selectedBuilding: "", + selectedFloors: [], + selectedActivities: [], + }); + }, [selectedProject]); + + // Filter and Group wise data + + const filteredTasks = useMemo(() => { + if (!data?.data) return []; + return data?.data.filter((task) => { + const { selectedBuilding, selectedFloors, selectedActivities } = filters; + + if ( + selectedBuilding && + task?.workItem?.workArea?.floor?.building?.name !== selectedBuilding + ) + return false; + if ( + selectedFloors.length > 0 && + !selectedFloors.includes(task?.workItem?.workArea?.floor?.floorName) + ) + return false; + if ( + selectedActivities.length > 0 && + !selectedActivities.includes( + task?.workItem?.activityMaster?.activityName + ) + ) + return false; + + return true; + }); + }, [data?.data, filters, currentPage]); + + const groupedTasks = useMemo(() => { + const groups = {}; + filteredTasks.forEach((task) => { + const date = task.assignmentDate.split("T")[0]; + if (!groups[date]) groups[date] = []; + groups[date].push(task); + }); + return Object.keys(groups) + .sort((a, b) => new Date(b) - new Date(a)) + .map((date) => ({ date, tasks: groups[date] })); + }, [filteredTasks, paginate, currentPage, selectedProject]); + + const renderTeamMembers = (task, refIndex) => ( +
+ ${task.teamMembers + .map( + (m) => ` +
+
+ + ${m?.firstName?.charAt(0) || ""}${ + m?.lastName?.charAt(0) || "" + } + +
+ ${m.firstName} ${m.lastName} +
` + ) + .join("")} +
+ `} + > + {task.teamMembers.slice(0, 3).map((m) => ( +
+ + {m?.firstName.slice(0, 1)} + +
+ ))} + {task.teamMembers.length > 3 && ( +
+ + +{task.teamMembers.length - 3} + +
+ )} + + ); + + if (isLoading) return ; + if (isError) return
Loading....
; + return ( +
+ + + + + + + + + + + + + {groupedTasks.length === 0 && ( + + + + )} + + {groupedTasks.map(({ date, tasks }) => ( + + + + + {tasks.map((task, idx) => ( + + + + + + + + + ))} + + ))} + +
ActivityTotal Pending + + Reported/Planned{" "} + This shows the reported versus planned tasks for each activity. on that Date

} + > + +
+
+
Assign DateTeamActions
+ No reports available +
+ {formatUTCToLocalTime(date)} +
+
+ {task.workItem.activityMaster?.activityName || "No Activity Name"} +
+
+ {task.workItem.workArea?.floor?.building?.name} ›{" "} + {task.workItem.workArea?.floor?.floorName} ›{" "} + {task.workItem.workArea?.areaName} +
+
+ {formatNumber(task.workItem.plannedWork)} + {`${formatNumber(task.completedTask)} / ${formatNumber(task.plannedTask)}`}{formatUTCToLocalTime(task.assignmentDate)}{renderTeamMembers(task, idx)} +
+ {ReportTaskRights && !task.reportedDate && ( + + )} + {ApprovedTaskRights && task.reportedDate && !task.approvedBy && ( + + )} + +
+
+ {data?.data?.length > 0 && ( + + )} +
+ ); +}; + +export default TaskReportList; diff --git a/src/components/DailyProgressRport/TaskRepprtListSkeleton.jsx b/src/components/DailyProgressRport/TaskRepprtListSkeleton.jsx new file mode 100644 index 00000000..5580e924 --- /dev/null +++ b/src/components/DailyProgressRport/TaskRepprtListSkeleton.jsx @@ -0,0 +1,62 @@ + +const SkeletonLine = ({ height = 20, width = "100%", className = "" }) => ( +
+); +export const TaskReportListSkeleton = () => { + const skeletonRows = 8; // Number of placeholder rows + + return ( +
+ + + + + + + + + + + + + {[...Array(skeletonRows)].map((_, idx) => ( + + + + + + + + + ))} + +
ActivityAssignedCompletedAssign OnTeamActions
+ + + + + + + + + +
+ {[...Array(3)].map((_, i) => ( + + ))} +
+
+
+ + +
+
+
+ ); +}; diff --git a/src/components/common/HoverPopup.jsx b/src/components/common/HoverPopup.jsx new file mode 100644 index 00000000..ddf0a062 --- /dev/null +++ b/src/components/common/HoverPopup.jsx @@ -0,0 +1,65 @@ +import React, { useEffect, useRef, useState } from "react"; + +const HoverPopup = ({ title, content, children }) => { + const [visible, setVisible] = useState(false); + const triggerRef = useRef(null); + const popupRef = useRef(null); + + // Toggle on hover or click + const handleMouseEnter = () => setVisible(true); + const handleClick = () => setVisible((prev) => !prev); + + // Hide on outside click + useEffect(() => { + const handleDocumentClick = (e) => { + if ( + !popupRef.current?.contains(e.target) && + !triggerRef.current?.contains(e.target) + ) { + setVisible(false); + } + }; + + if (visible) { + document.addEventListener("click", handleDocumentClick); + } + + return () => { + document.removeEventListener("click", handleDocumentClick); + }; + }, [visible]); + + return ( +
+ {children} + + {visible && ( +
+ {title &&
{title}
} +
{content}
+
+ )} +
+ ); +}; + +export default HoverPopup; + diff --git a/src/hooks/useTasks.js b/src/hooks/useTasks.js index b793ced4..5073c115 100644 --- a/src/hooks/useTasks.js +++ b/src/hooks/useTasks.js @@ -8,30 +8,25 @@ import { useSelector } from "react-redux"; // ---------Query--------------------------------- -export const useTaskList = (projectId, dateFrom, toDate) => { - const enabled = !!projectId && !!dateFrom && !!toDate; +export const useTaskList = (projectId, pageSize, pageNumber, serviceId, filter) => { - const { - data: TaskList = [], - isLoading: loading, - error, - refetch, - } = useQuery({ - queryKey: ["taskList", projectId, dateFrom, toDate], + return useQuery({ + queryKey: ["taskList", projectId, pageSize, pageNumber, serviceId, filter], queryFn: async () => { const response = await TasksRepository.getTaskList( projectId, - dateFrom, - toDate + pageSize, + pageNumber, + serviceId, + filter ); return response.data; }, - enabled, + enabled:!!projectId }); - - return { TaskList, loading, error, refetch }; }; + export const useTaskById = (TaskId) => { const { data: Task = null, diff --git a/src/pages/DailyProgressReport/DailyProgrssReport.jsx b/src/pages/DailyProgressReport/DailyProgrssReport.jsx new file mode 100644 index 00000000..72b360b4 --- /dev/null +++ b/src/pages/DailyProgressReport/DailyProgrssReport.jsx @@ -0,0 +1,114 @@ +import React, { createContext, useContext, useEffect, useState } from "react"; +import Breadcrumb from "../../components/common/Breadcrumb"; +import { useServices } from "../../hooks/masterHook/useMaster"; +import TaskReportList from "../../components/DailyProgressRport/TaskReportList"; +import GlobalModel from "../../components/common/GlobalModel"; +import ReportTaskComments from "../../components/Activities/ReportTaskComments"; +import ReportTask from "../../components/Activities/ReportTask"; +import TaskReportFilterPanel from "../../components/DailyProgressRport/TaskReportFilterPanel"; +import { useFab } from "../../Context/FabContext"; + +const DailyProgrssContext = createContext(); +export const useDailyProgrssContext = () => { + const context = useContext(DailyProgrssContext); + if (!context) { + throw new Error( + "useDailyTaskContext must be used within a DailyTaskProvider" + ); + } + return context; +}; + +const DailyProgrssReport = () => { + const [service, setService] = useState(""); + + const { setOffcanvasContent, setShowTrigger } = useFab(); + const { data, isLoading, isError, error } = useServices(); + + const [modal, setModal] = useState({ type: null, data: null }); + + const openModal = (type, data = null) => setModal({ type, data }); + const closeModal = () => setModal({ type: null, data: null }); + + const contextObj = { + service, + openModal, + closeModal, + }; + + useEffect(() => { + setShowTrigger(true); + setOffcanvasContent("Report Filter", ); + + return () => { + setShowTrigger(false); + setOffcanvasContent("", null); + }; + }, []); + return ( +
+ + {modal.type === "report" && ( + + + + )} + {modal.type === "comments" && ( + + { + if (isSubTask) openModal("subtask", modal.data.task); + else closeModal(); + }} + closeModal={closeModal} + /> + + )} + {modal.type === "subtask" && ( + + + + )} + + + +
+
+ +
+
+ +
+
+
+
+ ); +}; + +export default DailyProgrssReport; diff --git a/src/pages/authentication/TenantSelectionPage.jsx b/src/pages/authentication/TenantSelectionPage.jsx index 349c054c..22b72292 100644 --- a/src/pages/authentication/TenantSelectionPage.jsx +++ b/src/pages/authentication/TenantSelectionPage.jsx @@ -23,7 +23,7 @@ const TenantSelectionPage = () => { useEffect(() => { if (localStorage.getItem("ctnt")) { - navigate("/dashboard"); + chooseTenant(localStorage.getItem("ctnt")) } }, [navigate]); diff --git a/src/repositories/AuthRepository.jsx b/src/repositories/AuthRepository.jsx index cf3f6e31..700020fd 100644 --- a/src/repositories/AuthRepository.jsx +++ b/src/repositories/AuthRepository.jsx @@ -2,7 +2,7 @@ import { api } from "../utils/axiosClient"; const AuthRepository = { // Public routes (no auth token required) - login: (data) => api.postPublic("/api/auth/login/v1", data), + login: (data) => api.postPublic("/api/auth/login", data), refreshToken: (data) => api.postPublic("/api/auth/refresh-token", data), forgotPassword: (data) => api.postPublic("/api/auth/forgot-password", data), resetPassword: (data) => api.postPublic("/api/auth/reset-password", data), diff --git a/src/repositories/TaskRepository.jsx b/src/repositories/TaskRepository.jsx index 40ad8750..9afc47a0 100644 --- a/src/repositories/TaskRepository.jsx +++ b/src/repositories/TaskRepository.jsx @@ -1,25 +1,25 @@ import { api } from "../utils/axiosClient"; export const TasksRepository = { - getTaskList: (id, fromdate = null, todate = null) => { - let url = `api/task/list?projectId=${id}`; + getTaskList: (projectId, pageSize, pageNumber, serviceId, filter) => { + const payloadJsonString = JSON.stringify(filter); + let url = `api/task/list?projectId=${projectId}&pageSize=${pageSize}&pageNumber=${pageNumber}`; - if (fromdate) { - url += `&dateFrom=${fromdate}`; + if (serviceId) { + url += `&serviceId=${serviceId}`; } - - if (todate) { - url += `&dateTo=${todate}`; + if (filter && Object.keys(filter).length > 0) { + const payloadJsonString = encodeURIComponent(JSON.stringify(filter)); + url += `&filter=${payloadJsonString}`; } return api.get(url); }, - getTaskById:(id)=>api.get(`/api/task/get/${id}`), + getTaskById: (id) => api.get(`/api/task/get/${id}`), reportTask: (data) => api.post("api/task/report", data), - taskComments: ( data ) => api.post( "api/task/comment", data ), - auditTask: ( data ) => api.post( '/api/task/approve', data ), - - assignTask:(data) =>api.post('/api/task/assign',data) + taskComments: (data) => api.post("api/task/comment", data), + auditTask: (data) => api.post("/api/task/approve", data), + assignTask: (data) => api.post("/api/task/assign", data), }; diff --git a/src/router/AppRoutes.jsx b/src/router/AppRoutes.jsx index 70052cad..9c30e768 100644 --- a/src/router/AppRoutes.jsx +++ b/src/router/AppRoutes.jsx @@ -52,6 +52,7 @@ import CreateTenant from "../pages/Tenant/CreateTenant"; import OrganizationPage from "../pages/Organization/OrganizationPage"; import LandingPage from "../pages/Home/LandingPage"; import TenantSelectionPage from "../pages/authentication/TenantSelectionPage"; +import DailyProgrssReport from "../pages/DailyProgressReport/DailyProgrssReport"; const router = createBrowserRouter( [ { @@ -89,7 +90,7 @@ const router = createBrowserRouter( { path: "/directory", element: }, { path: "/inventory", element: }, { path: "/activities/attendance", element: }, - { path: "/activities/records/:projectId?", element: }, + { path: "/activities/records/:projectId?", element: }, { path: "/activities/task", element: }, { path: "/activities/reports", element: }, { path: "/gallary", element: },