Files
Consultoria AS c3ce7199af feat: bulk XML upload, period selector, and session persistence
- Add bulk XML CFDI upload support (up to 300MB)
- Add period selector component for month/year navigation
- Fix session persistence on page refresh (Zustand hydration)
- Fix income/expense classification based on tenant RFC
- Fix IVA calculation from XML (correct Impuestos element)
- Add error handling to reportes page
- Support multiple CORS origins
- Update reportes service with proper Decimal/BigInt handling
- Add RFC to tenant view store for proper CFDI classification
- Update README with changelog and new features

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 06:51:53 +00:00

206 lines
8.1 KiB
TypeScript

'use client';
import { useState } from 'react';
import { DashboardShell } from '@/components/layouts/dashboard-shell';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { useAlertas, useAlertasStats, useUpdateAlerta, useDeleteAlerta, useMarkAllAsRead } from '@/lib/hooks/use-alertas';
import { Bell, Check, Trash2, AlertTriangle, Info, AlertCircle, CheckCircle } from 'lucide-react';
import { cn } from '@/lib/utils';
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 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-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium">Total</CardTitle>
<Bell className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats?.total || 0}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium">No Leidas</CardTitle>
<AlertCircle className="h-4 w-4 text-destructive" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-destructive">{stats?.noLeidas || 0}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium">Alta Prioridad</CardTitle>
<AlertTriangle className="h-4 w-4 text-warning" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-warning">{stats?.alta || 0}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium">Pendientes</CardTitle>
<Info className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{(stats?.alta || 0) + (stats?.media || 0) + (stats?.baja || 0)}</div>
</CardContent>
</Card>
</div>
{/* Filters */}
<div className="flex gap-2">
<Button
variant={filter === 'todas' ? 'default' : 'outline'}
size="sm"
onClick={() => setFilter('todas')}
>
Todas
</Button>
<Button
variant={filter === 'pendientes' ? 'default' : 'outline'}
size="sm"
onClick={() => setFilter('pendientes')}
>
Pendientes
</Button>
<Button
variant={filter === 'resueltas' ? 'default' : 'outline'}
size="sm"
onClick={() => setFilter('resueltas')}
>
Resueltas
</Button>
<div className="flex-1" />
<Button
variant="outline"
size="sm"
onClick={() => markAllAsRead.mutate()}
disabled={markAllAsRead.isPending}
>
Marcar todas como leidas
</Button>
</div>
{/* Alertas List */}
<div className="space-y-2">
{isLoading ? (
<div className="text-center py-8 text-muted-foreground">Cargando...</div>
) : alertas?.length === 0 ? (
<Card>
<CardContent className="py-8 text-center text-muted-foreground">
<CheckCircle className="h-12 w-12 mx-auto mb-4 text-success" />
<p>No hay alertas {filter === 'pendientes' ? 'pendientes' : ''}</p>
</CardContent>
</Card>
) : (
alertas?.map((alerta) => {
const Icon = prioridadIcons[alerta.prioridad];
return (
<Card key={alerta.id} className={cn(prioridadStyles[alerta.prioridad], alerta.leida && 'opacity-60')}>
<CardContent className="py-4">
<div className="flex items-start gap-4">
<Icon className={cn(
'h-5 w-5 mt-0.5',
alerta.prioridad === 'alta' && 'text-destructive',
alerta.prioridad === 'media' && 'text-warning',
alerta.prioridad === 'baja' && 'text-muted-foreground'
)} />
<div className="flex-1">
<div className="flex items-center gap-2">
<h4 className="font-medium">{alerta.titulo}</h4>
{!alerta.leida && (
<span className="px-2 py-0.5 text-xs bg-primary text-primary-foreground rounded-full">
Nueva
</span>
)}
</div>
<p className="text-sm text-muted-foreground mt-1">{alerta.mensaje}</p>
<div className="flex items-center gap-4 mt-2 text-xs text-muted-foreground">
<span>{new Date(alerta.createdAt).toLocaleDateString('es-MX')}</span>
{alerta.fechaVencimiento && (
<span>Vence: {new Date(alerta.fechaVencimiento).toLocaleDateString('es-MX')}</span>
)}
</div>
</div>
<div className="flex gap-1">
{!alerta.leida && (
<Button
variant="ghost"
size="icon"
onClick={() => handleMarkAsRead(alerta.id)}
title="Marcar como leida"
>
<Check className="h-4 w-4" />
</Button>
)}
{!alerta.resuelta && (
<Button
variant="ghost"
size="icon"
onClick={() => handleResolve(alerta.id)}
title="Marcar como resuelta"
>
<CheckCircle className="h-4 w-4" />
</Button>
)}
<Button
variant="ghost"
size="icon"
onClick={() => handleDelete(alerta.id)}
title="Eliminar"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</CardContent>
</Card>
);
})
)}
</div>
</div>
</DashboardShell>
);
}