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 (