added project level feature

This commit is contained in:
pramod mahajan 2025-09-04 18:06:58 +05:30
parent 1eaf4a080c
commit 2b5fc9aaac
6 changed files with 384 additions and 82 deletions

View File

@ -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>
);

View 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;

View 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;

View File

@ -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();
},
});
};

View File

@ -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 />;

View File

@ -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 = {