feat: tenant-subscription
fix: deleted updatemodule
This commit is contained in:
@@ -12,7 +12,7 @@ import CustomButton from '../../../../components/custom/CustomButton';
|
||||
const ModuleList = () => {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation(['modules', 'common']);
|
||||
const { listModules, deleteModule, updateModule, loading } = useModuleApi();
|
||||
const { listModules, deleteModule, loading } = useModuleApi();
|
||||
const [modules, setModules] = useState<Module[]>([]);
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [selectedModule, setSelectedModule] = useState<Module | null>(null);
|
||||
|
||||
@@ -3,6 +3,7 @@ export type SubscriptionPlan = {
|
||||
name: string;
|
||||
description?: string | null;
|
||||
price?: number | null;
|
||||
duration_days?: number | null;
|
||||
is_public: boolean;
|
||||
status: string;
|
||||
created_at: string;
|
||||
@@ -18,6 +19,7 @@ export type SubscriptionPlanCreateRequest = {
|
||||
name: string;
|
||||
description?: string;
|
||||
price?: number;
|
||||
duration_days?: number;
|
||||
is_public?: boolean;
|
||||
status?: string;
|
||||
access_ids?: string[];
|
||||
@@ -28,6 +30,7 @@ export type SubscriptionPlanUpdateRequest = {
|
||||
name?: string;
|
||||
description?: string;
|
||||
price?: number;
|
||||
duration_days?: number;
|
||||
is_public?: boolean;
|
||||
status?: string;
|
||||
access_ids?: string[];
|
||||
|
||||
@@ -19,6 +19,7 @@ const AddSubscriptions = () => {
|
||||
name: "",
|
||||
description: "",
|
||||
price: undefined,
|
||||
duration_days: undefined,
|
||||
is_public: true,
|
||||
status: "active",
|
||||
access_ids: [],
|
||||
@@ -96,6 +97,7 @@ const AddSubscriptions = () => {
|
||||
name: formData.name.trim(),
|
||||
description: formData.description?.trim() || undefined,
|
||||
price: formData.price ? Number(formData.price) : undefined,
|
||||
duration_days: formData.duration_days ? Number(formData.duration_days) : undefined,
|
||||
is_public: formData.is_public,
|
||||
status: formData.status,
|
||||
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>
|
||||
|
||||
<CustomInput
|
||||
|
||||
@@ -43,6 +43,7 @@ interface EditFormState {
|
||||
name: string;
|
||||
description: string;
|
||||
price: number | undefined;
|
||||
duration_days: number | undefined;
|
||||
is_public: boolean;
|
||||
status: string;
|
||||
}
|
||||
@@ -88,6 +89,7 @@ const AllSubscriptions = () => {
|
||||
name: "",
|
||||
description: "",
|
||||
price: undefined,
|
||||
duration_days: undefined,
|
||||
is_public: true,
|
||||
status: "active",
|
||||
});
|
||||
@@ -249,6 +251,7 @@ const AllSubscriptions = () => {
|
||||
name: plan.name,
|
||||
description: plan.description ?? "",
|
||||
price: plan.price ?? undefined,
|
||||
duration_days: plan.duration_days ?? undefined,
|
||||
is_public: plan.is_public,
|
||||
status: plan.status,
|
||||
});
|
||||
@@ -311,6 +314,7 @@ const AllSubscriptions = () => {
|
||||
name: editForm.name.trim(),
|
||||
description: editForm.description.trim() || undefined,
|
||||
price: editForm.price ? Number(editForm.price) : undefined,
|
||||
duration_days: editForm.duration_days ? Number(editForm.duration_days) : undefined,
|
||||
is_public: editForm.is_public,
|
||||
status: editForm.status,
|
||||
access_ids: accessIds,
|
||||
@@ -381,6 +385,16 @@ const AllSubscriptions = () => {
|
||||
</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",
|
||||
header: (
|
||||
@@ -570,6 +584,14 @@ const AllSubscriptions = () => {
|
||||
{formatPrice(selectedPlan.price)}
|
||||
</p>
|
||||
</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>
|
||||
<p className="text-xs uppercase tracking-wide text-[var(--text-secondary)]">
|
||||
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>
|
||||
|
||||
<CustomInput
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
export type TenantStatus = "ACTIVE" | "INACTIVE" | "EXPIRED";
|
||||
|
||||
export type Tenant = {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
tenant_name: string;
|
||||
tenant_domain: string;
|
||||
tenant_logo_url?: string | null;
|
||||
is_active: boolean;
|
||||
plan_id?: string | null;
|
||||
start_date?: string | null;
|
||||
end_date?: string | null;
|
||||
status: TenantStatus;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
@@ -19,6 +25,9 @@ export type TenantCreateRequest = {
|
||||
tenant_domain: string;
|
||||
tenant_logo_url?: string;
|
||||
plan_id: string;
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
status?: TenantStatus;
|
||||
module_environments?: ModuleEnvironmentAssignment[];
|
||||
};
|
||||
|
||||
@@ -28,6 +37,9 @@ export type TenantUpdateRequest = {
|
||||
tenant_logo_url?: string | null;
|
||||
is_active?: boolean;
|
||||
plan_id?: string;
|
||||
start_date?: string | null;
|
||||
end_date?: string | null;
|
||||
status?: TenantStatus;
|
||||
};
|
||||
|
||||
export type TenantPaginatedResponse = {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useEffect, useState } from "react";
|
||||
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 { CustomLoader } from "../../../components/custom";
|
||||
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 { subscriptionsApi } from "../../subscriptions/SubscriptionsApi";
|
||||
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 { 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 today = new Date().toISOString().slice(0, 10);
|
||||
const { hasAccess, isLoading: isAuthLoading } = useAuth();
|
||||
const canCreateTenant = hasAccess("superadmin.tenant.create");
|
||||
const navigate = useNavigate();
|
||||
@@ -22,6 +32,9 @@ const AddTenants = () => {
|
||||
const [tenantDomain, setTenantDomain] = useState("");
|
||||
const [tenantLogoUrl, setTenantLogoUrl] = 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 [plans, setPlans] = useState<SubscriptionPlan[]>([]);
|
||||
@@ -43,6 +56,15 @@ const AddTenants = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
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(() => {
|
||||
if (isAuthLoading || canCreateTenant) {
|
||||
setIsTenantLoading(false);
|
||||
@@ -195,6 +217,9 @@ const AddTenants = () => {
|
||||
tenant_domain: tenantDomain.trim(),
|
||||
tenant_logo_url: tenantLogoUrl.trim() || undefined,
|
||||
plan_id: selectedPlanId,
|
||||
start_date: startDate || undefined,
|
||||
end_date: endDate || undefined,
|
||||
status: tenantStatus,
|
||||
module_environments: moduleEnvAssignments.filter((a) => a.environment_slug),
|
||||
};
|
||||
|
||||
@@ -313,6 +338,35 @@ const AddTenants = () => {
|
||||
/>
|
||||
</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 */}
|
||||
{selectedPlanId && (
|
||||
<div className="border-t border-gray-200 pt-6">
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
CustomCheckBox,
|
||||
CustomColumnFilter,
|
||||
CustomConfirmationModal,
|
||||
CustomDatePicker,
|
||||
CustomDropdown,
|
||||
CustomInput,
|
||||
CustomModal,
|
||||
@@ -22,12 +23,21 @@ import {
|
||||
} from "../../../components/custom/CustomColumnFilter.utils";
|
||||
import type { ColumnSortDirection } from "../../../components/custom/CustomColumnFilter";
|
||||
import { formatDate } from "../../../lib/dateFormat";
|
||||
import type { Tenant, TenantUpdateRequest } from "../TenantsTypes";
|
||||
import type { Tenant, TenantStatus, TenantUpdateRequest } from "../TenantsTypes";
|
||||
import { tenantsApi } from "../TenantsApi";
|
||||
import { useAuth } from "../../../context/AuthContext";
|
||||
import { subscriptionsApi } from "../../subscriptions/SubscriptionsApi";
|
||||
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 {
|
||||
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||
useEffect(() => {
|
||||
@@ -58,6 +68,9 @@ interface EditFormState {
|
||||
tenant_logo_url: string;
|
||||
is_active: boolean;
|
||||
plan_id: string;
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
status: TenantStatus;
|
||||
}
|
||||
|
||||
const AllTenants = () => {
|
||||
@@ -77,11 +90,13 @@ const AllTenants = () => {
|
||||
tenant_logo_url: "",
|
||||
is_active: true,
|
||||
plan_id: "",
|
||||
start_date: "",
|
||||
end_date: "",
|
||||
status: "ACTIVE",
|
||||
});
|
||||
|
||||
// Subscription plans for dropdown
|
||||
const [plans, setPlans] = useState<SubscriptionPlan[]>([]);
|
||||
const [isPlansLoading, setIsPlansLoading] = useState(false);
|
||||
|
||||
const [editError, setEditError] = useState("");
|
||||
const [deleteError, setDeleteError] = useState("");
|
||||
@@ -224,14 +239,11 @@ const AllTenants = () => {
|
||||
|
||||
let isMounted = true;
|
||||
const loadPlans = async () => {
|
||||
setIsPlansLoading(true);
|
||||
try {
|
||||
const data = await subscriptionsApi.getAll();
|
||||
if (isMounted) setPlans(data);
|
||||
} catch (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 ?? "",
|
||||
is_active: tenant.is_active,
|
||||
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,
|
||||
is_active: editForm.is_active,
|
||||
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);
|
||||
@@ -520,7 +538,27 @@ const AllTenants = () => {
|
||||
),
|
||||
searchable: false,
|
||||
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>
|
||||
)}
|
||||
</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>
|
||||
<p className="text-xs uppercase tracking-wide text-[var(--text-secondary)]">
|
||||
Status
|
||||
</p>
|
||||
<p className="text-sm font-medium text-[var(--text-primary)]">
|
||||
{selectedTenant.is_active ? "Active" : "Inactive"}
|
||||
</p>
|
||||
<CustomStatus status={selectedTenant.status} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-wide text-[var(--text-secondary)]">
|
||||
@@ -685,6 +729,22 @@ const AllTenants = () => {
|
||||
{getPlanName(selectedTenant.plan_id)}
|
||||
</p>
|
||||
</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>
|
||||
<p className="text-xs uppercase tracking-wide text-[var(--text-secondary)]">
|
||||
Created
|
||||
@@ -780,15 +840,64 @@ const AllTenants = () => {
|
||||
label="Select Plan"
|
||||
name="plan_id"
|
||||
value={editForm.plan_id}
|
||||
onChange={(e) =>
|
||||
setEditForm((prev) => ({ ...prev, plan_id: e.target.value }))
|
||||
}
|
||||
onChange={(e) => {
|
||||
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) => ({
|
||||
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,
|
||||
}))}
|
||||
placeholder={isPlansLoading ? "Loading plans..." : "Select a subscription plan"}
|
||||
disabled={isPlansLoading}
|
||||
placeholder="Select a subscription plan"
|
||||
/>
|
||||
</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>
|
||||
|
||||
|
||||
@@ -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 { useTranslation } from "react-i18next";
|
||||
import {
|
||||
@@ -146,6 +146,16 @@ const AppSidebar: React.FC = () => {
|
||||
[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(
|
||||
() => navItems.filter((item) => {
|
||||
if (!item.access) return true;
|
||||
@@ -161,32 +171,6 @@ const AppSidebar: React.FC = () => {
|
||||
|
||||
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) => {
|
||||
if (!isExpanded && !isMobile) {
|
||||
toggleSidebar(); // Auto expand when clicking submenu in collapsed state
|
||||
@@ -288,7 +272,7 @@ const AppSidebar: React.FC = () => {
|
||||
{visibleNavItems.map((nav) => {
|
||||
const active = isActive(nav.path);
|
||||
const hasSubmenu = nav.submenu && nav.submenu.length > 0;
|
||||
const isMenuOpen = openSubmenus[nav.name];
|
||||
const isMenuOpen = openSubmenus[nav.name] || isSubmenuRouteActive(nav.submenu);
|
||||
|
||||
if (hasSubmenu) {
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user