feat: tenant-subscription
fix: deleted updatemodule
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
Reference in New Issue
Block a user