added self Tenant Profile

This commit is contained in:
pramod mahajan 2025-08-22 15:16:51 +05:30
parent 735e766215
commit 4ab33fd5fd
11 changed files with 331 additions and 184 deletions

View File

@ -5,9 +5,12 @@ import GlobalModel from "../common/GlobalModel";
import { useTenantContext } from "../../pages/Tenant/TenantPage"; import { useTenantContext } from "../../pages/Tenant/TenantPage";
import { useTenantDetailsContext } from "../../pages/Tenant/TenantDetails"; import { useTenantDetailsContext } from "../../pages/Tenant/TenantDetails";
import IconButton from "../common/IconButton"; import IconButton from "../common/IconButton";
import { hasUserPermission } from "../../utils/authUtils";
import { MANAGE_TENANTS } from "../../utils/constants";
const Profile = ({ data }) => { const Profile = ({ data }) => {
const {setEditTenant} = useTenantDetailsContext() const {setEditTenant} = useTenantDetailsContext()
const canUpdateTenant = hasUserPermission(MANAGE_TENANTS)
return ( return (
<> <>
<div className="container-fuid"> <div className="container-fuid">
@ -33,12 +36,12 @@ const Profile = ({ data }) => {
<i className="bx bx-globe text-primary bx-xs me-1"></i> <i className="bx bx-globe text-primary bx-xs me-1"></i>
<span>{data?.domainName}</span> <span>{data?.domainName}</span>
</div> </div>
<span {canUpdateTenant && ( <span
className="position-absolute top-0 end-0 cursor-auto" className="position-absolute top-0 end-0 cursor-auto"
onClick={() => setEditTenant(true)} onClick={() => setEditTenant(true)}
> >
<i className="bx bx-edit bs-sm text-primary cursor-pointer"></i> <i className="bx bx-edit bs-sm text-primary cursor-pointer"></i>
</span> </span>)}
</div> </div>
</div> </div>
</div> </div>

View File

@ -4,7 +4,9 @@ import { useDispatch } from "react-redux";
import { setCurrentTenant } from "../../slices/globalVariablesSlice"; import { setCurrentTenant } from "../../slices/globalVariablesSlice";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { formatUTCToLocalTime } from "../../utils/dateUtils"; import { formatUTCToLocalTime } from "../../utils/dateUtils";
import { SUBSCRIPTION_PLAN_FREQUENCIES } from "../../utils/constants"; import { MANAGE_TENANTS, SUBSCRIPTION_PLAN_FREQUENCIES } from "../../utils/constants";
import { useHasAnyPermission } from "../../hooks/useExpense";
const SubScriptionHistory = ({ tenantId }) => { const SubScriptionHistory = ({ tenantId }) => {
const { data, isLoading, isError, error } = useTenantDetails(tenantId); const { data, isLoading, isError, error } = useTenantDetails(tenantId);

View File

@ -63,7 +63,7 @@ export const subscriptionSchema = z.object({
.min(1, "Team size is required and must be greater than zero"), .min(1, "Team size is required and must be greater than zero"),
frequency: z frequency: z
.number({ invalid_type_error: "Frequency must be a number" }) .number({ invalid_type_error: "Frequency must be a number" })
.min(1, "Frequency must be at least 1"), .min(0, "Frequency must be at least 1"),
isTrial: z.boolean(), isTrial: z.boolean(),
autoRenew: z.boolean(), autoRenew: z.boolean(),
}); });

View File

@ -81,7 +81,7 @@
{ {
"text": "Tenant", "text": "Tenant",
"available": true, "available": true,
"link": "/tenants/" "link": "/tenants"
}, },
{ {
"text": "Masters", "text": "Masters",

View File

@ -55,6 +55,7 @@ export const useTenantDetails = (id) => {
const response = await TenantRepository.getTenantDetails(id); const response = await TenantRepository.getTenantDetails(id);
return response.data; return response.data;
}, },
enabled:!!id
}); });
}; };

View File

@ -0,0 +1,35 @@
import React, { useEffect, useMemo } from "react";
import { useProfile } from "../../hooks/useProfile";
import TenantDetails from "./TenantDetails";
import { hasUserPermission } from "../../utils/authUtils";
import { VIEW_TENANTS } from "../../utils/constants";
import { useNavigate } from "react-router-dom";
const SelfTenantDetails = () => {
const { profile, loading } = useProfile();
const tenantId = profile?.employeeInfo?.tenantId;
const navigate = useNavigate();
const isSelfTenantView = hasUserPermission(VIEW_TENANTS);
useEffect(() => {
if (!isSelfTenantView) {
navigate("/tenants");
}
}, [isSelfTenantView, navigate]);
if (loading || !tenantId) {
return <div>Loading...</div>;
}
return (
<TenantDetails
tenantId={tenantId}
wrapInContainer={true}
showBreadcrumb={true}
iTSelf={true}
/>
);
};
export default SelfTenantDetails;

View File

@ -0,0 +1,8 @@
import React from "react";
import TenantDetails from "./TenantDetails";
const SuperTenantDetails = () => {
return <TenantDetails wrapInContainer showBreadcrumb iTSelf={false} />;
};
export default SuperTenantDetails;

View File

@ -1,10 +1,8 @@
import React, { createContext, useContext,useState } from "react"; import React, { createContext, useContext, useState, useMemo } from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import Breadcrumb from "../../components/common/Breadcrumb"; import Breadcrumb from "../../components/common/Breadcrumb";
import Profile from "../../components/Tenant/Profile"; import Profile from "../../components/Tenant/Profile";
import { useTenantDetails } from "../../hooks/useTenant"; import { useTenantDetails } from "../../hooks/useTenant";
import Organization from "../../components/Tenant/Organization";
import { ComingSoonPage } from "../Misc/ComingSoonPage"; import { ComingSoonPage } from "../Misc/ComingSoonPage";
import GlobalModel from "../../components/common/GlobalModel"; import GlobalModel from "../../components/common/GlobalModel";
import EditProfile from "../../components/Tenant/EditProfile"; import EditProfile from "../../components/Tenant/EditProfile";
@ -13,109 +11,142 @@ import SubScriptionHistory from "../../components/Tenant/SubScriptionHistory";
const TenantDetailsContext = createContext(); const TenantDetailsContext = createContext();
export const useTenantDetailsContext = () => useContext(TenantDetailsContext); export const useTenantDetailsContext = () => useContext(TenantDetailsContext);
const TenantDetails = ({
const TenantDetails = () => { tenantId: tenantIdProp,
const { tenantId } = useParams(); wrapInContainer = true,
const { data, isLoading, isError, error } = useTenantDetails(tenantId); showBreadcrumb = true,
iTSelf = true
}) => {
const { tenantId: tenantIdFromUrl } = useParams();
const activeTenantId = tenantIdFromUrl || tenantIdProp;
const { data, isLoading, isError, error } = useTenantDetails(activeTenantId);
const [editTenant, setEditTenant] = useState(false); const [editTenant, setEditTenant] = useState(false);
const contextValues ={ const contextValues = useMemo(
setEditTenant,editTenant () => ({ editTenant, setEditTenant }),
} [editTenant]
);
const tabs = [ const tabs = useMemo(
{ () => [
id: "navs-left-home", {
label: "Profile", id: "navs-left-home",
icon: "bx bx-user-circle", label: "Profile",
iconSize: "bx-sm", icon: "bx bx-user-circle",
content: <Profile data={data} />, iconSize: "bx-sm",
}, content: <Profile data={data} />,
},
{ {
id: "navs-left-bill", id: "navs-left-bill",
label: "Bills and Plan ", label: "Bills and Plan",
icon: "bx bx-receipt", icon: "bx bx-receipt",
iconSize: "bx-sm", iconSize: "bx-sm",
content: ( content: (
<div className="text-center"> <div className="text-center">
<SubScriptionHistory tenantId={tenantId} /> <SubScriptionHistory tenantId={activeTenantId} />
</div> </div>
), ),
}, },
{
{ id: "navs-left-messages",
id: "navs-left-messages", label: "Messages",
label: "Messages", icon: "bx bx-message-rounded",
icon: "bx bx-message-rounded", iconSize: "bx-sm",
iconSize: "bx-sm", content: (
content: ( <div className="text-center">
<div className="text-center"> <ComingSoonPage />
<ComingSoonPage /> </div>
</div> ),
), },
}, ],
]; [data, activeTenantId]
);
if (!activeTenantId) return <div className="my-4">No tenant selected.</div>;
if (isLoading) return <div className="my-4">Loading...</div>; if (isLoading) return <div className="my-4">Loading...</div>;
if (isError) return <div className="my-3">{error.message}</div>; if (isError) return <div className="my-3">{error?.message}</div>;
const Shell = ({ children }) =>
wrapInContainer ? (
<div className="container-fluid py-0">{children}</div>
) : (
<>{children}</>
);
return ( return (
<> <>
<TenantDetailsContext.Provider value={contextValues}> <TenantDetailsContext.Provider value={contextValues}>
<div className="container-fluid py-0"> <Shell>
<Breadcrumb {showBreadcrumb && (
data={[ <Breadcrumb
{ label: "Home", link: "/dashboard" }, data={
{ label: "Tenant", link: "/tenants" }, iTSelf
{ label: "Tenant Details", link: null }, ? [
]} { label: "Home", link: "/dashboard" },
/> { label: "Tenant Details", link: null },
]
: [
{ label: "Home", link: "/dashboard" },
{ label: "Tenant", link: "/tenants" },
{ label: "Tenant Details", link: null },
]
}
/>
)}
<div className="nav-align-left nav-tabs-shadow mb-6"> <div className="nav-align-left nav-tabs-shadow mb-6">
<ul className="nav nav-tabs py-2 page-min-h" role="tablist"> <ul className="nav nav-tabs py-2 page-min-h" role="tablist">
{tabs.map((tab, index) => ( {tabs.map((tab, index) => (
<li key={tab.id} className="nav-item"> <li key={tab.id} className="nav-item">
<button <button
type="button" type="button"
className={`nav-link d-flex align-items-center text-tiny gap-2 ${ className={`nav-link d-flex align-items-center text-tiny gap-2 ${
index === 0 ? "active" : "" index === 0 ? "active" : ""
}`} }`}
role="tab" role="tab"
data-bs-toggle="tab" data-bs-toggle="tab"
data-bs-target={`#${tab.id}`} data-bs-target={`#${tab.id}`}
aria-controls={tab.id} aria-controls={tab.id}
aria-selected={index === 0} aria-selected={index === 0}
> >
{tab.icon && <i className={`${tab.icon} ${tab.iconSize}`} />} {tab.icon && (
{tab.label} <i className={`${tab.icon} ${tab.iconSize}`} />
</button> )}
</li> {tab.label}
))} </button>
</ul> </li>
))}
</ul>
<div className="tab-content"> <div className="tab-content">
{tabs.map((tab, index) => ( {tabs.map((tab, index) => (
<div <div
key={tab.id} key={tab.id}
className={`tab-pane fade ${ className={`tab-pane fade ${
index === 0 ? "show active" : "" index === 0 ? "show active" : ""
} text-start`} } text-start`}
id={tab.id} id={tab.id}
> >
{tab.content} {tab.content}
</div>
))}
</div> </div>
))} </div>
</div> </Shell>
</div> </TenantDetailsContext.Provider>
</div> {editTenant && (
</TenantDetailsContext.Provider> <GlobalModel
{editTenant && ( size="lg"
<GlobalModel size="lg" isOpen={editTenant} closeModal={()=>setEditTenant(false)}> isOpen={editTenant}
<EditProfile TenantId={tenantId} onClose={()=>setEditTenant(false)}/> closeModal={() => setEditTenant(false)}
>
<EditProfile
TenantId={activeTenantId}
onClose={() => setEditTenant(false)}
/>
</GlobalModel> </GlobalModel>
)} )}
</> </>
); );
}; };

View File

@ -1,70 +1,118 @@
import React, { useState, createContext, useEffect, useContext } from "react"; import React, {
useState,
createContext,
useEffect,
useContext,
useCallback,
useMemo,
} from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
// ------Components------- import { useNavigate } from "react-router-dom";
import { useDispatch } from "react-redux";
// ------ Components -------
import Breadcrumb from "../../components/common/Breadcrumb"; import Breadcrumb from "../../components/common/Breadcrumb";
import TenantsList from "../../components/Tenant/TenantsList"; import TenantsList from "../../components/Tenant/TenantsList";
import { useNavigate } from "react-router-dom"; import TenantFilterPanel from "../../components/Tenant/TenantFilterPanel";
// ------ Context & Utils -------
import { useDebounce } from "../../utils/appUtils"; import { useDebounce } from "../../utils/appUtils";
import { useFab } from "../../Context/FabContext"; import { useFab } from "../../Context/FabContext";
import { setCurrentTenant } from "../../slices/globalVariablesSlice";
import { hasUserPermission } from "../../utils/authUtils";
//---------- Schema and defaultValues---- // ------ Schema -------
import { import {
defaultFilterValues, defaultFilterValues,
filterSchema, filterSchema,
} from "../../components/Tenant/TenantSchema"; } from "../../components/Tenant/TenantSchema";
import TenantFilterPanel from "../../components/Tenant/TenantFilterPanel";
import { useDispatch } from "react-redux";
import { setCurrentTenant } from "../../slices/globalVariablesSlice";
import { hasUserPermission } from "../../utils/authUtils";
import { SUPPER_TENANT, VIEW_TENANTS } from "../../utils/constants";
// This is context that wrapping all components tenant releated , but must pass inside 'TenantContext.Provider' // ------ Constants -------
import {
MANAGE_TENANTS,
SUPPER_TENANT,
VIEW_TENANTS,
} from "../../utils/constants";
import { useProfile } from "../../hooks/useProfile";
// ---------- Context ----------
export const TenantContext = createContext(); export const TenantContext = createContext();
export const useTenantContext = () => { export const useTenantContext = () => {
const context = useContext(TenantContext); const context = useContext(TenantContext);
if (!context) { if (!context) {
throw new Error("useTenantContext must be used within an TenantProvider"); throw new Error(
"useTenantContext must be used within a TenantContext.Provider"
);
} }
return context; return context;
}; };
const TenantPage = () => { const TenantPage = () => {
const [searchText, setSearchText] = useState(""); const dispatch = useDispatch();
const [isRefetching,setRefetching] = useState(false)
const [refetchFn, setRefetchFn] = useState(null);
const [filters, setFilter] = useState();
const dispatch = useDispatch()
const debouncedSearch = useDebounce(searchText, 500);
const contextValue = {
};
const navigate = useNavigate(); const navigate = useNavigate();
const IsSupperTenant = hasUserPermission(SUPPER_TENANT) const { profile } = useProfile();
const IsViewTenant = hasUserPermission(VIEW_TENANTS)
// ---------- State ----------
const [searchText, setSearchText] = useState("");
const [isRefetching, setIsRefetching] = useState(false);
const [refetchFn, setRefetchFn] = useState(null);
const [filters, setFilters] = useState();
// ---------- Hooks ----------
const debouncedSearch = useDebounce(searchText, 500);
const { setOffcanvasContent, setShowTrigger } = useFab(); const { setOffcanvasContent, setShowTrigger } = useFab();
// This Hook allow us to right-side bar for filter Tenants const isSuperTenant = hasUserPermission(SUPPER_TENANT);
const canManageTenants = hasUserPermission(MANAGE_TENANTS);
const isSelfTenant = hasUserPermission(VIEW_TENANTS);
const methods = useForm({ const methods = useForm({
resolver: zodResolver(filterSchema), resolver: zodResolver(filterSchema),
defaultValues: defaultFilterValues, defaultValues: defaultFilterValues,
}); });
const { reset } = methods;
const { reset } = methods;
const handleApplyFilters = useCallback((values) => {
setFilters(values);
}, []);
const filterPanelElement = useMemo(
() => <TenantFilterPanel onApply={handleApplyFilters} />,
[handleApplyFilters]
);
console.log(isSelfTenant)
// ---------- Fab Filter Panel ----------
useEffect(() => { useEffect(() => {
if (!isSuperTenant) return;
setShowTrigger(true); setShowTrigger(true);
setOffcanvasContent("Tenant Filters", <TenantFilterPanel onApply={setFilter} setOffcanvasContent("Tenant Filters", filterPanelElement);
/>);
return () => { return () => {
setShowTrigger(false); setShowTrigger(false);
setOffcanvasContent("", null); setOffcanvasContent("", null);
}; };
}, []); }, [isSuperTenant, filterPanelElement, profile]);
const handleNewTenant =()=>{
dispatch(setCurrentTenant(null)) // ---------- Redirect for Self Tenant ----------
navigate("/tenants/new-tenant") useEffect(() => {
} if (!isSuperTenant && isSelfTenant) {
// Delay navigation to next tick to avoid "update during render" warning
setTimeout(() => {
navigate("/tenant/self");
}, 0);
}
}, [isSuperTenant, isSelfTenant, navigate]);
// ---------- Handlers ----------
const handleNewTenant = () => {
dispatch(setCurrentTenant(null));
navigate("/tenants/new-tenant");
};
// ---------- Context Value ----------
const contextValue = {};
return ( return (
<TenantContext.Provider value={contextValue}> <TenantContext.Provider value={contextValue}>
<div className="container-fluid"> <div className="container-fluid">
@ -75,49 +123,65 @@ const handleNewTenant =()=>{
]} ]}
/> />
<div className="card d-flex p-2"> {/* Super Tenant Actions */}
<div className="row align-items-center"> {isSuperTenant && (
<div className="col-6 col-md-6 col-lg-3 mb-md-0"> <div className="card d-flex p-2">
<input <div className="row align-items-center">
type="search" {/* Search */}
value={searchText} <div className="col-6 col-md-6 col-lg-3 mb-md-0">
onChange={(e)=>setSearchText(e.target.value)} <input
className="form-control form-control-sm" type="search"
placeholder="Search Tenant" value={searchText}
/> onChange={(e) => setSearchText(e.target.value)}
</div> className="form-control form-control-sm"
placeholder="Search Tenant"
/>
</div>
<div className="col-6 col-md-6 col-lg-9 text-end cursor-pointer"> {/* Actions */}
<span className="text-tiny text-muted p-1 border-0 bg-none lead mx-3 " disabled={isRefetching} onClick={() => refetchFn && refetchFn()}> <div className="col-6 col-md-6 col-lg-9 text-end">
Refresh <i className={`bx bx-refresh ms-1 ${isRefetching ? "bx-spin":""}`}></i> <span
</span> className="text-tiny text-muted p-1 border-0 bg-none lead mx-3 cursor-pointer"
disabled={isRefetching}
onClick={() => refetchFn && refetchFn()}
>
Refresh{" "}
<i
className={`bx bx-refresh ms-1 ${
isRefetching ? "bx-spin" : ""
}`}
></i>
</span>
{IsSupperTenant && (
<button <button
type="button" type="button"
data-bs-toggle="tooltip" title="Add New Tenant"
data-bs-offset="0,8" className="p-1 bg-primary rounded-circle cursor-pointer"
data-bs-placement="top" onClick={handleNewTenant}
data-bs-custom-class="tooltip" >
title="Add New Tenant" <i className="bx bx-plus fs-4 text-white"></i>
className="p-1 bg-primary rounded-circle cursror-pointer" </button>
onClick={handleNewTenant} </div>
>
<i className="bx bx-plus fs-4 text-white"></i>
</button>
)}
</div> </div>
</div> </div>
</div> )}
{IsViewTenant ? (<TenantsList filters={filters} searchText={debouncedSearch} setIsRefetching={setRefetching}
setRefetchFn={setRefetchFn} {/* Tenant List or Access Denied */}
/>):( {isSuperTenant ? (
<div className="text-center my-2"> <TenantsList
<i className="fa-solid fa-triangle-exclamation fs-5"></i> filters={filters}
<p>Access Denied: You don't have permission to perform this action. !</p> searchText={debouncedSearch}
</div> setIsRefetching={setIsRefetching}
) } setRefetchFn={setRefetchFn}
/>
) : !isSelfTenant ? (
<div className="card text-center my-4 p-2">
<i className="fa-solid fa-triangle-exclamation fs-5"></i>
<p>
Access Denied: You don't have permission to perform this action!
</p>
</div>
) : null}
</div> </div>
</TenantContext.Provider> </TenantContext.Provider>
); );

View File

@ -42,18 +42,20 @@ import TenantPage from "../pages/Tenant/TenantPage";
import CreateTenant from "../pages/Tenant/CreateTenant"; import CreateTenant from "../pages/Tenant/CreateTenant";
import ExpensePage from "../pages/Expense/ExpensePage"; import ExpensePage from "../pages/Expense/ExpensePage";
import TenantDetails from "../pages/Tenant/TenantDetails"; import TenantDetails from "../pages/Tenant/TenantDetails";
import SelfTenantDetails from "../pages/Tenant/SelfTenantDetails";
import SuperTenantDetails from "../pages/Tenant/SuperTenantDetails";
const router = createBrowserRouter( const router = createBrowserRouter(
[ [
{ {
element: <AuthLayout />, element: <AuthLayout />,
children: [ children: [
{path: "/auth/login", element: <LoginPage />}, { path: "/auth/login", element: <LoginPage /> },
{path: "/auth/login-otp", element: <LoginWithOtp />}, { path: "/auth/login-otp", element: <LoginWithOtp /> },
{ path: "/auth/reqest/demo", element: <RegisterPage /> }, { path: "/auth/reqest/demo", element: <RegisterPage /> },
{ path: "/auth/forgot-password", element: <ForgotPasswordPage /> }, { path: "/auth/forgot-password", element: <ForgotPasswordPage /> },
{ path: "/reset-password", element: <ResetPasswordPage /> }, { path: "/reset-password", element: <ResetPasswordPage /> },
{ path: "/legal-info", element: <LegalInfoCard /> }, { path: "/legal-info", element: <LegalInfoCard /> },
{ path: "/auth/changepassword", element: <ChangePasswordPage /> }, { path: "/auth/changepassword", element: <ChangePasswordPage /> },
], ],
}, },
@ -82,9 +84,10 @@ const router = createBrowserRouter(
{ path: "/gallary", element: <ImageGallary /> }, { path: "/gallary", element: <ImageGallary /> },
{ path: "/expenses", element: <ExpensePage /> }, { path: "/expenses", element: <ExpensePage /> },
{ path: "/masters", element: <MasterPage /> }, { path: "/masters", element: <MasterPage /> },
{ path: "/tenants", element: <TenantPage /> }, { path: "/tenants", element: <TenantPage /> },
{ path: "/tenants/new-tenant", element: <CreateTenant /> }, { path: "/tenants/new-tenant", element: <CreateTenant /> },
{ path: "/tenant/:tenantId", element: <TenantDetails /> }, { path: "/tenant/:tenantId", element: <SuperTenantDetails /> },
{ path: "/tenant/self", element: <SelfTenantDetails /> },
{ path: "/help/support", element: <Support /> }, { path: "/help/support", element: <Support /> },
{ path: "/help/docs", element: <Documentation /> }, { path: "/help/docs", element: <Documentation /> },
{ path: "/help/connect", element: <Connect /> }, { path: "/help/connect", element: <Connect /> },

View File

@ -96,10 +96,10 @@ export const reference = [
{ val: "root tenant", name: "Root Tenant" }, { val: "root tenant", name: "Root Tenant" },
]; ];
export const orgSize = [ export const orgSize = [
{ val: "50", name: "1-50" }, { val: "1-50", name: "1-50" },
{ val: "100", name: "51-100" }, { val: "51-100", name: "51-100" },
{ val: "500", name: "101-500" }, { val: "101-500", name: "101-500" },
{ val: "600", name: "500+" }, { val: "500+", name: "500+" },
]; ];
export const BASE_URL = process.env.VITE_BASE_URL; export const BASE_URL = process.env.VITE_BASE_URL;