diff --git a/src/app/layouts/SuperadminLayout.tsx b/src/app/layouts/SuperadminLayout.tsx
new file mode 100644
index 0000000..c74942f
--- /dev/null
+++ b/src/app/layouts/SuperadminLayout.tsx
@@ -0,0 +1,386 @@
+import { useEffect, type ReactNode } from 'react';
+import { Link, useLocation } from 'react-router-dom';
+import {
+ LayoutDashboard,
+ Building2,
+ CreditCard,
+ Receipt,
+ Settings2,
+ BarChart3,
+ Menu,
+ X,
+ ChevronDown,
+ LogOut,
+ Search,
+ Bell,
+ Shield,
+ Users,
+ ToggleLeft,
+ Server,
+} from 'lucide-react';
+import { cn } from '@utils/cn';
+import { useUIStore } from '@stores/useUIStore';
+import { useAuthStore } from '@stores/useAuthStore';
+import { useIsMobile } from '@hooks/useMediaQuery';
+import { ThemeToggle } from '@components/atoms/ThemeToggle';
+import { CommandPaletteWithRouter, useCommandPalette } from '@components/organisms/CommandPalette';
+
+interface SuperadminLayoutProps {
+ children: ReactNode;
+}
+
+/**
+ * Navigation item type for superadmin
+ */
+interface SuperadminNavigationItem {
+ name: string;
+ href: string;
+ icon: React.ComponentType<{ className?: string }>;
+ badge?: string | number;
+ children?: SuperadminNavigationItem[];
+}
+
+/**
+ * Search button component that opens the command palette
+ */
+function SearchButton() {
+ const { open } = useCommandPalette();
+ const isMac = typeof navigator !== 'undefined' && navigator.platform.toUpperCase().indexOf('MAC') >= 0;
+
+ return (
+
+
+ Buscar...
+
+ {isMac ? '⌘' : 'Ctrl'}+K
+
+
+ );
+}
+
+/**
+ * Superadmin-specific navigation items
+ */
+const superadminNavigation: SuperadminNavigationItem[] = [
+ {
+ name: 'Dashboard',
+ href: '/superadmin/dashboard',
+ icon: LayoutDashboard,
+ },
+ {
+ name: 'Tenants',
+ href: '/superadmin/tenants',
+ icon: Building2,
+ children: [
+ { name: 'Lista de Tenants', href: '/superadmin/tenants', icon: Building2 },
+ { name: 'Usuarios por Tenant', href: '/superadmin/tenants/users', icon: Users },
+ ],
+ },
+ {
+ name: 'Facturación',
+ href: '/superadmin/billing',
+ icon: CreditCard,
+ children: [
+ { name: 'Suscripciones', href: '/superadmin/billing/subscriptions', icon: CreditCard },
+ { name: 'Facturas', href: '/superadmin/billing/invoices', icon: Receipt },
+ ],
+ },
+ {
+ name: 'Sistema',
+ href: '/superadmin/system',
+ icon: Settings2,
+ children: [
+ { name: 'Feature Flags', href: '/superadmin/system/feature-flags', icon: ToggleLeft },
+ { name: 'Configuración', href: '/superadmin/system/config', icon: Server },
+ ],
+ },
+ {
+ name: 'Analytics',
+ href: '/superadmin/analytics',
+ icon: BarChart3,
+ },
+];
+
+/**
+ * Internal layout component (used inside CommandPaletteWithRouter)
+ */
+function SuperadminLayoutInner({ children }: SuperadminLayoutProps) {
+ const location = useLocation();
+ const isMobile = useIsMobile();
+ const { sidebarOpen, sidebarCollapsed, toggleSidebar, setSidebarOpen, setIsMobile } = useUIStore();
+ const { user, logout } = useAuthStore();
+
+ useEffect(() => {
+ setIsMobile(isMobile);
+ }, [isMobile, setIsMobile]);
+
+ // Close sidebar on mobile when route changes
+ useEffect(() => {
+ if (isMobile) {
+ setSidebarOpen(false);
+ }
+ }, [location.pathname, isMobile, setSidebarOpen]);
+
+ /**
+ * Check if a navigation item is active
+ */
+ const isItemActive = (href: string): boolean => {
+ return location.pathname === href || location.pathname.startsWith(href + '/');
+ };
+
+ /**
+ * Check if any child of a navigation item is active
+ */
+ const isParentActive = (item: SuperadminNavigationItem): boolean => {
+ if (item.children) {
+ return item.children.some(child => isItemActive(child.href));
+ }
+ return isItemActive(item.href);
+ };
+
+ return (
+
+ {/* Mobile sidebar backdrop */}
+ {isMobile && sidebarOpen && (
+
setSidebarOpen(false)}
+ />
+ )}
+
+ {/* Sidebar */}
+
+ {/* Logo */}
+
+ {(!sidebarCollapsed || isMobile) && (
+
+
+
+
+
+ Superadmin
+ Portal
+
+
+ )}
+ {sidebarCollapsed && !isMobile && (
+
+
+
+
+
+ )}
+ {isMobile && (
+
setSidebarOpen(false)} className="p-2 text-indigo-300 hover:text-white" aria-label="Cerrar menú">
+
+
+ )}
+
+
+ {/* Navigation */}
+
+ {superadminNavigation.map((item) => {
+ const isActive = isParentActive(item);
+ const Icon = item.icon;
+ const hasChildren = item.children && item.children.length > 0;
+
+ return (
+
+
+
+ {(!sidebarCollapsed || isMobile) && (
+ <>
+
{item.name}
+ {item.badge && (
+
+ {item.badge}
+
+ )}
+ >
+ )}
+
+
+ {/* Sub-navigation items */}
+ {hasChildren && isActive && (!sidebarCollapsed || isMobile) && (
+
+ {item.children!.map((child) => {
+ const isChildActive = isItemActive(child.href);
+ const ChildIcon = child.icon;
+ return (
+
+
+ {child.name}
+
+ );
+ })}
+
+ )}
+
+ );
+ })}
+
+
+ {/* User menu */}
+
+ {(!sidebarCollapsed || isMobile) ? (
+
+
+ {user?.firstName?.[0]}{user?.lastName?.[0]}
+
+
+
+ {user?.firstName} {user?.lastName}
+
+
{user?.email}
+
+
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+
+ {/* Main content */}
+
+ {/* Top bar */}
+
+
+ {/* Page content */}
+
{children}
+
+
+ );
+}
+
+/**
+ * Superadmin Layout with Command Palette support
+ *
+ * This layout is designed specifically for the superadmin portal with:
+ * - Indigo/purple color scheme to distinguish from regular dashboard
+ * - Superadmin-specific navigation (Tenants, Billing, System, Analytics)
+ * - Command palette support (Ctrl+K / Cmd+K)
+ * - Dark mode support
+ * - Responsive design with mobile hamburger menu
+ *
+ * @example
+ * ```tsx
+ * function SuperadminDashboard() {
+ * return (
+ *
+ * Superadmin Dashboard
+ *
+ * );
+ * }
+ * ```
+ */
+export function SuperadminLayout({ children }: SuperadminLayoutProps) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/app/layouts/index.ts b/src/app/layouts/index.ts
index a3acefd..e22494c 100644
--- a/src/app/layouts/index.ts
+++ b/src/app/layouts/index.ts
@@ -1,2 +1,3 @@
export * from './AuthLayout';
export * from './DashboardLayout';
+export * from './SuperadminLayout';
diff --git a/src/app/router/routes.tsx b/src/app/router/routes.tsx
index 04ff51c..d1ab4e3 100644
--- a/src/app/router/routes.tsx
+++ b/src/app/router/routes.tsx
@@ -2,6 +2,7 @@ import { lazy, Suspense } from 'react';
import { createBrowserRouter, Navigate } from 'react-router-dom';
import { ProtectedRoute } from './ProtectedRoute';
import { DashboardLayout } from '@app/layouts/DashboardLayout';
+import { SuperadminLayout } from '@app/layouts/SuperadminLayout';
import { FullPageSpinner } from '@components/atoms/Spinner';
// Lazy load pages
@@ -78,6 +79,13 @@ const StockAdjustmentPage = lazy(() => import('@pages/inventory/StockAdjustmentP
const WarehousesListPage = lazy(() => import('@pages/inventory/WarehousesListPage').then(m => ({ default: m.WarehousesListPage })));
const WarehouseDetailPage = lazy(() => import('@pages/inventory/WarehouseDetailPage').then(m => ({ default: m.WarehouseDetailPage })));
+// Superadmin pages
+const SuperadminDashboardPage = lazy(() => import('@pages/superadmin/SuperadminDashboardPage').then(m => ({ default: m.SuperadminDashboardPage })));
+const TenantsListPage = lazy(() => import('@pages/superadmin/tenants/TenantsListPage').then(m => ({ default: m.TenantsListPage })));
+const TenantDetailPage = lazy(() => import('@pages/superadmin/tenants/TenantDetailPage').then(m => ({ default: m.TenantDetailPage })));
+const TenantCreatePage = lazy(() => import('@pages/superadmin/tenants/TenantCreatePage').then(m => ({ default: m.TenantCreatePage })));
+const TenantEditPage = lazy(() => import('@pages/superadmin/tenants/TenantEditPage').then(m => ({ default: m.TenantEditPage })));
+
function LazyWrapper({ children }: { children: React.ReactNode }) {
return
}>{children};
}
@@ -92,6 +100,16 @@ function DashboardWrapper({ children }: { children: React.ReactNode }) {
);
}
+function SuperadminWrapper({ children }: { children: React.ReactNode }) {
+ return (
+
+
+ {children}
+
+
+ );
+}
+
export const router = createBrowserRouter([
// Public routes
{
@@ -602,6 +620,84 @@ export const router = createBrowserRouter([
),
},
+ // Superadmin routes
+ {
+ path: '/superadmin',
+ element:
,
+ },
+ {
+ path: '/superadmin/dashboard',
+ element: (
+
+
+
+ ),
+ },
+ {
+ path: '/superadmin/tenants',
+ element: (
+
+
+
+ ),
+ },
+ {
+ path: '/superadmin/tenants/new',
+ element: (
+
+
+
+ ),
+ },
+ {
+ path: '/superadmin/tenants/:id',
+ element: (
+
+
+
+ ),
+ },
+ {
+ path: '/superadmin/tenants/:id/edit',
+ element: (
+
+
+
+ ),
+ },
+ {
+ path: '/superadmin/billing/*',
+ element: (
+
+ Gestión de Facturación Global - En desarrollo
+
+ ),
+ },
+ {
+ path: '/superadmin/system/*',
+ element: (
+
+ Configuración del Sistema - En desarrollo
+
+ ),
+ },
+ {
+ path: '/superadmin/analytics',
+ element: (
+
+ Analytics Global - En desarrollo
+
+ ),
+ },
+ {
+ path: '/superadmin/*',
+ element: (
+
+ Portal Superadmin - En desarrollo
+
+ ),
+ },
+
// Error pages
{
path: '/unauthorized',
diff --git a/src/features/superadmin/api/index.ts b/src/features/superadmin/api/index.ts
new file mode 100644
index 0000000..e33a40e
--- /dev/null
+++ b/src/features/superadmin/api/index.ts
@@ -0,0 +1 @@
+export { tenantsApi, type TenantDashboardStats } from './tenants.api';
diff --git a/src/features/superadmin/api/tenants.api.ts b/src/features/superadmin/api/tenants.api.ts
new file mode 100644
index 0000000..0762d80
--- /dev/null
+++ b/src/features/superadmin/api/tenants.api.ts
@@ -0,0 +1,133 @@
+/**
+ * Tenants API - Superadmin Portal
+ * API client for tenant management operations
+ */
+
+import { api } from '@services/api/axios-instance';
+import type {
+ Tenant,
+ TenantStats,
+ TenantFilters,
+ TenantsResponse,
+ CreateTenantDto,
+ UpdateTenantDto,
+ SuspendTenantDto,
+ ActivateTenantDto,
+ TenantWithStats,
+} from '../types';
+
+const BASE_URL = '/api/v1/superadmin/tenants';
+
+export interface TenantDashboardStats {
+ total: number;
+ active: number;
+ trial: number;
+ suspended: number;
+ cancelled: number;
+ pending: number;
+}
+
+export const tenantsApi = {
+ /**
+ * Get all tenants with pagination and filters
+ */
+ async getAll(filters?: TenantFilters): Promise
{
+ const params = new URLSearchParams();
+
+ if (filters?.search) params.append('search', filters.search);
+ if (filters?.status) params.append('status', filters.status);
+ if (filters?.planTier) params.append('planTier', filters.planTier);
+ if (filters?.billingCycle) params.append('billingCycle', filters.billingCycle);
+ if (filters?.page) params.append('page', filters.page.toString());
+ if (filters?.limit) params.append('limit', filters.limit.toString());
+ if (filters?.sortBy) params.append('sortBy', filters.sortBy);
+ if (filters?.sortOrder) params.append('sortOrder', filters.sortOrder);
+ if (filters?.createdAfter) params.append('createdAfter', filters.createdAfter);
+ if (filters?.createdBefore) params.append('createdBefore', filters.createdBefore);
+ if (filters?.hasOverduePayment !== undefined) {
+ params.append('hasOverduePayment', filters.hasOverduePayment.toString());
+ }
+
+ const url = params.toString() ? `${BASE_URL}?${params}` : BASE_URL;
+ const response = await api.get(url);
+ return response.data;
+ },
+
+ /**
+ * Get tenant by ID
+ */
+ async getById(id: string): Promise {
+ const response = await api.get(`${BASE_URL}/${id}`);
+ return response.data;
+ },
+
+ /**
+ * Get tenant with stats
+ */
+ async getWithStats(id: string): Promise {
+ const response = await api.get(`${BASE_URL}/${id}/with-stats`);
+ return response.data;
+ },
+
+ /**
+ * Get tenant statistics
+ */
+ async getStats(id: string): Promise {
+ const response = await api.get(`${BASE_URL}/${id}/stats`);
+ return response.data;
+ },
+
+ /**
+ * Get dashboard statistics (counts by status)
+ */
+ async getDashboardStats(): Promise {
+ const response = await api.get(`${BASE_URL}/dashboard-stats`);
+ return response.data;
+ },
+
+ /**
+ * Create new tenant
+ */
+ async create(data: CreateTenantDto): Promise {
+ const response = await api.post(BASE_URL, data);
+ return response.data;
+ },
+
+ /**
+ * Update tenant
+ */
+ async update(id: string, data: UpdateTenantDto): Promise {
+ const response = await api.patch(`${BASE_URL}/${id}`, data);
+ return response.data;
+ },
+
+ /**
+ * Suspend tenant
+ */
+ async suspend(id: string, data: SuspendTenantDto): Promise {
+ const response = await api.post(`${BASE_URL}/${id}/suspend`, data);
+ return response.data;
+ },
+
+ /**
+ * Activate tenant
+ */
+ async activate(id: string, data?: ActivateTenantDto): Promise {
+ const response = await api.post(`${BASE_URL}/${id}/activate`, data || {});
+ return response.data;
+ },
+
+ /**
+ * Delete tenant (soft delete)
+ */
+ async delete(id: string): Promise {
+ await api.delete(`${BASE_URL}/${id}`);
+ },
+
+ /**
+ * Permanently delete tenant (hard delete)
+ */
+ async permanentDelete(id: string): Promise {
+ await api.delete(`${BASE_URL}/${id}/permanent`);
+ },
+};
diff --git a/src/features/superadmin/hooks/index.ts b/src/features/superadmin/hooks/index.ts
new file mode 100644
index 0000000..119176d
--- /dev/null
+++ b/src/features/superadmin/hooks/index.ts
@@ -0,0 +1,23 @@
+/**
+ * Superadmin Hooks - Index
+ * Exports all hooks for the superadmin portal
+ */
+
+export { useSuperadminStats } from './useSuperadminStats';
+export type {
+ SuperadminOverviewStats,
+ TenantGrowthDataPoint,
+ PlanDistribution,
+ RevenueDataPoint,
+ RecentTenantActivity,
+ TopTenant,
+ SystemHealthMetrics,
+ UseSuperadminStatsReturn,
+} from './useSuperadminStats';
+
+export {
+ useTenants,
+ useTenant,
+ useTenantUsageHistory,
+ useTenantSubscription,
+} from './useTenants';
diff --git a/src/features/superadmin/hooks/useSuperadminStats.ts b/src/features/superadmin/hooks/useSuperadminStats.ts
new file mode 100644
index 0000000..b738226
--- /dev/null
+++ b/src/features/superadmin/hooks/useSuperadminStats.ts
@@ -0,0 +1,417 @@
+/**
+ * useSuperadminStats Hook
+ * Provides statistics and data for the Superadmin Dashboard
+ */
+
+import { useState, useCallback, useEffect } from 'react';
+import type {
+ TenantActivity,
+ TenantPlanTier,
+} from '../types';
+
+// ==================== Types ====================
+
+export interface SuperadminOverviewStats {
+ totalTenants: number;
+ activeSubscriptions: number;
+ monthlyRecurringRevenue: number;
+ totalUsersAcrossTenants: number;
+ trialTenants: number;
+ suspendedTenants: number;
+ // Change metrics (vs previous period)
+ tenantsChange: number;
+ subscriptionsChange: number;
+ mrrChange: number;
+ usersChange: number;
+}
+
+export interface TenantGrowthDataPoint {
+ month: string;
+ totalTenants: number;
+ newTenants: number;
+ churnedTenants: number;
+}
+
+export interface PlanDistribution {
+ plan: TenantPlanTier;
+ planLabel: string;
+ count: number;
+ percentage: number;
+ color: string;
+}
+
+export interface RevenueDataPoint {
+ month: string;
+ revenue: number;
+ recurring: number;
+ oneTime: number;
+}
+
+export interface RecentTenantActivity {
+ id: string;
+ type: TenantActivity['type'];
+ tenantName: string;
+ description: string;
+ timestamp: string;
+ icon: 'signup' | 'upgrade' | 'downgrade' | 'suspension' | 'payment' | 'other';
+}
+
+export interface TopTenant {
+ id: string;
+ name: string;
+ slug: string;
+ plan: TenantPlanTier;
+ usersCount: number;
+ monthlyRevenue: number;
+ storageUsedGb: number;
+ lastActiveAt: string;
+}
+
+export interface SystemHealthMetrics {
+ apiResponseTimeMs: number;
+ apiResponseTimeTrend: 'up' | 'down' | 'stable';
+ errorRate: number;
+ errorRateTrend: 'up' | 'down' | 'stable';
+ uptimePercent: number;
+ activeConnections: number;
+ queuedJobs: number;
+ lastHealthCheck: string;
+}
+
+export interface UseSuperadminStatsReturn {
+ // Data
+ overview: SuperadminOverviewStats | null;
+ tenantGrowth: TenantGrowthDataPoint[];
+ planDistribution: PlanDistribution[];
+ revenueTrend: RevenueDataPoint[];
+ recentActivity: RecentTenantActivity[];
+ topTenants: TopTenant[];
+ systemHealth: SystemHealthMetrics | null;
+ // State
+ isLoading: boolean;
+ error: string | null;
+ // Actions
+ refresh: () => Promise;
+ refreshOverview: () => Promise;
+ refreshActivity: () => Promise;
+ refreshSystemHealth: () => Promise;
+}
+
+// ==================== Mock Data Generation ====================
+
+const PLAN_COLORS: Record = {
+ free: '#9CA3AF',
+ starter: '#3B82F6',
+ professional: '#8B5CF6',
+ enterprise: '#F59E0B',
+ custom: '#10B981',
+};
+
+const PLAN_LABELS: Record = {
+ free: 'Gratuito',
+ starter: 'Inicial',
+ professional: 'Profesional',
+ enterprise: 'Empresarial',
+ custom: 'Personalizado',
+};
+
+function generateMockOverviewStats(): SuperadminOverviewStats {
+ return {
+ totalTenants: 247,
+ activeSubscriptions: 189,
+ monthlyRecurringRevenue: 485600,
+ totalUsersAcrossTenants: 3842,
+ trialTenants: 34,
+ suspendedTenants: 8,
+ tenantsChange: 12.5,
+ subscriptionsChange: 8.3,
+ mrrChange: 15.7,
+ usersChange: 22.1,
+ };
+}
+
+function generateMockTenantGrowth(): TenantGrowthDataPoint[] {
+ const months = ['Mar', 'Abr', 'May', 'Jun', 'Jul', 'Ago', 'Sep', 'Oct', 'Nov', 'Dic', 'Ene', 'Feb'];
+ let total = 150;
+ return months.map((month) => {
+ const newTenants = Math.floor(Math.random() * 15) + 5;
+ const churnedTenants = Math.floor(Math.random() * 5);
+ total = total + newTenants - churnedTenants;
+ return {
+ month,
+ totalTenants: total,
+ newTenants,
+ churnedTenants,
+ };
+ });
+}
+
+function generateMockPlanDistribution(): PlanDistribution[] {
+ const data: Array<{ plan: TenantPlanTier; count: number }> = [
+ { plan: 'free', count: 45 },
+ { plan: 'starter', count: 78 },
+ { plan: 'professional', count: 89 },
+ { plan: 'enterprise', count: 28 },
+ { plan: 'custom', count: 7 },
+ ];
+ const total = data.reduce((acc, item) => acc + item.count, 0);
+ return data.map((item) => ({
+ plan: item.plan,
+ planLabel: PLAN_LABELS[item.plan],
+ count: item.count,
+ percentage: Math.round((item.count / total) * 100),
+ color: PLAN_COLORS[item.plan],
+ }));
+}
+
+function generateMockRevenueTrend(): RevenueDataPoint[] {
+ const months = ['Mar', 'Abr', 'May', 'Jun', 'Jul', 'Ago', 'Sep', 'Oct', 'Nov', 'Dic', 'Ene', 'Feb'];
+ let baseRevenue = 320000;
+ return months.map((month) => {
+ const growth = 1 + (Math.random() * 0.08 - 0.02);
+ baseRevenue = Math.round(baseRevenue * growth);
+ const recurring = Math.round(baseRevenue * 0.85);
+ const oneTime = baseRevenue - recurring;
+ return {
+ month,
+ revenue: baseRevenue,
+ recurring,
+ oneTime,
+ };
+ });
+}
+
+function generateMockRecentActivity(): RecentTenantActivity[] {
+ const activities: RecentTenantActivity[] = [
+ {
+ id: '1',
+ type: 'created',
+ tenantName: 'Construcciones del Norte',
+ description: 'Nuevo registro - Plan Trial',
+ timestamp: new Date(Date.now() - 1000 * 60 * 15).toISOString(),
+ icon: 'signup',
+ },
+ {
+ id: '2',
+ type: 'plan_changed',
+ tenantName: 'Metalurgica ABC',
+ description: 'Upgrade: Starter -> Professional',
+ timestamp: new Date(Date.now() - 1000 * 60 * 45).toISOString(),
+ icon: 'upgrade',
+ },
+ {
+ id: '3',
+ type: 'payment_received',
+ tenantName: 'Distribuidora XYZ',
+ description: 'Pago recibido: $4,500 MXN',
+ timestamp: new Date(Date.now() - 1000 * 60 * 120).toISOString(),
+ icon: 'payment',
+ },
+ {
+ id: '4',
+ type: 'created',
+ tenantName: 'Servicios Industriales MX',
+ description: 'Nuevo registro - Plan Starter',
+ timestamp: new Date(Date.now() - 1000 * 60 * 180).toISOString(),
+ icon: 'signup',
+ },
+ {
+ id: '5',
+ type: 'suspended',
+ tenantName: 'Comercializadora Sur',
+ description: 'Suspendido: Pago vencido (45 dias)',
+ timestamp: new Date(Date.now() - 1000 * 60 * 240).toISOString(),
+ icon: 'suspension',
+ },
+ {
+ id: '6',
+ type: 'plan_changed',
+ tenantName: 'Tech Solutions',
+ description: 'Downgrade: Enterprise -> Professional',
+ timestamp: new Date(Date.now() - 1000 * 60 * 300).toISOString(),
+ icon: 'downgrade',
+ },
+ {
+ id: '7',
+ type: 'created',
+ tenantName: 'Alimentos Premium',
+ description: 'Nuevo registro - Plan Enterprise',
+ timestamp: new Date(Date.now() - 1000 * 60 * 360).toISOString(),
+ icon: 'signup',
+ },
+ {
+ id: '8',
+ type: 'activated',
+ tenantName: 'Logistica Express',
+ description: 'Reactivado despues de suspension',
+ timestamp: new Date(Date.now() - 1000 * 60 * 420).toISOString(),
+ icon: 'other',
+ },
+ ];
+ return activities;
+}
+
+function generateMockTopTenants(): TopTenant[] {
+ return [
+ {
+ id: '1',
+ name: 'Grupo Industrial Monterrey',
+ slug: 'grupo-industrial-mty',
+ plan: 'enterprise',
+ usersCount: 245,
+ monthlyRevenue: 45000,
+ storageUsedGb: 128.5,
+ lastActiveAt: new Date(Date.now() - 1000 * 60 * 5).toISOString(),
+ },
+ {
+ id: '2',
+ name: 'Constructora Nacional',
+ slug: 'constructora-nacional',
+ plan: 'enterprise',
+ usersCount: 189,
+ monthlyRevenue: 38000,
+ storageUsedGb: 95.2,
+ lastActiveAt: new Date(Date.now() - 1000 * 60 * 12).toISOString(),
+ },
+ {
+ id: '3',
+ name: 'Distribuidora del Bajio',
+ slug: 'dist-bajio',
+ plan: 'professional',
+ usersCount: 124,
+ monthlyRevenue: 18500,
+ storageUsedGb: 67.8,
+ lastActiveAt: new Date(Date.now() - 1000 * 60 * 8).toISOString(),
+ },
+ {
+ id: '4',
+ name: 'Metalmecanica Avanzada',
+ slug: 'metalmecanica-avz',
+ plan: 'professional',
+ usersCount: 98,
+ monthlyRevenue: 15200,
+ storageUsedGb: 45.3,
+ lastActiveAt: new Date(Date.now() - 1000 * 60 * 25).toISOString(),
+ },
+ {
+ id: '5',
+ name: 'Comercializadora Express',
+ slug: 'comercializadora-exp',
+ plan: 'professional',
+ usersCount: 87,
+ monthlyRevenue: 12800,
+ storageUsedGb: 38.9,
+ lastActiveAt: new Date(Date.now() - 1000 * 60 * 45).toISOString(),
+ },
+ ];
+}
+
+function generateMockSystemHealth(): SystemHealthMetrics {
+ return {
+ apiResponseTimeMs: 127,
+ apiResponseTimeTrend: 'stable',
+ errorRate: 0.12,
+ errorRateTrend: 'down',
+ uptimePercent: 99.97,
+ activeConnections: 1247,
+ queuedJobs: 23,
+ lastHealthCheck: new Date().toISOString(),
+ };
+}
+
+// ==================== Hook ====================
+
+export function useSuperadminStats(): UseSuperadminStatsReturn {
+ const [overview, setOverview] = useState(null);
+ const [tenantGrowth, setTenantGrowth] = useState([]);
+ const [planDistribution, setPlanDistribution] = useState([]);
+ const [revenueTrend, setRevenueTrend] = useState([]);
+ const [recentActivity, setRecentActivity] = useState([]);
+ const [topTenants, setTopTenants] = useState([]);
+ const [systemHealth, setSystemHealth] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ const fetchAllData = useCallback(async () => {
+ setIsLoading(true);
+ setError(null);
+ try {
+ // Simulate API calls with mock data
+ // In production, replace with actual API calls:
+ // const [overviewData, growthData, ...] = await Promise.all([
+ // superadminApi.getOverview(),
+ // superadminApi.getTenantGrowth(),
+ // ...
+ // ]);
+
+ await new Promise((resolve) => setTimeout(resolve, 800));
+
+ setOverview(generateMockOverviewStats());
+ setTenantGrowth(generateMockTenantGrowth());
+ setPlanDistribution(generateMockPlanDistribution());
+ setRevenueTrend(generateMockRevenueTrend());
+ setRecentActivity(generateMockRecentActivity());
+ setTopTenants(generateMockTopTenants());
+ setSystemHealth(generateMockSystemHealth());
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Error al cargar estadisticas');
+ } finally {
+ setIsLoading(false);
+ }
+ }, []);
+
+ const refreshOverview = useCallback(async () => {
+ try {
+ await new Promise((resolve) => setTimeout(resolve, 300));
+ setOverview(generateMockOverviewStats());
+ } catch (err) {
+ console.error('Error refreshing overview:', err);
+ }
+ }, []);
+
+ const refreshActivity = useCallback(async () => {
+ try {
+ await new Promise((resolve) => setTimeout(resolve, 300));
+ setRecentActivity(generateMockRecentActivity());
+ } catch (err) {
+ console.error('Error refreshing activity:', err);
+ }
+ }, []);
+
+ const refreshSystemHealth = useCallback(async () => {
+ try {
+ await new Promise((resolve) => setTimeout(resolve, 200));
+ setSystemHealth(generateMockSystemHealth());
+ } catch (err) {
+ console.error('Error refreshing system health:', err);
+ }
+ }, []);
+
+ useEffect(() => {
+ fetchAllData();
+ }, [fetchAllData]);
+
+ // Auto-refresh system health every 30 seconds
+ useEffect(() => {
+ const interval = setInterval(() => {
+ refreshSystemHealth();
+ }, 30000);
+ return () => clearInterval(interval);
+ }, [refreshSystemHealth]);
+
+ return {
+ overview,
+ tenantGrowth,
+ planDistribution,
+ revenueTrend,
+ recentActivity,
+ topTenants,
+ systemHealth,
+ isLoading,
+ error,
+ refresh: fetchAllData,
+ refreshOverview,
+ refreshActivity,
+ refreshSystemHealth,
+ };
+}
diff --git a/src/features/superadmin/hooks/useTenants.ts b/src/features/superadmin/hooks/useTenants.ts
new file mode 100644
index 0000000..f8d5383
--- /dev/null
+++ b/src/features/superadmin/hooks/useTenants.ts
@@ -0,0 +1,818 @@
+import { useState, useCallback, useEffect } from 'react';
+import type {
+ Tenant,
+ TenantStats,
+ TenantFilters,
+ CreateTenantDto,
+ UpdateTenantDto,
+ SuspendTenantDto,
+ ActivateTenantDto,
+ ChangeTenantPlanDto,
+ TenantWithStats,
+ TenantSubscription,
+ TenantUsageHistory,
+} from '../types';
+
+// ==================== Mock Data ====================
+
+const MOCK_TENANTS: Tenant[] = [
+ {
+ id: 'tenant-001',
+ name: 'Acme Corporation',
+ slug: 'acme-corp',
+ domain: 'acme.erp-example.com',
+ subdomain: 'acme',
+ status: 'active',
+ planTier: 'enterprise',
+ planId: 'plan-enterprise',
+ planName: 'Enterprise',
+ billingCycle: 'annual',
+ ownerName: 'John Smith',
+ ownerEmail: 'john.smith@acme.com',
+ phone: '+1-555-0100',
+ timezone: 'America/New_York',
+ locale: 'en-US',
+ currency: 'USD',
+ maxUsers: 100,
+ maxBranches: 25,
+ maxStorageGb: 500,
+ maxApiCallsMonthly: 1000000,
+ enabledModules: ['crm', 'inventory', 'sales', 'purchases', 'hr', 'accounting'],
+ features: { advancedReporting: true, apiAccess: true, customBranding: true, sso: true },
+ createdAt: '2025-01-15T10:00:00Z',
+ updatedAt: '2026-01-20T14:30:00Z',
+ lastActiveAt: '2026-02-03T09:15:00Z',
+ },
+ {
+ id: 'tenant-002',
+ name: 'TechStart Inc',
+ slug: 'techstart',
+ subdomain: 'techstart',
+ status: 'active',
+ planTier: 'professional',
+ planId: 'plan-professional',
+ planName: 'Professional',
+ billingCycle: 'monthly',
+ ownerName: 'Sarah Johnson',
+ ownerEmail: 'sarah@techstart.io',
+ phone: '+1-555-0101',
+ timezone: 'America/Los_Angeles',
+ locale: 'en-US',
+ currency: 'USD',
+ maxUsers: 25,
+ maxBranches: 5,
+ maxStorageGb: 100,
+ maxApiCallsMonthly: 250000,
+ enabledModules: ['crm', 'inventory', 'sales'],
+ features: { advancedReporting: true, apiAccess: true, customBranding: false, sso: false },
+ createdAt: '2025-06-01T08:00:00Z',
+ updatedAt: '2026-02-01T11:20:00Z',
+ lastActiveAt: '2026-02-03T08:45:00Z',
+ },
+ {
+ id: 'tenant-003',
+ name: 'Global Retail Co',
+ slug: 'global-retail',
+ domain: 'retail.erp-example.com',
+ subdomain: 'globalretail',
+ status: 'suspended',
+ planTier: 'enterprise',
+ planId: 'plan-enterprise',
+ planName: 'Enterprise',
+ billingCycle: 'annual',
+ ownerName: 'Michael Chen',
+ ownerEmail: 'mchen@globalretail.com',
+ phone: '+1-555-0102',
+ timezone: 'Asia/Shanghai',
+ locale: 'zh-CN',
+ currency: 'CNY',
+ maxUsers: 200,
+ maxBranches: 50,
+ maxStorageGb: 1000,
+ maxApiCallsMonthly: 2000000,
+ enabledModules: ['crm', 'inventory', 'sales', 'purchases', 'hr', 'accounting', 'pos'],
+ features: { advancedReporting: true, apiAccess: true, customBranding: true, sso: true },
+ createdAt: '2024-11-01T06:00:00Z',
+ updatedAt: '2026-01-28T09:00:00Z',
+ suspendedAt: '2026-01-28T09:00:00Z',
+ suspensionReason: 'Payment overdue - 45 days',
+ lastActiveAt: '2026-01-15T12:00:00Z',
+ },
+ {
+ id: 'tenant-004',
+ name: 'StartupLabs',
+ slug: 'startuplabs',
+ subdomain: 'startuplabs',
+ status: 'trial',
+ planTier: 'starter',
+ planId: 'plan-starter',
+ planName: 'Starter',
+ billingCycle: 'monthly',
+ ownerName: 'Emily Davis',
+ ownerEmail: 'emily@startuplabs.co',
+ timezone: 'Europe/London',
+ locale: 'en-GB',
+ currency: 'GBP',
+ maxUsers: 5,
+ maxBranches: 1,
+ maxStorageGb: 10,
+ maxApiCallsMonthly: 50000,
+ enabledModules: ['crm', 'sales'],
+ features: { advancedReporting: false, apiAccess: false, customBranding: false, sso: false },
+ createdAt: '2026-01-25T14:00:00Z',
+ updatedAt: '2026-01-25T14:00:00Z',
+ trialEndsAt: '2026-02-24T14:00:00Z',
+ lastActiveAt: '2026-02-02T16:30:00Z',
+ },
+ {
+ id: 'tenant-005',
+ name: 'MedCare Solutions',
+ slug: 'medcare',
+ domain: 'erp.medcare.health',
+ subdomain: 'medcare',
+ status: 'active',
+ planTier: 'professional',
+ planId: 'plan-professional',
+ planName: 'Professional',
+ billingCycle: 'annual',
+ ownerName: 'Dr. Robert Wilson',
+ ownerEmail: 'rwilson@medcare.health',
+ phone: '+1-555-0105',
+ timezone: 'America/Chicago',
+ locale: 'en-US',
+ currency: 'USD',
+ maxUsers: 50,
+ maxBranches: 10,
+ maxStorageGb: 250,
+ maxApiCallsMonthly: 500000,
+ enabledModules: ['crm', 'inventory', 'sales', 'hr'],
+ features: { advancedReporting: true, apiAccess: true, customBranding: true, sso: false },
+ createdAt: '2025-03-10T12:00:00Z',
+ updatedAt: '2026-01-15T10:00:00Z',
+ lastActiveAt: '2026-02-03T07:00:00Z',
+ },
+ {
+ id: 'tenant-006',
+ name: 'BuildRight Construction',
+ slug: 'buildright',
+ subdomain: 'buildright',
+ status: 'pending',
+ planTier: 'professional',
+ planId: 'plan-professional',
+ planName: 'Professional',
+ billingCycle: 'monthly',
+ ownerName: 'James Martinez',
+ ownerEmail: 'jmartinez@buildright.com',
+ phone: '+1-555-0106',
+ timezone: 'America/Denver',
+ locale: 'en-US',
+ currency: 'USD',
+ maxUsers: 30,
+ maxBranches: 8,
+ maxStorageGb: 150,
+ maxApiCallsMonthly: 300000,
+ enabledModules: ['crm', 'inventory', 'sales', 'purchases', 'projects'],
+ features: { advancedReporting: true, apiAccess: true, customBranding: false, sso: false },
+ createdAt: '2026-02-01T09:00:00Z',
+ updatedAt: '2026-02-01T09:00:00Z',
+ },
+];
+
+const MOCK_TENANT_STATS: Record = {
+ 'tenant-001': {
+ tenantId: 'tenant-001',
+ totalUsers: 78,
+ activeUsers: 65,
+ inactiveUsers: 13,
+ pendingInvitations: 5,
+ storageUsedGb: 245.5,
+ storageUsedPercent: 49.1,
+ apiCallsThisMonth: 456789,
+ apiCallsPercent: 45.7,
+ apiCallsLastMonth: 512000,
+ sessionsToday: 45,
+ sessionsThisWeek: 312,
+ sessionsThisMonth: 1250,
+ avgSessionDurationMinutes: 35,
+ totalBranches: 18,
+ totalDocuments: 125000,
+ totalTransactions: 45000,
+ periodStart: '2026-02-01T00:00:00Z',
+ periodEnd: '2026-02-28T23:59:59Z',
+ updatedAt: '2026-02-03T10:00:00Z',
+ },
+ 'tenant-002': {
+ tenantId: 'tenant-002',
+ totalUsers: 18,
+ activeUsers: 15,
+ inactiveUsers: 3,
+ pendingInvitations: 2,
+ storageUsedGb: 45.2,
+ storageUsedPercent: 45.2,
+ apiCallsThisMonth: 89000,
+ apiCallsPercent: 35.6,
+ apiCallsLastMonth: 75000,
+ sessionsToday: 12,
+ sessionsThisWeek: 85,
+ sessionsThisMonth: 340,
+ avgSessionDurationMinutes: 28,
+ totalBranches: 3,
+ totalDocuments: 15000,
+ totalTransactions: 8500,
+ periodStart: '2026-02-01T00:00:00Z',
+ periodEnd: '2026-02-28T23:59:59Z',
+ updatedAt: '2026-02-03T10:00:00Z',
+ },
+ 'tenant-003': {
+ tenantId: 'tenant-003',
+ totalUsers: 156,
+ activeUsers: 0,
+ inactiveUsers: 156,
+ pendingInvitations: 0,
+ storageUsedGb: 680.8,
+ storageUsedPercent: 68.1,
+ apiCallsThisMonth: 0,
+ apiCallsPercent: 0,
+ apiCallsLastMonth: 1800000,
+ sessionsToday: 0,
+ sessionsThisWeek: 0,
+ sessionsThisMonth: 0,
+ avgSessionDurationMinutes: 0,
+ totalBranches: 42,
+ totalDocuments: 450000,
+ totalTransactions: 250000,
+ periodStart: '2026-02-01T00:00:00Z',
+ periodEnd: '2026-02-28T23:59:59Z',
+ updatedAt: '2026-02-03T10:00:00Z',
+ },
+ 'tenant-004': {
+ tenantId: 'tenant-004',
+ totalUsers: 3,
+ activeUsers: 3,
+ inactiveUsers: 0,
+ pendingInvitations: 1,
+ storageUsedGb: 0.8,
+ storageUsedPercent: 8,
+ apiCallsThisMonth: 2500,
+ apiCallsPercent: 5,
+ apiCallsLastMonth: 0,
+ sessionsToday: 5,
+ sessionsThisWeek: 28,
+ sessionsThisMonth: 45,
+ avgSessionDurationMinutes: 42,
+ totalBranches: 1,
+ totalDocuments: 150,
+ totalTransactions: 25,
+ periodStart: '2026-02-01T00:00:00Z',
+ periodEnd: '2026-02-28T23:59:59Z',
+ updatedAt: '2026-02-03T10:00:00Z',
+ },
+ 'tenant-005': {
+ tenantId: 'tenant-005',
+ totalUsers: 38,
+ activeUsers: 32,
+ inactiveUsers: 6,
+ pendingInvitations: 3,
+ storageUsedGb: 125.3,
+ storageUsedPercent: 50.1,
+ apiCallsThisMonth: 210000,
+ apiCallsPercent: 42,
+ apiCallsLastMonth: 195000,
+ sessionsToday: 28,
+ sessionsThisWeek: 195,
+ sessionsThisMonth: 780,
+ avgSessionDurationMinutes: 32,
+ totalBranches: 7,
+ totalDocuments: 65000,
+ totalTransactions: 28000,
+ periodStart: '2026-02-01T00:00:00Z',
+ periodEnd: '2026-02-28T23:59:59Z',
+ updatedAt: '2026-02-03T10:00:00Z',
+ },
+ 'tenant-006': {
+ tenantId: 'tenant-006',
+ totalUsers: 0,
+ activeUsers: 0,
+ inactiveUsers: 0,
+ pendingInvitations: 0,
+ storageUsedGb: 0,
+ storageUsedPercent: 0,
+ apiCallsThisMonth: 0,
+ apiCallsPercent: 0,
+ apiCallsLastMonth: 0,
+ sessionsToday: 0,
+ sessionsThisWeek: 0,
+ sessionsThisMonth: 0,
+ avgSessionDurationMinutes: 0,
+ totalBranches: 0,
+ totalDocuments: 0,
+ totalTransactions: 0,
+ periodStart: '2026-02-01T00:00:00Z',
+ periodEnd: '2026-02-28T23:59:59Z',
+ updatedAt: '2026-02-03T10:00:00Z',
+ },
+};
+
+// ==================== Simulated API Delay ====================
+
+const simulateApiDelay = (ms = 500) => new Promise((resolve) => setTimeout(resolve, ms));
+
+// ==================== Hook State ====================
+
+interface UseTenantsState {
+ tenants: Tenant[];
+ total: number;
+ page: number;
+ totalPages: number;
+ isLoading: boolean;
+ error: string | null;
+}
+
+interface UseTenantsReturn extends UseTenantsState {
+ filters: TenantFilters;
+ setFilters: (filters: TenantFilters) => void;
+ refresh: () => Promise;
+ createTenant: (data: CreateTenantDto) => Promise;
+ updateTenant: (id: string, data: UpdateTenantDto) => Promise;
+ deleteTenant: (id: string) => Promise;
+ suspendTenant: (id: string, data: SuspendTenantDto) => Promise;
+ activateTenant: (id: string, data?: ActivateTenantDto) => Promise;
+ changePlan: (id: string, data: ChangeTenantPlanDto) => Promise;
+ getTenantStats: (id: string) => Promise;
+}
+
+// ==================== useTenants Hook ====================
+
+export function useTenants(initialFilters?: TenantFilters): UseTenantsReturn {
+ const [state, setState] = useState({
+ tenants: [],
+ total: 0,
+ page: 1,
+ totalPages: 1,
+ isLoading: true,
+ error: null,
+ });
+
+ const [filters, setFilters] = useState(
+ initialFilters || { page: 1, limit: 10, sortBy: 'createdAt', sortOrder: 'desc' }
+ );
+
+ const fetchTenants = useCallback(async () => {
+ setState((prev) => ({ ...prev, isLoading: true, error: null }));
+
+ try {
+ await simulateApiDelay();
+
+ // Filter mock data
+ let filteredTenants = [...MOCK_TENANTS];
+
+ // Search filter
+ if (filters.search) {
+ const searchLower = filters.search.toLowerCase();
+ filteredTenants = filteredTenants.filter(
+ (t) =>
+ t.name.toLowerCase().includes(searchLower) ||
+ t.slug.toLowerCase().includes(searchLower) ||
+ t.ownerEmail.toLowerCase().includes(searchLower) ||
+ t.ownerName.toLowerCase().includes(searchLower)
+ );
+ }
+
+ // Status filter
+ if (filters.status) {
+ filteredTenants = filteredTenants.filter((t) => t.status === filters.status);
+ }
+
+ // Plan tier filter
+ if (filters.planTier) {
+ filteredTenants = filteredTenants.filter((t) => t.planTier === filters.planTier);
+ }
+
+ // Billing cycle filter
+ if (filters.billingCycle) {
+ filteredTenants = filteredTenants.filter((t) => t.billingCycle === filters.billingCycle);
+ }
+
+ // Sort
+ const sortBy = filters.sortBy || 'createdAt';
+ const sortOrder = filters.sortOrder || 'desc';
+ filteredTenants.sort((a, b) => {
+ const aVal = a[sortBy as keyof Tenant] ?? '';
+ const bVal = b[sortBy as keyof Tenant] ?? '';
+ const comparison = String(aVal).localeCompare(String(bVal));
+ return sortOrder === 'desc' ? -comparison : comparison;
+ });
+
+ // Pagination
+ const limit = filters.limit || 10;
+ const page = filters.page || 1;
+ const total = filteredTenants.length;
+ const totalPages = Math.ceil(total / limit);
+ const startIndex = (page - 1) * limit;
+ const paginatedTenants = filteredTenants.slice(startIndex, startIndex + limit);
+
+ setState({
+ tenants: paginatedTenants,
+ total,
+ page,
+ totalPages,
+ isLoading: false,
+ error: null,
+ });
+ } catch (err) {
+ setState((prev) => ({
+ ...prev,
+ isLoading: false,
+ error: err instanceof Error ? err.message : 'Error loading tenants',
+ }));
+ }
+ }, [filters]);
+
+ useEffect(() => {
+ fetchTenants();
+ }, [fetchTenants]);
+
+ const createTenant = async (data: CreateTenantDto): Promise => {
+ await simulateApiDelay();
+
+ const newTenant: Tenant = {
+ id: `tenant-${Date.now()}`,
+ name: data.name,
+ slug: data.slug,
+ domain: data.domain ?? null,
+ subdomain: data.subdomain ?? data.slug,
+ status: data.startTrial ? 'trial' : 'pending',
+ planTier: data.planTier || 'starter',
+ planId: data.planId,
+ planName: data.planTier ? data.planTier.charAt(0).toUpperCase() + data.planTier.slice(1) : 'Starter',
+ billingCycle: data.billingCycle || 'monthly',
+ ownerName: data.ownerName,
+ ownerEmail: data.ownerEmail,
+ phone: data.phone ?? null,
+ timezone: data.timezone || 'UTC',
+ locale: data.locale || 'en-US',
+ currency: data.currency || 'USD',
+ maxUsers: data.maxUsers || 5,
+ maxBranches: data.maxBranches || 1,
+ maxStorageGb: data.maxStorageGb || 10,
+ maxApiCallsMonthly: data.maxApiCallsMonthly || 50000,
+ enabledModules: data.enabledModules || ['crm', 'sales'],
+ features: data.features || {},
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ trialEndsAt: data.startTrial
+ ? new Date(Date.now() + (data.trialDays || 14) * 24 * 60 * 60 * 1000).toISOString()
+ : null,
+ };
+
+ MOCK_TENANTS.unshift(newTenant);
+ await fetchTenants();
+ return newTenant;
+ };
+
+ const updateTenant = async (id: string, data: UpdateTenantDto): Promise => {
+ await simulateApiDelay();
+
+ const index = MOCK_TENANTS.findIndex((t) => t.id === id);
+ if (index === -1) {
+ throw new Error('Tenant not found');
+ }
+
+ const existingTenant = MOCK_TENANTS[index]!;
+ const updatedTenant: Tenant = {
+ id: existingTenant.id,
+ name: data.name ?? existingTenant.name,
+ slug: data.slug ?? existingTenant.slug,
+ domain: data.domain !== undefined ? data.domain : existingTenant.domain,
+ subdomain: data.subdomain !== undefined ? data.subdomain : existingTenant.subdomain,
+ status: existingTenant.status,
+ planTier: existingTenant.planTier,
+ planId: existingTenant.planId,
+ planName: existingTenant.planName,
+ billingCycle: existingTenant.billingCycle,
+ ownerName: data.ownerName ?? existingTenant.ownerName,
+ ownerEmail: data.ownerEmail ?? existingTenant.ownerEmail,
+ phone: data.phone !== undefined ? data.phone : existingTenant.phone,
+ timezone: data.timezone ?? existingTenant.timezone,
+ locale: data.locale ?? existingTenant.locale,
+ currency: data.currency ?? existingTenant.currency,
+ maxUsers: data.maxUsers ?? existingTenant.maxUsers,
+ maxBranches: data.maxBranches ?? existingTenant.maxBranches,
+ maxStorageGb: data.maxStorageGb ?? existingTenant.maxStorageGb,
+ maxApiCallsMonthly: data.maxApiCallsMonthly ?? existingTenant.maxApiCallsMonthly,
+ enabledModules: data.enabledModules ?? existingTenant.enabledModules,
+ features: data.features ?? existingTenant.features,
+ createdAt: existingTenant.createdAt,
+ updatedAt: new Date().toISOString(),
+ trialEndsAt: existingTenant.trialEndsAt,
+ suspendedAt: existingTenant.suspendedAt,
+ suspensionReason: existingTenant.suspensionReason,
+ cancelledAt: existingTenant.cancelledAt,
+ cancellationReason: existingTenant.cancellationReason,
+ lastActiveAt: existingTenant.lastActiveAt,
+ };
+
+ MOCK_TENANTS[index] = updatedTenant;
+ await fetchTenants();
+ return updatedTenant;
+ };
+
+ const deleteTenant = async (id: string): Promise => {
+ await simulateApiDelay();
+
+ const index = MOCK_TENANTS.findIndex((t) => t.id === id);
+ if (index === -1) {
+ throw new Error('Tenant not found');
+ }
+
+ MOCK_TENANTS.splice(index, 1);
+ await fetchTenants();
+ };
+
+ const suspendTenant = async (id: string, data: SuspendTenantDto): Promise => {
+ await simulateApiDelay();
+
+ const index = MOCK_TENANTS.findIndex((t) => t.id === id);
+ if (index === -1) {
+ throw new Error('Tenant not found');
+ }
+
+ const existingTenant = MOCK_TENANTS[index]!;
+ const suspendedTenant: Tenant = {
+ ...existingTenant,
+ status: 'suspended',
+ suspendedAt: new Date().toISOString(),
+ suspensionReason: data.reason,
+ updatedAt: new Date().toISOString(),
+ };
+
+ MOCK_TENANTS[index] = suspendedTenant;
+ await fetchTenants();
+ return suspendedTenant;
+ };
+
+ const activateTenant = async (id: string, _data?: ActivateTenantDto): Promise => {
+ await simulateApiDelay();
+
+ const index = MOCK_TENANTS.findIndex((t) => t.id === id);
+ if (index === -1) {
+ throw new Error('Tenant not found');
+ }
+
+ const existingTenant = MOCK_TENANTS[index]!;
+ const activatedTenant: Tenant = {
+ ...existingTenant,
+ status: 'active',
+ suspendedAt: null,
+ suspensionReason: null,
+ updatedAt: new Date().toISOString(),
+ };
+
+ MOCK_TENANTS[index] = activatedTenant;
+ await fetchTenants();
+ return activatedTenant;
+ };
+
+ const changePlan = async (id: string, data: ChangeTenantPlanDto): Promise => {
+ await simulateApiDelay();
+
+ const index = MOCK_TENANTS.findIndex((t) => t.id === id);
+ if (index === -1) {
+ throw new Error('Tenant not found');
+ }
+
+ // Simulate plan change
+ const planTierMap: Record = {
+ 'plan-free': 'free',
+ 'plan-starter': 'starter',
+ 'plan-professional': 'professional',
+ 'plan-enterprise': 'enterprise',
+ };
+
+ const existingTenant = MOCK_TENANTS[index]!;
+ const changedPlanTenant: Tenant = {
+ ...existingTenant,
+ planId: data.planId,
+ planTier: planTierMap[data.planId] || 'custom',
+ billingCycle: data.billingCycle || existingTenant.billingCycle,
+ updatedAt: new Date().toISOString(),
+ };
+
+ MOCK_TENANTS[index] = changedPlanTenant;
+ await fetchTenants();
+ return changedPlanTenant;
+ };
+
+ const getTenantStats = async (id: string): Promise => {
+ await simulateApiDelay(300);
+
+ const stats = MOCK_TENANT_STATS[id];
+ if (!stats) {
+ throw new Error('Stats not found for tenant');
+ }
+
+ return stats;
+ };
+
+ return {
+ ...state,
+ filters,
+ setFilters,
+ refresh: fetchTenants,
+ createTenant,
+ updateTenant,
+ deleteTenant,
+ suspendTenant,
+ activateTenant,
+ changePlan,
+ getTenantStats,
+ };
+}
+
+// ==================== useTenant Hook (Single Tenant) ====================
+
+export function useTenant(id: string | undefined) {
+ const [tenant, setTenant] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ const fetchTenant = useCallback(async () => {
+ if (!id) {
+ setIsLoading(false);
+ return;
+ }
+
+ setIsLoading(true);
+ setError(null);
+
+ try {
+ await simulateApiDelay();
+
+ const foundTenant = MOCK_TENANTS.find((t) => t.id === id);
+ if (!foundTenant) {
+ throw new Error('Tenant not found');
+ }
+
+ const stats = MOCK_TENANT_STATS[id] || {
+ tenantId: id,
+ totalUsers: 0,
+ activeUsers: 0,
+ inactiveUsers: 0,
+ pendingInvitations: 0,
+ storageUsedGb: 0,
+ storageUsedPercent: 0,
+ apiCallsThisMonth: 0,
+ apiCallsPercent: 0,
+ apiCallsLastMonth: 0,
+ sessionsToday: 0,
+ sessionsThisWeek: 0,
+ sessionsThisMonth: 0,
+ avgSessionDurationMinutes: 0,
+ totalBranches: 0,
+ totalDocuments: 0,
+ totalTransactions: 0,
+ periodStart: new Date().toISOString(),
+ periodEnd: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ };
+
+ setTenant({ ...foundTenant, stats });
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Error loading tenant');
+ } finally {
+ setIsLoading(false);
+ }
+ }, [id]);
+
+ useEffect(() => {
+ fetchTenant();
+ }, [fetchTenant]);
+
+ return { tenant, isLoading, error, refresh: fetchTenant };
+}
+
+// ==================== useTenantUsageHistory Hook ====================
+
+export function useTenantUsageHistory(tenantId: string | undefined, days = 30) {
+ const [history, setHistory] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ if (!tenantId) {
+ setIsLoading(false);
+ return;
+ }
+
+ const fetchHistory = async () => {
+ setIsLoading(true);
+ setError(null);
+
+ try {
+ await simulateApiDelay(400);
+
+ // Generate mock history data
+ const mockHistory: TenantUsageHistory[] = [];
+ const now = new Date();
+
+ for (let i = days - 1; i >= 0; i--) {
+ const date = new Date(now);
+ date.setDate(date.getDate() - i);
+ const dateStr = date.toISOString().split('T')[0] ?? date.toISOString().slice(0, 10);
+
+ mockHistory.push({
+ date: dateStr,
+ activeUsers: Math.floor(Math.random() * 50) + 10,
+ apiCalls: Math.floor(Math.random() * 10000) + 1000,
+ storageGb: Math.random() * 10 + 40,
+ sessions: Math.floor(Math.random() * 100) + 20,
+ });
+ }
+
+ setHistory(mockHistory);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Error loading usage history');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ fetchHistory();
+ }, [tenantId, days]);
+
+ return { history, isLoading, error };
+}
+
+// ==================== useTenantSubscription Hook ====================
+
+export function useTenantSubscription(tenantId: string | undefined) {
+ const [subscription, setSubscription] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ if (!tenantId) {
+ setIsLoading(false);
+ return;
+ }
+
+ const fetchSubscription = async () => {
+ setIsLoading(true);
+ setError(null);
+
+ try {
+ await simulateApiDelay(300);
+
+ const tenant = MOCK_TENANTS.find((t) => t.id === tenantId);
+ if (!tenant) {
+ throw new Error('Tenant not found');
+ }
+
+ // Generate mock subscription
+ const mockSubscription: TenantSubscription = {
+ id: `sub-${tenantId}`,
+ tenantId,
+ planId: tenant.planId || 'plan-starter',
+ planName: tenant.planName || 'Starter',
+ planTier: tenant.planTier,
+ billingCycle: tenant.billingCycle,
+ basePrice: tenant.planTier === 'enterprise' ? 999 : tenant.planTier === 'professional' ? 299 : 49,
+ currentPrice: tenant.planTier === 'enterprise' ? 899 : tenant.planTier === 'professional' ? 299 : 49,
+ discountPercent: tenant.planTier === 'enterprise' ? 10 : 0,
+ discountReason: tenant.planTier === 'enterprise' ? 'Annual commitment discount' : null,
+ currency: tenant.currency || 'USD',
+ currentPeriodStart: '2026-02-01T00:00:00Z',
+ currentPeriodEnd: tenant.billingCycle === 'annual' ? '2027-02-01T00:00:00Z' : '2026-03-01T00:00:00Z',
+ nextBillingDate: tenant.billingCycle === 'annual' ? '2027-02-01T00:00:00Z' : '2026-03-01T00:00:00Z',
+ isTrialing: tenant.status === 'trial',
+ trialStart: tenant.status === 'trial' ? tenant.createdAt : null,
+ trialEnd: tenant.trialEndsAt,
+ paymentMethod: tenant.status !== 'trial' ? 'card_visa_4242' : null,
+ lastPaymentAt: tenant.status === 'active' ? '2026-02-01T10:00:00Z' : null,
+ lastPaymentAmount: tenant.planTier === 'enterprise' ? 899 : tenant.planTier === 'professional' ? 299 : 49,
+ lastPaymentStatus: tenant.status === 'active' ? 'succeeded' : null,
+ contractedUsers: tenant.maxUsers,
+ contractedBranches: tenant.maxBranches,
+ contractedStorageGb: tenant.maxStorageGb,
+ status: tenant.status === 'trial' ? 'trialing' : tenant.status === 'suspended' ? 'past_due' : 'active',
+ autoRenew: true,
+ cancelAtPeriodEnd: false,
+ stripeCustomerId: tenant.status !== 'trial' ? `cus_${tenantId.replace('tenant-', '')}` : null,
+ stripeSubscriptionId: tenant.status !== 'trial' ? `sub_${tenantId.replace('tenant-', '')}` : null,
+ createdAt: tenant.createdAt,
+ updatedAt: tenant.updatedAt,
+ };
+
+ setSubscription(mockSubscription);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Error loading subscription');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ fetchSubscription();
+ }, [tenantId]);
+
+ return { subscription, isLoading, error };
+}
diff --git a/src/features/superadmin/index.ts b/src/features/superadmin/index.ts
new file mode 100644
index 0000000..3a54535
--- /dev/null
+++ b/src/features/superadmin/index.ts
@@ -0,0 +1,13 @@
+/**
+ * Superadmin Feature - Index
+ * Exports all public API for the superadmin portal
+ */
+
+// Hooks
+export * from './hooks';
+
+// Types
+export * from './types';
+
+// API
+export * from './api';
diff --git a/src/features/superadmin/types/index.ts b/src/features/superadmin/types/index.ts
new file mode 100644
index 0000000..c8a0987
--- /dev/null
+++ b/src/features/superadmin/types/index.ts
@@ -0,0 +1,6 @@
+/**
+ * Superadmin Types - Index
+ * Exports all types for the superadmin portal
+ */
+
+export * from './tenant';
diff --git a/src/features/superadmin/types/tenant.ts b/src/features/superadmin/types/tenant.ts
new file mode 100644
index 0000000..505f90f
--- /dev/null
+++ b/src/features/superadmin/types/tenant.ts
@@ -0,0 +1,282 @@
+/**
+ * Tenant Management Types
+ * Superadmin Portal - ERP Core
+ */
+
+// ==================== Enums ====================
+
+export type TenantStatus = 'active' | 'suspended' | 'pending' | 'cancelled' | 'trial';
+export type TenantPlanTier = 'free' | 'starter' | 'professional' | 'enterprise' | 'custom';
+export type BillingCycle = 'monthly' | 'annual';
+
+// ==================== Main Tenant Interface ====================
+
+export interface Tenant {
+ id: string;
+ name: string;
+ slug: string;
+ domain?: string | null;
+ subdomain?: string | null;
+ status: TenantStatus;
+ planTier: TenantPlanTier;
+ planId?: string | null;
+ planName?: string | null;
+ billingCycle: BillingCycle;
+
+ // Contact Information
+ ownerName: string;
+ ownerEmail: string;
+ phone?: string | null;
+
+ // Settings
+ timezone?: string;
+ locale?: string;
+ currency?: string;
+
+ // Limits
+ maxUsers: number;
+ maxBranches: number;
+ maxStorageGb: number;
+ maxApiCallsMonthly: number;
+
+ // Feature Flags
+ enabledModules: string[];
+ features: Record;
+
+ // Timestamps
+ createdAt: string;
+ updatedAt: string;
+ trialEndsAt?: string | null;
+ suspendedAt?: string | null;
+ suspensionReason?: string | null;
+ cancelledAt?: string | null;
+ cancellationReason?: string | null;
+ lastActiveAt?: string | null;
+}
+
+// ==================== Tenant Statistics ====================
+
+export interface TenantStats {
+ tenantId: string;
+
+ // User Metrics
+ totalUsers: number;
+ activeUsers: number;
+ inactiveUsers: number;
+ pendingInvitations: number;
+
+ // Storage Metrics
+ storageUsedGb: number;
+ storageUsedPercent: number;
+
+ // API Usage
+ apiCallsThisMonth: number;
+ apiCallsPercent: number;
+ apiCallsLastMonth: number;
+
+ // Activity Metrics
+ sessionsToday: number;
+ sessionsThisWeek: number;
+ sessionsThisMonth: number;
+ avgSessionDurationMinutes: number;
+
+ // Business Metrics
+ totalBranches: number;
+ totalDocuments: number;
+ totalTransactions: number;
+
+ // Period
+ periodStart: string;
+ periodEnd: string;
+ updatedAt: string;
+}
+
+export interface TenantUsageHistory {
+ date: string;
+ activeUsers: number;
+ apiCalls: number;
+ storageGb: number;
+ sessions: number;
+}
+
+// ==================== Tenant Subscription ====================
+
+export interface TenantSubscription {
+ id: string;
+ tenantId: string;
+ planId: string;
+ planName: string;
+ planTier: TenantPlanTier;
+ billingCycle: BillingCycle;
+
+ // Pricing
+ basePrice: number;
+ currentPrice: number;
+ discountPercent: number;
+ discountReason?: string | null;
+ currency: string;
+
+ // Period
+ currentPeriodStart: string;
+ currentPeriodEnd: string;
+ nextBillingDate?: string | null;
+
+ // Trial
+ isTrialing: boolean;
+ trialStart?: string | null;
+ trialEnd?: string | null;
+
+ // Payment
+ paymentMethod?: string | null;
+ lastPaymentAt?: string | null;
+ lastPaymentAmount?: number | null;
+ lastPaymentStatus?: 'succeeded' | 'failed' | 'pending' | null;
+
+ // Contracted Limits
+ contractedUsers: number;
+ contractedBranches: number;
+ contractedStorageGb: number;
+
+ // Status
+ status: 'active' | 'past_due' | 'cancelled' | 'trialing' | 'incomplete';
+ autoRenew: boolean;
+ cancelAtPeriodEnd: boolean;
+
+ // External IDs
+ stripeCustomerId?: string | null;
+ stripeSubscriptionId?: string | null;
+
+ createdAt: string;
+ updatedAt: string;
+}
+
+// ==================== DTOs ====================
+
+export interface CreateTenantDto {
+ name: string;
+ slug: string;
+ domain?: string;
+ subdomain?: string;
+
+ // Owner
+ ownerName: string;
+ ownerEmail: string;
+ ownerPassword?: string;
+ phone?: string;
+
+ // Plan
+ planId?: string;
+ planTier?: TenantPlanTier;
+ billingCycle?: BillingCycle;
+
+ // Settings
+ timezone?: string;
+ locale?: string;
+ currency?: string;
+
+ // Trial
+ startTrial?: boolean;
+ trialDays?: number;
+
+ // Limits (override plan defaults)
+ maxUsers?: number;
+ maxBranches?: number;
+ maxStorageGb?: number;
+ maxApiCallsMonthly?: number;
+
+ // Features
+ enabledModules?: string[];
+ features?: Record;
+}
+
+export interface UpdateTenantDto {
+ name?: string;
+ slug?: string;
+ domain?: string | null;
+ subdomain?: string | null;
+
+ // Contact
+ ownerName?: string;
+ ownerEmail?: string;
+ phone?: string | null;
+
+ // Settings
+ timezone?: string;
+ locale?: string;
+ currency?: string;
+
+ // Limits
+ maxUsers?: number;
+ maxBranches?: number;
+ maxStorageGb?: number;
+ maxApiCallsMonthly?: number;
+
+ // Features
+ enabledModules?: string[];
+ features?: Record;
+}
+
+export interface SuspendTenantDto {
+ reason: string;
+ suspendImmediately?: boolean;
+ notifyOwner?: boolean;
+}
+
+export interface ActivateTenantDto {
+ reason?: string;
+ notifyOwner?: boolean;
+}
+
+export interface ChangeTenantPlanDto {
+ planId: string;
+ billingCycle?: BillingCycle;
+ effectiveDate?: string;
+ prorateCharges?: boolean;
+ contractedUsers?: number;
+ contractedBranches?: number;
+}
+
+// ==================== Filters and Responses ====================
+
+export interface TenantFilters {
+ search?: string;
+ status?: TenantStatus;
+ planTier?: TenantPlanTier;
+ billingCycle?: BillingCycle;
+ hasOverduePayment?: boolean;
+ createdAfter?: string;
+ createdBefore?: string;
+ page?: number;
+ limit?: number;
+ sortBy?: 'name' | 'createdAt' | 'lastActiveAt' | 'status' | 'planTier';
+ sortOrder?: 'asc' | 'desc';
+}
+
+export interface TenantsResponse {
+ data: Tenant[];
+ meta: {
+ total: number;
+ page: number;
+ limit: number;
+ totalPages: number;
+ };
+}
+
+export interface TenantWithStats extends Tenant {
+ stats: TenantStats;
+ subscription?: TenantSubscription;
+}
+
+// ==================== Activity ====================
+
+export interface TenantActivity {
+ id: string;
+ tenantId: string;
+ tenantName: string;
+ type: 'created' | 'updated' | 'suspended' | 'activated' | 'plan_changed' | 'payment_received' | 'payment_failed' | 'user_added' | 'user_removed';
+ description: string;
+ metadata?: Record;
+ performedBy?: string;
+ performedByName?: string;
+ createdAt: string;
+}
diff --git a/src/pages/superadmin/SuperadminDashboardPage.tsx b/src/pages/superadmin/SuperadminDashboardPage.tsx
new file mode 100644
index 0000000..cc99e8d
--- /dev/null
+++ b/src/pages/superadmin/SuperadminDashboardPage.tsx
@@ -0,0 +1,771 @@
+/**
+ * SuperadminDashboardPage
+ * Main dashboard for superadmin portal with stats, charts, activity feed, and system health
+ */
+
+import React from 'react';
+import {
+ Building2,
+ CreditCard,
+ DollarSign,
+ Users,
+ TrendingUp,
+ TrendingDown,
+ Activity,
+ AlertTriangle,
+ CheckCircle,
+ Clock,
+ RefreshCw,
+ ArrowUpRight,
+ UserPlus,
+ ArrowUp,
+ ArrowDown,
+ Zap,
+ HardDrive,
+ Server,
+} from 'lucide-react';
+import { Card, CardContent, CardHeader, CardTitle } from '@components/molecules/Card';
+import { Badge } from '@components/atoms/Badge';
+import { Spinner } from '@components/atoms/Spinner';
+import { Button } from '@components/atoms/Button';
+import {
+ useSuperadminStats,
+ type PlanDistribution,
+ type RecentTenantActivity,
+ type TopTenant,
+ type SystemHealthMetrics,
+} from '@features/superadmin/hooks';
+import { formatCurrency, formatRelativeTime, formatNumber } from '@utils/formatters';
+
+// ==================== Helper Functions ====================
+
+function formatLargeNumber(value: number): string {
+ if (value >= 1000000) {
+ return `${(value / 1000000).toFixed(1)}M`;
+ }
+ if (value >= 1000) {
+ return `${(value / 1000).toFixed(1)}K`;
+ }
+ return value.toString();
+}
+
+function getChangeColor(change: number): string {
+ if (change > 0) return 'text-success-600';
+ if (change < 0) return 'text-danger-600';
+ return 'text-gray-500';
+}
+
+function getActivityIcon(iconType: RecentTenantActivity['icon']): React.ReactNode {
+ const iconClass = 'h-4 w-4';
+ switch (iconType) {
+ case 'signup':
+ return ;
+ case 'upgrade':
+ return ;
+ case 'downgrade':
+ return ;
+ case 'suspension':
+ return ;
+ case 'payment':
+ return ;
+ default:
+ return ;
+ }
+}
+
+function getTrendIcon(trend: 'up' | 'down' | 'stable'): React.ReactNode {
+ switch (trend) {
+ case 'up':
+ return ;
+ case 'down':
+ return ;
+ default:
+ return ;
+ }
+}
+
+function getPlanBadgeVariant(plan: string): 'default' | 'primary' | 'success' | 'warning' | 'danger' {
+ switch (plan) {
+ case 'enterprise':
+ return 'warning';
+ case 'professional':
+ return 'primary';
+ case 'starter':
+ return 'success';
+ default:
+ return 'default';
+ }
+}
+
+// ==================== Sub-Components ====================
+
+interface StatCardProps {
+ title: string;
+ value: string | number;
+ change?: number;
+ changeLabel?: string;
+ icon: React.ReactNode;
+ format?: 'number' | 'currency' | 'percent';
+}
+
+function StatCard({ title, value, change, changeLabel = 'vs mes anterior', icon, format = 'number' }: StatCardProps) {
+ const formattedValue = format === 'currency'
+ ? formatCurrency(value as number)
+ : format === 'percent'
+ ? `${value}%`
+ : formatLargeNumber(value as number);
+
+ return (
+
+
+
+
+
{title}
+
{formattedValue}
+
+
+ {icon}
+
+
+ {change !== undefined && (
+
+ {change > 0 ? (
+
+ ) : change < 0 ? (
+
+ ) : null}
+
+ {change > 0 ? '+' : ''}{change}%
+
+ {changeLabel}
+
+ )}
+
+
+ );
+}
+
+interface WidgetContainerProps {
+ title: string;
+ children: React.ReactNode;
+ action?: React.ReactNode;
+ className?: string;
+}
+
+function WidgetContainer({ title, children, action, className = '' }: WidgetContainerProps) {
+ return (
+
+
+ {title}
+ {action}
+
+ {children}
+
+ );
+}
+
+// Chart placeholder components (to be replaced with actual chart library)
+interface LineChartPlaceholderProps {
+ data: Array<{ month: string; totalTenants: number }>;
+}
+
+function LineChartPlaceholder({ data }: LineChartPlaceholderProps) {
+ const max = Math.max(...data.map((d) => d.totalTenants));
+ const min = Math.min(...data.map((d) => d.totalTenants));
+ const range = max - min || 1;
+
+ return (
+
+
+ {data.map((point) => {
+ const height = ((point.totalTenants - min) / range) * 100;
+ const normalizedHeight = Math.max(height, 10);
+ return (
+
+ );
+ })}
+
+
+
+ );
+}
+
+interface PieChartPlaceholderProps {
+ data: PlanDistribution[];
+}
+
+function PieChartPlaceholder({ data }: PieChartPlaceholderProps) {
+ const total = data.reduce((acc, item) => acc + item.count, 0);
+
+ return (
+
+
+ {/* Visual representation */}
+
+
+
+ {data.reduce(
+ (acc, item) => {
+ const angle = (item.count / total) * 360;
+ const startAngle = acc.currentAngle;
+ const endAngle = startAngle + angle;
+
+ const x1 = 50 + 40 * Math.cos((Math.PI * (startAngle - 90)) / 180);
+ const y1 = 50 + 40 * Math.sin((Math.PI * (startAngle - 90)) / 180);
+ const x2 = 50 + 40 * Math.cos((Math.PI * (endAngle - 90)) / 180);
+ const y2 = 50 + 40 * Math.sin((Math.PI * (endAngle - 90)) / 180);
+
+ const largeArc = angle > 180 ? 1 : 0;
+
+ const path = (
+
+ {`${item.planLabel}: ${item.count} (${item.percentage}%)`}
+
+ );
+
+ acc.paths.push(path);
+ acc.currentAngle = endAngle;
+ return acc;
+ },
+ { paths: [] as React.ReactNode[], currentAngle: 0 }
+ ).paths}
+
+
+
+ {/* Legend */}
+
+ {data.map((item) => (
+
+
+
{item.planLabel}
+
{item.percentage}%
+
+ ))}
+
+
+
+ );
+}
+
+interface AreaChartPlaceholderProps {
+ data: Array<{ month: string; revenue: number }>;
+}
+
+function AreaChartPlaceholder({ data }: AreaChartPlaceholderProps) {
+ const max = Math.max(...data.map((d) => d.revenue));
+ const min = Math.min(...data.map((d) => d.revenue)) * 0.9;
+ const range = max - min || 1;
+
+ return (
+
+
+ {/* Area gradient background */}
+
+
+
+
+
+
+
+
+ {
+ const x = (i / (data.length - 1)) * 100;
+ const y = 100 - ((point.revenue - min) / range) * 100;
+ return `L ${x} ${y}`;
+ })
+ .join(' ')} L 100 100 L 0 100 Z`}
+ fill="url(#areaGradient)"
+ />
+
+
+ {/* Bars */}
+ {data.map((point) => {
+ const height = ((point.revenue - min) / range) * 100;
+ return (
+
+ );
+ })}
+
+
+ );
+}
+
+interface RecentActivityFeedProps {
+ activities: RecentTenantActivity[];
+}
+
+function RecentActivityFeed({ activities }: RecentActivityFeedProps) {
+ if (activities.length === 0) {
+ return (
+
+ No hay actividad reciente
+
+ );
+ }
+
+ return (
+
+ {activities.map((activity) => (
+
+
+ {getActivityIcon(activity.icon)}
+
+
+
{activity.tenantName}
+
{activity.description}
+
+
+ {formatRelativeTime(activity.timestamp)}
+
+
+
+ ))}
+
+ );
+}
+
+interface TopTenantsTableProps {
+ tenants: TopTenant[];
+}
+
+function TopTenantsTable({ tenants }: TopTenantsTableProps) {
+ if (tenants.length === 0) {
+ return (
+
+ No hay datos de tenants
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ Tenant
+
+
+ Plan
+
+
+ Usuarios
+
+
+ MRR
+
+
+ Storage
+
+
+
+
+ {tenants.map((tenant, index) => (
+
+
+
+
+ {index + 1}
+
+
+
{tenant.name}
+
{tenant.slug}
+
+
+
+
+
+ {tenant.plan.charAt(0).toUpperCase() + tenant.plan.slice(1)}
+
+
+
+ {formatNumber(tenant.usersCount)}
+
+
+ {formatCurrency(tenant.monthlyRevenue)}
+
+
+ {tenant.storageUsedGb.toFixed(1)} GB
+
+
+ ))}
+
+
+
+ );
+}
+
+interface SystemHealthCardProps {
+ health: SystemHealthMetrics | null;
+ onRefresh: () => void;
+}
+
+function SystemHealthCard({ health, onRefresh }: SystemHealthCardProps) {
+ if (!health) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ const getHealthStatus = () => {
+ if (health.errorRate > 1 || health.uptimePercent < 99) {
+ return { status: 'warning', label: 'Degradado', color: 'text-warning-600', bg: 'bg-warning-100' };
+ }
+ if (health.errorRate > 5 || health.uptimePercent < 95) {
+ return { status: 'critical', label: 'Critico', color: 'text-danger-600', bg: 'bg-danger-100' };
+ }
+ return { status: 'healthy', label: 'Saludable', color: 'text-success-600', bg: 'bg-success-100' };
+ };
+
+ const statusInfo = getHealthStatus();
+
+ return (
+
+
+ Estado del Sistema
+
+
+
+
+
+ {/* Overall Status */}
+
+
+ {statusInfo.status === 'healthy' ? (
+
+ ) : (
+
+ )}
+
{statusInfo.label}
+
+
+ Uptime: {health.uptimePercent}%
+
+
+
+ {/* Metrics Grid */}
+
+ {/* API Response Time */}
+
+
+
+
+ Resp. Time
+
+ {getTrendIcon(health.apiResponseTimeTrend)}
+
+
{health.apiResponseTimeMs}ms
+
+
+ {/* Error Rate */}
+
+
+
+ {getTrendIcon(health.errorRateTrend)}
+
+
{health.errorRate}%
+
+
+ {/* Active Connections */}
+
+
+
+ Conexiones
+
+
+ {formatNumber(health.activeConnections)}
+
+
+
+ {/* Queued Jobs */}
+
+
+
+ Jobs en Cola
+
+
{health.queuedJobs}
+
+
+
+
+ );
+}
+
+// ==================== Main Component ====================
+
+export function SuperadminDashboardPage() {
+ const {
+ overview,
+ tenantGrowth,
+ planDistribution,
+ revenueTrend,
+ recentActivity,
+ topTenants,
+ systemHealth,
+ isLoading,
+ error,
+ refresh,
+ refreshSystemHealth,
+ } = useSuperadminStats();
+
+ // Format current date
+ const currentDate = new Date().toLocaleDateString('es-MX', {
+ weekday: 'long',
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ });
+
+ if (isLoading) {
+ return (
+
+
+
+
Cargando dashboard...
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
+
Error al cargar datos
+
{error}
+
+ Reintentar
+
+
+
+ );
+ }
+
+ return (
+
+
+ {/* Header */}
+
+
+
Panel de Superadmin
+
{currentDate}
+
+
+
+ Actualizar
+
+
+
+ {/* Key Metrics Row */}
+
+ }
+ />
+ }
+ />
+ }
+ format="currency"
+ />
+ }
+ />
+
+
+ {/* Charts Row */}
+
+ {/* Tenant Growth Chart */}
+
Ultimos 12 meses
+ }
+ >
+
+
+
+ {/* Plan Distribution Chart */}
+
+
+
+
+ {/* Revenue Trend Chart */}
+
Ultimos 12 meses
+ }
+ >
+
+
+
+
+ {/* Activity & Top Tenants Row */}
+
+ {/* Recent Activity Feed */}
+
+ Ver todo
+
+
+ }
+ >
+
+
+
+ {/* Top Tenants Table */}
+
+ Ver todos
+
+
+ }
+ >
+
+
+
+
+ {/* System Health & Quick Stats Row */}
+
+ {/* System Health */}
+
+
+
+
+ {/* Additional Stats */}
+
+
+
+
+
+
+
+
+
En Periodo de Prueba
+
+ {overview?.trialTenants || 0}
+
+
+
+
+
+
+
+
+
+
+
+
Tenants Suspendidos
+
+ {overview?.suspendedTenants || 0}
+
+
+
+
+
+
+
+
+ Acciones Rapidas
+
+
+
+
+
+
+
+ );
+}
+
+export default SuperadminDashboardPage;
diff --git a/src/pages/superadmin/index.ts b/src/pages/superadmin/index.ts
new file mode 100644
index 0000000..6266bc1
--- /dev/null
+++ b/src/pages/superadmin/index.ts
@@ -0,0 +1,14 @@
+/**
+ * Superadmin Pages - Index
+ * Exports all pages for the superadmin portal
+ */
+
+export { SuperadminDashboardPage } from './SuperadminDashboardPage';
+
+// Tenant Management
+export {
+ TenantsListPage,
+ TenantDetailPage,
+ TenantCreatePage,
+ TenantEditPage,
+} from './tenants';
diff --git a/src/pages/superadmin/tenants/TenantCreatePage.tsx b/src/pages/superadmin/tenants/TenantCreatePage.tsx
new file mode 100644
index 0000000..6c5ae29
--- /dev/null
+++ b/src/pages/superadmin/tenants/TenantCreatePage.tsx
@@ -0,0 +1,683 @@
+/**
+ * TenantCreatePage - Superadmin Portal
+ * Create a new tenant in the system
+ */
+
+import { useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { useForm } from 'react-hook-form';
+import { ArrowLeft, Building2 } from 'lucide-react';
+import { Button } from '@components/atoms/Button';
+import { Input } from '@components/atoms/Input';
+import { Label } from '@components/atoms/Label';
+import { Switch } from '@components/atoms/Switch';
+import { Card, CardHeader, CardTitle, CardContent } from '@components/molecules/Card';
+import { FormField } from '@components/molecules/FormField';
+import { Alert } from '@components/molecules/Alert';
+import { Select } from '@components/organisms/Select';
+import { Breadcrumbs } from '@components/organisms/Breadcrumbs';
+import { useToast } from '@components/organisms/Toast';
+import { useTenants } from '@features/superadmin';
+import type { CreateTenantDto, TenantPlanTier, BillingCycle } from '@features/superadmin';
+
+// ==================== Form Types ====================
+
+interface FormData {
+ name: string;
+ slug: string;
+ domain: string;
+ subdomain: string;
+ ownerName: string;
+ ownerEmail: string;
+ ownerPassword: string;
+ phone: string;
+ planTier: TenantPlanTier;
+ billingCycle: BillingCycle;
+ timezone: string;
+ locale: string;
+ currency: string;
+ startTrial: boolean;
+ trialDays: number;
+ maxUsers: number;
+ maxBranches: number;
+ maxStorageGb: number;
+ // Features
+ advancedReporting: boolean;
+ apiAccess: boolean;
+ customBranding: boolean;
+ sso: boolean;
+ // Modules
+ moduleCrm: boolean;
+ moduleInventory: boolean;
+ moduleSales: boolean;
+ modulePurchases: boolean;
+ moduleHr: boolean;
+ moduleAccounting: boolean;
+ moduleProjects: boolean;
+}
+
+// ==================== Options ====================
+
+const PLAN_OPTIONS = [
+ { value: 'free', label: 'Gratis' },
+ { value: 'starter', label: 'Starter - $49/mes' },
+ { value: 'professional', label: 'Professional - $299/mes' },
+ { value: 'enterprise', label: 'Enterprise - $999/mes' },
+ { value: 'custom', label: 'Personalizado' },
+];
+
+const BILLING_OPTIONS = [
+ { value: 'monthly', label: 'Mensual' },
+ { value: 'annual', label: 'Anual (10% descuento)' },
+];
+
+const TIMEZONE_OPTIONS = [
+ { value: 'America/Mexico_City', label: 'America/Mexico_City (UTC-6)' },
+ { value: 'America/New_York', label: 'America/New_York (UTC-5)' },
+ { value: 'America/Los_Angeles', label: 'America/Los_Angeles (UTC-8)' },
+ { value: 'America/Chicago', label: 'America/Chicago (UTC-6)' },
+ { value: 'Europe/London', label: 'Europe/London (UTC+0)' },
+ { value: 'Europe/Madrid', label: 'Europe/Madrid (UTC+1)' },
+ { value: 'Asia/Shanghai', label: 'Asia/Shanghai (UTC+8)' },
+ { value: 'UTC', label: 'UTC' },
+];
+
+const LOCALE_OPTIONS = [
+ { value: 'es-MX', label: 'Espanol (Mexico)' },
+ { value: 'es-ES', label: 'Espanol (Espana)' },
+ { value: 'en-US', label: 'English (US)' },
+ { value: 'en-GB', label: 'English (UK)' },
+ { value: 'pt-BR', label: 'Portugues (Brasil)' },
+ { value: 'zh-CN', label: 'Chino (Simplificado)' },
+];
+
+const CURRENCY_OPTIONS = [
+ { value: 'MXN', label: 'MXN - Peso Mexicano' },
+ { value: 'USD', label: 'USD - Dolar Americano' },
+ { value: 'EUR', label: 'EUR - Euro' },
+ { value: 'GBP', label: 'GBP - Libra Esterlina' },
+ { value: 'CNY', label: 'CNY - Yuan Chino' },
+];
+
+// ==================== Main Component ====================
+
+export function TenantCreatePage() {
+ const navigate = useNavigate();
+ const { showToast } = useToast();
+ const { createTenant } = useTenants();
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [error, setError] = useState(null);
+
+ const {
+ register,
+ handleSubmit,
+ watch,
+ setValue,
+ formState: { errors },
+ } = useForm({
+ defaultValues: {
+ name: '',
+ slug: '',
+ domain: '',
+ subdomain: '',
+ ownerName: '',
+ ownerEmail: '',
+ ownerPassword: '',
+ phone: '',
+ planTier: 'starter',
+ billingCycle: 'monthly',
+ timezone: 'America/Mexico_City',
+ locale: 'es-MX',
+ currency: 'MXN',
+ startTrial: true,
+ trialDays: 14,
+ maxUsers: 10,
+ maxBranches: 1,
+ maxStorageGb: 10,
+ advancedReporting: false,
+ apiAccess: false,
+ customBranding: false,
+ sso: false,
+ moduleCrm: true,
+ moduleInventory: true,
+ moduleSales: true,
+ modulePurchases: false,
+ moduleHr: false,
+ moduleAccounting: false,
+ moduleProjects: false,
+ },
+ });
+
+ const startTrial = watch('startTrial');
+
+ // Auto-generate slug from name
+ const handleNameChange = (e: React.ChangeEvent) => {
+ const newName = e.target.value;
+ const slug = newName
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, '-')
+ .replace(/(^-|-$)/g, '');
+ setValue('slug', slug);
+ setValue('subdomain', slug);
+ };
+
+ // Update limits based on plan
+ const updatePlanDefaults = (tier: TenantPlanTier) => {
+ const planDefaults: Record = {
+ free: { users: 3, branches: 1, storage: 1 },
+ starter: { users: 10, branches: 1, storage: 10 },
+ professional: { users: 50, branches: 10, storage: 100 },
+ enterprise: { users: 200, branches: 50, storage: 500 },
+ custom: { users: 100, branches: 25, storage: 250 },
+ };
+ const defaults = planDefaults[tier];
+ setValue('maxUsers', defaults.users);
+ setValue('maxBranches', defaults.branches);
+ setValue('maxStorageGb', defaults.storage);
+
+ // Update features based on plan
+ if (tier === 'enterprise' || tier === 'custom') {
+ setValue('advancedReporting', true);
+ setValue('apiAccess', true);
+ setValue('customBranding', true);
+ setValue('sso', true);
+ } else if (tier === 'professional') {
+ setValue('advancedReporting', true);
+ setValue('apiAccess', true);
+ setValue('customBranding', false);
+ setValue('sso', false);
+ } else {
+ setValue('advancedReporting', false);
+ setValue('apiAccess', false);
+ setValue('customBranding', false);
+ setValue('sso', false);
+ }
+ };
+
+ const onFormSubmit = async (data: FormData) => {
+ setIsSubmitting(true);
+ setError(null);
+
+ try {
+ // Build enabled modules array
+ const enabledModules: string[] = [];
+ if (data.moduleCrm) enabledModules.push('crm');
+ if (data.moduleInventory) enabledModules.push('inventory');
+ if (data.moduleSales) enabledModules.push('sales');
+ if (data.modulePurchases) enabledModules.push('purchases');
+ if (data.moduleHr) enabledModules.push('hr');
+ if (data.moduleAccounting) enabledModules.push('accounting');
+ if (data.moduleProjects) enabledModules.push('projects');
+
+ const createData: CreateTenantDto = {
+ name: data.name,
+ slug: data.slug,
+ domain: data.domain || undefined,
+ subdomain: data.subdomain || data.slug,
+ ownerName: data.ownerName,
+ ownerEmail: data.ownerEmail,
+ ownerPassword: data.ownerPassword || undefined,
+ phone: data.phone || undefined,
+ planTier: data.planTier,
+ billingCycle: data.billingCycle,
+ timezone: data.timezone,
+ locale: data.locale,
+ currency: data.currency,
+ startTrial: data.startTrial,
+ trialDays: data.startTrial ? data.trialDays : undefined,
+ maxUsers: data.maxUsers,
+ maxBranches: data.maxBranches,
+ maxStorageGb: data.maxStorageGb,
+ enabledModules,
+ features: {
+ advancedReporting: data.advancedReporting,
+ apiAccess: data.apiAccess,
+ customBranding: data.customBranding,
+ sso: data.sso,
+ },
+ };
+
+ const tenant = await createTenant(createData);
+ showToast({
+ type: 'success',
+ title: 'Tenant creado',
+ message: `${tenant.name} ha sido creado exitosamente. Se ha enviado un email de invitacion al propietario.`,
+ });
+ navigate('/superadmin/tenants');
+ } catch (err) {
+ const message = err instanceof Error ? err.message : 'Error al crear tenant';
+ setError(message);
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ return (
+
+ {/* Breadcrumbs */}
+
+
+ {/* Header */}
+
+
navigate('/superadmin/tenants')}>
+
+ Volver
+
+
+
+
+
+
+
Nuevo tenant
+
+ Crea una nueva organizacion en la plataforma
+
+
+
+
+
+ {/* Form */}
+
+
+ );
+}
+
+export default TenantCreatePage;
diff --git a/src/pages/superadmin/tenants/TenantDetailPage.tsx b/src/pages/superadmin/tenants/TenantDetailPage.tsx
new file mode 100644
index 0000000..c23ff43
--- /dev/null
+++ b/src/pages/superadmin/tenants/TenantDetailPage.tsx
@@ -0,0 +1,850 @@
+/**
+ * TenantDetailPage - Superadmin Portal
+ * Detailed view of a single tenant with stats, users, usage, and settings
+ */
+
+import { useState } from 'react';
+import { useNavigate, useParams } from 'react-router-dom';
+import {
+ ArrowLeft,
+ Edit,
+ Trash2,
+ Pause,
+ Play,
+ Users,
+ HardDrive,
+ Activity,
+ Building2,
+ Calendar,
+ CreditCard,
+ Settings,
+ BarChart3,
+ Clock,
+ Globe,
+ Shield,
+ Zap,
+ TrendingUp,
+ TrendingDown,
+ CheckCircle,
+ AlertTriangle,
+ XCircle,
+} from 'lucide-react';
+import { Button } from '@components/atoms/Button';
+import { Badge } from '@components/atoms/Badge';
+import { Spinner } from '@components/atoms/Spinner';
+import { Card, CardHeader, CardTitle, CardContent } from '@components/molecules/Card';
+import { Breadcrumbs } from '@components/organisms/Breadcrumbs';
+import { ConfirmModal } from '@components/organisms/Modal';
+import { Tabs, TabList, Tab, TabPanels, TabPanel } from '@components/organisms/Tabs';
+import { useToast } from '@components/organisms/Toast';
+import { ErrorEmptyState } from '@components/templates/EmptyState';
+import { StatCard } from '@components/organisms/DashboardWidgets/StatCard';
+import { PerformanceChart } from '@components/organisms/DashboardWidgets/PerformanceChart';
+import {
+ useTenant,
+ useTenants,
+ useTenantUsageHistory,
+ useTenantSubscription,
+} from '@features/superadmin';
+import type {
+ TenantStatus,
+ SuspendTenantDto,
+} from '@features/superadmin';
+import { formatDate, formatCurrency } from '@utils/formatters';
+import { cn } from '@utils/cn';
+
+// ==================== Status Badge ====================
+
+const STATUS_CONFIG: Record<
+ TenantStatus,
+ { label: string; variant: 'success' | 'info' | 'danger' | 'default' | 'warning'; icon: typeof CheckCircle }
+> = {
+ active: { label: 'Activo', variant: 'success', icon: CheckCircle },
+ trial: { label: 'Prueba', variant: 'info', icon: Clock },
+ suspended: { label: 'Suspendido', variant: 'danger', icon: Pause },
+ cancelled: { label: 'Cancelado', variant: 'default', icon: XCircle },
+ pending: { label: 'Pendiente', variant: 'warning', icon: AlertTriangle },
+};
+
+function TenantStatusBadge({ status }: { status: TenantStatus }) {
+ const config = STATUS_CONFIG[status] || STATUS_CONFIG.pending;
+ const Icon = config.icon;
+ return (
+
+
+ {config.label}
+
+ );
+}
+
+// ==================== Main Component ====================
+
+export function TenantDetailPage() {
+ const { id } = useParams<{ id: string }>();
+ const navigate = useNavigate();
+ const { showToast } = useToast();
+
+ // Hooks
+ const { tenant, isLoading, error, refresh } = useTenant(id);
+ const { history: usageHistory, isLoading: historyLoading } = useTenantUsageHistory(id, 30);
+ const { subscription, isLoading: subscriptionLoading } = useTenantSubscription(id);
+ const { deleteTenant, suspendTenant, activateTenant } = useTenants();
+
+ // State
+ const [activeTab, setActiveTab] = useState('overview');
+ const [showDeleteModal, setShowDeleteModal] = useState(false);
+ const [showSuspendModal, setShowSuspendModal] = useState(false);
+ const [isProcessing, setIsProcessing] = useState(false);
+
+ // ==================== Handlers ====================
+
+ const handleDelete = async () => {
+ if (!id) return;
+ setIsProcessing(true);
+ try {
+ await deleteTenant(id);
+ showToast({
+ type: 'success',
+ title: 'Tenant eliminado',
+ message: 'El tenant ha sido eliminado exitosamente.',
+ });
+ navigate('/superadmin/tenants');
+ } catch {
+ showToast({
+ type: 'error',
+ title: 'Error',
+ message: 'No se pudo eliminar el tenant.',
+ });
+ } finally {
+ setIsProcessing(false);
+ setShowDeleteModal(false);
+ }
+ };
+
+ const handleSuspend = async () => {
+ if (!id) return;
+ setIsProcessing(true);
+ try {
+ const suspendData: SuspendTenantDto = {
+ reason: 'Suspension manual por superadmin',
+ suspendImmediately: true,
+ notifyOwner: true,
+ };
+ await suspendTenant(id, suspendData);
+ showToast({
+ type: 'success',
+ title: 'Tenant suspendido',
+ message: 'El tenant ha sido suspendido exitosamente.',
+ });
+ refresh();
+ } catch {
+ showToast({
+ type: 'error',
+ title: 'Error',
+ message: 'No se pudo suspender el tenant.',
+ });
+ } finally {
+ setIsProcessing(false);
+ setShowSuspendModal(false);
+ }
+ };
+
+ const handleActivate = async () => {
+ if (!id) return;
+ setIsProcessing(true);
+ try {
+ await activateTenant(id, { notifyOwner: true });
+ showToast({
+ type: 'success',
+ title: 'Tenant activado',
+ message: 'El tenant ha sido activado exitosamente.',
+ });
+ refresh();
+ } catch {
+ showToast({
+ type: 'error',
+ title: 'Error',
+ message: 'No se pudo activar el tenant.',
+ });
+ } finally {
+ setIsProcessing(false);
+ }
+ };
+
+ // ==================== Loading / Error States ====================
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ if (error || !tenant) {
+ return (
+
+
+
+ );
+ }
+
+ // ==================== Derived Data ====================
+
+ const stats = tenant.stats;
+
+ // Usage chart data
+ const usageChartData = usageHistory.map((h) => ({
+ name: formatDate(h.date, 'short'),
+ value: h.apiCalls,
+ storage: h.storageGb,
+ users: h.activeUsers,
+ }));
+
+ // Calculate usage percentages
+ const usagePercent = {
+ users: tenant.maxUsers > 0 ? Math.round((stats.totalUsers / tenant.maxUsers) * 100) : 0,
+ storage: tenant.maxStorageGb > 0 ? Math.round((stats.storageUsedGb / tenant.maxStorageGb) * 100) : 0,
+ apiCalls: tenant.maxApiCallsMonthly > 0 ? Math.round((stats.apiCallsThisMonth / tenant.maxApiCallsMonthly) * 100) : 0,
+ };
+
+ // ==================== Render ====================
+
+ return (
+
+ {/* Breadcrumbs */}
+
+
+ {/* Header */}
+
+
+
navigate('/superadmin/tenants')}>
+
+ Volver
+
+
+
+
+
+
+
+
{tenant.name}
+
+
+
{tenant.domain || tenant.subdomain || tenant.slug}
+
+
+
+
+
+
navigate(`/superadmin/tenants/${id}/edit`)}>
+
+ Editar
+
+ {(tenant.status === 'active' || tenant.status === 'trial') && (
+
setShowSuspendModal(true)}>
+
+ Suspender
+
+ )}
+ {tenant.status === 'suspended' && (
+
+
+ Reactivar
+
+ )}
+
setShowDeleteModal(true)}>
+
+ Eliminar
+
+
+
+
+ {/* Main content with activity timeline */}
+
+ {/* Main content */}
+
+
+
+ }>
+ Resumen
+
+ }>
+ Suscripcion
+
+ }>
+ Uso
+
+ }>
+ Configuracion
+
+
+
+
+ {/* Overview Tab */}
+
+
+ {/* Key metrics */}
+
+ 80 ? 'decrease' : 'neutral'}
+ changeLabel={`de ${tenant.maxUsers} max`}
+ />
+ 80 ? 'decrease' : 'neutral'}
+ changeLabel={`de ${tenant.maxStorageGb} GB`}
+ />
+ 80 ? 'decrease' : 'neutral'}
+ changeLabel="este mes"
+ />
+
+
+
+ {/* Info cards */}
+
+
+
+
+
+ Informacion general
+
+
+
+
+
+
Propietario
+ {tenant.ownerName}
+
+
+
Email
+ {tenant.ownerEmail}
+
+
+
Creado
+ {formatDate(tenant.createdAt, 'full')}
+
+
+
Ultima actividad
+
+ {tenant.lastActiveAt ? formatDate(tenant.lastActiveAt, 'relative') : 'N/A'}
+
+
+
+
+
+
+
+
+
+
+ Plan actual
+
+
+
+
+
+
+ {tenant.planName || tenant.planTier}
+
+
+ Ciclo: {tenant.billingCycle === 'annual' ? 'Anual' : 'Mensual'}
+
+
+
+ {tenant.status === 'active' ? 'Activo' :
+ tenant.status === 'trial' ? 'Prueba' :
+ tenant.status === 'suspended' ? 'Suspendido' : tenant.status}
+
+
+ {tenant.trialEndsAt && (
+
+ Prueba termina: {formatDate(tenant.trialEndsAt, 'full')}
+
+ )}
+
+
+
+
+ {/* Usage chart */}
+
+
+ Uso de API (ultimos 30 dias)
+
+
+
+
+
+
+
+
+ {/* Subscription Tab */}
+
+
+
+
+ Detalles de suscripcion
+
+
+ {subscriptionLoading ? (
+
+
+
+ ) : subscription ? (
+
+
+
Plan
+ {subscription.planName}
+
+
+
Precio
+
+ {formatCurrency(subscription.currentPrice, subscription.currency)}/
+ {subscription.billingCycle === 'annual' ? 'ano' : 'mes'}
+
+
+
+
Inicio del periodo
+ {formatDate(subscription.currentPeriodStart, 'full')}
+
+
+
Fin del periodo
+ {formatDate(subscription.currentPeriodEnd, 'full')}
+
+
+
Estado
+
+
+ {subscription.status === 'active' ? 'Activo' :
+ subscription.status === 'trialing' ? 'Prueba' :
+ subscription.status === 'past_due' ? 'Pago pendiente' : subscription.status}
+
+
+
+
+
Renovacion automatica
+
+ {subscription.autoRenew ? 'Si' : 'No'}
+
+
+ {subscription.discountPercent > 0 && (
+
+
Descuento
+
+ {subscription.discountPercent}% - {subscription.discountReason}
+
+
+ )}
+
+ ) : (
+ Sin suscripcion activa
+ )}
+
+
+
+ {/* Plan limits */}
+
+
+ Limites del plan
+
+
+
+
+
+
+ Usuarios
+
+
{tenant.maxUsers}
+
+
+
+
+ Almacenamiento
+
+
{tenant.maxStorageGb} GB
+
+
+
+
{tenant.maxApiCallsMonthly.toLocaleString()}
+
+
+
+
+ Sucursales
+
+
{tenant.maxBranches}
+
+
+
+
+
+
+
+ {/* Usage Tab */}
+
+
+ {/* Current usage */}
+
+
+
+
+
+
+
+ {/* Activity metrics */}
+
+
+ Metricas de actividad
+
+
+
+
+
{stats.sessionsToday}
+
Sesiones hoy
+
+
+
{stats.sessionsThisWeek}
+
Sesiones esta semana
+
+
+
{stats.sessionsThisMonth}
+
Sesiones este mes
+
+
+
{stats.avgSessionDurationMinutes} min
+
Duracion promedio
+
+
+
+
+
+ {/* Usage charts */}
+
+
+ Llamadas API
+
+
+
+
+
+
+
+
+ Almacenamiento
+
+
+ ({ ...d, value: d.storage }))}
+ type="line"
+ height={250}
+ loading={historyLoading}
+ />
+
+
+
+
+
+ {/* Settings Tab */}
+
+
+
+
+
+
+ Dominio y Acceso
+
+
+
+
+
+
Dominio
+
+ {tenant.domain || 'No configurado'}
+
+
+
+
Subdominio
+
+ {tenant.subdomain || tenant.slug}
+
+
+
+
Zona horaria
+
+ {tenant.timezone || 'UTC'}
+
+
+
+
Idioma
+
+ {tenant.locale || 'en-US'}
+
+
+
+
Moneda
+
+ {tenant.currency || 'USD'}
+
+
+
+
+
+
+
+
+
+
+ Caracteristicas
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Modulos habilitados
+
+
+
+
+ {tenant.enabledModules.map((module) => (
+
+ {module.charAt(0).toUpperCase() + module.slice(1)}
+
+ ))}
+
+
+
+
+
+
+
+
+
+ {/* Activity timeline sidebar */}
+
+
+
+
+
+ Informacion del sistema
+
+
+
+
+
+
ID
+ {tenant.id}
+
+
+
Slug
+ {tenant.slug}
+
+
+
Creado
+ {formatDate(tenant.createdAt, 'full')}
+
+
+
Actualizado
+ {formatDate(tenant.updatedAt, 'full')}
+
+ {tenant.suspendedAt && (
+
+
Suspendido
+ {formatDate(tenant.suspendedAt, 'full')}
+ {tenant.suspensionReason && (
+ {tenant.suspensionReason}
+ )}
+
+ )}
+
+
+ {/* Quick stats */}
+
+
Metricas rapidas
+
+
+
Usuarios activos
+ {stats.activeUsers}
+
+
+
Documentos
+ {stats.totalDocuments.toLocaleString()}
+
+
+
Transacciones
+ {stats.totalTransactions.toLocaleString()}
+
+
+
+
+
+
+
+
+ {/* Delete Modal */}
+
setShowDeleteModal(false)}
+ onConfirm={handleDelete}
+ title="Eliminar tenant"
+ message={`¿Estas seguro de que deseas eliminar "${tenant.name}"? Esta accion eliminara todos los datos asociados y no se puede deshacer.`}
+ variant="danger"
+ confirmText="Eliminar"
+ isLoading={isProcessing}
+ />
+
+ {/* Suspend Modal */}
+ setShowSuspendModal(false)}
+ onConfirm={handleSuspend}
+ title="Suspender tenant"
+ message={`¿Estas seguro de que deseas suspender "${tenant.name}"? Los usuarios no podran acceder hasta que se reactive.`}
+ variant="warning"
+ confirmText="Suspender"
+ isLoading={isProcessing}
+ />
+
+ );
+}
+
+// ==================== Helper Components ====================
+
+interface UsageCardProps {
+ label: string;
+ current: number;
+ max: number;
+ unit?: string;
+ icon: React.ComponentType<{ className?: string }>;
+}
+
+function UsageCard({ label, current, max, unit = '', icon: Icon }: UsageCardProps) {
+ const percent = max > 0 ? Math.round((current / max) * 100) : 0;
+ const isWarning = percent >= 80;
+ const isCritical = percent >= 95;
+
+ return (
+
+
+
+
+
+ {label}
+
+ {isWarning && (
+ isCritical ? (
+
+ ) : (
+
+ )
+ )}
+
+
+
+ {typeof current === 'number' && current % 1 !== 0 ? current.toFixed(1) : current.toLocaleString()}
+
+ / {max.toLocaleString()} {unit}
+
+
+
+
+ );
+}
+
+function FeatureBadge({ label, enabled }: { label: string; enabled: boolean }) {
+ return (
+
+ {label}
+
+ {enabled ? 'Activo' : 'Inactivo'}
+
+
+ );
+}
+
+export default TenantDetailPage;
diff --git a/src/pages/superadmin/tenants/TenantEditPage.tsx b/src/pages/superadmin/tenants/TenantEditPage.tsx
new file mode 100644
index 0000000..06b093f
--- /dev/null
+++ b/src/pages/superadmin/tenants/TenantEditPage.tsx
@@ -0,0 +1,652 @@
+/**
+ * TenantEditPage - Superadmin Portal
+ * Edit an existing tenant's configuration
+ */
+
+import { useState, useEffect } from 'react';
+import { useNavigate, useParams } from 'react-router-dom';
+import { useForm } from 'react-hook-form';
+import { ArrowLeft, Building2, AlertTriangle, CheckCircle, Clock, Pause, XCircle } from 'lucide-react';
+import { Button } from '@components/atoms/Button';
+import { Input } from '@components/atoms/Input';
+import { Label } from '@components/atoms/Label';
+import { Badge } from '@components/atoms/Badge';
+import { Switch } from '@components/atoms/Switch';
+import { Spinner } from '@components/atoms/Spinner';
+import { Card, CardHeader, CardTitle, CardContent } from '@components/molecules/Card';
+import { FormField } from '@components/molecules/FormField';
+import { Alert } from '@components/molecules/Alert';
+import { Select } from '@components/organisms/Select';
+import { Breadcrumbs } from '@components/organisms/Breadcrumbs';
+import { useToast } from '@components/organisms/Toast';
+import { ErrorEmptyState } from '@components/templates/EmptyState';
+import { useTenant, useTenants } from '@features/superadmin';
+import type { UpdateTenantDto, TenantStatus } from '@features/superadmin';
+import { formatDate } from '@utils/formatters';
+
+// ==================== Status Badge ====================
+
+const STATUS_CONFIG: Record<
+ TenantStatus,
+ { label: string; variant: 'success' | 'info' | 'danger' | 'default' | 'warning'; icon: typeof CheckCircle }
+> = {
+ active: { label: 'Activo', variant: 'success', icon: CheckCircle },
+ trial: { label: 'Prueba', variant: 'info', icon: Clock },
+ suspended: { label: 'Suspendido', variant: 'danger', icon: Pause },
+ cancelled: { label: 'Cancelado', variant: 'default', icon: XCircle },
+ pending: { label: 'Pendiente', variant: 'warning', icon: AlertTriangle },
+};
+
+function TenantStatusBadge({ status }: { status: TenantStatus }) {
+ const config = STATUS_CONFIG[status] || STATUS_CONFIG.pending;
+ const Icon = config.icon;
+ return (
+
+
+ {config.label}
+
+ );
+}
+
+// ==================== Form Types ====================
+
+interface FormData {
+ name: string;
+ slug: string;
+ domain: string;
+ subdomain: string;
+ ownerName: string;
+ ownerEmail: string;
+ phone: string;
+ timezone: string;
+ locale: string;
+ currency: string;
+ maxUsers: number;
+ maxBranches: number;
+ maxStorageGb: number;
+ maxApiCallsMonthly: number;
+ // Features
+ advancedReporting: boolean;
+ apiAccess: boolean;
+ customBranding: boolean;
+ sso: boolean;
+ // Modules
+ moduleCrm: boolean;
+ moduleInventory: boolean;
+ moduleSales: boolean;
+ modulePurchases: boolean;
+ moduleHr: boolean;
+ moduleAccounting: boolean;
+ moduleProjects: boolean;
+ modulePos: boolean;
+}
+
+// ==================== Options ====================
+
+const TIMEZONE_OPTIONS = [
+ { value: 'America/Mexico_City', label: 'America/Mexico_City (UTC-6)' },
+ { value: 'America/New_York', label: 'America/New_York (UTC-5)' },
+ { value: 'America/Los_Angeles', label: 'America/Los_Angeles (UTC-8)' },
+ { value: 'America/Chicago', label: 'America/Chicago (UTC-6)' },
+ { value: 'America/Denver', label: 'America/Denver (UTC-7)' },
+ { value: 'Europe/London', label: 'Europe/London (UTC+0)' },
+ { value: 'Europe/Madrid', label: 'Europe/Madrid (UTC+1)' },
+ { value: 'Asia/Shanghai', label: 'Asia/Shanghai (UTC+8)' },
+ { value: 'UTC', label: 'UTC' },
+];
+
+const LOCALE_OPTIONS = [
+ { value: 'es-MX', label: 'Espanol (Mexico)' },
+ { value: 'es-ES', label: 'Espanol (Espana)' },
+ { value: 'en-US', label: 'English (US)' },
+ { value: 'en-GB', label: 'English (UK)' },
+ { value: 'pt-BR', label: 'Portugues (Brasil)' },
+ { value: 'zh-CN', label: 'Chino (Simplificado)' },
+];
+
+const CURRENCY_OPTIONS = [
+ { value: 'MXN', label: 'MXN - Peso Mexicano' },
+ { value: 'USD', label: 'USD - Dolar Americano' },
+ { value: 'EUR', label: 'EUR - Euro' },
+ { value: 'GBP', label: 'GBP - Libra Esterlina' },
+ { value: 'CNY', label: 'CNY - Yuan Chino' },
+];
+
+// ==================== Main Component ====================
+
+export function TenantEditPage() {
+ const { id } = useParams<{ id: string }>();
+ const navigate = useNavigate();
+ const { showToast } = useToast();
+ const { tenant, isLoading, error, refresh } = useTenant(id);
+ const { updateTenant } = useTenants();
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [submitError, setSubmitError] = useState(null);
+
+ const {
+ register,
+ handleSubmit,
+ watch,
+ setValue,
+ reset,
+ formState: { errors, isDirty },
+ } = useForm();
+
+ // Populate form when tenant loads
+ useEffect(() => {
+ if (tenant) {
+ reset({
+ name: tenant.name,
+ slug: tenant.slug,
+ domain: tenant.domain || '',
+ subdomain: tenant.subdomain || '',
+ ownerName: tenant.ownerName,
+ ownerEmail: tenant.ownerEmail,
+ phone: tenant.phone || '',
+ timezone: tenant.timezone || 'UTC',
+ locale: tenant.locale || 'en-US',
+ currency: tenant.currency || 'USD',
+ maxUsers: tenant.maxUsers,
+ maxBranches: tenant.maxBranches,
+ maxStorageGb: tenant.maxStorageGb,
+ maxApiCallsMonthly: tenant.maxApiCallsMonthly,
+ advancedReporting: tenant.features.advancedReporting || false,
+ apiAccess: tenant.features.apiAccess || false,
+ customBranding: tenant.features.customBranding || false,
+ sso: tenant.features.sso || false,
+ moduleCrm: tenant.enabledModules.includes('crm'),
+ moduleInventory: tenant.enabledModules.includes('inventory'),
+ moduleSales: tenant.enabledModules.includes('sales'),
+ modulePurchases: tenant.enabledModules.includes('purchases'),
+ moduleHr: tenant.enabledModules.includes('hr'),
+ moduleAccounting: tenant.enabledModules.includes('accounting'),
+ moduleProjects: tenant.enabledModules.includes('projects'),
+ modulePos: tenant.enabledModules.includes('pos'),
+ });
+ }
+ }, [tenant, reset]);
+
+ const onFormSubmit = async (data: FormData) => {
+ if (!id) return;
+
+ setIsSubmitting(true);
+ setSubmitError(null);
+
+ try {
+ // Build enabled modules array
+ const enabledModules: string[] = [];
+ if (data.moduleCrm) enabledModules.push('crm');
+ if (data.moduleInventory) enabledModules.push('inventory');
+ if (data.moduleSales) enabledModules.push('sales');
+ if (data.modulePurchases) enabledModules.push('purchases');
+ if (data.moduleHr) enabledModules.push('hr');
+ if (data.moduleAccounting) enabledModules.push('accounting');
+ if (data.moduleProjects) enabledModules.push('projects');
+ if (data.modulePos) enabledModules.push('pos');
+
+ const updateData: UpdateTenantDto = {
+ name: data.name,
+ slug: data.slug,
+ domain: data.domain || null,
+ subdomain: data.subdomain || null,
+ ownerName: data.ownerName,
+ ownerEmail: data.ownerEmail,
+ phone: data.phone || null,
+ timezone: data.timezone,
+ locale: data.locale,
+ currency: data.currency,
+ maxUsers: data.maxUsers,
+ maxBranches: data.maxBranches,
+ maxStorageGb: data.maxStorageGb,
+ maxApiCallsMonthly: data.maxApiCallsMonthly,
+ enabledModules,
+ features: {
+ advancedReporting: data.advancedReporting,
+ apiAccess: data.apiAccess,
+ customBranding: data.customBranding,
+ sso: data.sso,
+ },
+ };
+
+ await updateTenant(id, updateData);
+ showToast({
+ type: 'success',
+ title: 'Tenant actualizado',
+ message: 'Los cambios han sido guardados exitosamente.',
+ });
+ navigate(`/superadmin/tenants/${id}`);
+ } catch (err) {
+ const message = err instanceof Error ? err.message : 'Error al actualizar tenant';
+ setSubmitError(message);
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ // ==================== Loading / Error States ====================
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ if (error || !tenant) {
+ return (
+
+
+
+ );
+ }
+
+ // ==================== Render ====================
+
+ return (
+
+ {/* Breadcrumbs */}
+
+
+ {/* Header */}
+
+
navigate(`/superadmin/tenants/${id}`)}>
+
+ Volver
+
+
+
+
+
+
+
+
Editar tenant
+
+
+
+ Modifica la configuracion de {tenant.name}
+
+
+
+
+
+ {/* Form */}
+
+
+ );
+}
+
+export default TenantEditPage;
diff --git a/src/pages/superadmin/tenants/TenantsListPage.tsx b/src/pages/superadmin/tenants/TenantsListPage.tsx
new file mode 100644
index 0000000..58a60ff
--- /dev/null
+++ b/src/pages/superadmin/tenants/TenantsListPage.tsx
@@ -0,0 +1,500 @@
+/**
+ * TenantsListPage - Superadmin Portal
+ * Main page for managing all tenants in the system
+ */
+
+import { useState, useMemo } from 'react';
+import { useNavigate } from 'react-router-dom';
+import {
+ Plus,
+ MoreVertical,
+ Eye,
+ Edit,
+ Pause,
+ Play,
+ Trash2,
+ Building2,
+ Users,
+ Search,
+ AlertTriangle,
+ CheckCircle,
+ Clock,
+ XCircle,
+} from 'lucide-react';
+import { Button } from '@components/atoms/Button';
+import { Input } from '@components/atoms/Input';
+import { Badge } from '@components/atoms/Badge';
+import { Card, CardHeader, CardTitle, CardContent } from '@components/molecules/Card';
+import { DataTable, type Column } from '@components/organisms/DataTable';
+import { Dropdown, type DropdownItem } from '@components/organisms/Dropdown';
+import { Select, type SelectOption } from '@components/organisms/Select';
+import { ConfirmModal } from '@components/organisms/Modal';
+import { Breadcrumbs } from '@components/organisms/Breadcrumbs';
+import { NoDataEmptyState, ErrorEmptyState, NoResultsEmptyState } from '@components/templates/EmptyState';
+import { StatCard } from '@components/organisms/DashboardWidgets/StatCard';
+import { useTenants } from '@features/superadmin';
+import type { Tenant, TenantStatus, TenantPlanTier, SuspendTenantDto } from '@features/superadmin';
+import { formatDate } from '@utils/formatters';
+
+// ==================== Status Badge Configuration ====================
+
+const STATUS_CONFIG: Record<
+ TenantStatus,
+ { label: string; variant: 'success' | 'info' | 'danger' | 'default' | 'warning'; icon: typeof CheckCircle }
+> = {
+ active: { label: 'Activo', variant: 'success', icon: CheckCircle },
+ trial: { label: 'Prueba', variant: 'info', icon: Clock },
+ suspended: { label: 'Suspendido', variant: 'danger', icon: Pause },
+ cancelled: { label: 'Cancelado', variant: 'default', icon: XCircle },
+ pending: { label: 'Pendiente', variant: 'warning', icon: AlertTriangle },
+};
+
+const PLAN_LABELS: Record = {
+ free: 'Gratis',
+ starter: 'Starter',
+ professional: 'Profesional',
+ enterprise: 'Enterprise',
+ custom: 'Personalizado',
+};
+
+// ==================== Status Badge Component ====================
+
+interface StatusBadgeProps {
+ status: TenantStatus;
+}
+
+function StatusBadge({ status }: StatusBadgeProps) {
+ const config = STATUS_CONFIG[status] || STATUS_CONFIG.pending;
+ const Icon = config.icon;
+
+ return (
+
+
+ {config.label}
+
+ );
+}
+
+// ==================== Filter Options ====================
+
+const STATUS_OPTIONS: SelectOption[] = [
+ { value: '', label: 'Todos los estados' },
+ { value: 'active', label: 'Activo' },
+ { value: 'trial', label: 'Prueba' },
+ { value: 'suspended', label: 'Suspendido' },
+ { value: 'cancelled', label: 'Cancelado' },
+ { value: 'pending', label: 'Pendiente' },
+];
+
+const PLAN_OPTIONS: SelectOption[] = [
+ { value: '', label: 'Todos los planes' },
+ { value: 'free', label: 'Gratis' },
+ { value: 'starter', label: 'Starter' },
+ { value: 'professional', label: 'Profesional' },
+ { value: 'enterprise', label: 'Enterprise' },
+ { value: 'custom', label: 'Personalizado' },
+];
+
+// ==================== Main Component ====================
+
+export function TenantsListPage() {
+ const navigate = useNavigate();
+ const [searchInput, setSearchInput] = useState('');
+ const [tenantToDelete, setTenantToDelete] = useState(null);
+ const [tenantToSuspend, setTenantToSuspend] = useState(null);
+ const [tenantToActivate, setTenantToActivate] = useState(null);
+
+ const {
+ tenants,
+ total,
+ page,
+ totalPages,
+ isLoading,
+ error,
+ filters,
+ setFilters,
+ deleteTenant,
+ suspendTenant,
+ activateTenant,
+ refresh,
+ } = useTenants({ page: 1, limit: 10, sortBy: 'createdAt', sortOrder: 'desc' });
+
+ // Calculate stats from tenants
+ const stats = useMemo(() => {
+ // Note: In production, these would come from the API
+ // For now, we calculate from loaded tenants (which is a subset)
+ return {
+ total: total,
+ active: tenants.filter((t) => t.status === 'active').length,
+ trial: tenants.filter((t) => t.status === 'trial').length,
+ suspended: tenants.filter((t) => t.status === 'suspended').length,
+ };
+ }, [tenants, total]);
+
+ // ==================== Handlers ====================
+
+ const handleSearch = () => {
+ setFilters({ ...filters, search: searchInput, page: 1 });
+ };
+
+ const handleSearchKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter') {
+ handleSearch();
+ }
+ };
+
+ const handleStatusChange = (value: string | string[]) => {
+ const status = (value as string) || undefined;
+ setFilters({ ...filters, status: status as TenantStatus | undefined, page: 1 });
+ };
+
+ const handlePlanChange = (value: string | string[]) => {
+ const planTier = (value as string) || undefined;
+ setFilters({ ...filters, planTier: planTier as TenantPlanTier | undefined, page: 1 });
+ };
+
+ const handlePageChange = (newPage: number) => {
+ setFilters({ ...filters, page: newPage });
+ };
+
+ const handleSort = (key: string) => {
+ const newOrder = filters.sortBy === key && filters.sortOrder === 'asc' ? 'desc' : 'asc';
+ setFilters({ ...filters, sortBy: key as typeof filters.sortBy, sortOrder: newOrder });
+ };
+
+ const handleDeleteConfirm = async () => {
+ if (tenantToDelete) {
+ await deleteTenant(tenantToDelete.id);
+ setTenantToDelete(null);
+ }
+ };
+
+ const handleSuspendConfirm = async () => {
+ if (tenantToSuspend) {
+ const suspendData: SuspendTenantDto = {
+ reason: 'Suspended by superadmin',
+ suspendImmediately: true,
+ notifyOwner: true,
+ };
+ await suspendTenant(tenantToSuspend.id, suspendData);
+ setTenantToSuspend(null);
+ }
+ };
+
+ const handleActivateConfirm = async () => {
+ if (tenantToActivate) {
+ await activateTenant(tenantToActivate.id, { notifyOwner: true });
+ setTenantToActivate(null);
+ }
+ };
+
+ const clearFilters = () => {
+ setSearchInput('');
+ setFilters({ page: 1, limit: 10, sortBy: 'createdAt', sortOrder: 'desc' });
+ };
+
+ // ==================== Actions Menu ====================
+
+ const getActionsMenu = (tenant: Tenant): DropdownItem[] => {
+ const items: DropdownItem[] = [
+ {
+ key: 'view',
+ label: 'Ver detalle',
+ icon: ,
+ onClick: () => navigate(`/superadmin/tenants/${tenant.id}`),
+ },
+ {
+ key: 'edit',
+ label: 'Editar',
+ icon: ,
+ onClick: () => navigate(`/superadmin/tenants/${tenant.id}/edit`),
+ },
+ ];
+
+ // Add suspend/activate based on current status
+ if (tenant.status === 'active' || tenant.status === 'trial') {
+ items.push({
+ key: 'suspend',
+ label: 'Suspender',
+ icon: ,
+ danger: true,
+ onClick: () => setTenantToSuspend(tenant),
+ });
+ }
+
+ if (tenant.status === 'suspended') {
+ items.push({
+ key: 'activate',
+ label: 'Reactivar',
+ icon: ,
+ onClick: () => setTenantToActivate(tenant),
+ });
+ }
+
+ items.push({
+ key: 'delete',
+ label: 'Eliminar',
+ icon: ,
+ danger: true,
+ onClick: () => setTenantToDelete(tenant),
+ });
+
+ return items;
+ };
+
+ // ==================== Table Columns ====================
+
+ const columns: Column[] = [
+ {
+ key: 'tenant',
+ header: 'Tenant',
+ render: (tenant) => (
+
+
+
+
+
+
{tenant.name}
+
{tenant.domain || tenant.subdomain || tenant.slug}
+
+
+ ),
+ },
+ {
+ key: 'planTier',
+ header: 'Plan',
+ sortable: true,
+ render: (tenant) => (
+
+ {PLAN_LABELS[tenant.planTier] || tenant.planName || tenant.planTier}
+
+ ),
+ },
+ {
+ key: 'status',
+ header: 'Estado',
+ sortable: true,
+ render: (tenant) => ,
+ },
+ {
+ key: 'users',
+ header: 'Usuarios',
+ render: (tenant) => (
+
+
+ {tenant.maxUsers}
+
+ ),
+ },
+ {
+ key: 'createdAt',
+ header: 'Creado',
+ sortable: true,
+ render: (tenant) => (
+ {formatDate(tenant.createdAt, 'short')}
+ ),
+ },
+ {
+ key: 'actions',
+ header: '',
+ align: 'right',
+ render: (tenant) => (
+
+
+
+ }
+ items={getActionsMenu(tenant)}
+ align="right"
+ />
+ ),
+ },
+ ];
+
+ // ==================== Render ====================
+
+ if (error) {
+ return (
+
+
+
+ );
+ }
+
+ const hasFilters = filters.search || filters.status || filters.planTier;
+
+ return (
+
+ {/* Breadcrumbs */}
+
+
+ {/* Header */}
+
+
+
Tenants
+
+ Administra todos los tenants de la plataforma
+
+
+
navigate('/superadmin/tenants/new')}>
+
+ Nuevo Tenant
+
+
+
+ {/* Stats Cards */}
+
+
+
+
+
+
+
+ {/* Main Content Card */}
+
+
+ Lista de Tenants
+
+
+
+ {/* Filters */}
+
+ {/* Search */}
+
+
+ setSearchInput(e.target.value)}
+ onKeyDown={handleSearchKeyDown}
+ className="pl-10"
+ />
+
+
+ {/* Status Filter */}
+
+
+
+
+ {/* Plan Filter */}
+
+
+
+
+ {/* Search Button */}
+
+
+ Buscar
+
+
+
+ {/* Table or Empty State */}
+ {tenants.length === 0 && !isLoading ? (
+ hasFilters ? (
+
+ ) : (
+
navigate('/superadmin/tenants/new')}
+ />
+ )
+ ) : (
+
+ )}
+
+
+
+
+ {/* Delete Confirmation Modal */}
+
setTenantToDelete(null)}
+ onConfirm={handleDeleteConfirm}
+ title="Eliminar tenant"
+ message={`¿Estas seguro de que deseas eliminar "${tenantToDelete?.name}"? Esta accion eliminara todos los datos asociados y no se puede deshacer.`}
+ variant="danger"
+ confirmText="Eliminar"
+ />
+
+ {/* Suspend Confirmation Modal */}
+ setTenantToSuspend(null)}
+ onConfirm={handleSuspendConfirm}
+ title="Suspender tenant"
+ message={`¿Estas seguro de que deseas suspender "${tenantToSuspend?.name}"? Los usuarios no podran acceder hasta que se reactive.`}
+ variant="danger"
+ confirmText="Suspender"
+ />
+
+ {/* Activate Confirmation Modal */}
+ setTenantToActivate(null)}
+ onConfirm={handleActivateConfirm}
+ title="Reactivar tenant"
+ message={`¿Deseas reactivar "${tenantToActivate?.name}"? Los usuarios podran acceder nuevamente.`}
+ variant="success"
+ confirmText="Reactivar"
+ />
+
+ );
+}
+
+export default TenantsListPage;
diff --git a/src/pages/superadmin/tenants/index.ts b/src/pages/superadmin/tenants/index.ts
new file mode 100644
index 0000000..4feda7a
--- /dev/null
+++ b/src/pages/superadmin/tenants/index.ts
@@ -0,0 +1,9 @@
+/**
+ * Superadmin Tenants Pages - Index
+ * Exports all tenant management pages
+ */
+
+export { TenantsListPage } from './TenantsListPage';
+export { TenantDetailPage } from './TenantDetailPage';
+export { TenantCreatePage } from './TenantCreatePage';
+export { TenantEditPage } from './TenantEditPage';