242 lines
9.8 KiB
TypeScript
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>
|
|
);
|
|
}
|