Merge branch 'Service_Project_Managment' of https://git.marcoaiot.com/admin/marco.pms.web into Advance_Payment_List

This commit is contained in:
Kartik Sharma 2025-11-19 11:27:51 +05:30
commit 4b5f8756b3
15 changed files with 272 additions and 263 deletions

View File

@ -6,11 +6,11 @@ import { ITEMS_PER_PAGE } from "../../utils/constants";
const ProjectCompletionChart = () => {
const [currentPage, setCurrentPage] = useState(1);
const { data: projects, isLoading: loading, isError, error } = useProjects(ITEMS_PER_PAGE,currentPage);
const { data: projects, isLoading: loading, isError, error } = useProjects(50,currentPage);
// Bar chart logic
const projectNames = projects?.data.map((p) => p.name) || [];
const projectNames = projects?.data?.map((p) => p.name) || [];
const projectProgress =
projects?.map((p) => {
projects?.data?.map((p) => {
const completed = p.completedWork || 0;
const planned = p.plannedWork || 1;
const percent = planned ? (completed / planned) * 100 : 0;

View File

@ -13,15 +13,9 @@ import { useNavigate } from "react-router-dom";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import { useProjectContext } from "../../pages/project/ProjectPage";
import usePagination from "../../hooks/usePagination";
import Pagination from "../common/Pagination";
const ProjectListView = ({
currentItems,
selectedStatuses,
handleStatusChange,
setCurrentPage,
totalPages,
isLoading,
}) => {
const ProjectListView = ({ data, currentPage, totalPages, paginate }) => {
const dispatch = useDispatch();
const navigate = useNavigate();
const { setMangeProject } = useProjectContext();
@ -132,152 +126,103 @@ const ProjectListView = ({
return (
<div className="card page-min-h py-4 px-6 shadow-sm">
<div
className="table-responsive text-nowrap page-min-h"
>
<table className="table table-hover align-middle m-0">
<thead className="border-bottom ">
<tr>
{projectColumns.map((col) => (
<th key={col.key} colSpan={col.colSpan} className={`${col.className} table_header_border`}>
{col.label}
</th>
))}
<th className="text-center py-3">Action</th>
</tr>
</thead>
<tbody>
{currentItems?.map((project) => (
<tr key={project.id}>
<div className="table-responsive text-nowrap page-min-h">
<table className="table table-hover align-middle m-0">
<thead className="border-bottom ">
<tr>
{projectColumns.map((col) => (
<td
<th
key={col.key}
colSpan={col.colSpan}
className={`${col.className} py-5`}
style={{ paddingTop: "20px", paddingBottom: "20px" }}
className={`${col.className} table_header_border`}
>
{col.getValue
? col.getValue(project)
: project[col.key] || "N/A"}
</td>
{col.label}
</th>
))}
<td
className={`mx-2 ${
canManageProject ? "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 cursor-pointer"
>
<i className="bx bx-detail me-2"></i>
<span className="align-left">View details</span>
</a>
</li>
<li>
<a
className="dropdown-item cursor-pointer"
onClick={() =>
setMangeProject({
isOpen: true,
Project: project.id,
})
}
>
<i className="bx bx-pencil me-2"></i>
<span className="align-left">Modify</span>
</a>
</li>
<li onClick={() => handleViewActivities(project.id)}>
<a className="dropdown-item cursor-pointer">
<i className="bx bx-task me-2"></i>
<span className="align-left">Activities</span>
</a>
</li>
</ul>
</div>
</td>
<th className="text-center py-3">Action</th>
</tr>
))}
</tbody>
</table>
</thead>
<tbody>
{data?.map((project) => (
<tr key={project.id}>
{projectColumns.map((col) => (
<td
key={col.key}
colSpan={col.colSpan}
className={`${col.className} py-5`}
style={{ paddingTop: "20px", paddingBottom: "20px" }}
>
{col.getValue
? col.getValue(project)
: project[col.key] || "N/A"}
</td>
))}
<td
className={`mx-2 ${
canManageProject ? "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 cursor-pointer"
>
<i className="bx bx-detail me-2"></i>
<span className="align-left">View details</span>
</a>
</li>
<li>
<a
className="dropdown-item cursor-pointer"
onClick={() =>
setMangeProject({
isOpen: true,
Project: project.id,
})
}
>
<i className="bx bx-pencil me-2"></i>
<span className="align-left">Modify</span>
</a>
</li>
<li onClick={() => handleViewActivities(project.id)}>
<a className="dropdown-item cursor-pointer">
<i className="bx bx-task me-2"></i>
<span className="align-left">Activities</span>
</a>
</li>
</ul>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{isLoading && (
<div className="py-4">
{" "}
{isLoading && <p className="text-center">Loading...</p>}
{!isLoading && filteredProjects.length === 0 && (
<p className="text-center text-muted">No projects found.</p>
)}
</div>
)}
{!isLoading && currentItems.length === 0 && (
<div className="py-6">
<p className="text-center text-muted">No projects found.</p>
</div>
)}
{!isLoading && totalPages > 1 && (
<nav>
<ul className="pagination pagination-sm justify-content-end py-2">
<li className={`page-item ${currentPage === 1 && "disabled"}`}>
<button
className="page-link"
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
>
&laquo;
</button>
</li>
{[...Array(totalPages)].map((_, i) => (
<li
key={i}
className={`page-item ${currentPage === i + 1 && "active"}`}
>
<button
className="page-link"
onClick={() => setCurrentPage(i + 1)}
>
{i + 1}
</button>
</li>
))}
<li
className={`page-item ${
currentPage === totalPages && "disabled"
}`}
>
<button
className="page-link"
onClick={() =>
setCurrentPage((p) => Math.min(totalPages, p + 1))
}
>
&raquo;
</button>
</li>
</ul>
</nav>
)}
<Pagination
currentPage={currentPage}
totalPages={totalPages}
paginate={paginate}
/>
</div>
);
};

View File

@ -11,7 +11,7 @@ import { daysLeft, getJobStatusBadge } from "../../utils/appUtils";
import HoverPopup from "../common/HoverPopup";
import ChangeStatus from "./ChangeStatus";
import { useParams } from "react-router-dom";
import { STATUS_JOB_DONE } from "../../utils/constants";
import { STATUS_JOB_CLOSED } from "../../utils/constants";
import Tooltip from "../common/Tooltip";
const ManageJobTicket = ({ Job }) => {
@ -58,7 +58,7 @@ const ManageJobTicket = ({ Job }) => {
<span className={`badge ${getJobStatusBadge(data?.status?.id)}`}>
{data?.status?.displayName}
</span>
{STATUS_JOB_DONE !== data?.status?.id && (
{STATUS_JOB_CLOSED !== data?.status?.id && (
<HoverPopup
id="STATUS_CHANEG"
title="Change Status"

View File

@ -95,7 +95,7 @@ const ServiceProjectProfile = () => {
<li className="d-flex justify-content-center mt-4"> {/* Added mt-4 for some top margin */}
<li className="d-flex justify-content-center mt-4"> {/* Added mt-4 for some top margin */}
<a className="d-flex justify-content-center mt-4"> {/* Added mt-4 for some top margin */}
<button
type="button"
@ -107,7 +107,7 @@ const ServiceProjectProfile = () => {
Modify Details
</button>
</li>
</a>
</li>
</ul>

View File

@ -1,56 +0,0 @@
import React from 'react'
const InputFieldSuggesstion = () => {
return (
<div className="position-relative">
<input
className="form-control form-control-sm"
value={value}
onChange={handleInputChange}
onBlur={() => setTimeout(() => setShowSuggestions(false), 150)}
onFocus={() => {
if (value) setShowSuggestions(true);
}}
disabled={disabled}
/>
{showSuggestions && filteredList.length > 0 && (
<ul
className="list-group shadow-sm position-absolute w-100 bg-white border zindex-tooltip"
style={{
maxHeight: "180px",
overflowY: "auto",
marginTop: "2px",
zIndex: 1000,
borderRadius:"0px"
}}
>
{filteredList.map((org) => (
<li
key={org}
className="list-group-item list-group-item-action border-none "
style={{
cursor: "pointer",
padding: "5px 12px",
fontSize: "14px",
transition: "background-color 0.2s",
}}
onMouseDown={() => handleSelectSuggestion(org)}
onMouseEnter={(e) =>
(e.currentTarget.style.backgroundColor = "#f8f9fa")
}
onMouseLeave={(e) =>
(e.currentTarget.style.backgroundColor = "transparent")
}
>
{org}
</li>
))}
</ul>
)}
{error && <small className="danger-text">{error}</small>}
</div>
)
}
export default InputFieldSuggesstion

View File

@ -0,0 +1,90 @@
import React, { useEffect, useRef, useState } from "react";
import Label from "../Label";
const InputSuggessionField = ({
organizationList = [],
value,
onChange,
error,
disabled=false
}) => {
const [open, setOpen] = useState(false);
const dropdownRef = useRef(null);
useEffect(() => {
const handleClickOutside = (event) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
const selectedOption = options.find((opt) => opt[valueKey] === value);
const displayText = selectedOption ? selectedOption[labelKey] : placeholder;
const handleSelect = (option) => {
onChange(option[valueKey]);
setOpen(false);
};
const toggleDropdown = () => setOpen((prev) => !prev);
return (
<div className="mb-3 position-relative" ref={dropdownRef}>
{label && (
<Label className="form-label" required={required}>
{label}
</Label>
)}
<button
type="button"
className={`select2-icons form-select d-flex align-items-center justify-content-between ${
open ? "show" : ""
}`}
onClick={toggleDropdown}
disabled={isLoading}
>
<span
className={`text-truncate ${!selectedOption ? "text-muted" : ""}`}
>
{isLoading ? "Loading..." : displayText}
</span>
</button>
{open && !isLoading && (
<ul
className="dropdown-menu w-100 shadow-sm show animate__fadeIn"
style={{
position: "absolute",
top: "100%",
left: 0,
zIndex: 1050,
marginTop: "4px",
borderRadius: "0.375rem",
overflow: "hidden",
}}
>
{options.map((option, i) => (
<li key={i}>
<button
type="button"
className={`dropdown-item ${
option[valueKey] === value ? "active" : ""
}`}
onClick={() => handleSelect(option)}
>
{option[labelKey]}
</button>
</li>
))}
</ul>
)}
</div>
);
};
export default InputSuggessionField;

View File

@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from "react";
import Label from "../Label";
import { useDebounce } from "../../../utils/appUtils";
import { useEmployeesName } from "../../../hooks/useEmployees";
import { useProjectBothName } from "../../../hooks/useProjects";
const SelectEmployeeServerSide = ({
label = "Select",
@ -154,7 +155,9 @@ const SelectEmployeeServerSide = ({
)}
{!isLoading && options.length === 0 && (
<li className="dropdown-item text-muted text-center">No results found</li>
<li className="dropdown-item text-muted text-center">
No results found
</li>
)}
{!isLoading &&
@ -183,24 +186,29 @@ const SelectEmployeeServerSide = ({
};
export default SelectEmployeeServerSide;
export const SelectProjectField = ()=>{
const [searchText, setSearchText] = useState("");
export const SelectProjectField = ({
label = "Select",
placeholder = "Select Project",
required = false,
value = null,
onChange,
valueKey = "id",
isFullObject = false,
isMultiple = false,
isAllProject = false,
}) => {
const [searchText, setSearchText] = useState("");
const debounce = useDebounce(searchText, 300);
const { data, isLoading } = useEmployeesName(
projectId,
debounce,
isAllEmployee
);
const { data, isLoading } = useProjectBothName(debounce);
const options = data?.data ?? [];
const options = data ?? [];
const [open, setOpen] = useState(false);
const dropdownRef = useRef(null);
const getDisplayName = (emp) => {
if (!emp) return "";
return `${emp.firstName || ""} ${emp.lastName || ""}`.trim();
const getDisplayName = (project) => {
if (!project) return "";
return `${project.name || ""}`.trim();
};
/** -----------------------------
@ -304,7 +312,7 @@ export const SelectProjectField = ()=>{
top: "100%",
left: 0,
zIndex: 1050,
marginTop: "4px",
marginTop: "2px",
borderRadius: "0.375rem",
overflow: "hidden",
}}
@ -324,7 +332,9 @@ export const SelectProjectField = ()=>{
)}
{!isLoading && options.length === 0 && (
<li className="dropdown-item text-muted text-center">No results found</li>
<li className="dropdown-item text-muted text-center">
No results found
</li>
)}
{!isLoading &&
@ -350,4 +360,4 @@ export const SelectProjectField = ()=>{
)}
</div>
);
};
};

View File

@ -10,7 +10,7 @@
display: flex;
justify-content: space-between;
align-items: center;
padding: 5px;
padding: 4px;
border: 1px solid #ddd;
border-radius: 5px;
cursor: pointer;

View File

@ -101,7 +101,7 @@ const SelectMultiple = ({
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
className="multi-select-dropdown-search-input"
style={{ width: "100%", padding: 4 }}
style={{ width: "100%", }}
/>
</div>
@ -177,7 +177,7 @@ const SelectMultiple = ({
return (
<span
key={val}
className="badge bg-label-primary mx-1 py-2 mb-1"
className="badge bg-label-primary mx-1 py-2"
>
{label}
</span>

View File

@ -20,12 +20,15 @@ export const useCurrentService = () => {
// ------------------------------Query-------------------
export const useProjects = (pageSize,pageNumber) => {
export const useProjects = (pageSize, pageNumber) => {
const loggedUser = useSelector((store) => store.globalVariables.loginUser);
return useQuery({
queryKey: ["ProjectsList",pageSize,pageNumber],
queryKey: ["ProjectsList", pageSize, pageNumber],
queryFn: async () => {
const response = await ProjectRepository.getProjectList(pageSize,pageNumber);
const response = await ProjectRepository.getProjectList(
pageSize,
pageNumber
);
return response?.data;
},
enabled: !!loggedUser,
@ -153,7 +156,7 @@ export const useProjectsAllocationByEmployee = (employeeId) => {
return { projectList, loading: isLoading, error, refetch };
};
export const useProjectName = (provideAll=false) => {
export const useProjectName = (provideAll = false) => {
const {
data = [],
isLoading,
@ -161,7 +164,7 @@ export const useProjectName = (provideAll=false) => {
refetch,
isError,
} = useQuery({
queryKey: ["basicProjectNameList",provideAll],
queryKey: ["basicProjectNameList", provideAll],
queryFn: async () => {
const res = await ProjectRepository.projectNameList(provideAll);
return res.data || res;
@ -179,6 +182,19 @@ export const useProjectName = (provideAll=false) => {
};
};
export const useProjectBothName = (searchString) => {
return useQuery({
queryKey: ["basic_bothProject", searchString],
queryFn: async () => {
const res = await ProjectRepository.projectNameListAll(searchString);
return res.data || res;
},
onError: (error) => {
showToast(error.message || "Error while Fetching project Name", "error");
},
});
};
export const useProjectInfra = (projectId, serviceId) => {
const {
data: projectInfra,
@ -337,7 +353,7 @@ export const useEmployeeForTaskAssign = (
// -- -------------Mutation-------------------------------
export const useCreateProject = ( onSuccessCallback ) => {
export const useCreateProject = (onSuccessCallback) => {
const queryClient = useQueryClient();
return useMutation({
@ -368,7 +384,7 @@ export const useCreateProject = ( onSuccessCallback ) => {
});
};
export const useUpdateProject = ( onSuccessCallback ) => {
export const useUpdateProject = (onSuccessCallback) => {
const queryClient = useQueryClient();
const { mutate, isPending, isSuccess, isError } = useMutation({
mutationFn: async ({ projectId, payload }) => {

View File

@ -52,7 +52,7 @@ const ServiceProjectDisplay = ({ listView ,selectedStatuses }) => {
<p>List</p>
) : (
filteredProjects?.map((project) => (
<ServiceProjectCard project={project} isCore={false} />
<ServiceProjectCard key={project.id} project={project} isCore={false} />
))
)}

View File

@ -83,7 +83,7 @@ const ProjectPage = () => {
<div className="card cursor-pointer mb-5">
<div className="card-body py-3 px-6 pb-1">
<div className="d-flex flex-wrap justify-content-between align-items-start">
<div className="d-flex flex-wrap justify-content-between align-items-center">
{/* LEFT SIDE — DATE TOGGLE BUTTONS */}
<div className="mb-2">
<div className="d-inline-flex border rounded-pill overflow-hidden shadow-none">
@ -179,7 +179,7 @@ const ProjectPage = () => {
{HasManageProject && (
<button
type="button"
className="btn btn-primary btn-sm d-flex align-items-center"
className="btn btn-primary btn-sm d-flex align-items-center my-2"
onClick={() =>
coreProjects
? setMangeProject({ isOpen: true, Project: null }) // Organization Project Infra

View File

@ -11,7 +11,12 @@ import { ITEMS_PER_PAGE, PROJECT_STATUS } from "../../utils/constants";
import usePagination from "../../hooks/usePagination";
import ManageProjectInfo from "../../components/Project/ManageProjectInfo";
const ProjectsDisplay = ({ listView, searchTerm, selectedStatuses, handleStatusChange }) => {
const ProjectsDisplay = ({
listView,
searchTerm,
selectedStatuses,
handleStatusChange,
}) => {
const [currentPage, setCurrentPage] = useState(1);
const {
manageProject,
@ -22,15 +27,12 @@ const ProjectsDisplay = ({ listView, searchTerm, selectedStatuses, handleStatusC
const [projectList, setProjectList] = useState([]);
const { data, isLoading, isError, error } = useProjects(ITEMS_PER_PAGE, 1);
const filteredProjects =
data?.data?.filter((project) => {
const statusId =
project.projectStatusId ??
project?.status?.id ??
project?.statusId;
project.projectStatusId ?? project?.status?.id ?? project?.statusId;
const matchesStatus = selectedStatuses.includes(statusId);
@ -41,15 +43,12 @@ const ProjectsDisplay = ({ listView, searchTerm, selectedStatuses, handleStatusC
return matchesStatus && matchesSearch;
}) ?? [];
const paginate = (page) => {
if (page >= 1 && page <= (data?.totalPages ?? 1)) {
setCurrentPage(page);
}
};
const sortingProject = (projects) => {
if (!isLoading && Array.isArray(projects)) {
const grouped = {};
@ -97,17 +96,15 @@ const ProjectsDisplay = ({ listView, searchTerm, selectedStatuses, handleStatusC
<p>{error.message}</p>
</div>
);
return (
<div className="row">
{listView ? (
<ProjectListView
currentItems={currentItems}
selectedStatuses={selectedStatuses}
handleStatusChange={handleStatusChange}
setCurrentPage={setCurrentPage}
totalPages={totalPages}
isLoading={isLoading}
data={projectList}
currentPage={currentPage}
totalPages={data?.totalPages}
paginate={paginate}
/>
) : (
<ProjectCardView
@ -118,7 +115,6 @@ const ProjectsDisplay = ({ listView, searchTerm, selectedStatuses, handleStatusC
/>
)}
{/* Project Manage Update or Create */}
{manageProject?.isOpen && (
<GlobalModel
size="md"
@ -131,7 +127,6 @@ const ProjectsDisplay = ({ listView, searchTerm, selectedStatuses, handleStatusC
/>
</GlobalModel>
)}
</div>
);
};

View File

@ -1,17 +1,24 @@
import { api } from "../utils/axiosClient";
const ProjectRepository = {
getProjectList: (pageSize,pageNumber) => api.get(`/api/project/list?pageSize=${pageSize}&pageNumber=${pageNumber}`),
getProjectList: (pageSize, pageNumber) =>
api.get(`/api/project/list?pageSize=${pageSize}&pageNumber=${pageNumber}`),
getProjectByprojectId: (projetid) =>
api.get(`/api/project/details/${projetid}`),
getProjectAllocation: (projectId, serviceId, organizationId, employeeStatus) => {
getProjectAllocation: (
projectId,
serviceId,
organizationId,
employeeStatus
) => {
let url = `/api/project/allocation/${projectId}`;
const params = [];
if (organizationId) params.push(`organizationId=${organizationId}`);
if (serviceId) params.push(`serviceId=${serviceId}`);
if (employeeStatus !== undefined) params.push(`includeInactive=${employeeStatus}`);
if (employeeStatus !== undefined)
params.push(`includeInactive=${employeeStatus}`);
if (params.length > 0) {
url += `?${params.join("&")}`;
@ -20,7 +27,6 @@ const ProjectRepository = {
return api.get(url);
},
getEmployeesByProject: (projectId) =>
api.get(`/api/Project/employees/get/${projectId}`),
@ -40,10 +46,13 @@ const ProjectRepository = {
api.get(`/api/project/allocation-histery/${id}`),
updateProjectsByEmployee: (id, data) =>
api.post(`/api/project/assign-projects/${id}`, data),
projectNameList: (provideAll) => api.get(`/api/project/list/basic?provideAll=${provideAll}`),
projectNameList: (provideAll) =>
api.get(`/api/project/list/basic?provideAll=${provideAll}`),
projectNameListAll: (searchString) =>
api.get(`/api/project/list/basic/all?searchString=${searchString}`),
getProjectDetails: (id) => api.get(`/api/project/details/${id}`),
getProjectInfraByproject: (projectId, serviceId) => {
let url = `/api/project/infra-details/${projectId}`;
if (serviceId) {
@ -85,7 +94,7 @@ const ProjectRepository = {
api.get(`/api/Project/get/assigned/services/${projectId}`),
getProjectAssignedOrganizations: (projectId) =>
api.get(`/api/Project/get/assigned/organization/${projectId}`),
getProjectAssignedOrganizationsName: (projectId) =>
getProjectAssignedOrganizationsName: (projectId) =>
api.get(`/api/Project/get/assigned/organization/dropdown/${projectId}`),
getEmployeeForTaskAssign: (projectId, serviceId, organizationId) => {

View File

@ -208,6 +208,6 @@ export const PAYEE_RECURRING_EXPENSE = [
//#region Service Project and Jobs
export const STATUS_JOB_DONE = "ed10ab57-dbaa-4ca5-8ecd-56745dcbdbd7"
export const STATUS_JOB_CLOSED = "3ddeefb5-ae3c-4e10-a922-35e0a452bb69"
//#endregion