Merge branch 'Organization_Hierarchy' into Service_Project_Managment

This commit is contained in:
pramod.mahajan 2025-11-13 10:20:48 +05:30
commit 651779adea
8 changed files with 384 additions and 45 deletions

View File

@ -1,6 +1,7 @@
import React, { useEffect, useState } from "react";
import EmpOverview from "./EmpOverview";
import { useProjectsAllocationByEmployee } from "../../hooks/useProjects";
import EmpReportingManager from "./EmpReportingManager";
const EmpDashboard = ({ profile }) => {
const {
@ -16,6 +17,7 @@ const EmpDashboard = ({ profile }) => {
{" "}
<EmpOverview profile={profile}></EmpOverview>
</div>
<div className="col col-sm-6 pt-5">
<div className="card ">
<div className="card-body">
@ -80,6 +82,10 @@ const EmpDashboard = ({ profile }) => {
</div>
</div>
</div>
<div className="col-12 col-sm-6 pt-5">
{" "}
<EmpReportingManager employeeId={profile?.id}></EmpReportingManager>
</div>
</div>
</>
);

View File

@ -0,0 +1,89 @@
import React, { useState } from "react";
import { useOrganizationHierarchy } from "../../hooks/useEmployees";
import GlobalModel from "../common/GlobalModel";
import ManageReporting from "./ManageReporting";
const EmpReportingManager = ({ employeeId, employee }) => {
const { data, isLoading } = useOrganizationHierarchy(employeeId);
const [showManageReportingModal, setShowManageReportingModal] = useState(false);
if (isLoading) return <span>Loading...</span>;
const primary = data?.find((item) => item.isPrimary);
const secondary = data?.filter((item) => !item.isPrimary);
// Create comma-separated string for secondary managers
const secondaryNames = secondary
?.map((item) => `${item.reportTo?.firstName || ""} ${item.reportTo?.lastName || ""}`.trim())
.join(", ");
return (
<div className="row">
<div className="col-12 mb-4">
<div className="card">
<div className="card-body">
<h5 className="m-0 py-1 mb-3">
Update Reporting Manager
</h5>
{/* Primary Reporting Manager */}
<div className="d-flex align-items-start mb-3">
<span className="d-flex">
<i className="bx bx-user bx-xs me-2 mt-1"></i>
<span>Primary Reporting Manager</span>
</span>
<span style={{ marginLeft: "75px" }}>:</span>
<span className="ms-5">
{primary?.reportTo?.firstName || <em>NA</em>}{" "}
{primary?.reportTo?.lastName || ""}
</span>
</div>
{/* Secondary Reporting Manager (comma-separated) */}
{secondary?.length > 0 && (
<div className="d-flex align-items-start mb-3" style={{ textAlign: "left" }}>
<span className="d-flex">
<i className="bx bx-user bx-xs me-2 mt-1"></i>
<span>Secondary Reporting Manager</span>
</span>
<span style={{ marginLeft: "57px" }}>:</span>
<span className="ms-5" >
{secondaryNames || <em>NA</em>}
</span>
</div>
)}
{/* Open Modal Button */}
<div className="mt-3 text-end">
<button
className="btn btn-sm btn-primary"
onClick={() => setShowManageReportingModal(true)}
>
<i className="bx bx-network-chart me-1"></i> Manage Reporting
</button>
</div>
</div>
</div>
</div>
{/* ManageReporting Modal */}
{showManageReportingModal && (
<GlobalModel
isOpen={showManageReportingModal}
closeModal={() => setShowManageReportingModal(false)}
>
<ManageReporting
employeeId={employeeId}
employee={primary?.employee || {}}
onClosed={() => setShowManageReportingModal(false)}
/>
</GlobalModel>
)}
</div>
);
};
export default EmpReportingManager;

View File

@ -3,8 +3,8 @@ import { z } from "zod"
const mobileNumberRegex = /^[0-9]\d{9}$/;
export const employeeSchema =
z.object({
export const employeeSchema =
z.object({
firstName: z.string().min(1, { message: "First Name is required" }),
middleName: z.string().optional(),
lastName: z.string().min(1, { message: "Last Name is required" }),
@ -90,35 +90,46 @@ export const employeeSchema =
.min(1, { message: "Phone Number is required" })
.regex(mobileNumberRegex, { message: "Invalid phone number " }),
jobRoleId: z.string().min(1, { message: "Role is required" }),
organizationId:z.string().min(1,{message:"Organization is required"}),
hasApplicationAccess:z.boolean().default(false),
organizationId: z.string().min(1, { message: "Organization is required" }),
hasApplicationAccess: z.boolean().default(false),
}).refine((data) => {
if (data.hasApplicationAccess) {
return data.email && data.email.trim() !== "";
}
return true;
}, {
message: "Email is required when employee has access",
path: ["email"],
if (data.hasApplicationAccess) {
return data.email && data.email.trim() !== "";
}
return true;
}, {
message: "Email is required when employee has access",
path: ["email"],
});
export const defatEmployeeObj = {
firstName: "",
middleName: "",
lastName: "",
email: "",
currentAddress: "",
birthDate: "",
joiningDate: "",
emergencyPhoneNumber: "",
emergencyContactPerson: "",
aadharNumber: "",
gender: "",
panNumber: "",
permanentAddress: "",
phoneNumber: "",
jobRoleId: null,
organizationId: "",
hasApplicationAccess: false
}
export const ManageReportingSchema = z.object({
primaryNotifyTo: z.array(z.string()).min(1, "Primary Reporting Manager is required"),
secondaryNotifyTo: z.array(z.string()).optional(),
});
export const defatEmployeeObj = {
firstName: "",
middleName: "",
lastName: "",
email: "",
currentAddress: "",
birthDate: "",
joiningDate: "",
emergencyPhoneNumber: "",
emergencyContactPerson: "",
aadharNumber: "",
gender: "",
panNumber: "",
permanentAddress: "",
phoneNumber: "",
jobRoleId: null,
organizationId:"",
hasApplicationAccess:false
}
export const defaultManageReporting = {
primaryNotifyTo: [],
secondaryNotifyTo: [],
};

View File

@ -0,0 +1,157 @@
import React, { useEffect } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import Label from "../common/Label";
import PmsEmployeeInputTag from "../common/PmsEmployeeInputTag";
import { useManageEmployeeHierarchy, useOrganizationHierarchy } from "../../hooks/useEmployees";
import { ManageReportingSchema, defaultManageReporting } from "./EmployeeSchema";
const ManageReporting = ({ onClosed, employee, employeeId }) => {
const {
handleSubmit,
control,
reset,
formState: { errors },
watch,
} = useForm({
resolver: zodResolver(ManageReportingSchema),
defaultValues: defaultManageReporting,
});
const { data, isLoading } = useOrganizationHierarchy(employeeId);
// mutation hook
const { mutate: manageHierarchy, isPending } = useManageEmployeeHierarchy(
employeeId,
onClosed
);
const primaryValue = watch("primaryNotifyTo");
const secondaryValue = watch("secondaryNotifyTo");
// Prefill hierarchy data
useEffect(() => {
if (data && Array.isArray(data)) {
const primary = data.find((r) => r.isPrimary);
const secondary = data.filter((r) => !r.isPrimary);
reset({
primaryNotifyTo: primary ? [primary.reportTo.id] : [],
secondaryNotifyTo: secondary.map((r) => r.reportTo.id),
});
}
}, [data, reset]);
const handleClose = () => {
reset(defaultManageReporting);
onClosed();
};
const onSubmit = (formData) => {
// Build set of currently selected IDs
const selectedIds = new Set([
...(formData.primaryNotifyTo || []),
...(formData.secondaryNotifyTo || []),
]);
// Build payload including previous assignments, setting isActive true/false accordingly
const payload = (data || []).map((item) => ({
reportToId: item.reportTo.id,
isPrimary: item.isPrimary,
isActive: selectedIds.has(item.reportTo.id),
}));
// Add any new IDs that were not previously assigned
if (formData.primaryNotifyTo?.length) {
const primaryId = formData.primaryNotifyTo[0];
if (!data?.some((d) => d.reportTo.id === primaryId)) {
payload.push({
reportToId: primaryId,
isPrimary: true,
isActive: true,
});
}
}
if (formData.secondaryNotifyTo?.length) {
formData.secondaryNotifyTo.forEach((id) => {
if (!data?.some((d) => d.reportTo.id === id)) {
payload.push({
reportToId: id,
isPrimary: false,
isActive: true,
});
}
});
}
manageHierarchy(payload);
};
return (
<div>
<form onSubmit={handleSubmit(onSubmit)} className="p-sm-0 p-2">
<h5 className="m-0 py-1 mb-3">
Update Reporting Manager (
{`${employee.firstName || ""} ${employee.middleName || ""} ${employee.lastName || ""}`.trim()}
)
</h5>
{/* Primary */}
<div className="mb-4 text-start">
<Label className="form-label" required>
Primary Reporting Manager
</Label>
<PmsEmployeeInputTag
control={control}
name="primaryNotifyTo"
placeholder={primaryValue?.length > 0 ? "" : "Select primary report-to"}
forAll={true}
disabled={primaryValue?.length > 0}
/>
{errors.primaryNotifyTo && (
<div className="text-danger small mt-1">
{errors.primaryNotifyTo.message}
</div>
)}
</div>
{/* Secondary */}
<div className="mb-4 text-start">
<Label className="form-label">
Secondary Reporting Manager
</Label>
<PmsEmployeeInputTag
control={control}
name="secondaryNotifyTo"
placeholder="Select secondary report-to(s)"
forAll={true}
/>
</div>
<div className="d-flex justify-content-end gap-3 mt-3 mb-3">
<button
type="button"
onClick={handleClose}
className="btn btn-label-secondary btn-sm"
disabled={isPending}
>
Cancel
</button>
<button
type="submit"
className="btn btn-primary btn-sm"
disabled={isPending}
>
{isPending ? "Saving..." : "Submit"}
</button>
</div>
</form>
</div>
);
};
export default ManageReporting;

View File

@ -11,6 +11,7 @@ const PmsEmployeeInputTag = ({
projectId,
forAll,
isApplicationUser = false,
disabled
}) => {
const {
field: { value = [], onChange },
@ -187,7 +188,7 @@ const PmsEmployeeInputTag = ({
<button
type="button"
className="tagify__tag__removeBtn"
className="tagify__tag__removeBtn border-none"
onClick={() => handleRemove(id)}
aria-label={`Remove ${u.firstName}`}
title="Remove"
@ -203,7 +204,7 @@ const PmsEmployeeInputTag = ({
id="TagifyUserList"
name="TagifyUserList"
className="tagify__input flex-grow-1 border-0 bg-transparent"
placeholder={placeholder || "Type to search users..."}
placeholder={placeholder}
onChange={(e) => {
setSearch(e.target.value);
setShowDropdown(true);
@ -215,6 +216,7 @@ const PmsEmployeeInputTag = ({
autoComplete="off"
aria-expanded={showDropdown}
aria-haspopup="listbox"
disabled={disabled}
/>
{showDropdown && (

View File

@ -341,3 +341,41 @@ export const useUpdateEmployeeRoles = ({
error: mutation.error,
};
};
export const useOrganizationHierarchy=(employeeId)=>{
return useQuery({
queryKey:["organizationHierarchy",employeeId],
queryFn:async()=> {
const resp = await EmployeeRepository.getOrganizaionHierarchy(employeeId);
return resp.data;
},
enabled:!!employeeId
})
}
export const useManageEmployeeHierarchy = (employeeId, onSuccessCallBack) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (payload) => {
return await EmployeeRepository.manageOrganizationHierarchy(employeeId, payload);
},
onSuccess: (_, variables) => {
queryClient.invalidateQueries({
queryKey: ["organizationHierarchy", employeeId],
});
showToast("Reporting hierarchy updated successfully", "success");
if (onSuccessCallBack) onSuccessCallBack();
},
onError: (error) => {
showToast(
error?.response?.data?.message ||
error.message ||
"Something went wrong, please try again!",
"error"
);
},
});
};

View File

@ -40,6 +40,7 @@ import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import Pagination from "../../components/common/Pagination";
import handleEmployeeExport from "../../components/Employee/handleEmployeeExport";
import { SpinnerLoader } from "../../components/common/Loader";
import ManageReporting from "../../components/Employee/ManageReporting";
const EmployeeList = () => {
const selectedProjectId = useSelector(
@ -68,9 +69,11 @@ const EmployeeList = () => {
const [searchText, setSearchText] = useState("");
const [filteredData, setFilteredData] = useState([]);
const [showModal, setShowModal] = useState(false);
const [selectedEmployeeId, setSelecedEmployeeId] = useState(null);
const [selectedEmployeeId, setSelectedEmployeeId] = useState(null);
const [selectedEmployee, setSelectedEmployee] = useState(null);
const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [selectedEmpFordelete, setSelectedEmpFordelete] = useState(null);
const [showManageReportingModal, setShowManageReportingModal] = useState(false);
const [employeeLodaing, setemployeeLodaing] = useState(false);
const ViewTeamMember = useHasUserPermission(VIEW_TEAM_MEMBERS);
const { mutate: suspendEmployee, isPending: empLodaing } = useSuspendEmployee(
@ -148,10 +151,16 @@ const EmployeeList = () => {
};
const handleEmployeeModel = (id) => {
setSelecedEmployeeId(id);
setSelectedEmployeeId(id);
setShowModal(true);
};
const handleManageReporting = (employee) => {
setSelectedEmployee(employee);
setSelectedEmployeeId(employee.id);
setShowManageReportingModal(true);
};
const handleOpenDelete = (employee) => {
setSelectedEmpFordelete(employee);
setIsDeleteModalOpen(true);
@ -234,6 +243,19 @@ const EmployeeList = () => {
</GlobalModel>
)}
{showManageReportingModal && (
<GlobalModel
isOpen={showManageReportingModal}
closeModal={() => setShowManageReportingModal(false)}
>
<ManageReporting
employee={selectedEmployee}
employeeId={selectedEmployeeId}
onClosed={() => setShowManageReportingModal(false)}
/>
</GlobalModel>
)}
{IsDeleteModalOpen && (
<ConfirmModal
isOpen={IsDeleteModalOpen}
@ -293,7 +315,7 @@ const EmployeeList = () => {
className="form-check-label ms-0"
htmlFor="inactiveEmployeesCheckbox"
>
In-active Employees
{showInactive ? "Hide In-active Employees":"Show In-active Employees"}
</label>
</div>
</div>
@ -633,6 +655,18 @@ const EmployeeList = () => {
<i className="bx bx-cog bx-sm"></i>{" "}
Manage Role
</button>
<button
className="dropdown-item py-1"
type="button"
data-bs-toggle="modal"
data-bs-target="#managerole-modal"
onClick={() =>
handleManageReporting(item)
}
>
<i className="bx bx-network-chart bx-sm"></i>{" "}
Manage Reporting
</button>
</>
)}

View File

@ -10,18 +10,20 @@ const EmployeeRepository = {
updateEmployee: (id, data) => api.put(`/users/${id}`, data),
// deleteEmployee: ( id ) => api.delete( `/users/${ id }` ),
getEmployeeProfile: (id) => api.get(`/api/employee/profile/get/${id}`),
deleteEmployee: (id,active) => api.delete(`/api/employee/${id}?active=${active}`),
getEmployeeName: (projectId, search,allEmployee) => {
const params = new URLSearchParams();
deleteEmployee: (id, active) => api.delete(`/api/employee/${id}?active=${active}`),
getEmployeeName: (projectId, search, allEmployee) => {
const params = new URLSearchParams();
if (projectId) params.append("projectId", projectId);
if (search) params.append("searchString", search);
if(allEmployee) params.append("allEmployee",allEmployee)
if (projectId) params.append("projectId", projectId);
if (search) params.append("searchString", search);
if (allEmployee) params.append("allEmployee", allEmployee)
const query = params.toString();
return api.get(`/api/Employee/basic${query ? `?${query}` : ""}`);
}
const query = params.toString();
return api.get(`/api/Employee/basic${query ? `?${query}` : ""}`);
},
getOrganizaionHierarchy: (employeeId) => api.get(`/api/organization/hierarchy/list/${employeeId}`),
manageOrganizationHierarchy: (employeeId, data) => api.post(`/api/organization/hierarchy/manage/${employeeId}`, data),
};
export default EmployeeRepository;