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