diff --git a/src/components/Tenant/Profile.jsx b/src/components/Tenant/Profile.jsx index da9901e5..74178503 100644 --- a/src/components/Tenant/Profile.jsx +++ b/src/components/Tenant/Profile.jsx @@ -5,9 +5,12 @@ import GlobalModel from "../common/GlobalModel"; import { useTenantContext } from "../../pages/Tenant/TenantPage"; import { useTenantDetailsContext } from "../../pages/Tenant/TenantDetails"; import IconButton from "../common/IconButton"; +import { hasUserPermission } from "../../utils/authUtils"; +import { MANAGE_TENANTS } from "../../utils/constants"; const Profile = ({ data }) => { const {setEditTenant} = useTenantDetailsContext() + const canUpdateTenant = hasUserPermission(MANAGE_TENANTS) return ( <>
@@ -33,12 +36,12 @@ const Profile = ({ data }) => { {data?.domainName}
- setEditTenant(true)} > - + )} diff --git a/src/components/Tenant/SubScriptionHistory.jsx b/src/components/Tenant/SubScriptionHistory.jsx index 2e781ac6..576c1e9e 100644 --- a/src/components/Tenant/SubScriptionHistory.jsx +++ b/src/components/Tenant/SubScriptionHistory.jsx @@ -4,7 +4,9 @@ import { useDispatch } from "react-redux"; import { setCurrentTenant } from "../../slices/globalVariablesSlice"; import { useNavigate } from "react-router-dom"; 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 { data, isLoading, isError, error } = useTenantDetails(tenantId); diff --git a/src/components/Tenant/TenantSchema.js b/src/components/Tenant/TenantSchema.js index 32ba8fe4..9c769a7c 100644 --- a/src/components/Tenant/TenantSchema.js +++ b/src/components/Tenant/TenantSchema.js @@ -63,7 +63,7 @@ export const subscriptionSchema = z.object({ .min(1, "Team size is required and must be greater than zero"), frequency: z .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(), autoRenew: z.boolean(), }); diff --git a/src/data/menuData.json b/src/data/menuData.json index b32d22a2..75809c9b 100644 --- a/src/data/menuData.json +++ b/src/data/menuData.json @@ -81,7 +81,7 @@ { "text": "Tenant", "available": true, - "link": "/tenants/" + "link": "/tenants" }, { "text": "Masters", diff --git a/src/hooks/useTenant.js b/src/hooks/useTenant.js index 29838349..57e5c4cc 100644 --- a/src/hooks/useTenant.js +++ b/src/hooks/useTenant.js @@ -55,6 +55,7 @@ export const useTenantDetails = (id) => { const response = await TenantRepository.getTenantDetails(id); return response.data; }, + enabled:!!id }); }; diff --git a/src/pages/Tenant/SelfTenantDetails.jsx b/src/pages/Tenant/SelfTenantDetails.jsx new file mode 100644 index 00000000..e46b605f --- /dev/null +++ b/src/pages/Tenant/SelfTenantDetails.jsx @@ -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
Loading...
; + } + + return ( + + ); +}; + +export default SelfTenantDetails; + diff --git a/src/pages/Tenant/SuperTenantDetails.jsx b/src/pages/Tenant/SuperTenantDetails.jsx new file mode 100644 index 00000000..1ac127a4 --- /dev/null +++ b/src/pages/Tenant/SuperTenantDetails.jsx @@ -0,0 +1,8 @@ +import React from "react"; +import TenantDetails from "./TenantDetails"; + +const SuperTenantDetails = () => { + return ; +}; + +export default SuperTenantDetails; diff --git a/src/pages/Tenant/TenantDetails.jsx b/src/pages/Tenant/TenantDetails.jsx index 07e54b88..8c99b792 100644 --- a/src/pages/Tenant/TenantDetails.jsx +++ b/src/pages/Tenant/TenantDetails.jsx @@ -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 Breadcrumb from "../../components/common/Breadcrumb"; import Profile from "../../components/Tenant/Profile"; - import { useTenantDetails } from "../../hooks/useTenant"; -import Organization from "../../components/Tenant/Organization"; import { ComingSoonPage } from "../Misc/ComingSoonPage"; import GlobalModel from "../../components/common/GlobalModel"; import EditProfile from "../../components/Tenant/EditProfile"; @@ -13,109 +11,142 @@ import SubScriptionHistory from "../../components/Tenant/SubScriptionHistory"; const TenantDetailsContext = createContext(); export const useTenantDetailsContext = () => useContext(TenantDetailsContext); - -const TenantDetails = () => { - const { tenantId } = useParams(); - const { data, isLoading, isError, error } = useTenantDetails(tenantId); +const TenantDetails = ({ + tenantId: tenantIdProp, + wrapInContainer = true, + 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 contextValues ={ - setEditTenant,editTenant - } - + const contextValues = useMemo( + () => ({ editTenant, setEditTenant }), + [editTenant] + ); - const tabs = [ - { - id: "navs-left-home", - label: "Profile", - icon: "bx bx-user-circle", - iconSize: "bx-sm", - content: , - }, - - { - id: "navs-left-bill", - label: "Bills and Plan ", - icon: "bx bx-receipt", - iconSize: "bx-sm", - content: ( -
- -
- ), - }, - - { - id: "navs-left-messages", - label: "Messages", - icon: "bx bx-message-rounded", - iconSize: "bx-sm", - content: ( -
- -
- ), - }, - ]; - + const tabs = useMemo( + () => [ + { + id: "navs-left-home", + label: "Profile", + icon: "bx bx-user-circle", + iconSize: "bx-sm", + content: , + }, + { + id: "navs-left-bill", + label: "Bills and Plan", + icon: "bx bx-receipt", + iconSize: "bx-sm", + content: ( +
+ +
+ ), + }, + { + id: "navs-left-messages", + label: "Messages", + icon: "bx bx-message-rounded", + iconSize: "bx-sm", + content: ( +
+ +
+ ), + }, + ], + [data, activeTenantId] + ); + if (!activeTenantId) return
No tenant selected.
; if (isLoading) return
Loading...
; - if (isError) return
{error.message}
; + if (isError) return
{error?.message}
; + + + const Shell = ({ children }) => + wrapInContainer ? ( +
{children}
+ ) : ( + <>{children} + ); + return ( <> - -
- + + + {showBreadcrumb && ( + + )} -
-
    - {tabs.map((tab, index) => ( -
  • - -
  • - ))} -
+
+
    + {tabs.map((tab, index) => ( +
  • + +
  • + ))} +
-
- {tabs.map((tab, index) => ( -
- {tab.content} +
+ {tabs.map((tab, index) => ( +
+ {tab.content} +
+ ))}
- ))} -
-
- -
- - {editTenant && ( - setEditTenant(false)}> - setEditTenant(false)}/> +
+
+
+ + {editTenant && ( + setEditTenant(false)} + > + setEditTenant(false)} + /> )} - + ); }; diff --git a/src/pages/Tenant/TenantPage.jsx b/src/pages/Tenant/TenantPage.jsx index 03b52877..f0ad9dd4 100644 --- a/src/pages/Tenant/TenantPage.jsx +++ b/src/pages/Tenant/TenantPage.jsx @@ -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 { 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 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 { useFab } from "../../Context/FabContext"; +import { setCurrentTenant } from "../../slices/globalVariablesSlice"; +import { hasUserPermission } from "../../utils/authUtils"; -//---------- Schema and defaultValues---- +// ------ Schema ------- import { defaultFilterValues, filterSchema, } 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 useTenantContext = () => { const context = useContext(TenantContext); 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; }; const TenantPage = () => { - const [searchText, setSearchText] = useState(""); - 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 dispatch = useDispatch(); const navigate = useNavigate(); - const IsSupperTenant = hasUserPermission(SUPPER_TENANT) - const IsViewTenant = hasUserPermission(VIEW_TENANTS) + const { profile } = useProfile(); + // ---------- 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(); - // 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({ - resolver: zodResolver(filterSchema), - defaultValues: defaultFilterValues, - }); - - const { reset } = methods; + const methods = useForm({ + resolver: zodResolver(filterSchema), + defaultValues: defaultFilterValues, + }); + const { reset } = methods; + + const handleApplyFilters = useCallback((values) => { + setFilters(values); + }, []); + + const filterPanelElement = useMemo( + () => , + [handleApplyFilters] + ); +console.log(isSelfTenant) + // ---------- Fab Filter Panel ---------- useEffect(() => { + if (!isSuperTenant) return; setShowTrigger(true); - setOffcanvasContent("Tenant Filters", ); + setOffcanvasContent("Tenant Filters", filterPanelElement); + return () => { setShowTrigger(false); setOffcanvasContent("", null); }; - }, []); -const handleNewTenant =()=>{ - dispatch(setCurrentTenant(null)) - navigate("/tenants/new-tenant") -} + }, [isSuperTenant, filterPanelElement, profile]); + + // ---------- Redirect for Self 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 (
@@ -75,49 +123,65 @@ const handleNewTenant =()=>{ ]} /> -
-
-
- setSearchText(e.target.value)} - className="form-control form-control-sm" - placeholder="Search Tenant" - /> -
+ {/* Super Tenant Actions */} + {isSuperTenant && ( +
+
+ {/* Search */} +
+ setSearchText(e.target.value)} + className="form-control form-control-sm" + placeholder="Search Tenant" + /> +
-
- refetchFn && refetchFn()}> - Refresh - + {/* Actions */} +
+ refetchFn && refetchFn()} + > + Refresh{" "} + + - {IsSupperTenant && ( - )} + type="button" + title="Add New Tenant" + className="p-1 bg-primary rounded-circle cursor-pointer" + onClick={handleNewTenant} + > + + +
-
- {IsViewTenant ? ():( -
- -

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

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

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

+
+ ) : null}
); diff --git a/src/router/AppRoutes.jsx b/src/router/AppRoutes.jsx index d46f397f..0e269a6f 100644 --- a/src/router/AppRoutes.jsx +++ b/src/router/AppRoutes.jsx @@ -42,18 +42,20 @@ import TenantPage from "../pages/Tenant/TenantPage"; import CreateTenant from "../pages/Tenant/CreateTenant"; import ExpensePage from "../pages/Expense/ExpensePage"; import TenantDetails from "../pages/Tenant/TenantDetails"; +import SelfTenantDetails from "../pages/Tenant/SelfTenantDetails"; +import SuperTenantDetails from "../pages/Tenant/SuperTenantDetails"; const router = createBrowserRouter( [ { element: , children: [ - {path: "/auth/login", element: }, - {path: "/auth/login-otp", element: }, + { path: "/auth/login", element: }, + { path: "/auth/login-otp", element: }, { path: "/auth/reqest/demo", element: }, { path: "/auth/forgot-password", element: }, { path: "/reset-password", element: }, - { path: "/legal-info", element: }, + { path: "/legal-info", element: }, { path: "/auth/changepassword", element: }, ], }, @@ -82,9 +84,10 @@ const router = createBrowserRouter( { path: "/gallary", element: }, { path: "/expenses", element: }, { path: "/masters", element: }, - { path: "/tenants", element: }, - { path: "/tenants/new-tenant", element: }, - { path: "/tenant/:tenantId", element: }, + { path: "/tenants", element: }, + { path: "/tenants/new-tenant", element: }, + { path: "/tenant/:tenantId", element: }, + { path: "/tenant/self", element: }, { path: "/help/support", element: }, { path: "/help/docs", element: }, { path: "/help/connect", element: }, diff --git a/src/utils/constants.jsx b/src/utils/constants.jsx index be70ca85..555e3a0b 100644 --- a/src/utils/constants.jsx +++ b/src/utils/constants.jsx @@ -96,10 +96,10 @@ export const reference = [ { val: "root tenant", name: "Root Tenant" }, ]; export const orgSize = [ - { val: "50", name: "1-50" }, - { val: "100", name: "51-100" }, - { val: "500", name: "101-500" }, - { val: "600", name: "500+" }, + { val: "1-50", name: "1-50" }, + { val: "51-100", name: "51-100" }, + { val: "101-500", name: "101-500" }, + { val: "500+", name: "500+" }, ]; export const BASE_URL = process.env.VITE_BASE_URL;