Issues_July_2W: Project overview widgets #256

Merged
vikas.nale merged 17 commits from Issues_July_2W into main 2025-07-15 12:28:46 +00:00
6 changed files with 186 additions and 229 deletions
Showing only changes of commit 3368403188 - Show all commits

View File

@ -1,3 +1,4 @@
import getGreetingMessage from "../../utils/greetingHandler"; import getGreetingMessage from "../../utils/greetingHandler";
import { import {
cacheData, cacheData,
@ -26,6 +27,8 @@ const Header = () => {
const { data, loading } = useMaster(); const { data, loading } = useMaster();
const navigate = useNavigate(); const navigate = useNavigate();
const HasManageProjectPermission = useHasUserPermission(MANAGE_PROJECT); const HasManageProjectPermission = useHasUserPermission(MANAGE_PROJECT);
const isDashboard = location.pathname === "/dashboard";
// const isDirectoryPath = location.pathname === "/directory";
const getRole = (roles, joRoleId) => { const getRole = (roles, joRoleId) => {
if (!Array.isArray(roles)) return "User"; if (!Array.isArray(roles)) return "User";
@ -99,7 +102,7 @@ const Header = () => {
if ( if (
projectNames && projectNames &&
projectNames.length > 0 && projectNames.length > 0 &&
selectedProject === undefined && selectedProject === undefined &&
!getCachedData("hasReceived") !getCachedData("hasReceived")
) { ) {
dispatch(setProjectId(null)); // Set to null for "All Projects" dispatch(setProjectId(null)); // Set to null for "All Projects"
@ -108,7 +111,7 @@ const Header = () => {
/** Check if current page is project details page or directory page */ /** Check if current page is project details page or directory page */
const isProjectPath = /^\/projects\/[a-f0-9-]{36}$/.test(location.pathname); // const isProjectPath = /^\/projects\/[a-f0-9-]{36}$/.test(location.pathname);
const isDirectoryPath = /^\/directory$/.test(location.pathname); const isDirectoryPath = /^\/directory$/.test(location.pathname);
const handler = useCallback( const handler = useCallback(
@ -172,69 +175,61 @@ const Header = () => {
id="navbar-collapse" id="navbar-collapse"
> >
{/* Project Selection Dropdown */} {/* Project Selection Dropdown */}
{projectNames && ( // Ensure projectNames is not null before rendering dropdown {projectNames && !isDirectoryPath && (
<div className="align-items-center"> <div className="align-items-center">
{(!isProjectPath && !isDirectoryPath) && ( <i className="rounded-circle bx bx-building-house bx-sm-lg bx-md me-2"></i>
<> <div className="btn-group">
<i className="rounded-circle bx bx-building-house bx-sm-lg bx-md me-2"></i> <button
<div className="btn-group"> className={`btn btn-sm-sm btn-xl ${projectNames.length > 0 ? "dropdown-toggle" : ""
<button } px-1`}
className={`btn btn-sm-sm btn-xl ${ type="button"
projectNames.length > 0 ? "dropdown-toggle" : "" data-bs-toggle="dropdown"
} px-1`} aria-expanded="false"
type="button" >
data-bs-toggle="dropdown" {displayText}
aria-expanded="false" </button>
>
{displayText}
</button>
{/* Render dropdown menu only if there are projects or the "All Projects" option is relevant */} {projectNames.length > 0 && (
{projectNames.length > 0 && ( <ul
<ul className="dropdown-menu"
className="dropdown-menu" style={{ overflow: "auto", maxHeight: "300px" }}
style={{ overflow: "auto", maxHeight: "300px" }} >
> {/* Show "All Projects" only on dashboard */}
{/* "All Projects" option */} {isDashboard && (
<li> <li>
<button
className="dropdown-item"
onClick={() => dispatch(setProjectId(null))}
>
All Projects
</button>
</li>
)}
{[...projectNames]
.sort((a, b) => a?.name?.localeCompare(b.name))
.map((project) => (
<li key={project?.id}>
<button <button
className="dropdown-item" className="dropdown-item"
onClick={() => dispatch(setProjectId(null))} // Set projectId to null for "All Projects" onClick={() => dispatch(setProjectId(project?.id))}
> >
All Projects {project?.name}
{project?.shortName && (
<span className="text-primary fw-semibold ms-1">
({project?.shortName})
</span>
)}
</button> </button>
</li> </li>
{[...projectNames] ))}
.sort((a, b) => a?.name?.localeCompare(b.name)) </ul>
.map((project) => ( )}
<li key={project?.id}> </div>
<button
className="dropdown-item"
onClick={() =>
dispatch(setProjectId(project?.id))
}
>
{project?.name}{" "}
{project?.shortName ? (
<span className="text-primary fw-semibold ">
({project?.shortName})
</span>
) : (
""
)}
</button>
</li>
))}
</ul>
)}
</div>
</>
)}
</div> </div>
)} )}
{/* Display project name on project details or directory pages */} {/* Display project name on project details or directory pages */}
{isProjectPath && (<span className="fs-5 align-items-center"><i className="rounded-circle bx bx-building-house bx-sm-lg bx-md me-2"></i>{displayText}</span>)} {/* { (<span className="fs-5 align-items-center"><i className="rounded-circle bx bx-building-house bx-sm-lg bx-md me-2"></i>{displayText}</span>)} */}
{/* User Profile and Shortcuts */} {/* User Profile and Shortcuts */}
<ul className="navbar-nav flex-row align-items-center ms-md-auto"> <ul className="navbar-nav flex-row align-items-center ms-md-auto">

View File

@ -2,48 +2,53 @@ import React, { useEffect, useState } from "react";
import moment from "moment"; import moment from "moment";
import { getProjectStatusName } from "../../utils/projectStatus"; import { getProjectStatusName } from "../../utils/projectStatus";
import {useProjectDetails, useUpdateProject} from "../../hooks/useProjects"; import {useProjectDetails, useUpdateProject} from "../../hooks/useProjects";
import {useParams} from "react-router-dom"; import { useSelector } from "react-redux"; // Import useSelector
import {useHasUserPermission} from "../../hooks/useHasUserPermission"; import {useHasUserPermission} from "../../hooks/useHasUserPermission";
import {MANAGE_PROJECT} from "../../utils/constants"; import {MANAGE_PROJECT} from "../../utils/constants";
import GlobalModel from "../common/GlobalModel"; import GlobalModel from "../common/GlobalModel";
import ManageProjectInfo from "./ManageProjectInfo"; import ManageProjectInfo from "./ManageProjectInfo";
import {useQueryClient} from "@tanstack/react-query"; import {useQueryClient} from "@tanstack/react-query";
const AboutProject = () =>
{ const AboutProject = () => {
const [ IsOpenModal, setIsOpenModal ] = useState( false ) const [IsOpenModal, setIsOpenModal] = useState(false);
const {mutate: UpdateProjectDetails, isPending} = useUpdateProject( { const {mutate: UpdateProjectDetails, isPending} = useUpdateProject({
onSuccessCallback: () => onSuccessCallback: () => {
{ setIsOpenModal(false);
setIsOpenModal(false)
} }
} ) });
const ClientQuery = useQueryClient() const ClientQuery = useQueryClient();
const {projectId} = useParams();
// *** MODIFIED LINE: Get projectId from Redux store using useSelector ***
const projectId = useSelector((store) => store.localVariables.projectId);
const manageProject = useHasUserPermission(MANAGE_PROJECT); const manageProject = useHasUserPermission(MANAGE_PROJECT);
const {projects_Details, isLoading, error,refetch} = useProjectDetails( projectId ) const {projects_Details, isLoading, error,refetch} = useProjectDetails( projectId ); // Pass projectId from useSelector
const handleFormSubmit = ( updatedProject ) =>
{ const handleFormSubmit = ( updatedProject ) => {
if ( projects_Details?.id ) if ( projects_Details?.id ) {
{ UpdateProjectDetails({ projectId: projects_Details?.id,updatedData: updatedProject });
UpdateProjectDetails({ projectId: projects_Details?.id,updatedData: updatedProject, // The refetch here might be redundant or could be handled by react-query's invalidateQueries
} ); // if UpdateProjectDetails properly invalidates the 'projectDetails' query key.
refetch() // If refetch is still needed, consider adding a delay or using onSuccess of UpdateProjectDetails.
} // For now, keeping it as is based on your original code.
}; refetch();
}
};
return ( return (
<> <>
{IsOpenModal && ( {IsOpenModal && (
<GlobalModel isOpen={IsOpenModal} closeModal={()=>setIsOpenModal(false)}> <GlobalModel isOpen={IsOpenModal} closeModal={()=>setIsOpenModal(false)}>
<ManageProjectInfo <ManageProjectInfo
project={projects_Details} project={projects_Details}
handleSubmitForm={handleFormSubmit} handleSubmitForm={handleFormSubmit}
onClose={() => setIsOpenModal( false )} onClose={() => setIsOpenModal( false )}
isPending={isPending} isPending={isPending}
/> />
</GlobalModel> </GlobalModel>
)} )}
{projects_Details && ( {projects_Details && (
<> <>
<div className="card mb-6"> <div className="card mb-6">
<div className="card-header text-start"> <div className="card-header text-start">
<h6 className="card-action-title mb-0"> <h6 className="card-action-title mb-0">
@ -126,10 +131,9 @@ const AboutProject = () =>
</> </>
)} )}
{isLoading && <span>loading...</span>} {isLoading && <span>loading...</span>}
</> </>
); );
}; };
export default AboutProject; export default AboutProject;

View File

@ -1,6 +1,6 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import moment from "moment"; import moment from "moment";
import { formatNumber, getCompletionPercentage, getDateDifferenceInDays } from "../../utils/dateUtils"; import { formatNumber, getDateDifferenceInDays } from "../../utils/dateUtils";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useProjectDetails, useUpdateProject } from "../../hooks/useProjects"; import { useProjectDetails, useUpdateProject } from "../../hooks/useProjects";
import ManageProjectInfo from "./ManageProjectInfo"; import ManageProjectInfo from "./ManageProjectInfo";
@ -57,7 +57,7 @@ const ProjectCard = ({ projectData, recall }) => {
const handleClose = () => setShowModal(false); const handleClose = () => setShowModal(false);
const handleViewProject = () => { const handleViewProject = () => {
navigate(`/projects/${projectData.id}`); navigate(`/projects/details`);
}; };
const handleFormSubmit = (updatedProject) => { const handleFormSubmit = (updatedProject) => {
@ -87,19 +87,18 @@ const ProjectCard = ({ projectData, recall }) => {
<div className={`card cursor-pointer ${isPending ? "bg-light opacity-50 pointer-events-none" : ""}`}> <div className={`card cursor-pointer ${isPending ? "bg-light opacity-50 pointer-events-none" : ""}`}>
<div className="card-header pb-4"> <div className="card-header pb-4">
<div className="d-flex align-items-start"> <div className="d-flex align-items-start">
<div className="d-flex align-items-center "> <div className="d-flex align-items-center">
<div className="avatar me-4"> <div className="avatar me-4">
<i <i
className="rounded-circle bx bx-building-house" className="rounded-circle bx bx-building-house"
style={{ fontSize: "xx-large" }} style={{ fontSize: "xx-large" }}
></i> ></i>
</div> </div>
<div className="me-2 text-wrap "> <div className="me-2">
<h5 <h5
className="mb-0 stretched-link text-heading text-start text-truncate" className="mb-0 stretched-link text-heading text-start"
onClick={handleViewProject} onClick={handleViewProject}
style={{ maxWidth: "180px", display: "inline-block" }} >
>
{projectInfo.shortName {projectInfo.shortName
? projectInfo.shortName ? projectInfo.shortName
: projectInfo.name} : projectInfo.name}
@ -228,7 +227,12 @@ const ProjectCard = ({ projectData, recall }) => {
Task: {formatNumber(projectInfo.completedWork)} / {formatNumber(projectInfo.plannedWork)} Task: {formatNumber(projectInfo.completedWork)} / {formatNumber(projectInfo.plannedWork)}
</small> </small>
<small className="text-body"> <small className="text-body">
{getCompletionPercentage(projectInfo.completedWork, projectInfo.plannedWork)} {Math.floor(
getProgressInNumber(
projectInfo.plannedWork,
projectInfo.completedWork
)
) || 0}{" "}
% Completed % Completed
</small> </small>
</div> </div>
@ -270,4 +274,4 @@ const ProjectCard = ({ projectData, recall }) => {
); );
}; };
export default ProjectCard; export default ProjectCard;

View File

@ -1,4 +1,4 @@
import { useParams } from "react-router-dom"; import { useSelector } from "react-redux"; // Import useSelector
import React, { useState, useEffect, useCallback } from "react"; import React, { useState, useEffect, useCallback } from "react";
import ActivityTimeline from "../../components/Project/ActivityTimeline"; import ActivityTimeline from "../../components/Project/ActivityTimeline";
@ -31,157 +31,111 @@ import eventBus from "../../services/eventBus";
import ProjectProgressChart from "../../components/Dashboard/ProjectProgressChart"; import ProjectProgressChart from "../../components/Dashboard/ProjectProgressChart";
const ProjectDetails = () => { const ProjectDetails = () => {
let { projectId } = useParams(); // const { projectId } = useParams(); // REMOVE THIS LINE
const {
projects_Details,
loading: projectLoading,
error: ProjectError,
refetch
} = useProjectDetails(projectId);
const dispatch = useDispatch(); const dispatch = useDispatch();
const [project, setProject] = useState(null);
// const [projectDetails, setProjectDetails] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
// const fetchData = async () => { // GET projectId FROM REDUX STORE
// const project_cache = getCachedData("projectInfo"); const projectId = useSelector((store) => store.localVariables.projectId);
// if (!project_cache || project_cache?.projectId !== projectId) {
// ProjectRepository.getProjectByprojectId(projectId) const {
// .then((response) => { projects_Details,
// setProjectDetails(response.data); loading: projectLoading,
// setProject(response.data); error: projectError,
// cacheData("projectInfo", { projectId, data: response.data }); refetch,
// setLoading(false); } = useProjectDetails(projectId);
// })
// .catch((error) => {
// console.error(error);
// setError("Failed to fetch data.");
// setLoading(false);
// });
// } else {
// setProjectDetails(project_cache.data);
// setProject(project_cache.data);
// setLoading(false);
// }
// };
const [activePill, setActivePill] = useState("profile"); const [activePill, setActivePill] = useState("profile");
const handlePillClick = (pillKey) => { // REMOVE THIS useEffect AS projectId IS NOW FROM REDUX
setActivePill(pillKey); // useEffect(() => {
}; // if (projectId) dispatch(setProjectId(projectId));
// }, [projectId, dispatch]);
const handleDataChange = (data) => {
fetchData();
};
const renderContent = () => {
if (projectLoading) return <Loader></Loader>;
switch (activePill) {
case "profile": {
return (
<div className="row">
<div className="col-xl-4 col-lg-5 col-md-5 mt-5">
<AboutProject ></AboutProject>
</div>
<div className="col-xl-4 col-lg-5 col-md-5 mt-5">
<ProjectOverview project={projectId} />
</div>
</div>
);
}
case "teams": {
return (
<div className="row">
<div className="col-lg-12 col-xl-12">
{/* Teams */}
<Teams ></Teams>
{/* Teams */}
</div>
</div>
);
break;
}
case "infra": {
return (
<ProjectInfra
data={projects_Details}
onDataChange={handleDataChange}
></ProjectInfra>
);
break;
}
case "workplan": {
return (
<WorkPlan
data={projects_Details}
onDataChange={handleDataChange}
></WorkPlan>
);
break;
}
case "directory": {
return (
<div className="row">
<Directory IsPage={false} prefernceContacts={projects_Details.id} />
</div>
);
}
default:
return <ComingSoonPage></ComingSoonPage>;
}
};
useEffect(() => {
dispatch(setProjectId(projectId));
}, [projects_Details, projectId]);
const handler = useCallback( const handler = useCallback(
(msg) => { (msg) => {
if (msg.keyword === "Update_Project" && projects_Details.id === msg.response.id) { if (msg.keyword === "Update_Project" && projects_Details?.id === msg.response.id) {
refetch() refetch();
} }
}, },
[projects_Details, handleDataChange] [projects_Details, refetch]
); );
useEffect(() => { useEffect(() => {
eventBus.on("project", handler); eventBus.on("project", handler);
return () => eventBus.off("project", handler); return () => eventBus.off("project", handler);
}, [handler]); }, [handler]);
const handlePillClick = (pillKey) => {
setActivePill(pillKey);
};
const renderContent = () => {
if (projectLoading || !projects_Details) return <Loader />;
switch (activePill) {
case "profile":
return (
<>
<div className="row">
<div className="col-lg-4 col-md-5 mt-5">
<AboutProject></AboutProject>
<ProjectOverview project={projectId} />
</div>
<div className="col-lg-8 col-md-7 mt-5">
<ProjectProgressChart ShowAllProject="false" DefaultRange="1M" />
</div>
</div>
</>
);
case "teams":
return (
<div className="row">
<div className="col-lg-12">
<Teams />
</div>
</div>
);
case "infra":
return (
<ProjectInfra data={projects_Details} onDataChange={refetch} />
);
case "workplan":
return (
<WorkPlan data={projects_Details} onDataChange={refetch} />
);
case "directory":
return (
<div className="row mt-2">
<Directory IsPage={false} prefernceContacts={projects_Details.id} />
</div>
);
default:
return <ComingSoonPage />;
}
};
return ( return (
<> <div className="container-fluid">
{} <Breadcrumb
<div className="container-fluid"> data={[
<Breadcrumb { label: "Home", link: "/dashboard" },
data={[ { label: "Projects", link: "/projects" },
{ label: "Home", link: "/dashboard" }, { label: projects_Details?.name || "Project", link: null },
{ label: "Projects", link: "/projects" }, ]}
{ label: `${project?.name ? project?.name : ""}`, link: null }, />
]}
></Breadcrumb>
<div className="row"> <div className="row">
{projectLoading && <p>Loading....</p>} <ProjectNav onPillClick={handlePillClick} activePill={activePill} />
{/* {!projectLoading && project && (
<ProjectBanner project_data={project}></ProjectBanner>
)} */}
<ProjectNav
onPillClick={handlePillClick}
activePill={activePill}
></ProjectNav>
</div>
<div className="row"></div>
{renderContent()}
</div> </div>
</>
{renderContent()}
</div>
); );
}; };
export default ProjectDetails; export default ProjectDetails;

View File

@ -62,7 +62,7 @@ const ProjectListView = ({ projectData, recall }) => {
const handleClose = () => setShowModal(false); const handleClose = () => setShowModal(false);
const handleViewProject = () => { const handleViewProject = () => {
navigate(`/projects/${projectData.id}`); navigate(`/projects/details`);
}; };
const handleFormSubmit = (updatedProject) => { const handleFormSubmit = (updatedProject) => {
@ -89,7 +89,7 @@ const ProjectListView = ({ projectData, recall }) => {
<td className="text-start" colSpan={5}> <td className="text-start" colSpan={5}>
<span <span
className="text-primary cursor-pointer" className="text-primary cursor-pointer"
onClick={() => navigate(`/projects/${projectInfo.id}`)} onClick={() => navigate(`/projects/details`)}
> >
{projectInfo.shortName {projectInfo.shortName
? `${projectInfo.name} (${projectInfo.shortName})` ? `${projectInfo.name} (${projectInfo.shortName})`
@ -162,7 +162,7 @@ const ProjectListView = ({ projectData, recall }) => {
<a <a
aria-label="click to View details" aria-label="click to View details"
className="dropdown-item" className="dropdown-item"
onClick={() => navigate(`/projects/${projectInfo.id}`)} onClick={() => navigate(`/projects/details`)}
> >
<i className="bx bx-detail me-2"></i> <i className="bx bx-detail me-2"></i>
<span className="align-left">View details</span> <span className="align-left">View details</span>
@ -193,4 +193,4 @@ const ProjectListView = ({ projectData, recall }) => {
); );
}; };
export default ProjectListView; export default ProjectListView;

View File

@ -63,7 +63,7 @@ const router = createBrowserRouter(
{ path: "/", element: <Dashboard /> }, { path: "/", element: <Dashboard /> },
{ path: "/dashboard", element: <Dashboard /> }, { path: "/dashboard", element: <Dashboard /> },
{ path: "/projects", element: <ProjectList /> }, { path: "/projects", element: <ProjectList /> },
{ path: "/projects/:projectId", element: <ProjectDetails /> }, { path: "/projects/details", element: <ProjectDetails /> },
{ path: "/project/manage/:projectId", element: <ManageProject /> }, { path: "/project/manage/:projectId", element: <ManageProject /> },
{ path: "/employees", element: <EmployeeList /> }, { path: "/employees", element: <EmployeeList /> },
{ path: "/employee/:employeeId", element: <EmployeeProfile /> }, { path: "/employee/:employeeId", element: <EmployeeProfile /> },