fixed blocking of dailyprogress by using pagination

This commit is contained in:
pramod.mahajan 2025-09-25 12:36:55 +05:30
parent 49eaf857ad
commit 57edd92dce
10 changed files with 569 additions and 29 deletions

View File

@ -0,0 +1,11 @@
import React from 'react'
const TaskReportFilterPanel = () => {
return (
<div>
</div>
)
}
export default TaskReportFilterPanel

View File

@ -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) => (
<div
key={refIndex}
tabIndex="0"
className="d-flex align-items-center avatar-group justify-content-center"
data-bs-toggle="popover"
data-bs-trigger="focus"
data-bs-placement="left"
data-bs-html="true"
data-bs-content={`
<div class="border border-secondary rounded custom-popover p-2 px-3">
${task.teamMembers
.map(
(m) => `
<div class="d-flex align-items-center gap-2 mb-2">
<div class="avatar avatar-xs">
<span class="avatar-initial rounded-circle bg-label-primary">
${m?.firstName?.charAt(0) || ""}${
m?.lastName?.charAt(0) || ""
}
</span>
</div>
<span>${m.firstName} ${m.lastName}</span>
</div>`
)
.join("")}
</div>
`}
>
{task.teamMembers.slice(0, 3).map((m) => (
<div
key={m.id}
className="avatar avatar-xs"
title={`${m.firstName} ${m.lastName}`}
>
<span className="avatar-initial rounded-circle bg-label-primary">
{m?.firstName.slice(0, 1)}
</span>
</div>
))}
{task.teamMembers.length > 3 && (
<div
className="avatar avatar-xs"
title={`${task.teamMembers.length - 3} more`}
>
<span className="avatar-initial rounded-circle bg-label-secondary">
+{task.teamMembers.length - 3}
</span>
</div>
)}
</div>
);
if (isLoading) return <TaskReportListSkeleton />;
if (isError) return <div>Loading....</div>;
return (
<div className="mt-2">
<table className="table">
<thead>
<tr>
<th className="text-start">Activity</th>
<th>Total Pending</th>
<th>
<span>
Reported/Planned{" "}
<HoverPopup
title="Reported and Planned Task"
content={<p>This shows the reported versus planned tasks for each activity. on that Date</p>}
>
<i className="bx bx-xs ms-1 bx-info-circle cursor-pointer"></i>
</HoverPopup>
</span>
</th>
<th>Assign Date</th>
<th>Team</th>
<th className="text-center">Actions</th>
</tr>
</thead>
<tbody>
{groupedTasks.length === 0 && (
<tr>
<td colSpan={6} className="text-center align-middle" style={{ height: "200px", borderBottom: "none" }}>
No reports available
</td>
</tr>
)}
{groupedTasks.map(({ date, tasks }) => (
<React.Fragment key={date}>
<tr className="table-row-header text-start">
<td colSpan={6}>
<strong>{formatUTCToLocalTime(date)}</strong>
</td>
</tr>
{tasks.map((task, idx) => (
<tr key={task.id || idx}>
<td className="flex-wrap text-start">
<div>
{task.workItem.activityMaster?.activityName || "No Activity Name"}
</div>
<div className="text-sm py-2">
{task.workItem.workArea?.floor?.building?.name} {" "}
{task.workItem.workArea?.floor?.floorName} {" "}
{task.workItem.workArea?.areaName}
</div>
</td>
<td>
{formatNumber(task.workItem.plannedWork)}
</td>
<td>{`${formatNumber(task.completedTask)} / ${formatNumber(task.plannedTask)}`}</td>
<td>{formatUTCToLocalTime(task.assignmentDate)}</td>
<td className="text-center">{renderTeamMembers(task, idx)}</td>
<td className="text-center">
<div className="d-flex justify-content-end gap-2">
{ReportTaskRights && !task.reportedDate && (
<button className="btn btn-xs btn-primary" onClick={() => openModal("report", task)}>
Report
</button>
)}
{ApprovedTaskRights && task.reportedDate && !task.approvedBy && (
<button
className="btn btn-xs btn-warning"
onClick={() => openModal("comments", { task, isActionAllow: true })}
>
QC
</button>
)}
<button
className="btn btn-xs btn-primary"
onClick={() => openModal("comments", { task, isActionAllow: false })}
>
Comment
</button>
</div>
</td>
</tr>
))}
</React.Fragment>
))}
</tbody>
</table>
{data?.data?.length > 0 && (
<Pagination
currentPage={currentPage}
totalPages={data.totalPages}
onPageChange={paginate}
/>
)}
</div>
);
};
export default TaskReportList;

View File

@ -0,0 +1,62 @@
const SkeletonLine = ({ height = 20, width = "100%", className = "" }) => (
<div
className={`skeleton mb-2 ${className}`}
style={{
height,
width,
}}
></div>
);
export const TaskReportListSkeleton = () => {
const skeletonRows = 8; // Number of placeholder rows
return (
<div>
<table className="table">
<thead>
<tr>
<th>Activity</th>
<th>Assigned</th>
<th>Completed</th>
<th>Assign On</th>
<th>Team</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{[...Array(skeletonRows)].map((_, idx) => (
<tr key={idx}>
<td>
<SkeletonLine height={16} width="70%" />
<SkeletonLine height={12} width="50%" />
</td>
<td>
<SkeletonLine height={16} width="60%" />
</td>
<td>
<SkeletonLine height={16} width="60%" />
</td>
<td>
<SkeletonLine height={16} width="80%" />
</td>
<td className="text-center">
<div className="d-flex justify-content-center gap-1">
{[...Array(3)].map((_, i) => (
<SkeletonLine key={i} height={24} width={24} className="rounded-circle" />
))}
</div>
</td>
<td>
<div className="d-flex justify-content-end gap-2">
<SkeletonLine height={24} width="60px" />
<SkeletonLine height={24} width="60px" />
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
};

View File

@ -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 (
<div
className="d-inline-block position-relative"
ref={triggerRef}
onMouseEnter={handleMouseEnter}
onClick={handleClick}
style={{ cursor: "pointer" }}
>
{children}
{visible && (
<div
ref={popupRef}
className="bg-white border rounded shadow-sm py-1 px-2 text-start text-lowercase"
style={{
position: "absolute",
top: "100%",
left: "50%",
transform: "translateX(-50%)",
zIndex: 1050,
minWidth: "220px",
marginTop: "8px",
}}
>
{title && <h6 className="mb-2">{title}</h6>}
<div>{content}</div>
</div>
)}
</div>
);
};
export default HoverPopup;

View File

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

View File

@ -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", <TaskReportFilterPanel />);
return () => {
setShowTrigger(false);
setOffcanvasContent("", null);
};
}, []);
return (
<div className="container-fluid">
<DailyProgrssContext.Provider value={contextObj}>
{modal.type === "report" && (
<GlobalModel isOpen size="md" closeModal={closeModal}>
<ReportTask report={modal.data} closeModal={closeModal} />
</GlobalModel>
)}
{modal.type === "comments" && (
<GlobalModel isOpen size="lg" closeModal={closeModal}>
<ReportTaskComments
commentsData={modal.data.task}
actionAllow={modal.data.isActionAllow}
handleCloseAction={(isSubTask) => {
if (isSubTask) openModal("subtask", modal.data.task);
else closeModal();
}}
closeModal={closeModal}
/>
</GlobalModel>
)}
{modal.type === "subtask" && (
<GlobalModel isOpen size="lg" closeModal={closeModal}>
<SubTask activity={modal.data} onClose={closeModal} />
</GlobalModel>
)}
<Breadcrumb
data={[
{ label: "Home", link: "/dashboard" },
{ label: "Daily Progress Report" },
]}
/>
<div className="card card-fullscreen p-5">
<div className="col-sm-4 col-md-3 col-12">
<select
className="form-select form-select-sm"
value={service}
onChange={(e) => setService(e.target.value)}
>
{isLoading ? (
<option>Loading...</option>
) : (
<>
<option value="" selected>
Select service
</option>
{data?.data?.map((service) => (
<option key={service.id} value={service.id}>
{service.name}
</option>
))}
</>
)}
</select>
</div>
<div>
<TaskReportList />
</div>
</div>
</DailyProgrssContext.Provider>
</div>
);
};
export default DailyProgrssReport;

View File

@ -23,7 +23,7 @@ const TenantSelectionPage = () => {
useEffect(() => {
if (localStorage.getItem("ctnt")) {
navigate("/dashboard");
chooseTenant(localStorage.getItem("ctnt"))
}
}, [navigate]);

View File

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

View File

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

View File

@ -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: <DirectoryPage /> },
{ path: "/inventory", element: <Inventory /> },
{ path: "/activities/attendance", element: <AttendancePage /> },
{ path: "/activities/records/:projectId?", element: <DailyTask /> },
{ path: "/activities/records/:projectId?", element: <DailyProgrssReport /> },
{ path: "/activities/task", element: <TaskPlannng /> },
{ path: "/activities/reports", element: <Reports /> },
{ path: "/gallary", element: <ImageGallary /> },