Add skeleton in Dashboard Projects, Teams and Tasks. #401
@ -19,54 +19,60 @@ import { useSelectedProject } from "../../slices/apiDataManager";
|
|||||||
import Loader from "../common/Loader";
|
import Loader from "../common/Loader";
|
||||||
|
|
||||||
|
|
||||||
const InfraPlanning = () =>
|
|
||||||
{
|
|
||||||
const {profile: LoggedUser, refetch : fetchData} = useProfile()
|
const InfraPlanning = () => {
|
||||||
const dispatch = useDispatch()
|
const { profile: LoggedUser, refetch: fetchData } = useProfile();
|
||||||
// const selectedProject = useSelector((store)=>store.localVariables.projectId)
|
const dispatch = useDispatch();
|
||||||
const selectedProject = useSelectedProject();
|
const selectedProject = useSelectedProject();
|
||||||
const {projectInfra, isLoading, error} = useProjectInfra( selectedProject )
|
|
||||||
|
|
||||||
|
const { projectInfra, isLoading, isError, error, isFetched } = useProjectInfra(selectedProject);
|
||||||
|
|
||||||
|
const canManageInfra = useHasUserPermission(MANAGE_PROJECT_INFRA);
|
||||||
const ManageInfra = useHasUserPermission( MANAGE_PROJECT_INFRA )
|
const canApproveTask = useHasUserPermission(APPROVE_TASK);
|
||||||
const ApprovedTaskRights = useHasUserPermission(APPROVE_TASK)
|
const canReportTask = useHasUserPermission(ASSIGN_REPORT_TASK);
|
||||||
const ReportTaskRights = useHasUserPermission(ASSIGN_REPORT_TASK)
|
|
||||||
const reloadedData = useSelector( ( store ) => store.localVariables.reload )
|
|
||||||
|
|
||||||
|
const reloadedData = useSelector((store) => store.localVariables.reload);
|
||||||
// useEffect( () =>
|
|
||||||
// {
|
|
||||||
// if (reloadedData)
|
|
||||||
// {
|
|
||||||
// refetch()
|
|
||||||
// dispatch( refreshData( false ) )
|
|
||||||
// }
|
|
||||||
|
|
||||||
// },[reloadedData])
|
const hasAccess = canManageInfra || canApproveTask || canReportTask;
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
return <div>{error?.response?.data?.message || error?.message}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasAccess && !isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="text-center">
|
||||||
|
<i className="fa-solid fa-triangle-exclamation fs-5"></i>
|
||||||
|
<p>Access Denied: You don't have permission to perform this action.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <Loader />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFetched && (!projectInfra || projectInfra.length === 0)) {
|
||||||
|
return (
|
||||||
|
<div className="card text-center">
|
||||||
|
<p className="my-3">No Result Found</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="col-md-12 col-lg-12 col-xl-12 order-0 mb-4">
|
<div className="col-md-12 col-lg-12 col-xl-12 order-0 mb-4">
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="card-body" style={{ padding: "0.5rem" }}>
|
<div className="card-body" style={{ padding: "0.5rem" }}>
|
||||||
{(ApprovedTaskRights || ReportTaskRights) ? (
|
<div className="row">
|
||||||
<div className="align-items-center">
|
<InfraTable buildings={projectInfra} projectId={selectedProject} />
|
||||||
<div className="row ">
|
|
||||||
{isLoading && (<Loader/> )}
|
|
||||||
{( !isLoading && projectInfra?.length === 0 ) && ( <p>No Result Found</p> )}
|
|
||||||
{(!isLoading && projectInfra?.length > 0) && (<InfraTable buildings={projectInfra} projectId={selectedProject}/>)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<div className="text-center">
|
|
||||||
<i className="fa-solid fa-triangle-exclamation fs-5"></i>
|
|
||||||
<p>Access Denied: You don't have permission to perform this action. !</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default InfraPlanning;
|
export default InfraPlanning;
|
||||||
|
|
||||||
|
|||||||
32
src/components/Charts/ProjectCompletionChartSkeleton.jsx
Normal file
32
src/components/Charts/ProjectCompletionChartSkeleton.jsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const ProjectCompletionChartSkeleton = () => {
|
||||||
|
return (
|
||||||
|
<div className="card h-100">
|
||||||
|
<div className="card-header d-flex align-items-start justify-content-between">
|
||||||
|
<div className="card-title mb-0 text-start">
|
||||||
|
<h5 className="mb-1 fw-bold placeholder-glow">
|
||||||
|
<span className="placeholder col-6 bg-light"></span>
|
||||||
|
</h5>
|
||||||
|
<p className="card-subtitle placeholder-glow">
|
||||||
|
<span className="placeholder col-8 bg-light"></span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Keep a fixed height so card doesn't shrink */}
|
||||||
|
<div className="card-body" >
|
||||||
|
<div className="placeholder-glow">
|
||||||
|
{Array.from({ length: 5 }).map((_, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="placeholder col-12 mb-2 bg-light"
|
||||||
|
style={{ height: "20px", borderRadius: "4px" }}
|
||||||
|
></div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProjectCompletionChartSkeleton;
|
||||||
44
src/components/Charts/ProjectProgressChartSkeleton.jsx
Normal file
44
src/components/Charts/ProjectProgressChartSkeleton.jsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
// ProjectProgressChartSkeleton.jsx
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const ProjectProgressChartSkeleton = () => {
|
||||||
|
return (
|
||||||
|
<div className="card" style={{ minHeight: "400px" }}>
|
||||||
|
<div className="card-header">
|
||||||
|
<div className="d-flex flex-wrap justify-content-between align-items-start mb-2">
|
||||||
|
{/* Left: Title */}
|
||||||
|
<div className="card-title text-start">
|
||||||
|
<div className="placeholder-glow">
|
||||||
|
<span className="placeholder col-6 mb-2"></span>
|
||||||
|
<span className="placeholder col-4"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Row 2: Time Range Buttons */}
|
||||||
|
<div className="d-flex flex-wrap mt-2">
|
||||||
|
{Array(7)
|
||||||
|
.fill(0)
|
||||||
|
.map((_, idx) => (
|
||||||
|
<span
|
||||||
|
key={idx}
|
||||||
|
className="placeholder bg-light col-1 me-2"
|
||||||
|
style={{ height: "25px", borderRadius: "5px" }}
|
||||||
|
></span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card-body">
|
||||||
|
<div
|
||||||
|
className="placeholder-glow"
|
||||||
|
style={{ height: "250px", width: "100%" }}
|
||||||
|
>
|
||||||
|
<span className="placeholder bg-light col-12 h-100"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProjectProgressChartSkeleton;
|
||||||
18
src/components/Charts/TeamsSkeleton.jsx
Normal file
18
src/components/Charts/TeamsSkeleton.jsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const TeamsSkeleton = () => {
|
||||||
|
return (
|
||||||
|
<div className="d-flex justify-content-around align-items-start mt-n2 flex-grow-1">
|
||||||
|
<div>
|
||||||
|
<div className="bg-light rounded" style={{ width: "80px", height: "24px", marginBottom: "5px" }}></div>
|
||||||
|
<div className="bg-light rounded" style={{ width: "60px", height: "12px" }}></div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="bg-light rounded" style={{ width: "80px", height: "24px", marginBottom: "5px" }}></div>
|
||||||
|
<div className="bg-light rounded" style={{ width: "60px", height: "12px" }}></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TeamsSkeleton;
|
||||||
@ -100,7 +100,7 @@ const AttendanceOverview = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="bg-white p-4 rounded shadow d-flex flex-column"
|
className="bg-white p-4 rounded shadow d-flex flex-column"
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="d-flex justify-content-between align-items-center mb-3">
|
<div className="d-flex justify-content-between align-items-center mb-3">
|
||||||
@ -119,18 +119,22 @@ const AttendanceOverview = () => {
|
|||||||
<option value={30}>Last 30 Days</option>
|
<option value={30}>Last 30 Days</option>
|
||||||
</select>
|
</select>
|
||||||
<button
|
<button
|
||||||
className={`btn btn-sm ${view === "chart" ? "btn-primary" : "btn-outline-primary"}`}
|
className={`btn btn-sm p-1 ${
|
||||||
|
view === "chart" ? "btn-primary" : "btn-outline-primary"
|
||||||
|
}`}
|
||||||
onClick={() => setView("chart")}
|
onClick={() => setView("chart")}
|
||||||
title="Chart View"
|
title="Chart View"
|
||||||
>
|
>
|
||||||
<i className="bx bx-bar-chart-alt-2"></i>
|
<i className="bx bx-bar-chart-alt-2"></i>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`btn btn-sm ${view === "table" ? "btn-primary" : "btn-outline-primary"}`}
|
className={`btn btn-sm p-1 ${
|
||||||
|
view === "table" ? "btn-primary" : "btn-outline-primary"
|
||||||
|
}`}
|
||||||
onClick={() => setView("table")}
|
onClick={() => setView("table")}
|
||||||
title="Table View"
|
title="Table View"
|
||||||
>
|
>
|
||||||
<i className="bx bx-task text-success"></i>
|
<i class="bx bx-list-ul fs-5"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import HorizontalBarChart from "../Charts/HorizontalBarChart";
|
import HorizontalBarChart from "../Charts/HorizontalBarChart";
|
||||||
import { useProjects } from "../../hooks/useProjects";
|
import { useProjects } from "../../hooks/useProjects";
|
||||||
|
import ProjectCompletionChartSkeleton from "../Charts/ProjectCompletionChartSkeleton";
|
||||||
|
|
||||||
const ProjectCompletionChart = () => {
|
const ProjectCompletionChart = () => {
|
||||||
const { projects, loading } = useProjects();
|
const { projects, loading } = useProjects();
|
||||||
|
|
||||||
// Bar chart logic
|
if (loading) return <ProjectCompletionChartSkeleton />;
|
||||||
|
|
||||||
const projectNames = projects?.map((p) => p.name) || [];
|
const projectNames = projects?.map((p) => p.name) || [];
|
||||||
const projectProgress =
|
const projectProgress =
|
||||||
projects?.map((p) => {
|
projects?.map((p) => {
|
||||||
@ -16,14 +18,15 @@ const ProjectCompletionChart = () => {
|
|||||||
}) || [];
|
}) || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card h-100">
|
<div className="card" style={{ minHeight: "490px" }}>
|
||||||
<div className="card-header d-flex align-items-start justify-content-between">
|
<div className="card-header d-flex align-items-start justify-content-between">
|
||||||
<div className="card-title mb-0 text-start">
|
<div className="card-title mb-0 text-start">
|
||||||
<h5 className="mb-1 fw-bold ">Projects</h5>
|
<h5 className="mb-1 fw-bold">Projects</h5>
|
||||||
<p className="card-subtitle">Projects Completion Status</p>
|
<p className="card-subtitle">Projects Completion Status</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="card-body">
|
{/* Keep same minHeight as skeleton to avoid shrinking */}
|
||||||
|
<div className="card-body" >
|
||||||
<HorizontalBarChart
|
<HorizontalBarChart
|
||||||
categories={projectNames}
|
categories={projectNames}
|
||||||
seriesData={projectProgress}
|
seriesData={projectProgress}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import LineChart from "../Charts/LineChart";
|
|||||||
import { useProjects } from "../../hooks/useProjects";
|
import { useProjects } from "../../hooks/useProjects";
|
||||||
import { useDashboard_Data } from "../../hooks/useDashboard_Data";
|
import { useDashboard_Data } from "../../hooks/useDashboard_Data";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
|
import ProjectProgressChartSkeleton from "../Charts/ProjectProgressChartSkeleton";
|
||||||
|
|
||||||
const ProjectProgressChart = ({
|
const ProjectProgressChart = ({
|
||||||
ShowAllProject = true,
|
ShowAllProject = true,
|
||||||
@ -85,7 +86,7 @@ const ProjectProgressChart = ({
|
|||||||
: selectedProjectData?.name;
|
: selectedProjectData?.name;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card">
|
<div className="card" style={{ minHeight: "490px" }}>
|
||||||
<div className="card-header">
|
<div className="card-header">
|
||||||
<div className="d-flex flex-wrap justify-content-between align-items-start mb-2">
|
<div className="d-flex flex-wrap justify-content-between align-items-start mb-2">
|
||||||
{/* Left: Title */}
|
{/* Left: Title */}
|
||||||
@ -100,11 +101,10 @@ const ProjectProgressChart = ({
|
|||||||
{["1D", "1W", "15D", "1M", "3M", "1Y", "5Y"].map((key) => (
|
{["1D", "1W", "15D", "1M", "3M", "1Y", "5Y"].map((key) => (
|
||||||
<button
|
<button
|
||||||
key={key}
|
key={key}
|
||||||
className={`border-0 bg-transparent px-2 py-1 text-sm rounded ${
|
className={`border-0 bg-transparent px-2 py-1 text-sm rounded ${range === key
|
||||||
range === key
|
|
||||||
? "border-bottom border-primary text-primary"
|
? "border-bottom border-primary text-primary"
|
||||||
: "text-muted"
|
: "text-muted"
|
||||||
}`}
|
}`}
|
||||||
style={{ cursor: "pointer", transition: "all 0.2s ease" }}
|
style={{ cursor: "pointer", transition: "all 0.2s ease" }}
|
||||||
onClick={() => setRange(key)}
|
onClick={() => setRange(key)}
|
||||||
>
|
>
|
||||||
@ -114,14 +114,17 @@ const ProjectProgressChart = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="card-body">
|
{isLineChartLoading ? (
|
||||||
|
<ProjectProgressChartSkeleton />
|
||||||
|
) : (
|
||||||
<LineChart
|
<LineChart
|
||||||
seriesData={lineChartSeries}
|
seriesData={lineChartSeries}
|
||||||
categories={lineChartCategories}
|
categories={lineChartCategories}
|
||||||
loading={isLineChartLoading}
|
loading={isLineChartLoading}
|
||||||
lineChartCategoriesDates={lineChartCategoriesDates}
|
lineChartCategoriesDates={lineChartCategoriesDates}
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,9 +2,10 @@ import React, { useCallback, useEffect, useState } from "react";
|
|||||||
import { useDashboardProjectsCardData } from "../../hooks/useDashboard_Data";
|
import { useDashboardProjectsCardData } from "../../hooks/useDashboard_Data";
|
||||||
import eventBus from "../../services/eventBus";
|
import eventBus from "../../services/eventBus";
|
||||||
import GlobalRepository from "../../repositories/GlobalRepository";
|
import GlobalRepository from "../../repositories/GlobalRepository";
|
||||||
|
import TeamsSkeleton from "../Charts/TeamsSkeleton";
|
||||||
|
|
||||||
const Projects = () => {
|
const Projects = () => {
|
||||||
const { projectsCardData } = useDashboardProjectsCardData();
|
const { projectsCardData,loading } = useDashboardProjectsCardData();
|
||||||
const [projectData, setProjectsData] = useState(projectsCardData);
|
const [projectData, setProjectsData] = useState(projectsCardData);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -13,13 +14,13 @@ const Projects = () => {
|
|||||||
|
|
||||||
const handler = useCallback(
|
const handler = useCallback(
|
||||||
async (msg) => {
|
async (msg) => {
|
||||||
try {
|
try {
|
||||||
const response =
|
const response =
|
||||||
await GlobalRepository.getDashboardProjectsCardData();
|
await GlobalRepository.getDashboardProjectsCardData();
|
||||||
setProjectsData(response.data);
|
setProjectsData(response.data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[GlobalRepository]
|
[GlobalRepository]
|
||||||
);
|
);
|
||||||
@ -37,20 +38,24 @@ const Projects = () => {
|
|||||||
Projects
|
Projects
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div className="d-flex justify-content-around align-items-start mt-n2">
|
{loading ? (
|
||||||
<div>
|
<TeamsSkeleton />
|
||||||
<h4 className="mb-0 fw-bold">
|
) : (
|
||||||
{projectData.totalProjects?.toLocaleString()}
|
<div className="d-flex justify-content-around align-items-start mt-n2">
|
||||||
</h4>
|
<div>
|
||||||
<small className="text-muted">Total</small>
|
<h4 className="mb-0 fw-bold">
|
||||||
|
{projectData.totalProjects?.toLocaleString()}
|
||||||
|
</h4>
|
||||||
|
<small className="text-muted">Total</small>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="mb-0 fw-bold">
|
||||||
|
{projectData.ongoingProjects?.toLocaleString()}
|
||||||
|
</h4>
|
||||||
|
<small className="text-muted">Ongoing</small>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
)}
|
||||||
<h4 className="mb-0 fw-bold">
|
|
||||||
{projectData.ongoingProjects?.toLocaleString()}
|
|
||||||
</h4>
|
|
||||||
<small className="text-muted">Ongoing</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { useDashboardTasksCardData } from "../../hooks/useDashboard_Data";
|
import { useDashboardTasksCardData } from "../../hooks/useDashboard_Data";
|
||||||
|
import TeamsSkeleton from "../Charts/TeamsSkeleton";
|
||||||
const TasksCard = () => {
|
const TasksCard = () => {
|
||||||
const projectId = useSelector((store) => store.localVariables?.projectId);
|
const projectId = useSelector((store) => store.localVariables?.projectId);
|
||||||
const { tasksCardData, loading, error } = useDashboardTasksCardData(projectId);
|
const { tasksCardData, loading, error } = useDashboardTasksCardData(projectId);
|
||||||
@ -16,11 +16,7 @@ const TasksCard = () => {
|
|||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
// Loader will be displayed when loading is true
|
// Loader will be displayed when loading is true
|
||||||
<div className="d-flex justify-content-center align-items-center flex-grow-1">
|
<TeamsSkeleton/>
|
||||||
<div className="spinner-border text-primary" role="status">
|
|
||||||
<span className="visually-hidden">Loading...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : error ? (
|
) : error ? (
|
||||||
// Error message if there's an error
|
// Error message if there's an error
|
||||||
<div className="text-danger flex-grow-1 d-flex justify-content-center align-items-center">{error}</div>
|
<div className="text-danger flex-grow-1 d-flex justify-content-center align-items-center">{error}</div>
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useState } from "react";
|
|||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { useDashboardTeamsCardData } from "../../hooks/useDashboard_Data";
|
import { useDashboardTeamsCardData } from "../../hooks/useDashboard_Data";
|
||||||
import eventBus from "../../services/eventBus";
|
import eventBus from "../../services/eventBus";
|
||||||
|
import TeamsSkeleton from "../Charts/TeamsSkeleton";
|
||||||
|
|
||||||
const Teams = () => {
|
const Teams = () => {
|
||||||
const projectId = useSelector((store) => store.localVariables?.projectId);
|
const projectId = useSelector((store) => store.localVariables?.projectId);
|
||||||
@ -38,11 +39,7 @@ const Teams = () => {
|
|||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
// Blue spinner loader
|
// Blue spinner loader
|
||||||
<div className="d-flex justify-content-center align-items-center flex-grow-1">
|
<TeamsSkeleton/>
|
||||||
<div className="spinner-border text-primary" role="status">
|
|
||||||
<span className="visually-hidden">Loading...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : error ? (
|
) : error ? (
|
||||||
// Error message if data fetching fails
|
// Error message if data fetching fails
|
||||||
<div className="text-danger flex-grow-1 d-flex justify-content-center align-items-center">{error}</div>
|
<div className="text-danger flex-grow-1 d-flex justify-content-center align-items-center">{error}</div>
|
||||||
|
|||||||
@ -102,10 +102,10 @@ const ListViewContact = ({ data, Pagination }) => {
|
|||||||
className="card-datatable table-responsive"
|
className="card-datatable table-responsive"
|
||||||
id="horizontal-example"
|
id="horizontal-example"
|
||||||
>
|
>
|
||||||
<div className="dataTables_wrapper no-footer ">
|
<div className="dataTables_wrapper no-footer mx-5 pb-2">
|
||||||
<table className="table dataTable text-nowrap">
|
<table className="table dataTable text-nowrap">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="shadow-sm ">
|
<tr style={{ borderBottom: "2px solid var(--bs-table-border-color)"}}>
|
||||||
{contactList?.map((col) => (
|
{contactList?.map((col) => (
|
||||||
<th key={col.key} className={col.align}>
|
<th key={col.key} className={col.align}>
|
||||||
{col.label}
|
{col.label}
|
||||||
@ -116,7 +116,7 @@ const ListViewContact = ({ data, Pagination }) => {
|
|||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody >
|
||||||
{Array.isArray(data) && data.length > 0 ? (
|
{Array.isArray(data) && data.length > 0 ? (
|
||||||
data.map((row, i) => (
|
data.map((row, i) => (
|
||||||
<tr
|
<tr
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import BucketList from "./BucketList";
|
|||||||
import BucketForm from "./BucketForm";
|
import BucketForm from "./BucketForm";
|
||||||
import AssignEmployees from "./AssignedBucket";
|
import AssignEmployees from "./AssignedBucket";
|
||||||
import AssignedBucket from "./AssignedBucket";
|
import AssignedBucket from "./AssignedBucket";
|
||||||
|
import { useDirectoryContext } from "../../pages/Directory/DirectoryPage";
|
||||||
|
|
||||||
const ManageBucket1 = () => {
|
const ManageBucket1 = () => {
|
||||||
const { data, isError, isLoading, error } = useBucketList();
|
const { data, isError, isLoading, error } = useBucketList();
|
||||||
@ -17,18 +18,21 @@ const ManageBucket1 = () => {
|
|||||||
const [action, setAction] = useState(null); // "create" | "edit" | null
|
const [action, setAction] = useState(null); // "create" | "edit" | null
|
||||||
const [selectedBucket, setSelectedBucket] = useState(null);
|
const [selectedBucket, setSelectedBucket] = useState(null);
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const { setContactOpen, setDeleteBucket } = useDirectoryContext();
|
||||||
|
|
||||||
const handleClose = ()=>{
|
const handleClose = () => {
|
||||||
setAction(null);
|
setAction(null);
|
||||||
setSelectedBucket(null);
|
setSelectedBucket(null);
|
||||||
}
|
setDeleteId(null);
|
||||||
|
};
|
||||||
const { mutate: createBucket, isPending: creating } = useCreateBucket(() => {
|
const { mutate: createBucket, isPending: creating } = useCreateBucket(() => {
|
||||||
handleClose()
|
handleClose();
|
||||||
});
|
});
|
||||||
const { mutate: updateBucket, isPending: updating } = useUpdateBucket(() => {
|
const { mutate: updateBucket, isPending: updating } = useUpdateBucket(() => {
|
||||||
handleClose()
|
handleClose();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const handleSubmit = (BucketPayload) => {
|
const handleSubmit = (BucketPayload) => {
|
||||||
if (selectedBucket) {
|
if (selectedBucket) {
|
||||||
updateBucket({
|
updateBucket({
|
||||||
@ -39,13 +43,13 @@ const ManageBucket1 = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container m-0 p-0" style={{ minHeight: "00px" }}>
|
<div className="container m-0 p-0" style={{ minHeight: "00px" }}>
|
||||||
<div className="d-flex justify-content-center">
|
<div className="d-flex justify-content-center">
|
||||||
<p className="fs-5 fw-semibold m-0">Manage Buckets</p>
|
<p className="fs-5 fw-semibold m-0">Manage Buckets</p>
|
||||||
</div>
|
</div>
|
||||||
{action ? (
|
|
||||||
|
{action == "create" ? (
|
||||||
<>
|
<>
|
||||||
<BucketForm
|
<BucketForm
|
||||||
selectedBucket={selectedBucket}
|
selectedBucket={selectedBucket}
|
||||||
@ -58,7 +62,10 @@ const ManageBucket1 = () => {
|
|||||||
isPending={creating || updating}
|
isPending={creating || updating}
|
||||||
/>
|
/>
|
||||||
{action === "edit" && selectedBucket && (
|
{action === "edit" && selectedBucket && (
|
||||||
<AssignedBucket selectedBucket={selectedBucket} handleClose={handleClose} />
|
<AssignedBucket
|
||||||
|
selectedBucket={selectedBucket}
|
||||||
|
handleClose={handleClose}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@ -84,11 +91,7 @@ const ManageBucket1 = () => {
|
|||||||
buckets={data}
|
buckets={data}
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
searchTerm={searchTerm}
|
searchTerm={searchTerm}
|
||||||
onEdit={(bucket) => {
|
onDelete={(id) => setDeleteBucket({isOpen:true,bucketId:id})}
|
||||||
setSelectedBucket(bucket);
|
|
||||||
setAction("edit");
|
|
||||||
}}
|
|
||||||
onDelete={(id) => console.log("delete", id)}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -189,7 +189,7 @@ const DocumentFilterPanel = ({ entityTypeId, onApply }) => {
|
|||||||
<div className="d-flex justify-content-end py-3 gap-2">
|
<div className="d-flex justify-content-end py-3 gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-secondary btn-xs"
|
className="btn btn-label-secondary btn-xs"
|
||||||
onClick={onClear}
|
onClick={onClear}
|
||||||
>
|
>
|
||||||
Clear
|
Clear
|
||||||
|
|||||||
@ -235,24 +235,9 @@ const ManageDocument = ({ closeModal, Document_Entity, Entity }) => {
|
|||||||
<p className="fw-bold fs-6">Upload New Document</p>
|
<p className="fw-bold fs-6">Upload New Document</p>
|
||||||
<FormProvider key={documentTypeId} {...methods}>
|
<FormProvider key={documentTypeId} {...methods}>
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="text-start">
|
<form onSubmit={handleSubmit(onSubmit)} className="text-start">
|
||||||
{/* Document Name */}
|
|
||||||
<div className="mb-2">
|
|
||||||
<Label htmlFor="name" required>
|
|
||||||
Document Name
|
|
||||||
</Label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="form-control form-control-sm"
|
|
||||||
{...register("name")}
|
|
||||||
/>
|
|
||||||
{errors.name && (
|
|
||||||
<div className="danger-text">{errors.name.message}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Category */}
|
{/* Category */}
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<Label htmlFor="documentCategoryId">Document Category</Label>
|
<Label htmlFor="documentCategoryId" required>Document Category</Label>
|
||||||
<select
|
<select
|
||||||
{...register("documentCategoryId")}
|
{...register("documentCategoryId")}
|
||||||
className="form-select form-select-sm"
|
className="form-select form-select-sm"
|
||||||
@ -279,7 +264,7 @@ const ManageDocument = ({ closeModal, Document_Entity, Entity }) => {
|
|||||||
{/* Type */}
|
{/* Type */}
|
||||||
{categoryId && (
|
{categoryId && (
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<Label htmlFor="documentTypeId">Document Type</Label>
|
<Label htmlFor="documentTypeId" required>Document Type</Label>
|
||||||
<select
|
<select
|
||||||
{...register("documentTypeId")}
|
{...register("documentTypeId")}
|
||||||
className="form-select form-select-sm"
|
className="form-select form-select-sm"
|
||||||
@ -303,14 +288,15 @@ const ManageDocument = ({ closeModal, Document_Entity, Entity }) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
{/* Document ID */}
|
{/* Document ID */}
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<Label
|
<label
|
||||||
htmlFor="documentId"
|
htmlFor="documentId"
|
||||||
required={selectedType?.isMandatory ?? false}
|
required={selectedType?.isMandatory ?? false}
|
||||||
>
|
>
|
||||||
Document ID
|
Document ID
|
||||||
</Label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="form-control form-control-sm"
|
className="form-control form-control-sm"
|
||||||
@ -321,6 +307,23 @@ const ManageDocument = ({ closeModal, Document_Entity, Entity }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Document Name */}
|
||||||
|
<div className="mb-2">
|
||||||
|
<Label htmlFor="name" required>
|
||||||
|
Document Name
|
||||||
|
</Label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control form-control-sm"
|
||||||
|
{...register("name")}
|
||||||
|
/>
|
||||||
|
{errors.name && (
|
||||||
|
<div className="danger-text">{errors.name.message}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{/* Upload */}
|
{/* Upload */}
|
||||||
<div className="row my-2">
|
<div className="row my-2">
|
||||||
<div className="col-md-12">
|
<div className="col-md-12">
|
||||||
|
|||||||
@ -16,29 +16,27 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { localToUtc } from "../../utils/appUtils";
|
import { localToUtc } from "../../utils/appUtils";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const EmpAttendance = ({ employee }) => {
|
const EmpAttendance = ({ employee }) => {
|
||||||
const [attendances, setAttendnaces] = useState([]);
|
const [attendances, setAttendnaces] = useState([]);
|
||||||
const [selectedDate, setSelectedDate] = useState("");
|
const [selectedDate, setSelectedDate] = useState("");
|
||||||
const [dateRange, setDateRange] = useState({ startDate: "", endDate: "" });
|
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const [attendanceId, setAttendanecId] = useState();
|
const [attendanceId, setAttendanecId] = useState();
|
||||||
|
|
||||||
|
|
||||||
const methods = useForm({
|
const methods = useForm({
|
||||||
resolver: zodResolver(z.object({
|
resolver: zodResolver(
|
||||||
startDate: z.string(),
|
z.object({
|
||||||
endDate: z.string()
|
startDate: z.string(),
|
||||||
})),
|
endDate: z.string(),
|
||||||
|
})
|
||||||
|
),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
startDate: "",
|
startDate: "",
|
||||||
endDate: ""
|
endDate: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const { control, register, handleSubmit, reset, watch } = methods;
|
const { control, register, handleSubmit, reset, watch } = methods;
|
||||||
const startDate = watch('startDate')
|
const startDate = watch("startDate");
|
||||||
const endDate = watch('endDate')
|
const endDate = watch("endDate");
|
||||||
const {
|
const {
|
||||||
data = [],
|
data = [],
|
||||||
isLoading: loading,
|
isLoading: loading,
|
||||||
@ -46,15 +44,13 @@ const EmpAttendance = ({ employee }) => {
|
|||||||
isError,
|
isError,
|
||||||
error,
|
error,
|
||||||
refetch,
|
refetch,
|
||||||
} = useAttendanceByEmployee(employee, localToUtc(startDate), localToUtc(endDate));
|
} = useAttendanceByEmployee(
|
||||||
|
employee,
|
||||||
|
localToUtc(startDate),
|
||||||
|
localToUtc(endDate)
|
||||||
|
);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
// const { data, loading, error } = useSelector(
|
|
||||||
// (store) => store.employeeAttendance
|
|
||||||
// );
|
|
||||||
|
|
||||||
const [isRefreshing, setIsRefreshing] = useState(true);
|
|
||||||
|
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
today.setHours(0, 0, 0, 0);
|
today.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
@ -92,13 +88,6 @@ const EmpAttendance = ({ employee }) => {
|
|||||||
.sort(sortByName);
|
.sort(sortByName);
|
||||||
const group5 = data.filter((d) => d.activity === 5).sort(sortByName);
|
const group5 = data.filter((d) => d.activity === 5).sort(sortByName);
|
||||||
|
|
||||||
// const sortedFinalList = [
|
|
||||||
// ...group1,
|
|
||||||
// ...group2,
|
|
||||||
// ...group3,
|
|
||||||
// ...group4,
|
|
||||||
// ...group5,
|
|
||||||
// ];
|
|
||||||
|
|
||||||
const uniqueMap = new Map();
|
const uniqueMap = new Map();
|
||||||
|
|
||||||
@ -111,7 +100,7 @@ const EmpAttendance = ({ employee }) => {
|
|||||||
if (
|
if (
|
||||||
!existing ||
|
!existing ||
|
||||||
new Date(rec.checkInTime || rec.checkOutTime) >
|
new Date(rec.checkInTime || rec.checkOutTime) >
|
||||||
new Date(existing.checkInTime || existing.checkOutTime)
|
new Date(existing.checkInTime || existing.checkOutTime)
|
||||||
) {
|
) {
|
||||||
uniqueMap.set(key, rec);
|
uniqueMap.set(key, rec);
|
||||||
}
|
}
|
||||||
@ -135,11 +124,7 @@ const EmpAttendance = ({ employee }) => {
|
|||||||
};
|
};
|
||||||
const closeModal = () => setIsModalOpen(false);
|
const closeModal = () => setIsModalOpen(false);
|
||||||
|
|
||||||
|
const onSubmit = (formData) => {};
|
||||||
|
|
||||||
const onSubmit = (formData) => {
|
|
||||||
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isModalOpen && (
|
{isModalOpen && (
|
||||||
@ -152,27 +137,28 @@ const EmpAttendance = ({ employee }) => {
|
|||||||
className="dataTables_length text-start py-2 d-flex justify-content-between "
|
className="dataTables_length text-start py-2 d-flex justify-content-between "
|
||||||
id="DataTables_Table_0_length"
|
id="DataTables_Table_0_length"
|
||||||
>
|
>
|
||||||
<div className="col-md-4 my-0 ">
|
<div className="col-3 my-0 ">
|
||||||
<>
|
<>
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="p-2 text-start">
|
<form
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
className="p-2 text-start"
|
||||||
|
>
|
||||||
<DateRangePicker1
|
<DateRangePicker1
|
||||||
|
|
||||||
placeholder="DD-MM-YYYY To DD-MM-YYYY"
|
placeholder="DD-MM-YYYY To DD-MM-YYYY"
|
||||||
startField="startDate"
|
startField="startDate"
|
||||||
endField="endDate"
|
endField="endDate"
|
||||||
|
|
||||||
defaultRange={true}
|
defaultRange={true}
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
</>
|
</>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-2 m-0 text-end">
|
<div className="col-md-2 m-0 text-end">
|
||||||
<i
|
<i
|
||||||
className={`bx bx-refresh cursor-pointer fs-4 ${isFetching ? "spin" : ""
|
className={`bx bx-refresh cursor-pointer fs-4 ${
|
||||||
}`}
|
isFetching ? "spin" : ""
|
||||||
|
}`}
|
||||||
data-toggle="tooltip"
|
data-toggle="tooltip"
|
||||||
title="Refresh"
|
title="Refresh"
|
||||||
onClick={() => refetch()}
|
onClick={() => refetch()}
|
||||||
@ -265,8 +251,9 @@ const EmpAttendance = ({ employee }) => {
|
|||||||
{[...Array(totalPages)].map((_, index) => (
|
{[...Array(totalPages)].map((_, index) => (
|
||||||
<li
|
<li
|
||||||
key={index}
|
key={index}
|
||||||
className={`page-item ${currentPage === index + 1 ? "active" : ""
|
className={`page-item ${
|
||||||
}`}
|
currentPage === index + 1 ? "active" : ""
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
className="page-link "
|
className="page-link "
|
||||||
@ -277,8 +264,9 @@ const EmpAttendance = ({ employee }) => {
|
|||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
<li
|
<li
|
||||||
className={`page-item ${currentPage === totalPages ? "disabled" : ""
|
className={`page-item ${
|
||||||
}`}
|
currentPage === totalPages ? "disabled" : ""
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
className="page-link "
|
className="page-link "
|
||||||
|
|||||||
@ -7,6 +7,10 @@ import {
|
|||||||
DIRECTORY_USER,
|
DIRECTORY_USER,
|
||||||
MANAGE_PROJECT_INFRA,
|
MANAGE_PROJECT_INFRA,
|
||||||
MANAGE_TASK,
|
MANAGE_TASK,
|
||||||
|
MANAGE_TEAM,
|
||||||
|
MODIFY_DOCUMENT,
|
||||||
|
UPLOAD_DOCUMENT,
|
||||||
|
VIEW_DOCUMENT,
|
||||||
VIEW_PROJECT_INFRA,
|
VIEW_PROJECT_INFRA,
|
||||||
} from "../../utils/constants";
|
} from "../../utils/constants";
|
||||||
|
|
||||||
@ -17,6 +21,10 @@ const ProjectNav = ({ onPillClick, activePill }) => {
|
|||||||
const DirAdmin = useHasUserPermission(DIRECTORY_ADMIN);
|
const DirAdmin = useHasUserPermission(DIRECTORY_ADMIN);
|
||||||
const DireManager = useHasUserPermission(DIRECTORY_MANAGER);
|
const DireManager = useHasUserPermission(DIRECTORY_MANAGER);
|
||||||
const DirUser = useHasUserPermission(DIRECTORY_USER);
|
const DirUser = useHasUserPermission(DIRECTORY_USER);
|
||||||
|
const isManageTeam = useHasUserPermission(MANAGE_TEAM)
|
||||||
|
const isViewDocuments = hasUserPermission(VIEW_DOCUMENT);
|
||||||
|
const isUploadDocument = useHasUserPermission(UPLOAD_DOCUMENT)
|
||||||
|
const isModifyDocument = useHasUserPermission(MODIFY_DOCUMENT)
|
||||||
|
|
||||||
const ProjectTab = [
|
const ProjectTab = [
|
||||||
{ key: "profile", icon: "bx bx-user", label: "Profile" },
|
{ key: "profile", icon: "bx bx-user", label: "Profile" },
|
||||||
@ -33,8 +41,8 @@ const ProjectNav = ({ onPillClick, activePill }) => {
|
|||||||
label: "Directory",
|
label: "Directory",
|
||||||
hidden: !(DirAdmin || DireManager || DirUser),
|
hidden: !(DirAdmin || DireManager || DirUser),
|
||||||
},
|
},
|
||||||
{ key: "documents", icon: "bx bx-folder-open", label: "Documents" },
|
{ key: "documents", icon: "bx bx-folder-open", label: "Documents",hidden:!(isViewDocuments || isModifyDocument || isUploadDocument) },
|
||||||
{ key: "setting", icon: "bx bxs-cog", label: "Setting" },
|
{ key: "setting", icon: "bx bxs-cog", label: "Setting",hidden:!isManageTeam },
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
<div className="nav-align-top">
|
<div className="nav-align-top">
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, { useCallback, useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
useProjectLevelEmployeePermission,
|
useProjectLevelEmployeePermission,
|
||||||
useProjectLevelModules,
|
useProjectLevelModules,
|
||||||
@ -6,7 +6,7 @@ import {
|
|||||||
} from "../../hooks/useProjects";
|
} from "../../hooks/useProjects";
|
||||||
import { useSelectedProject } from "../../slices/apiDataManager";
|
import { useSelectedProject } from "../../slices/apiDataManager";
|
||||||
import { useEmployeesByProject } from "../../hooks/useEmployees";
|
import { useEmployeesByProject } from "../../hooks/useEmployees";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm, Controller } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import showToast from "../../services/toastService";
|
import showToast from "../../services/toastService";
|
||||||
@ -27,7 +27,9 @@ const ProjectPermission = () => {
|
|||||||
watch,
|
watch,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
formState: { errors },
|
control,
|
||||||
|
setValue,
|
||||||
|
formState: { errors, isDirty },
|
||||||
} = useForm({
|
} = useForm({
|
||||||
resolver: zodResolver(ProjectPermissionSchema),
|
resolver: zodResolver(ProjectPermissionSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@ -43,7 +45,7 @@ const ProjectPermission = () => {
|
|||||||
selectedProject
|
selectedProject
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedEmployee) return;
|
if (!selectedEmployee) return;
|
||||||
|
|
||||||
const enabledPerms =
|
const enabledPerms =
|
||||||
@ -51,127 +53,167 @@ useEffect(() => {
|
|||||||
?.filter((perm) => perm.isEnabled)
|
?.filter((perm) => perm.isEnabled)
|
||||||
?.map((perm) => perm.id) || [];
|
?.map((perm) => perm.id) || [];
|
||||||
|
|
||||||
reset((prev) => ({
|
setValue("selectedPermissions", enabledPerms, { shouldValidate: true });
|
||||||
...prev,
|
}, [selectedEmpPermissions, setValue, selectedEmployee]);
|
||||||
selectedPermissions: enabledPerms,
|
|
||||||
}));
|
|
||||||
}, [selectedEmpPermissions, reset, selectedEmployee]);
|
|
||||||
|
|
||||||
|
const selectedPermissions = watch("selectedPermissions") || [];
|
||||||
|
|
||||||
|
const existingEnabledIds =
|
||||||
|
selectedEmpPermissions?.permissions
|
||||||
|
?.filter((p) => p.isEnabled)
|
||||||
|
?.map((p) => p.id) || [];
|
||||||
|
|
||||||
|
const hasChanges =
|
||||||
|
selectedPermissions.length !== existingEnabledIds.length ||
|
||||||
|
selectedPermissions.some((id) => !existingEnabledIds.includes(id));
|
||||||
|
|
||||||
const { mutate: updatePermission, isPending } =
|
const { mutate: updatePermission, isPending } =
|
||||||
useUpdateProjectLevelEmployeePermission();
|
useUpdateProjectLevelEmployeePermission();
|
||||||
|
|
||||||
const onSubmit = (formData) => {
|
const onSubmit = (formData) => {
|
||||||
if (!formData.employeeId) {
|
if (!formData.employeeId) {
|
||||||
showToast("Please select an employee", "warn");
|
showToast("Please select an employee", "warn");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingPermissions = selectedEmpPermissions?.permissions || [];
|
const existingPermissions = selectedEmpPermissions?.permissions || [];
|
||||||
const existingEnabledIds = existingPermissions
|
const existingEnabledIds = existingPermissions
|
||||||
.filter((p) => p.isEnabled)
|
.filter((p) => p.isEnabled)
|
||||||
.map((p) => p.id);
|
.map((p) => p.id);
|
||||||
|
|
||||||
const newSelectedIds = formData.selectedPermissions || [];
|
const newSelectedIds = formData.selectedPermissions || [];
|
||||||
|
|
||||||
const removed = existingEnabledIds
|
const added = newSelectedIds
|
||||||
.filter((id) => !newSelectedIds.includes(id))
|
.filter((id) => !existingEnabledIds.includes(id))
|
||||||
.map((id) => ({ id, isEnabled: false }));
|
.map((id) => ({ id, isEnabled: true }));
|
||||||
|
|
||||||
const added = newSelectedIds
|
const removed = existingEnabledIds
|
||||||
.filter((id) => !existingEnabledIds.includes(id))
|
.filter((id) => !newSelectedIds.includes(id))
|
||||||
.map((id) => ({ id, isEnabled: true }));
|
.map((id) => ({ id, isEnabled: false }));
|
||||||
|
|
||||||
const payloadPermissions = [...removed, ...added];
|
const payloadPermissions = [...added, ...removed];
|
||||||
|
|
||||||
if (payloadPermissions.length === 0) {
|
if (payloadPermissions.length === 0) {
|
||||||
showToast("No changes detected", "info");
|
showToast("No changes detected", "info");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
employeeId: formData.employeeId,
|
employeeId: formData.employeeId,
|
||||||
projectId: selectedProject,
|
projectId: selectedProject,
|
||||||
permission: payloadPermissions,
|
permission: payloadPermissions,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("Final payload:", payload);
|
||||||
|
updatePermission(payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
updatePermission(payload);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="row px-2 py-1">
|
<div className="w-100 p py-1 ">
|
||||||
|
<div className="text-start m-0">
|
||||||
|
<p className="fw-semibold fs-6">Project Permission</p>
|
||||||
|
</div>
|
||||||
<form className="row" onSubmit={handleSubmit(onSubmit)}>
|
<form className="row" onSubmit={handleSubmit(onSubmit)}>
|
||||||
{/* Employee Dropdown */}
|
<div className="d-flex justify-content-between align-items-end gap-2 mb-3">
|
||||||
<div className="d-flex align-items-end gap-2">
|
<div className="text-start d-flex align-items-center gap-2">
|
||||||
<div className="text-start">
|
<div className="d-block">
|
||||||
<label className="form-label">Select Employee</label>
|
<label className="form-label">Select Employee</label>
|
||||||
<select
|
</div>
|
||||||
className="form-select form-select-sm"
|
<div className="d-block">
|
||||||
{...register("employeeId")}
|
{" "}
|
||||||
disabled={isPending}
|
<select
|
||||||
>
|
className="form-select form-select-sm"
|
||||||
{loading ? (
|
{...register("employeeId")}
|
||||||
<option value="">Loading...</option>
|
disabled={isPending}
|
||||||
) : (
|
>
|
||||||
<>
|
{loading ? (
|
||||||
<option value="">-- Select Employee --</option>
|
<option value="">Loading...</option>
|
||||||
{[...employees]?.sort((a, b) =>
|
) : (
|
||||||
`${a.firstName} ${a.firstName}`?.localeCompare(
|
<>
|
||||||
`${b.firstName} ${b.lastName}`
|
<option value="">-- Select Employee --</option>
|
||||||
|
{[...employees]
|
||||||
|
?.sort((a, b) =>
|
||||||
|
`${a?.firstName} ${a?.firstName}`?.localeCompare(
|
||||||
|
`${b?.firstName} ${b?.lastName}`
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
?.map((emp) => (
|
||||||
?.map((emp) => (
|
<option key={emp.id} value={emp.id}>
|
||||||
<option key={emp.id} value={emp.id}>
|
{emp.firstName} {emp.lastName}
|
||||||
{emp.firstName} {emp.lastName}
|
</option>
|
||||||
</option>
|
))}
|
||||||
))}
|
</>
|
||||||
</>
|
)}
|
||||||
|
</select>
|
||||||
|
{errors.employeeId && (
|
||||||
|
<div className="d-block text-danger small">
|
||||||
|
{errors.employeeId.message}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</select>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{errors.employeeId && (
|
<div className="mt-3 text-end">
|
||||||
<div className="text-danger small">
|
{hasChanges && (
|
||||||
{errors.employeeId.message}
|
<button
|
||||||
</div>
|
type="submit"
|
||||||
|
className="btn btn-sm btn-primary"
|
||||||
|
disabled={isPending || loading}
|
||||||
|
>
|
||||||
|
{isPending ? "Please Wait..." : "Save Permission"}
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
className="btn btn-sm btn-primary"
|
|
||||||
disabled={isPending || loading}
|
|
||||||
>
|
|
||||||
{isPending ? "Please Wait..." : "Update Permission"}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Permissions */}
|
|
||||||
{ProjectModules.map((feature) => (
|
{ProjectModules.map((feature) => (
|
||||||
<div key={feature.id} className="row my-2 px-3 ">
|
<div
|
||||||
<div className="col-12 text-start fw-semibold mb-2">
|
key={feature.id}
|
||||||
{feature.name}
|
className="col-12 col-sm-6 col-md-4 col-lg-4 mb-4"
|
||||||
</div>
|
>
|
||||||
<div className="col-12">
|
<div className="card text-start border-1 p-1">
|
||||||
<div className="row">
|
<p className="card-title fs-6 fw-semibold">{feature.name}</p>
|
||||||
{feature.featurePermissions?.map((perm) => (
|
<div className="px-2">
|
||||||
<div className="col-12 col-sm-6 col-md-4 mb-2" key={perm.id}>
|
<ul className="list-unstyled">
|
||||||
<label
|
{feature.featurePermissions?.map((perm) => (
|
||||||
className="form-check-label d-flex align-items-center"
|
<div className="d-flex my-2" key={perm.id}>
|
||||||
htmlFor={perm.id}
|
<Controller
|
||||||
>
|
name="selectedPermissions"
|
||||||
<input
|
control={control}
|
||||||
type="checkbox"
|
render={({ field }) => {
|
||||||
className="form-check-input me-2"
|
const value = field.value || [];
|
||||||
id={perm.id}
|
const isChecked = value.includes(perm.id);
|
||||||
value={perm.id}
|
|
||||||
{...register("selectedPermissions")}
|
return (
|
||||||
|
<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}
|
||||||
|
checked={isChecked}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.checked) {
|
||||||
|
field.onChange([...value, perm.id]); // add
|
||||||
|
} else {
|
||||||
|
field.onChange(
|
||||||
|
value.filter((v) => v !== perm.id)
|
||||||
|
); // remove
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{perm.name}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{perm.name}
|
</div>
|
||||||
</label>
|
))}
|
||||||
</div>
|
</ul>
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr className="my-2" />
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -3,9 +3,10 @@ import { ComingSoonPage } from "../../pages/Misc/ComingSoonPage";
|
|||||||
import ProjectPermission from "./ProjectPermission";
|
import ProjectPermission from "./ProjectPermission";
|
||||||
|
|
||||||
const ProjectSetting = () => {
|
const ProjectSetting = () => {
|
||||||
const [activePill, setActivePill] = useState(() => {
|
const [activePill, setActivePill] = useState("Permissions")
|
||||||
return localStorage.getItem("lastActiveProjectSettingTab") || "Permissions";
|
// const [activePill, setActivePill] = useState(() => {
|
||||||
});
|
// return localStorage.getItem("lastActiveProjectSettingTab") || "Permissions";
|
||||||
|
// });
|
||||||
const projectSettingTab = [
|
const projectSettingTab = [
|
||||||
{ key: "Permissions", label: "Permissions" },
|
{ key: "Permissions", label: "Permissions" },
|
||||||
{ key: "Notification", label: "Notification" },
|
{ key: "Notification", label: "Notification" },
|
||||||
@ -32,7 +33,7 @@ const ProjectSetting = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="w-100">
|
<div className="w-100">
|
||||||
<div className="card py-2 px-5">
|
<div className="card py-2 px-5">
|
||||||
<div className="col-12">
|
{/* <div className="col-12">
|
||||||
<div className="dropdown text-end">
|
<div className="dropdown text-end">
|
||||||
<button
|
<button
|
||||||
className="btn btn-sm btn-outline-primary dropdown-toggle"
|
className="btn btn-sm btn-outline-primary dropdown-toggle"
|
||||||
@ -63,7 +64,7 @@ const ProjectSetting = () => {
|
|||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
<div className="mt-3">{renderContent()}</div>
|
<div className="mt-3">{renderContent()}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -125,18 +125,18 @@ const TenantsList = ({
|
|||||||
];
|
];
|
||||||
if (isInitialLoading)
|
if (isInitialLoading)
|
||||||
return <TenantTableSkeleton columns={TenantColumns} rows={13} />;
|
return <TenantTableSkeleton columns={TenantColumns} rows={13} />;
|
||||||
if (isError)
|
if (isError)
|
||||||
return (
|
return (
|
||||||
<div className="">
|
<div className="">
|
||||||
<div className="card text-center my-4 p-2">
|
<div className="text-center my-4 p-2">
|
||||||
<i className="fa-solid fa-triangle-exclamation fs-5"></i>
|
<i className="fa-solid fa-triangle-exclamation fs-5"></i>
|
||||||
<p>{error.message}</p>
|
<p>{error.message}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="card p-2 mt-3">
|
<div className="p-2 mt-3">
|
||||||
<div className="card-datatable text-nowrap table-responsive">
|
<div className="card-datatable text-nowrap table-responsive">
|
||||||
<table className="table border-top dataTable text-nowrap">
|
<table className="table border-top dataTable text-nowrap">
|
||||||
<thead>
|
<thead>
|
||||||
|
|||||||
@ -1,11 +1,26 @@
|
|||||||
|
import { useSelectedProject } from "../slices/apiDataManager";
|
||||||
|
import { useAllProjectLevelPermissions, useProfile } from "./useProfile";
|
||||||
|
|
||||||
import { useProfile } from "./useProfile"
|
|
||||||
export const useHasUserPermission = (permission) => {
|
export const useHasUserPermission = (permission) => {
|
||||||
|
const selectedProject = useSelectedProject();
|
||||||
const { profile } = useProfile();
|
const { profile } = useProfile();
|
||||||
|
const {
|
||||||
|
data: projectPermissions = [],
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
} = useAllProjectLevelPermissions(selectedProject);
|
||||||
|
|
||||||
if (profile && permission && typeof permission === "string") {
|
if (isLoading || !permission) return false;
|
||||||
return profile?.featurePermissions.includes(permission);
|
|
||||||
|
const globalPerms = profile?.featurePermissions ?? [];
|
||||||
|
const projectPerms = projectPermissions ?? [];
|
||||||
|
if (selectedProject) {
|
||||||
|
if (projectPerms.length === 0) {
|
||||||
|
return projectPerms.includes(permission);
|
||||||
|
} else {
|
||||||
|
return projectPerms.includes(permission);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return globalPerms.includes(permission);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,67 +1,20 @@
|
|||||||
import {useState,useEffect, useCallback} from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import AuthRepository from "../repositories/AuthRepository";
|
import AuthRepository from "../repositories/AuthRepository";
|
||||||
import {cacheData, cacheProfileData, getCachedData, getCachedProfileData} from "../slices/apiDataManager";
|
import {
|
||||||
import {useSelector} from "react-redux";
|
cacheData,
|
||||||
|
cacheProfileData,
|
||||||
|
getCachedData,
|
||||||
|
getCachedProfileData,
|
||||||
|
useSelectedProject,
|
||||||
|
} from "../slices/apiDataManager";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
import eventBus from "../services/eventBus";
|
import eventBus from "../services/eventBus";
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import ProjectRepository from "../repositories/ProjectRepository";
|
||||||
|
|
||||||
let hasFetched = false;
|
let hasFetched = false;
|
||||||
let hasReceived = false;
|
let hasReceived = false;
|
||||||
|
|
||||||
// export const useProfile = () => {
|
|
||||||
// const loggedUser = useSelector( ( store ) => store.globalVariables.loginUser );
|
|
||||||
// const [profile, setProfile] = useState(null);
|
|
||||||
// const [loading, setLoading] = useState(false);
|
|
||||||
// const [error, setError] = useState("");
|
|
||||||
|
|
||||||
// const fetchData = async () => {
|
|
||||||
// try {
|
|
||||||
// setLoading(true);
|
|
||||||
// let response = await AuthRepository.profile();
|
|
||||||
// setProfile(response.data);
|
|
||||||
// cacheProfileData(response.data);
|
|
||||||
// } catch (error) {
|
|
||||||
// setError("Failed to fetch data.");
|
|
||||||
// } finally {
|
|
||||||
// setLoading(false);
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const validation = () => {
|
|
||||||
// if (!hasFetched) {
|
|
||||||
// hasFetched = true;
|
|
||||||
// if (!loggedUser) {
|
|
||||||
// fetchData();
|
|
||||||
// } else {
|
|
||||||
// setProfile(loggedUser);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// setProfile(loggedUser);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// validation();
|
|
||||||
// }, [loggedUser]);
|
|
||||||
|
|
||||||
// const handler = useCallback(
|
|
||||||
// (data) => {
|
|
||||||
// if(!getCachedData("hasReceived")){
|
|
||||||
// cacheData("hasReceived", true);
|
|
||||||
// hasFetched = false;
|
|
||||||
// validation();
|
|
||||||
// }
|
|
||||||
// },[]
|
|
||||||
// );
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// eventBus.on("assign_project_one", handler);
|
|
||||||
// return () => eventBus.off("assign_project_one", handler);
|
|
||||||
// }, [handler]);
|
|
||||||
|
|
||||||
// return { profile, loading, error };
|
|
||||||
// };
|
|
||||||
|
|
||||||
export const useProfile = () => {
|
export const useProfile = () => {
|
||||||
const loggedUser = useSelector((store) => store.globalVariables.loginUser);
|
const loggedUser = useSelector((store) => store.globalVariables.loginUser);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@ -100,12 +53,26 @@ export const useProfile = () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useSidBarMenu = () => {
|
||||||
export const useSidBarMenu = ()=>{
|
const userLogged = useSelector((store) => store.globalVariables.loginUser);
|
||||||
const userLogged = useSelector((store)=>store.globalVariables.loginUser);
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey:["AppMenu"],
|
queryKey: ["AppMenu"],
|
||||||
queryFn:async()=> await AuthRepository.appmenu(),
|
queryFn: async () => await AuthRepository.appmenu(),
|
||||||
enabled: !!userLogged
|
enabled: !!userLogged,
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export const useAllProjectLevelPermissions = (projectId) => {
|
||||||
|
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["AllProjectLevelPermission", projectId],
|
||||||
|
queryFn: async () => {
|
||||||
|
const resp = await ProjectRepository.getAllProjectLevelPermission(
|
||||||
|
projectId
|
||||||
|
);
|
||||||
|
return resp.data;
|
||||||
|
},
|
||||||
|
enabled: !!projectId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
26
src/hooks/useProjectAccess.js
Normal file
26
src/hooks/useProjectAccess.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useHasUserPermission } from "./useHasUserPermission";
|
||||||
|
import { useAllProjectLevelPermissions } from "./useProfile";
|
||||||
|
import { VIEW_PROJECTS } from "../utils/constants";
|
||||||
|
import showToast from "../services/toastService";
|
||||||
|
|
||||||
|
export const useProjectAccess = (projectId) => {
|
||||||
|
const { data: projectPermissions, isLoading, isFetched } =
|
||||||
|
useAllProjectLevelPermissions(projectId);
|
||||||
|
|
||||||
|
const canView = useHasUserPermission(VIEW_PROJECTS);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (projectId && isFetched && !isLoading && !canView) {
|
||||||
|
showToast("You don't have permission to view project details", "warning");
|
||||||
|
navigate("/projects");
|
||||||
|
}
|
||||||
|
}, [projectId, isFetched, isLoading, canView, navigate]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
canView,
|
||||||
|
loading: isLoading || !isFetched,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -177,6 +177,7 @@ export const useProjectInfra = (projectId) => {
|
|||||||
data: projectInfra,
|
data: projectInfra,
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
|
isFetched
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["ProjectInfra", projectId],
|
queryKey: ["ProjectInfra", projectId],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
@ -190,7 +191,7 @@ export const useProjectInfra = (projectId) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return { projectInfra, isLoading, error };
|
return { projectInfra, isLoading, error,isFetched };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useProjectTasks = (workAreaId, IsExpandedArea = false) => {
|
export const useProjectTasks = (workAreaId, IsExpandedArea = false) => {
|
||||||
|
|||||||
@ -95,11 +95,11 @@ const AttendancePage = () => {
|
|||||||
{(modelConfig?.action === 0 ||
|
{(modelConfig?.action === 0 ||
|
||||||
modelConfig?.action === 1 ||
|
modelConfig?.action === 1 ||
|
||||||
modelConfig?.action === 2) && (
|
modelConfig?.action === 2) && (
|
||||||
<CheckCheckOutmodel
|
<CheckCheckOutmodel
|
||||||
modeldata={modelConfig}
|
modeldata={modelConfig}
|
||||||
closeModal={closeModal}
|
closeModal={closeModal}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/* For view logs */}
|
{/* For view logs */}
|
||||||
{modelConfig?.action === 6 && (
|
{modelConfig?.action === 6 && (
|
||||||
<AttendLogs Id={modelConfig?.id} closeModal={closeModal} />
|
<AttendLogs Id={modelConfig?.id} closeModal={closeModal} />
|
||||||
@ -118,19 +118,19 @@ const AttendancePage = () => {
|
|||||||
]}
|
]}
|
||||||
></Breadcrumb>
|
></Breadcrumb>
|
||||||
|
|
||||||
<div className="nav-align-top nav-tabs-shadow">
|
<div className="nav-align-top nav-tabs-shadow ">
|
||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
<div className="nav-align-top nav-tabs-shadow bg-white border-bottom">
|
<div className="nav-align-top nav-tabs-shadow bg-white border-bottom pt-5">
|
||||||
<div className="row align-items-center g-0 mb-3 mb-md-0">
|
<div className="row align-items-center g-0 mb-3 mb-md-0 mx-5">
|
||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
<div className="col-12 col-md">
|
<div className="col-12 col-md">
|
||||||
<ul className="nav nav-tabs" role="tablist">
|
<ul className="nav nav-tabs" role="tablist">
|
||||||
<li className="nav-item">
|
<li className="nav-item">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`nav-link ${
|
className={`nav-link ${
|
||||||
activeTab === "all" ? "active" : ""
|
activeTab === "all" ? "active" : ""
|
||||||
} fs-6`}
|
} fs-6`}
|
||||||
onClick={() => handleTabChange("all")}
|
onClick={() => handleTabChange("all")}
|
||||||
data-bs-toggle="tab"
|
data-bs-toggle="tab"
|
||||||
data-bs-target="#navs-top-home"
|
data-bs-target="#navs-top-home"
|
||||||
@ -141,9 +141,9 @@ const AttendancePage = () => {
|
|||||||
<li className="nav-item">
|
<li className="nav-item">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`nav-link ${
|
className={`nav-link ${
|
||||||
activeTab === "logs" ? "active" : ""
|
activeTab === "logs" ? "active" : ""
|
||||||
} fs-6`}
|
} fs-6`}
|
||||||
onClick={() => handleTabChange("logs")}
|
onClick={() => handleTabChange("logs")}
|
||||||
data-bs-toggle="tab"
|
data-bs-toggle="tab"
|
||||||
data-bs-target="#navs-top-profile"
|
data-bs-target="#navs-top-profile"
|
||||||
@ -155,9 +155,9 @@ const AttendancePage = () => {
|
|||||||
<li className={`nav-item ${!DoRegularized ? "d-none" : ""}`}>
|
<li className={`nav-item ${!DoRegularized ? "d-none" : ""}`}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`nav-link ${
|
className={`nav-link ${
|
||||||
activeTab === "regularization" ? "active" : ""
|
activeTab === "regularization" ? "active" : ""
|
||||||
} fs-6`}
|
} fs-6`}
|
||||||
onClick={() => handleTabChange("regularization")}
|
onClick={() => handleTabChange("regularization")}
|
||||||
data-bs-toggle="tab"
|
data-bs-toggle="tab"
|
||||||
data-bs-target="#navs-top-messages"
|
data-bs-target="#navs-top-messages"
|
||||||
@ -182,11 +182,11 @@ const AttendancePage = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="tab-content attedanceTabs py-0 px-1 px-sm-3">
|
<div className="tab-content attedanceTabs py-0 px-1 px-sm-3 pb-10">
|
||||||
{selectedProject ? (
|
{selectedProject ? (
|
||||||
<>
|
<>
|
||||||
{activeTab === "all" && (
|
{activeTab === "all" && (
|
||||||
<div className="tab-pane fade show active py-0">
|
<div className="tab-pane fade show active py-0 mx-5">
|
||||||
<Attendance
|
<Attendance
|
||||||
handleModalData={handleModalData}
|
handleModalData={handleModalData}
|
||||||
getRole={getRole}
|
getRole={getRole}
|
||||||
@ -214,7 +214,6 @@ const AttendancePage = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -13,7 +13,7 @@ const { projectNames = [], loading: projectLoading } = useProjectName();
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedProject) {
|
if (!selectedProject) {
|
||||||
dispatch(setProjectId(projectNames[0].id));
|
dispatch(setProjectId(projectNames[0]?.id));
|
||||||
}
|
}
|
||||||
}, [projectNames, selectedProject?.id, dispatch]);
|
}, [projectNames, selectedProject?.id, dispatch]);
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,11 @@ import {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import Breadcrumb from "../../components/common/Breadcrumb";
|
import Breadcrumb from "../../components/common/Breadcrumb";
|
||||||
import { useFab } from "../../Context/FabContext";
|
import { useFab } from "../../Context/FabContext";
|
||||||
import { useBucketList, useBuckets } from "../../hooks/useDirectory";
|
import {
|
||||||
|
useBucketList,
|
||||||
|
useBuckets,
|
||||||
|
useDeleteBucket,
|
||||||
|
} from "../../hooks/useDirectory";
|
||||||
import ManageBucket1 from "../../components/Directory/ManageBucket1";
|
import ManageBucket1 from "../../components/Directory/ManageBucket1";
|
||||||
import ManageContact from "../../components/Directory/ManageContact";
|
import ManageContact from "../../components/Directory/ManageContact";
|
||||||
import BucketList from "../../components/Directory/BucketList";
|
import BucketList from "../../components/Directory/BucketList";
|
||||||
@ -16,6 +20,8 @@ import { MainDirectoryPageSkeleton } from "../../components/Directory/DirectoryP
|
|||||||
import ContactProfile from "../../components/Directory/ContactProfile";
|
import ContactProfile from "../../components/Directory/ContactProfile";
|
||||||
import GlobalModel from "../../components/common/GlobalModel";
|
import GlobalModel from "../../components/common/GlobalModel";
|
||||||
import { exportToCSV } from "../../utils/exportUtils";
|
import { exportToCSV } from "../../utils/exportUtils";
|
||||||
|
import ConfirmModal from "../../components/common/ConfirmModal";
|
||||||
|
import { useSelectedProject } from "../../slices/apiDataManager";
|
||||||
|
|
||||||
const NotesPage = lazy(() => import("./NotesPage"));
|
const NotesPage = lazy(() => import("./NotesPage"));
|
||||||
const ContactsPage = lazy(() => import("./ContactsPage"));
|
const ContactsPage = lazy(() => import("./ContactsPage"));
|
||||||
@ -44,6 +50,10 @@ export default function DirectoryPage({ IsPage = true, projectId = null }) {
|
|||||||
isOpen: false,
|
isOpen: false,
|
||||||
contactId: null,
|
contactId: null,
|
||||||
});
|
});
|
||||||
|
const [deleteBucket, setDeleteBucket] = useState({
|
||||||
|
isOpen: false,
|
||||||
|
bucketId: null,
|
||||||
|
});
|
||||||
const [showActive, setShowActive] = useState(true);
|
const [showActive, setShowActive] = useState(true);
|
||||||
const [contactOpen, setContactOpen] = useState({
|
const [contactOpen, setContactOpen] = useState({
|
||||||
contact: null,
|
contact: null,
|
||||||
@ -100,129 +110,140 @@ export default function DirectoryPage({ IsPage = true, projectId = null }) {
|
|||||||
data,
|
data,
|
||||||
setManageContact,
|
setManageContact,
|
||||||
setContactOpen,
|
setContactOpen,
|
||||||
|
setDeleteBucket,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { mutate: DeleteBucket, isPending: Deleting } = useDeleteBucket(() => {
|
||||||
|
setDeleteBucket({ isOpen: false, bucketId: null });
|
||||||
|
});
|
||||||
|
const handleDelete = (bucketId) => {
|
||||||
|
DeleteBucket(bucketId);
|
||||||
|
};
|
||||||
if (isLoading) return <MainDirectoryPageSkeleton />;
|
if (isLoading) return <MainDirectoryPageSkeleton />;
|
||||||
if (isError) return <div>{error.message}</div>;
|
if (isError) return <div>{error.message}</div>;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DirectoryContext.Provider value={contextValues}>
|
<DirectoryContext.Provider value={contextValues}>
|
||||||
<div className={`${IsPage ? "container-fluid" : ""}`}>
|
<div className={`${IsPage ? "container-fluid" : ""}`}>
|
||||||
{IsPage && (<Breadcrumb
|
{IsPage && (
|
||||||
data={[
|
<Breadcrumb
|
||||||
{ label: "Home", link: "/dashboard" },
|
data={[
|
||||||
{ label: "Directory", link: null },
|
{ label: "Home", link: "/dashboard" },
|
||||||
]}
|
{ label: "Directory", link: null },
|
||||||
></Breadcrumb>)}
|
]}
|
||||||
|
></Breadcrumb>
|
||||||
|
)}
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="d-flex justify-content-between align-items-center mb-1 px-2">
|
<div className="d-flex-row px-2">
|
||||||
<ul className="nav nav-tabs">
|
<div className="d-flex justify-content-between align-items-center mb-1">
|
||||||
<li className="nav-item cursor-pointer">
|
<ul className="nav nav-tabs">
|
||||||
<a
|
<li className="nav-item cursor-pointer">
|
||||||
className={`nav-link ${
|
|
||||||
activeTab === "notes" ? "active" : ""
|
|
||||||
} fs-6`}
|
|
||||||
onClick={(e) => handleTabClick("notes", e)}
|
|
||||||
>
|
|
||||||
<i className="bx bx-note bx-sm me-1_5"></i>
|
|
||||||
<span className="d-none d-md-inline">Notes</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li className="nav-item cursor-pointer">
|
|
||||||
<a
|
|
||||||
className={`nav-link ${
|
|
||||||
activeTab === "contacts" ? "active" : ""
|
|
||||||
} fs-6`}
|
|
||||||
onClick={(e) => handleTabClick("contacts", e)}
|
|
||||||
>
|
|
||||||
<i className="bx bxs-contact bx-sm me-1_5"></i>
|
|
||||||
<span className="d-none d-md-inline">Contacts</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<div className="btn-group">
|
|
||||||
<button
|
|
||||||
className="btn btn-sm btn-label-secondary dropdown-toggle"
|
|
||||||
type="button"
|
|
||||||
data-bs-toggle="dropdown"
|
|
||||||
aria-expanded="false"
|
|
||||||
>
|
|
||||||
<i className="bx bx-export me-2 bx-sm"></i>Export
|
|
||||||
</button>
|
|
||||||
<ul className="dropdown-menu">
|
|
||||||
<li>
|
|
||||||
{/* <a className="dropdown-item" href="#"> */}
|
|
||||||
<a
|
<a
|
||||||
className="dropdown-item cursor-pointer"
|
className={`nav-link ${
|
||||||
onClick={() => handleExport("csv")}
|
activeTab === "notes" ? "active" : ""
|
||||||
|
} fs-6`}
|
||||||
|
onClick={(e) => handleTabClick("notes", e)}
|
||||||
>
|
>
|
||||||
<i className="bx bx-file me-1"></i> CSV
|
<i className="bx bx-notepad bx-sm me-1_5"></i>
|
||||||
|
<span className="d-none d-md-inline">Notes</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li className="nav-item cursor-pointer">
|
||||||
|
<a
|
||||||
|
className={`nav-link ${
|
||||||
|
activeTab === "contacts" ? "active" : ""
|
||||||
|
} fs-6`}
|
||||||
|
onClick={(e) => handleTabClick("contacts", e)}
|
||||||
|
>
|
||||||
|
<i className="bx bxs-contact bx-sm me-1_5"></i>
|
||||||
|
<span className="d-none d-md-inline">Contacts</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-1 px-2">
|
<div className="mb-1 px-2 py-3">
|
||||||
<div className="d-flex align-items-center justify-content-between">
|
<div className="d-flex align-items-center justify-content-between">
|
||||||
<div>
|
<div className="d-flex align-items-center gap-3">
|
||||||
{activeTab === "notes" && (
|
{activeTab === "notes" && (
|
||||||
<input
|
<input
|
||||||
type="search"
|
type="search"
|
||||||
className="form-control form-control-sm"
|
className="form-control form-control-sm"
|
||||||
placeholder="Search notes..."
|
placeholder="Search notes..."
|
||||||
value={searchNote}
|
value={searchNote}
|
||||||
onChange={(e) => setSearchNote(e.target.value)}
|
onChange={(e) => setSearchNote(e.target.value)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === "contacts" && (
|
{activeTab === "contacts" && (
|
||||||
<div className="d-flex align-items-center">
|
<div className="d-flex align-items-center gap-3">
|
||||||
<div className="d-flex gap-2 align-items-center">
|
<div className="d-flex gap-2 align-items-center">
|
||||||
<input
|
<input
|
||||||
type="search"
|
type="search"
|
||||||
className="form-control form-control-sm"
|
className="form-control form-control-sm"
|
||||||
placeholder="Search contacts..."
|
placeholder="Search contacts..."
|
||||||
value={searchContact}
|
value={searchContact}
|
||||||
onChange={(e) => setsearchContact(e.target.value)}
|
onChange={(e) => setsearchContact(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
className={`btn btn-xs ${
|
className={`btn btn-sm p-1 ${
|
||||||
!gridView ? "btn-primary" : "btn-outline-secondary"
|
!gridView ? "btn-primary" : "btn-outline-primary"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setGridView(false)}
|
onClick={() => setGridView(false)}
|
||||||
>
|
>
|
||||||
<i className="bx bx-list-ul"></i>
|
<i className="bx bx-list-ul"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className={`btn btn-xs ${
|
className={`btn btn-sm p-1 ${
|
||||||
gridView ? "btn-primary" : "btn-outline-secondary"
|
gridView ? " btn-primary" : " btn-outline-primary"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setGridView(true)}
|
onClick={() => setGridView(true)}
|
||||||
>
|
>
|
||||||
<i className="bx bx-grid-alt"></i>
|
<i className="bx bx-grid-alt"></i>
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-check form-switch d-flex align-items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="form-check-input"
|
||||||
|
role="switch"
|
||||||
|
id="inactiveEmployeesCheckbox"
|
||||||
|
checked={showActive}
|
||||||
|
onChange={(e) => setShowActive(e.target.checked)}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
className="form-check-label ms-2"
|
||||||
|
htmlFor="inactiveEmployeesCheckbox"
|
||||||
|
>
|
||||||
|
{showActive ? "Active" : "Inactive"} Contacts
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-check form-switch d-flex align-items-center ms-4">
|
)}
|
||||||
<input
|
</div>
|
||||||
type="checkbox"
|
|
||||||
className="form-check-input"
|
<div className="btn-group">
|
||||||
role="switch"
|
<button
|
||||||
id="inactiveEmployeesCheckbox"
|
className="btn btn-sm btn-label-secondary dropdown-toggle"
|
||||||
checked={showActive}
|
type="button"
|
||||||
onChange={(e) => setShowActive(e.target.checked)}
|
data-bs-toggle="dropdown"
|
||||||
/>
|
aria-expanded="false"
|
||||||
<label
|
>
|
||||||
className="form-check-label ms-2"
|
<i className="bx bx-export me-2 bx-sm"></i>Export
|
||||||
htmlFor="inactiveEmployeesCheckbox"
|
</button>
|
||||||
|
<ul className="dropdown-menu">
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
className="dropdown-item cursor-pointer"
|
||||||
|
onClick={() => handleExport("csv")}
|
||||||
>
|
>
|
||||||
{showActive ? "Active" : "Inactive" }
|
<i className="bx bx-file me-1"></i> CSV
|
||||||
</label>
|
</a>
|
||||||
</div>
|
</li>
|
||||||
</div>
|
</ul>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -231,10 +252,18 @@ export default function DirectoryPage({ IsPage = true, projectId = null }) {
|
|||||||
<div>
|
<div>
|
||||||
<Suspense fallback={<MainDirectoryPageSkeleton />}>
|
<Suspense fallback={<MainDirectoryPageSkeleton />}>
|
||||||
{activeTab === "notes" && (
|
{activeTab === "notes" && (
|
||||||
<NotesPage projectId={projectId} searchText={searchNote} onExport={setNotesData} />
|
<NotesPage
|
||||||
|
projectId={projectId}
|
||||||
|
searchText={searchNote}
|
||||||
|
onExport={setNotesData}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{activeTab === "contacts" && (
|
{activeTab === "contacts" && (
|
||||||
<ContactsPage projectId={projectId} searchText={searchContact} onExport={setContactData} />
|
<ContactsPage
|
||||||
|
projectId={projectId}
|
||||||
|
searchText={searchContact}
|
||||||
|
onExport={setContactData}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
@ -274,6 +303,19 @@ export default function DirectoryPage({ IsPage = true, projectId = null }) {
|
|||||||
/>
|
/>
|
||||||
</GlobalModel>
|
</GlobalModel>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{deleteBucket.isOpen && (
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={deleteBucket.isOpen}
|
||||||
|
type="delete"
|
||||||
|
header="Delete Bucket"
|
||||||
|
message="Are you sure you want delete?"
|
||||||
|
onSubmit={handleDelete}
|
||||||
|
onClose={() => setDeleteBucket({ isOpen: false, bucketId: null })}
|
||||||
|
loading={Deleting}
|
||||||
|
paramData={deleteBucket.bucketId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</DirectoryContext.Provider>
|
</DirectoryContext.Provider>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -299,7 +299,7 @@ nav.layout-navbar.navbar-active::after {
|
|||||||
color: #d3d4dc;
|
color: #d3d4dc;
|
||||||
}
|
}
|
||||||
.landing-footer .footer-bottom {
|
.landing-footer .footer-bottom {
|
||||||
background-color: #282c3e;
|
background-color: #f44336;
|
||||||
}
|
}
|
||||||
.landing-footer .footer-link {
|
.landing-footer .footer-link {
|
||||||
transition: all 0.2s ease-in-out;
|
transition: all 0.2s ease-in-out;
|
||||||
@ -312,6 +312,7 @@ nav.layout-navbar.navbar-active::after {
|
|||||||
padding-bottom: 1.3rem;
|
padding-bottom: 1.3rem;
|
||||||
border-top-left-radius: 1.75rem;
|
border-top-left-radius: 1.75rem;
|
||||||
border-top-right-radius: 1.75rem;
|
border-top-right-radius: 1.75rem;
|
||||||
|
background-color: #f44336;
|
||||||
}
|
}
|
||||||
@media (max-width: 767.98px) {
|
@media (max-width: 767.98px) {
|
||||||
.landing-footer .footer-top {
|
.landing-footer .footer-top {
|
||||||
|
|||||||
@ -12,8 +12,7 @@ import "swiper/css";
|
|||||||
import "swiper/css/navigation";
|
import "swiper/css/navigation";
|
||||||
import SwaperSlideContent from "./SwaperSlideContent";
|
import SwaperSlideContent from "./SwaperSlideContent";
|
||||||
import SwaperBlogContent from "./SwaperBlogContent";
|
import SwaperBlogContent from "./SwaperBlogContent";
|
||||||
|
import SubscriptionPlans from "./SubscriptionPlans";
|
||||||
|
|
||||||
|
|
||||||
const swiperConfig = {
|
const swiperConfig = {
|
||||||
spaceBetween: 30,
|
spaceBetween: 30,
|
||||||
@ -110,7 +109,7 @@ const LandingPage = () => {
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li className="nav-item">
|
<li className="nav-item">
|
||||||
<a className="nav-link fw-medium" href="#landingReviews">
|
<a className="nav-link fw-medium" href="#sectionBlog">
|
||||||
Blogs
|
Blogs
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@ -182,7 +181,7 @@ const LandingPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
id="heroDashboardAnimation"
|
id="heroDashboardAnimation"
|
||||||
className="hero-animation-img mt-10"
|
className="hero-animation-img mt-10"
|
||||||
>
|
>
|
||||||
@ -296,10 +295,7 @@ const LandingPage = () => {
|
|||||||
<div className="features-icon-wrapper row gx-0 gy-6 g-sm-12">
|
<div className="features-icon-wrapper row gx-0 gy-6 g-sm-12">
|
||||||
<div className="col-lg-3 col-sm-4 text-center features-icon-box">
|
<div className="col-lg-3 col-sm-4 text-center features-icon-box">
|
||||||
<div className="text-center mb-4">
|
<div className="text-center mb-4">
|
||||||
<img
|
<img src="/img/icons/laptop.svg" alt="laptop charging" />
|
||||||
src="/img/icons/laptop.svg"
|
|
||||||
alt="laptop charging"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<h5 className="mb-2">Project & Task Management</h5>
|
<h5 className="mb-2">Project & Task Management</h5>
|
||||||
<p className="features-icon-description">
|
<p className="features-icon-description">
|
||||||
@ -309,10 +305,7 @@ const LandingPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="col-lg-3 col-sm-4 text-center features-icon-box">
|
<div className="col-lg-3 col-sm-4 text-center features-icon-box">
|
||||||
<div className="text-center mb-4">
|
<div className="text-center mb-4">
|
||||||
<img
|
<img src="/img/icons/rocket.svg" alt="transition up" />
|
||||||
src="/img/icons/rocket.svg"
|
|
||||||
alt="transition up"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<h5 className="mb-2">Attendance & Leave Tracking</h5>
|
<h5 className="mb-2">Attendance & Leave Tracking</h5>
|
||||||
<p className="features-icon-description">
|
<p className="features-icon-description">
|
||||||
@ -332,10 +325,7 @@ const LandingPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="col-lg-3 col-sm-4 text-center features-icon-box">
|
<div className="col-lg-3 col-sm-4 text-center features-icon-box">
|
||||||
<div className="text-center mb-4">
|
<div className="text-center mb-4">
|
||||||
<img
|
<img src="/img/icons/check.svg" alt="3d select solid" />
|
||||||
src="/img/icons/check.svg"
|
|
||||||
alt="3d select solid"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<h5 className="mb-2">Expense & Budget Tracking</h5>
|
<h5 className="mb-2">Expense & Budget Tracking</h5>
|
||||||
<p className="features-icon-description">
|
<p className="features-icon-description">
|
||||||
@ -355,10 +345,7 @@ const LandingPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="col-lg-3 col-sm-4 text-center features-icon-box">
|
<div className="col-lg-3 col-sm-4 text-center features-icon-box">
|
||||||
<div className="text-center mb-4">
|
<div className="text-center mb-4">
|
||||||
<img
|
<img src="/img/icons/keyboard.svg" alt="keyboard" />
|
||||||
src="/img/icons/keyboard.svg"
|
|
||||||
alt="keyboard"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<h5 className="mb-2">Document Management</h5>
|
<h5 className="mb-2">Document Management</h5>
|
||||||
<p className="features-icon-description">
|
<p className="features-icon-description">
|
||||||
@ -368,10 +355,7 @@ const LandingPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="col-lg-3 col-sm-4 text-center features-icon-box">
|
<div className="col-lg-3 col-sm-4 text-center features-icon-box">
|
||||||
<div className="text-center mb-4">
|
<div className="text-center mb-4">
|
||||||
<img
|
<img src="/img/icons/keyboard.svg" alt="keyboard" />
|
||||||
src="/img/icons/keyboard.svg"
|
|
||||||
alt="keyboard"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<h5 className="mb-2">
|
<h5 className="mb-2">
|
||||||
Service Provider & Subcontractor Tracking
|
Service Provider & Subcontractor Tracking
|
||||||
@ -383,10 +367,7 @@ const LandingPage = () => {
|
|||||||
</div>{" "}
|
</div>{" "}
|
||||||
<div className="col-lg-3 col-sm-4 text-center features-icon-box">
|
<div className="col-lg-3 col-sm-4 text-center features-icon-box">
|
||||||
<div className="text-center mb-4">
|
<div className="text-center mb-4">
|
||||||
<img
|
<img src="/img/icons/inventory.svg" alt="keyboard" />
|
||||||
src="/img/icons/inventory.svg"
|
|
||||||
alt="keyboard"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<h5 className="mb-2">Inventory Management</h5>
|
<h5 className="mb-2">Inventory Management</h5>
|
||||||
<p className="features-icon-description">
|
<p className="features-icon-description">
|
||||||
@ -396,10 +377,7 @@ const LandingPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="col-lg-3 col-sm-4 text-center features-icon-box">
|
<div className="col-lg-3 col-sm-4 text-center features-icon-box">
|
||||||
<div className="text-center mb-4">
|
<div className="text-center mb-4">
|
||||||
<img
|
<img src="/img/icons/keyboard.svg" alt="keyboard" />
|
||||||
src="/img/icons/keyboard.svg"
|
|
||||||
alt="keyboard"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<h5 className="mb-2">Directory</h5>
|
<h5 className="mb-2">Directory</h5>
|
||||||
<p className="features-icon-description">
|
<p className="features-icon-description">
|
||||||
@ -411,10 +389,11 @@ const LandingPage = () => {
|
|||||||
</section>
|
</section>
|
||||||
{/* Useful features: End */}
|
{/* Useful features: End */}
|
||||||
|
|
||||||
{/* <!-- Real customers reviews: Start --> */}
|
{/* <!-- Real blog/ case studies: Start --> */}
|
||||||
<section
|
<section
|
||||||
id="landingReviews"
|
id="sectionBlog"
|
||||||
class="section-py bg-body landing-reviews pb-0"
|
class="section-py bg-body landing-reviews pb-0"
|
||||||
|
hidden
|
||||||
>
|
>
|
||||||
{/* <!-- What people say slider: Start --> */}
|
{/* <!-- What people say slider: Start --> */}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@ -518,341 +497,8 @@ const LandingPage = () => {
|
|||||||
No matter which plan you choose, you’ll get access to powerful
|
No matter which plan you choose, you’ll get access to powerful
|
||||||
features. <strong>Choose the best plan to fit your needs.</strong>
|
features. <strong>Choose the best plan to fit your needs.</strong>
|
||||||
</p>
|
</p>
|
||||||
<div className="text-center mb-8">
|
{/* <SubscriptionPlans/> */}
|
||||||
<div className="position-relative d-inline-block pt-3 pt-md-0">
|
<SubscriptionPlans />
|
||||||
<div class="btn-group" role="group" aria-label="Basic example">
|
|
||||||
<button type="button" class="btn btn-outline-secondary">
|
|
||||||
Basic
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-primary">
|
|
||||||
Team
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-outline-secondary">
|
|
||||||
Enterprise
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{/* <label className="switch switch-sm switch-primary me-0">
|
|
||||||
<span className="switch-label fs-6 text-body me-3">
|
|
||||||
Pay Monthly
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="switch-input price-duration-toggler"
|
|
||||||
checked
|
|
||||||
/>
|
|
||||||
<span className="switch-toggle-slider">
|
|
||||||
<span className="switch-on"></span>
|
|
||||||
<span className="switch-off"></span>
|
|
||||||
</span>
|
|
||||||
<span className="switch-label fs-6 text-body ms-3">
|
|
||||||
Pay Annual
|
|
||||||
</span>
|
|
||||||
</label> */}
|
|
||||||
{/* <div className="pricing-plans-item position-absolute d-flex">
|
|
||||||
<img
|
|
||||||
src="./../../public/img/icons/pricing-plans-arrow.png"
|
|
||||||
alt="pricing plans arrow"
|
|
||||||
className="scaleX-n1-rtl"
|
|
||||||
/>
|
|
||||||
<span className="fw-medium mt-2 ms-1"> Save 25%</span>
|
|
||||||
</div> */}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="row g-6 pt-0">
|
|
||||||
{/* Basic Plan: Start */}
|
|
||||||
<div className="col-xl-4 col-lg-6 col-md-4">
|
|
||||||
<div className="card">
|
|
||||||
<div className="card-header">
|
|
||||||
<div className="text-center">
|
|
||||||
<img
|
|
||||||
src="/img/icons/paper-airplane.png"
|
|
||||||
alt="paper airplane icon"
|
|
||||||
className="mb-8 pb-2"
|
|
||||||
/>
|
|
||||||
<h4 className="mb-0">Basic</h4>
|
|
||||||
<div className="d-flex align-items-center justify-content-center">
|
|
||||||
<span className="price-monthly h2 text-primary fw-extrabold mb-0">
|
|
||||||
$19
|
|
||||||
</span>
|
|
||||||
<span className="price-yearly h2 text-primary fw-extrabold mb-0 d-none">
|
|
||||||
$14
|
|
||||||
</span>
|
|
||||||
<sub className="h6 text-muted mb-n1 ms-1">/mo</sub>
|
|
||||||
</div>
|
|
||||||
<div className="position-relative pt-2">
|
|
||||||
<div className="price-yearly text-muted price-yearly-toggle d-none">
|
|
||||||
$ 168 / year
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="card-body">
|
|
||||||
<ul className="text start list-unstyled pricing-list">
|
|
||||||
<li>
|
|
||||||
<h6 className="d-flex align-items-center mb-3">
|
|
||||||
<span className="badge badge-center rounded-pill bg-label-primary p-0 me-3">
|
|
||||||
<i className="bx bx-check bx-12px"></i>
|
|
||||||
</span>
|
|
||||||
Timeline
|
|
||||||
</h6>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<h6 className="d-flex align-items-center mb-3">
|
|
||||||
<span className="badge badge-center rounded-pill bg-label-primary p-0 me-3">
|
|
||||||
<i className="bx bx-check bx-12px"></i>
|
|
||||||
</span>
|
|
||||||
Basic search
|
|
||||||
</h6>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<h6 className="d-flex align-items-center mb-3">
|
|
||||||
<span className="badge badge-center rounded-pill bg-label-primary p-0 me-3">
|
|
||||||
<i className="bx bx-check bx-12px"></i>
|
|
||||||
</span>
|
|
||||||
Live chat widget
|
|
||||||
</h6>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<h6 className="d-flex align-items-center mb-3">
|
|
||||||
<span className="badge badge-center rounded-pill bg-label-primary p-0 me-3">
|
|
||||||
<i className="bx bx-check bx-12px"></i>
|
|
||||||
</span>
|
|
||||||
Email marketing
|
|
||||||
</h6>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<h6 className="d-flex align-items-center mb-3">
|
|
||||||
<span className="badge badge-center rounded-pill bg-label-primary p-0 me-3">
|
|
||||||
<i className="bx bx-check bx-12px"></i>
|
|
||||||
</span>
|
|
||||||
Custom Forms
|
|
||||||
</h6>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<h6 className="d-flex align-items-center mb-3">
|
|
||||||
<span className="badge badge-center rounded-pill bg-label-primary p-0 me-3">
|
|
||||||
<i className="bx bx-check bx-12px"></i>
|
|
||||||
</span>
|
|
||||||
Traffic analytics
|
|
||||||
</h6>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<h6 className="d-flex align-items-center mb-3">
|
|
||||||
<span className="badge badge-center rounded-pill bg-label-primary p-0 me-3">
|
|
||||||
<i className="bx bx-check bx-12px"></i>
|
|
||||||
</span>
|
|
||||||
Basic Support
|
|
||||||
</h6>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<div className="d-grid mt-8">
|
|
||||||
<a
|
|
||||||
href="#landingPricing"
|
|
||||||
className="btn btn-label-primary"
|
|
||||||
>
|
|
||||||
Get Started
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* Basic Plan: End */}
|
|
||||||
|
|
||||||
{/* Favourite Plan: Start */}
|
|
||||||
<div className="col-xl-4 col-lg-6 col-md-4">
|
|
||||||
<div className="card border border-primary shadow-xl">
|
|
||||||
<div className="card-header">
|
|
||||||
<div className="text-center">
|
|
||||||
<img
|
|
||||||
src="/img/icons/plane.png"
|
|
||||||
alt="plane icon"
|
|
||||||
className="mb-8 pb-2"
|
|
||||||
/>
|
|
||||||
<h4 className="mb-0">Team</h4>
|
|
||||||
<div className="d-flex align-items-center justify-content-center">
|
|
||||||
<span className="price-monthly h2 text-primary fw-extrabold mb-0">
|
|
||||||
$29
|
|
||||||
</span>
|
|
||||||
<span className="price-yearly h2 text-primary fw-extrabold mb-0 d-none">
|
|
||||||
$22
|
|
||||||
</span>
|
|
||||||
<sub className="h6 text-muted mb-n1 ms-1">/mo</sub>
|
|
||||||
</div>
|
|
||||||
<div className="position-relative pt-2">
|
|
||||||
<div className="price-yearly text-muted price-yearly-toggle d-none">
|
|
||||||
$ 264 / year
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="card-body">
|
|
||||||
<ul className="text start list-unstyled pricing-list">
|
|
||||||
<li>
|
|
||||||
<h6 className="d-flex align-items-center mb-3">
|
|
||||||
<span className="badge badge-center rounded-pill bg-primary p-0 me-3">
|
|
||||||
<i className="bx bx-check bx-12px"></i>
|
|
||||||
</span>
|
|
||||||
Everything in basic
|
|
||||||
</h6>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<h6 className="d-flex align-items-center mb-3">
|
|
||||||
<span className="badge badge-center rounded-pill bg-primary p-0 me-3">
|
|
||||||
<i className="bx bx-check bx-12px"></i>
|
|
||||||
</span>
|
|
||||||
Timeline with database
|
|
||||||
</h6>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<h6 className="d-flex align-items-center mb-3">
|
|
||||||
<span className="badge badge-center rounded-pill bg-primary p-0 me-3">
|
|
||||||
<i className="bx bx-check bx-12px"></i>
|
|
||||||
</span>
|
|
||||||
Advanced search
|
|
||||||
</h6>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<h6 className="d-flex align-items-center mb-3">
|
|
||||||
<span className="badge badge-center rounded-pill bg-primary p-0 me-3">
|
|
||||||
<i className="bx bx-check bx-12px"></i>
|
|
||||||
</span>
|
|
||||||
Marketing automation
|
|
||||||
</h6>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<h6 className="d-flex align-items-center mb-3">
|
|
||||||
<span className="badge badge-center rounded-pill bg-primary p-0 me-3">
|
|
||||||
<i className="bx bx-check bx-12px"></i>
|
|
||||||
</span>
|
|
||||||
Advanced chatbot
|
|
||||||
</h6>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<h6 className="d-flex align-items-center mb-3">
|
|
||||||
<span className="badge badge-center rounded-pill bg-primary p-0 me-3">
|
|
||||||
<i className="bx bx-check bx-12px"></i>
|
|
||||||
</span>
|
|
||||||
Campaign management
|
|
||||||
</h6>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<h6 className="d-flex align-items-center mb-3">
|
|
||||||
<span className="badge badge-center rounded-pill bg-primary p-0 me-3">
|
|
||||||
<i className="bx bx-check bx-12px"></i>
|
|
||||||
</span>
|
|
||||||
Collaboration tools
|
|
||||||
</h6>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<div className="d-grid mt-8">
|
|
||||||
<a href="payment-page.html" className="btn btn-primary">
|
|
||||||
Get Started
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* Favourite Plan: End */}
|
|
||||||
|
|
||||||
{/* Standard Plan: Start */}
|
|
||||||
<div className="col-xl-4 col-lg-6 col-md-4">
|
|
||||||
<div className="card">
|
|
||||||
<div className="card-header">
|
|
||||||
<div className="text-center">
|
|
||||||
<img
|
|
||||||
src="/img/icons/shuttle-rocket.png"
|
|
||||||
alt="shuttle rocket icon"
|
|
||||||
className="mb-8 pb-2"
|
|
||||||
/>
|
|
||||||
<h4 className="mb-0">Enterprise</h4>
|
|
||||||
<div className="d-flex align-items-center justify-content-center">
|
|
||||||
<span className="price-monthly h2 text-primary fw-extrabold mb-0">
|
|
||||||
$49
|
|
||||||
</span>
|
|
||||||
<span className="price-yearly h2 text-primary fw-extrabold mb-0 d-none">
|
|
||||||
$37
|
|
||||||
</span>
|
|
||||||
<sub className="h6 text-muted mb-n1 ms-1">/mo</sub>
|
|
||||||
</div>
|
|
||||||
<div className="position-relative pt-2">
|
|
||||||
<div className="price-yearly text-muted price-yearly-toggle d-none">
|
|
||||||
$ 444 / year
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="card-body">
|
|
||||||
<ul className="text start list-unstyled pricing-list">
|
|
||||||
<li>
|
|
||||||
<h6 className="d-flex align-items-center mb-3">
|
|
||||||
<span className="badge badge-center rounded-pill bg-label-primary p-0 me-3">
|
|
||||||
<i className="bx bx-check bx-12px"></i>
|
|
||||||
</span>
|
|
||||||
Everything in premium
|
|
||||||
</h6>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<h6 className="d-flex align-items-center mb-3">
|
|
||||||
<span className="badge badge-center rounded-pill bg-label-primary p-0 me-3">
|
|
||||||
<i className="bx bx-check bx-12px"></i>
|
|
||||||
</span>
|
|
||||||
Timeline with database
|
|
||||||
</h6>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<h6 className="d-flex align-items-center mb-3">
|
|
||||||
<span className="badge badge-center rounded-pill bg-label-primary p-0 me-3">
|
|
||||||
<i className="bx bx-check bx-12px"></i>
|
|
||||||
</span>
|
|
||||||
Fuzzy search
|
|
||||||
</h6>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<h6 className="d-flex align-items-center mb-3">
|
|
||||||
<span className="badge badge-center rounded-pill bg-label-primary p-0 me-3">
|
|
||||||
<i className="bx bx-check bx-12px"></i>
|
|
||||||
</span>
|
|
||||||
A/B testing sanbox
|
|
||||||
</h6>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<h6 className="d-flex align-items-center mb-3">
|
|
||||||
<span className="badge badge-center rounded-pill bg-label-primary p-0 me-3">
|
|
||||||
<i className="bx bx-check bx-12px"></i>
|
|
||||||
</span>
|
|
||||||
Custom permissions
|
|
||||||
</h6>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<h6 className="d-flex align-items-center mb-3">
|
|
||||||
<span className="badge badge-center rounded-pill bg-label-primary p-0 me-3">
|
|
||||||
<i className="bx bx-check bx-12px"></i>
|
|
||||||
</span>
|
|
||||||
Social media automation
|
|
||||||
</h6>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<h6 className="d-flex align-items-center mb-3">
|
|
||||||
<span className="badge badge-center rounded-pill bg-label-primary p-0 me-3">
|
|
||||||
<i className="bx bx-check bx-12px"></i>
|
|
||||||
</span>
|
|
||||||
Sales automation tools
|
|
||||||
</h6>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<div className="d-grid mt-8">
|
|
||||||
<a
|
|
||||||
href="payment-page.html"
|
|
||||||
className="btn btn-label-primary"
|
|
||||||
>
|
|
||||||
Get Started
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* Standard Plan: End */}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{/* Pricing plans: End */}
|
{/* Pricing plans: End */}
|
||||||
@ -945,14 +591,14 @@ const LandingPage = () => {
|
|||||||
<span className="badge bg-label-primary heading">FAQ</span>
|
<span className="badge bg-label-primary heading">FAQ</span>
|
||||||
</div>
|
</div>
|
||||||
<h4 className="text-center mb-1">
|
<h4 className="text-center mb-1">
|
||||||
Frequently asked
|
Frequently Asked
|
||||||
<span className="position-relative fw-extrabold z-1">
|
<span className="position-relative fw-extrabold z-1 ms-2">
|
||||||
questions
|
Questions
|
||||||
<img
|
{/* <img
|
||||||
src="/img/icons/section-title-icon.png"
|
src="/img/icons/section-title-icon.png"
|
||||||
alt="laptop charging"
|
alt="laptop charging"
|
||||||
className="section-title-img position-absolute object-fit-contain bottom-0 z-n1"
|
className="section-title-img position-absolute object-fit-contain bottom-0 z-n1"
|
||||||
/>
|
/> */}
|
||||||
</span>
|
</span>
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-center mb-12 pb-md-4">
|
<p className="text-center mb-12 pb-md-4">
|
||||||
@ -990,12 +636,12 @@ const LandingPage = () => {
|
|||||||
className="accordion-collapse collapse"
|
className="accordion-collapse collapse"
|
||||||
data-bs-parent="#accordionExample"
|
data-bs-parent="#accordionExample"
|
||||||
>
|
>
|
||||||
<div className="accordion-body">
|
<div className="accordion-body text-start">
|
||||||
Lemon drops chocolate cake gummies carrot cake chupa
|
A smart Project Management System designed to bring
|
||||||
chups muffin topping. Sesame snaps icing marzipan gummi
|
teams, tasks, and timelines together in one place. With
|
||||||
bears macaroon dragée danish caramels powder. Bear claw
|
AI-driven insights, role-based access, and seamless
|
||||||
dragée pastry topping soufflé. Wafer gummi bears
|
reporting, it empowers organizations to deliver projects
|
||||||
marshmallow pastry pie.
|
faster and smarter.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1018,12 +664,13 @@ const LandingPage = () => {
|
|||||||
aria-labelledby="headingTwo"
|
aria-labelledby="headingTwo"
|
||||||
data-bs-parent="#accordionExample"
|
data-bs-parent="#accordionExample"
|
||||||
>
|
>
|
||||||
<div className="accordion-body">
|
<div className="accordion-body text-start">
|
||||||
Dessert ice cream donut oat cake jelly-o pie sugar plum
|
Yes, you have full flexibility to manage your
|
||||||
cheesecake. Bear claw dragée oat cake dragée ice cream
|
subscription. You can upgrade to a higher plan to unlock
|
||||||
halvah tootsie roll. Danish cake oat cake pie macaroon
|
more features, downgrade to a smaller plan if your needs
|
||||||
tart donut gummies. Jelly beans candy canes carrot cake.
|
change, or cancel your subscription anytime. Plan
|
||||||
Fruitcake chocolate chupa chups.
|
changes take effect instantly, and billing adjustments
|
||||||
|
are applied on a pro-rated basis.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1046,17 +693,16 @@ const LandingPage = () => {
|
|||||||
aria-labelledby="headingThree"
|
aria-labelledby="headingThree"
|
||||||
data-bs-parent="#accordionExample"
|
data-bs-parent="#accordionExample"
|
||||||
>
|
>
|
||||||
<div className="accordion-body">
|
<div className="accordion-body text-start">
|
||||||
Regular license can be used for end products that do not
|
Security is at the core of Marco PMS. We use
|
||||||
charge users for access or service(access is free and
|
industry-standard encryption (SSL/TLS) to protect data
|
||||||
there will be no monthly subscription fee). Single
|
in transit and advanced encryption to safeguard data at
|
||||||
regular license can be used for single end product and
|
rest. Role-based access controls ensure that only
|
||||||
end product can be used by you or your client. If you
|
authorized users can access sensitive information. Our
|
||||||
want to sell end product to multiple clients then you
|
system is hosted on secure, cloud-ready infrastructure
|
||||||
will need to purchase separate license for each client.
|
with regular backups, monitoring, and compliance with
|
||||||
The same rule applies if you want to use the same end
|
best practices to keep your data safe and available at
|
||||||
product on multiple domains(unique setup). For more info
|
all times.
|
||||||
on regular license you can check official description.
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1079,12 +725,12 @@ const LandingPage = () => {
|
|||||||
aria-labelledby="headingFour"
|
aria-labelledby="headingFour"
|
||||||
data-bs-parent="#accordionExample"
|
data-bs-parent="#accordionExample"
|
||||||
>
|
>
|
||||||
<div className="accordion-body">
|
<div className="accordion-body text-start">
|
||||||
Lorem ipsum dolor sit amet consectetur adipisicing elit.
|
You can reach our support team anytime through the
|
||||||
Nobis et aliquid quaerat possimus maxime! Mollitia
|
in-app help center, email, or live chat. We also provide
|
||||||
reprehenderit neque repellat deleniti delectus
|
a detailed knowledge base and FAQs to guide you through
|
||||||
architecto dolorum maxime, blanditiis earum ea, incidunt
|
common queries. For personalized assistance, our support
|
||||||
quam possimus cumque.
|
specialists are always ready to help you.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1107,15 +753,47 @@ const LandingPage = () => {
|
|||||||
aria-labelledby="headingFive"
|
aria-labelledby="headingFive"
|
||||||
data-bs-parent="#accordionExample"
|
data-bs-parent="#accordionExample"
|
||||||
>
|
>
|
||||||
<div className="accordion-body">
|
<div className="accordion-body text-start">
|
||||||
Lorem ipsum dolor sit amet consectetur, adipisicing
|
Marco PMS operate under a proprietary license combined
|
||||||
elit. Sequi molestias exercitationem ab cum nemo facere
|
with a subscription model. This means customers don’t
|
||||||
voluptates veritatis quia, eveniet veniam at et
|
own the software but are granted the right to access and
|
||||||
repudiandae mollitia ipsam quasi labore enim architecto
|
use it through the cloud under our Terms of Service.
|
||||||
non!
|
Depending on the plan, licensing may be based on users,
|
||||||
|
features, or usage, and you can upgrade, downgrade, or
|
||||||
|
cancel at any time. non!
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="card accordion-item">
|
||||||
|
<h2 className="accordion-header" id="headingSix">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="accordion-button collapsed"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#accordionSix"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-controls="accordionSix"
|
||||||
|
>
|
||||||
|
Can I customize Marco PMS for my business needs?
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div
|
||||||
|
id="accordionSix"
|
||||||
|
className="accordion-collapse collapse"
|
||||||
|
aria-labelledby="headingSix"
|
||||||
|
data-bs-parent="#accordionExample"
|
||||||
|
>
|
||||||
|
<div className="accordion-body text-start">
|
||||||
|
Yes, Marco PMS is designed to be flexible and adaptable.
|
||||||
|
You can customize workflows, user roles, permissions,
|
||||||
|
and reporting to match your organization’s unique
|
||||||
|
processes. Depending on your plan, we also support
|
||||||
|
advanced customization such as integrating with
|
||||||
|
third-party tools, adding custom fields, and tailoring
|
||||||
|
modules to fit your business requirements.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>{" "}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1138,19 +816,26 @@ const LandingPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="row align-items-center gy-12 mb-12">
|
<div className="row align-items-center gy-12 mb-12">
|
||||||
|
<div className="col-lg-6 pt-lg-12 text-center text-lg-start">
|
||||||
|
<img
|
||||||
|
style={{ width: "80%" }}
|
||||||
|
src="/img/images/contact-customer-service.png"
|
||||||
|
alt="hero elements"
|
||||||
|
></img>
|
||||||
|
</div>
|
||||||
<div className="col-lg-6 text-start text-sm-center text-lg-start">
|
<div className="col-lg-6 text-start text-sm-center text-lg-start">
|
||||||
<div className="mt-5">
|
<div className="mt-5">
|
||||||
{" "}
|
{" "}
|
||||||
<h4 className="text-start mb-1">
|
<h4 className="text-start mb-1">
|
||||||
<span className="position-relative fw-extrabold z-1">
|
<span className="position-relative fw-extrabold z-1">
|
||||||
Let's work
|
Let's Work
|
||||||
<img
|
{/* <img
|
||||||
src="/img/icons/section-title-icon.png"
|
src="/img/icons/section-title-icon.png"
|
||||||
alt="laptop charging"
|
alt="laptop charging"
|
||||||
className="section-title-img position-absolute object-fit-contain bottom-0 z-n1"
|
className="section-title-img position-absolute object-fit-contain bottom-0 z-n1"
|
||||||
/>
|
/> */}
|
||||||
</span>
|
</span>
|
||||||
together
|
Together
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-start pb-md-4">
|
<p className="text-start pb-md-4">
|
||||||
Any question or remark? just write us a message
|
Any question or remark? just write us a message
|
||||||
@ -1186,7 +871,7 @@ const LandingPage = () => {
|
|||||||
href="tel:+1234-568-963"
|
href="tel:+1234-568-963"
|
||||||
className="text-heading"
|
className="text-heading"
|
||||||
>
|
>
|
||||||
+1234 568 963
|
+91 70288 83755
|
||||||
</a>
|
</a>
|
||||||
</h6>
|
</h6>
|
||||||
</div>
|
</div>
|
||||||
@ -1195,15 +880,15 @@ const LandingPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-10">
|
<div className="mt-10">
|
||||||
<h4 className="cta-title text-primary mb-1">
|
<h5 className="cta-title text-primary mb-1">
|
||||||
Ready to Get Started?
|
Ready to Get Started?
|
||||||
</h4>
|
|
||||||
<h5 className="text-body mb-8">
|
|
||||||
Start your project with a 14-day free trial
|
|
||||||
</h5>
|
</h5>
|
||||||
<a href="#landingPricing" className="btn btn-lg btn-primary">
|
<h5 className="text-body mb-8">
|
||||||
|
Start your project with a free trial
|
||||||
|
</h5>
|
||||||
|
{/* <a href="#landingPricing" className="btn btn-lg btn-primary">
|
||||||
Get Started
|
Get Started
|
||||||
</a>{" "}
|
</a>{" "} */}
|
||||||
<a
|
<a
|
||||||
href="/auth/reqest/demo"
|
href="/auth/reqest/demo"
|
||||||
className="btn btn-lg btn-primary"
|
className="btn btn-lg btn-primary"
|
||||||
@ -1212,13 +897,6 @@ const LandingPage = () => {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-lg-6 pt-lg-12 text-center text-lg-end">
|
|
||||||
<img
|
|
||||||
style={{ width: "80%" }}
|
|
||||||
src="/img/images/contact-customer-service.png"
|
|
||||||
alt="hero elements"
|
|
||||||
></img>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -1378,7 +1056,10 @@ const LandingPage = () => {
|
|||||||
|
|
||||||
{/* Footer: Start */}
|
{/* Footer: Start */}
|
||||||
<footer className="landing-footer bg-body footer-text">
|
<footer className="landing-footer bg-body footer-text">
|
||||||
<div className="footer-top position-relative overflow-hidden z-1">
|
<div
|
||||||
|
className="footer-top position-relative overflow-hidden z-1"
|
||||||
|
hidden
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
src="/img/backgrounds/footer-bg.png"
|
src="/img/backgrounds/footer-bg.png"
|
||||||
alt="footer bg"
|
alt="footer bg"
|
||||||
@ -1515,11 +1196,8 @@ const LandingPage = () => {
|
|||||||
|
|
||||||
<div className="col-lg-6 col-md-6 d-flex gap-3 align-items-center justify-content-end">
|
<div className="col-lg-6 col-md-6 d-flex gap-3 align-items-center justify-content-end">
|
||||||
<h6 className="footer-title mt-3">Download our app</h6>
|
<h6 className="footer-title mt-3">Download our app</h6>
|
||||||
<a href="javascript:void(0);">
|
<a href="javascript:void(0);" hidden>
|
||||||
<img
|
<img src="/img/icons/apple-icon.png" alt="apple icon" />
|
||||||
src="/img/icons/apple-icon.png"
|
|
||||||
alt="apple icon"
|
|
||||||
/>
|
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="https://play.google.com/store/apps/details?id=com.marco.aiotstage&pcampaignid=web_share"
|
href="https://play.google.com/store/apps/details?id=com.marco.aiotstage&pcampaignid=web_share"
|
||||||
@ -1534,9 +1212,28 @@ const LandingPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="footer-bottom py-3 py-md-5">
|
<div className="footer-bottom py-md-4">
|
||||||
<div className="container d-flex flex-wrap justify-content-between flex-md-row flex-column text-center text-md-start">
|
<div className="container d-flex flex-wrap justify-content-between flex-md-row flex-column text-center text-md-start">
|
||||||
<div className="mb-2 mb-md-0">
|
<div className="col-lg-4 col-md-4 d-flex align-items-center justify-content-start">
|
||||||
|
<a
|
||||||
|
href="https://www.facebook.com/marcoaiot/"
|
||||||
|
className="me-4"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<img src="/img/icons/facebook.svg" alt="facebook icon" />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://twitter.com/marcoaiot"
|
||||||
|
className="me-4"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<img src="/img/icons/twitter.svg" alt="twitter icon" />
|
||||||
|
</a>
|
||||||
|
<a href="https://www.instagram.com/marcoaiot/" target="_blank">
|
||||||
|
<img src="/img/icons/instagram.svg" alt="google icon" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="col-lg-4 col-md-4 mb-2 mb-md-0 d-flex gap-3 align-items-center justify-content-center">
|
||||||
<span className="footer-bottom-text me-1">
|
<span className="footer-bottom-text me-1">
|
||||||
©{new Date().getFullYear()}
|
©{new Date().getFullYear()}
|
||||||
</span>
|
</span>
|
||||||
@ -1548,31 +1245,19 @@ const LandingPage = () => {
|
|||||||
Marco AIoT Technologies Pvt. Ltd.,
|
Marco AIoT Technologies Pvt. Ltd.,
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
|
<div className="col-lg-4 col-md-4 d-flex gap-3 align-items-center justify-content-end">
|
||||||
|
<h6 className="footer-title mt-3">Download our app</h6>
|
||||||
|
<a href="javascript:void(0);" hidden>
|
||||||
|
<img src="/img/icons/apple-icon.png" alt="apple icon" />
|
||||||
|
</a>
|
||||||
<a
|
<a
|
||||||
href="https://www.facebook.com/marcoaiot/"
|
href="https://play.google.com/store/apps/details?id=com.marco.aiotstage&pcampaignid=web_share"
|
||||||
className="me-4"
|
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src="/img/icons/facebook.svg"
|
src="/img/icons/google-play-icon.png"
|
||||||
alt="facebook icon"
|
alt="google play icon"
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="https://twitter.com/marcoaiot"
|
|
||||||
className="me-4"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src="/img/icons/twitter.svg"
|
|
||||||
alt="twitter icon"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
<a href="https://www.instagram.com/marcoaiot/" target="_blank">
|
|
||||||
<img
|
|
||||||
src="/img/icons/instagram.svg"
|
|
||||||
alt="google icon"
|
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
44
src/pages/Home/PlanCardSkeleton.jsx
Normal file
44
src/pages/Home/PlanCardSkeleton.jsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const SubscriptionPlanSkeleton = () => {
|
||||||
|
return (
|
||||||
|
<div className="col-xl-4 col-lg-6 col-md-6">
|
||||||
|
<div className="card h-100 shadow-sm border-0 p-3 text-center">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="mb-3">
|
||||||
|
<div className="bg-light rounded-circle mx-auto mb-3" style={{ width: "50px", height: "50px" }}></div>
|
||||||
|
<div className="bg-light rounded w-75 mx-auto mb-2" style={{ height: "20px" }}></div>
|
||||||
|
<div className="bg-light rounded w-50 mx-auto" style={{ height: "16px" }}></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Price */}
|
||||||
|
<div className="mb-3">
|
||||||
|
<div className="bg-light rounded w-50 mx-auto" style={{ height: "24px" }}></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Storage & Trial */}
|
||||||
|
<div className="d-flex justify-content-center gap-4 mb-5">
|
||||||
|
<div className="bg-light rounded" style={{ width: "100px", height: "16px" }}></div>
|
||||||
|
<div className="bg-light rounded" style={{ width: "100px", height: "16px" }}></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Features */}
|
||||||
|
<h6 className="fw-bold text-uppercase border-top pt-3 mb-3 text-center">
|
||||||
|
Features
|
||||||
|
</h6>
|
||||||
|
<ul className="list-unstyled text-start mb-4 ms-7">
|
||||||
|
{[1, 2, 3].map((i) => (
|
||||||
|
<li key={i} className="mb-3">
|
||||||
|
<div className="bg-light rounded" style={{ width: "70%", height: "16px" }}></div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{/* Button */}
|
||||||
|
<div className="bg-light rounded w-100" style={{ height: "40px" }}></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SubscriptionPlanSkeleton;
|
||||||
141
src/pages/Home/SubscriptionPlans.jsx
Normal file
141
src/pages/Home/SubscriptionPlans.jsx
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import axios from "axios";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import PlanCardSkeleton from "./PlanCardSkeleton";
|
||||||
|
|
||||||
|
const SubscriptionPlans = () => {
|
||||||
|
const [plans, setPlans] = useState([]);
|
||||||
|
const [frequency, setFrequency] = useState(1);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchPlans = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const res = await axios.get(
|
||||||
|
`http://localhost:5032/api/market/list/subscription-plan?frequency=${frequency}`,
|
||||||
|
{ headers: { "Content-Type": "application/json" } }
|
||||||
|
);
|
||||||
|
setPlans(res.data?.data || []);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error fetching plans:", err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchPlans();
|
||||||
|
}, [frequency]);
|
||||||
|
|
||||||
|
const frequencyLabel = (freq) => {
|
||||||
|
switch (freq) {
|
||||||
|
case 0: return "1 mo";
|
||||||
|
case 1: return "3 mo";
|
||||||
|
case 2: return "6 mo";
|
||||||
|
case 3: return "1 yr";
|
||||||
|
default: return "mo";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container py-5">
|
||||||
|
{/* Frequency Switcher */}
|
||||||
|
<div className="text-center mb-4">
|
||||||
|
<div className="btn-group" role="group" aria-label="Plan frequency">
|
||||||
|
{["Monthly", "Quarterly", "Half-Yearly", "Yearly"].map((label, idx) => (
|
||||||
|
<button
|
||||||
|
key={idx}
|
||||||
|
type="button"
|
||||||
|
className={`btn btn-${frequency === idx ? "primary" : "outline-secondary"}`}
|
||||||
|
onClick={() => setFrequency(idx)}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Cards */}
|
||||||
|
<div className="row g-4 mt-10">
|
||||||
|
{loading ? (
|
||||||
|
// Show 3 skeletons
|
||||||
|
<>
|
||||||
|
<PlanCardSkeleton />
|
||||||
|
<PlanCardSkeleton />
|
||||||
|
<PlanCardSkeleton />
|
||||||
|
</>
|
||||||
|
) : plans.length === 0 ? (
|
||||||
|
<div className="text-center">No plans found</div>
|
||||||
|
) : (
|
||||||
|
plans.map((plan) => (
|
||||||
|
<div key={plan.id} className="col-xl-4 col-lg-6 col-md-6">
|
||||||
|
<div className="card h-100 shadow-lg border-0 p-3 text-center p-10">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="mb-3">
|
||||||
|
<i className="bx bxs-package text-primary fs-1 mb-2"></i>
|
||||||
|
<p className="card-title fs-3 fw-bold mb-1">{plan.planName}</p>
|
||||||
|
<p className="text-muted mb-0 fs-5">{plan.description}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Price */}
|
||||||
|
<div className="mb-3">
|
||||||
|
<h4 className="fw-semibold mt-auto mb-0 fs-3">
|
||||||
|
{plan.currency?.symbol} {plan.price}
|
||||||
|
<small className="text-muted ms-1">/ {frequencyLabel(frequency)}</small>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Storage & Trial */}
|
||||||
|
<div className="text-muted mb-5 d-flex justify-content-center gap-4">
|
||||||
|
<div>
|
||||||
|
<i className="fa-solid fa-hdd me-2"></i>
|
||||||
|
Storage {plan.maxStorage} MB
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<i className="fa-regular fa-calendar-check text-success me-2"></i>
|
||||||
|
Trial Days {plan.trialDays}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Features */}
|
||||||
|
<h6 className="fw-bold text-uppercase border-top pt-3 mb-3 text-center">
|
||||||
|
Features
|
||||||
|
</h6>
|
||||||
|
<ul className="list-unstyled text-start mb-4 ms-7 fs-5">
|
||||||
|
{plan.features?.modules &&
|
||||||
|
Object.values(plan.features.modules).map((mod) =>
|
||||||
|
mod && mod.name ? (
|
||||||
|
<li
|
||||||
|
key={mod.id}
|
||||||
|
className="d-flex align-items-center mb-4"
|
||||||
|
>
|
||||||
|
{mod.enabled ? (
|
||||||
|
<i className="fa-regular fa-circle-check text-success me-2"></i>
|
||||||
|
) : (
|
||||||
|
<i className="fa-regular fa-circle-xmark text-danger me-2"></i>
|
||||||
|
)}
|
||||||
|
{mod.name}
|
||||||
|
</li>
|
||||||
|
) : null
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{/* Button */}
|
||||||
|
<div className="mt-auto">
|
||||||
|
<Link
|
||||||
|
to="/auth/reqest/demo"
|
||||||
|
className="btn btn-outline-primary w-100 fw-bold"
|
||||||
|
>
|
||||||
|
Request a Demo
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SubscriptionPlans;
|
||||||
@ -121,66 +121,67 @@ const TenantPage = () => {
|
|||||||
{ label: "Tenant", link: null },
|
{ label: "Tenant", link: null },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
<div className="card text-center my-4 p-5 pb-10">
|
||||||
|
{/* Super Tenant Actions */}
|
||||||
|
{isSuperTenant && (
|
||||||
|
<div className="p-0">
|
||||||
|
<div className="row align-items-center">
|
||||||
|
{/* Search */}
|
||||||
|
<div className="col-6 col-md-6 col-lg-3 mb-md-0">
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
value={searchText}
|
||||||
|
onChange={(e) => setSearchText(e.target.value)}
|
||||||
|
className="form-control form-control"
|
||||||
|
placeholder="Search Tenant"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Super Tenant Actions */}
|
{/* Actions */}
|
||||||
{isSuperTenant && (
|
<div className="col-6 col-md-6 col-lg-9 text-end">
|
||||||
<div className="card d-flex p-2">
|
<span
|
||||||
<div className="row align-items-center">
|
className="text-tiny text-muted p-1 border-0 bg-none lead mx-3 cursor-pointer"
|
||||||
{/* Search */}
|
disabled={isRefetching}
|
||||||
<div className="col-6 col-md-6 col-lg-3 mb-md-0">
|
onClick={() => refetchFn && refetchFn()}
|
||||||
<input
|
>
|
||||||
type="search"
|
Refresh{" "}
|
||||||
value={searchText}
|
<i
|
||||||
onChange={(e) => setSearchText(e.target.value)}
|
className={`bx bx-refresh ms-1 ${
|
||||||
className="form-control form-control-sm"
|
isRefetching ? "bx-spin" : ""
|
||||||
placeholder="Search Tenant"
|
}`}
|
||||||
/>
|
></i>
|
||||||
</div>
|
</span>
|
||||||
|
|
||||||
{/* Actions */}
|
<button
|
||||||
<div className="col-6 col-md-6 col-lg-9 text-end">
|
type="button"
|
||||||
<span
|
title="Add New Tenant"
|
||||||
className="text-tiny text-muted p-1 border-0 bg-none lead mx-3 cursor-pointer"
|
className="p-1 bg-primary rounded-circle cursor-pointer"
|
||||||
disabled={isRefetching}
|
onClick={handleNewTenant}
|
||||||
onClick={() => refetchFn && refetchFn()}
|
>
|
||||||
>
|
<i className="bx bx-plus fs-4 text-white"></i>
|
||||||
Refresh{" "}
|
</button>
|
||||||
<i
|
</div>
|
||||||
className={`bx bx-refresh ms-1 ${
|
|
||||||
isRefetching ? "bx-spin" : ""
|
|
||||||
}`}
|
|
||||||
></i>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
title="Add New Tenant"
|
|
||||||
className="p-1 bg-primary rounded-circle cursor-pointer"
|
|
||||||
onClick={handleNewTenant}
|
|
||||||
>
|
|
||||||
<i className="bx bx-plus fs-4 text-white"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Tenant List or Access Denied */}
|
{/* Tenant List or Access Denied */}
|
||||||
{isSuperTenant ? (
|
{isSuperTenant ? (
|
||||||
<TenantsList
|
<TenantsList
|
||||||
filters={filters}
|
filters={filters}
|
||||||
searchText={debouncedSearch}
|
searchText={debouncedSearch}
|
||||||
setIsRefetching={setIsRefetching}
|
setIsRefetching={setIsRefetching}
|
||||||
setRefetchFn={setRefetchFn}
|
setRefetchFn={setRefetchFn}
|
||||||
/>
|
/>
|
||||||
) : !isSelfTenant ? (
|
) : !isSelfTenant ? (
|
||||||
<div className="card text-center my-4 p-2">
|
<div className="text-center my-4 p-2">
|
||||||
<i className="fa-solid fa-triangle-exclamation fs-5"></i>
|
<i className="fa-solid fa-triangle-exclamation fs-5"></i>
|
||||||
<p>
|
<p>
|
||||||
Access Denied: You don't have permission to perform this action!
|
Access Denied: You don't have permission to perform this action!
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TenantContext.Provider>
|
</TenantContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -136,7 +136,7 @@ const LoginPage = () => {
|
|||||||
)}
|
)}
|
||||||
</div> */}
|
</div> */}
|
||||||
|
|
||||||
<div className="mb-3 form-password-toggle text-start">
|
<div className="mb-3 form-password-toggle text-start">
|
||||||
<label htmlFor="password" className="form-label">
|
<label htmlFor="password" className="form-label">
|
||||||
Password
|
Password
|
||||||
</label>
|
</label>
|
||||||
@ -146,7 +146,8 @@ const LoginPage = () => {
|
|||||||
type={hidepass ? "password" : "text"}
|
type={hidepass ? "password" : "text"}
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
id="password"
|
id="password"
|
||||||
className="form-control form-control-xl shadow-none"
|
className={`form-control form-control-xl shadow-none ${errors.password ? "is-invalid" : ""
|
||||||
|
}`}
|
||||||
name="password"
|
name="password"
|
||||||
{...register("password")}
|
{...register("password")}
|
||||||
placeholder="••••••••••••"
|
placeholder="••••••••••••"
|
||||||
@ -155,7 +156,7 @@ const LoginPage = () => {
|
|||||||
<span className="input-group-text cursor-pointer border-start-0">
|
<span className="input-group-text cursor-pointer border-start-0">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-link p-0"
|
className="btn btn-link-secondary p-0"
|
||||||
onClick={() => setHidepass(!hidepass)}
|
onClick={() => setHidepass(!hidepass)}
|
||||||
>
|
>
|
||||||
{hidepass ? (
|
{hidepass ? (
|
||||||
@ -166,8 +167,16 @@ const LoginPage = () => {
|
|||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* ✅ Error message */}
|
||||||
|
{errors.password && (
|
||||||
|
<div className="invalid-feedback text-start" style={{ fontSize: "12px" }}>
|
||||||
|
{errors.password.message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{/* Remember Me + Forgot Password */}
|
{/* Remember Me + Forgot Password */}
|
||||||
<div className="mb-3 d-flex justify-content-between align-items-center">
|
<div className="mb-3 d-flex justify-content-between align-items-center">
|
||||||
<div className="form-check">
|
<div className="form-check">
|
||||||
|
|||||||
@ -176,10 +176,12 @@ const EmployeeList = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loading && Array.isArray(employees)) {
|
if (!loading && Array.isArray(employees)) {
|
||||||
const sorted = [...employees].sort((a, b) => {
|
const sorted = [...employees].sort((a, b) => {
|
||||||
const nameA = `${a.firstName || ""}${a.middleName || ""}${a.lastName || ""
|
const nameA = `${a.firstName || ""}${a.middleName || ""}${
|
||||||
}`.toLowerCase();
|
a.lastName || ""
|
||||||
const nameB = `${b.firstName || ""}${b.middleName || ""}${b.lastName || ""
|
}`.toLowerCase();
|
||||||
}`.toLowerCase();
|
const nameB = `${b.firstName || ""}${b.middleName || ""}${
|
||||||
|
b.lastName || ""
|
||||||
|
}`.toLowerCase();
|
||||||
return nameA?.localeCompare(nameB);
|
return nameA?.localeCompare(nameB);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -266,8 +268,9 @@ const EmployeeList = () => {
|
|||||||
? "Suspend Employee"
|
? "Suspend Employee"
|
||||||
: "Reactivate Employee"
|
: "Reactivate Employee"
|
||||||
}
|
}
|
||||||
message={`Are you sure you want to ${selectedEmpFordelete?.isActive ? "suspend" : "reactivate"
|
message={`Are you sure you want to ${
|
||||||
} this employee?`}
|
selectedEmpFordelete?.isActive ? "suspend" : "reactivate"
|
||||||
|
} this employee?`}
|
||||||
onSubmit={(id) =>
|
onSubmit={(id) =>
|
||||||
suspendEmployee({
|
suspendEmployee({
|
||||||
employeeId: id,
|
employeeId: id,
|
||||||
@ -291,11 +294,11 @@ const EmployeeList = () => {
|
|||||||
{ViewTeamMember ? (
|
{ViewTeamMember ? (
|
||||||
// <div className="row">
|
// <div className="row">
|
||||||
<div className="card p-1">
|
<div className="card p-1">
|
||||||
<div className="card-datatable table-responsive pt-2">
|
<div className="card-datatable table-responsive pt-5 mx-5 py-10">
|
||||||
<div
|
<div
|
||||||
id="DataTables_Table_0_wrapper"
|
id="DataTables_Table_0_wrapper"
|
||||||
className="dataTables_wrapper dt-bootstrap5 no-footer"
|
className="dataTables_wrapper dt-bootstrap5 no-footer"
|
||||||
style={{ width: "98%" }}
|
style={{ width: "100%" }}
|
||||||
>
|
>
|
||||||
<div className="d-flex flex-wrap align-items-center justify-content-between gap-3 mb-3">
|
<div className="d-flex flex-wrap align-items-center justify-content-between gap-3 mb-3">
|
||||||
{/* Switches: All Employees + Inactive */}
|
{/* Switches: All Employees + Inactive */}
|
||||||
@ -315,7 +318,7 @@ const EmployeeList = () => {
|
|||||||
className="form-check-label ms-0"
|
className="form-check-label ms-0"
|
||||||
htmlFor="allEmployeesCheckbox"
|
htmlFor="allEmployeesCheckbox"
|
||||||
>
|
>
|
||||||
All Employees
|
Show All Employees
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -351,7 +354,7 @@ const EmployeeList = () => {
|
|||||||
value={searchText}
|
value={searchText}
|
||||||
onChange={handleSearch}
|
onChange={handleSearch}
|
||||||
className="form-control form-control-sm"
|
className="form-control form-control-sm"
|
||||||
placeholder="Search User"
|
placeholder="Search Employee"
|
||||||
aria-controls="DataTables_Table_0"
|
aria-controls="DataTables_Table_0"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
@ -499,8 +502,9 @@ const EmployeeList = () => {
|
|||||||
Status
|
Status
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
className={`sorting_disabled ${!Manage_Employee && "d-none"
|
className={`sorting_disabled ${
|
||||||
}`}
|
!Manage_Employee && "d-none"
|
||||||
|
}`}
|
||||||
rowSpan="1"
|
rowSpan="1"
|
||||||
colSpan="1"
|
colSpan="1"
|
||||||
style={{ width: "50px" }}
|
style={{ width: "50px" }}
|
||||||
@ -520,9 +524,9 @@ const EmployeeList = () => {
|
|||||||
)}
|
)}
|
||||||
{/* Conditional messages for no data or no search results */}
|
{/* Conditional messages for no data or no search results */}
|
||||||
{!loading &&
|
{!loading &&
|
||||||
displayData?.length === 0 &&
|
displayData?.length === 0 &&
|
||||||
searchText &&
|
searchText &&
|
||||||
!showAllEmployees ? (
|
!showAllEmployees ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={8}>
|
<td colSpan={8}>
|
||||||
<small className="muted">
|
<small className="muted">
|
||||||
@ -532,8 +536,8 @@ const EmployeeList = () => {
|
|||||||
</tr>
|
</tr>
|
||||||
) : null}
|
) : null}
|
||||||
{!loading &&
|
{!loading &&
|
||||||
displayData?.length === 0 &&
|
displayData?.length === 0 &&
|
||||||
(!searchText || showAllEmployees) ? (
|
(!searchText || showAllEmployees) ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td
|
<td
|
||||||
colSpan={8}
|
colSpan={8}
|
||||||
@ -578,7 +582,7 @@ const EmployeeList = () => {
|
|||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-truncate text-italic">
|
<span className="text-truncate text-italic">
|
||||||
NA
|
-
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
@ -627,7 +631,9 @@ const EmployeeList = () => {
|
|||||||
<div className="dropdown-menu dropdown-menu-end">
|
<div className="dropdown-menu dropdown-menu-end">
|
||||||
{/* View always visible */}
|
{/* View always visible */}
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate(`/employee/${item.id}`)}
|
onClick={() =>
|
||||||
|
navigate(`/employee/${item.id}`)
|
||||||
|
}
|
||||||
className="dropdown-item py-1"
|
className="dropdown-item py-1"
|
||||||
>
|
>
|
||||||
<i className="bx bx-detail bx-sm"></i> View
|
<i className="bx bx-detail bx-sm"></i> View
|
||||||
@ -638,9 +644,12 @@ const EmployeeList = () => {
|
|||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
className="dropdown-item py-1"
|
className="dropdown-item py-1"
|
||||||
onClick={() => handleEmployeeModel(item.id)}
|
onClick={() =>
|
||||||
|
handleEmployeeModel(item.id)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<i className="bx bx-edit bx-sm"></i> Edit
|
<i className="bx bx-edit bx-sm"></i>{" "}
|
||||||
|
Edit
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Suspend only when active */}
|
{/* Suspend only when active */}
|
||||||
@ -649,7 +658,8 @@ const EmployeeList = () => {
|
|||||||
className="dropdown-item py-1"
|
className="dropdown-item py-1"
|
||||||
onClick={() => handleOpenDelete(item)}
|
onClick={() => handleOpenDelete(item)}
|
||||||
>
|
>
|
||||||
<i className="bx bx-task-x bx-sm"></i> Suspend
|
<i className="bx bx-task-x bx-sm"></i>{" "}
|
||||||
|
Suspend
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -658,11 +668,13 @@ const EmployeeList = () => {
|
|||||||
type="button"
|
type="button"
|
||||||
data-bs-toggle="modal"
|
data-bs-toggle="modal"
|
||||||
data-bs-target="#managerole-modal"
|
data-bs-target="#managerole-modal"
|
||||||
onClick={() => setEmpForManageRole(item.id)}
|
onClick={() =>
|
||||||
|
setEmpForManageRole(item.id)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<i className="bx bx-cog bx-sm"></i> Manage Role
|
<i className="bx bx-cog bx-sm"></i>{" "}
|
||||||
|
Manage Role
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -672,7 +684,8 @@ const EmployeeList = () => {
|
|||||||
className="dropdown-item py-1"
|
className="dropdown-item py-1"
|
||||||
onClick={() => handleOpenDelete(item)}
|
onClick={() => handleOpenDelete(item)}
|
||||||
>
|
>
|
||||||
<i className="bx bx-refresh bx-sm me-1"></i> Re-activate
|
<i className="bx bx-refresh bx-sm me-1"></i>{" "}
|
||||||
|
Re-activate
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -691,8 +704,9 @@ const EmployeeList = () => {
|
|||||||
<nav aria-label="Page">
|
<nav aria-label="Page">
|
||||||
<ul className="pagination pagination-sm justify-content-end py-1">
|
<ul className="pagination pagination-sm justify-content-end py-1">
|
||||||
<li
|
<li
|
||||||
className={`page-item ${currentPage === 1 ? "disabled" : ""
|
className={`page-item ${
|
||||||
}`}
|
currentPage === 1 ? "disabled" : ""
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
className="page-link btn-xs"
|
className="page-link btn-xs"
|
||||||
@ -705,8 +719,9 @@ const EmployeeList = () => {
|
|||||||
{[...Array(totalPages)]?.map((_, index) => (
|
{[...Array(totalPages)]?.map((_, index) => (
|
||||||
<li
|
<li
|
||||||
key={index}
|
key={index}
|
||||||
className={`page-item ${currentPage === index + 1 ? "active" : ""
|
className={`page-item ${
|
||||||
}`}
|
currentPage === index + 1 ? "active" : ""
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
className="page-link"
|
className="page-link"
|
||||||
@ -718,8 +733,9 @@ const EmployeeList = () => {
|
|||||||
))}
|
))}
|
||||||
|
|
||||||
<li
|
<li
|
||||||
className={`page-item ${currentPage === totalPages ? "disabled" : ""
|
className={`page-item ${
|
||||||
}`}
|
currentPage === totalPages ? "disabled" : ""
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
className="page-link"
|
className="page-link"
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { useSearchParams, useParams, useNavigate } from "react-router-dom";
|
import { useSearchParams, useParams, useNavigate } from "react-router-dom";
|
||||||
import { useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useEmployee,
|
useEmployee,
|
||||||
@ -23,22 +23,33 @@ import EmpBanner from "../../components/Employee/EmpBanner";
|
|||||||
import EmpDashboard from "../../components/Employee/EmpDashboard";
|
import EmpDashboard from "../../components/Employee/EmpDashboard";
|
||||||
import EmpDocuments from "../../components/Employee/EmpDocuments";
|
import EmpDocuments from "../../components/Employee/EmpDocuments";
|
||||||
import EmpActivities from "../../components/Employee/EmpActivities";
|
import EmpActivities from "../../components/Employee/EmpActivities";
|
||||||
|
import { setProjectId } from "../../slices/localVariablesSlice";
|
||||||
|
|
||||||
const EmployeeProfile = () => {
|
const EmployeeProfile = () => {
|
||||||
const { profile } = useProfile();
|
const { profile } = useProfile();
|
||||||
|
|
||||||
const projectID = useSelector((store) => store.localVariables.projectId);
|
const projectID = useSelector((store) => store.localVariables.projectId);
|
||||||
|
|
||||||
const { employeeId } = useParams();
|
const { employeeId } = useParams();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const [SearchParams] = useSearchParams();
|
const [SearchParams] = useSearchParams();
|
||||||
const tab = SearchParams.get("for");
|
const tab = SearchParams.get("for");
|
||||||
const [activePill, setActivePill] = useState(tab || "profile");
|
const [activePill, setActivePill] = useState(tab || "profile");
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
const {data:currentEmployee,isLoading,isError,error} = useEmployee(employeeId)
|
const {
|
||||||
|
data: currentEmployee,
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
error,
|
||||||
|
} = useEmployee(employeeId);
|
||||||
const handlePillClick = (pillKey) => {
|
const handlePillClick = (pillKey) => {
|
||||||
setActivePill(pillKey);
|
setActivePill(pillKey);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(setProjectId(null));
|
||||||
|
}, [projectID]);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
@ -87,7 +98,7 @@ const EmployeeProfile = () => {
|
|||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <div>Loading...</div>;
|
return <div>Loading...</div>;
|
||||||
}
|
}
|
||||||
if(isError) return <div >{error.message}</div>
|
if (isError) return <div>{error.message}</div>;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="container-fluid">
|
<div className="container-fluid">
|
||||||
@ -115,11 +126,11 @@ const EmployeeProfile = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className=" ">{renderContent()}</div>
|
<div className=" ">{renderContent()}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default EmployeeProfile;
|
export default EmployeeProfile;
|
||||||
|
|||||||
@ -5,7 +5,10 @@ import Breadcrumb from "../../components/common/Breadcrumb";
|
|||||||
import MasterModal from "../../components/master/MasterModal";
|
import MasterModal from "../../components/master/MasterModal";
|
||||||
import ConfirmModal from "../../components/common/ConfirmModal";
|
import ConfirmModal from "../../components/common/ConfirmModal";
|
||||||
import MasterTable from "./MasterTable";
|
import MasterTable from "./MasterTable";
|
||||||
import useMaster, { useDeleteMasterItem, useMasterMenu } from "../../hooks/masterHook/useMaster";
|
import useMaster, {
|
||||||
|
useDeleteMasterItem,
|
||||||
|
useMasterMenu,
|
||||||
|
} from "../../hooks/masterHook/useMaster";
|
||||||
import { changeMaster } from "../../slices/localVariablesSlice";
|
import { changeMaster } from "../../slices/localVariablesSlice";
|
||||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||||
import { MANAGE_MASTER } from "../../utils/constants";
|
import { MANAGE_MASTER } from "../../utils/constants";
|
||||||
@ -14,11 +17,22 @@ import GlobalModel from "../../components/common/GlobalModel";
|
|||||||
const MasterPage = () => {
|
const MasterPage = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const selectedMaster = useSelector((store) => store.localVariables.selectedMaster);
|
const selectedMaster = useSelector(
|
||||||
|
(store) => store.localVariables.selectedMaster
|
||||||
|
);
|
||||||
const hasMasterPermission = useHasUserPermission(MANAGE_MASTER);
|
const hasMasterPermission = useHasUserPermission(MANAGE_MASTER);
|
||||||
|
|
||||||
const { data: menuData, isLoading: menuLoading, isError: menuErrorFlag, error: menuError } = useMasterMenu();
|
const {
|
||||||
const { data: masterData = [], loading, isError: isMasterError } = useMaster();
|
data: menuData,
|
||||||
|
isLoading: menuLoading,
|
||||||
|
isError: menuErrorFlag,
|
||||||
|
error: menuError,
|
||||||
|
} = useMasterMenu();
|
||||||
|
const {
|
||||||
|
data: masterData = [],
|
||||||
|
loading,
|
||||||
|
isError: isMasterError,
|
||||||
|
} = useMaster();
|
||||||
const { mutate: DeleteMaster, isPending: isDeleting } = useDeleteMasterItem();
|
const { mutate: DeleteMaster, isPending: isDeleting } = useDeleteMasterItem();
|
||||||
|
|
||||||
const [modalConfig, setModalConfig] = useState(null);
|
const [modalConfig, setModalConfig] = useState(null);
|
||||||
@ -26,7 +40,8 @@ const MasterPage = () => {
|
|||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
|
||||||
const displayData = useMemo(() => {
|
const displayData = useMemo(() => {
|
||||||
const dataSource = queryClient.getQueryData(["masterData", selectedMaster]) || masterData;
|
const dataSource =
|
||||||
|
queryClient.getQueryData(["masterData", selectedMaster]) || masterData;
|
||||||
if (!searchTerm) return dataSource;
|
if (!searchTerm) return dataSource;
|
||||||
return dataSource.filter((item) =>
|
return dataSource.filter((item) =>
|
||||||
Object.values(item).some((val) =>
|
Object.values(item).some((val) =>
|
||||||
@ -37,7 +52,10 @@ const MasterPage = () => {
|
|||||||
|
|
||||||
const columns = useMemo(() => {
|
const columns = useMemo(() => {
|
||||||
if (!displayData.length) return [];
|
if (!displayData.length) return [];
|
||||||
return Object.keys(displayData[0]).map((key) => ({ key, label: key.toUpperCase() }));
|
return Object.keys(displayData[0]).map((key) => ({
|
||||||
|
key,
|
||||||
|
label: key.toUpperCase(),
|
||||||
|
}));
|
||||||
}, [displayData]);
|
}, [displayData]);
|
||||||
|
|
||||||
const handleModalData = (type, item = null, masterType = selectedMaster) => {
|
const handleModalData = (type, item = null, masterType = selectedMaster) => {
|
||||||
@ -47,18 +65,24 @@ const MasterPage = () => {
|
|||||||
|
|
||||||
const handleDeleteSubmit = () => {
|
const handleDeleteSubmit = () => {
|
||||||
if (!deleteData) return;
|
if (!deleteData) return;
|
||||||
DeleteMaster({ masterType: deleteData.masterType, item: deleteData.item }, {
|
DeleteMaster(
|
||||||
onSuccess: () => setDeleteData(null),
|
{ masterType: deleteData.masterType, item: deleteData.item },
|
||||||
});
|
{
|
||||||
|
onSuccess: () => setDeleteData(null),
|
||||||
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (menuErrorFlag || isMasterError)
|
if (menuErrorFlag || isMasterError)
|
||||||
return (
|
return (
|
||||||
<div className="d-flex flex-column align-items-center justify-content-center py-5">
|
<div className="d-flex flex-column align-items-center justify-content-center py-5">
|
||||||
<h4 className="mb-3">
|
<h4 className="mb-3">
|
||||||
<i className="fa-solid fa-triangle-exclamation fs-5" /> Oops, an error occurred
|
<i className="fa-solid fa-triangle-exclamation fs-5" /> Oops, an error
|
||||||
|
occurred
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-muted">{menuError?.message || "Error fetching master data"}</p>
|
<p className="text-muted">
|
||||||
|
{menuError?.message || "Error fetching master data"}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -66,11 +90,20 @@ const MasterPage = () => {
|
|||||||
<>
|
<>
|
||||||
{modalConfig && (
|
{modalConfig && (
|
||||||
<GlobalModel
|
<GlobalModel
|
||||||
size={["Application Role", "Edit-Application Role"].includes(modalConfig.masterType) ? "lg" : "md"}
|
size={
|
||||||
|
["Application Role", "Edit-Application Role"].includes(
|
||||||
|
modalConfig.masterType
|
||||||
|
)
|
||||||
|
? "lg"
|
||||||
|
: "md"
|
||||||
|
}
|
||||||
isOpen={!!modalConfig}
|
isOpen={!!modalConfig}
|
||||||
closeModal={() => setModalConfig(null)}
|
closeModal={() => setModalConfig(null)}
|
||||||
>
|
>
|
||||||
<MasterModal modaldata={modalConfig} closeModal={() => setModalConfig(null)} />
|
<MasterModal
|
||||||
|
modaldata={modalConfig}
|
||||||
|
closeModal={() => setModalConfig(null)}
|
||||||
|
/>
|
||||||
</GlobalModel>
|
</GlobalModel>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -85,40 +118,58 @@ const MasterPage = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="container-fluid">
|
<div className="container-fluid">
|
||||||
<Breadcrumb data={[{ label: "Home", link: "/dashboard" }, { label: "Masters" }]} />
|
<Breadcrumb
|
||||||
|
data={[{ label: "Home", link: "/dashboard" }, { label: "Masters" }]}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="card-datatable table-responsive py-4">
|
<div
|
||||||
|
className="card-datatable table-responsive py-10 mx-5 "
|
||||||
|
style={{ overflow: "hidden" }}
|
||||||
|
>
|
||||||
<div className="row mb-2">
|
<div className="row mb-2">
|
||||||
<div className="col-md-3 col-sm-6">
|
<div className="col-md-3 col-sm-6">
|
||||||
<select
|
<select
|
||||||
className="form-select py-1 px-2"
|
className="form-select py-1 px-2"
|
||||||
style={{ fontSize: "0.875rem", height: "32px", width: "190px" }}
|
style={{
|
||||||
|
fontSize: "0.875rem",
|
||||||
|
height: "32px",
|
||||||
|
width: "190px",
|
||||||
|
}}
|
||||||
value={selectedMaster}
|
value={selectedMaster}
|
||||||
onChange={(e) => dispatch(changeMaster(e.target.value))}
|
onChange={(e) => dispatch(changeMaster(e.target.value))}
|
||||||
>
|
>
|
||||||
{menuLoading ? (
|
{menuLoading ? (
|
||||||
<option value="">Loading...</option>
|
<option value="">Loading...</option>
|
||||||
) : (
|
) : (
|
||||||
menuData?.map((item) => <option key={item.id} value={item.name}>{item.name}</option>)
|
menuData?.map((item) => (
|
||||||
|
<option key={item.id} value={item.name}>
|
||||||
|
{item.name}
|
||||||
|
</option>
|
||||||
|
))
|
||||||
)}
|
)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-9 col-sm-6 d-flex justify-content-end align-items-center gap-2">
|
<div className="col-md-9 col-sm-6 d-flex justify-content-end align-items-center gap-2">
|
||||||
<div className="w-25"><input
|
<div className="w-25">
|
||||||
type="search"
|
<input
|
||||||
className="form-control form-control-sm"
|
type="search"
|
||||||
placeholder="Search"
|
className="form-control form-control-sm"
|
||||||
value={searchTerm}
|
placeholder="Search"
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
value={searchTerm}
|
||||||
/></div>
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{hasMasterPermission && (
|
{hasMasterPermission && (
|
||||||
<button
|
<button
|
||||||
className="btn btn-sm btn-primary"
|
className="btn btn-sm btn-primary"
|
||||||
onClick={() => handleModalData(selectedMaster, null, selectedMaster)}
|
onClick={() =>
|
||||||
|
handleModalData(selectedMaster, null, selectedMaster)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<i className="bx bx-plus-circle me-2"></i>Add {selectedMaster}
|
<i className="bx bx-plus-circle me-2"></i>Add{" "}
|
||||||
|
{selectedMaster}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -179,8 +179,9 @@ const MasterTable = ({ data, columns, loading, handleModalData }) => {
|
|||||||
aria-label="Delete"
|
aria-label="Delete"
|
||||||
type="button"
|
type="button"
|
||||||
className="btn p-0 dropdown-toggle hide-arrow"
|
className="btn p-0 dropdown-toggle hide-arrow"
|
||||||
onClick={() => handleModalData("delete", item, selectedMaster)}
|
onClick={() =>
|
||||||
|
handleModalData("delete", item, selectedMaster)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<i className="bx bx-trash me-1 text-danger"></i>
|
<i className="bx bx-trash me-1 text-danger"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { useSelector, useDispatch } from "react-redux"; // Import useSelector
|
|
||||||
import React, { useState, useEffect, useCallback } from "react";
|
import React, { useState, useEffect, useCallback } from "react";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import ProjectOverview from "../../components/Project/ProjectOverview";
|
import ProjectOverview from "../../components/Project/ProjectOverview";
|
||||||
import AboutProject from "../../components/Project/AboutProject";
|
import AboutProject from "../../components/Project/AboutProject";
|
||||||
@ -9,56 +10,43 @@ import ProjectInfra from "../../components/Project/ProjectInfra";
|
|||||||
import Loader from "../../components/common/Loader";
|
import Loader from "../../components/common/Loader";
|
||||||
import WorkPlan from "../../components/Project/WorkPlan";
|
import WorkPlan from "../../components/Project/WorkPlan";
|
||||||
import Breadcrumb from "../../components/common/Breadcrumb";
|
import Breadcrumb from "../../components/common/Breadcrumb";
|
||||||
import {
|
import { useSelectedProject } from "../../slices/apiDataManager";
|
||||||
cacheData,
|
import { useProjectDetails, useProjectName } from "../../hooks/useProjects";
|
||||||
clearCacheKey,
|
|
||||||
getCachedData,
|
|
||||||
useSelectedProject,
|
|
||||||
} from "../../slices/apiDataManager";
|
|
||||||
import "./ProjectDetails.css";
|
|
||||||
import { useProjectDetails } from "../../hooks/useProjects";
|
|
||||||
import { ComingSoonPage } from "../Misc/ComingSoonPage";
|
import { ComingSoonPage } from "../Misc/ComingSoonPage";
|
||||||
import eventBus from "../../services/eventBus";
|
import eventBus from "../../services/eventBus";
|
||||||
import ProjectProgressChart from "../../components/Dashboard/ProjectProgressChart";
|
import ProjectProgressChart from "../../components/Dashboard/ProjectProgressChart";
|
||||||
import { useProjectName } from "../../hooks/useProjects";
|
|
||||||
import AttendanceOverview from "../../components/Dashboard/AttendanceChart";
|
import AttendanceOverview from "../../components/Dashboard/AttendanceChart";
|
||||||
import { setProjectId } from "../../slices/localVariablesSlice";
|
import { setProjectId } from "../../slices/localVariablesSlice";
|
||||||
import ProjectDocument from "../../components/Project/ProjectDocuments";
|
|
||||||
import ProjectDocuments from "../../components/Project/ProjectDocuments";
|
import ProjectDocuments from "../../components/Project/ProjectDocuments";
|
||||||
import ProjectSetting from "../../components/Project/ProjectSetting";
|
import ProjectSetting from "../../components/Project/ProjectSetting";
|
||||||
import DirectoryPage from "../Directory/DirectoryPage";
|
import DirectoryPage from "../Directory/DirectoryPage";
|
||||||
|
import { useProjectAccess } from "../../hooks/useProjectAccess"; // ✅ new
|
||||||
|
|
||||||
|
import "./ProjectDetails.css";
|
||||||
|
|
||||||
const ProjectDetails = () => {
|
const ProjectDetails = () => {
|
||||||
|
const projectId = useSelectedProject();
|
||||||
const projectId = useSelectedProject()
|
|
||||||
|
|
||||||
const { projectNames, fetchData } = useProjectName();
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const { projectNames } = useProjectName();
|
||||||
|
const { projects_Details, loading: projectLoading, refetch } =
|
||||||
|
useProjectDetails(projectId);
|
||||||
|
|
||||||
|
const { canView, loading: permsLoading } = useProjectAccess(projectId);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (projectId == null) {
|
if (!projectId && projectNames.length > 0) {
|
||||||
dispatch(setProjectId(projectNames[0]?.id));
|
dispatch(setProjectId(projectNames[0].id));
|
||||||
}
|
}
|
||||||
}, [projectNames]);
|
}, [projectId, projectNames, dispatch]);
|
||||||
|
|
||||||
const {
|
const [activePill, setActivePill] = useState(
|
||||||
projects_Details,
|
localStorage.getItem("lastActiveProjectTab") || "profile"
|
||||||
loading: projectLoading,
|
);
|
||||||
error: projectError,
|
|
||||||
refetch,
|
|
||||||
} = useProjectDetails(projectId);
|
|
||||||
|
|
||||||
// const [activePill, setActivePill] = useState("profile");
|
|
||||||
const [activePill, setActivePill] = useState(() => {
|
|
||||||
return localStorage.getItem("lastActiveProjectTab") || "profile";
|
|
||||||
});
|
|
||||||
|
|
||||||
const handler = useCallback(
|
const handler = useCallback(
|
||||||
(msg) => {
|
(msg) => {
|
||||||
if (
|
if (msg.keyword === "Update_Project" && projects_Details?.id === msg.response.id) {
|
||||||
msg.keyword === "Update_Project" &&
|
|
||||||
projects_Details?.id === msg.response.id
|
|
||||||
) {
|
|
||||||
refetch();
|
refetch();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -72,69 +60,42 @@ const ProjectDetails = () => {
|
|||||||
|
|
||||||
const handlePillClick = (pillKey) => {
|
const handlePillClick = (pillKey) => {
|
||||||
setActivePill(pillKey);
|
setActivePill(pillKey);
|
||||||
localStorage.setItem("lastActiveProjectTab", pillKey); // ✅ Save to localStorage
|
localStorage.setItem("lastActiveProjectTab", pillKey);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderContent = () => {
|
if (projectLoading || permsLoading || !projects_Details) {
|
||||||
if (projectLoading || !projects_Details) return <Loader />;
|
return <Loader />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderContent = () => {
|
||||||
switch (activePill) {
|
switch (activePill) {
|
||||||
case "profile":
|
case "profile":
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="row">
|
|
||||||
<div className="col-lg-4 col-md-5 mt-2">
|
|
||||||
<AboutProject></AboutProject>
|
|
||||||
<ProjectOverview project={projectId} />
|
|
||||||
</div>
|
|
||||||
<div className="col-lg-8 col-md-7 mt-5">
|
|
||||||
<ProjectProgressChart
|
|
||||||
ShowAllProject="false"
|
|
||||||
DefaultRange="1M"
|
|
||||||
/>
|
|
||||||
<div className="mt-5">
|
|
||||||
{" "}
|
|
||||||
<AttendanceOverview />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
case "teams":
|
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-lg-12">
|
<div className="col-lg-4 col-md-5 mt-2">
|
||||||
<Teams />
|
<AboutProject />
|
||||||
|
<ProjectOverview project={projectId} />
|
||||||
|
</div>
|
||||||
|
<div className="col-lg-8 col-md-7 mt-5">
|
||||||
|
<ProjectProgressChart ShowAllProject="false" DefaultRange="1M" />
|
||||||
|
<div className="mt-5">
|
||||||
|
<AttendanceOverview />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
case "teams":
|
||||||
|
return <Teams />;
|
||||||
case "infra":
|
case "infra":
|
||||||
return <ProjectInfra data={projects_Details} onDataChange={refetch} />;
|
return <ProjectInfra data={projects_Details} onDataChange={refetch} />;
|
||||||
|
|
||||||
case "workplan":
|
case "workplan":
|
||||||
return <WorkPlan data={projects_Details} onDataChange={refetch} />;
|
return <WorkPlan data={projects_Details} onDataChange={refetch} />;
|
||||||
|
|
||||||
case "directory":
|
case "directory":
|
||||||
return (
|
return <DirectoryPage IsPage={false} projectId={projects_Details.id} />;
|
||||||
<div className="row mt-2">
|
|
||||||
<DirectoryPage IsPage={false} projectId={projects_Details.id} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
case "documents":
|
case "documents":
|
||||||
return (
|
return <ProjectDocuments />;
|
||||||
<div className="row">
|
case "setting":
|
||||||
<ProjectDocuments />
|
return <ProjectSetting />;
|
||||||
</div>
|
|
||||||
);
|
|
||||||
case "setting":
|
|
||||||
return (
|
|
||||||
<div className="row">
|
|
||||||
<ProjectSetting />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return <ComingSoonPage />;
|
return <ComingSoonPage />;
|
||||||
}
|
}
|
||||||
@ -149,7 +110,6 @@ const ProjectDetails = () => {
|
|||||||
{ label: projects_Details?.name || "Project", link: null },
|
{ label: projects_Details?.name || "Project", link: null },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<ProjectNav onPillClick={handlePillClick} activePill={activePill} />
|
<ProjectNav onPillClick={handlePillClick} activePill={activePill} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -44,7 +44,8 @@ const ProjectRepository = {
|
|||||||
getProjectLevelEmployeeList:(projectId)=>api.get(`/api/Project/get/proejct-level/employees/${projectId}`),
|
getProjectLevelEmployeeList:(projectId)=>api.get(`/api/Project/get/proejct-level/employees/${projectId}`),
|
||||||
getProjectLevelModules:()=>api.get(`/api/Project/get/proejct-level/modules`),
|
getProjectLevelModules:()=>api.get(`/api/Project/get/proejct-level/modules`),
|
||||||
getProjectLevelEmployeePermissions:(employeeId,projectId)=>api.get(`/api/Project/get/project-level-permission/employee/${employeeId}/project/${projectId}`),
|
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)
|
updateProjectLevelEmployeePermission:(data)=>api.post(`/api/Project/assign/project-level-permission`,data),
|
||||||
|
getAllProjectLevelPermission:(projectId)=>api.get(`/api/Project/get/all/project-level-permission/${projectId}`)
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TasksRepository = {
|
export const TasksRepository = {
|
||||||
|
|||||||
@ -17,6 +17,7 @@ export const VIEW_ALL_EMPLOYEES = "60611762-7f8a-4fb5-b53f-b1139918796b"
|
|||||||
|
|
||||||
export const VIEW_TEAM_MEMBERS = "b82d2b7e-0d52-45f3-997b-c008ea460e7f"
|
export const VIEW_TEAM_MEMBERS = "b82d2b7e-0d52-45f3-997b-c008ea460e7f"
|
||||||
|
|
||||||
|
export const MANAGE_TEAM = "b94802ce-0689-4643-9e1d-11c86950c35b"
|
||||||
|
|
||||||
export const MANAGE_PROJECT_INFRA = "cf2825ad-453b-46aa-91d9-27c124d63373"
|
export const MANAGE_PROJECT_INFRA = "cf2825ad-453b-46aa-91d9-27c124d63373"
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user