FlotillasGPS - Sistema completo de monitoreo de flotillas GPS
Sistema completo para monitoreo y gestion de flotas de vehiculos con: - Backend FastAPI con PostgreSQL/TimescaleDB - Frontend React con TypeScript y TailwindCSS - App movil React Native con Expo - Soporte para dispositivos GPS, Meshtastic y celulares - Video streaming en vivo con MediaMTX - Geocercas, alertas, viajes y reportes - Autenticacion JWT y WebSockets en tiempo real Documentacion completa y guias de usuario incluidas.
This commit is contained in:
383
frontend/src/pages/Reportes.tsx
Normal file
383
frontend/src/pages/Reportes.tsx
Normal file
@@ -0,0 +1,383 @@
|
||||
import { useState } from 'react'
|
||||
import { useQuery, useMutation } from '@tanstack/react-query'
|
||||
import {
|
||||
DocumentChartBarIcon,
|
||||
ArrowDownTrayIcon,
|
||||
CalendarIcon,
|
||||
ClockIcon,
|
||||
CheckCircleIcon,
|
||||
ExclamationCircleIcon,
|
||||
} from '@heroicons/react/24/outline'
|
||||
import clsx from 'clsx'
|
||||
import { format } from 'date-fns'
|
||||
import { es } from 'date-fns/locale'
|
||||
import { reportesApi, vehiculosApi } from '@/api'
|
||||
import Card, { CardHeader } from '@/components/ui/Card'
|
||||
import Button from '@/components/ui/Button'
|
||||
import Input from '@/components/ui/Input'
|
||||
import Select from '@/components/ui/Select'
|
||||
import Table from '@/components/ui/Table'
|
||||
import Badge from '@/components/ui/Badge'
|
||||
import { useToast } from '@/components/ui/Toast'
|
||||
import { Reporte } from '@/types'
|
||||
|
||||
const tiposReporte = [
|
||||
{
|
||||
id: 'actividad',
|
||||
nombre: 'Reporte de Actividad',
|
||||
descripcion: 'Resumen de actividad de vehiculos incluyendo viajes, distancia y tiempo de operacion',
|
||||
icono: DocumentChartBarIcon,
|
||||
},
|
||||
{
|
||||
id: 'combustible',
|
||||
nombre: 'Reporte de Combustible',
|
||||
descripcion: 'Analisis de consumo de combustible, costos y rendimiento por vehiculo',
|
||||
icono: DocumentChartBarIcon,
|
||||
},
|
||||
{
|
||||
id: 'alertas',
|
||||
nombre: 'Reporte de Alertas',
|
||||
descripcion: 'Historial de alertas generadas, tiempos de respuesta y resolucion',
|
||||
icono: DocumentChartBarIcon,
|
||||
},
|
||||
{
|
||||
id: 'velocidad',
|
||||
nombre: 'Reporte de Velocidad',
|
||||
descripcion: 'Analisis de velocidades, excesos y patrones de conduccion',
|
||||
icono: DocumentChartBarIcon,
|
||||
},
|
||||
{
|
||||
id: 'geocercas',
|
||||
nombre: 'Reporte de Geocercas',
|
||||
descripcion: 'Entradas, salidas y tiempo de permanencia en geocercas',
|
||||
icono: DocumentChartBarIcon,
|
||||
},
|
||||
{
|
||||
id: 'mantenimiento',
|
||||
nombre: 'Reporte de Mantenimiento',
|
||||
descripcion: 'Historial de servicios, costos y programacion de mantenimiento',
|
||||
icono: DocumentChartBarIcon,
|
||||
},
|
||||
]
|
||||
|
||||
export default function Reportes() {
|
||||
const toast = useToast()
|
||||
const [selectedTipo, setSelectedTipo] = useState<string | null>(null)
|
||||
const [params, setParams] = useState({
|
||||
vehiculoId: '',
|
||||
desde: format(new Date(Date.now() - 30 * 24 * 60 * 60 * 1000), 'yyyy-MM-dd'),
|
||||
hasta: format(new Date(), 'yyyy-MM-dd'),
|
||||
formato: 'pdf' as 'pdf' | 'excel' | 'csv',
|
||||
})
|
||||
|
||||
const { data: vehiculos } = useQuery({
|
||||
queryKey: ['vehiculos'],
|
||||
queryFn: () => vehiculosApi.list(),
|
||||
})
|
||||
|
||||
const { data: historial, isLoading: loadingHistorial } = useQuery({
|
||||
queryKey: ['reportes-historial'],
|
||||
queryFn: () => reportesApi.listHistorial({ pageSize: 20 }),
|
||||
})
|
||||
|
||||
const generarMutation = useMutation({
|
||||
mutationFn: () =>
|
||||
reportesApi.generar({
|
||||
tipo: selectedTipo!,
|
||||
vehiculoId: params.vehiculoId || undefined,
|
||||
desde: params.desde,
|
||||
hasta: params.hasta,
|
||||
formato: params.formato,
|
||||
}),
|
||||
onSuccess: (data) => {
|
||||
toast.success('Reporte generado exitosamente')
|
||||
// Download file
|
||||
if (data.url) {
|
||||
window.open(data.url, '_blank')
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
toast.error('Error al generar reporte')
|
||||
},
|
||||
})
|
||||
|
||||
const handleGenerarReporte = () => {
|
||||
if (!selectedTipo) {
|
||||
toast.error('Selecciona un tipo de reporte')
|
||||
return
|
||||
}
|
||||
generarMutation.mutate()
|
||||
}
|
||||
|
||||
const historialReportes = historial?.items || []
|
||||
|
||||
const columns = [
|
||||
{
|
||||
key: 'tipo',
|
||||
header: 'Tipo',
|
||||
render: (r: Reporte) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<DocumentChartBarIcon className="w-5 h-5 text-accent-400" />
|
||||
<span className="text-white capitalize">{r.tipo.replace('_', ' ')}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'periodo',
|
||||
header: 'Periodo',
|
||||
render: (r: Reporte) => (
|
||||
<span className="text-slate-300">
|
||||
{format(new Date(r.desde), 'd MMM', { locale: es })} -{' '}
|
||||
{format(new Date(r.hasta), 'd MMM yyyy', { locale: es })}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'vehiculo',
|
||||
header: 'Vehiculo',
|
||||
render: (r: Reporte) => (
|
||||
<span className="text-slate-300">
|
||||
{r.vehiculo?.nombre || 'Todos los vehiculos'}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'generado',
|
||||
header: 'Generado',
|
||||
render: (r: Reporte) => (
|
||||
<div>
|
||||
<p className="text-slate-300">
|
||||
{format(new Date(r.createdAt), 'd MMM yyyy', { locale: es })}
|
||||
</p>
|
||||
<p className="text-xs text-slate-500">
|
||||
{format(new Date(r.createdAt), 'HH:mm')}
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'estado',
|
||||
header: 'Estado',
|
||||
render: (r: Reporte) => {
|
||||
switch (r.estado) {
|
||||
case 'completado':
|
||||
return <Badge variant="success">Completado</Badge>
|
||||
case 'procesando':
|
||||
return <Badge variant="warning">Procesando</Badge>
|
||||
case 'error':
|
||||
return <Badge variant="error">Error</Badge>
|
||||
default:
|
||||
return <Badge variant="default">{r.estado}</Badge>
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'formato',
|
||||
header: 'Formato',
|
||||
render: (r: Reporte) => (
|
||||
<span className="text-slate-400 uppercase text-sm">{r.formato}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'acciones',
|
||||
header: '',
|
||||
sortable: false,
|
||||
render: (r: Reporte) => (
|
||||
<div className="flex justify-end">
|
||||
{r.estado === 'completado' && r.url && (
|
||||
<Button
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
leftIcon={<ArrowDownTrayIcon className="w-4 h-4" />}
|
||||
onClick={() => window.open(r.url, '_blank')}
|
||||
>
|
||||
Descargar
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white">Generador de Reportes</h1>
|
||||
<p className="text-slate-500 mt-1">
|
||||
Crea reportes personalizados de tu flota
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Report types */}
|
||||
<div className="lg:col-span-2 space-y-4">
|
||||
<h2 className="text-lg font-semibold text-white">Tipos de reporte</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{tiposReporte.map((tipo) => (
|
||||
<Card
|
||||
key={tipo.id}
|
||||
padding="md"
|
||||
className={clsx(
|
||||
'cursor-pointer transition-all',
|
||||
selectedTipo === tipo.id
|
||||
? 'ring-2 ring-accent-500 bg-accent-500/10'
|
||||
: 'hover:bg-slate-800/50'
|
||||
)}
|
||||
onClick={() => setSelectedTipo(tipo.id)}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div
|
||||
className={clsx(
|
||||
'p-2 rounded-lg',
|
||||
selectedTipo === tipo.id
|
||||
? 'bg-accent-500/20'
|
||||
: 'bg-slate-800'
|
||||
)}
|
||||
>
|
||||
<tipo.icono
|
||||
className={clsx(
|
||||
'w-6 h-6',
|
||||
selectedTipo === tipo.id
|
||||
? 'text-accent-400'
|
||||
: 'text-slate-400'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-white font-medium">{tipo.nombre}</h3>
|
||||
<p className="text-sm text-slate-500 mt-1">{tipo.descripcion}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Report configuration */}
|
||||
<div>
|
||||
<Card padding="lg">
|
||||
<CardHeader title="Configuracion" />
|
||||
<div className="space-y-4">
|
||||
<Select
|
||||
label="Vehiculo"
|
||||
value={params.vehiculoId}
|
||||
onChange={(e) => setParams({ ...params, vehiculoId: e.target.value })}
|
||||
options={[
|
||||
{ value: '', label: 'Todos los vehiculos' },
|
||||
...(vehiculos?.items?.map((v) => ({
|
||||
value: v.id,
|
||||
label: v.nombre || v.placa,
|
||||
})) || []),
|
||||
]}
|
||||
/>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-medium text-slate-400">
|
||||
Periodo
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="date"
|
||||
value={params.desde}
|
||||
onChange={(e) => setParams({ ...params, desde: e.target.value })}
|
||||
className={clsx(
|
||||
'flex-1 px-3 py-2 bg-slate-800/50 border border-slate-700/50 rounded-lg',
|
||||
'text-white focus:outline-none focus:ring-2 focus:ring-accent-500'
|
||||
)}
|
||||
/>
|
||||
<span className="text-slate-500">-</span>
|
||||
<input
|
||||
type="date"
|
||||
value={params.hasta}
|
||||
onChange={(e) => setParams({ ...params, hasta: e.target.value })}
|
||||
className={clsx(
|
||||
'flex-1 px-3 py-2 bg-slate-800/50 border border-slate-700/50 rounded-lg',
|
||||
'text-white focus:outline-none focus:ring-2 focus:ring-accent-500'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Select
|
||||
label="Formato"
|
||||
value={params.formato}
|
||||
onChange={(e) =>
|
||||
setParams({ ...params, formato: e.target.value as 'pdf' | 'excel' | 'csv' })
|
||||
}
|
||||
options={[
|
||||
{ value: 'pdf', label: 'PDF' },
|
||||
{ value: 'excel', label: 'Excel' },
|
||||
{ value: 'csv', label: 'CSV' },
|
||||
]}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="w-full"
|
||||
leftIcon={<DocumentChartBarIcon className="w-5 h-5" />}
|
||||
onClick={handleGenerarReporte}
|
||||
isLoading={generarMutation.isPending}
|
||||
disabled={!selectedTipo}
|
||||
>
|
||||
Generar reporte
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Quick stats */}
|
||||
<Card padding="md" className="mt-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<ClockIcon className="w-4 h-4 text-slate-500" />
|
||||
<span className="text-sm text-slate-400">Ultimo generado</span>
|
||||
</div>
|
||||
<span className="text-white text-sm">
|
||||
{historialReportes[0]
|
||||
? format(new Date(historialReportes[0].createdAt), 'd MMM HH:mm', {
|
||||
locale: es,
|
||||
})
|
||||
: '-'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircleIcon className="w-4 h-4 text-success-400" />
|
||||
<span className="text-sm text-slate-400">Exitosos</span>
|
||||
</div>
|
||||
<span className="text-success-400 text-sm">
|
||||
{historialReportes.filter((r) => r.estado === 'completado').length}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<ExclamationCircleIcon className="w-4 h-4 text-error-400" />
|
||||
<span className="text-sm text-slate-400">Con errores</span>
|
||||
</div>
|
||||
<span className="text-error-400 text-sm">
|
||||
{historialReportes.filter((r) => r.estado === 'error').length}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* History */}
|
||||
<Card padding="none">
|
||||
<div className="p-4 border-b border-slate-700/50">
|
||||
<h3 className="font-medium text-white">Historial de reportes</h3>
|
||||
</div>
|
||||
<Table
|
||||
data={historialReportes}
|
||||
columns={columns}
|
||||
keyExtractor={(r) => r.id}
|
||||
isLoading={loadingHistorial}
|
||||
pagination
|
||||
pageSize={10}
|
||||
emptyMessage="No hay reportes generados"
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user