855 lines
36 KiB
TypeScript
855 lines
36 KiB
TypeScript
/**
|
|
* Subscription Management Page
|
|
* Allows users to view current subscription, billing details, and upgrade plans
|
|
*/
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
Card,
|
|
Button,
|
|
Badge,
|
|
Modal
|
|
} from '../../../../components/ui';
|
|
import { PageHeader } from '../../../../components/layout';
|
|
import {
|
|
CreditCard,
|
|
Users,
|
|
MapPin,
|
|
Package,
|
|
TrendingUp,
|
|
Calendar,
|
|
CheckCircle,
|
|
AlertCircle,
|
|
ArrowRight,
|
|
Crown,
|
|
Star,
|
|
Zap,
|
|
X,
|
|
RefreshCw,
|
|
Settings,
|
|
Download,
|
|
ExternalLink
|
|
} from 'lucide-react';
|
|
import { useAuth } from '../../../../hooks/api/useAuth';
|
|
import { useBakeryStore } from '../../../../stores/bakery.store';
|
|
import { useToast } from '../../../../hooks/ui/useToast';
|
|
import {
|
|
subscriptionService,
|
|
type UsageSummary,
|
|
type AvailablePlans
|
|
} from '../../../../services/api';
|
|
import { isMockMode, getMockSubscription } from '../../../../config/mock.config';
|
|
|
|
interface PlanComparisonProps {
|
|
plans: AvailablePlans['plans'];
|
|
currentPlan: string;
|
|
onUpgrade: (planKey: string) => void;
|
|
}
|
|
|
|
const ProgressBar: React.FC<{ value: number; className?: string }> = ({ value, className = '' }) => {
|
|
const getProgressColor = () => {
|
|
if (value >= 90) return 'bg-red-500';
|
|
if (value >= 80) return 'bg-yellow-500';
|
|
return 'bg-green-500';
|
|
};
|
|
|
|
return (
|
|
<div className={`w-full bg-[var(--bg-tertiary)] border border-[var(--border-secondary)] rounded-full h-3 ${className}`}>
|
|
<div
|
|
className={`${getProgressColor()} h-full rounded-full transition-all duration-500 relative`}
|
|
style={{ width: `${Math.min(100, Math.max(0, value))}%` }}
|
|
>
|
|
<div className="absolute inset-0 bg-gradient-to-r from-transparent to-white/20 rounded-full"></div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// Tabs implementation
|
|
interface TabsProps {
|
|
defaultValue: string;
|
|
className?: string;
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
interface TabsListProps {
|
|
className?: string;
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
interface TabsTriggerProps {
|
|
value: string;
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
}
|
|
|
|
interface TabsContentProps {
|
|
value: string;
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
}
|
|
|
|
const TabsContext = React.createContext<{ activeTab: string; setActiveTab: (value: string) => void } | null>(null);
|
|
|
|
const Tabs: React.FC<TabsProps> & {
|
|
List: React.FC<TabsListProps>;
|
|
Trigger: React.FC<TabsTriggerProps>;
|
|
Content: React.FC<TabsContentProps>;
|
|
} = ({
|
|
defaultValue,
|
|
className = '',
|
|
children
|
|
}) => {
|
|
const [activeTab, setActiveTab] = useState(defaultValue);
|
|
|
|
return (
|
|
<div className={className}>
|
|
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
|
|
{children}
|
|
</TabsContext.Provider>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const TabsList: React.FC<TabsListProps> = ({ className = '', children }) => {
|
|
return (
|
|
<div className={`flex border-b border-[var(--border-primary)] bg-[var(--bg-primary)] ${className}`}>
|
|
{children}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const TabsTrigger: React.FC<TabsTriggerProps> = ({ value, children, className = '' }) => {
|
|
const context = React.useContext(TabsContext);
|
|
if (!context) throw new Error('TabsTrigger must be used within Tabs');
|
|
|
|
const { activeTab, setActiveTab } = context;
|
|
const isActive = activeTab === value;
|
|
|
|
return (
|
|
<button
|
|
onClick={() => setActiveTab(value)}
|
|
className={`px-6 py-4 text-sm font-medium border-b-2 transition-all duration-200 relative flex items-center ${
|
|
isActive
|
|
? 'text-[var(--color-primary)] border-[var(--color-primary)] bg-[var(--bg-primary)]'
|
|
: 'text-[var(--text-secondary)] border-transparent hover:text-[var(--text-primary)] hover:border-[var(--color-primary)]/30 hover:bg-[var(--bg-secondary)]'
|
|
} ${className}`}
|
|
>
|
|
{children}
|
|
</button>
|
|
);
|
|
};
|
|
|
|
const TabsContent: React.FC<TabsContentProps> = ({ value, children, className = '' }) => {
|
|
const context = React.useContext(TabsContext);
|
|
if (!context) throw new Error('TabsContent must be used within Tabs');
|
|
|
|
const { activeTab } = context;
|
|
|
|
if (activeTab !== value) return null;
|
|
|
|
return (
|
|
<div className={`bg-[var(--bg-primary)] rounded-b-lg p-6 ${className}`}>
|
|
{children}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
Tabs.List = TabsList;
|
|
Tabs.Trigger = TabsTrigger;
|
|
Tabs.Content = TabsContent;
|
|
|
|
const PlanComparison: React.FC<PlanComparisonProps> = ({ plans, currentPlan, onUpgrade }) => {
|
|
const planOrder = ['starter', 'professional', 'enterprise'];
|
|
const sortedPlans = Object.entries(plans).sort(([a], [b]) =>
|
|
planOrder.indexOf(a) - planOrder.indexOf(b)
|
|
);
|
|
|
|
const getPlanColor = (planKey: string) => {
|
|
switch (planKey) {
|
|
case 'starter': return 'border-blue-500/30 bg-blue-500/5';
|
|
case 'professional': return 'border-purple-500/30 bg-purple-500/5';
|
|
case 'enterprise': return 'border-amber-500/30 bg-amber-500/5';
|
|
default: return 'border-[var(--border-primary)] bg-[var(--bg-secondary)]';
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
{sortedPlans.map(([planKey, plan]) => (
|
|
<Card
|
|
key={planKey}
|
|
className={`relative p-6 ${getPlanColor(planKey)} ${
|
|
currentPlan === planKey ? 'ring-2 ring-[var(--color-primary)]' : ''
|
|
}`}
|
|
>
|
|
{plan.popular && (
|
|
<div className="absolute -top-3 left-1/2 transform -translate-x-1/2">
|
|
<Badge variant="primary" className="px-3 py-1">
|
|
<Star className="w-3 h-3 mr-1" />
|
|
Más Popular
|
|
</Badge>
|
|
</div>
|
|
)}
|
|
|
|
<div className="text-center mb-6">
|
|
<h3 className="text-xl font-bold text-[var(--text-primary)] mb-2">{plan.name}</h3>
|
|
<div className="text-3xl font-bold text-[var(--color-primary)] mb-1">
|
|
{subscriptionService.formatPrice(plan.monthly_price)}
|
|
<span className="text-lg text-[var(--text-secondary)]">/mes</span>
|
|
</div>
|
|
<p className="text-sm text-[var(--text-secondary)]">{plan.description}</p>
|
|
</div>
|
|
|
|
<div className="space-y-3 mb-6">
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<Users className="w-4 h-4 text-[var(--color-primary)]" />
|
|
<span>{plan.max_users === -1 ? 'Usuarios ilimitados' : `${plan.max_users} usuarios`}</span>
|
|
</div>
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<MapPin className="w-4 h-4 text-[var(--color-primary)]" />
|
|
<span>{plan.max_locations === -1 ? 'Ubicaciones ilimitadas' : `${plan.max_locations} ubicación${plan.max_locations > 1 ? 'es' : ''}`}</span>
|
|
</div>
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<Package className="w-4 h-4 text-[var(--color-primary)]" />
|
|
<span>{plan.max_products === -1 ? 'Productos ilimitados' : `${plan.max_products} productos`}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{currentPlan === planKey ? (
|
|
<Badge variant="success" className="w-full justify-center py-2">
|
|
<CheckCircle className="w-4 h-4 mr-2" />
|
|
Plan Actual
|
|
</Badge>
|
|
) : (
|
|
<Button
|
|
variant={plan.popular ? 'primary' : 'outline'}
|
|
className="w-full"
|
|
onClick={() => onUpgrade(planKey)}
|
|
>
|
|
{plan.contact_sales ? 'Contactar Ventas' : 'Cambiar Plan'}
|
|
<ArrowRight className="w-4 h-4 ml-2" />
|
|
</Button>
|
|
)}
|
|
</Card>
|
|
))}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const SubscriptionPage: React.FC = () => {
|
|
const { user, tenant_id } = useAuth();
|
|
const { currentTenant } = useBakeryStore();
|
|
const toast = useToast();
|
|
const [usageSummary, setUsageSummary] = useState<UsageSummary | null>(null);
|
|
const [availablePlans, setAvailablePlans] = useState<AvailablePlans | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [upgradeDialogOpen, setUpgradeDialogOpen] = useState(false);
|
|
const [selectedPlan, setSelectedPlan] = useState<string>('');
|
|
const [upgrading, setUpgrading] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (currentTenant?.id || tenant_id || isMockMode()) {
|
|
loadSubscriptionData();
|
|
}
|
|
}, [currentTenant, tenant_id]);
|
|
|
|
const loadSubscriptionData = async () => {
|
|
let tenantId = currentTenant?.id || tenant_id;
|
|
|
|
// In mock mode, use the mock tenant ID if no real tenant is available
|
|
if (isMockMode() && !tenantId) {
|
|
tenantId = getMockSubscription().tenant_id;
|
|
console.log('🧪 Mock mode: Using mock tenant ID:', tenantId);
|
|
}
|
|
|
|
console.log('📊 Loading subscription data for tenant:', tenantId, '| Mock mode:', isMockMode());
|
|
|
|
if (!tenantId) return;
|
|
|
|
try {
|
|
setLoading(true);
|
|
const [usage, plans] = await Promise.all([
|
|
subscriptionService.getUsageSummary(tenantId),
|
|
subscriptionService.getAvailablePlans()
|
|
]);
|
|
|
|
setUsageSummary(usage);
|
|
setAvailablePlans(plans);
|
|
} catch (error) {
|
|
console.error('Error loading subscription data:', error);
|
|
toast.error("No se pudo cargar la información de suscripción");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleUpgradeClick = (planKey: string) => {
|
|
setSelectedPlan(planKey);
|
|
setUpgradeDialogOpen(true);
|
|
};
|
|
|
|
const handleUpgradeConfirm = async () => {
|
|
let tenantId = currentTenant?.id || tenant_id;
|
|
|
|
// In mock mode, use the mock tenant ID if no real tenant is available
|
|
if (isMockMode() && !tenantId) {
|
|
tenantId = getMockSubscription().tenant_id;
|
|
}
|
|
|
|
if (!tenantId || !selectedPlan) return;
|
|
|
|
try {
|
|
setUpgrading(true);
|
|
|
|
const validation = await subscriptionService.validatePlanUpgrade(
|
|
tenantId,
|
|
selectedPlan
|
|
);
|
|
|
|
if (!validation.can_upgrade) {
|
|
toast.error(validation.reason);
|
|
return;
|
|
}
|
|
|
|
const result = await subscriptionService.upgradePlan(tenantId, selectedPlan);
|
|
|
|
if (result.success) {
|
|
toast.success(result.message);
|
|
|
|
await loadSubscriptionData();
|
|
setUpgradeDialogOpen(false);
|
|
setSelectedPlan('');
|
|
} else {
|
|
toast.error('Error al cambiar el plan');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error upgrading plan:', error);
|
|
toast.error('Error al procesar el cambio de plan');
|
|
} finally {
|
|
setUpgrading(false);
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="space-y-6">
|
|
<PageHeader
|
|
title="Suscripción"
|
|
description="Gestiona tu plan de suscripción y facturación"
|
|
icon={CreditCard}
|
|
loading={loading}
|
|
/>
|
|
<div className="flex items-center justify-center min-h-[400px]">
|
|
<div className="flex flex-col items-center gap-4">
|
|
<RefreshCw className="w-8 h-8 animate-spin text-[var(--color-primary)]" />
|
|
<p className="text-[var(--text-secondary)]">Cargando información de suscripción...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!usageSummary || !availablePlans) {
|
|
return (
|
|
<div className="space-y-6">
|
|
<PageHeader
|
|
title="Suscripción"
|
|
description="Gestiona tu plan de suscripción y facturación"
|
|
icon={CreditCard}
|
|
error="No se pudo cargar la información de suscripción"
|
|
onRefresh={loadSubscriptionData}
|
|
showRefreshButton
|
|
/>
|
|
<div className="flex items-center justify-center min-h-[400px]">
|
|
<div className="flex flex-col items-center gap-4">
|
|
<AlertCircle className="w-12 h-12 text-[var(--text-tertiary)]" />
|
|
<div className="text-center">
|
|
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">No se pudo cargar la información</h3>
|
|
<p className="text-[var(--text-secondary)] mb-4">Hubo un problema al cargar los datos de suscripción</p>
|
|
<Button onClick={loadSubscriptionData} variant="primary">
|
|
<RefreshCw className="w-4 h-4 mr-2" />
|
|
Reintentar
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const nextBillingDate = usageSummary.next_billing_date
|
|
? new Date(usageSummary.next_billing_date).toLocaleDateString('es-ES', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric'
|
|
})
|
|
: 'No disponible';
|
|
|
|
const planInfo = subscriptionService.getPlanDisplayInfo(usageSummary.plan);
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<PageHeader
|
|
title="Suscripción"
|
|
subtitle={`Plan ${planInfo.name}`}
|
|
description="Gestiona tu plan de suscripción y facturación"
|
|
icon={CreditCard}
|
|
status={{
|
|
text: usageSummary.status === 'active' ? 'Activo' : usageSummary.status,
|
|
variant: usageSummary.status === 'active' ? 'success' : 'default'
|
|
}}
|
|
actions={[
|
|
{
|
|
id: 'manage-billing',
|
|
label: 'Gestionar Facturación',
|
|
icon: ExternalLink,
|
|
onClick: () => window.open('https://billing.bakery.com', '_blank'),
|
|
variant: 'outline'
|
|
},
|
|
{
|
|
id: 'download-invoice',
|
|
label: 'Descargar Factura',
|
|
icon: Download,
|
|
onClick: () => console.log('Download latest invoice'),
|
|
variant: 'outline'
|
|
}
|
|
]}
|
|
metadata={[
|
|
{
|
|
id: 'next-billing',
|
|
label: 'Próxima facturación',
|
|
value: nextBillingDate,
|
|
icon: Calendar
|
|
},
|
|
{
|
|
id: 'monthly-cost',
|
|
label: 'Coste mensual',
|
|
value: subscriptionService.formatPrice(usageSummary.monthly_price),
|
|
icon: CreditCard
|
|
}
|
|
]}
|
|
onRefresh={loadSubscriptionData}
|
|
showRefreshButton
|
|
/>
|
|
|
|
{/* Quick Stats Overview */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
<Card className="p-6 border border-[var(--border-primary)] bg-[var(--bg-primary)]">
|
|
<div className="flex items-center gap-4">
|
|
<div className="p-3 bg-blue-500/10 rounded-xl border border-blue-500/20">
|
|
<Users className="w-6 h-6 text-blue-500" />
|
|
</div>
|
|
<div className="flex-1">
|
|
<p className="text-sm text-[var(--text-secondary)] mb-1">Usuarios</p>
|
|
<p className="text-xl font-bold text-[var(--text-primary)]">
|
|
{usageSummary.usage.users.current}<span className="text-[var(--text-tertiary)]">/{usageSummary.usage.users.unlimited ? '∞' : usageSummary.usage.users.limit}</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
<Card className="p-6 border border-[var(--border-primary)] bg-[var(--bg-primary)]">
|
|
<div className="flex items-center gap-4">
|
|
<div className="p-3 bg-green-500/10 rounded-xl border border-green-500/20">
|
|
<MapPin className="w-6 h-6 text-green-500" />
|
|
</div>
|
|
<div className="flex-1">
|
|
<p className="text-sm text-[var(--text-secondary)] mb-1">Ubicaciones</p>
|
|
<p className="text-xl font-bold text-[var(--text-primary)]">
|
|
{usageSummary.usage.locations.current}<span className="text-[var(--text-tertiary)]">/{usageSummary.usage.locations.unlimited ? '∞' : usageSummary.usage.locations.limit}</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
<Card className="p-6 border border-[var(--border-primary)] bg-[var(--bg-primary)]">
|
|
<div className="flex items-center gap-4">
|
|
<div className="p-3 bg-purple-500/10 rounded-xl border border-purple-500/20">
|
|
<Package className="w-6 h-6 text-purple-500" />
|
|
</div>
|
|
<div className="flex-1">
|
|
<p className="text-sm text-[var(--text-secondary)] mb-1">Productos</p>
|
|
<p className="text-xl font-bold text-[var(--text-primary)]">
|
|
{usageSummary.usage.products.current}<span className="text-[var(--text-tertiary)]">/{usageSummary.usage.products.unlimited ? '∞' : usageSummary.usage.products.limit}</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
<Card className="p-6 border border-[var(--border-primary)] bg-[var(--bg-primary)]">
|
|
<div className="flex items-center gap-4">
|
|
<div className="p-3 bg-yellow-500/10 rounded-xl border border-yellow-500/20">
|
|
<TrendingUp className="w-6 h-6 text-yellow-500" />
|
|
</div>
|
|
<div className="flex-1">
|
|
<p className="text-sm text-[var(--text-secondary)] mb-1">Estado</p>
|
|
<Badge
|
|
variant={usageSummary.status === 'active' ? 'success' : 'default'}
|
|
className="text-sm font-medium"
|
|
>
|
|
{usageSummary.status === 'active' ? 'Activo' : usageSummary.status}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<Card className="overflow-hidden">
|
|
<Tabs defaultValue="overview">
|
|
<Tabs.List>
|
|
<Tabs.Trigger value="overview">
|
|
<TrendingUp className="w-4 h-4 mr-2" />
|
|
Resumen
|
|
</Tabs.Trigger>
|
|
<Tabs.Trigger value="usage">
|
|
<Users className="w-4 h-4 mr-2" />
|
|
Uso
|
|
</Tabs.Trigger>
|
|
<Tabs.Trigger value="plans">
|
|
<Crown className="w-4 h-4 mr-2" />
|
|
Planes
|
|
</Tabs.Trigger>
|
|
<Tabs.Trigger value="billing">
|
|
<CreditCard className="w-4 h-4 mr-2" />
|
|
Facturación
|
|
</Tabs.Trigger>
|
|
</Tabs.List>
|
|
|
|
<Tabs.Content value="overview">
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
{/* Current Plan Summary */}
|
|
<Card className="p-6 lg:col-span-2 border border-[var(--border-primary)] bg-[var(--bg-primary)]">
|
|
<h3 className="text-lg font-semibold mb-6 flex items-center text-[var(--text-primary)]">
|
|
<Crown className="w-5 h-5 mr-2 text-yellow-500" />
|
|
Tu Plan Actual
|
|
</h3>
|
|
<div className="space-y-6">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div className="p-4 bg-[var(--bg-secondary)] border border-[var(--border-secondary)] rounded-lg">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm text-[var(--text-secondary)]">Plan</span>
|
|
<div className="flex items-center gap-2">
|
|
<span className="font-semibold text-[var(--text-primary)]">{planInfo.name}</span>
|
|
{usageSummary.plan === 'professional' && (
|
|
<Badge variant="primary" size="sm">
|
|
<Star className="w-3 h-3 mr-1" />
|
|
Popular
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="p-4 bg-[var(--bg-secondary)] border border-[var(--border-secondary)] rounded-lg">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm text-[var(--text-secondary)]">Precio</span>
|
|
<span className="font-semibold text-[var(--text-primary)]">{subscriptionService.formatPrice(usageSummary.monthly_price)}/mes</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div className="p-4 bg-[var(--bg-secondary)] border border-[var(--border-secondary)] rounded-lg">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm text-[var(--text-secondary)]">Estado</span>
|
|
<Badge variant={usageSummary.status === 'active' ? 'success' : 'error'}>
|
|
{usageSummary.status === 'active' ? 'Activo' : 'Inactivo'}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
<div className="p-4 bg-[var(--bg-secondary)] border border-[var(--border-secondary)] rounded-lg">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm text-[var(--text-secondary)]">Próxima facturación</span>
|
|
<span className="font-medium text-[var(--text-primary)]">{nextBillingDate}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Quick Actions */}
|
|
<Card className="p-6 border border-[var(--border-primary)] bg-[var(--bg-primary)]">
|
|
<h3 className="text-lg font-semibold mb-4 text-[var(--text-primary)]">
|
|
Acciones Rápidas
|
|
</h3>
|
|
<div className="space-y-3">
|
|
<Button variant="outline" className="w-full justify-start text-sm" onClick={() => window.open('https://billing.bakery.com', '_blank')}>
|
|
<ExternalLink className="w-4 h-4 mr-2" />
|
|
Portal de Facturación
|
|
</Button>
|
|
<Button variant="outline" className="w-full justify-start text-sm" onClick={() => console.log('Download invoice')}>
|
|
<Download className="w-4 h-4 mr-2" />
|
|
Descargar Facturas
|
|
</Button>
|
|
<Button variant="outline" className="w-full justify-start text-sm">
|
|
<Settings className="w-4 h-4 mr-2" />
|
|
Configurar Alertas
|
|
</Button>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Usage at a Glance */}
|
|
<Card className="p-6 border border-[var(--border-primary)] bg-[var(--bg-primary)]">
|
|
<h3 className="text-lg font-semibold mb-6 flex items-center text-[var(--text-primary)]">
|
|
<TrendingUp className="w-5 h-5 mr-2 text-orange-500" />
|
|
Uso de Recursos
|
|
</h3>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
{/* Users */}
|
|
<div className="space-y-4 p-4 bg-[var(--bg-secondary)] border border-[var(--border-secondary)] rounded-lg">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-2 bg-blue-500/10 rounded-lg border border-blue-500/20">
|
|
<Users className="w-4 h-4 text-blue-500" />
|
|
</div>
|
|
<span className="font-medium text-[var(--text-primary)]">Usuarios</span>
|
|
</div>
|
|
<span className="text-sm font-bold text-[var(--text-primary)]">
|
|
{usageSummary.usage.users.current}<span className="text-[var(--text-tertiary)]">/{usageSummary.usage.users.unlimited ? '∞' : usageSummary.usage.users.limit}</span>
|
|
</span>
|
|
</div>
|
|
<ProgressBar value={usageSummary.usage.users.usage_percentage} />
|
|
<p className="text-xs text-[var(--text-secondary)] flex items-center justify-between">
|
|
<span>{usageSummary.usage.users.usage_percentage}% utilizado</span>
|
|
<span className="font-medium">{usageSummary.usage.users.unlimited ? 'Ilimitado' : `${usageSummary.usage.users.limit - usageSummary.usage.users.current} restantes`}</span>
|
|
</p>
|
|
</div>
|
|
|
|
{/* Locations */}
|
|
<div className="space-y-4 p-4 bg-[var(--bg-secondary)] border border-[var(--border-secondary)] rounded-lg">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-2 bg-green-500/10 rounded-lg border border-green-500/20">
|
|
<MapPin className="w-4 h-4 text-green-500" />
|
|
</div>
|
|
<span className="font-medium text-[var(--text-primary)]">Ubicaciones</span>
|
|
</div>
|
|
<span className="text-sm font-bold text-[var(--text-primary)]">
|
|
{usageSummary.usage.locations.current}<span className="text-[var(--text-tertiary)]">/{usageSummary.usage.locations.unlimited ? '∞' : usageSummary.usage.locations.limit}</span>
|
|
</span>
|
|
</div>
|
|
<ProgressBar value={usageSummary.usage.locations.usage_percentage} />
|
|
<p className="text-xs text-[var(--text-secondary)] flex items-center justify-between">
|
|
<span>{usageSummary.usage.locations.usage_percentage}% utilizado</span>
|
|
<span className="font-medium">{usageSummary.usage.locations.unlimited ? 'Ilimitado' : `${usageSummary.usage.locations.limit - usageSummary.usage.locations.current} restantes`}</span>
|
|
</p>
|
|
</div>
|
|
|
|
{/* Products */}
|
|
<div className="space-y-4 p-4 bg-[var(--bg-secondary)] border border-[var(--border-secondary)] rounded-lg">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-2 bg-purple-500/10 rounded-lg border border-purple-500/20">
|
|
<Package className="w-4 h-4 text-purple-500" />
|
|
</div>
|
|
<span className="font-medium text-[var(--text-primary)]">Productos</span>
|
|
</div>
|
|
<span className="text-sm font-bold text-[var(--text-primary)]">
|
|
{usageSummary.usage.products.current}<span className="text-[var(--text-tertiary)]">/{usageSummary.usage.products.unlimited ? '∞' : usageSummary.usage.products.limit}</span>
|
|
</span>
|
|
</div>
|
|
<ProgressBar value={usageSummary.usage.products.usage_percentage} />
|
|
<p className="text-xs text-[var(--text-secondary)] flex items-center justify-between">
|
|
<span>{usageSummary.usage.products.usage_percentage}% utilizado</span>
|
|
<span className="font-medium">{usageSummary.usage.products.unlimited ? 'Ilimitado' : 'Ilimitado'}</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</Tabs.Content>
|
|
|
|
<Tabs.Content value="usage">
|
|
<div className="space-y-6">
|
|
<Card className="p-6">
|
|
<h3 className="text-lg font-semibold mb-6 text-[var(--text-primary)]">Detalles de Uso</h3>
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
{/* Users Usage */}
|
|
<div className="space-y-4">
|
|
<div className="flex items-center gap-2">
|
|
<Users className="w-5 h-5 text-blue-500" />
|
|
<h4 className="font-medium text-[var(--text-primary)]">Gestión de Usuarios</h4>
|
|
</div>
|
|
<div className="space-y-3">
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-[var(--text-secondary)]">Usuarios activos</span>
|
|
<span className="font-medium">{usageSummary.usage.users.current}</span>
|
|
</div>
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-[var(--text-secondary)]">Límite del plan</span>
|
|
<span className="font-medium">{usageSummary.usage.users.unlimited ? 'Ilimitado' : usageSummary.usage.users.limit}</span>
|
|
</div>
|
|
<ProgressBar value={usageSummary.usage.users.usage_percentage} />
|
|
<p className="text-xs text-[var(--text-secondary)]">
|
|
{usageSummary.usage.users.usage_percentage}% de capacidad utilizada
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Locations Usage */}
|
|
<div className="space-y-4">
|
|
<div className="flex items-center gap-2">
|
|
<MapPin className="w-5 h-5 text-green-500" />
|
|
<h4 className="font-medium text-[var(--text-primary)]">Ubicaciones</h4>
|
|
</div>
|
|
<div className="space-y-3">
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-[var(--text-secondary)]">Ubicaciones activas</span>
|
|
<span className="font-medium">{usageSummary.usage.locations.current}</span>
|
|
</div>
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-[var(--text-secondary)]">Límite del plan</span>
|
|
<span className="font-medium">{usageSummary.usage.locations.unlimited ? 'Ilimitado' : usageSummary.usage.locations.limit}</span>
|
|
</div>
|
|
<ProgressBar value={usageSummary.usage.locations.usage_percentage} />
|
|
<p className="text-xs text-[var(--text-secondary)]">
|
|
{usageSummary.usage.locations.usage_percentage}% de capacidad utilizada
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Products Usage */}
|
|
<div className="space-y-4">
|
|
<div className="flex items-center gap-2">
|
|
<Package className="w-5 h-5 text-purple-500" />
|
|
<h4 className="font-medium text-[var(--text-primary)]">Productos</h4>
|
|
</div>
|
|
<div className="space-y-3">
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-[var(--text-secondary)]">Productos registrados</span>
|
|
<span className="font-medium">{usageSummary.usage.products.current}</span>
|
|
</div>
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-[var(--text-secondary)]">Límite del plan</span>
|
|
<span className="font-medium">{usageSummary.usage.products.unlimited ? 'Ilimitado' : usageSummary.usage.products.limit}</span>
|
|
</div>
|
|
<ProgressBar value={usageSummary.usage.products.usage_percentage} />
|
|
<p className="text-xs text-[var(--text-secondary)]">
|
|
{usageSummary.usage.products.usage_percentage}% de capacidad utilizada
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</Tabs.Content>
|
|
|
|
<Tabs.Content value="plans">
|
|
<div className="space-y-6">
|
|
<div className="text-center">
|
|
<h3 className="text-xl font-semibold text-[var(--text-primary)] mb-2">
|
|
Planes de Suscripción
|
|
</h3>
|
|
<p className="text-[var(--text-secondary)]">
|
|
Elige el plan que mejor se adapte a las necesidades de tu panadería
|
|
</p>
|
|
</div>
|
|
<PlanComparison
|
|
plans={availablePlans.plans}
|
|
currentPlan={usageSummary.plan}
|
|
onUpgrade={handleUpgradeClick}
|
|
/>
|
|
</div>
|
|
</Tabs.Content>
|
|
|
|
<Tabs.Content value="billing">
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
<Card className="p-6">
|
|
<h3 className="text-lg font-semibold mb-6 text-[var(--text-primary)]">
|
|
Información de Facturación
|
|
</h3>
|
|
<div className="space-y-4">
|
|
<div className="flex justify-between p-3 bg-[var(--bg-secondary)] rounded-lg">
|
|
<span className="text-[var(--text-secondary)]">Plan actual:</span>
|
|
<span className="font-medium">{planInfo.name}</span>
|
|
</div>
|
|
<div className="flex justify-between p-3 bg-[var(--bg-secondary)] rounded-lg">
|
|
<span className="text-[var(--text-secondary)]">Precio mensual:</span>
|
|
<span className="font-medium">{subscriptionService.formatPrice(usageSummary.monthly_price)}</span>
|
|
</div>
|
|
<div className="flex justify-between p-3 bg-[var(--bg-secondary)] rounded-lg">
|
|
<span className="text-[var(--text-secondary)]">Próxima facturación:</span>
|
|
<span className="font-medium flex items-center">
|
|
<Calendar className="w-4 h-4 mr-2" />
|
|
{nextBillingDate}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
<Card className="p-6">
|
|
<h3 className="text-lg font-semibold mb-6 text-[var(--text-primary)]">
|
|
Métodos de Pago
|
|
</h3>
|
|
<div className="space-y-4">
|
|
<div className="flex items-center gap-4 p-4 border border-[var(--border-primary)] rounded-lg">
|
|
<CreditCard className="w-8 h-8 text-[var(--text-tertiary)]" />
|
|
<div className="flex-1">
|
|
<div className="font-medium">•••• •••• •••• 4242</div>
|
|
<div className="text-sm text-[var(--text-secondary)]">Visa terminada en 4242</div>
|
|
</div>
|
|
<Badge variant="success">Principal</Badge>
|
|
</div>
|
|
<Button variant="outline" className="w-full">
|
|
<Settings className="w-4 h-4 mr-2" />
|
|
Gestionar Métodos de Pago
|
|
</Button>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</Tabs.Content>
|
|
</Tabs>
|
|
</Card>
|
|
|
|
{/* Upgrade Modal */}
|
|
{upgradeDialogOpen && selectedPlan && availablePlans && (
|
|
<Modal
|
|
isOpen={upgradeDialogOpen}
|
|
onClose={() => setUpgradeDialogOpen(false)}
|
|
title="Confirmar Cambio de Plan"
|
|
>
|
|
<div className="space-y-4">
|
|
<p className="text-[var(--text-secondary)]">
|
|
¿Estás seguro de que quieres cambiar tu plan de suscripción?
|
|
</p>
|
|
{availablePlans.plans[selectedPlan] && (
|
|
<div className="p-4 bg-[var(--bg-secondary)] rounded-lg space-y-2">
|
|
<div className="flex justify-between">
|
|
<span>Plan actual:</span>
|
|
<span>{planInfo.name}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span>Nuevo plan:</span>
|
|
<span>{availablePlans.plans[selectedPlan].name}</span>
|
|
</div>
|
|
<div className="flex justify-between font-medium">
|
|
<span>Nuevo precio:</span>
|
|
<span>{subscriptionService.formatPrice(availablePlans.plans[selectedPlan].monthly_price)}/mes</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex gap-2 pt-4">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setUpgradeDialogOpen(false)}
|
|
className="flex-1"
|
|
>
|
|
Cancelar
|
|
</Button>
|
|
<Button
|
|
variant="primary"
|
|
onClick={handleUpgradeConfirm}
|
|
disabled={upgrading}
|
|
className="flex-1"
|
|
>
|
|
{upgrading ? 'Procesando...' : 'Confirmar Cambio'}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default SubscriptionPage; |