'use client'; import Link from 'next/link'; import { AlertTriangle, AlertCircle, Sparkles, Clock, XCircle } from 'lucide-react'; import { cn } from '@horux/shared-ui'; import { useAuthStore } from '@/stores/auth-store'; import { useSubscription } from '@/lib/hooks/use-subscription'; import { useTenantViewStore } from '@/stores/tenant-view-store'; import { getSubscriptionState, isGlobalAdminRfc } from '@horux/shared'; /** * Banner persistente arriba del dashboard. Visible siempre que la suscripción * está bloqueada (trial vencido, cancelled vencido, sin sub) O cuando faltan * ≤7 días para que se cumpla el período (trial activo o suscripción authorized). * * Se oculta automáticamente cuando el admin global está impersonando un tenant * (la barra de impersonación ya cumple la función de "estás viendo otro tenant"). */ export function SubscriptionBanner() { const { user } = useAuthStore(); const { viewingTenantId } = useTenantViewStore(); const { data: subscription } = useSubscription(user?.tenantId); const isGlobalAdmin = isGlobalAdminRfc( user?.tenantRfc, user?.role || 'visor', user?.platformRoles, ); // Admin global: nunca mostramos su propio estado (su tenant es Horux 360, // siempre activo). Si está impersonando, el banner del tenant impersonado se // omite — la barra de impersonación ya cubre esa señal. if (isGlobalAdmin || viewingTenantId) return null; // Mientras la query carga, no mostramos nada para evitar flicker. if (subscription === undefined) return null; const state = getSubscriptionState(subscription); // Sin estado relevante (active >7 días) no se muestra el banner. if (state.isActive && !state.isWarning) return null; if (state.isPending && !state.isWarning) return null; // Variantes ordenadas por severidad if (state.isTrialExpired) { return ( } title="Tu prueba gratuita terminó" message="Contrata un plan para continuar registrando movimientos." cta="Elegir plan" tone="red" /> ); } if (state.isCancelledExpired) { return ( } title="Tu suscripción venció" message="Reactiva o elige un plan para continuar registrando movimientos." cta="Renovar" tone="red" /> ); } if (state.needsRenewal) { return ( } title="No tienes una suscripción activa" message="Contrata un plan para usar todas las funciones." cta="Ver planes" tone="red" /> ); } if (state.isCancelledInPeriod) { return ( } title="Suscripción cancelada" message={`Seguirás teniendo acceso ${state.daysUntilEnd ?? 0} día${state.daysUntilEnd === 1 ? '' : 's'} más. Reactívala para no perder continuidad.`} cta="Reactivar" tone="orange" /> ); } if (state.isTrial && state.isWarning) { return ( } title={`Tu prueba gratuita termina en ${state.daysUntilEnd} día${state.daysUntilEnd === 1 ? '' : 's'}`} message="Contrata un plan antes para continuar sin interrupciones." cta="Elegir plan" tone="blue" /> ); } if (state.isActive && state.isWarning) { return ( } title={`Tu período termina en ${state.daysUntilEnd} día${state.daysUntilEnd === 1 ? '' : 's'}`} message="Verifica tu método de pago para evitar interrupciones." cta="Ver suscripción" tone="amber" /> ); } return null; } interface BannerProps { icon: React.ReactNode; title: string; message: string; cta: string; tone: 'red' | 'orange' | 'amber' | 'blue'; } const TONE_STYLES: Record = { red: { bg: 'bg-red-50', border: 'border-red-300', text: 'text-red-800', button: 'bg-red-600 hover:bg-red-700 text-white' }, orange: { bg: 'bg-orange-50', border: 'border-orange-300', text: 'text-orange-800', button: 'bg-orange-600 hover:bg-orange-700 text-white' }, amber: { bg: 'bg-amber-50', border: 'border-amber-300', text: 'text-amber-800', button: 'bg-amber-600 hover:bg-amber-700 text-white' }, blue: { bg: 'bg-blue-50', border: 'border-blue-300', text: 'text-blue-800', button: 'bg-blue-600 hover:bg-blue-700 text-white' }, }; function Banner({ icon, title, message, cta, tone }: BannerProps) { const styles = TONE_STYLES[tone]; return (
{icon}

{title}

{message}

{cta}
); }