Primer commit — OPSV Dashboard de siniestralidad vial
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
import { useState } from 'react'
|
||||
import { FileDown, Loader2, X } from 'lucide-react'
|
||||
import { exportarPDF, SECCIONES_EXPORTABLES } from '../../utils/exportPdf'
|
||||
|
||||
export default function PdfExportModal({ year, onClose }) {
|
||||
const [seleccionadas, setSeleccionadas] = useState(
|
||||
SECCIONES_EXPORTABLES.map(s => s.id)
|
||||
)
|
||||
const [generando, setGenerando] = useState(false)
|
||||
const [progreso, setProgreso] = useState(0)
|
||||
const [error, setError] = useState(null)
|
||||
|
||||
function toggleSeccion(id) {
|
||||
setSeleccionadas(prev =>
|
||||
prev.includes(id) ? prev.filter(s => s !== id) : [...prev, id]
|
||||
)
|
||||
}
|
||||
|
||||
function toggleTodas() {
|
||||
setSeleccionadas(prev =>
|
||||
prev.length === SECCIONES_EXPORTABLES.length
|
||||
? []
|
||||
: SECCIONES_EXPORTABLES.map(s => s.id)
|
||||
)
|
||||
}
|
||||
|
||||
async function handleExportar() {
|
||||
if (!seleccionadas.length) return
|
||||
setGenerando(true)
|
||||
setProgreso(0)
|
||||
setError(null)
|
||||
try {
|
||||
await exportarPDF({
|
||||
seccionesIds: SECCIONES_EXPORTABLES
|
||||
.map(s => s.id)
|
||||
.filter(id => seleccionadas.includes(id)),
|
||||
year,
|
||||
onProgress: setProgreso,
|
||||
})
|
||||
onClose()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
setError('Ocurrió un error al generar el PDF. Intentá de nuevo.')
|
||||
} finally {
|
||||
setGenerando(false)
|
||||
}
|
||||
}
|
||||
|
||||
const todasMarcadas = seleccionadas.length === SECCIONES_EXPORTABLES.length
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 bg-black/40 backdrop-blur-sm"
|
||||
onClick={!generando ? onClose : undefined}
|
||||
/>
|
||||
|
||||
{/* Modal */}
|
||||
<div className="relative w-full max-w-md rounded-3xl border border-opsv-border dark:border-slate-700 bg-white dark:bg-slate-900 p-6 shadow-2xl">
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between mb-5">
|
||||
<div>
|
||||
<h2 className="text-lg font-black text-opsv-navy dark:text-white">
|
||||
Exportar a PDF
|
||||
</h2>
|
||||
<p className="mt-1 text-sm text-opsv-muted dark:text-slate-400">
|
||||
Seleccioná las secciones a incluir en el informe {year}.
|
||||
</p>
|
||||
</div>
|
||||
{!generando && (
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="rounded-full p-1.5 hover:bg-opsv-bg dark:hover:bg-slate-800 transition"
|
||||
aria-label="Cerrar"
|
||||
>
|
||||
<X className="h-4 w-4 text-opsv-muted dark:text-slate-400" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Seleccionar todas */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleTodas}
|
||||
disabled={generando}
|
||||
className="mb-3 text-xs font-semibold text-opsv-blue dark:text-sky-400 hover:underline disabled:opacity-50"
|
||||
>
|
||||
{todasMarcadas ? 'Deseleccionar todas' : 'Seleccionar todas'}
|
||||
</button>
|
||||
|
||||
{/* Checkboxes */}
|
||||
<div className="flex flex-col gap-2 mb-5">
|
||||
{SECCIONES_EXPORTABLES.map(sec => (
|
||||
<label
|
||||
key={sec.id}
|
||||
className={`flex items-center gap-3 rounded-2xl border px-4 py-3 cursor-pointer transition
|
||||
${seleccionadas.includes(sec.id)
|
||||
? 'border-opsv-blue bg-opsv-blue/5 dark:border-sky-500 dark:bg-sky-500/10'
|
||||
: 'border-opsv-border dark:border-slate-700 bg-white dark:bg-slate-800 hover:border-opsv-navy/30'
|
||||
}
|
||||
${generando ? 'opacity-50 cursor-not-allowed' : ''}
|
||||
`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={seleccionadas.includes(sec.id)}
|
||||
onChange={() => !generando && toggleSeccion(sec.id)}
|
||||
className="h-4 w-4 rounded accent-opsv-navy dark:accent-sky-400"
|
||||
/>
|
||||
<span className="text-sm font-medium text-opsv-navy dark:text-slate-100">
|
||||
{sec.label}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Barra de progreso */}
|
||||
{generando && (
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
<span className="text-xs text-opsv-muted dark:text-slate-400">
|
||||
Generando PDF...
|
||||
</span>
|
||||
<span className="text-xs font-semibold text-opsv-navy dark:text-white">
|
||||
{progreso}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-2 w-full rounded-full bg-opsv-border dark:bg-slate-700 overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full bg-opsv-navy dark:bg-sky-500 transition-all duration-300"
|
||||
style={{ width: `${progreso}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div className="mb-4 rounded-2xl border border-red-200 bg-red-50 dark:border-red-900/40 dark:bg-red-950/30 px-3 py-2 text-xs text-red-700 dark:text-red-300">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Botones */}
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
disabled={generando}
|
||||
className="rounded-2xl border border-opsv-border dark:border-slate-700 bg-white dark:bg-slate-800 px-4 py-2.5 text-sm font-medium text-opsv-muted dark:text-slate-300 hover:text-opsv-navy dark:hover:text-white transition disabled:opacity-50"
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleExportar}
|
||||
disabled={generando || seleccionadas.length === 0}
|
||||
className="flex items-center gap-2 rounded-2xl bg-opsv-navy dark:bg-slate-700 px-5 py-2.5 text-sm font-semibold text-white hover:bg-opsv-navy-dark dark:hover:bg-slate-600 transition disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{generando
|
||||
? <><Loader2 className="h-4 w-4 animate-spin" /> Generando...</>
|
||||
: <><FileDown className="h-4 w-4" /> Descargar PDF</>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user