Modulo de Reportes v1

This commit is contained in:
2025-11-19 23:49:37 -03:00
parent 0917d24029
commit cfe49ee0c8
2 changed files with 534 additions and 4 deletions

View File

@@ -339,10 +339,7 @@ function DashboardPage({ user, setUser }) {
) : activeTab === 'users' ? (
<UsersTab user={user} />
) : activeTab === 'reports' ? (
<div className="text-center py-12">
<div className="text-xl text-gray-400">📊</div>
<div className="text-gray-500 mt-2">Módulo de Reportes en desarrollo...</div>
</div>
<ReportsTab user={user} />
) : null}
</div>
</div>
@@ -1732,6 +1729,38 @@ function InspectionDetailModal({ inspection, user, onClose, onUpdate }) {
{/* Footer */}
<div className="border-t p-4 bg-gray-50">
<div className="flex gap-3">
<button
onClick={async () => {
try {
const token = localStorage.getItem('token')
const API_URL = import.meta.env.VITE_API_URL || ''
const response = await fetch(`${API_URL}/api/inspections/${inspection.id}/pdf`, {
headers: { 'Authorization': `Bearer ${token}` }
})
if (response.ok) {
const blob = await response.blob()
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `inspeccion_${inspection.id}_${inspection.vehicle_plate || 'sin-patente'}.pdf`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
window.URL.revokeObjectURL(url)
} else {
alert('Error al generar PDF')
}
} catch (error) {
console.error('Error:', error)
alert('Error al generar PDF')
}
}}
className="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition flex items-center gap-2"
>
<span>📄</span>
Exportar PDF
</button>
{user?.role === 'admin' && (
<>
<button
@@ -3142,5 +3171,272 @@ function UsersTab({ user }) {
)
}
// Componente de Reportes
function ReportsTab({ user }) {
const [dashboardData, setDashboardData] = useState(null)
const [inspections, setInspections] = useState([])
const [loading, setLoading] = useState(true)
const [filters, setFilters] = useState({
startDate: '',
endDate: '',
mechanicId: ''
})
const loadDashboard = async () => {
try {
const token = localStorage.getItem('token')
const API_URL = import.meta.env.VITE_API_URL || ''
let url = `${API_URL}/api/reports/dashboard?`
if (filters.startDate) url += `start_date=${filters.startDate}&`
if (filters.endDate) url += `end_date=${filters.endDate}&`
if (filters.mechanicId) url += `mechanic_id=${filters.mechanicId}&`
console.log('Loading dashboard from:', url)
const response = await fetch(url, {
headers: { 'Authorization': `Bearer ${token}` }
})
console.log('Dashboard response status:', response.status)
if (response.ok) {
const data = await response.json()
console.log('Dashboard data:', data)
setDashboardData(data)
} else {
const errorText = await response.text()
console.error('Dashboard error response:', response.status, errorText)
}
} catch (error) {
console.error('Error loading dashboard:', error)
}
}
const loadInspections = async () => {
try {
const token = localStorage.getItem('token')
const API_URL = import.meta.env.VITE_API_URL || ''
let url = `${API_URL}/api/reports/inspections?`
if (filters.startDate) url += `start_date=${filters.startDate}&`
if (filters.endDate) url += `end_date=${filters.endDate}&`
if (filters.mechanicId) url += `mechanic_id=${filters.mechanicId}&`
console.log('Loading inspections from:', url)
const response = await fetch(url, {
headers: { 'Authorization': `Bearer ${token}` }
})
console.log('Inspections response status:', response.status)
if (response.ok) {
const data = await response.json()
console.log('Inspections data:', data)
setInspections(data)
} else {
const errorText = await response.text()
console.error('Inspections error response:', response.status, errorText)
}
} catch (error) {
console.error('Error loading inspections:', error)
}
}
useEffect(() => {
const loadData = async () => {
setLoading(true)
await Promise.all([loadDashboard(), loadInspections()])
setLoading(false)
}
loadData()
}, [filters])
if (loading) {
return <div className="text-center py-12">Cargando reportes...</div>
}
if (!dashboardData || !dashboardData.stats) {
return <div className="text-center py-12">No hay datos disponibles</div>
}
return (
<div className="space-y-6">
{/* Filtros */}
<div className="bg-white rounded-lg shadow p-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Fecha Inicio
</label>
<input
type="date"
value={filters.startDate}
onChange={(e) => setFilters({...filters, startDate: e.target.value})}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Fecha Fin
</label>
<input
type="date"
value={filters.endDate}
onChange={(e) => setFilters({...filters, endDate: e.target.value})}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500"
/>
</div>
{user.role === 'admin' && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Mecánico
</label>
<input
type="number"
placeholder="ID del mecánico"
value={filters.mechanicId}
onChange={(e) => setFilters({...filters, mechanicId: e.target.value})}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500"
/>
</div>
)}
</div>
</div>
{/* Métricas principales */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-white rounded-lg shadow p-6">
<div className="text-sm text-gray-500 mb-1">Total Inspecciones</div>
<div className="text-3xl font-bold text-indigo-600">
{dashboardData.stats?.total_inspections || 0}
</div>
</div>
<div className="bg-white rounded-lg shadow p-6">
<div className="text-sm text-gray-500 mb-1">Completadas</div>
<div className="text-3xl font-bold text-green-600">
{dashboardData.stats?.completed_inspections || 0}
</div>
</div>
<div className="bg-white rounded-lg shadow p-6">
<div className="text-sm text-gray-500 mb-1">Tasa de Completado</div>
<div className="text-3xl font-bold text-blue-600">
{(dashboardData.stats?.completion_rate || 0).toFixed(1)}%
</div>
</div>
<div className="bg-white rounded-lg shadow p-6">
<div className="text-sm text-gray-500 mb-1">Promedio Score</div>
<div className="text-3xl font-bold text-purple-600">
{(dashboardData.stats?.avg_score || 0).toFixed(1)}
</div>
</div>
</div>
{/* Ranking de Mecánicos */}
{user.role === 'admin' && dashboardData.mechanic_ranking?.length > 0 && (
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-semibold mb-4">🏆 Ranking de Mecánicos</h3>
<div className="overflow-x-auto">
<table className="min-w-full">
<thead>
<tr className="border-b">
<th className="text-left py-2 px-4">Posición</th>
<th className="text-left py-2 px-4">Mecánico</th>
<th className="text-right py-2 px-4">Inspecciones</th>
<th className="text-right py-2 px-4">Promedio</th>
<th className="text-right py-2 px-4">% Completado</th>
</tr>
</thead>
<tbody>
{dashboardData.mechanic_ranking.map((mechanic, index) => (
<tr key={mechanic.mechanic_id} className="border-b hover:bg-gray-50">
<td className="py-2 px-4">
{index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : `#${index + 1}`}
</td>
<td className="py-2 px-4 font-medium">{mechanic.mechanic_name}</td>
<td className="py-2 px-4 text-right">{mechanic.total_inspections}</td>
<td className="py-2 px-4 text-right">{mechanic.avg_score.toFixed(1)}</td>
<td className="py-2 px-4 text-right">{mechanic.completion_rate.toFixed(1)}%</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* Estadísticas por Checklist */}
{dashboardData.checklist_stats?.length > 0 && (
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-semibold mb-4">📋 Estadísticas por Checklist</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{dashboardData.checklist_stats.map((checklist) => (
<div key={checklist.checklist_id} className="border rounded-lg p-4">
<div className="font-medium text-gray-900 mb-2">{checklist.checklist_name}</div>
<div className="text-sm text-gray-600 space-y-1">
<div>Inspecciones: {checklist.total_inspections}</div>
<div>Promedio: {checklist.avg_score.toFixed(1)}</div>
</div>
</div>
))}
</div>
</div>
)}
{/* Lista de Inspecciones */}
{inspections.length > 0 && (
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-semibold mb-4">📝 Inspecciones Recientes</h3>
<div className="overflow-x-auto">
<table className="min-w-full">
<thead>
<tr className="border-b">
<th className="text-left py-2 px-4">Fecha</th>
<th className="text-left py-2 px-4">Checklist</th>
<th className="text-left py-2 px-4">Mecánico</th>
<th className="text-left py-2 px-4">Placa</th>
<th className="text-right py-2 px-4">Score</th>
<th className="text-center py-2 px-4">Estado</th>
<th className="text-center py-2 px-4">Alertas</th>
</tr>
</thead>
<tbody>
{inspections.slice(0, 20).map((inspection) => (
<tr key={inspection.id} className="border-b hover:bg-gray-50">
<td className="py-2 px-4">
{new Date(inspection.started_at).toLocaleDateString()}
</td>
<td className="py-2 px-4">{inspection.checklist_name}</td>
<td className="py-2 px-4">{inspection.mechanic_name}</td>
<td className="py-2 px-4 font-mono">{inspection.vehicle_plate}</td>
<td className="py-2 px-4 text-right font-medium">{inspection.score}</td>
<td className="py-2 px-4 text-center">
<span className={`px-2 py-1 text-xs rounded-full ${
inspection.status === 'completed'
? 'bg-green-100 text-green-800'
: 'bg-yellow-100 text-yellow-800'
}`}>
{inspection.status === 'completed' ? 'Completada' : 'Pendiente'}
</span>
</td>
<td className="py-2 px-4 text-center">
{inspection.flagged_items > 0 && (
<span className="px-2 py-1 text-xs bg-red-100 text-red-800 rounded-full">
🚩 {inspection.flagged_items}
</span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</div>
)
}
export default App