Merge branch 'main' of https://git.marcoaiot.com/admin/marco.pms.web into Image_Gallery_filter

This commit is contained in:
Kartik Sharma 2025-09-18 14:51:24 +05:30
commit 9d5c33eeff
12 changed files with 331 additions and 346 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 ManageInfra = useHasUserPermission( MANAGE_PROJECT_INFRA ) const canApproveTask = useHasUserPermission(APPROVE_TASK);
const ApprovedTaskRights = useHasUserPermission(APPROVE_TASK) const canReportTask = useHasUserPermission(ASSIGN_REPORT_TASK);
const ReportTaskRights = useHasUserPermission(ASSIGN_REPORT_TASK)
const reloadedData = useSelector( ( store ) => store.localVariables.reload )
const reloadedData = useSelector((store) => store.localVariables.reload);
// useEffect( () =>
// {
// if (reloadedData)
// {
// refetch()
// dispatch( refreshData( false ) )
// }
// },[reloadedData]) const hasAccess = canManageInfra || canApproveTask || canReportTask;
if (isError) {
return <div>{error?.response?.data?.message || error?.message}</div>;
}
if (!hasAccess && !isLoading) {
return (
<div className="text-center">
<i className="fa-solid fa-triangle-exclamation fs-5"></i>
<p>Access Denied: You don't have permission to perform this action.</p>
</div>
);
}
if (isLoading) {
return <Loader />;
}
if (isFetched && (!projectInfra || projectInfra.length === 0)) {
return (
<div className="card text-center">
<p className="my-3">No Result Found</p>
</div>
);
}
return ( return (
<div className="col-md-12 col-lg-12 col-xl-12 order-0 mb-4"> <div className="col-md-12 col-lg-12 col-xl-12 order-0 mb-4">
<div className="card"> <div className="card">
<div className="card-body" style={{ padding: "0.5rem" }}> <div className="card-body" style={{ padding: "0.5rem" }}>
{(ApprovedTaskRights || ReportTaskRights) ? ( <div className="row">
<div className="align-items-center"> <InfraTable buildings={projectInfra} projectId={selectedProject} />
<div className="row ">
{isLoading && (<Loader/> )}
{( !isLoading && projectInfra?.length === 0 ) && ( <p>No Result Found</p> )}
{(!isLoading && projectInfra?.length > 0) && (<InfraTable buildings={projectInfra} projectId={selectedProject}/>)}
</div> </div>
</div> </div>
) : (
<div className="text-center">
<i className="fa-solid fa-triangle-exclamation fs-5"></i>
<p>Access Denied: You don't have permission to perform this action. !</p>
</div>
)}
</div> </div>
</div> </div>
</div>
); );
}; };
export default InfraPlanning; export default InfraPlanning;

View File

@ -28,6 +28,7 @@ const ProjectPermission = () => {
handleSubmit, handleSubmit,
reset, reset,
control, control,
setValue,
formState: { errors, isDirty }, formState: { errors, isDirty },
} = useForm({ } = useForm({
resolver: zodResolver(ProjectPermissionSchema), resolver: zodResolver(ProjectPermissionSchema),
@ -45,18 +46,26 @@ const ProjectPermission = () => {
); );
useEffect(() => { useEffect(() => {
if (!selectedEmployee) return; if (!selectedEmployee) return;
const enabledPerms = const enabledPerms =
selectedEmpPermissions?.permissions selectedEmpPermissions?.permissions
?.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,
})); const selectedPermissions = watch("selectedPermissions") || [];
}, [selectedEmpPermissions, reset, selectedEmployee]);
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();
@ -145,7 +154,7 @@ const ProjectPermission = () => {
</div> </div>
<div className="mt-3 text-end"> <div className="mt-3 text-end">
{isDirty && ( {hasChanges && (
<button <button
type="submit" type="submit"
className="btn btn-sm btn-primary" className="btn btn-sm btn-primary"

View File

@ -1,16 +1,26 @@
import { useSelectedProject } from "../slices/apiDataManager"; import { useSelectedProject } from "../slices/apiDataManager";
import { useAllProjectLevelPermissions, useProfile } from "./useProfile"; import { useAllProjectLevelPermissions, useProfile } from "./useProfile";
export const useHasUserPermission = (permission) => { export const useHasUserPermission = (permission) => {
const selectedProject = useSelectedProject(); const selectedProject = useSelectedProject();
const { profile } = useProfile(); const { profile } = useProfile();
const { data: projectPermissions = [], isLoading, isError } = useAllProjectLevelPermissions(selectedProject); const {
data: projectPermissions = [],
isLoading,
isError,
} = useAllProjectLevelPermissions(selectedProject);
if (isLoading || !permission) return false; if (isLoading || !permission) return false;
const globalPerms = profile?.featurePermissions ?? []; const globalPerms = profile?.featurePermissions ?? [];
const projectPerms = projectPermissions ?? []; const projectPerms = projectPermissions ?? [];
if (selectedProject) {
return globalPerms.includes(permission) || projectPerms.includes(permission); if (projectPerms.length === 0) {
return projectPerms.includes(permission);
} else {
return projectPerms.includes(permission);
}
} else {
return globalPerms.includes(permission);
}
}; };

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

@ -21,6 +21,7 @@ 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 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"));

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

@ -591,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">
@ -816,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
@ -873,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"
@ -890,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>
@ -1056,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"
@ -1193,7 +1196,7 @@ 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 src="/img/icons/apple-icon.png" alt="apple icon" /> <img src="/img/icons/apple-icon.png" alt="apple icon" />
</a> </a>
<a <a
@ -1209,21 +1212,9 @@ 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">
<span className="footer-bottom-text me-1">
©{new Date().getFullYear()}
</span>
<a
href="https://marcoaiot.com"
target="_blank"
className="text-white"
>
Marco AIoT Technologies Pvt. Ltd.,
</a>
</div>
<div>
<a <a
href="https://www.facebook.com/marcoaiot/" href="https://www.facebook.com/marcoaiot/"
className="me-4" className="me-4"
@ -1242,6 +1233,34 @@ const LandingPage = () => {
<img src="/img/icons/instagram.svg" alt="google icon" /> <img src="/img/icons/instagram.svg" alt="google icon" />
</a> </a>
</div> </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">
©{new Date().getFullYear()}
</span>
<a
href="https://marcoaiot.com"
target="_blank"
className="text-white"
>
Marco AIoT Technologies Pvt. Ltd.,
</a>
</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
href="https://play.google.com/store/apps/details?id=com.marco.aiotstage&pcampaignid=web_share"
target="_blank"
>
<img
src="/img/icons/google-play-icon.png"
alt="google play icon"
/>
</a>
</div>
</div> </div>
</div> </div>
</footer> </footer>

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

@ -1,115 +1,7 @@
// // src/components/SubscriptionPlans.jsx
// import React, { useState, useEffect } from "react";
// import axios from "axios";
// const SubscriptionPlans = () => {
// const [plans, setPlans] = useState([]);
// const [frequency, setFrequency] = useState(1); // 1=Monthly, 2=Quarterly, 3=Half-Yearly, 4=Yearly
// const [loading, setLoading] = useState(false);
// useEffect(() => {
// const fetchPlans = async () => {
// try {
// setLoading(true);
// const res = await axios.get(
// `/api/market/list/subscription-plan?frequency=${frequency}`
// );
// setPlans(res.data?.data || []);
// } catch (err) {
// console.error("Error fetching plans:", err);
// } finally {
// setLoading(false);
// }
// };
// fetchPlans();
// }, [frequency]);
// 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 + 1 ? "primary" : "outline-secondary"}`}
// onClick={() => setFrequency(idx + 1)}
// >
// {label}
// </button>
// ))}
// </div>
// </div>
// {/* Cards */}
// <div className="row g-4">
// {loading ? (
// <div className="text-center">Loading...</div>
// ) : 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-sm">
// <div className="card-header text-center">
// <h4 className="mb-1">{plan.planName}</h4>
// <p className="text-muted small">{plan.description}</p>
// <div className="d-flex align-items-center justify-content-center">
// <span className="h2 text-primary fw-bold mb-0">
// {plan.currency?.symbol}
// {plan.price}
// </span>
// <sub className="h6 text-muted mb-n1 ms-1">/mo</sub>
// </div>
// <div className="text-muted">
// Max Users: {plan.maxUser} | Storage: {plan.maxStorage} MB
// </div>
// </div>
// <div className="card-body">
// <ul className="list-unstyled">
// {plan.features?.modules &&
// Object.values(plan.features.modules).map((mod) => (
// <li key={mod.id} className="mb-2">
// <i className="bx bx-check text-success me-2"></i>
// {mod.name}{" "}
// {mod.enabled ? (
// <span className="badge bg-success ms-2">Enabled</span>
// ) : (
// <span className="badge bg-secondary ms-2">Disabled</span>
// )}
// </li>
// ))}
// <li className="mb-2">
// <i className="bx bx-support text-primary me-2"></i>
// Support:{" "}
// {plan.features?.supports?.prioritySupport
// ? "Priority"
// : plan.features?.supports?.phoneSupport
// ? "Phone & Email"
// : "Email Only"}
// </li>
// </ul>
// </div>
// <div className="card-footer text-center">
// <button className="btn btn-primary w-100">Get Started</button>
// </div>
// </div>
// </div>
// ))
// )}
// </div>
// </div>
// );
// };
// export default SubscriptionPlans;
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import axios from "axios"; import axios from "axios";
import { Link } from "react-router-dom";
import PlanCardSkeleton from "./PlanCardSkeleton";
const SubscriptionPlans = () => { const SubscriptionPlans = () => {
const [plans, setPlans] = useState([]); const [plans, setPlans] = useState([]);
@ -120,11 +12,10 @@ const SubscriptionPlans = () => {
const fetchPlans = async () => { const fetchPlans = async () => {
try { try {
setLoading(true); setLoading(true);
const res = await axios.get(`http://localhost:5032/api/market/list/subscription-plan?frequency=${frequency}`, { const res = await axios.get(
headers: { `http://localhost:5032/api/market/list/subscription-plan?frequency=${frequency}`,
"Content-Type": "application/json" { headers: { "Content-Type": "application/json" } }
} );
});
setPlans(res.data?.data || []); setPlans(res.data?.data || []);
} catch (err) { } catch (err) {
console.error("Error fetching plans:", err); console.error("Error fetching plans:", err);
@ -137,10 +28,10 @@ const SubscriptionPlans = () => {
const frequencyLabel = (freq) => { const frequencyLabel = (freq) => {
switch (freq) { switch (freq) {
case 0: return "mo"; case 0: return "1 mo";
case 1: return "3mo"; case 1: return "3 mo";
case 2: return "6mo"; case 2: return "6 mo";
case 3: return "yr"; case 3: return "1 yr";
default: return "mo"; default: return "mo";
} }
}; };
@ -155,7 +46,7 @@ const SubscriptionPlans = () => {
key={idx} key={idx}
type="button" type="button"
className={`btn btn-${frequency === idx ? "primary" : "outline-secondary"}`} className={`btn btn-${frequency === idx ? "primary" : "outline-secondary"}`}
onClick={() => setFrequency(idx)} // use idx directly (0,1,2,3) onClick={() => setFrequency(idx)}
> >
{label} {label}
</button> </button>
@ -163,73 +54,88 @@ const SubscriptionPlans = () => {
</div> </div>
</div> </div>
{/* Cards */} {/* Cards */}
<div className="row g-4"> <div className="row g-4 mt-10">
{loading ? ( {loading ? (
<div className="text-center">Loading...</div> // Show 3 skeletons
<>
<PlanCardSkeleton />
<PlanCardSkeleton />
<PlanCardSkeleton />
</>
) : plans.length === 0 ? ( ) : plans.length === 0 ? (
<div className="text-center">No plans found</div> <div className="text-center">No plans found</div>
) : ( ) : (
plans.map((plan) => ( plans.map((plan) => (
<div key={plan.id} className="col-xl-4 col-lg-6 col-md-6"> <div key={plan.id} className="col-xl-4 col-lg-6 col-md-6">
<div className="card h-100 shadow-sm"> <div className="card h-100 shadow-lg border-0 p-3 text-center p-10">
<div className="card-header text-center"> {/* Header */}
<h4 className="mb-1">{plan.planName}</h4> <div className="mb-3">
<p className="text-muted small">{plan.description}</p> <i className="bx bxs-package text-primary fs-1 mb-2"></i>
<div className="d-flex align-items-center justify-content-center"> <p className="card-title fs-3 fw-bold mb-1">{plan.planName}</p>
<span className="h2 text-primary fw-bold mb-0"> <p className="text-muted mb-0 fs-5">{plan.description}</p>
{plan.currency?.symbol}{plan.price} </div>
</span>
<sub className="h6 text-muted mb-n1 ms-1">/{frequencyLabel(frequency)}</sub> {/* 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>
<div className="text-muted"> <div>
Max Users: {plan.maxUser} | Storage: {plan.maxStorage} MB <i className="fa-regular fa-calendar-check text-success me-2"></i>
Trial Days {plan.trialDays}
</div> </div>
</div> </div>
<div className="card-body"> {/* Features */}
<ul className="list-unstyled"> <h6 className="fw-bold text-uppercase border-top pt-3 mb-3 text-center">
{plan.features?.modules && Features
Object.values(plan.features.modules).map((modGroup) => </h6>
Object.values(modGroup).map((mod) => <ul className="list-unstyled text-start mb-4 ms-7 fs-5">
mod && mod.name ? ( {plan.features?.modules &&
<li key={mod.id} className="mb-2"> Object.values(plan.features.modules).map((mod) =>
<i className="bx bx-check text-success me-2"></i> mod && mod.name ? (
{mod.name}{" "} <li
{mod.enabled ? ( key={mod.id}
<span className="badge bg-success ms-2">Enabled</span> className="d-flex align-items-center mb-4"
) : ( >
<span className="badge bg-secondary ms-2">Disabled</span> {mod.enabled ? (
)} <i className="fa-regular fa-circle-check text-success me-2"></i>
</li> ) : (
) : null <i className="fa-regular fa-circle-xmark text-danger me-2"></i>
) )}
)} {mod.name}
</li>
) : null
)}
</ul>
<li className="mb-2"> {/* Button */}
<i className="bx bx-support text-primary me-2"></i> <div className="mt-auto">
Support:{" "} <Link
{plan.features?.supports?.prioritySupport to="/auth/reqest/demo"
? "Priority" className="btn btn-outline-primary w-100 fw-bold"
: plan.features?.supports?.phoneSupport >
? "Phone & Email" Request a Demo
: "Email Only"} </Link>
</li>
</ul>
</div>
<div className="card-footer text-center">
<button className="btn btn-primary w-100">Get Started</button>
</div> </div>
</div> </div>
</div> </div>
)) ))
)} )}
</div> </div>
</div> </div>
); );
}; };
export default SubscriptionPlans; export default SubscriptionPlans;

View File

@ -136,7 +136,7 @@ const LoginPage = () => {
)} )}
</div> */} </div> */}
<div className="mb-3 form-password-toggle text-start"> <div className="mb-3 form-password-toggle text-start">
<label htmlFor="password" className="form-label"> <label htmlFor="password" className="form-label">
Password Password
</label> </label>
@ -146,7 +146,8 @@ const LoginPage = () => {
type={hidepass ? "password" : "text"} type={hidepass ? "password" : "text"}
autoComplete="new-password" autoComplete="new-password"
id="password" id="password"
className="form-control form-control-xl shadow-none" className={`form-control form-control-xl shadow-none ${errors.password ? "is-invalid" : ""
}`}
name="password" name="password"
{...register("password")} {...register("password")}
placeholder="••••••••••••" placeholder="••••••••••••"
@ -155,7 +156,7 @@ const LoginPage = () => {
<span className="input-group-text cursor-pointer border-start-0"> <span className="input-group-text cursor-pointer border-start-0">
<button <button
type="button" type="button"
className="btn btn-link p-0" className="btn btn-link-secondary p-0"
onClick={() => setHidepass(!hidepass)} onClick={() => setHidepass(!hidepass)}
> >
{hidepass ? ( {hidepass ? (
@ -166,8 +167,16 @@ const LoginPage = () => {
</button> </button>
</span> </span>
</div> </div>
{/* ✅ Error message */}
{errors.password && (
<div className="invalid-feedback text-start" style={{ fontSize: "12px" }}>
{errors.password.message}
</div>
)}
</div> </div>
{/* Remember Me + Forgot Password */} {/* Remember Me + Forgot Password */}
<div className="mb-3 d-flex justify-content-between align-items-center"> <div className="mb-3 d-flex justify-content-between align-items-center">
<div className="form-check"> <div className="form-check">

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,63 +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 { useHasAnyPermission } from "../../hooks/useExpense"; import { useProjectAccess } from "../../hooks/useProjectAccess"; // new
import { VIEW_PROJECTS } from "../../utils/constants";
import { useNavigate, useRoutes } from "react-router-dom"; import "./ProjectDetails.css";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
const ProjectDetails = () => { const ProjectDetails = () => {
const projectId = useSelectedProject(); const projectId = useSelectedProject();
const CanViewProject = useHasUserPermission(VIEW_PROJECTS);
const navigate = useNavigate();
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 (!CanViewProject) { if (!projectId && projectNames.length > 0) {
navigate("/dashboard"); dispatch(setProjectId(projectNames[0].id));
} }
if (projectId == null) { }, [projectId, projectNames, dispatch]);
dispatch(setProjectId(projectNames[0]?.id));
}
}, [projectNames]);
const { const [activePill, setActivePill] = useState(
projects_Details, localStorage.getItem("lastActiveProjectTab") || "profile"
loading: projectLoading, );
error: projectError,
refetch,
} = useProjectDetails(projectId);
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();
} }
}, },
@ -79,69 +60,42 @@ const ProjectDetails = () => {
const handlePillClick = (pillKey) => { const handlePillClick = (pillKey) => {
setActivePill(pillKey); setActivePill(pillKey);
localStorage.setItem("lastActiveProjectTab", pillKey); localStorage.setItem("lastActiveProjectTab", pillKey);
}; };
const renderContent = () => { if (projectLoading || permsLoading || !projects_Details) {
if (projectLoading || !projects_Details) return <Loader />; return <Loader />;
}
const renderContent = () => {
switch (activePill) { switch (activePill) {
case "profile": case "profile":
return (
<>
<div className="row">
<div className="col-lg-4 col-md-5 mt-2">
<AboutProject></AboutProject>
<ProjectOverview project={projectId} />
</div>
<div className="col-lg-8 col-md-7 mt-5">
<ProjectProgressChart
ShowAllProject="false"
DefaultRange="1M"
/>
<div className="mt-5">
{" "}
<AttendanceOverview />
</div>
</div>
</div>
</>
);
case "teams":
return ( return (
<div className="row"> <div className="row">
<div className="col-lg-12"> <div className="col-lg-4 col-md-5 mt-2">
<Teams /> <AboutProject />
<ProjectOverview project={projectId} />
</div>
<div className="col-lg-8 col-md-7 mt-5">
<ProjectProgressChart ShowAllProject="false" DefaultRange="1M" />
<div className="mt-5">
<AttendanceOverview />
</div>
</div> </div>
</div> </div>
); );
case "teams":
return <Teams />;
case "infra": case "infra":
return <ProjectInfra data={projects_Details} onDataChange={refetch} />; return <ProjectInfra data={projects_Details} onDataChange={refetch} />;
case "workplan": case "workplan":
return <WorkPlan data={projects_Details} onDataChange={refetch} />; return <WorkPlan data={projects_Details} onDataChange={refetch} />;
case "directory": case "directory":
return ( return <DirectoryPage IsPage={false} projectId={projects_Details.id} />;
<div className="row mt-2">
<DirectoryPage IsPage={false} projectId={projects_Details.id} />
</div>
);
case "documents": case "documents":
return ( return <ProjectDocuments />;
<div className="row">
<ProjectDocuments />
</div>
);
case "setting": case "setting":
return ( return <ProjectSetting />;
<div className="row">
<ProjectSetting />
</div>
);
default: default:
return <ComingSoonPage />; return <ComingSoonPage />;
} }
@ -156,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>