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

This commit is contained in:
Kartik Sharma 2025-11-17 09:43:27 +05:30
commit 85b81b35da
10 changed files with 337 additions and 53 deletions

View File

@ -4,8 +4,8 @@ import { useServiceProject } from "../../hooks/useServiceProject";
import { formatUTCToLocalTime } from "../../utils/dateUtils"; import { formatUTCToLocalTime } from "../../utils/dateUtils";
const ServiceProjectProfile = () => { const ServiceProjectProfile = () => {
const { id } = useParams(); const { projectId } = useParams();
const { data, isLoading, isError, error } = useServiceProject(id); const { data, isLoading, isError, error } = useServiceProject(projectId);
if (isLoading) { if (isLoading) {
return <div className="">Loadng.</div>; return <div className="">Loadng.</div>;
} }

View File

@ -1,15 +1,17 @@
import React from 'react' import React from 'react'
import { useModal } from '../../../hooks/useAuth' import { useModal } from '../../../hooks/useAuth'
import { useParams } from 'react-router-dom';
const ProjectTeam = () => { const ProjectTeam = () => {
const {onOpen} = useModal("ServiceTeamAllocation"); const {onOpen} = useModal("ServiceTeamAllocation");
const {projectId}= useParams();
return ( return (
<div className='card page-min-h mt-2'> <div className='card page-min-h mt-2'>
<div className='row text-end'> <div className='row text-end'>
<div className='col-12'> <div className='col-12'>
<div className='p-2'><button onClick={onOpen} className='btn btn-sm btn-primary'><i className='bx bx-plus-circle me-2'></i>Assign Employee</button></div> <div className='p-2'><button onClick={()=>onOpen({projectId:projectId})} className='btn btn-sm btn-primary'><i className='bx bx-plus-circle me-2'></i>Assign Employee</button></div>
</div> </div>
</div> </div>
{/* <ServiceProjectTeamList/> */} {/* <ServiceProjectTeamList/> */}

View File

@ -1,4 +1,4 @@
import React, { useState } from "react"; import React, { useEffect, useState } from "react";
import { useModal } from "../../../hooks/useAuth"; import { useModal } from "../../../hooks/useAuth";
import Modal from "../../common/Modal"; import Modal from "../../common/Modal";
import Label from "../../common/Label"; import Label from "../../common/Label";
@ -6,29 +6,136 @@ import { useTeamRole } from "../../../hooks/masterHook/useMaster";
import SelectField from "../../common/Forms/SelectField"; import SelectField from "../../common/Forms/SelectField";
import SelectFieldServerSide from "../../common/Forms/SelectFieldServerSide"; import SelectFieldServerSide from "../../common/Forms/SelectFieldServerSide";
import SelectEmployeeServerSide from "../../common/Forms/SelectFieldServerSide"; import SelectEmployeeServerSide from "../../common/Forms/SelectFieldServerSide";
import Avatar from "../../common/Avatar";
import { useParams } from "react-router-dom";
import { EmployeeChip } from "../../common/Chips";
import {
useAllocationServiceProjectTeam,
useServiceProjectTeam,
} from "../../../hooks/useServiceProject";
import { SpinnerLoader } from "../../common/Loader";
import showToast from "../../../services/toastService";
const ServiceProjectTeamAllocation = () => { const ServiceProjectTeamAllocation = () => {
const { isOpen, onClose } = useModal("ServiceTeamAllocation"); const { isOpen, onClose, data: Project } = useModal("ServiceTeamAllocation");
const [deletingId, setSeletingId] = useState(null);
const {
data: Team,
isLoading: isTeamLoading,
isError: isTeamError,
error: teamError,
} = useServiceProjectTeam(Project?.projectId, true);
const [isManageEmployee, setIsMenageEmployee] = useState(false);
const [nonDuplicateEmployee, setNonDuplicateEmployee] = useState([]);
const [selectedTeam, setSelectTeam] = useState(null); const [selectedTeam, setSelectTeam] = useState(null);
const [selectedEmployees, setSelectedEmployees] = useState(null); const [selectedEmployees, setSelectedEmployees] = useState([]);
const { data, isLoading, isError, error } = useTeamRole(); const { data, isLoading } = useTeamRole();
console.log(selectedEmployees);
const TeamAllocation = ( const TeamAllocationColumns = [
<div className="row text-start"> {
<div className="col-6"> key: "team",
label: "Role",
getValue: (e) => e?.teamName || "-",
},
];
const handleRemove = (id) => {
setSelectedEmployees((prev) => prev.filter((e) => e.id !== id));
};
const { mutate: AllocationTeam, isPending } = useAllocationServiceProjectTeam(
() => {
setSelectedEmployees([]);
setSeletingId(null);
}
);
const AssignEmployee = () => {
let payload = selectedEmployees.map((e) => {
return {
projectId: Project?.projectId,
employeeId: e.id,
teamRoleId: selectedTeam,
isActive: true,
};
});
AllocationTeam({ payload, isActive: true });
};
const handleDeAllocation = (emp) => {
setSeletingId(emp?.id);
let payload = [
{
projectId: Project?.projectId,
employeeId: emp.employee.id,
teamRoleId: emp.teamRole.id,
isActive: false,
},
];
AllocationTeam({payload:payload,isActive:false});
};
useEffect(() => {
if(selectedEmployees?.length > 0 && !selectedTeam){
handleRemove(selectedEmployees[0]?.id)
showToast(
`Please select a first role`,
"warning"
);
}
if (Team?.length > 0 && selectedEmployees?.length > 0) {
setNonDuplicateEmployee((prev) => {
let updated = [...prev];
selectedEmployees.forEach((selectedEmployee) => {
const alreadyAllocated = Team.some(
(e) =>
e?.employee?.id === selectedEmployee?.id &&
e?.teamRole?.id === selectedTeam
);
if (alreadyAllocated) {
handleRemove(selectedEmployee?.id);
showToast(
`${selectedEmployee.firstName} ${selectedEmployee.lastName} already allocated with same role`,
"warning"
);
}
});
return updated;
});
}
}, [Team, selectedEmployees, selectedTeam]);
const TeamAllocationBody = (
<div className=" text-start">
<div className="row">
<div className="d-flex justify-content-end">
<button
className="btn btn-sm btn-primary"
onClick={() => setIsMenageEmployee(!isManageEmployee)}
>
Manage Employee
</button>
</div>
{isManageEmployee && (
<>
<div className="col-12 col-md-6 mb-3">
<SelectField <SelectField
label="Select Team" label="Select Role"
required required
options={data} options={data}
value={selectedTeam} value={selectedTeam}
labelKey="name" labelKey="name"
valueKey="id" valueKey="id"
valueKey="name" onChange={(e) => {
onChange={(e) => setSelectTeam(e)} setSelectedEmployees([]);
setSelectTeam(e);
}}
isLoading={isLoading} isLoading={isLoading}
/> />
</div> </div>
<div className="col-6">
<div className="col-12 col-md-6 mb-3">
<SelectEmployeeServerSide <SelectEmployeeServerSide
isMultiple={true} isMultiple={true}
isFullObject={true} isFullObject={true}
@ -36,15 +143,96 @@ const ServiceProjectTeamAllocation = () => {
onChange={setSelectedEmployees} onChange={setSelectedEmployees}
/> />
</div> </div>
<div className="col-12 d-flex flex-row gap-2 flex-wrap">
{selectedEmployees.map((e) => (
<EmployeeChip handleRemove={handleRemove} employee={e} />
))}
</div>
</>
)}
</div>
{(selectedEmployees?.length > 0 && isManageEmployee ) && (
<div className="d-flex justify-content-end">
{" "}
<button
className="btn btn-sm btn-primary"
disabled={isPending}
onClick={AssignEmployee}
>
{isPending && !deletingId ? "Please wait..." : "Save"}
</button>
</div>
)}
<div className="col-12">
<table className="table text-center ">
<thead>
<tr>
<th>Name</th>
<th>Role</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{Team?.length > 0 ? (
Team?.map((emp) => (
<tr key={emp?.id}>
<td className="w-min">
<div className="d-flex align-items-center ">
{" "}
<Avatar
size="xs"
firstName={emp?.employee?.firstName}
lastName={emp?.employee?.lastName}
/>
<span className="fw-medium">{`${emp?.employee?.firstName} ${emp?.employee?.lastName}`}</span>
</div>
</td>
<td>{emp?.teamRole?.name}</td>
<td className="">
{deletingId === emp.id ? (
<div className="spinner-border spinner-border-sm "></div>
) : (
<span disabled={isPending}>
<i
className="bx bx-trash bx-sm text-danger cursor-pointer"
onClick={() => handleDeAllocation(emp)}
></i>
</span>
)}
</td>
</tr>
))
) : (
<tr>
<td
colSpan={3}
className="text-muted border-0 d-flex justify-content-center "
>
{isTeamLoading ? (
<SpinnerLoader />
) : (
<div className="bg-light-secondary py-3 w-50 m-auto my-2">
<i className="bx bx-box bx-md text-primary"></i>
<p className="fw-medium mt-3"> Please Add a Employee </p>
</div>
)}
</td>
</tr>
)}
</tbody>
</table>
</div>
</div> </div>
); );
return ( return (
<Modal <Modal
size="lg" size="lg"
title={"Team Allocation"} title={"Team Allocation"}
isOpen={isOpen} isOpen={isOpen}
onClose={onClose} onClose={onClose}
body={TeamAllocation} body={TeamAllocationBody}
/> />
); );
}; };

View File

@ -0,0 +1,45 @@
import React from 'react'
export const EmployeeChip = ({handleRemove,employee}) => {
return(
<span
key={employee?.id}
className="tagify__tag d-inline-flex align-items-center me-1 mb-1"
role="listitem"
>
<div className="d-flex align-items-center">
{employee?.photo ? (
<span className="tagify__tag__avatar-wrap me-1">
<img
src={employee?.avataremployeerl || "/defaemployeelt-avatar.png"}
alt={`${employee?.firstName || ""} ${employee?.lastName || ""}`}
style={{ width: 12, height: 12, objectFit: "cover" }}
/>
</span>
) : (
<div className="avatar avatar-xs me-2">
<span className="avatar-initial roemployeended-circle bg-label-secondary">
{employee?.firstName?.[0] || ""}
{employee?.lastName?.[0] || ""}
</span>
</div>
)}
<div className="d-flex flex-colemployeemn">
<span className="tagify__tag-text">
{employee?.firstName} {employee?.lastName}
</span>
</div>
</div>
<bemployeetton
type="bemployeetton"
className="tagify__tag__removeBtn border-none"
onClick={() => handleRemove(employee?.id)}
aria-label={`Remove ${employee?.firstName}`}
title="Remove"
/>
</span>
)
}

View File

@ -128,7 +128,7 @@ const SelectEmployeeServerSide = ({
{/* DROPDOWN */} {/* DROPDOWN */}
{open && ( {open && (
<ul <ul
className="dropdown-menu w-100 shadow-sm show animate__fadeIn" className="dropdown-menu w-100 shadow-sm show animate__fadeIn h-64 overflow-auto rounded"
style={{ style={{
position: "absolute", position: "absolute",
top: "100%", top: "100%",

View File

@ -25,16 +25,24 @@ import showToast from "../services/toastService";
export const useModal = (modalType) => { export const useModal = (modalType) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const isOpen = useSelector(
(state) => state.localVariables.modals[modalType]?.isOpen const modalState = useSelector(
(state) => state.localVariables.modals[modalType] || {}
); );
const onOpen = (data = {}) => dispatch(openModal({ modalType, data })); const { isOpen = false, data = null } = modalState;
const onOpen = (payload = {}) =>
dispatch(openModal({ modalType, data: payload }));
const onClose = () => dispatch(closeModal({ modalType })); const onClose = () => dispatch(closeModal({ modalType }));
const onToggle = () => dispatch(toggleModal({ modalType })); const onToggle = () => dispatch(toggleModal({ modalType }));
return { isOpen, onOpen, onClose, onToggle }; return { isOpen, data, onOpen, onClose, onToggle };
}; };
export const useSubscription = (frequency) => { export const useSubscription = (frequency) => {
return useQuery({ return useQuery({
queryKey: ["subscriptionPlans", frequency], queryKey: ["subscriptionPlans", frequency],

View File

@ -32,6 +32,20 @@ export const useServiceProject = (projectId) => {
enabled: !!projectId, enabled: !!projectId,
}); });
}; };
export const useServiceProjectTeam = (projectId, isActive) => {
return useQuery({
queryKey: ["serviceProjectTeam", projectId, isActive],
queryFn: async () => {
const response = await ServiceProjectRepository.GetAllocatedEmployees(
projectId,
isActive
);
return response.data;
},
enabled: !!projectId,
});
};
export const useCreateServiceProject = (onSuccessCallback) => { export const useCreateServiceProject = (onSuccessCallback) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
@ -104,14 +118,22 @@ export const useActiveInActiveServiceProject = (onSuccessCallback) => {
}); });
}; };
export const useAllocationTeam=()=>{ export const useAllocationServiceProjectTeam = (onSuccessCallback) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn:async(paylaod)=> await ServiceProjectRepository.AllocateEmployee(paylaod), mutationFn: async ({ payload, isActive }) =>
await ServiceProjectRepository.AllocateEmployee(payload),
onSuccess: (data, variables) => { onSuccess: (data, variables) => {
// queryClient.invalidateQueries({ queryKey: ["serviceProjects"] }); queryClient.invalidateQueries({ queryKey: ["serviceProjectTeam"] });
if (onSuccessCallback) onSuccessCallback(); if (onSuccessCallback) onSuccessCallback();
showToast(`${variables?.length} employee allocated successfully`, "success"); if (variables.isActive) {
showToast(
`${variables?.payload.length} Employee allocated successfully`,
"success"
);
} else {
showToast(`Employee DeAllocated successfully`, "success");
}
}, },
onError: (error) => { onError: (error) => {
showToast( showToast(
@ -121,8 +143,9 @@ export const useAllocationTeam=()=>{
"error" "error"
); );
}, },
}) });
} };
//#endregion //#endregion
//#region Service Jobs //#region Service Jobs

View File

@ -11,7 +11,12 @@ export const ServiceProjectRepository = {
api.put(`/api/ServiceProject/edit/${id}`, data), api.put(`/api/ServiceProject/edit/${id}`, data),
DeleteServiceProject: (id, isActive = false) => DeleteServiceProject: (id, isActive = false) =>
api.delete(`/api/ServiceProject/delete/${id}?isActive=${isActive}`), api.delete(`/api/ServiceProject/delete/${id}?isActive=${isActive}`),
AllocateEmployee: (data) => api.post(`/api/ServiceProject/manage/allocation`), AllocateEmployee: (data) =>
api.post(`/api/ServiceProject/manage/allocation`, data),
GetAllocatedEmployees: (projectId, isActive) =>
api.get(
`/api/ServiceProject/get/allocation/list?projectId=${projectId}&isActive=${isActive} `
),
//#region Job //#region Job
CreateJob: (data) => api.post(`/api/ServiceProject/job/create`, data), CreateJob: (data) => api.post(`/api/ServiceProject/job/create`, data),

View File

@ -95,7 +95,7 @@ const router = createBrowserRouter(
{ path: "/projects", element: <ProjectPage /> }, { path: "/projects", element: <ProjectPage /> },
{ path: "/projects/details", element: <ProjectDetails /> }, { path: "/projects/details", element: <ProjectDetails /> },
{ path: "/project/manage/:projectId", element: <ManageProject /> }, { path: "/project/manage/:projectId", element: <ManageProject /> },
{ path: "service-projects/:id", element: <ServiceProjectDetail /> }, { path: "/service-projects/:projectId", element: <ServiceProjectDetail /> },
{path:"/service/job",element:<ManageJob/>}, {path:"/service/job",element:<ManageJob/>},
{ path: "/employees", element: <EmployeeList /> }, { path: "/employees", element: <EmployeeList /> },

View File

@ -35,9 +35,9 @@ const localVariablesSlice = createSlice({
selfTenant: { selfTenant: {
tenantEnquireId: null, tenantEnquireId: null,
planId: null, planId: null,
details:null, details: null,
frequency:null, frequency: null,
paymentDetailId:null paymentDetailId: null,
}, },
}, },
reducers: { reducers: {
@ -94,16 +94,27 @@ const localVariablesSlice = createSlice({
openModal: (state, action) => { openModal: (state, action) => {
const { modalType, data } = action.payload; const { modalType, data } = action.payload;
state.modals[modalType] = { isOpen: true, ...data };
state.modals[modalType] = {
isOpen: true,
data: data ?? {},
};
}, },
closeModal: (state, action) => { closeModal: (state, action) => {
const { modalType } = action.payload; const { modalType } = action.payload;
state.modals[modalType] = { ...state.modals[modalType], isOpen: false }; state.modals[modalType] = {
isOpen: false,
data: null,
};
}, },
toggleModal: (state, action) => { toggleModal: (state, action) => {
const { modalType } = action.payload; const { modalType } = action.payload;
state.modals[modalType].isOpen = !state.modals[modalType].isOpen; const modal = state.modals[modalType] || {};
modal.isOpen = !modal.isOpen;
}, },
setSelfTenant: (state, action) => { setSelfTenant: (state, action) => {
state.selfTenant.tenantEnquireId = state.selfTenant.tenantEnquireId =
action.payload.tenantEnquireId ?? state.selfTenant.tenantEnquireId; action.payload.tenantEnquireId ?? state.selfTenant.tenantEnquireId;
@ -111,8 +122,10 @@ const localVariablesSlice = createSlice({
action.payload.planId ?? state.selfTenant.planId; action.payload.planId ?? state.selfTenant.planId;
state.selfTenant.details = state.selfTenant.details =
action.payload.details ?? state.selfTenant.details; action.payload.details ?? state.selfTenant.details;
state.selfTenant.frequency = action.payload.frequency ?? state.selfTenant.frequency; state.selfTenant.frequency =
state.selfTenant.paymentDetailId = action.payload.paymentDetailId ?? state.selfTenant.paymentDetailId; action.payload.frequency ?? state.selfTenant.frequency;
state.selfTenant.paymentDetailId =
action.payload.paymentDetailId ?? state.selfTenant.paymentDetailId;
}, },
}, },
}); });