Files
HoruxDespachosNuevo/apps/web/app/(dashboard)/alertas/page.tsx

242 lines
9.8 KiB
TypeScript

'use client';
import { useState } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/navigation';
import { DashboardShell } from '@/components/layouts/dashboard-shell';
import { Card, CardContent, CardHeader, CardTitle, Button } from '@horux/shared-ui';
import { useAlertas, useAlertasStats, useUpdateAlerta, useDeleteAlerta, useMarkAllAsRead } from '@/lib/hooks/use-alertas';
import { apiClient } from '@/lib/api/client';
import { Bell, Check, Trash2, AlertTriangle, Info, AlertCircle, CheckCircle, ShieldAlert, ChevronRight, Clock } from 'lucide-react';
import { cn } from '@horux/shared-ui';
import { useContribuyenteStore } from '@/stores/contribuyente-store';
interface AlertaAuto {
id: string;
tipo: string;
titulo: string;
mensaje: string;
prioridad: 'alta' | 'media' | 'baja';
detalle?: string;
valor?: number;
}
const prioridadStyles = {
alta: 'border-l-4 border-l-destructive bg-destructive/5',
media: 'border-l-4 border-l-warning bg-warning/5',
baja: 'border-l-4 border-l-muted bg-muted/5',
};
const prioridadIcons = {
alta: AlertCircle,
media: AlertTriangle,
baja: Info,
};
export default function AlertasPage() {
const [filter, setFilter] = useState<'todas' | 'pendientes' | 'resueltas'>('pendientes');
const { data: alertas, isLoading } = useAlertas({
resuelta: filter === 'resueltas' ? true : filter === 'pendientes' ? false : undefined,
});
const { data: stats } = useAlertasStats();
const updateAlerta = useUpdateAlerta();
const deleteAlerta = useDeleteAlerta();
const markAllAsRead = useMarkAllAsRead();
const router = useRouter();
const { selectedContribuyenteId } = useContribuyenteStore();
const queryClient = useQueryClient();
const { data: alertasAuto } = useQuery({
queryKey: ['alertas-automaticas', selectedContribuyenteId],
queryFn: async () => {
const params = new URLSearchParams();
if (selectedContribuyenteId) params.set('contribuyenteId', selectedContribuyenteId);
const res = await apiClient.get<AlertaAuto[]>(`/alertas/automaticas?${params}`);
return res.data;
},
});
const { data: alertasManuales } = useQuery({
queryKey: ['alertas-manuales', selectedContribuyenteId],
queryFn: async () => {
const params = new URLSearchParams();
if (selectedContribuyenteId) params.set('contribuyenteId', selectedContribuyenteId);
const res = await apiClient.get<any[]>(`/alertas/manuales?${params}`);
return res.data;
},
});
const handleResolver = async (id: string) => {
await apiClient.patch(`/alertas/manuales/${id}/resolver`);
queryClient.invalidateQueries({ queryKey: ['alertas-manuales'] });
};
const handleMarkAsRead = (id: number) => {
updateAlerta.mutate({ id, data: { leida: true } });
};
const handleResolve = (id: number) => {
updateAlerta.mutate({ id, data: { resuelta: true } });
};
const handleDelete = (id: number) => {
if (confirm('¿Eliminar esta alerta?')) {
deleteAlerta.mutate(id);
}
};
return (
<DashboardShell title="Alertas">
<div className="space-y-4">
{/* Stats */}
<div className="grid gap-4 md:grid-cols-3">
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium">Alertas del Sistema</CardTitle>
<ShieldAlert className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{alertasAuto?.length || 0}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium">Obligaciones Pendientes</CardTitle>
<Clock className="h-4 w-4 text-warning" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-warning">{alertasManuales?.length || 0}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium">Total Alertas</CardTitle>
<Bell className="h-4 w-4 text-destructive" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-destructive">
{(alertasAuto?.length || 0) + (alertasManuales?.length || 0)}
</div>
</CardContent>
</Card>
</div>
{/* Alertas Automáticas */}
{alertasAuto && alertasAuto.length > 0 && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<ShieldAlert className="h-4 w-4" />
Alertas del Sistema ({alertasAuto.length})
</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
{alertasAuto.map((alerta) => {
const Icon = alerta.prioridad === 'alta' ? AlertCircle : AlertTriangle;
return (
<div
key={alerta.id}
className={cn(
'p-3 rounded-lg border',
alerta.prioridad === 'alta' && 'border-l-4 border-l-destructive bg-destructive/5',
alerta.prioridad === 'media' && 'border-l-4 border-l-warning bg-warning/5',
alerta.prioridad === 'baja' && 'border-l-4 border-l-muted bg-muted/5',
)}
>
<div className="flex items-start gap-3">
<Icon className={cn(
'h-5 w-5 mt-0.5 flex-shrink-0',
alerta.prioridad === 'alta' && 'text-destructive',
alerta.prioridad === 'media' && 'text-warning',
)} />
<div className="flex-1 min-w-0">
<h4 className="font-medium text-sm">{alerta.titulo}</h4>
<p className="text-xs text-muted-foreground mt-1">{alerta.mensaje}</p>
</div>
</div>
{alerta.detalle && (
<div className="mt-2 flex justify-end">
<Button
variant="outline"
size="sm"
onClick={() => router.push(alerta.detalle!)}
>
Ver detalle <ChevronRight className="h-3 w-3 ml-1" />
</Button>
</div>
)}
</div>
);
})}
</CardContent>
</Card>
)}
{/* Obligaciones Fiscales Pendientes */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<Clock className="h-4 w-4" />
Obligaciones Fiscales Pendientes ({alertasManuales?.length || 0})
</CardTitle>
</CardHeader>
<CardContent>
{!alertasManuales || alertasManuales.length === 0 ? (
<div className="py-6 text-center text-muted-foreground">
<CheckCircle className="h-10 w-10 mx-auto mb-3 text-success" />
<p className="text-sm">Todas las obligaciones fiscales estan al dia</p>
</div>
) : (
<div className="space-y-2">
{alertasManuales.map((alerta: any) => {
const esPago = alerta.tipo.startsWith('pago-');
const Icon = prioridadIcons[alerta.prioridad as keyof typeof prioridadIcons] || AlertTriangle;
return (
<div
key={alerta.id}
className={cn(
'p-3 rounded-lg border',
alerta.prioridad === 'alta' && 'border-l-4 border-l-destructive bg-destructive/5',
alerta.prioridad === 'media' && 'border-l-4 border-l-warning bg-warning/5',
)}
>
<div className="flex items-start gap-3">
<Icon className={cn(
'h-5 w-5 mt-0.5 flex-shrink-0',
alerta.prioridad === 'alta' && 'text-destructive',
alerta.prioridad === 'media' && 'text-warning',
)} />
<div className="flex-1 min-w-0">
<h4 className="font-medium text-sm">{alerta.titulo}</h4>
<p className="text-xs text-muted-foreground mt-1">{alerta.mensaje}</p>
</div>
</div>
<div className="mt-2 flex items-center justify-between">
<span className="text-xs text-muted-foreground">
Vencio: {(() => {
const d = new Date(alerta.fechaVencimiento);
return isNaN(d.getTime()) ? '' : d.toLocaleDateString('es-MX', { day: 'numeric', month: 'short', year: 'numeric' });
})()}
</span>
<Button
variant="outline"
size="sm"
onClick={() => handleResolver(alerta.id)}
>
<Check className="h-3 w-3 mr-1" />
{esPago ? 'Marcar como pagado' : 'Marcar como presentada'}
</Button>
</div>
</div>
);
})}
</div>
)}
</CardContent>
</Card>
</div>
</DashboardShell>
);
}