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";
const ServiceProjectProfile = () => {
const { id } = useParams();
const { data, isLoading, isError, error } = useServiceProject(id);
const { projectId } = useParams();
const { data, isLoading, isError, error } = useServiceProject(projectId);
if (isLoading) {
return <div className="">Loadng.</div>;
}

View File

@ -1,15 +1,17 @@
import React from 'react'
import { useModal } from '../../../hooks/useAuth'
import { useParams } from 'react-router-dom';
const ProjectTeam = () => {
const {onOpen} = useModal("ServiceTeamAllocation");
const {projectId}= useParams();
return (
<div className='card page-min-h mt-2'>
<div className='row text-end'>
<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>
{/* <ServiceProjectTeamList/> */}

View File

@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import { useModal } from "../../../hooks/useAuth";
import Modal from "../../common/Modal";
import Label from "../../common/Label";
@ -6,45 +6,233 @@ import { useTeamRole } from "../../../hooks/masterHook/useMaster";
import SelectField from "../../common/Forms/SelectField";
import SelectFieldServerSide 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 { 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 [selectedEmployees, setSelectedEmployees] = useState(null);
const { data, isLoading, isError, error } = useTeamRole();
console.log(selectedEmployees);
const TeamAllocation = (
<div className="row text-start">
<div className="col-6">
<SelectField
label="Select Team"
required
options={data}
value={selectedTeam}
labelKey="name"
valueKey="id"
valueKey="name"
onChange={(e) => setSelectTeam(e)}
isLoading={isLoading}
/>
const [selectedEmployees, setSelectedEmployees] = useState([]);
const { data, isLoading } = useTeamRole();
const TeamAllocationColumns = [
{
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
label="Select Role"
required
options={data}
value={selectedTeam}
labelKey="name"
valueKey="id"
onChange={(e) => {
setSelectedEmployees([]);
setSelectTeam(e);
}}
isLoading={isLoading}
/>
</div>
<div className="col-12 col-md-6 mb-3">
<SelectEmployeeServerSide
isMultiple={true}
isFullObject={true}
value={selectedEmployees}
onChange={setSelectedEmployees}
/>
</div>
<div className="col-12 d-flex flex-row gap-2 flex-wrap">
{selectedEmployees.map((e) => (
<EmployeeChip handleRemove={handleRemove} employee={e} />
))}
</div>
</>
)}
</div>
<div className="col-6">
<SelectEmployeeServerSide
isMultiple={true}
isFullObject={true}
value={selectedEmployees}
onChange={setSelectedEmployees}
/>
{(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>
);
return (
<Modal
size="lg"
title={"Team Allocation"}
isOpen={isOpen}
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 */}
{open && (
<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={{
position: "absolute",
top: "100%",

View File

@ -25,16 +25,24 @@ import showToast from "../services/toastService";
export const useModal = (modalType) => {
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 onToggle = () => dispatch(toggleModal({ modalType }));
return { isOpen, onOpen, onClose, onToggle };
return { isOpen, data, onOpen, onClose, onToggle };
};
export const useSubscription = (frequency) => {
return useQuery({
queryKey: ["subscriptionPlans", frequency],

View File

@ -32,6 +32,20 @@ export const useServiceProject = (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) => {
const queryClient = useQueryClient();
return useMutation({
@ -104,14 +118,22 @@ export const useActiveInActiveServiceProject = (onSuccessCallback) => {
});
};
export const useAllocationTeam=()=>{
export const useAllocationServiceProjectTeam = (onSuccessCallback) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn:async(paylaod)=> await ServiceProjectRepository.AllocateEmployee(paylaod),
onSuccess: (data, variables) => {
// queryClient.invalidateQueries({ queryKey: ["serviceProjects"] });
mutationFn: async ({ payload, isActive }) =>
await ServiceProjectRepository.AllocateEmployee(payload),
onSuccess: (data, variables) => {
queryClient.invalidateQueries({ queryKey: ["serviceProjectTeam"] });
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) => {
showToast(
@ -121,8 +143,9 @@ export const useAllocationTeam=()=>{
"error"
);
},
})
}
});
};
//#endregion
//#region Service Jobs

View File

@ -11,7 +11,12 @@ export const ServiceProjectRepository = {
api.put(`/api/ServiceProject/edit/${id}`, data),
DeleteServiceProject: (id, isActive = false) =>
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
CreateJob: (data) => api.post(`/api/ServiceProject/job/create`, data),

View File

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

View File

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