v1.0.67 Backend / v1.0.64 Frontend - Paginación de 10 elementos en todas las pestañas
Frontend (1.0.64): - 📄 Paginación en InspectionsTab (10 inspecciones/página) - 📄 Paginación en ChecklistsTab (10 checklists/página) - 📊 Paginación en ReportsTab (10 informes/página) - Auto-reset a página 1 cuando cambian filtros de búsqueda - Navegación inteligente con puntos suspensivos para rangos grandes - Muestra primera, última y páginas cercanas (actual ± 1) - Contador 'Mostrando X-Y de Z' en cada pestaña - Botones Anterior/Siguiente con estados deshabilitados - useEffect para sincronizar currentPage con filtros Mejoras de UX: - Navegación directa por número de página - Diseño consistente en las 3 pestañas - Controles responsive con hover states - Indicadores visuales claros de página actual Backend (1.0.67): - Sin cambios (mantiene versión actual) Documentación: - 📝 Agregada sección 'Control de Versiones' en README.md - Instrucciones detalladas para commits con versiones - Formato estándar para mensajes de commit - Tipos de commit (feat, fix, refactor, etc.) - Reglas de Semantic Versioning - Ubicación de archivos de versión"
This commit is contained in:
48
README.md
48
README.md
@@ -360,6 +360,54 @@ UPDATE checklists SET max_score = (
|
|||||||
|
|
||||||
MIT License - Uso libre para proyectos comerciales y personales
|
MIT License - Uso libre para proyectos comerciales y personales
|
||||||
|
|
||||||
|
## 📝 Control de Versiones
|
||||||
|
|
||||||
|
### Instrucciones para commits de Git
|
||||||
|
|
||||||
|
**IMPORTANTE**: Siempre incluir la versión actualizada en los mensajes de commit.
|
||||||
|
|
||||||
|
Formato recomendado:
|
||||||
|
```bash
|
||||||
|
git add .
|
||||||
|
git commit -m "tipo: descripción del cambio
|
||||||
|
|
||||||
|
- Detalle 1
|
||||||
|
- Detalle 2
|
||||||
|
- Frontend vX.X.XX / Backend vX.X.XX"
|
||||||
|
```
|
||||||
|
|
||||||
|
Tipos de commit:
|
||||||
|
- `feat`: Nueva funcionalidad
|
||||||
|
- `fix`: Corrección de bugs
|
||||||
|
- `refactor`: Refactorización de código
|
||||||
|
- `style`: Cambios de formato/estilo
|
||||||
|
- `docs`: Actualización de documentación
|
||||||
|
- `perf`: Mejoras de rendimiento
|
||||||
|
- `test`: Añadir o actualizar tests
|
||||||
|
|
||||||
|
**Ejemplo real**:
|
||||||
|
```bash
|
||||||
|
git add .
|
||||||
|
git commit -m "feat: Add pagination (10 items/page) to all main tabs
|
||||||
|
|
||||||
|
- Pagination for Inspections, Checklists, and Reports
|
||||||
|
- Auto-reset on filter changes
|
||||||
|
- Smart page navigation with ellipsis
|
||||||
|
- Result counters showing X-Y of Z items
|
||||||
|
- Frontend v1.0.64"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Versionado
|
||||||
|
|
||||||
|
Seguir **Semantic Versioning** (MAJOR.MINOR.PATCH):
|
||||||
|
- **MAJOR**: Cambios incompatibles en la API
|
||||||
|
- **MINOR**: Nueva funcionalidad compatible con versiones anteriores
|
||||||
|
- **PATCH**: Correcciones de bugs
|
||||||
|
|
||||||
|
Ubicación de versiones:
|
||||||
|
- Frontend: `frontend/package.json` → `"version": "X.X.XX"`
|
||||||
|
- Backend: `backend/app/main.py` → `version="X.X.XX"` en FastAPI app
|
||||||
|
|
||||||
## 🆘 Soporte
|
## 🆘 Soporte
|
||||||
|
|
||||||
Para problemas o preguntas:
|
Para problemas o preguntas:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "checklist-frontend",
|
"name": "checklist-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.63",
|
"version": "1.0.64",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -1735,6 +1735,8 @@ function ChecklistsTab({ checklists, user, onChecklistCreated, onStartInspection
|
|||||||
const [mechanics, setMechanics] = useState([])
|
const [mechanics, setMechanics] = useState([])
|
||||||
const [searchTerm, setSearchTerm] = useState('')
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
const [aiModeFilter, setAiModeFilter] = useState('all') // all, off, optional, required
|
const [aiModeFilter, setAiModeFilter] = useState('all') // all, off, optional, required
|
||||||
|
const [currentPage, setCurrentPage] = useState(1)
|
||||||
|
const itemsPerPage = 10
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
@@ -1783,6 +1785,17 @@ function ChecklistsTab({ checklists, user, onChecklistCreated, onStartInspection
|
|||||||
return matchesSearch && matchesAiMode
|
return matchesSearch && matchesAiMode
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Calcular paginación
|
||||||
|
const totalPages = Math.ceil(filteredChecklists.length / itemsPerPage)
|
||||||
|
const startIndex = (currentPage - 1) * itemsPerPage
|
||||||
|
const endIndex = startIndex + itemsPerPage
|
||||||
|
const paginatedChecklists = filteredChecklists.slice(startIndex, endIndex)
|
||||||
|
|
||||||
|
// Reset a página 1 cuando cambian los filtros
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentPage(1)
|
||||||
|
}, [searchTerm, aiModeFilter])
|
||||||
|
|
||||||
const handleCreate = async (e) => {
|
const handleCreate = async (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setCreating(true)
|
setCreating(true)
|
||||||
@@ -1969,7 +1982,7 @@ function ChecklistsTab({ checklists, user, onChecklistCreated, onStartInspection
|
|||||||
|
|
||||||
{/* Contador de resultados */}
|
{/* Contador de resultados */}
|
||||||
<div className="text-sm text-gray-600">
|
<div className="text-sm text-gray-600">
|
||||||
Mostrando {filteredChecklists.length} de {checklists.length} checklists
|
Mostrando {startIndex + 1}-{Math.min(endIndex, filteredChecklists.length)} de {filteredChecklists.length} checklists
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -2010,7 +2023,8 @@ function ChecklistsTab({ checklists, user, onChecklistCreated, onStartInspection
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
filteredChecklists.map((checklist) => (
|
<>
|
||||||
|
{paginatedChecklists.map((checklist) => (
|
||||||
<div key={checklist.id} className="border border-gray-200 rounded-lg p-4 hover:shadow-md transition">
|
<div key={checklist.id} className="border border-gray-200 rounded-lg p-4 hover:shadow-md transition">
|
||||||
<div className="flex justify-between items-start gap-4">
|
<div className="flex justify-between items-start gap-4">
|
||||||
{/* Logo del Checklist */}
|
{/* Logo del Checklist */}
|
||||||
@@ -2110,7 +2124,58 @@ function ChecklistsTab({ checklists, user, onChecklistCreated, onStartInspection
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))
|
))}
|
||||||
|
|
||||||
|
{/* Controles de paginación */}
|
||||||
|
{filteredChecklists.length > itemsPerPage && (
|
||||||
|
<div className="flex items-center justify-center gap-2 mt-6">
|
||||||
|
<button
|
||||||
|
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
className="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
← Anterior
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="flex gap-1">
|
||||||
|
{[...Array(totalPages)].map((_, index) => {
|
||||||
|
const page = index + 1
|
||||||
|
// Mostrar solo páginas cercanas a la actual
|
||||||
|
if (
|
||||||
|
page === 1 ||
|
||||||
|
page === totalPages ||
|
||||||
|
(page >= currentPage - 1 && page <= currentPage + 1)
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={page}
|
||||||
|
onClick={() => setCurrentPage(page)}
|
||||||
|
className={`px-3 py-2 rounded-lg ${
|
||||||
|
currentPage === page
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'border border-gray-300 hover:bg-gray-50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{page}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
} else if (page === currentPage - 2 || page === currentPage + 2) {
|
||||||
|
return <span key={page} className="px-2 py-2">...</span>
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
|
||||||
|
disabled={currentPage === totalPages}
|
||||||
|
className="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
Siguiente →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Modal Gestionar Preguntas */}
|
{/* Modal Gestionar Preguntas */}
|
||||||
@@ -3173,6 +3238,8 @@ function InspectionsTab({ inspections, user, onUpdate }) {
|
|||||||
const [selectedInspection, setSelectedInspection] = useState(null)
|
const [selectedInspection, setSelectedInspection] = useState(null)
|
||||||
const [searchTerm, setSearchTerm] = useState('')
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
const [statusFilter, setStatusFilter] = useState('all') // all, completed, draft
|
const [statusFilter, setStatusFilter] = useState('all') // all, completed, draft
|
||||||
|
const [currentPage, setCurrentPage] = useState(1)
|
||||||
|
const itemsPerPage = 10
|
||||||
|
|
||||||
// Filtrar inspecciones
|
// Filtrar inspecciones
|
||||||
const filteredInspections = inspections.filter(inspection => {
|
const filteredInspections = inspections.filter(inspection => {
|
||||||
@@ -3192,6 +3259,17 @@ function InspectionsTab({ inspections, user, onUpdate }) {
|
|||||||
return matchesSearch && matchesStatus
|
return matchesSearch && matchesStatus
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Calcular paginación
|
||||||
|
const totalPages = Math.ceil(filteredInspections.length / itemsPerPage)
|
||||||
|
const startIndex = (currentPage - 1) * itemsPerPage
|
||||||
|
const endIndex = startIndex + itemsPerPage
|
||||||
|
const paginatedInspections = filteredInspections.slice(startIndex, endIndex)
|
||||||
|
|
||||||
|
// Reset a página 1 cuando cambian los filtros
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentPage(1)
|
||||||
|
}, [searchTerm, statusFilter])
|
||||||
|
|
||||||
if (inspections.length === 0) {
|
if (inspections.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
@@ -3231,7 +3309,7 @@ function InspectionsTab({ inspections, user, onUpdate }) {
|
|||||||
|
|
||||||
{/* Contador de resultados */}
|
{/* Contador de resultados */}
|
||||||
<div className="text-sm text-gray-600">
|
<div className="text-sm text-gray-600">
|
||||||
Mostrando {filteredInspections.length} de {inspections.length} inspecciones
|
Mostrando {startIndex + 1}-{Math.min(endIndex, filteredInspections.length)} de {filteredInspections.length} inspecciones
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -3251,7 +3329,7 @@ function InspectionsTab({ inspections, user, onUpdate }) {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{filteredInspections.map((inspection) => (
|
{paginatedInspections.map((inspection) => (
|
||||||
<div key={inspection.id} className="border border-gray-200 rounded-lg p-4 hover:shadow-md transition">
|
<div key={inspection.id} className="border border-gray-200 rounded-lg p-4 hover:shadow-md transition">
|
||||||
<div className="flex justify-between items-start">
|
<div className="flex justify-between items-start">
|
||||||
<div>
|
<div>
|
||||||
@@ -3296,6 +3374,56 @@ function InspectionsTab({ inspections, user, onUpdate }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Controles de paginación */}
|
||||||
|
{filteredInspections.length > itemsPerPage && (
|
||||||
|
<div className="flex items-center justify-center gap-2 mt-6">
|
||||||
|
<button
|
||||||
|
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
className="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
← Anterior
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="flex gap-1">
|
||||||
|
{[...Array(totalPages)].map((_, index) => {
|
||||||
|
const page = index + 1
|
||||||
|
// Mostrar solo páginas cercanas a la actual
|
||||||
|
if (
|
||||||
|
page === 1 ||
|
||||||
|
page === totalPages ||
|
||||||
|
(page >= currentPage - 1 && page <= currentPage + 1)
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={page}
|
||||||
|
onClick={() => setCurrentPage(page)}
|
||||||
|
className={`px-3 py-2 rounded-lg ${
|
||||||
|
currentPage === page
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'border border-gray-300 hover:bg-gray-50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{page}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
} else if (page === currentPage - 2 || page === currentPage + 2) {
|
||||||
|
return <span key={page} className="px-2 py-2">...</span>
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
|
||||||
|
disabled={currentPage === totalPages}
|
||||||
|
className="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
Siguiente →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Modal de Detalle */}
|
{/* Modal de Detalle */}
|
||||||
{selectedInspection && (
|
{selectedInspection && (
|
||||||
<InspectionDetailModal
|
<InspectionDetailModal
|
||||||
@@ -4898,6 +5026,8 @@ function ReportsTab({ user }) {
|
|||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [mechanics, setMechanics] = useState([])
|
const [mechanics, setMechanics] = useState([])
|
||||||
const [checklists, setChecklists] = useState([])
|
const [checklists, setChecklists] = useState([])
|
||||||
|
const [currentPage, setCurrentPage] = useState(1)
|
||||||
|
const itemsPerPage = 10
|
||||||
const [filters, setFilters] = useState({
|
const [filters, setFilters] = useState({
|
||||||
date: '',
|
date: '',
|
||||||
mechanicId: '',
|
mechanicId: '',
|
||||||
@@ -5023,6 +5153,7 @@ function ReportsTab({ user }) {
|
|||||||
const applyFilters = async () => {
|
const applyFilters = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setAppliedFilters(filters)
|
setAppliedFilters(filters)
|
||||||
|
setCurrentPage(1) // Reset a página 1 al aplicar filtros
|
||||||
await Promise.all([loadDashboard(filters), loadInspections(filters)])
|
await Promise.all([loadDashboard(filters), loadInspections(filters)])
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
@@ -5207,6 +5338,12 @@ function ReportsTab({ user }) {
|
|||||||
{inspections.length > 0 && (
|
{inspections.length > 0 && (
|
||||||
<div className="bg-white rounded-lg shadow p-6">
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
<h3 className="text-lg font-semibold mb-4">📝 Inspecciones Recientes</h3>
|
<h3 className="text-lg font-semibold mb-4">📝 Inspecciones Recientes</h3>
|
||||||
|
|
||||||
|
{/* Información de paginación */}
|
||||||
|
<div className="text-sm text-gray-600 mb-4">
|
||||||
|
Mostrando {((currentPage - 1) * itemsPerPage) + 1}-{Math.min(currentPage * itemsPerPage, inspections.length)} de {inspections.length} inspecciones
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="min-w-full">
|
<table className="min-w-full">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -5222,7 +5359,7 @@ function ReportsTab({ user }) {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{inspections.slice(0, 20).map((inspection) => (
|
{inspections.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage).map((inspection) => (
|
||||||
<tr key={inspection.id} className="border-b hover:bg-gray-50">
|
<tr key={inspection.id} className="border-b hover:bg-gray-50">
|
||||||
<td className="py-2 px-4">
|
<td className="py-2 px-4">
|
||||||
{new Date(inspection.started_at).toLocaleDateString()}
|
{new Date(inspection.started_at).toLocaleDateString()}
|
||||||
@@ -5286,6 +5423,57 @@ function ReportsTab({ user }) {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Controles de paginación */}
|
||||||
|
{inspections.length > itemsPerPage && (
|
||||||
|
<div className="flex items-center justify-center gap-2 mt-6">
|
||||||
|
<button
|
||||||
|
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
className="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
← Anterior
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="flex gap-1">
|
||||||
|
{[...Array(Math.ceil(inspections.length / itemsPerPage))].map((_, index) => {
|
||||||
|
const page = index + 1
|
||||||
|
const totalPages = Math.ceil(inspections.length / itemsPerPage)
|
||||||
|
// Mostrar solo páginas cercanas a la actual
|
||||||
|
if (
|
||||||
|
page === 1 ||
|
||||||
|
page === totalPages ||
|
||||||
|
(page >= currentPage - 1 && page <= currentPage + 1)
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={page}
|
||||||
|
onClick={() => setCurrentPage(page)}
|
||||||
|
className={`px-3 py-2 rounded-lg ${
|
||||||
|
currentPage === page
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'border border-gray-300 hover:bg-gray-50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{page}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
} else if (page === currentPage - 2 || page === currentPage + 2) {
|
||||||
|
return <span key={page} className="px-2 py-2">...</span>
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setCurrentPage(prev => Math.min(Math.ceil(inspections.length / itemsPerPage), prev + 1))}
|
||||||
|
disabled={currentPage === Math.ceil(inspections.length / itemsPerPage)}
|
||||||
|
className="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
Siguiente →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user