This reverts commit d3b326e.
The deployment caused reports of blank screens and 400 errors. Reverting to restore stable state while investigating root cause.
143 lines
5.9 KiB
TypeScript
143 lines
5.9 KiB
TypeScript
'use client';
|
|
|
|
import { Button, Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@horux/shared-ui';
|
|
import { Sparkles, Check, ExternalLink } from 'lucide-react';
|
|
import { useMyAddons, useSubscribeAddon, useCancelAddon } from '@/lib/hooks/use-addons';
|
|
|
|
/**
|
|
* Catálogo de add-ons disponibles **por contribuyente**. No se ofrecen aquí
|
|
* los add-ons tenant-level (maxRfcs, timbres, etc.) — esos viven en la
|
|
* pantalla de suscripción del despacho.
|
|
*/
|
|
const ADDONS_POR_CONTRIBUYENTE: Array<{
|
|
codename: string;
|
|
nombre: string;
|
|
descripcion: string;
|
|
precio: number;
|
|
frecuencia: 'mensual';
|
|
}> = [
|
|
{
|
|
codename: 'lolita_ia_contribuyente',
|
|
nombre: 'Lolita IA',
|
|
descripcion: 'Agente IA fiscal dedicado al RFC. Responde dudas, sugiere optimizaciones, prepara resúmenes.',
|
|
precio: 250,
|
|
frecuencia: 'mensual',
|
|
},
|
|
];
|
|
|
|
export function AddonsDialog({
|
|
target,
|
|
onClose,
|
|
}: {
|
|
target: { id: string; nombre: string } | null;
|
|
onClose: () => void;
|
|
}) {
|
|
const { data, isLoading } = useMyAddons(target?.id);
|
|
const subscribeMut = useSubscribeAddon();
|
|
const cancelMut = useCancelAddon();
|
|
|
|
const handleSubscribe = async (codename: string) => {
|
|
if (!target) return;
|
|
try {
|
|
const result = await subscribeMut.mutateAsync({
|
|
addonCodename: codename,
|
|
contribuyenteId: target.id,
|
|
});
|
|
if (result.paymentUrl) {
|
|
window.open(result.paymentUrl, '_blank');
|
|
}
|
|
} catch (err: any) {
|
|
alert(err.response?.data?.message || err.message || 'Error al contratar add-on');
|
|
}
|
|
};
|
|
|
|
const handleCancel = async (addonId: string, nombre: string) => {
|
|
if (!confirm(`¿Cancelar ${nombre}? Se deja de cobrar al final del período actual.`)) return;
|
|
try {
|
|
await cancelMut.mutateAsync(addonId);
|
|
} catch (err: any) {
|
|
alert(err.response?.data?.message || err.message || 'Error al cancelar');
|
|
}
|
|
};
|
|
|
|
const fmtMoney = (n: number) => n.toLocaleString('es-MX', { style: 'currency', currency: 'MXN', minimumFractionDigits: 0 });
|
|
const fmtDate = (iso: string | null) => iso ? new Date(iso).toLocaleDateString('es-MX', { day: '2-digit', month: 'short', year: 'numeric' }) : '—';
|
|
|
|
const activeAddons = data?.addons ?? [];
|
|
|
|
return (
|
|
<Dialog open={!!target} onOpenChange={(o) => !o && onClose()}>
|
|
<DialogContent className="max-w-2xl">
|
|
<DialogHeader>
|
|
<DialogTitle className="flex items-center gap-2"><Sparkles className="h-5 w-5 text-amber-500" /> Add-ons — {target?.nombre}</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="py-4 space-y-4">
|
|
<p className="text-sm text-muted-foreground">
|
|
Servicios adicionales de cobro mensual que se contratan por contribuyente. El cobro va en un preapproval MercadoPago independiente — se puede cancelar sin afectar la licencia anual del despacho.
|
|
</p>
|
|
|
|
{isLoading ? (
|
|
<p className="text-sm text-muted-foreground">Cargando add-ons activos...</p>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{ADDONS_POR_CONTRIBUYENTE.map((a) => {
|
|
const active = activeAddons.find((x) => x.codename === a.codename);
|
|
const isActive = active && (active.status === 'authorized' || active.status === 'pending');
|
|
return (
|
|
<div key={a.codename} className="border rounded-lg p-4">
|
|
<div className="flex items-start justify-between gap-4">
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2">
|
|
<h3 className="font-semibold">{a.nombre}</h3>
|
|
{isActive && (
|
|
<span className={`text-xs px-2 py-0.5 rounded ${active.status === 'authorized' ? 'bg-green-100 text-green-700' : 'bg-amber-100 text-amber-700'}`}>
|
|
{active.status === 'authorized' ? <><Check className="inline h-3 w-3 mr-1" />Activo</> : 'Pendiente de pago'}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<p className="text-sm text-muted-foreground mt-1">{a.descripcion}</p>
|
|
<p className="text-sm font-medium mt-2">{fmtMoney(a.precio)}<span className="text-muted-foreground font-normal"> / mes</span></p>
|
|
{isActive && active.currentPeriodEnd && (
|
|
<p className="text-xs text-muted-foreground mt-1">Próximo cobro: {fmtDate(active.currentPeriodEnd)}</p>
|
|
)}
|
|
</div>
|
|
<div>
|
|
{isActive ? (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => handleCancel(active.id, a.nombre)}
|
|
disabled={cancelMut.isPending}
|
|
>
|
|
Cancelar
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
size="sm"
|
|
onClick={() => handleSubscribe(a.codename)}
|
|
disabled={subscribeMut.isPending}
|
|
className="flex items-center gap-1"
|
|
>
|
|
Contratar <ExternalLink className="h-3 w-3" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
<p className="text-xs text-muted-foreground border-t pt-3">
|
|
Al contratar abre una pestaña de MercadoPago para autorizar el cobro recurrente. Si no tienes una suscripción activa del despacho, el add-on no podrá crearse.
|
|
</p>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={onClose}>Cerrar</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|