Add skeleton in Dashboard Projects, Teams and Tasks. #401

Closed
kartik.sharma wants to merge 36 commits from UI_Changes_Add_Skeleton into Issues_Sep_1W_V2
39 changed files with 1254 additions and 1129 deletions

View File

@ -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 canApproveTask = useHasUserPermission(APPROVE_TASK);
const canReportTask = useHasUserPermission(ASSIGN_REPORT_TASK);
const ManageInfra = useHasUserPermission( MANAGE_PROJECT_INFRA ) const reloadedData = useSelector((store) => store.localVariables.reload);
const ApprovedTaskRights = useHasUserPermission(APPROVE_TASK)
const ReportTaskRights = useHasUserPermission(ASSIGN_REPORT_TASK)
const reloadedData = useSelector( ( store ) => store.localVariables.reload )
const hasAccess = canManageInfra || canApproveTask || canReportTask;
// useEffect( () => if (isError) {
// { return <div>{error?.response?.data?.message || error?.message}</div>;
// if (reloadedData) }
// {
// refetch()
// dispatch( refreshData( false ) )
// }
// },[reloadedData]) 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;

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

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

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

View File

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

View File

@ -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}

View File

@ -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,8 +101,7 @@ 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"
}`} }`}
@ -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>
); );
}; };

View File

@ -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(() => {
@ -37,6 +38,9 @@ const Projects = () => {
Projects Projects
</h5> </h5>
</div> </div>
{loading ? (
<TeamsSkeleton />
) : (
<div className="d-flex justify-content-around align-items-start mt-n2"> <div className="d-flex justify-content-around align-items-start mt-n2">
<div> <div>
<h4 className="mb-0 fw-bold"> <h4 className="mb-0 fw-bold">
@ -51,6 +55,7 @@ const Projects = () => {
<small className="text-muted">Ongoing</small> <small className="text-muted">Ongoing</small>
</div> </div>
</div> </div>
)}
</div> </div>
); );
}; };

View File

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

View File

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

View File

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

View File

@ -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)}
/> />
</> </>
)} )}

View File

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

View File

@ -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">

View File

@ -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(
z.object({
startDate: z.string(), startDate: z.string(),
endDate: 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();
@ -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,26 +137,27 @@ 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"
@ -265,7 +251,8 @@ 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
@ -277,7 +264,8 @@ const EmpAttendance = ({ employee }) => {
</li> </li>
))} ))}
<li <li
className={`page-item ${currentPage === totalPages ? "disabled" : "" className={`page-item ${
currentPage === totalPages ? "disabled" : ""
}`} }`}
> >
<button <button

View File

@ -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">

View File

@ -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,12 +53,19 @@ 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();
@ -74,15 +83,15 @@ useEffect(() => {
const newSelectedIds = formData.selectedPermissions || []; const newSelectedIds = formData.selectedPermissions || [];
const removed = existingEnabledIds
.filter((id) => !newSelectedIds.includes(id))
.map((id) => ({ id, isEnabled: false }));
const added = newSelectedIds const added = newSelectedIds
.filter((id) => !existingEnabledIds.includes(id)) .filter((id) => !existingEnabledIds.includes(id))
.map((id) => ({ id, isEnabled: true })); .map((id) => ({ id, isEnabled: true }));
const payloadPermissions = [...removed, ...added]; const removed = existingEnabledIds
.filter((id) => !newSelectedIds.includes(id))
.map((id) => ({ id, isEnabled: false }));
const payloadPermissions = [...added, ...removed];
if (payloadPermissions.length === 0) { if (payloadPermissions.length === 0) {
showToast("No changes detected", "info"); showToast("No changes detected", "info");
@ -95,17 +104,23 @@ useEffect(() => {
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>
</div>
<div className="d-block">
{" "}
<select <select
className="form-select form-select-sm" className="form-select form-select-sm"
{...register("employeeId")} {...register("employeeId")}
@ -116,9 +131,10 @@ useEffect(() => {
) : ( ) : (
<> <>
<option value="">-- Select Employee --</option> <option value="">-- Select Employee --</option>
{[...employees]?.sort((a, b) => {[...employees]
`${a.firstName} ${a.firstName}`?.localeCompare( ?.sort((a, b) =>
`${b.firstName} ${b.lastName}` `${a?.firstName} ${a?.firstName}`?.localeCompare(
`${b?.firstName} ${b?.lastName}`
) )
) )
?.map((emp) => ( ?.map((emp) => (
@ -129,31 +145,46 @@ useEffect(() => {
</> </>
)} )}
</select> </select>
{errors.employeeId && ( {errors.employeeId && (
<div className="text-danger small"> <div className="d-block text-danger small">
{errors.employeeId.message} {errors.employeeId.message}
</div> </div>
)} )}
</div> </div>
</div>
<div className="mt-3 text-end">
{hasChanges && (
<button <button
type="submit"
className="btn btn-sm btn-primary" className="btn btn-sm btn-primary"
disabled={isPending || loading} disabled={isPending || loading}
> >
{isPending ? "Please Wait..." : "Update Permission"} {isPending ? "Please Wait..." : "Save Permission"}
</button> </button>
)}
</div>
</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>
<div className="px-2">
<ul className="list-unstyled">
{feature.featurePermissions?.map((perm) => ( {feature.featurePermissions?.map((perm) => (
<div className="col-12 col-sm-6 col-md-4 mb-2" key={perm.id}> <div className="d-flex my-2" key={perm.id}>
<Controller
name="selectedPermissions"
control={control}
render={({ field }) => {
const value = field.value || [];
const isChecked = value.includes(perm.id);
return (
<label <label
className="form-check-label d-flex align-items-center" className="form-check-label d-flex align-items-center"
htmlFor={perm.id} htmlFor={perm.id}
@ -162,16 +193,27 @@ useEffect(() => {
type="checkbox" type="checkbox"
className="form-check-input me-2" className="form-check-input me-2"
id={perm.id} id={perm.id}
value={perm.id} checked={isChecked}
{...register("selectedPermissions")} onChange={(e) => {
if (e.target.checked) {
field.onChange([...value, perm.id]); // add
} else {
field.onChange(
value.filter((v) => v !== perm.id)
); // remove
}
}}
/> />
{perm.name} {perm.name}
</label> </label>
);
}}
/>
</div> </div>
))} ))}
</ul>
</div> </div>
</div> </div>
<hr className="my-2" />
</div> </div>
))} ))}
</form> </form>

View File

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

View File

@ -128,7 +128,7 @@ const TenantsList = ({
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>
@ -136,7 +136,7 @@ const TenantsList = ({
); );
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>

View File

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

View File

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

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

View File

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

View File

@ -118,10 +118,10 @@ 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">
@ -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>
</> </>

View File

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

View File

@ -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,22 +110,32 @@ 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 && (
<Breadcrumb
data={[ data={[
{ label: "Home", link: "/dashboard" }, { label: "Home", link: "/dashboard" },
{ label: "Directory", link: null }, { 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">
<div className="d-flex justify-content-between align-items-center mb-1">
<ul className="nav nav-tabs"> <ul className="nav nav-tabs">
<li className="nav-item cursor-pointer"> <li className="nav-item cursor-pointer">
<a <a
@ -124,7 +144,7 @@ export default function DirectoryPage({ IsPage = true, projectId = null }) {
} fs-6`} } fs-6`}
onClick={(e) => handleTabClick("notes", e)} onClick={(e) => handleTabClick("notes", e)}
> >
<i className="bx bx-note bx-sm me-1_5"></i> <i className="bx bx-notepad bx-sm me-1_5"></i>
<span className="d-none d-md-inline">Notes</span> <span className="d-none d-md-inline">Notes</span>
</a> </a>
</li> </li>
@ -140,33 +160,11 @@ export default function DirectoryPage({ IsPage = true, projectId = null }) {
</a> </a>
</li> </li>
</ul> </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
className="dropdown-item cursor-pointer"
onClick={() => handleExport("csv")}
>
<i className="bx bx-file me-1"></i> CSV
</a>
</li>
</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"
@ -178,7 +176,7 @@ export default function DirectoryPage({ IsPage = true, projectId = null }) {
)} )}
{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"
@ -188,8 +186,8 @@ export default function DirectoryPage({ IsPage = true, projectId = null }) {
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)}
> >
@ -197,15 +195,16 @@ export default function DirectoryPage({ IsPage = true, projectId = null }) {
</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>
<div className="form-check form-switch d-flex align-items-center ms-4">
<div className="form-check form-switch d-flex align-items-center">
<input <input
type="checkbox" type="checkbox"
className="form-check-input" className="form-check-input"
@ -218,12 +217,34 @@ export default function DirectoryPage({ IsPage = true, projectId = null }) {
className="form-check-label ms-2" className="form-check-label ms-2"
htmlFor="inactiveEmployeesCheckbox" htmlFor="inactiveEmployeesCheckbox"
> >
{showActive ? "Active" : "Inactive" } {showActive ? "Active" : "Inactive"} Contacts
</label> </label>
</div> </div>
</div> </div>
)} )}
</div> </div>
<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 cursor-pointer"
onClick={() => handleExport("csv")}
>
<i className="bx bx-file me-1"></i> CSV
</a>
</li>
</ul>
</div>
</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>
</> </>

View File

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

View File

@ -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>
@ -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, youll get access to powerful No matter which plan you choose, youll 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 dont
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 organizations 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>

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

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

View File

@ -121,10 +121,10 @@ const TenantPage = () => {
{ label: "Tenant", link: null }, { label: "Tenant", link: null },
]} ]}
/> />
<div className="card text-center my-4 p-5 pb-10">
{/* Super Tenant Actions */} {/* Super Tenant Actions */}
{isSuperTenant && ( {isSuperTenant && (
<div className="card d-flex p-2"> <div className="p-0">
<div className="row align-items-center"> <div className="row align-items-center">
{/* Search */} {/* Search */}
<div className="col-6 col-md-6 col-lg-3 mb-md-0"> <div className="col-6 col-md-6 col-lg-3 mb-md-0">
@ -132,7 +132,7 @@ const TenantPage = () => {
type="search" type="search"
value={searchText} value={searchText}
onChange={(e) => setSearchText(e.target.value)} onChange={(e) => setSearchText(e.target.value)}
className="form-control form-control-sm" className="form-control form-control"
placeholder="Search Tenant" placeholder="Search Tenant"
/> />
</div> </div>
@ -174,7 +174,7 @@ const TenantPage = () => {
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!
@ -182,6 +182,7 @@ const TenantPage = () => {
</div> </div>
) : null} ) : null}
</div> </div>
</div>
</TenantContext.Provider> </TenantContext.Provider>
); );
}; };

View File

@ -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,7 +167,15 @@ 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">

View File

@ -176,9 +176,11 @@ 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 || ""}${
a.lastName || ""
}`.toLowerCase(); }`.toLowerCase();
const nameB = `${b.firstName || ""}${b.middleName || ""}${b.lastName || "" const nameB = `${b.firstName || ""}${b.middleName || ""}${
b.lastName || ""
}`.toLowerCase(); }`.toLowerCase();
return nameA?.localeCompare(nameB); return nameA?.localeCompare(nameB);
}); });
@ -266,7 +268,8 @@ 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 ${
selectedEmpFordelete?.isActive ? "suspend" : "reactivate"
} this employee?`} } this employee?`}
onSubmit={(id) => onSubmit={(id) =>
suspendEmployee({ suspendEmployee({
@ -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,7 +502,8 @@ 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"
@ -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,7 +704,8 @@ 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
@ -705,7 +719,8 @@ 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
@ -718,7 +733,8 @@ const EmployeeList = () => {
))} ))}
<li <li
className={`page-item ${currentPage === totalPages ? "disabled" : "" className={`page-item ${
currentPage === totalPages ? "disabled" : ""
}`} }`}
> >
<button <button

View File

@ -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">

View File

@ -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(
{ masterType: deleteData.masterType, item: deleteData.item },
{
onSuccess: () => setDeleteData(null), 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">
<input
type="search" type="search"
className="form-control form-control-sm" className="form-control form-control-sm"
placeholder="Search" placeholder="Search"
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
/></div> />
</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>

View File

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

View File

@ -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 ( return (
<>
<div className="row"> <div className="row">
<div className="col-lg-4 col-md-5 mt-2"> <div className="col-lg-4 col-md-5 mt-2">
<AboutProject></AboutProject> <AboutProject />
<ProjectOverview project={projectId} /> <ProjectOverview project={projectId} />
</div> </div>
<div className="col-lg-8 col-md-7 mt-5"> <div className="col-lg-8 col-md-7 mt-5">
<ProjectProgressChart <ProjectProgressChart ShowAllProject="false" DefaultRange="1M" />
ShowAllProject="false"
DefaultRange="1M"
/>
<div className="mt-5"> <div className="mt-5">
{" "}
<AttendanceOverview /> <AttendanceOverview />
</div> </div>
</div> </div>
</div> </div>
</>
); );
case "teams": case "teams":
return ( return <Teams />;
<div className="row">
<div className="col-lg-12">
<Teams />
</div>
</div>
);
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">
<ProjectDocuments />
</div>
);
case "setting": case "setting":
return ( return <ProjectSetting />;
<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>

View File

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

View File

@ -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"