Files
HoruxDespachosNuevo/apps/web/app/(dashboard)/configuracion/planes-despacho/page.tsx

484 lines
26 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useEffect, useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle, Button } from '@horux/shared-ui';
import { CheckCircle2, Server, Cloud, Clock, ExternalLink, CreditCard } from 'lucide-react';
import { apiClient } from '@/lib/api/client';
import { subscribeMe, changeMyPlan, cancelMySubscription, upgradeMe, generatePaymentLink } from '@/lib/api/subscription';
import { useAuthStore } from '@/stores/auth-store';
type Despachoplan = 'trial' | 'business_control' | 'business_cloud' | 'mi_empresa' | 'mi_empresa_plus' | 'custom';
type PaidPlan = 'business_control' | 'business_cloud' | 'mi_empresa' | 'mi_empresa_plus';
interface SubscriptionInfo {
status: string;
plan: string;
amount: number;
currentPeriodStart: string | null;
currentPeriodEnd: string | null;
}
interface PlanInfo {
plan: Despachoplan;
dbMode: string;
trialEndsAt: string | null;
isTrialActive: boolean;
subscription: SubscriptionInfo | null;
}
function daysUntil(isoDate: string): number {
const diff = new Date(isoDate).getTime() - Date.now();
return Math.max(0, Math.ceil(diff / (1000 * 60 * 60 * 24)));
}
type Frequency = 'monthly' | 'annual';
export default function PlanesDespachoPage() {
const { user } = useAuthStore();
const [planInfo, setPlanInfo] = useState<PlanInfo | null>(null);
const [loading, setLoading] = useState(true);
const [busy, setBusy] = useState<null | PaidPlan | 'cancel' | 'pay-now'>(null);
const [message, setMessage] = useState<{ kind: 'ok' | 'err'; text: string } | null>(null);
// Toggle mensual/anual solo aplica a Mi Empresa y Mi Empresa+. Business
// Control y Enterprise siempre se cobran anual. Default monthly para
// bajar friction inicial; el descuento del 17% al pagar anual se
// muestra como CTA secundario.
const [meFreq, setMeFreq] = useState<Frequency>('monthly');
const [mePlusFreq, setMePlusFreq] = useState<Frequency>('monthly');
const fetchPlan = () => {
apiClient.get<PlanInfo>('/despachos/me/plan')
.then(res => setPlanInfo(res.data))
.catch(() => setPlanInfo(null));
};
useEffect(() => {
setLoading(true);
apiClient.get<PlanInfo>('/despachos/me/plan')
.then(res => setPlanInfo(res.data))
.catch(() => setPlanInfo(null))
.finally(() => setLoading(false));
}, []);
const currentPlan = planInfo?.plan ?? null;
const trialDaysLeft = planInfo?.trialEndsAt ? daysUntil(planInfo.trialEndsAt) : 0;
const hasPaidPlan = currentPlan === 'business_control' || currentPlan === 'business_cloud' || currentPlan === 'mi_empresa' || currentPlan === 'mi_empresa_plus';
const isCustomPlan = currentPlan === 'custom';
// El usuario puede cancelar si tiene una suscripción que aún corre (paid, trial,
// custom). Si ya está cancelada o expirada, no hay nada que cancelar.
const subStatus = planInfo?.subscription?.status ?? null;
const hasActiveSub = subStatus != null
&& subStatus !== 'cancelled'
&& subStatus !== 'trial_expired';
/** Resuelve la frecuencia para un plan. Mi Empresa y Mi Empresa+ leen su
* propio toggle; el resto (business_*) siempre annual. */
function frequencyFor(plan: PaidPlan): Frequency {
if (plan === 'mi_empresa') return meFreq;
if (plan === 'mi_empresa_plus') return mePlusFreq;
return 'annual';
}
async function handleContratar(plan: PaidPlan) {
const frequency = frequencyFor(plan);
setBusy(plan);
setMessage(null);
try {
// Sin sub activa: subscribe directo → MP (preapproval del plan completo).
const result = await subscribeMe({ plan, frequency });
window.open(result.paymentUrl, '_blank');
setMessage({ kind: 'ok', text: 'Abrimos el pago de MercadoPago en otra pestaña. Al completar regresa aquí.' });
} catch (err: any) {
const msg: string = err?.response?.data?.message || err?.message || '';
if (!/Ya existe una suscripci/i.test(msg)) {
setMessage({ kind: 'err', text: msg || 'Error al contratar el plan' });
setBusy(null);
return;
}
// Hay sub activa en otro plan. Intentamos upgrade (prorrateado, MP) primero
// — si el backend determina que es downgrade o misma frecuencia más barata,
// rechaza y caemos a cambio programado para fin de período.
try {
const upgradeResult = await upgradeMe(plan);
window.open(upgradeResult.checkoutUrl, '_blank');
const monto = Number(upgradeResult.proratedAmount).toLocaleString('es-MX', { minimumFractionDigits: 2 });
setMessage({ kind: 'ok', text: `Upgrade a ${plan} — abrimos el cobro prorrateado de $${monto} en MercadoPago.` });
} catch (upErr: any) {
try {
await changeMyPlan({ plan, frequency });
setMessage({ kind: 'ok', text: 'Cambio de plan programado para el final del período actual (sin cobro inmediato).' });
fetchPlan();
} catch (changeErr: any) {
setMessage({ kind: 'err', text: changeErr?.response?.data?.message || changeErr?.message || 'Error al cambiar el plan' });
}
}
} finally {
setBusy(null);
}
}
/**
* Genera un link de pago one-off en MercadoPago para el monto vigente de la
* suscripción actual. Útil cuando: (a) el usuario quiere pagar el período
* actual antes de que venza, (b) la sub está en `pending` y nunca se ejecutó
* el primer cobro, (c) custom plans con monto manual.
*/
async function handlePagarAhora() {
if (!user?.tenantId) return;
setBusy('pay-now');
setMessage(null);
try {
const result = await generatePaymentLink(user.tenantId);
window.open(result.paymentUrl, '_blank');
setMessage({ kind: 'ok', text: 'Abrimos el link de pago de MercadoPago en otra pestaña. Al completar regresa aquí.' });
} catch (err: any) {
setMessage({ kind: 'err', text: err?.response?.data?.message || err?.message || 'Error al generar el link de pago' });
} finally {
setBusy(null);
}
}
async function handleCancelar() {
if (!confirm('Seguro que quieres cancelar la suscripcion? Conservaras acceso hasta el final del periodo pagado.')) return;
setBusy('cancel');
setMessage(null);
try {
await cancelMySubscription();
setMessage({ kind: 'ok', text: 'Suscripcion cancelada. Acceso activo hasta el final del periodo actual.' });
fetchPlan();
} catch (err: any) {
setMessage({ kind: 'err', text: err?.response?.data?.message || err?.message || 'Error al cancelar' });
} finally {
setBusy(null);
}
}
function ActiveBadge() {
return (
<div className="absolute -top-3 left-1/2 -translate-x-1/2 bg-green-600 text-white text-xs px-3 py-1 rounded-full font-medium whitespace-nowrap">
Plan actual
</div>
);
}
/**
* Toggle binario Mensual/Anual. La opción anual va resaltada con un
* pequeño badge "17%" para enfocar el descuento.
*/
function FrequencyToggle({ value, onChange }: { value: Frequency; onChange: (v: Frequency) => void }) {
return (
<div className="flex bg-muted rounded-lg p-1 text-xs font-medium">
<button
type="button"
onClick={() => onChange('monthly')}
className={`flex-1 py-1.5 rounded-md transition-colors ${value === 'monthly' ? 'bg-background shadow-sm text-foreground' : 'text-muted-foreground hover:text-foreground'}`}
>
Mensual
</button>
<button
type="button"
onClick={() => onChange('annual')}
className={`flex-1 py-1.5 rounded-md transition-colors flex items-center justify-center gap-1 ${value === 'annual' ? 'bg-background shadow-sm text-foreground' : 'text-muted-foreground hover:text-foreground'}`}
>
Anual <span className="text-emerald-600 dark:text-emerald-400 text-[10px] font-bold">17%</span>
</button>
</div>
);
}
function PlanActionButton({ plan }: { plan: PaidPlan }) {
const isCurrent = currentPlan === plan;
if (isCurrent) {
return <Button disabled className="w-full">Plan actual</Button>;
}
const label = hasActiveSub ? 'Cambiar a este plan' : 'Contratar';
return (
<Button
className="w-full"
onClick={() => handleContratar(plan)}
disabled={busy === plan}
>
{busy === plan ? 'Procesando...' : (
<>
<ExternalLink className="h-4 w-4 mr-2" />
{label}
</>
)}
</Button>
);
}
return (
<div className="p-6 max-w-5xl mx-auto space-y-8">
<div className="text-center space-y-2">
<h1 className="text-2xl font-bold">Planes Horux Despachos</h1>
<p className="text-muted-foreground">Tres planes: Mi Empresa para usuarios individuales, Business Control y Enterprise para despachos.</p>
</div>
{/* Banner Custom — plan asignado por admin, sin cobro */}
{!loading && isCustomPlan && (
<div className="flex items-start gap-3 bg-pink-50 dark:bg-pink-950 border border-pink-200 dark:border-pink-800 rounded-lg px-4 py-3 max-w-3xl mx-auto">
<CheckCircle2 className="h-5 w-5 text-pink-600 dark:text-pink-400 flex-shrink-0 mt-0.5" />
<div className="text-sm space-y-0.5">
<div className="font-semibold text-pink-800 dark:text-pink-300">
Plan Custom sin cobro, vigencia indefinida
</div>
<div className="text-pink-700 dark:text-pink-400">
Tu cuenta está bajo un plan especial asignado por tu administrador.
Contacta a soporte si necesitas cambiar de plan.
</div>
</div>
</div>
)}
{/* Trial banner */}
{!loading && planInfo?.isTrialActive && (
<div className="flex items-center gap-3 bg-amber-50 dark:bg-amber-950 border border-amber-200 dark:border-amber-800 rounded-lg px-4 py-3 max-w-3xl mx-auto">
<Clock className="h-5 w-5 text-amber-600 dark:text-amber-400 flex-shrink-0" />
<div className="text-sm">
<span className="font-semibold text-amber-800 dark:text-amber-300">Periodo de prueba activo</span>
<span className="text-amber-700 dark:text-amber-400"> {trialDaysLeft} {trialDaysLeft === 1 ? 'dia restante' : 'dias restantes'}</span>
</div>
</div>
)}
{/* Banner de suscripción activa */}
{!loading && planInfo?.subscription && hasPaidPlan && (() => {
const sub = planInfo.subscription;
const periodEndDate = sub.currentPeriodEnd ? new Date(sub.currentPeriodEnd) : null;
const fechaFormato = periodEndDate
? periodEndDate.toLocaleDateString('es-MX', { year: 'numeric', month: 'long', day: 'numeric' })
: null;
const montoFmt = sub.amount.toLocaleString('es-MX');
return (
<div className="flex items-start gap-3 bg-green-50 dark:bg-green-950 border border-green-200 dark:border-green-800 rounded-lg px-4 py-3 max-w-3xl mx-auto">
<CheckCircle2 className="h-5 w-5 text-green-600 dark:text-green-400 flex-shrink-0 mt-0.5" />
<div className="text-sm space-y-0.5">
<div className="font-semibold text-green-800 dark:text-green-300">
Suscripcion activa {
sub.plan === 'business_control' ? 'Business Control'
: sub.plan === 'business_cloud' ? 'Enterprise'
: sub.plan === 'mi_empresa_plus' ? 'Mi Empresa +'
: 'Mi Empresa'
}
</div>
<div className="text-green-700 dark:text-green-400">
Proxima renovacion{fechaFormato ? ` el ${fechaFormato}` : ''}: <strong>${montoFmt}/año</strong>
</div>
</div>
</div>
);
})()}
{/* Botón "Pagar mi período actual" — visible cuando la sub corre y hay
un monto > 0 que cobrar. Crea una MP Preference one-off por el monto
actual (custom $10, paid plan, lo que sea). Útil para pre-pagar antes
del cobro automático o cuando no hay preapproval recurrente activo. */}
{!loading && hasActiveSub && planInfo?.subscription && Number(planInfo.subscription.amount) > 0 && (() => {
const sub = planInfo.subscription!;
const periodEnd = sub.currentPeriodEnd ? new Date(sub.currentPeriodEnd) : null;
const fechaFmt = periodEnd
? periodEnd.toLocaleDateString('es-MX', { year: 'numeric', month: 'long', day: 'numeric' })
: null;
const dias = periodEnd ? Math.max(0, Math.ceil((periodEnd.getTime() - Date.now()) / (1000 * 60 * 60 * 24))) : null;
const montoFmt = Number(sub.amount).toLocaleString('es-MX', { minimumFractionDigits: 0, maximumFractionDigits: 2 });
return (
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-4 bg-blue-50 dark:bg-blue-950 border border-blue-200 dark:border-blue-800 rounded-lg px-5 py-4 max-w-3xl mx-auto">
<CreditCard className="h-6 w-6 text-blue-600 dark:text-blue-400 flex-shrink-0" />
<div className="flex-1 text-sm">
<div className="font-semibold text-blue-900 dark:text-blue-200">
Pagar mi período actual ${montoFmt}
</div>
<div className="text-blue-700 dark:text-blue-400">
{dias != null && dias > 0
? `Tu período termina ${dias === 1 ? 'mañana' : `en ${dias} días`}${fechaFmt ? ` (${fechaFmt})` : ''}.`
: fechaFmt
? `Tu período terminó el ${fechaFmt}.`
: 'Renueva tu suscripción.'}
</div>
</div>
<Button
onClick={handlePagarAhora}
disabled={busy === 'pay-now'}
className="w-full sm:w-auto"
>
{busy === 'pay-now' ? 'Generando link…' : (
<>
<ExternalLink className="h-4 w-4 mr-2" />
Pagar ahora
</>
)}
</Button>
</div>
);
})()}
{/* Toast de resultado */}
{message && (
<div
className={`max-w-3xl mx-auto rounded-lg px-4 py-3 text-sm ${
message.kind === 'ok'
? 'bg-green-50 dark:bg-green-950 border border-green-200 dark:border-green-800 text-green-800 dark:text-green-300'
: 'bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-800 text-red-800 dark:text-red-300'
}`}
>
{message.text}
</div>
)}
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6 max-w-7xl mx-auto">
{/* Mi Empresa */}
<Card className={`relative flex flex-col${currentPlan === 'mi_empresa' ? ' ring-2 ring-green-500' : ''}`}>
{currentPlan === 'mi_empresa' && <ActiveBadge />}
<CardHeader className="text-center pb-2">
<div className="mx-auto bg-emerald-100 dark:bg-emerald-900 rounded-full p-3 w-fit mb-2">
<Cloud className="h-6 w-6 text-emerald-600 dark:text-emerald-400" />
</div>
<CardTitle className="text-xl">Mi Empresa</CardTitle>
<p className="text-sm text-muted-foreground">Para una sola empresa</p>
</CardHeader>
<CardContent className="flex flex-col flex-1 gap-4">
<FrequencyToggle value={meFreq} onChange={setMeFreq} />
<div className="text-center">
<div className="text-3xl font-bold">${meFreq === 'monthly' ? '580' : '5,800'}</div>
<p className="text-sm text-muted-foreground">{meFreq === 'monthly' ? 'por mes (IVA incluido)' : 'por año (IVA incluido)'}</p>
{meFreq === 'monthly' ? (
<p className="text-xs text-muted-foreground mt-1">o $5,800/año (ahorras 17%)</p>
) : (
<p className="text-xs text-emerald-600 dark:text-emerald-400 mt-1 font-medium">Pagas 10 meses en lugar de 12</p>
)}
</div>
<div className="space-y-2 text-sm">
<div className="flex items-center gap-2"><CheckCircle2 className="h-4 w-4 text-green-500 flex-shrink-0" /><span>1 RFC</span></div>
<div className="flex items-center gap-2"><CheckCircle2 className="h-4 w-4 text-green-500 flex-shrink-0" /><span>3 usuarios</span></div>
<div className="flex items-center gap-2"><CheckCircle2 className="h-4 w-4 text-green-500 flex-shrink-0" /><span>Hasta 1,000,000 CFDIs</span></div>
<div className="flex items-center gap-2"><CheckCircle2 className="h-4 w-4 text-green-500 flex-shrink-0" /><span>Base de datos en la nube</span></div>
<div className="flex items-center gap-2"><CheckCircle2 className="h-4 w-4 text-green-500 flex-shrink-0" /><span>Dashboard, CFDI, IVA/ISR, alertas, calendario</span></div>
<div className="flex items-center gap-2"><CheckCircle2 className="h-4 w-4 text-green-500 flex-shrink-0" /><span>Reportes, conciliación, documentos, facturación</span></div>
<div className="flex items-center gap-2"><CheckCircle2 className="h-4 w-4 text-green-500 flex-shrink-0" /><span>50 timbres/mes incluidos</span></div>
</div>
<div className="mt-auto"><PlanActionButton plan="mi_empresa" /></div>
</CardContent>
</Card>
{/* Mi Empresa + */}
<Card className={`relative flex flex-col${currentPlan === 'mi_empresa_plus' ? ' ring-2 ring-green-500' : ''}`}>
{currentPlan === 'mi_empresa_plus' && <ActiveBadge />}
<CardHeader className="text-center pb-2">
<div className="mx-auto bg-teal-100 dark:bg-teal-900 rounded-full p-3 w-fit mb-2">
<Cloud className="h-6 w-6 text-teal-600 dark:text-teal-400" />
</div>
<CardTitle className="text-xl">Mi Empresa +</CardTitle>
<p className="text-sm text-muted-foreground">Mi Empresa con API y Lolita IA</p>
</CardHeader>
<CardContent className="flex flex-col flex-1 gap-4">
<FrequencyToggle value={mePlusFreq} onChange={setMePlusFreq} />
<div className="text-center">
<div className="text-3xl font-bold">${mePlusFreq === 'monthly' ? '900' : '9,000'}</div>
<p className="text-sm text-muted-foreground">{mePlusFreq === 'monthly' ? 'por mes (IVA incluido)' : 'por año (IVA incluido)'}</p>
{mePlusFreq === 'monthly' ? (
<p className="text-xs text-muted-foreground mt-1">o $9,000/año (ahorras 17%)</p>
) : (
<p className="text-xs text-emerald-600 dark:text-emerald-400 mt-1 font-medium">Pagas 10 meses en lugar de 12</p>
)}
</div>
<div className="space-y-2 text-sm">
<div className="flex items-center gap-2"><CheckCircle2 className="h-4 w-4 text-green-500 flex-shrink-0" /><span>1 RFC</span></div>
<div className="flex items-center gap-2"><CheckCircle2 className="h-4 w-4 text-green-500 flex-shrink-0" /><span>3 usuarios</span></div>
<div className="flex items-center gap-2"><CheckCircle2 className="h-4 w-4 text-green-500 flex-shrink-0" /><span>Hasta 1,000,000 CFDIs</span></div>
<div className="flex items-center gap-2"><CheckCircle2 className="h-4 w-4 text-green-500 flex-shrink-0" /><span>Base de datos en la nube</span></div>
<div className="flex items-center gap-2"><CheckCircle2 className="h-4 w-4 text-green-500 flex-shrink-0" /><span>Dashboard, CFDI, IVA/ISR, alertas, calendario</span></div>
<div className="flex items-center gap-2"><CheckCircle2 className="h-4 w-4 text-green-500 flex-shrink-0" /><span>Reportes, conciliación, documentos, facturación</span></div>
<div className="flex items-center gap-2"><CheckCircle2 className="h-4 w-4 text-green-500 flex-shrink-0" /><span>50 timbres/mes incluidos</span></div>
<div className="flex items-center gap-2"><CheckCircle2 className="h-4 w-4 text-green-500 flex-shrink-0" /><span><strong>API REST</strong> incluida</span></div>
<div className="flex items-center gap-2"><CheckCircle2 className="h-4 w-4 text-green-500 flex-shrink-0" /><span><strong>Lolita IA</strong> agente fiscal</span></div>
</div>
<div className="mt-auto"><PlanActionButton plan="mi_empresa_plus" /></div>
</CardContent>
</Card>
{/* Business Control */}
<Card className={`relative flex flex-col${currentPlan === 'business_control' ? ' ring-2 ring-green-500' : ' border-primary ring-2 ring-primary/20'}`}>
{currentPlan === 'business_control'
? <ActiveBadge />
: (
<div className="absolute -top-3 left-1/2 -translate-x-1/2 bg-primary text-primary-foreground text-xs px-3 py-1 rounded-full">
Más popular
</div>
)
}
<CardHeader className="text-center pb-2">
<div className="mx-auto bg-blue-100 dark:bg-blue-900 rounded-full p-3 w-fit mb-2">
<Server className="h-6 w-6 text-blue-600 dark:text-blue-400" />
</div>
<CardTitle className="text-xl">Business Control</CardTitle>
<p className="text-sm text-muted-foreground">Tu servidor, tus datos</p>
</CardHeader>
<CardContent className="flex flex-col flex-1 gap-4">
<div className="text-center">
<div className="text-3xl font-bold">$25,850</div>
<p className="text-sm text-muted-foreground">por año (IVA incluido)</p>
<p className="text-xs text-muted-foreground mt-1">+ $45/mes por cada RFC adicional sobre 100</p>
</div>
<div className="space-y-2 text-sm">
<div className="flex items-center gap-2"><CheckCircle2 className="h-4 w-4 text-green-500 flex-shrink-0" /><span>Hasta 100 RFCs</span></div>
<div className="flex items-center gap-2"><CheckCircle2 className="h-4 w-4 text-green-500 flex-shrink-0" /><span>Usuarios ilimitados</span></div>
<div className="flex items-center gap-2"><CheckCircle2 className="h-4 w-4 text-green-500 flex-shrink-0" /><span>Hasta 1,000,000 CFDIs por contribuyente</span></div>
<div className="flex items-center gap-2"><CheckCircle2 className="h-4 w-4 text-green-500 flex-shrink-0" /><span>Servidor local con backup</span></div>
<div className="flex items-center gap-2"><CheckCircle2 className="h-4 w-4 text-green-500 flex-shrink-0" /><span>Control total de tus datos</span></div>
<div className="flex items-center gap-2"><CheckCircle2 className="h-4 w-4 text-green-500 flex-shrink-0" /><span>Dashboard, CFDI, IVA/ISR, alertas, calendario</span></div>
<div className="flex items-center gap-2"><CheckCircle2 className="h-4 w-4 text-green-500 flex-shrink-0" /><span>Reportes, conciliación, documentos, facturación, API</span></div>
</div>
<div className="mt-auto"><PlanActionButton plan="business_control" /></div>
</CardContent>
</Card>
{/* Enterprise (key interna: business_cloud) */}
<Card className={`relative flex flex-col${currentPlan === 'business_cloud' ? ' ring-2 ring-green-500' : ''}`}>
{currentPlan === 'business_cloud' && <ActiveBadge />}
<CardHeader className="text-center pb-2">
<div className="mx-auto bg-purple-100 dark:bg-purple-900 rounded-full p-3 w-fit mb-2">
<Cloud className="h-6 w-6 text-purple-600 dark:text-purple-400" />
</div>
<CardTitle className="text-xl">Enterprise</CardTitle>
<p className="text-sm text-muted-foreground">Despachos grandes con alto volumen</p>
</CardHeader>
<CardContent className="flex flex-col flex-1 gap-4">
<div className="text-center">
<div className="text-3xl font-bold">$43,000</div>
<p className="text-sm text-muted-foreground">por año (IVA incluido)</p>
<p className="text-xs text-muted-foreground mt-1">+ $45/mes por cada RFC adicional sobre 100</p>
</div>
<div className="space-y-2 text-sm">
<div className="flex items-center gap-2"><CheckCircle2 className="h-4 w-4 text-green-500 flex-shrink-0" /><span>Hasta 100 RFCs</span></div>
<div className="flex items-center gap-2"><CheckCircle2 className="h-4 w-4 text-green-500 flex-shrink-0" /><span>Usuarios ilimitados</span></div>
<div className="flex items-center gap-2"><CheckCircle2 className="h-4 w-4 text-green-500 flex-shrink-0" /><span>Hasta 3,000,000 CFDIs por contribuyente</span></div>
<div className="flex items-center gap-2"><CheckCircle2 className="h-4 w-4 text-green-500 flex-shrink-0" /><span>Servidor local con backup</span></div>
<div className="flex items-center gap-2"><CheckCircle2 className="h-4 w-4 text-green-500 flex-shrink-0" /><span>Backups automáticos en la nube</span></div>
<div className="flex items-center gap-2"><CheckCircle2 className="h-4 w-4 text-green-500 flex-shrink-0" /><span>Dashboard, CFDI, IVA/ISR, alertas, calendario</span></div>
<div className="flex items-center gap-2"><CheckCircle2 className="h-4 w-4 text-green-500 flex-shrink-0" /><span>Reportes, conciliación, documentos, facturación, API</span></div>
<div className="flex items-center gap-2"><CheckCircle2 className="h-4 w-4 text-green-500 flex-shrink-0" /><span>Soporte prioritario</span></div>
</div>
<div className="mt-auto"><PlanActionButton plan="business_cloud" /></div>
</CardContent>
</Card>
</div>
{/* Cancelar — visible para cualquier suscripción aún corriendo (paid, trial, custom).
No se muestra si ya está cancelada o expirada. */}
{hasActiveSub && (
<div className="text-center pt-4">
<button
type="button"
onClick={handleCancelar}
disabled={busy === 'cancel'}
className="text-sm text-muted-foreground hover:text-destructive underline underline-offset-4 disabled:opacity-50"
>
{busy === 'cancel' ? 'Cancelando...' : 'Cancelar suscripción'}
</button>
</div>
)}
</div>
);
}