feat: tenant-subscription

fix: deleted updatemodule
This commit is contained in:
amee
2026-04-18 16:31:52 +05:30
parent 10c53e7d50
commit 51bac652df
8 changed files with 258 additions and 46 deletions
@@ -12,7 +12,7 @@ import CustomButton from '../../../../components/custom/CustomButton';
const ModuleList = () => { const ModuleList = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { t } = useTranslation(['modules', 'common']); const { t } = useTranslation(['modules', 'common']);
const { listModules, deleteModule, updateModule, loading } = useModuleApi(); const { listModules, deleteModule, loading } = useModuleApi();
const [modules, setModules] = useState<Module[]>([]); const [modules, setModules] = useState<Module[]>([]);
const [showForm, setShowForm] = useState(false); const [showForm, setShowForm] = useState(false);
const [selectedModule, setSelectedModule] = useState<Module | null>(null); const [selectedModule, setSelectedModule] = useState<Module | null>(null);
@@ -3,6 +3,7 @@ export type SubscriptionPlan = {
name: string; name: string;
description?: string | null; description?: string | null;
price?: number | null; price?: number | null;
duration_days?: number | null;
is_public: boolean; is_public: boolean;
status: string; status: string;
created_at: string; created_at: string;
@@ -18,6 +19,7 @@ export type SubscriptionPlanCreateRequest = {
name: string; name: string;
description?: string; description?: string;
price?: number; price?: number;
duration_days?: number;
is_public?: boolean; is_public?: boolean;
status?: string; status?: string;
access_ids?: string[]; access_ids?: string[];
@@ -28,6 +30,7 @@ export type SubscriptionPlanUpdateRequest = {
name?: string; name?: string;
description?: string; description?: string;
price?: number; price?: number;
duration_days?: number;
is_public?: boolean; is_public?: boolean;
status?: string; status?: string;
access_ids?: string[]; access_ids?: string[];
@@ -19,6 +19,7 @@ const AddSubscriptions = () => {
name: "", name: "",
description: "", description: "",
price: undefined, price: undefined,
duration_days: undefined,
is_public: true, is_public: true,
status: "active", status: "active",
access_ids: [], access_ids: [],
@@ -96,6 +97,7 @@ const AddSubscriptions = () => {
name: formData.name.trim(), name: formData.name.trim(),
description: formData.description?.trim() || undefined, description: formData.description?.trim() || undefined,
price: formData.price ? Number(formData.price) : undefined, price: formData.price ? Number(formData.price) : undefined,
duration_days: formData.duration_days ? Number(formData.duration_days) : undefined,
is_public: formData.is_public, is_public: formData.is_public,
status: formData.status, status: formData.status,
access_ids: accessIds, access_ids: accessIds,
@@ -157,6 +159,19 @@ const AddSubscriptions = () => {
})) }))
} }
/> />
<CustomInput
label="Duration (Days)"
name="duration_days"
type="number"
placeholder="30"
value={formData.duration_days ?? ""}
onChange={(e) =>
setFormData((prev) => ({
...prev,
duration_days: e.target.value ? Number(e.target.value) : undefined,
}))
}
/>
</div> </div>
<CustomInput <CustomInput
@@ -43,6 +43,7 @@ interface EditFormState {
name: string; name: string;
description: string; description: string;
price: number | undefined; price: number | undefined;
duration_days: number | undefined;
is_public: boolean; is_public: boolean;
status: string; status: string;
} }
@@ -88,6 +89,7 @@ const AllSubscriptions = () => {
name: "", name: "",
description: "", description: "",
price: undefined, price: undefined,
duration_days: undefined,
is_public: true, is_public: true,
status: "active", status: "active",
}); });
@@ -249,6 +251,7 @@ const AllSubscriptions = () => {
name: plan.name, name: plan.name,
description: plan.description ?? "", description: plan.description ?? "",
price: plan.price ?? undefined, price: plan.price ?? undefined,
duration_days: plan.duration_days ?? undefined,
is_public: plan.is_public, is_public: plan.is_public,
status: plan.status, status: plan.status,
}); });
@@ -311,6 +314,7 @@ const AllSubscriptions = () => {
name: editForm.name.trim(), name: editForm.name.trim(),
description: editForm.description.trim() || undefined, description: editForm.description.trim() || undefined,
price: editForm.price ? Number(editForm.price) : undefined, price: editForm.price ? Number(editForm.price) : undefined,
duration_days: editForm.duration_days ? Number(editForm.duration_days) : undefined,
is_public: editForm.is_public, is_public: editForm.is_public,
status: editForm.status, status: editForm.status,
access_ids: accessIds, access_ids: accessIds,
@@ -381,6 +385,16 @@ const AllSubscriptions = () => {
</span> </span>
), ),
}, },
{
key: "duration_days",
header: "Duration",
searchable: false,
render: (row) => (
<span className="text-sm font-medium text-[var(--text-primary)]">
{row.duration_days ? `${row.duration_days} days` : "-"}
</span>
),
},
{ {
key: "status", key: "status",
header: ( header: (
@@ -570,6 +584,14 @@ const AllSubscriptions = () => {
{formatPrice(selectedPlan.price)} {formatPrice(selectedPlan.price)}
</p> </p>
</div> </div>
<div>
<p className="text-xs uppercase tracking-wide text-[var(--text-secondary)]">
Duration
</p>
<p className="text-sm font-medium text-[var(--text-primary)]">
{selectedPlan.duration_days ? `${selectedPlan.duration_days} days` : "-"}
</p>
</div>
<div> <div>
<p className="text-xs uppercase tracking-wide text-[var(--text-secondary)]"> <p className="text-xs uppercase tracking-wide text-[var(--text-secondary)]">
Status Status
@@ -689,6 +711,19 @@ const AllSubscriptions = () => {
})) }))
} }
/> />
<CustomInput
label="Duration (Days)"
name="duration_days"
type="number"
placeholder="30"
value={editForm.duration_days ?? ""}
onChange={(e) =>
setEditForm((prev) => ({
...prev,
duration_days: e.target.value ? Number(e.target.value) : undefined,
}))
}
/>
</div> </div>
<CustomInput <CustomInput
+12
View File
@@ -1,10 +1,16 @@
export type TenantStatus = "ACTIVE" | "INACTIVE" | "EXPIRED";
export type Tenant = { export type Tenant = {
id: string; id: string;
tenant_id: string;
tenant_name: string; tenant_name: string;
tenant_domain: string; tenant_domain: string;
tenant_logo_url?: string | null; tenant_logo_url?: string | null;
is_active: boolean; is_active: boolean;
plan_id?: string | null; plan_id?: string | null;
start_date?: string | null;
end_date?: string | null;
status: TenantStatus;
created_at: string; created_at: string;
updated_at: string; updated_at: string;
}; };
@@ -19,6 +25,9 @@ export type TenantCreateRequest = {
tenant_domain: string; tenant_domain: string;
tenant_logo_url?: string; tenant_logo_url?: string;
plan_id: string; plan_id: string;
start_date?: string;
end_date?: string;
status?: TenantStatus;
module_environments?: ModuleEnvironmentAssignment[]; module_environments?: ModuleEnvironmentAssignment[];
}; };
@@ -28,6 +37,9 @@ export type TenantUpdateRequest = {
tenant_logo_url?: string | null; tenant_logo_url?: string | null;
is_active?: boolean; is_active?: boolean;
plan_id?: string; plan_id?: string;
start_date?: string | null;
end_date?: string | null;
status?: TenantStatus;
}; };
export type TenantPaginatedResponse = { export type TenantPaginatedResponse = {
@@ -1,10 +1,10 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { CustomButton, CustomInput, CustomDropdown } from "../../../components/custom"; import { CustomButton, CustomDatePicker, CustomInput, CustomDropdown } from "../../../components/custom";
import CustomBackButton from "../../../components/custom/CustomBackButton"; import CustomBackButton from "../../../components/custom/CustomBackButton";
import { CustomLoader } from "../../../components/custom"; import { CustomLoader } from "../../../components/custom";
import { tenantsApi } from "../TenantsApi"; import { tenantsApi } from "../TenantsApi";
import type { Tenant, TenantCreateRequest, ModuleEnvironmentAssignment } from "../TenantsTypes"; import type { Tenant, TenantCreateRequest, ModuleEnvironmentAssignment, TenantStatus } from "../TenantsTypes";
import { useAuth } from "../../../context/AuthContext"; import { useAuth } from "../../../context/AuthContext";
import { subscriptionsApi } from "../../subscriptions/SubscriptionsApi"; import { subscriptionsApi } from "../../subscriptions/SubscriptionsApi";
import type { SubscriptionPlan } from "../../subscriptions/SubscriptionTypes"; import type { SubscriptionPlan } from "../../subscriptions/SubscriptionTypes";
@@ -13,7 +13,17 @@ import { adminModuleApi } from "../../modules/admin/AdminModuleApi";
import type { Module, ModuleEnvironment } from "../../modules/admin/AdminModuleTypes"; import type { Module, ModuleEnvironment } from "../../modules/admin/AdminModuleTypes";
import { AlertCircle } from "lucide-react"; import { AlertCircle } from "lucide-react";
const toIsoDate = (value: Date) => value.toISOString().slice(0, 10);
const addDays = (dateValue: string, days?: number | null) => {
if (!dateValue || !days) return "";
const nextDate = new Date(`${dateValue}T00:00:00`);
nextDate.setDate(nextDate.getDate() + days);
return toIsoDate(nextDate);
};
const AddTenants = () => { const AddTenants = () => {
const today = new Date().toISOString().slice(0, 10);
const { hasAccess, isLoading: isAuthLoading } = useAuth(); const { hasAccess, isLoading: isAuthLoading } = useAuth();
const canCreateTenant = hasAccess("superadmin.tenant.create"); const canCreateTenant = hasAccess("superadmin.tenant.create");
const navigate = useNavigate(); const navigate = useNavigate();
@@ -22,6 +32,9 @@ const AddTenants = () => {
const [tenantDomain, setTenantDomain] = useState(""); const [tenantDomain, setTenantDomain] = useState("");
const [tenantLogoUrl, setTenantLogoUrl] = useState(""); const [tenantLogoUrl, setTenantLogoUrl] = useState("");
const [selectedPlanId, setSelectedPlanId] = useState(""); const [selectedPlanId, setSelectedPlanId] = useState("");
const [startDate, setStartDate] = useState(today);
const [endDate, setEndDate] = useState("");
const [tenantStatus, setTenantStatus] = useState<TenantStatus>("ACTIVE");
const [moduleEnvAssignments, setModuleEnvAssignments] = useState<ModuleEnvironmentAssignment[]>([]); const [moduleEnvAssignments, setModuleEnvAssignments] = useState<ModuleEnvironmentAssignment[]>([]);
const [plans, setPlans] = useState<SubscriptionPlan[]>([]); const [plans, setPlans] = useState<SubscriptionPlan[]>([]);
@@ -43,6 +56,15 @@ const AddTenants = () => {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [errorMessage, setErrorMessage] = useState(""); const [errorMessage, setErrorMessage] = useState("");
useEffect(() => {
if (!selectedPlanId) return;
const selectedPlan = plans.find((plan) => plan.id === selectedPlanId);
if (!selectedPlan) return;
setStartDate((prev) => prev || today);
setEndDate(addDays(startDate || today, selectedPlan.duration_days));
}, [plans, selectedPlanId, startDate, today]);
useEffect(() => { useEffect(() => {
if (isAuthLoading || canCreateTenant) { if (isAuthLoading || canCreateTenant) {
setIsTenantLoading(false); setIsTenantLoading(false);
@@ -195,6 +217,9 @@ const AddTenants = () => {
tenant_domain: tenantDomain.trim(), tenant_domain: tenantDomain.trim(),
tenant_logo_url: tenantLogoUrl.trim() || undefined, tenant_logo_url: tenantLogoUrl.trim() || undefined,
plan_id: selectedPlanId, plan_id: selectedPlanId,
start_date: startDate || undefined,
end_date: endDate || undefined,
status: tenantStatus,
module_environments: moduleEnvAssignments.filter((a) => a.environment_slug), module_environments: moduleEnvAssignments.filter((a) => a.environment_slug),
}; };
@@ -313,6 +338,35 @@ const AddTenants = () => {
/> />
</div> </div>
<div className="grid grid-cols-1 gap-6 border-t border-gray-200 pt-6 md:grid-cols-3">
<CustomDatePicker
label="Start Date"
value={startDate}
onChange={(e) => {
const nextStartDate = e.target.value;
setStartDate(nextStartDate);
const selectedPlan = plans.find((plan) => plan.id === selectedPlanId);
setEndDate(addDays(nextStartDate, selectedPlan?.duration_days));
}}
/>
<CustomDatePicker
label="End Date"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
min={startDate || undefined}
/>
<CustomDropdown
label="Status"
value={tenantStatus}
onChange={(e) => setTenantStatus(e.target.value as TenantStatus)}
options={[
{ label: "Active", value: "ACTIVE" },
{ label: "Inactive", value: "INACTIVE" },
{ label: "Expired", value: "EXPIRED" },
]}
/>
</div>
{/* Module Environment Assignment — appears after plan selection */} {/* Module Environment Assignment — appears after plan selection */}
{selectedPlanId && ( {selectedPlanId && (
<div className="border-t border-gray-200 pt-6"> <div className="border-t border-gray-200 pt-6">
+124 -15
View File
@@ -6,6 +6,7 @@ import {
CustomCheckBox, CustomCheckBox,
CustomColumnFilter, CustomColumnFilter,
CustomConfirmationModal, CustomConfirmationModal,
CustomDatePicker,
CustomDropdown, CustomDropdown,
CustomInput, CustomInput,
CustomModal, CustomModal,
@@ -22,12 +23,21 @@ import {
} from "../../../components/custom/CustomColumnFilter.utils"; } from "../../../components/custom/CustomColumnFilter.utils";
import type { ColumnSortDirection } from "../../../components/custom/CustomColumnFilter"; import type { ColumnSortDirection } from "../../../components/custom/CustomColumnFilter";
import { formatDate } from "../../../lib/dateFormat"; import { formatDate } from "../../../lib/dateFormat";
import type { Tenant, TenantUpdateRequest } from "../TenantsTypes"; import type { Tenant, TenantStatus, TenantUpdateRequest } from "../TenantsTypes";
import { tenantsApi } from "../TenantsApi"; import { tenantsApi } from "../TenantsApi";
import { useAuth } from "../../../context/AuthContext"; import { useAuth } from "../../../context/AuthContext";
import { subscriptionsApi } from "../../subscriptions/SubscriptionsApi"; import { subscriptionsApi } from "../../subscriptions/SubscriptionsApi";
import type { SubscriptionPlan } from "../../subscriptions/SubscriptionTypes"; import type { SubscriptionPlan } from "../../subscriptions/SubscriptionTypes";
const toIsoDate = (value: Date) => value.toISOString().slice(0, 10);
const addDays = (dateValue: string, days?: number | null) => {
if (!dateValue || !days) return "";
const nextDate = new Date(`${dateValue}T00:00:00`);
nextDate.setDate(nextDate.getDate() + days);
return toIsoDate(nextDate);
};
function useDebounce<T>(value: T, delay: number): T { function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value); const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => { useEffect(() => {
@@ -58,6 +68,9 @@ interface EditFormState {
tenant_logo_url: string; tenant_logo_url: string;
is_active: boolean; is_active: boolean;
plan_id: string; plan_id: string;
start_date: string;
end_date: string;
status: TenantStatus;
} }
const AllTenants = () => { const AllTenants = () => {
@@ -77,11 +90,13 @@ const AllTenants = () => {
tenant_logo_url: "", tenant_logo_url: "",
is_active: true, is_active: true,
plan_id: "", plan_id: "",
start_date: "",
end_date: "",
status: "ACTIVE",
}); });
// Subscription plans for dropdown // Subscription plans for dropdown
const [plans, setPlans] = useState<SubscriptionPlan[]>([]); const [plans, setPlans] = useState<SubscriptionPlan[]>([]);
const [isPlansLoading, setIsPlansLoading] = useState(false);
const [editError, setEditError] = useState(""); const [editError, setEditError] = useState("");
const [deleteError, setDeleteError] = useState(""); const [deleteError, setDeleteError] = useState("");
@@ -224,14 +239,11 @@ const AllTenants = () => {
let isMounted = true; let isMounted = true;
const loadPlans = async () => { const loadPlans = async () => {
setIsPlansLoading(true);
try { try {
const data = await subscriptionsApi.getAll(); const data = await subscriptionsApi.getAll();
if (isMounted) setPlans(data); if (isMounted) setPlans(data);
} catch (error) { } catch (error) {
console.error("Failed to load subscription plans:", error); console.error("Failed to load subscription plans:", error);
} finally {
if (isMounted) setIsPlansLoading(false);
} }
}; };
@@ -298,6 +310,9 @@ const AllTenants = () => {
tenant_logo_url: tenant.tenant_logo_url ?? "", tenant_logo_url: tenant.tenant_logo_url ?? "",
is_active: tenant.is_active, is_active: tenant.is_active,
plan_id: tenant.plan_id ?? "", plan_id: tenant.plan_id ?? "",
start_date: tenant.start_date ?? "",
end_date: tenant.end_date ?? "",
status: tenant.status,
}); });
}, []); }, []);
@@ -355,6 +370,9 @@ const AllTenants = () => {
tenant_logo_url: editForm.tenant_logo_url.trim() || null, tenant_logo_url: editForm.tenant_logo_url.trim() || null,
is_active: editForm.is_active, is_active: editForm.is_active,
plan_id: editForm.plan_id || undefined, plan_id: editForm.plan_id || undefined,
start_date: editForm.start_date || null,
end_date: editForm.end_date || null,
status: editForm.status,
}; };
const updatedTenant = await tenantsApi.update(tenantId, payload); const updatedTenant = await tenantsApi.update(tenantId, payload);
@@ -520,7 +538,27 @@ const AllTenants = () => {
), ),
searchable: false, searchable: false,
render: (row) => ( render: (row) => (
<CustomStatus status={row.is_active ? "Active" : "Inactive"} /> <CustomStatus status={row.status} />
),
},
{
key: "start_date",
header: "Start Date",
searchable: false,
render: (row) => (
<div className="text-sm font-medium text-(--text-primary)">
{row.start_date ? formatDate(row.start_date) : "-"}
</div>
),
},
{
key: "end_date",
header: "End Date",
searchable: false,
render: (row) => (
<div className="text-sm font-medium text-(--text-primary)">
{row.end_date ? formatDate(row.end_date) : "-"}
</div>
), ),
}, },
{ {
@@ -669,13 +707,19 @@ const AllTenants = () => {
<p className="text-sm text-[var(--text-secondary)]"></p> <p className="text-sm text-[var(--text-secondary)]"></p>
)} )}
</div> </div>
<div>
<p className="text-xs uppercase tracking-wide text-[var(--text-secondary)]">
Tenant ID
</p>
<p className="text-sm font-medium text-[var(--text-primary)] break-all">
{selectedTenant.tenant_id}
</p>
</div>
<div> <div>
<p className="text-xs uppercase tracking-wide text-[var(--text-secondary)]"> <p className="text-xs uppercase tracking-wide text-[var(--text-secondary)]">
Status Status
</p> </p>
<p className="text-sm font-medium text-[var(--text-primary)]"> <CustomStatus status={selectedTenant.status} />
{selectedTenant.is_active ? "Active" : "Inactive"}
</p>
</div> </div>
<div> <div>
<p className="text-xs uppercase tracking-wide text-[var(--text-secondary)]"> <p className="text-xs uppercase tracking-wide text-[var(--text-secondary)]">
@@ -685,6 +729,22 @@ const AllTenants = () => {
{getPlanName(selectedTenant.plan_id)} {getPlanName(selectedTenant.plan_id)}
</p> </p>
</div> </div>
<div>
<p className="text-xs uppercase tracking-wide text-[var(--text-secondary)]">
Start Date
</p>
<p className="text-sm font-medium text-[var(--text-primary)]">
{selectedTenant.start_date ? formatDate(selectedTenant.start_date) : "-"}
</p>
</div>
<div>
<p className="text-xs uppercase tracking-wide text-[var(--text-secondary)]">
End Date
</p>
<p className="text-sm font-medium text-[var(--text-primary)]">
{selectedTenant.end_date ? formatDate(selectedTenant.end_date) : "-"}
</p>
</div>
<div> <div>
<p className="text-xs uppercase tracking-wide text-[var(--text-secondary)]"> <p className="text-xs uppercase tracking-wide text-[var(--text-secondary)]">
Created Created
@@ -780,15 +840,64 @@ const AllTenants = () => {
label="Select Plan" label="Select Plan"
name="plan_id" name="plan_id"
value={editForm.plan_id} value={editForm.plan_id}
onChange={(e) => onChange={(e) => {
setEditForm((prev) => ({ ...prev, plan_id: e.target.value })) const nextPlanId = e.target.value;
} const selectedPlan = plans.find((plan) => plan.id === nextPlanId);
const nextStartDate = editForm.start_date || toIsoDate(new Date());
setEditForm((prev) => ({
...prev,
plan_id: nextPlanId,
start_date: nextStartDate,
end_date: addDays(nextStartDate, selectedPlan?.duration_days),
}));
}}
options={plans.map((plan) => ({ options={plans.map((plan) => ({
label: `${plan.name}${plan.price != null ? `${new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(plan.price)}` : ""}`, label: plan.duration_days
? `${plan.name} (${plan.duration_days} days)`
: plan.name,
value: plan.id, value: plan.id,
}))} }))}
placeholder={isPlansLoading ? "Loading plans..." : "Select a subscription plan"} placeholder="Select a subscription plan"
disabled={isPlansLoading} />
</div>
<div className="grid grid-cols-1 gap-6 border-t border-gray-200 pt-6 md:grid-cols-3">
<CustomDatePicker
label="Start Date"
value={editForm.start_date}
onChange={(e) => {
const nextStartDate = e.target.value;
const selectedPlan = plans.find((plan) => plan.id === editForm.plan_id);
setEditForm((prev) => ({
...prev,
start_date: nextStartDate,
end_date: addDays(nextStartDate, selectedPlan?.duration_days),
}));
}}
/>
<CustomDatePicker
label="End Date"
value={editForm.end_date}
min={editForm.start_date || undefined}
onChange={(e) =>
setEditForm((prev) => ({ ...prev, end_date: e.target.value }))
}
/>
<CustomDropdown
label="Status"
value={editForm.status}
onChange={(e) =>
setEditForm((prev) => ({
...prev,
status: e.target.value as TenantStatus,
}))
}
options={[
{ label: "Active", value: "ACTIVE" },
{ label: "Inactive", value: "INACTIVE" },
{ label: "Expired", value: "EXPIRED" },
]}
/> />
</div> </div>
+12 -28
View File
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useMemo, useState } from "react";
import { Link, useLocation, useNavigate } from "react-router-dom"; import { Link, useLocation, useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
@@ -146,6 +146,16 @@ const AppSidebar: React.FC = () => {
[location.pathname] [location.pathname]
); );
const isSubmenuRouteActive = useCallback(
(submenu?: SubMenuItem[]) =>
submenu?.some(
(subItem) =>
location.pathname === subItem.path ||
location.pathname.startsWith(subItem.path + "/")
) ?? false,
[location.pathname]
);
const visibleNavItems = useMemo( const visibleNavItems = useMemo(
() => navItems.filter((item) => { () => navItems.filter((item) => {
if (!item.access) return true; if (!item.access) return true;
@@ -161,32 +171,6 @@ const AppSidebar: React.FC = () => {
const [openSubmenus, setOpenSubmenus] = useState<Record<string, boolean>>({}); const [openSubmenus, setOpenSubmenus] = useState<Record<string, boolean>>({});
useEffect(() => {
const currentPath = location.pathname;
setOpenSubmenus(prev => {
const newState = { ...prev };
Object.keys(newState).forEach(menuName => {
if (newState[menuName]) {
const navItem = navItems.find(item => item.name === menuName);
if (navItem && navItem.submenu) {
const isMatchingSubmenu = navItem.submenu.some(subItem =>
currentPath === subItem.path || currentPath.startsWith(subItem.path + "/")
);
if (!isMatchingSubmenu) {
newState[menuName] = false;
}
}
}
});
return newState;
});
}, [location.pathname]);
const toggleSubmenu = (name: string) => { const toggleSubmenu = (name: string) => {
if (!isExpanded && !isMobile) { if (!isExpanded && !isMobile) {
toggleSidebar(); // Auto expand when clicking submenu in collapsed state toggleSidebar(); // Auto expand when clicking submenu in collapsed state
@@ -288,7 +272,7 @@ const AppSidebar: React.FC = () => {
{visibleNavItems.map((nav) => { {visibleNavItems.map((nav) => {
const active = isActive(nav.path); const active = isActive(nav.path);
const hasSubmenu = nav.submenu && nav.submenu.length > 0; const hasSubmenu = nav.submenu && nav.submenu.length > 0;
const isMenuOpen = openSubmenus[nav.name]; const isMenuOpen = openSubmenus[nav.name] || isSubmenuRouteActive(nav.submenu);
if (hasSubmenu) { if (hasSubmenu) {
return ( return (