Merge pull request 'pramod_Feature#51ProjectLisFilter' (#50) from pramod_Feature#51ProjectLisFilter into Issues_April_4W

Reviewed-on: #50
This commit is contained in:
Gitea Admin 2025-04-23 09:05:08 +00:00
commit b30f3b8d98
7 changed files with 820 additions and 491 deletions

View File

@ -1,6 +1,6 @@
import React, { useState } from "react"; import React, { useState } from "react";
import moment from "moment"; import moment from "moment";
import { ProjectStatus } from "../../utils/projectStatus"; import { getProjectStatusName } from "../../utils/projectStatus";
const AboutProject = ({ data }) => { const AboutProject = ({ data }) => {
const [CurrentProject, setCurrentProject] = useState(data); const [CurrentProject, setCurrentProject] = useState(data);
@ -34,7 +34,7 @@ const AboutProject = ({ data }) => {
<li className="d-flex align-items-center mb-2"> <li className="d-flex align-items-center mb-2">
<i className="bx bx-trophy"></i> <i className="bx bx-trophy"></i>
<span className="fw-medium mx-2">Status:</span>{" "} <span className="fw-medium mx-2">Status:</span>{" "}
<span>{ProjectStatus(data.projectStatusId)}</span> <span>{getProjectStatusName(data.projectStatusId)}</span>
</li> </li>
<li className="d-flex align-items-center mb-4"> <li className="d-flex align-items-center mb-4">
<i className="bx bx-user"></i> <i className="bx bx-user"></i>

View File

@ -8,278 +8,270 @@ import ProjectRepository from "../../repositories/ProjectRepository";
import { cacheData, getCachedData } from "../../slices/apiDataManager"; import { cacheData, getCachedData } from "../../slices/apiDataManager";
import showToast from "../../services/toastService"; import showToast from "../../services/toastService";
import { useHasUserPermission } from "../../hooks/useHasUserPermission"; import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import { MANAGE_PROJECT } from "../../utils/constants"; import {MANAGE_PROJECT} from "../../utils/constants";
import { getProjectStatusColor,getProjectStatusName } from "../../utils/projectStatus";
const ProjectCard = ({ projectData }) => { const ProjectCard = ({ projectData }) => {
const [projectInfo, setProjectInfo] = useState(projectData); const [projectInfo, setProjectInfo] = useState(projectData);
const [projectDetails, setProjectDetails] = useState(null); const [projectDetails, setProjectDetails] = useState(null);
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
const navigate = useNavigate(); const navigate = useNavigate();
const ManageProject = useHasUserPermission(MANAGE_PROJECT); const ManageProject = useHasUserPermission(MANAGE_PROJECT);
const handleShow = async () => { const handleShow = async () => {
try { try {
const response = await ProjectRepository.getProjectByprojectId(projectInfo.id); const response = await ProjectRepository.getProjectByprojectId(
setProjectDetails(response.data); projectInfo.id
setShowModal(true); );
} catch (error) { setProjectDetails(response.data);
showToast("Failed to load project details", "error"); setShowModal(true);
} } catch (error) {
}; showToast("Failed to load project details", "error");
}
};
const getProgress = (planned, completed) => { const getProgress = (planned, completed) => {
return (completed * 100) / planned + "%"; return (completed * 100) / planned + "%";
}; };
const getProgressInNumber = (planned, completed) => { const getProgressInNumber = (planned, completed) => {
return (completed * 100) / planned; return (completed * 100) / planned;
}; };
const handleClose = () => setShowModal(false); const handleClose = () => setShowModal(false);
const getProjectStatusName = (statusId) => {
switch (statusId) {
case 1:
return "Active";
case 2:
return "On Hold";
// case 3:
// return "Suspended";
case 3:
return "Inactive";
case 4:
return "Completed";
}
};
const getProjectStatusColor = (statusId) => {
switch (statusId) {
case 1:
return "bg-label-success";
case 2:
return "bg-label-warning";
case 3:
return "bg-label-info";
case 4:
return "bg-label-secondary";
case 5:
return "bg-label-dark";
}
};
const handleViewProject = () => {
navigate(`/projects/${projectData.id}`);
};
const handleFormSubmit = (updatedProject) => { const handleViewProject = () => {
if (projectInfo?.id) { navigate(`/projects/${projectData.id}`);
ProjectRepository.updateProject(projectInfo.id, updatedProject) };
.then((response) => {
const updatedProjectData = {
...projectInfo,
...response.data,
building: projectDetails?.building,
};
setProjectInfo(updatedProject); const handleFormSubmit = (updatedProject) => {
if (projectInfo?.id) {
ProjectRepository.updateProject(projectInfo.id, updatedProject)
.then((response) => {
const updatedProjectData = {
...projectInfo,
...response.data,
building: projectDetails?.building,
};
if (getCachedData(`projectinfo-${projectInfo.id}`)) { setProjectInfo(updatedProject);
cacheData(`projectinfo-${projectInfo.id}`, updatedProjectData);
}
const projects_list = getCachedData("projectslist"); if (getCachedData(`projectinfo-${projectInfo.id}`)) {
if (projects_list) { cacheData(`projectinfo-${projectInfo.id}`, updatedProjectData);
const updatedProjectsList = projects_list.map((project) => }
project.id === projectInfo.id
? { ...project, ...response.data, tenant: project.tenant }
: project
);
cacheData("projectslist", updatedProjectsList);
}
showToast("Project updated successfully.", "success"); const projects_list = getCachedData("projectslist");
setShowModal(false); if (projects_list) {
}) const updatedProjectsList = projects_list.map((project) =>
.catch((error) => { project.id === projectInfo.id
showToast(error.message, "error"); ? { ...project, ...response.data, tenant: project.tenant }
}); : project
} );
}; cacheData("projectslist", updatedProjectsList);
}
return ( showToast("Project updated successfully.", "success");
<> setShowModal(false);
{showModal && projectDetails && ( })
<div .catch((error) => {
className="modal fade show" showToast(error.message, "error");
tabIndex="-1" });
role="dialog" }
style={{ display: "block" }} };
aria-hidden="false"
> return (
<ManageProjectInfo <>
project={projectDetails} {showModal && projectDetails && (
handleSubmitForm={handleFormSubmit} <div
onClose={handleClose} className="modal fade show"
/> tabIndex="-1"
role="dialog"
style={{ display: "block" }}
aria-hidden="false"
>
<ManageProjectInfo
project={projectDetails}
handleSubmitForm={handleFormSubmit}
onClose={handleClose}
/>
</div>
)}
<div className="col-md-6 col-lg-4 col-xl-4 order-0 mb-4">
<div className="card cursor-pointer">
<div className="card-header pb-4">
<div className="d-flex align-items-start">
<div className="d-flex align-items-center">
<div className="avatar me-4">
<i
className="rounded-circle bx bx-building-house"
style={{ fontSize: "xx-large" }}
></i>
</div> </div>
)} <div className="me-2">
<h5 className="mb-0">
<div className="col-md-6 col-lg-4 col-xl-4 order-0 mb-4"> <a
<div className="card cursor-pointer"> className="stretched-link text-heading"
<div className="card-header pb-4"> onClick={handleViewProject}
<div className="d-flex align-items-start"> >
<div className="d-flex align-items-center"> {projectInfo.name}
<div className="avatar me-4"> </a>
<i </h5>
className="rounded-circle bx bx-building-house" <div className="client-info text-body">
style={{ fontSize: "xx-large" }} <span className="fw-medium">Client: </span>
></i> <span>{projectInfo.contactPerson}</span>
</div> </div>
<div className="me-2">
<h5 className="mb-0">
<a
className="stretched-link text-heading"
onClick={handleViewProject}
>
{projectInfo.name}
</a>
</h5>
<div className="client-info text-body">
<span className="fw-medium">Client: </span>
<span>{projectInfo.contactPerson}</span>
</div>
</div>
</div>
<div className={`ms-auto ${!ManageProject && "d-none"}`}>
<div className="dropdown z-2">
<button
type="button"
className="btn btn-icon btn-text-secondary rounded-pill dropdown-toggle hide-arrow p-0"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<i className="bx bx-dots-vertical-rounded bx-sm text-muted" data-bs-toggle="tooltip" data-bs-offset="0,8" data-bs-placement="top" data-bs-custom-class="tooltip-dark" title="More Action"></i>
</button>
<ul className="dropdown-menu dropdown-menu-end">
<li>
<a
aria-label="click to View details"
className="dropdown-item"
onClick={handleViewProject}
>
<i className="bx bx-detail me-2"></i>
<span className="align-left">View details</span>
</a>
</li>
<li onClick={handleShow}>
<a className="dropdown-item">
<i className="bx bx-pencil me-2"></i>
<span className="align-left">Modify</span>
</a>
</li>
<li onClick={()=>navigate(`/activities/records?project=${projectInfo.id}`)}>
<a className="dropdown-item">
<i className="bx bx-task me-2"></i>
<span className="align-left" >Activities</span>
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
<div className="card-body pb-1">
<div className="d-flex align-items-center flex-wrap">
<div className="text-start mb-4">
<p className="mb-1">
<span className="text-heading fw-medium">Start Date: </span>
{projectInfo.startDate
? moment(projectInfo.startDate).format("DD-MMM-YYYY")
: "NA"}
</p>
<p className="mb-1">
<span className="text-heading fw-medium">Deadline: </span>
{projectInfo.endDate
? moment(projectInfo.endDate).format("DD-MMM-YYYY")
: "NA"}
</p>
<p className="mb-0">{projectInfo.projectAddress}</p>
</div>
</div>
</div>
<div className="card-body border-top">
<div className="d-flex align-items-center mb-4">
<p className="mb-0">
<span
className={
`badge rounded-pill ` +
getProjectStatusColor(projectInfo.projectStatusId)
}
>
{getProjectStatusName(projectInfo.projectStatusId)}
</span>
</p>{" "}
{getDateDifferenceInDays(projectInfo.endDate, Date()) >= 0 &&
( <span className="badge bg-label-success ms-auto">
{projectInfo.endDate &&
getDateDifferenceInDays(projectInfo.endDate, Date())}{" "}
Days left
</span>) }
{getDateDifferenceInDays(projectInfo.endDate, Date()) < 0 &&
( <span className="badge bg-label-danger ms-auto">
{projectInfo.endDate &&
getDateDifferenceInDays(projectInfo.endDate, Date())}{" "}
Days overdue
</span>)}
</div>
<div className="d-flex justify-content-between align-items-center mb-2">
<small className="text-body">Task: {projectInfo.completedWork} / {projectInfo.plannedWork}</small>
<small className="text-body">{Math.floor(getProgressInNumber(projectInfo.plannedWork, projectInfo.completedWork)) || 0} % Completed</small>
</div>
<div className="progress mb-4 rounded" style={{ height: "8px" }}>
<div
className="progress-bar rounded"
role="progressbar"
style={{ width: getProgress(projectInfo.plannedWork, projectInfo.completedWork) }}
aria-valuenow={projectInfo.completedWork}
aria-valuemin="0"
aria-valuemax={projectInfo.plannedWork}
></div>
</div>
<div className="d-flex align-items-center justify-content-between">
{/* <div className="d-flex align-items-center ">
</div> */}
<div >
<a
className="text-muted d-flex " alt="Active team size"
>
<i className="bx bx-group bx-sm me-1_5"></i>{projectInfo?.teamSize} Members
</a>
</div>
<div >
<a
className="text-muted d-flex align-items-center"
>
<i className="bx bx-chat me-1 "></i> <span className="text-decoration-line-through">15</span>
</a>
</div>
</div>
</div>
</div> </div>
</div>
<div className={`ms-auto ${!ManageProject && "d-none"}`}>
<div className="dropdown z-2">
<button
type="button"
className="btn btn-icon btn-text-secondary rounded-pill dropdown-toggle hide-arrow p-0"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<i
className="bx bx-dots-vertical-rounded bx-sm text-muted"
data-bs-toggle="tooltip"
data-bs-offset="0,8"
data-bs-placement="top"
data-bs-custom-class="tooltip-dark"
title="More Action"
></i>
</button>
<ul className="dropdown-menu dropdown-menu-end">
<li>
<a
aria-label="click to View details"
className="dropdown-item"
onClick={handleViewProject}
>
<i className="bx bx-detail me-2"></i>
<span className="align-left">View details</span>
</a>
</li>
<li onClick={handleShow}>
<a className="dropdown-item">
<i className="bx bx-pencil me-2"></i>
<span className="align-left">Modify</span>
</a>
</li>
<li
onClick={() =>
navigate(
`/activities/records?project=${projectInfo.id}`
)
}
>
<a className="dropdown-item">
<i className="bx bx-task me-2"></i>
<span className="align-left">Activities</span>
</a>
</li>
</ul>
</div>
</div>
</div> </div>
</> </div>
); <div className="card-body pb-1">
<div className="d-flex align-items-center flex-wrap">
<div className="text-start mb-4">
<p className="mb-1">
<span className="text-heading fw-medium">Start Date: </span>
{projectInfo.startDate
? moment(projectInfo.startDate).format("DD-MMM-YYYY")
: "NA"}
</p>
<p className="mb-1">
<span className="text-heading fw-medium">Deadline: </span>
{projectInfo.endDate
? moment(projectInfo.endDate).format("DD-MMM-YYYY")
: "NA"}
</p>
<p className="mb-0">{projectInfo.projectAddress}</p>
</div>
</div>
</div>
<div className="card-body border-top">
<div className="d-flex align-items-center mb-4">
<p className="mb-0">
<span
className={
`badge rounded-pill ` +
getProjectStatusColor(projectInfo.projectStatusId)
}
>
{getProjectStatusName(projectInfo.projectStatusId)}
</span>
</p>{" "}
{getDateDifferenceInDays(projectInfo.endDate, Date()) >= 0 && (
<span className="badge bg-label-success ms-auto">
{projectInfo.endDate &&
getDateDifferenceInDays(projectInfo.endDate, Date())}{" "}
Days left
</span>
)}
{getDateDifferenceInDays(projectInfo.endDate, Date()) < 0 && (
<span className="badge bg-label-danger ms-auto">
{projectInfo.endDate &&
getDateDifferenceInDays(projectInfo.endDate, Date())}{" "}
Days overdue
</span>
)}
</div>
<div className="d-flex justify-content-between align-items-center mb-2">
<small className="text-body">
Task: {projectInfo.completedWork} / {projectInfo.plannedWork}
</small>
<small className="text-body">
{Math.floor(
getProgressInNumber(
projectInfo.plannedWork,
projectInfo.completedWork
)
) || 0}{" "}
% Completed
</small>
</div>
<div className="progress mb-4 rounded" style={{ height: "8px" }}>
<div
className="progress-bar rounded"
role="progressbar"
style={{
width: getProgress(
projectInfo.plannedWork,
projectInfo.completedWork
),
}}
aria-valuenow={projectInfo.completedWork}
aria-valuemin="0"
aria-valuemax={projectInfo.plannedWork}
></div>
</div>
<div className="d-flex align-items-center justify-content-between">
{/* <div className="d-flex align-items-center ">
</div> */}
<div>
<a className="text-muted d-flex " alt="Active team size">
<i className="bx bx-group bx-sm me-1_5"></i>
{projectInfo?.teamSize} Members
</a>
</div>
<div>
<a className="text-muted d-flex align-items-center">
<i className="bx bx-chat me-1 "></i>{" "}
<span className="text-decoration-line-through">15</span>
</a>
</div>
</div>
</div>
</div>
</div>
</>
);
}; };
export default ProjectCard; export default ProjectCard;

View File

@ -1,30 +1,37 @@
import React from "react"; import React from "react";
const ProgressBar = ( {completeValue, totalValue} ) => const ProgressBar = ({
{ plannedWork = 100,
completedWork = 0,
height = "8px",
const getProgress = (complete, total) => { className = "mb-4",
return (total * 100) / complete + "%"; rounded = true,
}) => {
const getProgress = (planned, completed) => {
if (!planned || planned === 0) return "0%";
return `${Math.min((completed / planned) * 100, 100).toFixed(2)}%`;
}; };
return (
<div className="progress mb-4 rounded" style={{height: "8px"}}> const progressStyle = {
<div className="progress p-0"> width: getProgress(plannedWork, completedWork),
};
return (
<div
className={`progress ${className} ${rounded ? "rounded" : ""}`}
style={{ height }}
>
<div <div
className="progress-bar" className={`progress-bar ${rounded ? "rounded" : ""}`}
role="progressbar" role="progressbar"
style={{ style={progressStyle}
width: `${getProgress( totalValue,completeValue)}`, aria-valuenow={completedWork}
height: "10px",
}}
aria-valuenow={completeValue}
aria-valuemin="0" aria-valuemin="0"
aria-valuemax={totalValue} aria-valuemax={plannedWork}
></div> ></div>
</div> </div>
</div>
); );
}; };
export default ProgressBar; export default ProgressBar;

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect,useRef } from "react"; import React, { useState, useEffect, useRef } from "react";
import moment from "moment"; import moment from "moment";
import { Link, NavLink, useNavigate } from "react-router-dom"; import { Link, NavLink, useNavigate } from "react-router-dom";
import Avatar from "../../components/common/Avatar"; import Avatar from "../../components/common/Avatar";
@ -11,7 +11,12 @@ import { hasUserPermission } from "../../utils/authUtils";
import { MANAGE_EMPLOYEES } from "../../utils/constants"; import { MANAGE_EMPLOYEES } from "../../utils/constants";
import { useHasUserPermission } from "../../hooks/useHasUserPermission"; import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import SuspendEmp from "../../components/Employee/SuspendEmp"; import SuspendEmp from "../../components/Employee/SuspendEmp";
import {exportToCSV,exportToExcel,printTable,exportToPDF} from "../../utils/tableExportUtils"; import {
exportToCSV,
exportToExcel,
printTable,
exportToPDF,
} from "../../utils/tableExportUtils";
const EmployeeList = () => { const EmployeeList = () => {
const { profile: loginUser } = useProfile(); const { profile: loginUser } = useProfile();
@ -105,7 +110,7 @@ const EmployeeList = () => {
const tableRef = useRef(null); const tableRef = useRef(null);
const handleExport = (type) => { const handleExport = (type) => {
if (!currentItems || currentItems.length === 0) return; if (!currentItems || currentItems.length === 0) return;
switch (type) { switch (type) {
case "csv": case "csv":
exportToCSV(currentItems, "employees"); exportToCSV(currentItems, "employees");
@ -114,17 +119,16 @@ const EmployeeList = () => {
exportToExcel(currentItems, "employees"); exportToExcel(currentItems, "employees");
break; break;
case "pdf": case "pdf":
exportToPDF(currentItems, "employees"); // Pass the employeeList directly exportToPDF(currentItems, "employees"); // Pass the employeeList directly
break; break;
case "print": case "print":
printTable(tableRef.current); printTable(tableRef.current);
break; break;
default: default:
break; break;
} }
}; };
return ( return (
<> <>
{isCreateModalOpen && ( {isCreateModalOpen && (
@ -163,32 +167,36 @@ const EmployeeList = () => {
className="dataTables_length text-start" className="dataTables_length text-start"
id="DataTables_Table_0_length" id="DataTables_Table_0_length"
> >
<label> <label>
<select <select
id="project-select" id="project-select"
onChange={(e) => setSelectedProject(e.target.value)} onChange={(e) => setSelectedProject(e.target.value)}
name="DataTables_Table_0_length" name="DataTables_Table_0_length"
aria-controls="DataTables_Table_0" aria-controls="DataTables_Table_0"
className="form-select form-select-sm" className="form-select form-select-sm"
value={selectedProject || ""} value={selectedProject || ""}
> >
{projectLoading ? ( {projectLoading ? (
<option value="Loading">Loading...</option> <option value="Loading">Loading...</option>
) : ( ) : (
<> <>
<option value="">All Employees</option> <option value="">All Employees</option>
{Array.isArray(projects) && {Array.isArray(projects) &&
projects projects
.filter((item) => loginUser?.projects?.includes(String(item.id))) .filter((item) =>
.map((item) => ( loginUser?.projects?.includes(
<option key={item.id} value={item.id}> String(item.id)
{item.name} )
</option> )
))} .map((item) => (
</> <option key={item.id} value={item.id}>
)} {item.name}
</select> </option>
</label> ))}
</>
)}
</select>
</label>
</div> </div>
</div> </div>
</div> </div>
@ -223,22 +231,39 @@ const EmployeeList = () => {
</button> </button>
<ul className="dropdown-menu"> <ul className="dropdown-menu">
<li> <li>
<a className="dropdown-item" href="#" onClick={() => handleExport("print")}> <a
className="dropdown-item"
href="#"
onClick={() => handleExport("print")}
>
<i className="bx bx-printer me-1"></i> Print <i className="bx bx-printer me-1"></i> Print
</a> </a>
</li> </li>
<li> <li>
<a className="dropdown-item" href="#" onClick={() => handleExport("csv")}> <a
className="dropdown-item"
href="#"
onClick={() => handleExport("csv")}
>
<i className="bx bx-file me-1"></i> CSV <i className="bx bx-file me-1"></i> CSV
</a> </a>
</li> </li>
<li> <li>
<a className="dropdown-item" href="#" onClick={() => handleExport("excel")}> <a
<i className="bx bxs-file-export me-1"></i> Excel className="dropdown-item"
href="#"
onClick={() => handleExport("excel")}
>
<i className="bx bxs-file-export me-1"></i>{" "}
Excel
</a> </a>
</li> </li>
<li> <li>
<a className="dropdown-item" href="#" onClick={() => handleExport("pdf")}> <a
className="dropdown-item"
href="#"
onClick={() => handleExport("pdf")}
>
<i className="bx bxs-file-pdf me-1"></i> PDF <i className="bx bxs-file-pdf me-1"></i> PDF
</a> </a>
</li> </li>
@ -272,7 +297,7 @@ const EmployeeList = () => {
id="DataTables_Table_0" id="DataTables_Table_0"
aria-describedby="DataTables_Table_0_info" aria-describedby="DataTables_Table_0_info"
style={{ width: "100%" }} style={{ width: "100%" }}
ref={tableRef} ref={tableRef}
> >
<thead> <thead>
<tr> <tr>
@ -462,19 +487,19 @@ const EmployeeList = () => {
} }
className="dropdown-item py-1" className="dropdown-item py-1"
> >
<i className="bx bx-detail bx-sm"></i> View <i className="bx bx-detail bx-sm"></i> View
</button> </button>
<Link <Link
to={`/employee/manage/${item.id}`} to={`/employee/manage/${item.id}`}
className="dropdown-item py-1" className="dropdown-item py-1"
> >
<i class='bx bx-edit bx-sm'></i> Edit <i class="bx bx-edit bx-sm"></i> Edit
</Link> </Link>
<button <button
className="dropdown-item py-1" className="dropdown-item py-1"
onClick={handleShow} onClick={handleShow}
> >
<i class='bx bx-task-x bx-sm'></i> Suspend <i class="bx bx-task-x bx-sm"></i> Suspend
</button> </button>
<button <button
className="dropdown-item py-1" className="dropdown-item py-1"
@ -483,7 +508,7 @@ const EmployeeList = () => {
data-bs-target="#managerole-modal" data-bs-target="#managerole-modal"
onClick={() => handleConfigData(item.id)} onClick={() => handleConfigData(item.id)}
> >
<i class='bx bx-cog bx-sm'></i> Manage Role <i class="bx bx-cog bx-sm"></i> Manage Role
</button> </button>
</div> </div>
</div> </div>

View File

@ -6,86 +6,74 @@ import ProjectRepository from "../../repositories/ProjectRepository";
import { useProjects } from "../../hooks/useProjects"; import { useProjects } from "../../hooks/useProjects";
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
import showToast from "../../services/toastService"; import showToast from "../../services/toastService";
import { getCachedData, cacheData} from "../../slices/apiDataManager"; import { getCachedData, cacheData } from "../../slices/apiDataManager";
import {useHasUserPermission} from "../../hooks/useHasUserPermission" import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import { useProfile } from "../../hooks/useProfile"; import { useProfile } from "../../hooks/useProfile";
import {MANAGE_PROJECT} from "../../utils/constants"; import { MANAGE_PROJECT } from "../../utils/constants";
import ProjectListView from "./ProjectListView";
const ProjectList = () => const ProjectList = () => {
{ const { profile: loginUser } = useProfile();
const [listView, setListView] = useState(true);
const {profile: loginUser} = useProfile();
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
const {projects, loading, error, refetch} = useProjects(); const { projects, loading, error, refetch } = useProjects();
const [refresh, setRefresh] = useState(false); const [projectList, setProjectList] = useState([]);
const [ projectList, setProjectList ] = useState( [] ); const HasManageProjectPermission = useHasUserPermission(MANAGE_PROJECT);
const HasManageProjectPermission = useHasUserPermission( MANAGE_PROJECT ) const [HasManageProject, setHasManageProject] = useState(
const[HasManageProject,setHasManageProject] = useState(HasManageProjectPermission) HasManageProjectPermission
);
const dispatch = useDispatch(); const dispatch = useDispatch();
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage] = useState(6); const [itemsPerPage] = useState(6);
const [searchTerm, setSearchTerm] = useState("");
const [selectedStatuses, setSelectedStatuses] = useState([1, 2, 3, 4]);
const handleShow = () => setShowModal(true);
const handleShow = () => setShowModal(true); const handleClose = () => setShowModal(false);
const handleClose = () => setShowModal( false );
useEffect(() => { useEffect(() => {
if (!loading && Array.isArray(projects)) { if (!loading && Array.isArray(projects)) {
// Step 1: Group projects by statusId
const grouped = {}; const grouped = {};
projects.forEach((project) => { projects.forEach((project) => {
const statusId = project.projectStatusId; const statusId = project.projectStatusId;
if (!grouped[statusId]) { if (!grouped[statusId]) grouped[statusId] = [];
grouped[statusId] = [];
}
grouped[statusId].push(project); grouped[statusId].push(project);
}); });
// Step 2: Sort each group by name
const sortedGrouped = Object.keys(grouped) const sortedGrouped = Object.keys(grouped)
.sort() // sort group keys (status IDs) .sort()
.flatMap((statusId) => .flatMap((statusId) =>
grouped[statusId].sort((a, b) => grouped[statusId].sort((a, b) =>
a.name.toLowerCase().localeCompare(b.name.toLowerCase()) a.name.toLowerCase().localeCompare(b.name.toLowerCase())
) )
); );
setProjectList(sortedGrouped); // final sorted flat list setProjectList(sortedGrouped);
} }
}, [projects, loginUser?.projects, loading]);
}, [ projects, loginUser?.projects, loading ] );
useEffect(() => { useEffect(() => {
if (loginUser) { if (loginUser) {
setHasManageProject(HasManageProjectPermission); setHasManageProject(HasManageProjectPermission);
} else { } else {
setHasManageProject(false); } setHasManageProject(false);
}
}, [loginUser, HasManageProjectPermission]); }, [loginUser, HasManageProjectPermission]);
const handleSubmitForm = (newProject) => { const handleSubmitForm = (newProject) => {
ProjectRepository.manageProject(newProject) ProjectRepository.manageProject(newProject)
.then( ( response ) => .then((response) => {
{ const cachedProjects = getCachedData("projectslist") || [];
const updatedProjects = [...cachedProjects, response.data];
const cachedProjects_list = getCachedData( "projectslist" ) || []; cacheData("projectslist", updatedProjects);
setProjectList((prev) => [...prev, response.data]);
const updated_Projects_list = [ ...cachedProjects_list, response.data ];
cacheData("projectslist", updated_Projects_list);
setProjectList((prevProjectList) => [...prevProjectList, response.data]);
showToast("Project Created successfully.", "success"); showToast("Project Created successfully.", "success");
setShowModal(false) setShowModal(false);
}) })
.catch((error) => { .catch((error) => {
closeModal();
showToast(error.message, "error"); showToast(error.message, "error");
setShowModal(false);
}); });
}; };
@ -93,58 +81,122 @@ const ProjectList = () =>
if (!projects || projects.length === 0) { if (!projects || projects.length === 0) {
refetch(); refetch();
} }
setRefresh((prev) => !prev);
}; };
const handleStatusChange = (statusId) => {
setCurrentPage(1);
setSelectedStatuses((prev) =>
prev.includes(statusId)
? prev.filter((id) => id !== statusId)
: [...prev, statusId]
);
};
const handleStatusFilterFromChild = (statusesFromChild) => {
setSelectedStatuses(statusesFromChild);
};
const filteredProjects = projectList.filter((project) => {
const matchesStatus = selectedStatuses.includes(project.projectStatusId);
const matchesSearch = project.name
.toLowerCase()
.includes(searchTerm.toLowerCase());
return matchesStatus && matchesSearch;
});
const indexOfLastItem = currentPage * itemsPerPage; const indexOfLastItem = currentPage * itemsPerPage;
const indexOfFirstItem = indexOfLastItem - itemsPerPage; const indexOfFirstItem = indexOfLastItem - itemsPerPage;
const currentItems = Array.isArray(projectList) const currentItems = filteredProjects.slice(
? projectList.slice(indexOfFirstItem, indexOfLastItem) indexOfFirstItem,
: []; indexOfLastItem
);
const paginate = (pageNumber) => setCurrentPage(pageNumber); const totalPages = Math.ceil(filteredProjects.length / itemsPerPage);
const totalPages = Array.isArray(projectList)
? Math.ceil(projectList.length / itemsPerPage) useEffect(() => {
: 0; const tooltipTriggerList = Array.from(
document.querySelectorAll('[data-bs-toggle="tooltip"]')
);
tooltipTriggerList.forEach((el) => new bootstrap.Tooltip(el));
}, []);
return ( return (
<> <>
<div <div
className={`modal fade ${showModal ? 'show' : ''}`} className={`modal fade ${showModal ? "show" : ""}`}
tabIndex="-1" tabIndex="-1"
role="dialog" role="dialog"
style={{ display: showModal ? 'block' : 'none' }} style={{ display: showModal ? "block" : "none" }}
aria-hidden={!showModal} aria-hidden={!showModal}
> >
<ManageProjectInfo <ManageProjectInfo
project={null} project={null}
handleSubmitForm={handleSubmitForm} handleSubmitForm={handleSubmitForm}
onClose={handleClose} onClose={handleClose}
></ManageProjectInfo> />
</div> </div>
<div className="container-xxl flex-grow-1 container-p-y"> <div className="container-xxl flex-grow-1 container-p-y">
<Breadcrumb <Breadcrumb
data={[ data={[
{ label: "Home", link: "/dashboard" }, { label: "Home", link: "/dashboard" },
{ label: "Projects", link: null }, { label: "Projects", link: null },
]} ]}
></Breadcrumb> />
<div className="row"> <div className="d-flex flex-wrap justify-content-between align-items-start mb-4">
<div <div className="d-flex flex-wrap align-items-start">
className={`col-md-12 col-lg-12 col-xl-12 order-0 mb-4 ${ <div className="flex-grow-1 me-2 mb-2">
!error && !projects ? "text-center" : "text-end" <input
}`} type="search"
> className="form-control form-control-sm"
{" "} placeholder="Search projects..."
value={searchTerm}
onChange={(e) => {
setSearchTerm(e.target.value);
setCurrentPage(1);
}}
/>
</div>
<div className="d-flex gap-2 mb-2">
<button
type="button"
className={`btn btn-sm ${
listView ? "btn-primary" : "btn-outline-primary"
}`}
onClick={() => setListView(true)}
data-bs-toggle="tooltip"
data-bs-offset="0,8"
data-bs-placement="top"
data-bs-custom-class="tooltip"
title="List View"
>
<i className="bx bx-list-ul"></i>
</button>
<button
type="button"
className={`btn btn-sm ${
!listView ? "btn-primary" : "btn-outline-primary"
}`}
onClick={() => setListView(false)}
data-bs-toggle="tooltip"
data-bs-offset="0,8"
data-bs-placement="top"
data-bs-custom-class="tooltip"
title="Card View"
>
<i className="bx bx-grid-alt"></i>
</button>
</div>
</div>
<div>
<button <button
type="button" type="button"
className={`btn btn-xs btn-primary ${!HasManageProject && 'd-none' }`} className={`btn btn-sm btn-primary ${
data-bs-toggle="modal" !HasManageProject && "d-none"
data-bs-target="#create-project-model" }`}
onClick={handleShow} onClick={handleShow}
> >
<i className="bx bx-plus-circle me-2"></i> <i className="bx bx-plus-circle me-2"></i>
@ -153,83 +205,130 @@ const ProjectList = () =>
</div> </div>
</div> </div>
{((error && !loading) || !projects) && ( {loading && <p className="text-center">Loading...</p>}
<p className="text-center text-body-secondary"> {!loading && filteredProjects.length === 0 && !listView && (
There was an error loading the projects. Please try again. <p className="text-center text-muted">No projects found.</p>
</p>
)} )}
{(!projects || projects.length === 0 || projectList.length == 0) &&
!loading &&
error && (
<div className="text-center">
<button
className="btn btn-xs btn-label-secondary"
onClick={handleReFresh}
>
Retry Fetching Projects
</button>
</div>
)}
<div className="row"> <div className="row">
{loading && <p className="text-center">Loading...</p>} {listView ? (
<div className="table-responsive text-nowrap py-2 ">
<table className="table px-2">
{Array.isArray(currentItems) && loginUser?.projects && ( <thead>
currentItems <tr>
.filter((item) => loginUser.projects.includes(String(item.id))) <th className="text-start" colSpan={5}>
.map((item) => ( Project Name
<ProjectCard projectData={item} key={item.id} /> </th>
)) <th className="mx-2">Project Manger</th>
)} <th className="mx-2">START DATE</th>
<th className="mx-2">DEADLINE</th>
<th className="mx-2">Task</th>
<th className="mx-2">Progress</th>
<th className="mx-2">
<div className="dropdown">
<a
className="dropdown-toggle hide-arrow cursor-pointer"
data-bs-toggle="dropdown"
aria-expanded="false"
>
Status <i className="bx bx-filter"></i>
</a>
<ul className="dropdown-menu p-2 text-capitalize">
{[
{ id: 1, label: "Active" },
{ id: 2, label: "On Hold" },
{ id: 3, label: "Inactive" },
{ id: 4, label: "Completed" },
].map(({ id, label }) => (
<li key={id}>
<div className="form-check">
<input
className="form-check-input "
type="checkbox"
checked={selectedStatuses.includes(id)}
onChange={() => handleStatusChange(id)}
/>
<label className="form-check-label">
{label}
</label>
</div>
</li>
))}
</ul>
</div>
</th>
<th
className={`mx-2 ${
HasManageProject ? "d-sm-table-cell" : "d-none"
}`}
>
Action
</th>
</tr>
</thead>
<tbody className="table-border-bottom-0 overflow-auto ">
{currentItems.length === 0 ? (
<tr>
<td colSpan="12" className="text-center py-4">
No projects found
</td>
</tr>
) : (
currentItems.map((project) => (
<ProjectListView key={project.id} projectData={project} />
))
)}
</tbody>
</table>
</div>
) : (
currentItems.map((project) => (
<ProjectCard key={project.id} projectData={project} />
))
)}
</div> </div>
{/* Pagination */}
{!loading && ( {!loading && totalPages > 1 && (
<nav aria-label="Page "> <nav>
<ul className="pagination pagination-sm justify-content-end py-1"> <ul className="pagination pagination-sm justify-content-end py-2">
<li <li className={`page-item ${currentPage === 1 && "disabled"}`}>
className={`page-item ${ <button
currentPage === 1 ? "disabled" : "" className="page-link"
}`} onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
> >
<button &laquo;
className="page-link btn-xs" </button>
onClick={() => paginate(currentPage - 1)} </li>
> {[...Array(totalPages)].map((_, i) => (
&laquo; <li
</button> key={i}
</li> className={`page-item ${currentPage === i + 1 && "active"}`}
{[...Array(totalPages)]?.map((_, index) => ( >
<li <button
key={index} className="page-link"
className={`page-item ${ onClick={() => setCurrentPage(i + 1)}
currentPage === index + 1 ? "active" : "" >
}`} {i + 1}
> </button>
<button </li>
className="page-link " ))}
onClick={() => paginate(index + 1)} <li
> className={`page-item ${
{index + 1} currentPage === totalPages && "disabled"
</button> }`}
</li> >
))} <button
<li className="page-link"
className={`page-item ${ onClick={() =>
currentPage === totalPages ? "disabled" : "" setCurrentPage((p) => Math.min(totalPages, p + 1))
}`} }
> >
<button &raquo;
className="page-link " </button>
onClick={() => paginate(currentPage + 1)} </li>
> </ul>
&raquo; </nav>
</button> )}
</li>
</ul>
</nav>
)}
</div> </div>
</> </>
); );

View File

@ -0,0 +1,199 @@
import React, { useState, useEffect } from "react";
import moment from "moment";
import { useProjects } from "../../hooks/useProjects";
import {
getProjectStatusName,
getProjectStatusColor,
} from "../../utils/projectStatus";
import ProgressBar from "../../components/common/ProgressBar";
import { useNavigate } from "react-router-dom";
import ManageProject from "../../components/Project/ManageProject";
import ProjectRepository from "../../repositories/ProjectRepository";
import { MANAGE_PROJECT } from "../../utils/constants";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import ManageProjectInfo from "../../components/Project/ManageProjectInfo";
import showToast from "../../services/toastService";
import { getCachedData, cacheData } from "../../slices/apiDataManager";
const ProjectListView = ({ projectData }) => {
const [projectInfo, setProjectInfo] = useState(projectData);
const [projectDetails, setProjectDetails] = useState(null);
const [showModal, setShowModal] = useState(false);
const navigate = useNavigate();
const ManageProject = useHasUserPermission(MANAGE_PROJECT);
const handleShow = async () => {
try {
const response = await ProjectRepository.getProjectByprojectId(
projectInfo.id
);
setProjectDetails(response.data);
setShowModal(true);
} catch (error) {
showToast("Failed to load project details", "error");
}
};
const getProgress = (planned, completed) => {
return (completed * 100) / planned + "%";
};
const getProgressInNumber = (planned, completed) => {
return (completed * 100) / planned;
};
const handleClose = () => setShowModal(false);
const handleViewProject = () => {
navigate(`/projects/${projectData.id}`);
};
const handleFormSubmit = (updatedProject) => {
if (projectInfo?.id) {
ProjectRepository.updateProject(projectInfo.id, updatedProject)
.then((response) => {
const updatedProjectData = {
...projectInfo,
...response.data,
building: projectDetails?.building,
};
setProjectInfo( updatedProjectData );
if (getCachedData(`projectinfo-${projectInfo.id}`)) {
cacheData(`projectinfo-${projectInfo.id}`, updatedProjectData);
}
const projects_list = getCachedData("projectslist");
if (projects_list) {
const updatedProjectsList = projects_list.map((project) =>
project.id === projectInfo.id
? { ...project, ...response.data, tenant: project.tenant }
: project
);
cacheData("projectslist", updatedProjectsList);
}
showToast("Project updated successfully.", "success");
setShowModal(false);
})
.catch((error) => {
showToast(error.message, "error");
});
}
};
return (
<>
{showModal && projectDetails && (
<div
className="modal fade show"
tabIndex="-1"
role="dialog"
style={{ display: "block" }}
aria-hidden="false"
>
<ManageProjectInfo
project={projectDetails}
handleSubmitForm={handleFormSubmit}
onClose={handleClose}
/>
</div>
)}
<tr className="py-8">
<td className="text-start" colSpan={5}>
<strong
className="text-primary cursor-pointer"
onClick={() => navigate(`/projects/${projectInfo.id}`)}
>
{projectInfo.name}
</strong>
</td>
<td className="text-start small">{projectInfo.contactPerson}</td>
<td className="small text-start">
<small>
{projectInfo.startDate
? moment(projectInfo.startDate).format("DD-MMM-YYYY")
: "NA"}
</small>
</td>
<td className="mx-2 text-start small">
{projectInfo.endDate
? moment(projectInfo.endDate).format("DD-MMM-YYYY")
: "NA"}
</td>
<td className="mx-2 text-start small">{projectInfo.plannedWork}</td>
<td className="py-6 mx-2 text-start small align-items-center">
<ProgressBar
plannedWork={projectInfo.plannedWork}
completedWork={projectInfo.completedWork}
className="mb-0"
height="4px"
/>
</td>
<td className="mx-6">
<p className="mb-0">
<span
className={`badge ${getProjectStatusColor(
projectInfo.projectStatusId
)}`}
>
{getProjectStatusName(projectInfo.projectStatusId)}
</span>
</p>
</td>
<td className={`mx-2 ${ManageProject ? "d-sm-table-cell":"d-none"}`}>
<div className="dropdown z-2">
<button
type="button"
className="btn btn-icon btn-text-secondary rounded-pill dropdown-toggle hide-arrow p-0"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<i
className="bx bx-dots-vertical-rounded bx-sm text-muted"
data-bs-toggle="tooltip"
data-bs-offset="0,8"
data-bs-placement="top"
data-bs-custom-class="tooltip-dark"
title="More Action"
></i>
</button>
<ul className="dropdown-menu dropdown-menu-end">
<li>
<a
aria-label="click to View details"
className="dropdown-item"
onClick={() => navigate(`/projects/${projectInfo.id}`)}
>
<i className="bx bx-detail me-2"></i>
<span className="align-left">View details</span>
</a>
</li>
<li onClick={handleShow}>
<a className="dropdown-item">
<i className="bx bx-pencil me-2"></i>
<span className="align-left">Modify</span>
</a>
</li>
<li
onClick={() =>
navigate(`/activities/records?project=${projectInfo.id}`)
}
>
<a className="dropdown-item">
<i className="bx bx-task me-2"></i>
<span className="align-left">Activities</span>
</a>
</li>
</ul>
</div>
</td>
</tr>
</>
);
};
export default ProjectListView;

View File

@ -1,25 +1,32 @@
export const ProjectStatus =(statusId)=>{ export const getProjectStatusName = (statusId) => {
switch (statusId) { switch (statusId) {
case 1: case 1:
return "Active" return "Active";
break; case 2:
case 2: return "On Hold";
return "On Hold" // case 3:
break; // return "Suspended";
// case 3: case 3:
// return "Suspended" return "Inactive";
// break; case 4:
case 3: return "Completed";
return "Inactive" }
break; };
case 4:
return "Completed"
break;
default:
break;
}
}
export const getProjectStatusColor = (statusId) => {
switch (statusId) {
case 1:
return "bg-label-success";
case 2:
return "bg-label-warning";
case 3:
return "bg-label-info";
case 4:
return "bg-label-secondary";
case 5:
return "bg-label-dark";
}
};
// for different color for each user // for different color for each user
export function hashString(str) { export function hashString(str) {