Initial commit - Horux Despachos NL
This commit is contained in:
155
apps/web/components/subscription-banner.tsx
Normal file
155
apps/web/components/subscription-banner.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user