Cambios realizados:
Estados de filtros actualizados - Reemplazado date por dateFrom y dateTo API calls modificadas - Ahora loadDashboard() y loadInspections() envían start_date y end_date separados UI mejorada - Grid de 6 columnas con: Fecha Desde / Fecha Hasta (selectores de fecha) Mecánico (solo admins) Checklist Matrícula Botón Aplicar + botón Limpiar (🗑️) que aparece solo cuando hay filtros activos Consistencia - Mismo patrón de diseño que el módulo Inspecciones para experiencia uniforme Características: ✅ Filtrado por rango de fechas (desde - hasta) ✅ Paginación (10 items por página, se resetea al filtrar) ✅ Botón limpiar filtros condicional ✅ Labels descriptivos en español ✅ Compatibilidad con filtros existentes (mecánico, checklist, matrícula) Los módulos de Inspecciones y Reportes ahora tienen el mismo sistema de filtrado avanzado.
This commit is contained in:
@@ -4156,6 +4156,8 @@ function InspectionsTab({ inspections, user, onUpdate, onContinue }) {
|
||||
const [selectedInspection, setSelectedInspection] = useState(null)
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [statusFilter, setStatusFilter] = useState('all') // all, completed, incomplete
|
||||
const [dateFrom, setDateFrom] = useState('')
|
||||
const [dateTo, setDateTo] = useState('')
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const itemsPerPage = 10
|
||||
|
||||
@@ -4174,7 +4176,23 @@ function InspectionsTab({ inspections, user, onUpdate, onContinue }) {
|
||||
(statusFilter === 'completed' && inspection.status === 'completed') ||
|
||||
(statusFilter === 'incomplete' && inspection.status !== 'completed')
|
||||
|
||||
return matchesSearch && matchesStatus
|
||||
// Filtro por rango de fechas
|
||||
let matchesDate = true
|
||||
if (dateFrom || dateTo) {
|
||||
const inspectionDate = new Date(inspection.created_at)
|
||||
if (dateFrom) {
|
||||
const fromDate = new Date(dateFrom)
|
||||
fromDate.setHours(0, 0, 0, 0)
|
||||
matchesDate = matchesDate && inspectionDate >= fromDate
|
||||
}
|
||||
if (dateTo) {
|
||||
const toDate = new Date(dateTo)
|
||||
toDate.setHours(23, 59, 59, 999)
|
||||
matchesDate = matchesDate && inspectionDate <= toDate
|
||||
}
|
||||
}
|
||||
|
||||
return matchesSearch && matchesStatus && matchesDate
|
||||
})
|
||||
|
||||
// Calcular paginación
|
||||
@@ -4186,7 +4204,7 @@ function InspectionsTab({ inspections, user, onUpdate, onContinue }) {
|
||||
// Reset a página 1 cuando cambian los filtros
|
||||
useEffect(() => {
|
||||
setCurrentPage(1)
|
||||
}, [searchTerm, statusFilter])
|
||||
}, [searchTerm, statusFilter, dateFrom, dateTo])
|
||||
|
||||
if (inspections.length === 0) {
|
||||
return (
|
||||
@@ -4201,12 +4219,13 @@ function InspectionsTab({ inspections, user, onUpdate, onContinue }) {
|
||||
<>
|
||||
{/* Buscador y Filtros */}
|
||||
<div className="mb-6 space-y-4">
|
||||
<div className="flex gap-4 flex-wrap">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{/* Buscador */}
|
||||
<div className="flex-1 min-w-[300px]">
|
||||
<div className="lg:col-span-2">
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">Buscar</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Buscar por placa, marca, modelo, Nº pedido, OR o ID..."
|
||||
placeholder="Placa, marca, modelo, Nº pedido, OR o ID..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
@@ -4214,36 +4233,92 @@ function InspectionsTab({ inspections, user, onUpdate, onContinue }) {
|
||||
</div>
|
||||
|
||||
{/* Filtro de Estado */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">Estado</label>
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
>
|
||||
<option value="all">Todos los estados</option>
|
||||
<option value="all">Todos</option>
|
||||
<option value="completed">Completadas</option>
|
||||
<option value="incomplete">Incompletas</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Filtro por fecha desde */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">Fecha desde</label>
|
||||
<input
|
||||
type="date"
|
||||
value={dateFrom}
|
||||
onChange={(e) => setDateFrom(e.target.value)}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Segunda fila de filtros */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{/* Filtro por fecha hasta */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">Fecha hasta</label>
|
||||
<input
|
||||
type="date"
|
||||
value={dateTo}
|
||||
onChange={(e) => setDateTo(e.target.value)}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Botón limpiar filtros */}
|
||||
{(searchTerm || statusFilter !== 'all' || dateFrom || dateTo) && (
|
||||
<div className="flex items-end">
|
||||
<button
|
||||
onClick={() => {
|
||||
setSearchTerm('')
|
||||
setStatusFilter('all')
|
||||
setDateFrom('')
|
||||
setDateTo('')
|
||||
}}
|
||||
className="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition"
|
||||
>
|
||||
🗑️ Limpiar filtros
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Contador de resultados */}
|
||||
<div className="text-sm text-gray-600">
|
||||
Mostrando {startIndex + 1}-{Math.min(endIndex, filteredInspections.length)} de {filteredInspections.length} inspecciones
|
||||
<div className="flex justify-between items-center text-sm text-gray-600">
|
||||
<span>
|
||||
Mostrando {filteredInspections.length > 0 ? startIndex + 1 : 0}-{Math.min(endIndex, filteredInspections.length)} de {filteredInspections.length} inspecciones
|
||||
</span>
|
||||
{filteredInspections.length > 0 && (
|
||||
<span className="text-xs text-gray-500">
|
||||
Página {currentPage} de {totalPages}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Lista de Inspecciones */}
|
||||
{filteredInspections.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-gray-500">No se encontraron inspecciones con los filtros aplicados</p>
|
||||
<p className="text-gray-500">No se encontraron inspecciones</p>
|
||||
{(searchTerm || statusFilter !== 'all' || dateFrom || dateTo) && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setSearchTerm('')
|
||||
setStatusFilter('all')
|
||||
setDateFrom('')
|
||||
setDateTo('')
|
||||
}}
|
||||
className="mt-4 text-blue-600 hover:text-blue-700 underline"
|
||||
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition"
|
||||
>
|
||||
Limpiar filtros
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
@@ -4280,11 +4355,11 @@ function InspectionsTab({ inspections, user, onUpdate, onContinue }) {
|
||||
)}
|
||||
</div>
|
||||
{/* Fechas de inspección */}
|
||||
<div className="flex flex-wrap gap-4 mt-2 text-xs text-gray-500">
|
||||
<div className="flex flex-wrap gap-3 mt-2 text-xs">
|
||||
{inspection.created_at && (
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="flex items-center gap-1 text-gray-600">
|
||||
<span>📅</span>
|
||||
<span>Iniciada: <strong>{new Date(inspection.created_at).toLocaleDateString('es-ES', {
|
||||
<span>Inicio: <strong>{new Date(inspection.created_at).toLocaleDateString('es-ES', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
@@ -6812,13 +6887,15 @@ function ReportsTab({ user }) {
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const itemsPerPage = 10
|
||||
const [filters, setFilters] = useState({
|
||||
date: '',
|
||||
dateFrom: '',
|
||||
dateTo: '',
|
||||
mechanicId: '',
|
||||
checklistId: '',
|
||||
vehiclePlate: ''
|
||||
})
|
||||
const [appliedFilters, setAppliedFilters] = useState({
|
||||
date: '',
|
||||
dateFrom: '',
|
||||
dateTo: '',
|
||||
mechanicId: '',
|
||||
checklistId: '',
|
||||
vehiclePlate: ''
|
||||
@@ -6862,8 +6939,11 @@ function ReportsTab({ user }) {
|
||||
const API_URL = import.meta.env.VITE_API_URL || ''
|
||||
|
||||
let url = `${API_URL}/api/reports/dashboard?`
|
||||
if (filtersToApply.date) {
|
||||
url += `start_date=${filtersToApply.date}&end_date=${filtersToApply.date}&`
|
||||
if (filtersToApply.dateFrom) {
|
||||
url += `start_date=${filtersToApply.dateFrom}&`
|
||||
}
|
||||
if (filtersToApply.dateTo) {
|
||||
url += `end_date=${filtersToApply.dateTo}&`
|
||||
}
|
||||
if (filtersToApply.mechanicId) url += `mechanic_id=${filtersToApply.mechanicId}&`
|
||||
|
||||
@@ -6892,8 +6972,11 @@ function ReportsTab({ user }) {
|
||||
const API_URL = import.meta.env.VITE_API_URL || ''
|
||||
|
||||
let url = `${API_URL}/api/reports/inspections?`
|
||||
if (filtersToApply.date) {
|
||||
url += `start_date=${filtersToApply.date}&end_date=${filtersToApply.date}&`
|
||||
if (filtersToApply.dateFrom) {
|
||||
url += `start_date=${filtersToApply.dateFrom}&`
|
||||
}
|
||||
if (filtersToApply.dateTo) {
|
||||
url += `end_date=${filtersToApply.dateTo}&`
|
||||
}
|
||||
if (filtersToApply.mechanicId) url += `mechanic_id=${filtersToApply.mechanicId}&`
|
||||
|
||||
@@ -6947,7 +7030,7 @@ function ReportsTab({ user }) {
|
||||
// Cargar mecánicos y checklists primero
|
||||
await Promise.all([loadMechanics(), loadChecklists()])
|
||||
// Luego cargar datos sin filtros (filtros vacíos = todos los datos)
|
||||
const emptyFilters = { date: '', mechanicId: '', checklistId: '', vehiclePlate: '' }
|
||||
const emptyFilters = { dateFrom: '', dateTo: '', mechanicId: '', checklistId: '', vehiclePlate: '' }
|
||||
await Promise.all([loadDashboard(emptyFilters), loadInspections(emptyFilters)])
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -6967,15 +7050,26 @@ function ReportsTab({ user }) {
|
||||
{/* Filtros */}
|
||||
<div className="bg-white rounded-lg shadow p-4">
|
||||
<h3 className="text-lg font-semibold mb-4">🔍 Filtros</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Fecha
|
||||
Fecha Desde
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={filters.date}
|
||||
onChange={(e) => setFilters({...filters, date: e.target.value})}
|
||||
value={filters.dateFrom}
|
||||
onChange={(e) => setFilters({...filters, dateFrom: 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 Hasta
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={filters.dateTo}
|
||||
onChange={(e) => setFilters({...filters, dateTo: e.target.value})}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
@@ -6989,10 +7083,10 @@ function ReportsTab({ user }) {
|
||||
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"
|
||||
>
|
||||
<option value="">Todos los mecánicos</option>
|
||||
<option value="">Todos</option>
|
||||
{mechanics.map(mechanic => (
|
||||
<option key={mechanic.id} value={mechanic.id}>
|
||||
{mechanic.full_name || mechanic.username} ({mechanic.role})
|
||||
{mechanic.full_name || mechanic.username}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
@@ -7007,7 +7101,7 @@ function ReportsTab({ user }) {
|
||||
onChange={(e) => setFilters({...filters, checklistId: e.target.value})}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500"
|
||||
>
|
||||
<option value="">Todos los checklists</option>
|
||||
<option value="">Todos</option>
|
||||
{checklists.map(checklist => (
|
||||
<option key={checklist.id} value={checklist.id}>
|
||||
{checklist.name}
|
||||
@@ -7027,13 +7121,29 @@ function ReportsTab({ user }) {
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-end">
|
||||
<div className="flex items-end gap-2">
|
||||
<button
|
||||
onClick={applyFilters}
|
||||
className="w-full px-4 py-2 bg-gradient-to-r from-indigo-600 to-purple-600 text-white rounded-lg hover:from-indigo-700 hover:to-purple-700 transition"
|
||||
className="flex-1 px-4 py-2 bg-gradient-to-r from-indigo-600 to-purple-600 text-white rounded-lg hover:from-indigo-700 hover:to-purple-700 transition"
|
||||
>
|
||||
Aplicar Filtros
|
||||
Aplicar
|
||||
</button>
|
||||
{(filters.dateFrom || filters.dateTo || filters.mechanicId || filters.checklistId || filters.vehiclePlate) && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setFilters({ dateFrom: '', dateTo: '', mechanicId: '', checklistId: '', vehiclePlate: '' })
|
||||
setAppliedFilters({ dateFrom: '', dateTo: '', mechanicId: '', checklistId: '', vehiclePlate: '' })
|
||||
setCurrentPage(1)
|
||||
const emptyFilters = { dateFrom: '', dateTo: '', mechanicId: '', checklistId: '', vehiclePlate: '' }
|
||||
loadDashboard(emptyFilters)
|
||||
loadInspections(emptyFilters)
|
||||
}}
|
||||
className="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition"
|
||||
title="Limpiar filtros"
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user