added project level feature
This commit is contained in:
parent
1eaf4a080c
commit
2b5fc9aaac
@ -1,84 +1,57 @@
|
||||
import React from "react";
|
||||
import { hasUserPermission } from "../../utils/authUtils";
|
||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||
import { DIRECTORY_ADMIN, DIRECTORY_MANAGER, DIRECTORY_USER, VIEW_PROJECT_INFRA } from "../../utils/constants";
|
||||
import {
|
||||
DIRECTORY_ADMIN,
|
||||
DIRECTORY_MANAGER,
|
||||
DIRECTORY_USER,
|
||||
VIEW_PROJECT_INFRA,
|
||||
} from "../../utils/constants";
|
||||
|
||||
const ProjectNav = ({ onPillClick, activePill }) => {
|
||||
const HasViewInfraStructure = useHasUserPermission( VIEW_PROJECT_INFRA );
|
||||
const HasViewInfraStructure = useHasUserPermission(VIEW_PROJECT_INFRA);
|
||||
const DirAdmin = useHasUserPermission(DIRECTORY_ADMIN);
|
||||
const DireManager = useHasUserPermission(DIRECTORY_MANAGER)
|
||||
const DirUser = useHasUserPermission(DIRECTORY_USER)
|
||||
const DireManager = useHasUserPermission(DIRECTORY_MANAGER);
|
||||
const DirUser = useHasUserPermission(DIRECTORY_USER);
|
||||
|
||||
const ProjectTab = [
|
||||
{ key: "profile", icon: "bx bx-user", label: "Profile" },
|
||||
{ key: "teams", icon: "bx bx-group", label: "Teams" },
|
||||
{
|
||||
key: "infra",
|
||||
icon: "bx bx-grid-alt",
|
||||
label: "Infrastructure",
|
||||
hidden: !HasViewInfraStructure,
|
||||
},
|
||||
{
|
||||
key: "directory",
|
||||
icon: "bx bxs-contact",
|
||||
label: "Directory",
|
||||
hidden: !(DirAdmin || DireManager || DirUser),
|
||||
},
|
||||
{ key: "documents", icon: "bx bx-folder-open", label: "Documents" },
|
||||
{ key: "setting", icon: "bx bxs-cog", label: "Setting" },
|
||||
];
|
||||
return (
|
||||
<div className="nav-align-top">
|
||||
<ul className="nav nav-tabs ">
|
||||
<li className="nav-item">
|
||||
<a
|
||||
className={`nav-link ${activePill === "profile" ? "active" : ""} fs-6`}
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onPillClick("profile");
|
||||
}}
|
||||
>
|
||||
<i className="bx bx-user bx-sm me-1_5"></i> <span className="d-none d-md-inline">Profile</span>
|
||||
</a>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<a
|
||||
className={`nav-link ${activePill === "teams" ? "active" : ""} fs-6`}
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onPillClick("teams");
|
||||
}}
|
||||
>
|
||||
<i className="bx bx-group bx-sm me-1_5"></i><span className="d-none d-md-inline" > Teams</span>
|
||||
</a>
|
||||
</li>
|
||||
<li className={`nav-item ${!HasViewInfraStructure && "d-none"} `}>
|
||||
<a
|
||||
className={`nav-link ${activePill === "infra" ? "active" : ""} fs-6`}
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onPillClick("infra");
|
||||
}}
|
||||
>
|
||||
<i className="bx bx-grid-alt bx-sm me-1_5"></i> <span className="d-none d-md-inline">Infrastructure</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
{(DirAdmin || DireManager || DirUser) && (
|
||||
<li className="nav-item">
|
||||
<a
|
||||
className={`nav-link ${activePill === "directory" ? "active" : ""} fs-6`}
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault(); // Prevent page reload
|
||||
onPillClick("directory");
|
||||
}}
|
||||
>
|
||||
<i className='bx bxs-contact bx-sm me-1_5'></i> <span className="d-none d-md-inline">Directory</span>
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
<li className="nav-item">
|
||||
<a
|
||||
className={`nav-link ${
|
||||
activePill === "documents" ? "active" : ""
|
||||
} fs-6`}
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault(); // Prevent page reload
|
||||
onPillClick("documents");
|
||||
}}
|
||||
>
|
||||
<i className='bx bxs-cog bx-sm me-1_5'></i> <span className="d-none d-md-inline">Documents</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<ul className="nav nav-tabs">
|
||||
{ProjectTab?.filter((tab) => !tab.hidden)?.map((tab) => (
|
||||
<li key={tab.key} className="nav-item cursor-pointer">
|
||||
<a
|
||||
|
||||
className={`nav-link ${
|
||||
activePill === tab.key ? "active cursor-pointer" : ""
|
||||
} fs-6`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onPillClick(tab.key);
|
||||
}}
|
||||
>
|
||||
<i className={`${tab.icon} bx-sm me-1_5`}></i>
|
||||
<span className="d-none d-md-inline ">{tab.label}</span>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
|
180
src/components/Project/ProjectPermission.jsx
Normal file
180
src/components/Project/ProjectPermission.jsx
Normal file
@ -0,0 +1,180 @@
|
||||
import React, { useEffect } from "react";
|
||||
import {
|
||||
useProjectLevelEmployeePermission,
|
||||
useProjectLevelModules,
|
||||
useUpdateProjectLevelEmployeePermission,
|
||||
} from "../../hooks/useProjects";
|
||||
import { useSelectedproject } from "../../slices/apiDataManager";
|
||||
import { useEmployeesByProject } from "../../hooks/useEmployees";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import showToast from "../../services/toastService";
|
||||
|
||||
export const ProjectPermissionSchema = z.object({
|
||||
employeeId: z.string(),
|
||||
selectedPermissions: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
const ProjectPermission = () => {
|
||||
const selectedProject = useSelectedproject();
|
||||
|
||||
const { data: ProjectModules } = useProjectLevelModules();
|
||||
const { employees = [], isLoading: isEmployeeLoading } =
|
||||
useEmployeesByProject(selectedProject);
|
||||
|
||||
const {
|
||||
register,
|
||||
watch,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: zodResolver(ProjectPermissionSchema),
|
||||
defaultValues: {
|
||||
employeeId: "",
|
||||
selectedPermissions: [],
|
||||
},
|
||||
});
|
||||
|
||||
const selectedEmployee = watch("employeeId");
|
||||
|
||||
// Fetch permissions for the selected employee
|
||||
const { data: selectedEmpPermissions } = useProjectLevelEmployeePermission(
|
||||
selectedEmployee || "",
|
||||
selectedProject
|
||||
);
|
||||
|
||||
// Update form state when employee permissions change
|
||||
useEffect(() => {
|
||||
const enabledPerms =
|
||||
selectedEmpPermissions?.permissions
|
||||
?.filter((perm) => perm.isEnabled)
|
||||
?.map((perm) => perm.id) || [];
|
||||
|
||||
reset({
|
||||
employeeId: selectedEmployee || employees[0]?.id || "",
|
||||
selectedPermissions: enabledPerms,
|
||||
});
|
||||
}, [selectedEmpPermissions, reset, selectedEmployee, employees]);
|
||||
|
||||
const { mutate: UpdatePermission, isPending } =
|
||||
useUpdateProjectLevelEmployeePermission();
|
||||
const onSubmit = (formData) => {
|
||||
// Guard 1: Ensure employee selected
|
||||
if (!formData.employeeId) {
|
||||
console.warn("No employee selected");
|
||||
return;
|
||||
}
|
||||
|
||||
const existingPermissions = selectedEmpPermissions?.permissions || [];
|
||||
|
||||
// Build payload
|
||||
const payloadPermissions =
|
||||
existingPermissions.length > 0
|
||||
? existingPermissions.map((perm) => ({
|
||||
id: perm.id,
|
||||
isEnabled: formData.selectedPermissions?.includes(perm.id) || false,
|
||||
}))
|
||||
: (formData.selectedPermissions || []).map((id) => ({
|
||||
id,
|
||||
isEnabled: true,
|
||||
}));
|
||||
|
||||
// Guard 2: Prevent API call if no permissions at all
|
||||
if (payloadPermissions.length === 0) {
|
||||
showToast("No permissions selected", "warn");
|
||||
return;
|
||||
}
|
||||
|
||||
// Guard 3 (optional): Prevent API call if nothing changed
|
||||
const hasChanges = existingPermissions.some(
|
||||
(perm) =>
|
||||
perm.isEnabled !==
|
||||
(formData.selectedPermissions?.includes(perm.id) || false)
|
||||
);
|
||||
if (!hasChanges && existingPermissions.length > 0) {
|
||||
showToast("No changes detected", "warn");
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
employeeId: formData.employeeId,
|
||||
projectId: selectedProject,
|
||||
permission: payloadPermissions,
|
||||
};
|
||||
|
||||
UpdatePermission(payload);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
{isEmployeeLoading ? (
|
||||
<>Loading...</>
|
||||
) : (
|
||||
<form className="row" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="d-flex align-items-end gap-2">
|
||||
<div className="text-start">
|
||||
<label className="form-label">Select Employee</label>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
{...register("employeeId")}
|
||||
disabled={isPending}
|
||||
>
|
||||
<option value="">-- Select --</option>
|
||||
{employees.map((emp) => (
|
||||
<option key={emp.id} value={emp.id}>
|
||||
{emp.firstName} {emp.lastName}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{errors.employeeId && (
|
||||
<div className="text-danger small">
|
||||
{errors.employeeId.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button className="btn btn-sm btn-primary">
|
||||
{isPending ? "Please Wait..." : "Update Permission"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{ProjectModules?.map((feature) => (
|
||||
<div key={feature.id} className="row my-1">
|
||||
<div className="col-12 text-start fw-semibold mb-2">
|
||||
{feature.name}
|
||||
</div>
|
||||
<div className="col-12">
|
||||
<div className="row">
|
||||
{feature.featurePermissions?.map((perm) => (
|
||||
<div
|
||||
className="col-12 col-sm-6 col-md-4 mb-3"
|
||||
key={perm.id}
|
||||
>
|
||||
<label
|
||||
className="form-check-label d-flex align-items-center"
|
||||
htmlFor={perm.id}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-check-input me-2"
|
||||
id={perm.id}
|
||||
value={perm.id}
|
||||
{...register("selectedPermissions")}
|
||||
/>
|
||||
{perm.name}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<hr className="my-2" />
|
||||
</div>
|
||||
))}
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectPermission;
|
74
src/components/Project/ProjectSetting.jsx
Normal file
74
src/components/Project/ProjectSetting.jsx
Normal file
@ -0,0 +1,74 @@
|
||||
import React, { useState } from "react";
|
||||
import { ComingSoonPage } from "../../pages/Misc/ComingSoonPage";
|
||||
import ProjectPermission from "./ProjectPermission";
|
||||
|
||||
const ProjectSetting = () => {
|
||||
const [activePill, setActivePill] = useState(() => {
|
||||
return localStorage.getItem("lastActiveProjectSettingTab") || "Permissions";
|
||||
});
|
||||
const projectSettingTab = [
|
||||
{ key: "Permissions", label: "Permissions" },
|
||||
{ key: "Notification", label: "Notification" },
|
||||
{ key: "SeparatedLink", label: "Separated link", isButton: true },
|
||||
];
|
||||
const handlePillClick = (pillKey) => {
|
||||
setActivePill(pillKey);
|
||||
localStorage.setItem("lastActiveProjectSettingTab", pillKey);
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
switch (activePill) {
|
||||
case "Permissions":
|
||||
return <ProjectPermission />;
|
||||
|
||||
case "Notification":
|
||||
return <ComingSoonPage />;
|
||||
|
||||
default:
|
||||
return <ComingSoonPage />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-100">
|
||||
<div className="card p-3">
|
||||
<div className="col-4">
|
||||
<div className="dropdown text-start">
|
||||
<button
|
||||
className="btn btn-sm btn-outline-primary dropdown-toggle"
|
||||
type="button"
|
||||
id="dropdownMenuButton"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
{activePill || "Select Option"}
|
||||
</button>
|
||||
|
||||
<ul className="dropdown-menu" aria-labelledby="dropdownMenuButton">
|
||||
{projectSettingTab.map((item) =>
|
||||
item.isButton ? (
|
||||
<li key={item.key}>
|
||||
<button className="dropdown-item">{item.label}</button>
|
||||
</li>
|
||||
) : (
|
||||
<li key={item.key}>
|
||||
<button
|
||||
className="dropdown-item"
|
||||
onClick={() => handlePillClick(item.key)}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3">{renderContent()}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectSetting;
|
@ -14,7 +14,6 @@ import {
|
||||
} from "@tanstack/react-query";
|
||||
import showToast from "../services/toastService";
|
||||
|
||||
|
||||
// ------------------------------Query-------------------
|
||||
|
||||
export const useProjects = () => {
|
||||
@ -153,7 +152,7 @@ export const useProjectName = () => {
|
||||
isLoading,
|
||||
error,
|
||||
refetch,
|
||||
isError
|
||||
isError,
|
||||
} = useQuery({
|
||||
queryKey: ["basicProjectNameList"],
|
||||
queryFn: async () => {
|
||||
@ -164,7 +163,13 @@ export const useProjectName = () => {
|
||||
showToast(error.message || "Error while Fetching project Name", "error");
|
||||
},
|
||||
});
|
||||
return { projectNames: data, loading: isLoading, Error: error, refetch,isError };
|
||||
return {
|
||||
projectNames: data,
|
||||
loading: isLoading,
|
||||
Error: error,
|
||||
refetch,
|
||||
isError,
|
||||
};
|
||||
};
|
||||
|
||||
export const useProjectInfra = (projectId) => {
|
||||
@ -175,7 +180,7 @@ export const useProjectInfra = (projectId) => {
|
||||
} = useQuery({
|
||||
queryKey: ["ProjectInfra", projectId],
|
||||
queryFn: async () => {
|
||||
if(!projectId) return null;
|
||||
if (!projectId) return null;
|
||||
const res = await ProjectRepository.getProjectInfraByproject(projectId);
|
||||
return res.data;
|
||||
},
|
||||
@ -207,12 +212,7 @@ export const useProjectTasks = (workAreaId, IsExpandedArea = false) => {
|
||||
return { ProjectTaskList, isLoading, error };
|
||||
};
|
||||
|
||||
export const useProjectTasksByEmployee = (
|
||||
employeeId,
|
||||
fromDate,
|
||||
toDate,
|
||||
) => {
|
||||
|
||||
export const useProjectTasksByEmployee = (employeeId, fromDate, toDate) => {
|
||||
return useQuery({
|
||||
queryKey: ["TasksByEmployee", employeeId],
|
||||
queryFn: async () => {
|
||||
@ -230,6 +230,43 @@ export const useProjectTasksByEmployee = (
|
||||
});
|
||||
};
|
||||
|
||||
export const useProjectLevelEmployeeList = (ProjectId) => {
|
||||
return useQuery({
|
||||
queryKey: ["ProjectLevelEmployeeList", ProjectId],
|
||||
queryFn: async () => {
|
||||
const resp = await ProjectRepository.getProjectLevelEmployeeList(
|
||||
ProjectId
|
||||
);
|
||||
return resp.data;
|
||||
},
|
||||
enabled: !!ProjectId,
|
||||
});
|
||||
};
|
||||
|
||||
export const useProjectLevelModules = () => {
|
||||
return useQuery({
|
||||
queryKey: ["ProjectLevelModules"],
|
||||
queryFn: async () => {
|
||||
const resp = await ProjectRepository.getProjectLevelModules();
|
||||
return resp.data;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useProjectLevelEmployeePermission = (employeeId, projectId) => {
|
||||
return useQuery({
|
||||
queryKey: ["ProjectLevelEmployeePermission", employeeId, projectId],
|
||||
queryFn: async () => {
|
||||
const resp = await ProjectRepository.getProjectLevelEmployeePermissions(
|
||||
employeeId,
|
||||
projectId
|
||||
);
|
||||
return resp.data;
|
||||
},
|
||||
enabled: !!employeeId && !!projectId,
|
||||
});
|
||||
};
|
||||
|
||||
// -- -------------Mutation-------------------------------
|
||||
|
||||
export const useCreateProject = ({ onSuccessCallback }) => {
|
||||
@ -558,3 +595,26 @@ export const useDeleteProjectTask = (onSuccessCallback) => {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateProjectLevelEmployeePermission = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (payload) =>
|
||||
await ProjectRepository.updateProjectLevelEmployeePermission(payload),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["ProjectLevelEmployeePermission"],
|
||||
});
|
||||
showToast("Permission Updated successfully", "success");
|
||||
},
|
||||
onError: (error) => {
|
||||
showToast(
|
||||
error?.response?.data?.message ||
|
||||
error.message ||
|
||||
"Failed to delete task",
|
||||
"error"
|
||||
);
|
||||
if (onSuccessCallback) onSuccessCallback();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -26,6 +26,7 @@ import AttendanceOverview from "../../components/Dashboard/AttendanceChart";
|
||||
import { setProjectId } from "../../slices/localVariablesSlice";
|
||||
import ProjectDocument from "../../components/Project/ProjectDocuments";
|
||||
import ProjectDocuments from "../../components/Project/ProjectDocuments";
|
||||
import ProjectSetting from "../../components/Project/ProjectSetting";
|
||||
|
||||
const ProjectDetails = () => {
|
||||
const projectId = useSelectedproject();
|
||||
@ -126,6 +127,12 @@ const ProjectDetails = () => {
|
||||
<ProjectDocuments />
|
||||
</div>
|
||||
);
|
||||
case "setting":
|
||||
return (
|
||||
<div className="row">
|
||||
<ProjectSetting />
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
return <ComingSoonPage />;
|
||||
|
@ -37,6 +37,14 @@ const ProjectRepository = {
|
||||
api.get(
|
||||
`/api/project/tasks-employee/${id}?fromDate=${fromDate}&toDate=${toDate}`
|
||||
),
|
||||
|
||||
|
||||
// Permission Managment for Employee at Project Level
|
||||
|
||||
getProjectLevelEmployeeList:(projectId)=>api.get(`/api/Project/get/proejct-level/employees/${projectId}`),
|
||||
getProjectLevelModules:()=>api.get(`/api/Project/get/proejct-level/modules`),
|
||||
getProjectLevelEmployeePermissions:(employeeId,projectId)=>api.get(`/api/Project/get/project-level-permission/employee/${employeeId}/project/${projectId}`),
|
||||
updateProjectLevelEmployeePermission:(data)=>api.post(`/api/Project/assign/project-level-permission`,data)
|
||||
};
|
||||
|
||||
export const TasksRepository = {
|
||||
|
Loading…
x
Reference in New Issue
Block a user