diff --git a/src/components/Activities/InfraPlanning.jsx b/src/components/Activities/InfraPlanning.jsx index 664c0613..dc237c63 100644 --- a/src/components/Activities/InfraPlanning.jsx +++ b/src/components/Activities/InfraPlanning.jsx @@ -19,54 +19,60 @@ import { useSelectedProject } from "../../slices/apiDataManager"; import Loader from "../common/Loader"; -const InfraPlanning = () => -{ - const {profile: LoggedUser, refetch : fetchData} = useProfile() - const dispatch = useDispatch() - // const selectedProject = useSelector((store)=>store.localVariables.projectId) + + +const InfraPlanning = () => { + const { profile: LoggedUser, refetch: fetchData } = useProfile(); + const dispatch = useDispatch(); const selectedProject = useSelectedProject(); - const {projectInfra, isLoading, error} = useProjectInfra( selectedProject ) + const { projectInfra, isLoading, isError, error, isFetched } = useProjectInfra(selectedProject); - - const ManageInfra = useHasUserPermission( MANAGE_PROJECT_INFRA ) - const ApprovedTaskRights = useHasUserPermission(APPROVE_TASK) - const ReportTaskRights = useHasUserPermission(ASSIGN_REPORT_TASK) - const reloadedData = useSelector( ( store ) => store.localVariables.reload ) + const canManageInfra = useHasUserPermission(MANAGE_PROJECT_INFRA); + const canApproveTask = useHasUserPermission(APPROVE_TASK); + const canReportTask = useHasUserPermission(ASSIGN_REPORT_TASK); - - // useEffect( () => - // { - // if (reloadedData) - // { - // refetch() - // dispatch( refreshData( false ) ) - // } + const reloadedData = useSelector((store) => store.localVariables.reload); - // },[reloadedData]) + const hasAccess = canManageInfra || canApproveTask || canReportTask; + + if (isError) { + return
{error?.response?.data?.message || error?.message}
; + } + + if (!hasAccess && !isLoading) { + return ( +
+ +

Access Denied: You don't have permission to perform this action.

+
+ ); + } + + if (isLoading) { + return ; + } + + if (isFetched && (!projectInfra || projectInfra.length === 0)) { + return ( +
+

No Result Found

+
+ ); + } return (
-
-
- {(ApprovedTaskRights || ReportTaskRights) ? ( -
-
- {isLoading && ( )} - {( !isLoading && projectInfra?.length === 0 ) && (

No Result Found

)} - {(!isLoading && projectInfra?.length > 0) && ()} +
+
+
+
- ) : ( -
- -

Access Denied: You don't have permission to perform this action. !

-
- )}
-
); }; export default InfraPlanning; + diff --git a/src/components/Charts/ProjectCompletionChartSkeleton.jsx b/src/components/Charts/ProjectCompletionChartSkeleton.jsx new file mode 100644 index 00000000..56307f17 --- /dev/null +++ b/src/components/Charts/ProjectCompletionChartSkeleton.jsx @@ -0,0 +1,32 @@ +import React from "react"; + +const ProjectCompletionChartSkeleton = () => { + return ( +
+
+
+
+ +
+

+ +

+
+
+ {/* Keep a fixed height so card doesn't shrink */} +
+
+ {Array.from({ length: 5 }).map((_, i) => ( +
+ ))} +
+
+
+ ); +}; + +export default ProjectCompletionChartSkeleton; diff --git a/src/components/Charts/ProjectProgressChartSkeleton.jsx b/src/components/Charts/ProjectProgressChartSkeleton.jsx new file mode 100644 index 00000000..c2d89404 --- /dev/null +++ b/src/components/Charts/ProjectProgressChartSkeleton.jsx @@ -0,0 +1,44 @@ +// ProjectProgressChartSkeleton.jsx +import React from "react"; + +const ProjectProgressChartSkeleton = () => { + return ( +
+
+
+ {/* Left: Title */} +
+
+ + +
+
+
+ + {/* Row 2: Time Range Buttons */} +
+ {Array(7) + .fill(0) + .map((_, idx) => ( + + ))} +
+
+ +
+
+ +
+
+
+ ); +}; + +export default ProjectProgressChartSkeleton; diff --git a/src/components/Charts/TeamsSkeleton.jsx b/src/components/Charts/TeamsSkeleton.jsx new file mode 100644 index 00000000..6236e939 --- /dev/null +++ b/src/components/Charts/TeamsSkeleton.jsx @@ -0,0 +1,18 @@ +import React from "react"; + +const TeamsSkeleton = () => { + return ( +
+
+
+
+
+
+
+
+
+
+ ); +}; + +export default TeamsSkeleton; diff --git a/src/components/Dashboard/AttendanceChart.jsx b/src/components/Dashboard/AttendanceChart.jsx index 01b62eed..418303b1 100644 --- a/src/components/Dashboard/AttendanceChart.jsx +++ b/src/components/Dashboard/AttendanceChart.jsx @@ -100,7 +100,7 @@ const AttendanceOverview = () => { return (
{/* Header */}
@@ -119,18 +119,22 @@ const AttendanceOverview = () => {
diff --git a/src/components/Dashboard/ProjectCompletionChart.jsx b/src/components/Dashboard/ProjectCompletionChart.jsx index 8ce4b13a..d2d48973 100644 --- a/src/components/Dashboard/ProjectCompletionChart.jsx +++ b/src/components/Dashboard/ProjectCompletionChart.jsx @@ -1,11 +1,13 @@ import React from "react"; import HorizontalBarChart from "../Charts/HorizontalBarChart"; import { useProjects } from "../../hooks/useProjects"; +import ProjectCompletionChartSkeleton from "../Charts/ProjectCompletionChartSkeleton"; const ProjectCompletionChart = () => { const { projects, loading } = useProjects(); - // Bar chart logic + if (loading) return ; + const projectNames = projects?.map((p) => p.name) || []; const projectProgress = projects?.map((p) => { @@ -16,14 +18,15 @@ const ProjectCompletionChart = () => { }) || []; return ( -
+
-
Projects
+
Projects

Projects Completion Status

-
+ {/* Keep same minHeight as skeleton to avoid shrinking */} +
+
{/* Left: Title */} @@ -100,11 +101,10 @@ const ProjectProgressChart = ({ {["1D", "1W", "15D", "1M", "3M", "1Y", "5Y"].map((key) => (
-
+ {isLineChartLoading ? ( + + ) : ( -
+ )} +
); }; diff --git a/src/components/Dashboard/Projects.jsx b/src/components/Dashboard/Projects.jsx index 0be20321..db7dad52 100644 --- a/src/components/Dashboard/Projects.jsx +++ b/src/components/Dashboard/Projects.jsx @@ -2,9 +2,10 @@ import React, { useCallback, useEffect, useState } from "react"; import { useDashboardProjectsCardData } from "../../hooks/useDashboard_Data"; import eventBus from "../../services/eventBus"; import GlobalRepository from "../../repositories/GlobalRepository"; +import TeamsSkeleton from "../Charts/TeamsSkeleton"; const Projects = () => { - const { projectsCardData } = useDashboardProjectsCardData(); + const { projectsCardData,loading } = useDashboardProjectsCardData(); const [projectData, setProjectsData] = useState(projectsCardData); useEffect(() => { @@ -13,13 +14,13 @@ const Projects = () => { const handler = useCallback( async (msg) => { - try { - const response = - await GlobalRepository.getDashboardProjectsCardData(); - setProjectsData(response.data); - } catch (err) { - console.error(err); - } + try { + const response = + await GlobalRepository.getDashboardProjectsCardData(); + setProjectsData(response.data); + } catch (err) { + console.error(err); + } }, [GlobalRepository] ); @@ -37,20 +38,24 @@ const Projects = () => { Projects
-
-
-

- {projectData.totalProjects?.toLocaleString()} -

- Total + {loading ? ( + + ) : ( +
+
+

+ {projectData.totalProjects?.toLocaleString()} +

+ Total +
+
+

+ {projectData.ongoingProjects?.toLocaleString()} +

+ Ongoing +
-
-

- {projectData.ongoingProjects?.toLocaleString()} -

- Ongoing -
-
+ )}
); }; diff --git a/src/components/Dashboard/Tasks.jsx b/src/components/Dashboard/Tasks.jsx index 2513e061..cb431d78 100644 --- a/src/components/Dashboard/Tasks.jsx +++ b/src/components/Dashboard/Tasks.jsx @@ -1,7 +1,7 @@ import React from "react"; import { useSelector } from "react-redux"; import { useDashboardTasksCardData } from "../../hooks/useDashboard_Data"; - +import TeamsSkeleton from "../Charts/TeamsSkeleton"; const TasksCard = () => { const projectId = useSelector((store) => store.localVariables?.projectId); const { tasksCardData, loading, error } = useDashboardTasksCardData(projectId); @@ -16,11 +16,7 @@ const TasksCard = () => { {loading ? ( // Loader will be displayed when loading is true -
-
- Loading... -
-
+ ) : error ? ( // Error message if there's an error
{error}
diff --git a/src/components/Dashboard/Teams.jsx b/src/components/Dashboard/Teams.jsx index 00e881f5..9303909d 100644 --- a/src/components/Dashboard/Teams.jsx +++ b/src/components/Dashboard/Teams.jsx @@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useState } from "react"; import { useSelector } from "react-redux"; import { useDashboardTeamsCardData } from "../../hooks/useDashboard_Data"; import eventBus from "../../services/eventBus"; +import TeamsSkeleton from "../Charts/TeamsSkeleton"; const Teams = () => { const projectId = useSelector((store) => store.localVariables?.projectId); @@ -38,11 +39,7 @@ const Teams = () => { {loading ? ( // Blue spinner loader -
-
- Loading... -
-
+ ) : error ? ( // Error message if data fetching fails
{error}
diff --git a/src/components/Directory/ListViewContact.jsx b/src/components/Directory/ListViewContact.jsx index 9f9b26d1..ef484c31 100644 --- a/src/components/Directory/ListViewContact.jsx +++ b/src/components/Directory/ListViewContact.jsx @@ -102,10 +102,10 @@ const ListViewContact = ({ data, Pagination }) => { className="card-datatable table-responsive" id="horizontal-example" > -
+
- + {contactList?.map((col) => ( - + {Array.isArray(data) && data.length > 0 ? ( data.map((row, i) => ( { const { data, isError, isLoading, error } = useBucketList(); @@ -17,18 +18,21 @@ const ManageBucket1 = () => { const [action, setAction] = useState(null); // "create" | "edit" | null const [selectedBucket, setSelectedBucket] = useState(null); const [searchTerm, setSearchTerm] = useState(""); + const { setContactOpen, setDeleteBucket } = useDirectoryContext(); - const handleClose = ()=>{ - setAction(null); + const handleClose = () => { + setAction(null); setSelectedBucket(null); - } + setDeleteId(null); + }; const { mutate: createBucket, isPending: creating } = useCreateBucket(() => { - handleClose() + handleClose(); }); const { mutate: updateBucket, isPending: updating } = useUpdateBucket(() => { - handleClose() + handleClose(); }); + const handleSubmit = (BucketPayload) => { if (selectedBucket) { updateBucket({ @@ -39,13 +43,13 @@ const ManageBucket1 = () => { }; - return (

Manage Buckets

- {action ? ( + + {action == "create" ? ( <> { isPending={creating || updating} /> {action === "edit" && selectedBucket && ( - + )} ) : ( @@ -84,11 +91,7 @@ const ManageBucket1 = () => { buckets={data} loading={isLoading} searchTerm={searchTerm} - onEdit={(bucket) => { - setSelectedBucket(bucket); - setAction("edit"); - }} - onDelete={(id) => console.log("delete", id)} + onDelete={(id) => setDeleteBucket({isOpen:true,bucketId:id})} /> )} diff --git a/src/components/Documents/DocumentFilterPanel.jsx b/src/components/Documents/DocumentFilterPanel.jsx index c94403fd..c305b9bb 100644 --- a/src/components/Documents/DocumentFilterPanel.jsx +++ b/src/components/Documents/DocumentFilterPanel.jsx @@ -189,7 +189,7 @@ const DocumentFilterPanel = ({ entityTypeId, onApply }) => {
)}
-
- {/* Permissions */} {ProjectModules.map((feature) => ( -
-
- {feature.name} -
-
-
- {feature.featurePermissions?.map((perm) => ( -
-
-
))} diff --git a/src/components/Project/ProjectSetting.jsx b/src/components/Project/ProjectSetting.jsx index 54991cec..35e334bf 100644 --- a/src/components/Project/ProjectSetting.jsx +++ b/src/components/Project/ProjectSetting.jsx @@ -3,9 +3,10 @@ import { ComingSoonPage } from "../../pages/Misc/ComingSoonPage"; import ProjectPermission from "./ProjectPermission"; const ProjectSetting = () => { - const [activePill, setActivePill] = useState(() => { - return localStorage.getItem("lastActiveProjectSettingTab") || "Permissions"; - }); + const [activePill, setActivePill] = useState("Permissions") + // const [activePill, setActivePill] = useState(() => { + // return localStorage.getItem("lastActiveProjectSettingTab") || "Permissions"; + // }); const projectSettingTab = [ { key: "Permissions", label: "Permissions" }, { key: "Notification", label: "Notification" }, @@ -32,7 +33,7 @@ const ProjectSetting = () => { return (
-
+ {/*
-
+
*/}
{renderContent()}
diff --git a/src/components/Tenant/TenantsList.jsx b/src/components/Tenant/TenantsList.jsx index 4788d7a2..cc039890 100644 --- a/src/components/Tenant/TenantsList.jsx +++ b/src/components/Tenant/TenantsList.jsx @@ -125,18 +125,18 @@ const TenantsList = ({ ]; if (isInitialLoading) return ; - if (isError) + if (isError) return (
-
- -

{error.message}

-
+
+ +

{error.message}

+
); return ( <> -
+
{col.label} @@ -116,7 +116,7 @@ const ListViewContact = ({ data, Pagination }) => {
diff --git a/src/hooks/useHasUserPermission.js b/src/hooks/useHasUserPermission.js index dc58c46d..b4ec3e52 100644 --- a/src/hooks/useHasUserPermission.js +++ b/src/hooks/useHasUserPermission.js @@ -1,11 +1,26 @@ +import { useSelectedProject } from "../slices/apiDataManager"; +import { useAllProjectLevelPermissions, useProfile } from "./useProfile"; -import { useProfile } from "./useProfile" export const useHasUserPermission = (permission) => { + const selectedProject = useSelectedProject(); const { profile } = useProfile(); + const { + data: projectPermissions = [], + isLoading, + isError, + } = useAllProjectLevelPermissions(selectedProject); - if (profile && permission && typeof permission === "string") { - return profile?.featurePermissions.includes(permission); + if (isLoading || !permission) return false; + + 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; }; diff --git a/src/hooks/useProfile.js b/src/hooks/useProfile.js index d1619ed6..73fcfe7b 100644 --- a/src/hooks/useProfile.js +++ b/src/hooks/useProfile.js @@ -1,67 +1,20 @@ -import {useState,useEffect, useCallback} from "react"; +import { useState, useEffect, useCallback } from "react"; import AuthRepository from "../repositories/AuthRepository"; -import {cacheData, cacheProfileData, getCachedData, getCachedProfileData} from "../slices/apiDataManager"; -import {useSelector} from "react-redux"; +import { + cacheData, + cacheProfileData, + getCachedData, + getCachedProfileData, + useSelectedProject, +} from "../slices/apiDataManager"; +import { useSelector } from "react-redux"; 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 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 = () => { const loggedUser = useSelector((store) => store.globalVariables.loginUser); const queryClient = useQueryClient(); @@ -100,12 +53,26 @@ export const useProfile = () => { }; }; - -export const useSidBarMenu = ()=>{ - const userLogged = useSelector((store)=>store.globalVariables.loginUser); +export const useSidBarMenu = () => { + const userLogged = useSelector((store) => store.globalVariables.loginUser); return useQuery({ - queryKey:["AppMenu"], - queryFn:async()=> await AuthRepository.appmenu(), - enabled: !!userLogged - }) -} \ No newline at end of file + queryKey: ["AppMenu"], + queryFn: async () => await AuthRepository.appmenu(), + enabled: !!userLogged, + }); +}; + +export const useAllProjectLevelPermissions = (projectId) => { + + + return useQuery({ + queryKey: ["AllProjectLevelPermission", projectId], + queryFn: async () => { + const resp = await ProjectRepository.getAllProjectLevelPermission( + projectId + ); + return resp.data; + }, + enabled: !!projectId, + }); +}; diff --git a/src/hooks/useProjectAccess.js b/src/hooks/useProjectAccess.js new file mode 100644 index 00000000..a3ff27ac --- /dev/null +++ b/src/hooks/useProjectAccess.js @@ -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, + }; +}; diff --git a/src/hooks/useProjects.js b/src/hooks/useProjects.js index a9815fc2..5650f0fa 100644 --- a/src/hooks/useProjects.js +++ b/src/hooks/useProjects.js @@ -177,6 +177,7 @@ export const useProjectInfra = (projectId) => { data: projectInfra, isLoading, error, + isFetched } = useQuery({ queryKey: ["ProjectInfra", projectId], 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) => { diff --git a/src/pages/Activities/AttendancePage.jsx b/src/pages/Activities/AttendancePage.jsx index 717af11a..f0e00ad5 100644 --- a/src/pages/Activities/AttendancePage.jsx +++ b/src/pages/Activities/AttendancePage.jsx @@ -95,11 +95,11 @@ const AttendancePage = () => { {(modelConfig?.action === 0 || modelConfig?.action === 1 || modelConfig?.action === 2) && ( - - )} + + )} {/* For view logs */} {modelConfig?.action === 6 && ( @@ -118,19 +118,19 @@ const AttendancePage = () => { ]} > -
+
{/* Tabs */} -
-
+
+
{/* Tabs */}
-
+
{selectedProject ? ( <> {activeTab === "all" && ( -
+
{
)}
-
diff --git a/src/pages/Activities/TaskPlannng.jsx b/src/pages/Activities/TaskPlannng.jsx index 09acc21f..25cf63f2 100644 --- a/src/pages/Activities/TaskPlannng.jsx +++ b/src/pages/Activities/TaskPlannng.jsx @@ -13,7 +13,7 @@ const { projectNames = [], loading: projectLoading } = useProjectName(); useEffect(() => { if (!selectedProject) { - dispatch(setProjectId(projectNames[0].id)); + dispatch(setProjectId(projectNames[0]?.id)); } }, [projectNames, selectedProject?.id, dispatch]); diff --git a/src/pages/Directory/DirectoryPage.jsx b/src/pages/Directory/DirectoryPage.jsx index 35857b3e..8e17b802 100644 --- a/src/pages/Directory/DirectoryPage.jsx +++ b/src/pages/Directory/DirectoryPage.jsx @@ -8,7 +8,11 @@ import { } from "react"; import Breadcrumb from "../../components/common/Breadcrumb"; 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 ManageContact from "../../components/Directory/ManageContact"; import BucketList from "../../components/Directory/BucketList"; @@ -16,6 +20,8 @@ import { MainDirectoryPageSkeleton } from "../../components/Directory/DirectoryP import ContactProfile from "../../components/Directory/ContactProfile"; import GlobalModel from "../../components/common/GlobalModel"; import { exportToCSV } from "../../utils/exportUtils"; +import ConfirmModal from "../../components/common/ConfirmModal"; +import { useSelectedProject } from "../../slices/apiDataManager"; const NotesPage = lazy(() => import("./NotesPage")); const ContactsPage = lazy(() => import("./ContactsPage")); @@ -44,6 +50,10 @@ export default function DirectoryPage({ IsPage = true, projectId = null }) { isOpen: false, contactId: null, }); + const [deleteBucket, setDeleteBucket] = useState({ + isOpen: false, + bucketId: null, + }); const [showActive, setShowActive] = useState(true); const [contactOpen, setContactOpen] = useState({ contact: null, @@ -100,129 +110,140 @@ export default function DirectoryPage({ IsPage = true, projectId = null }) { data, setManageContact, setContactOpen, + setDeleteBucket, }; + const { mutate: DeleteBucket, isPending: Deleting } = useDeleteBucket(() => { + setDeleteBucket({ isOpen: false, bucketId: null }); + }); + const handleDelete = (bucketId) => { + DeleteBucket(bucketId); + }; if (isLoading) return ; if (isError) return
{error.message}
; return ( <>
- {IsPage && ()} + {IsPage && ( + + )}
-
- - -
- - +
@@ -231,10 +252,18 @@ export default function DirectoryPage({ IsPage = true, projectId = null }) {
}> {activeTab === "notes" && ( - + )} {activeTab === "contacts" && ( - + )}
@@ -274,6 +303,19 @@ export default function DirectoryPage({ IsPage = true, projectId = null }) { /> )} + + {deleteBucket.isOpen && ( + setDeleteBucket({ isOpen: false, bucketId: null })} + loading={Deleting} + paramData={deleteBucket.bucketId} + /> + )}
diff --git a/src/pages/Home/LandingPage.css b/src/pages/Home/LandingPage.css index 318eadd5..35b05e9e 100644 --- a/src/pages/Home/LandingPage.css +++ b/src/pages/Home/LandingPage.css @@ -299,7 +299,7 @@ nav.layout-navbar.navbar-active::after { color: #d3d4dc; } .landing-footer .footer-bottom { - background-color: #282c3e; + background-color: #f44336; } .landing-footer .footer-link { transition: all 0.2s ease-in-out; @@ -312,6 +312,7 @@ nav.layout-navbar.navbar-active::after { padding-bottom: 1.3rem; border-top-left-radius: 1.75rem; border-top-right-radius: 1.75rem; + background-color: #f44336; } @media (max-width: 767.98px) { .landing-footer .footer-top { diff --git a/src/pages/Home/LandingPage.jsx b/src/pages/Home/LandingPage.jsx index 1d441312..35b01202 100644 --- a/src/pages/Home/LandingPage.jsx +++ b/src/pages/Home/LandingPage.jsx @@ -12,8 +12,7 @@ import "swiper/css"; import "swiper/css/navigation"; import SwaperSlideContent from "./SwaperSlideContent"; import SwaperBlogContent from "./SwaperBlogContent"; - - +import SubscriptionPlans from "./SubscriptionPlans"; const swiperConfig = { spaceBetween: 30, @@ -110,7 +109,7 @@ const LandingPage = () => {
  • - + Blogs
  • @@ -182,7 +181,7 @@ const LandingPage = () => {
    -
    @@ -296,10 +295,7 @@ const LandingPage = () => {
    - laptop charging + laptop charging
    Project & Task Management

    @@ -309,10 +305,7 @@ const LandingPage = () => {

    - transition up + transition up
    Attendance & Leave Tracking

    @@ -332,10 +325,7 @@ const LandingPage = () => {

    - 3d select solid + 3d select solid
    Expense & Budget Tracking

    @@ -355,10 +345,7 @@ const LandingPage = () => {

    - keyboard + keyboard
    Document Management

    @@ -368,10 +355,7 @@ const LandingPage = () => {

    - keyboard + keyboard
    Service Provider & Subcontractor Tracking @@ -383,10 +367,7 @@ const LandingPage = () => {
    {" "}
    - keyboard + keyboard
    Inventory Management

    @@ -396,10 +377,7 @@ const LandingPage = () => {

    - keyboard + keyboard
    Directory

    @@ -411,10 +389,11 @@ const LandingPage = () => { {/* Useful features: End */} - {/* */} + {/* */}

    {/* Pricing plans: End */} @@ -945,14 +591,14 @@ const LandingPage = () => { FAQ

    - Frequently asked - - questions - + Questions + {/* laptop charging + /> */}

    @@ -990,12 +636,12 @@ const LandingPage = () => { className="accordion-collapse collapse" data-bs-parent="#accordionExample" > -

    - Lemon drops chocolate cake gummies carrot cake chupa - chups muffin topping. Sesame snaps icing marzipan gummi - bears macaroon dragée danish caramels powder. Bear claw - dragée pastry topping soufflé. Wafer gummi bears - marshmallow pastry pie. +
    + A smart Project Management System designed to bring + teams, tasks, and timelines together in one place. With + AI-driven insights, role-based access, and seamless + reporting, it empowers organizations to deliver projects + faster and smarter.
    @@ -1018,12 +664,13 @@ const LandingPage = () => { aria-labelledby="headingTwo" data-bs-parent="#accordionExample" > -
    - Dessert ice cream donut oat cake jelly-o pie sugar plum - cheesecake. Bear claw dragée oat cake dragée ice cream - halvah tootsie roll. Danish cake oat cake pie macaroon - tart donut gummies. Jelly beans candy canes carrot cake. - Fruitcake chocolate chupa chups. +
    + Yes, you have full flexibility to manage your + subscription. You can upgrade to a higher plan to unlock + more features, downgrade to a smaller plan if your needs + change, or cancel your subscription anytime. Plan + changes take effect instantly, and billing adjustments + are applied on a pro-rated basis.
    @@ -1046,17 +693,16 @@ const LandingPage = () => { aria-labelledby="headingThree" data-bs-parent="#accordionExample" > -
    - Regular license can be used for end products that do not - charge users for access or service(access is free and - there will be no monthly subscription fee). Single - regular license can be used for single end product and - end product can be used by you or your client. If you - want to sell end product to multiple clients then you - will need to purchase separate license for each client. - The same rule applies if you want to use the same end - product on multiple domains(unique setup). For more info - on regular license you can check official description. +
    + Security is at the core of Marco PMS. We use + industry-standard encryption (SSL/TLS) to protect data + in transit and advanced encryption to safeguard data at + rest. Role-based access controls ensure that only + authorized users can access sensitive information. Our + system is hosted on secure, cloud-ready infrastructure + with regular backups, monitoring, and compliance with + best practices to keep your data safe and available at + all times.
    @@ -1079,12 +725,12 @@ const LandingPage = () => { aria-labelledby="headingFour" data-bs-parent="#accordionExample" > -
    - Lorem ipsum dolor sit amet consectetur adipisicing elit. - Nobis et aliquid quaerat possimus maxime! Mollitia - reprehenderit neque repellat deleniti delectus - architecto dolorum maxime, blanditiis earum ea, incidunt - quam possimus cumque. +
    + You can reach our support team anytime through the + in-app help center, email, or live chat. We also provide + a detailed knowledge base and FAQs to guide you through + common queries. For personalized assistance, our support + specialists are always ready to help you.
    @@ -1107,15 +753,47 @@ const LandingPage = () => { aria-labelledby="headingFive" data-bs-parent="#accordionExample" > -
    - Lorem ipsum dolor sit amet consectetur, adipisicing - elit. Sequi molestias exercitationem ab cum nemo facere - voluptates veritatis quia, eveniet veniam at et - repudiandae mollitia ipsam quasi labore enim architecto - non! +
    + Marco PMS operate under a proprietary license combined + with a subscription model. This means customers don’t + own the software but are granted the right to access and + use it through the cloud under our Terms of Service. + Depending on the plan, licensing may be based on users, + features, or usage, and you can upgrade, downgrade, or + cancel at any time. non!
    +
    +

    + +

    +
    +
    + Yes, Marco PMS is designed to be flexible and adaptable. + You can customize workflows, user roles, permissions, + and reporting to match your organization’s unique + processes. Depending on your plan, we also support + advanced customization such as integrating with + third-party tools, adding custom fields, and tailoring + modules to fit your business requirements. +
    +
    +
    {" "} @@ -1138,19 +816,26 @@ const LandingPage = () => {
    +
    + hero elements +
    {" "}

    - Let's work - laptop charging + /> */} - together + Together

    Any question or remark? just write us a message @@ -1186,7 +871,7 @@ const LandingPage = () => { href="tel:+1234-568-963" className="text-heading" > - +1234 568 963 + +91 70288 83755

    @@ -1195,15 +880,15 @@ const LandingPage = () => {
    -

    +

    Ready to Get Started? -
    -
    - Start your project with a 14-day free trial
    - +
    + Start your project with a free trial +
    + {/*
    Get Started - {" "} + {" "} */} {
    -
    - hero elements -
    @@ -1378,7 +1056,10 @@ const LandingPage = () => { {/* Footer: Start */}
    -
    +
    -
    +
    -
    + +
    ©{new Date().getFullYear()} @@ -1548,31 +1245,19 @@ const LandingPage = () => { Marco AIoT Technologies Pvt. Ltd.,
    -
    + +
    +
    Download our app
    + facebook icon - - - twitter icon - - - google icon
    diff --git a/src/pages/Home/PlanCardSkeleton.jsx b/src/pages/Home/PlanCardSkeleton.jsx new file mode 100644 index 00000000..837ea686 --- /dev/null +++ b/src/pages/Home/PlanCardSkeleton.jsx @@ -0,0 +1,44 @@ +import React from "react"; + +const SubscriptionPlanSkeleton = () => { + return ( +
    +
    + {/* Header */} +
    +
    +
    +
    +
    + + {/* Price */} +
    +
    +
    + + {/* Storage & Trial */} +
    +
    +
    +
    + + {/* Features */} +
    + Features +
    +
      + {[1, 2, 3].map((i) => ( +
    • +
      +
    • + ))} +
    + + {/* Button */} +
    +
    +
    + ); +}; + +export default SubscriptionPlanSkeleton; diff --git a/src/pages/Home/SubscriptionPlans.jsx b/src/pages/Home/SubscriptionPlans.jsx new file mode 100644 index 00000000..24be62f9 --- /dev/null +++ b/src/pages/Home/SubscriptionPlans.jsx @@ -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 ( +
    + {/* Frequency Switcher */} +
    +
    + {["Monthly", "Quarterly", "Half-Yearly", "Yearly"].map((label, idx) => ( + + ))} +
    +
    + + {/* Cards */} +
    + {loading ? ( + // Show 3 skeletons + <> + + + + + ) : plans.length === 0 ? ( +
    No plans found
    + ) : ( + plans.map((plan) => ( +
    +
    + {/* Header */} +
    + +

    {plan.planName}

    +

    {plan.description}

    +
    + + {/* Price */} +
    +

    + {plan.currency?.symbol} {plan.price} + / {frequencyLabel(frequency)} +

    +
    + + {/* Storage & Trial */} +
    +
    + + Storage {plan.maxStorage} MB +
    +
    + + Trial Days {plan.trialDays} +
    +
    + + {/* Features */} +
    + Features +
    +
      + {plan.features?.modules && + Object.values(plan.features.modules).map((mod) => + mod && mod.name ? ( +
    • + {mod.enabled ? ( + + ) : ( + + )} + {mod.name} +
    • + ) : null + )} +
    + + {/* Button */} +
    + + Request a Demo + +
    +
    +
    + )) + )} +
    + +
    + ); +}; + +export default SubscriptionPlans; diff --git a/src/pages/Tenant/TenantPage.jsx b/src/pages/Tenant/TenantPage.jsx index f93163d9..ef33efbf 100644 --- a/src/pages/Tenant/TenantPage.jsx +++ b/src/pages/Tenant/TenantPage.jsx @@ -121,66 +121,67 @@ const TenantPage = () => { { label: "Tenant", link: null }, ]} /> +
    + {/* Super Tenant Actions */} + {isSuperTenant && ( +
    +
    + {/* Search */} +
    + setSearchText(e.target.value)} + className="form-control form-control" + placeholder="Search Tenant" + /> +
    - {/* Super Tenant Actions */} - {isSuperTenant && ( -
    -
    - {/* Search */} -
    - setSearchText(e.target.value)} - className="form-control form-control-sm" - placeholder="Search Tenant" - /> -
    + {/* Actions */} +
    + refetchFn && refetchFn()} + > + Refresh{" "} + + - {/* Actions */} -
    - refetchFn && refetchFn()} - > - Refresh{" "} - - - - + +
    -
    - )} + )} - {/* Tenant List or Access Denied */} - {isSuperTenant ? ( - - ) : !isSelfTenant ? ( -
    - -

    - Access Denied: You don't have permission to perform this action! -

    -
    - ) : null} + {/* Tenant List or Access Denied */} + {isSuperTenant ? ( + + ) : !isSelfTenant ? ( +
    + +

    + Access Denied: You don't have permission to perform this action! +

    +
    + ) : null} +
    ); diff --git a/src/pages/authentication/LoginPage.jsx b/src/pages/authentication/LoginPage.jsx index db99e6a1..3e331376 100644 --- a/src/pages/authentication/LoginPage.jsx +++ b/src/pages/authentication/LoginPage.jsx @@ -136,7 +136,7 @@ const LoginPage = () => { )}
    */} -
    +
    @@ -146,7 +146,8 @@ const LoginPage = () => { type={hidepass ? "password" : "text"} autoComplete="new-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" {...register("password")} placeholder="••••••••••••" @@ -155,7 +156,7 @@ const LoginPage = () => {
    + + {/* ✅ Error message */} + {errors.password && ( +
    + {errors.password.message} +
    + )}
    + {/* Remember Me + Forgot Password */}
    diff --git a/src/pages/employee/EmployeeList.jsx b/src/pages/employee/EmployeeList.jsx index a7d6b918..648b736b 100644 --- a/src/pages/employee/EmployeeList.jsx +++ b/src/pages/employee/EmployeeList.jsx @@ -176,10 +176,12 @@ const EmployeeList = () => { useEffect(() => { if (!loading && Array.isArray(employees)) { const sorted = [...employees].sort((a, b) => { - const nameA = `${a.firstName || ""}${a.middleName || ""}${a.lastName || "" - }`.toLowerCase(); - const nameB = `${b.firstName || ""}${b.middleName || ""}${b.lastName || "" - }`.toLowerCase(); + const nameA = `${a.firstName || ""}${a.middleName || ""}${ + a.lastName || "" + }`.toLowerCase(); + const nameB = `${b.firstName || ""}${b.middleName || ""}${ + b.lastName || "" + }`.toLowerCase(); return nameA?.localeCompare(nameB); }); @@ -266,8 +268,9 @@ const EmployeeList = () => { ? "Suspend Employee" : "Reactivate Employee" } - message={`Are you sure you want to ${selectedEmpFordelete?.isActive ? "suspend" : "reactivate" - } this employee?`} + message={`Are you sure you want to ${ + selectedEmpFordelete?.isActive ? "suspend" : "reactivate" + } this employee?`} onSubmit={(id) => suspendEmployee({ employeeId: id, @@ -291,11 +294,11 @@ const EmployeeList = () => { {ViewTeamMember ? ( //
    -
    +
    {/* Switches: All Employees + Inactive */} @@ -315,7 +318,7 @@ const EmployeeList = () => { className="form-check-label ms-0" htmlFor="allEmployeesCheckbox" > - All Employees + Show All Employees
    )} @@ -351,7 +354,7 @@ const EmployeeList = () => { value={searchText} onChange={handleSearch} className="form-control form-control-sm" - placeholder="Search User" + placeholder="Search Employee" aria-controls="DataTables_Table_0" /> @@ -499,8 +502,9 @@ const EmployeeList = () => { Status
    ) : null} {!loading && - displayData?.length === 0 && - (!searchText || showAllEmployees) ? ( + displayData?.length === 0 && + (!searchText || showAllEmployees) ? ( @@ -627,7 +631,9 @@ const EmployeeList = () => {
    {/* View always visible */} {/* Suspend only when active */} @@ -649,7 +658,8 @@ const EmployeeList = () => { className="dropdown-item py-1" onClick={() => handleOpenDelete(item)} > - Suspend + {" "} + Suspend )} @@ -658,11 +668,13 @@ const EmployeeList = () => { type="button" data-bs-toggle="modal" data-bs-target="#managerole-modal" - onClick={() => setEmpForManageRole(item.id)} + onClick={() => + setEmpForManageRole(item.id) + } > - Manage Role + {" "} + Manage Role - )} @@ -672,7 +684,8 @@ const EmployeeList = () => { className="dropdown-item py-1" onClick={() => handleOpenDelete(item)} > - Re-activate + {" "} + Re-activate )}
    @@ -691,8 +704,9 @@ const EmployeeList = () => {
    { )} {/* Conditional messages for no data or no search results */} {!loading && - displayData?.length === 0 && - searchText && - !showAllEmployees ? ( + displayData?.length === 0 && + searchText && + !showAllEmployees ? (
    @@ -532,8 +536,8 @@ const EmployeeList = () => {
    { ) : ( - NA + - )}