From 51bac652dfacc7019d1e13e8dcbe5dd726e5efc3 Mon Sep 17 00:00:00 2001
From: amee
Date: Sat, 18 Apr 2026 16:31:52 +0530
Subject: [PATCH] feat: tenant-subscription fix: deleted updatemodule
---
.../modules/admin/components/ModuleList.tsx | 2 +-
.../subscriptions/SubscriptionTypes.ts | 3 +
.../components/AddSubscriptions.tsx | 15 ++
.../components/AllSubscriptions.tsx | 35 +++++
src/application/tenants/TenantsTypes.ts | 12 ++
.../tenants/components/AddTenants.tsx | 58 +++++++-
.../tenants/components/AllTenants.tsx | 139 ++++++++++++++++--
src/components/layout/AppSidebar.tsx | 40 ++---
8 files changed, 258 insertions(+), 46 deletions(-)
diff --git a/src/application/modules/admin/components/ModuleList.tsx b/src/application/modules/admin/components/ModuleList.tsx
index 981001e..b9ca4db 100644
--- a/src/application/modules/admin/components/ModuleList.tsx
+++ b/src/application/modules/admin/components/ModuleList.tsx
@@ -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([]);
const [showForm, setShowForm] = useState(false);
const [selectedModule, setSelectedModule] = useState(null);
diff --git a/src/application/subscriptions/SubscriptionTypes.ts b/src/application/subscriptions/SubscriptionTypes.ts
index fa81502..4c75ea7 100644
--- a/src/application/subscriptions/SubscriptionTypes.ts
+++ b/src/application/subscriptions/SubscriptionTypes.ts
@@ -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[];
diff --git a/src/application/subscriptions/components/AddSubscriptions.tsx b/src/application/subscriptions/components/AddSubscriptions.tsx
index 8a8f3f9..23db135 100644
--- a/src/application/subscriptions/components/AddSubscriptions.tsx
+++ b/src/application/subscriptions/components/AddSubscriptions.tsx
@@ -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 = () => {
}))
}
/>
+
+ setFormData((prev) => ({
+ ...prev,
+ duration_days: e.target.value ? Number(e.target.value) : undefined,
+ }))
+ }
+ />
{
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 = () => {
),
},
+ {
+ key: "duration_days",
+ header: "Duration",
+ searchable: false,
+ render: (row) => (
+
+ {row.duration_days ? `${row.duration_days} days` : "-"}
+
+ ),
+ },
{
key: "status",
header: (
@@ -570,6 +584,14 @@ const AllSubscriptions = () => {
{formatPrice(selectedPlan.price)}
+
+
+ Duration
+
+
+ {selectedPlan.duration_days ? `${selectedPlan.duration_days} days` : "-"}
+
+
Status
@@ -689,6 +711,19 @@ const AllSubscriptions = () => {
}))
}
/>
+
+ setEditForm((prev) => ({
+ ...prev,
+ duration_days: e.target.value ? Number(e.target.value) : undefined,
+ }))
+ }
+ />
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("ACTIVE");
const [moduleEnvAssignments, setModuleEnvAssignments] = useState([]);
const [plans, setPlans] = useState([]);
@@ -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 = () => {
/>
+
+ {
+ const nextStartDate = e.target.value;
+ setStartDate(nextStartDate);
+ const selectedPlan = plans.find((plan) => plan.id === selectedPlanId);
+ setEndDate(addDays(nextStartDate, selectedPlan?.duration_days));
+ }}
+ />
+ setEndDate(e.target.value)}
+ min={startDate || undefined}
+ />
+ setTenantStatus(e.target.value as TenantStatus)}
+ options={[
+ { label: "Active", value: "ACTIVE" },
+ { label: "Inactive", value: "INACTIVE" },
+ { label: "Expired", value: "EXPIRED" },
+ ]}
+ />
+
+
{/* Module Environment Assignment — appears after plan selection */}
{selectedPlanId && (
diff --git a/src/application/tenants/components/AllTenants.tsx b/src/application/tenants/components/AllTenants.tsx
index ef8d7cd..15cccb2 100644
--- a/src/application/tenants/components/AllTenants.tsx
+++ b/src/application/tenants/components/AllTenants.tsx
@@ -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
(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([]);
- 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) => (
-
+
+ ),
+ },
+ {
+ key: "start_date",
+ header: "Start Date",
+ searchable: false,
+ render: (row) => (
+
+ {row.start_date ? formatDate(row.start_date) : "-"}
+
+ ),
+ },
+ {
+ key: "end_date",
+ header: "End Date",
+ searchable: false,
+ render: (row) => (
+
+ {row.end_date ? formatDate(row.end_date) : "-"}
+
),
},
{
@@ -669,13 +707,19 @@ const AllTenants = () => {
)}
+
+
+ Tenant ID
+
+
+ {selectedTenant.tenant_id}
+
+
Status
-
- {selectedTenant.is_active ? "Active" : "Inactive"}
-
+
@@ -685,6 +729,22 @@ const AllTenants = () => {
{getPlanName(selectedTenant.plan_id)}
+
+
+ Start Date
+
+
+ {selectedTenant.start_date ? formatDate(selectedTenant.start_date) : "-"}
+
+
+
+
+ End Date
+
+
+ {selectedTenant.end_date ? formatDate(selectedTenant.end_date) : "-"}
+
+
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"
+ />
+
+
+
+ {
+ 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),
+ }));
+ }}
+ />
+
+ setEditForm((prev) => ({ ...prev, end_date: e.target.value }))
+ }
+ />
+
+ setEditForm((prev) => ({
+ ...prev,
+ status: e.target.value as TenantStatus,
+ }))
+ }
+ options={[
+ { label: "Active", value: "ACTIVE" },
+ { label: "Inactive", value: "INACTIVE" },
+ { label: "Expired", value: "EXPIRED" },
+ ]}
/>
diff --git a/src/components/layout/AppSidebar.tsx b/src/components/layout/AppSidebar.tsx
index d992099..3f3d0e7 100644
--- a/src/components/layout/AppSidebar.tsx
+++ b/src/components/layout/AppSidebar.tsx
@@ -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>({});
- 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 (