Files
HoruxDespachosNuevo/apps/web/components/subscription-banner.tsx

156 lines
5.3 KiB
TypeScript

'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 (
<Banner
icon={<AlertTriangle className="h-5 w-5" />}
title="Tu prueba gratuita terminó"
message="Contrata un plan para continuar registrando movimientos."
cta="Elegir plan"
tone="red"
/>
);
}
if (state.isCancelledExpired) {
return (
<Banner
icon={<XCircle className="h-5 w-5" />}
title="Tu suscripción venció"
message="Reactiva o elige un plan para continuar registrando movimientos."
cta="Renovar"
tone="red"
/>
);
}
if (state.needsRenewal) {
return (
<Banner
icon={<XCircle className="h-5 w-5" />}
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 (
<Banner
icon={<AlertCircle className="h-5 w-5" />}
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 (
<Banner
icon={<Sparkles className="h-5 w-5" />}
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 (
<Banner
icon={<Clock className="h-5 w-5" />}
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<BannerProps['tone'], { bg: string; border: string; text: string; button: string }> = {
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 (
<div className={cn('flex items-center gap-3 border-b px-6 py-3', styles.bg, styles.border)}>
<div className={styles.text}>{icon}</div>
<div className="flex-1 min-w-0">
<p className={cn('text-sm font-semibold', styles.text)}>{title}</p>
<p className={cn('text-xs', styles.text, 'opacity-80')}>{message}</p>
</div>
<Link
href="/configuracion/planes-despacho"
className={cn(
'shrink-0 rounded-md px-4 py-2 text-sm font-medium transition-colors',
styles.button,
)}
>
{cta}
</Link>
</div>
);
}