Modulo de Reportes v1
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user