Nuevo Commit
This commit is contained in:
131
GUIA_MECANICO.md
Normal file
131
GUIA_MECANICO.md
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
# Guía de Uso - Interfaz del Mecánico
|
||||||
|
|
||||||
|
## 🔧 Nueva Funcionalidad: Realizar Inspecciones
|
||||||
|
|
||||||
|
### Inicio de Sesión
|
||||||
|
1. Accede a http://localhost:5173
|
||||||
|
2. Ingresa como mecánico:
|
||||||
|
- Usuario: `mecanico1`
|
||||||
|
- Contraseña: `mecanico123`
|
||||||
|
|
||||||
|
### Realizar una Nueva Inspección
|
||||||
|
|
||||||
|
#### Paso 1: Seleccionar Checklist
|
||||||
|
1. En la pestaña "Checklists", verás todos los checklists disponibles
|
||||||
|
2. Cada checklist muestra:
|
||||||
|
- Nombre y descripción
|
||||||
|
- Puntuación máxima
|
||||||
|
- Modo de IA (off/assisted/full)
|
||||||
|
3. Haz clic en el botón **"Nueva Inspección"** del checklist que deseas usar
|
||||||
|
|
||||||
|
#### Paso 2: Datos del Vehículo
|
||||||
|
El modal se abrirá mostrando un formulario con:
|
||||||
|
- **Placa del Vehículo*** (requerido)
|
||||||
|
- **Marca*** (requerido)
|
||||||
|
- **Modelo*** (requerido)
|
||||||
|
- Año
|
||||||
|
- Kilometraje
|
||||||
|
- Número de OR (Orden de Reparación)
|
||||||
|
- Nombre del Cliente
|
||||||
|
- Teléfono del Cliente
|
||||||
|
|
||||||
|
*Los campos marcados con asterisco son obligatorios*
|
||||||
|
|
||||||
|
Haz clic en **"Continuar"** para avanzar.
|
||||||
|
|
||||||
|
#### Paso 3: Responder Preguntas
|
||||||
|
El sistema te mostrará cada pregunta del checklist una por una:
|
||||||
|
|
||||||
|
**Tipos de Respuesta:**
|
||||||
|
- **Pasa/Falla**: Opciones de radio para indicar si pasa o no la verificación
|
||||||
|
- **Bueno/Regular/Malo**: Menú desplegable para calificar el estado
|
||||||
|
- **Numérico**: Campo para ingresar valores numéricos (ej: presión de neumáticos)
|
||||||
|
- **Estado**: Opciones OK/Advertencia/Crítico
|
||||||
|
- **Texto**: Campo de texto libre para respuestas descriptivas
|
||||||
|
|
||||||
|
**Para cada pregunta puedes:**
|
||||||
|
- ✅ Ingresar la respuesta requerida
|
||||||
|
- 📝 Agregar observaciones (opcional)
|
||||||
|
- 📷 Subir fotografías si la pregunta lo permite (opcional)
|
||||||
|
|
||||||
|
**Navegación:**
|
||||||
|
- Botón **"← Anterior"** para volver a la pregunta anterior
|
||||||
|
- Botón **"Siguiente →"** para avanzar a la siguiente pregunta
|
||||||
|
- Botón **"Continuar a Firmas"** al finalizar todas las preguntas
|
||||||
|
|
||||||
|
#### Paso 4: Firmas
|
||||||
|
Al completar todas las preguntas:
|
||||||
|
1. Se mostrará un mensaje de confirmación verde
|
||||||
|
2. Verás dos áreas de firma:
|
||||||
|
- **Firma del Mecánico** (tú)
|
||||||
|
- **Firma del Cliente**
|
||||||
|
3. Usa el cursor/stylus/dedo para dibujar las firmas
|
||||||
|
4. Botón "Limpiar" para borrar y volver a firmar
|
||||||
|
5. Haz clic en **"✓ Finalizar Inspección"**
|
||||||
|
|
||||||
|
#### Paso 5: PDF Automático
|
||||||
|
Al finalizar:
|
||||||
|
- El sistema generará automáticamente un PDF con todos los datos
|
||||||
|
- El PDF se descargará a tu dispositivo
|
||||||
|
- La inspección quedará registrada en la pestaña "Inspecciones"
|
||||||
|
|
||||||
|
### Ver Inspecciones Realizadas
|
||||||
|
- Cambia a la pestaña **"Inspecciones"**
|
||||||
|
- Verás todas tus inspecciones con:
|
||||||
|
- Datos del vehículo
|
||||||
|
- Fecha de realización
|
||||||
|
- Puntuación obtenida (score/max_score y porcentaje)
|
||||||
|
- Número de elementos señalados (si los hay)
|
||||||
|
- Botón "Ver Detalle" para consultar la inspección completa
|
||||||
|
|
||||||
|
## 📊 Ejemplo de Checklist Disponible
|
||||||
|
|
||||||
|
### "Inspección Vehicular Completa" (103 preguntas)
|
||||||
|
|
||||||
|
**Secciones:**
|
||||||
|
1. **Documentación del Vehículo** (8 preguntas)
|
||||||
|
2. **Inspección Exterior** (9 preguntas)
|
||||||
|
3. **Neumáticos y Ruedas** (9 preguntas)
|
||||||
|
4. **Sistema Eléctrico** (10 preguntas)
|
||||||
|
5. **Motor y Compartimento** (9 preguntas)
|
||||||
|
6. **Sistema de Frenos** (9 preguntas)
|
||||||
|
7. **Suspensión y Dirección** (8 preguntas)
|
||||||
|
8. **Interior del Vehículo** (10 pregunas)
|
||||||
|
9. **Sistema de Seguridad** (9 preguntas)
|
||||||
|
10. **Transmisión** (6 preguntas)
|
||||||
|
11. **Sistema de Escape** (7 preguntas)
|
||||||
|
12. **Prueba de Ruta** (6 preguntas)
|
||||||
|
13. **Verificación Final** (3 preguntas)
|
||||||
|
|
||||||
|
**Total**: 147 puntos máximos
|
||||||
|
|
||||||
|
## 🎯 Consejos
|
||||||
|
|
||||||
|
- ✅ Completa todas las preguntas con atención
|
||||||
|
- 📸 Toma fotos de elementos importantes o con fallas
|
||||||
|
- 📝 Agrega observaciones detalladas cuando sea necesario
|
||||||
|
- ✍️ Asegúrate de que ambas firmas estén claras
|
||||||
|
- 💾 El PDF se genera automáticamente al finalizar
|
||||||
|
|
||||||
|
## 🔄 Flujo Técnico (Backend)
|
||||||
|
|
||||||
|
1. **POST** `/api/checklists/{id}/inspections` - Crea la inspección con datos del vehículo
|
||||||
|
2. **POST** `/api/inspections/{id}/answers` - Guarda cada respuesta
|
||||||
|
3. **POST** `/api/inspections/{id}/answers/{answer_id}/upload-photo` - Sube fotos (si hay)
|
||||||
|
4. **PATCH** `/api/inspections/{id}` - Actualiza con firmas en base64
|
||||||
|
5. **GET** `/api/inspections/{id}/pdf` - Genera y descarga el PDF
|
||||||
|
|
||||||
|
## 🛠️ Próximos Pasos
|
||||||
|
|
||||||
|
Esta es la primera versión del MVP. Funcionalidades futuras:
|
||||||
|
- Vista detallada de inspecciones completadas
|
||||||
|
- Edición de inspecciones en progreso
|
||||||
|
- Filtros y búsqueda de inspecciones
|
||||||
|
- Dashboard con estadísticas
|
||||||
|
- Notificaciones y alertas
|
||||||
|
- Integración con IA para recomendaciones
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**¿Preguntas o problemas?**
|
||||||
|
Revisa la consola del navegador (F12) o los logs de Docker para más información.
|
||||||
180
add_questions.py
Normal file
180
add_questions.py
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
"""
|
||||||
|
Script para agregar preguntas a un checklist basado en el formato del PDF
|
||||||
|
Ejecutar: docker cp add_questions.py checklist-backend:/app/ && docker-compose exec -T backend python /app/add_questions.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
from app.core.database import SessionLocal
|
||||||
|
from app.models import Checklist, Question
|
||||||
|
|
||||||
|
# ID del checklist al que quieres agregar preguntas
|
||||||
|
CHECKLIST_ID = 2 # Cambia este número según el ID de tu checklist
|
||||||
|
|
||||||
|
db = SessionLocal()
|
||||||
|
|
||||||
|
# Verificar que el checklist existe
|
||||||
|
checklist = db.query(Checklist).filter(Checklist.id == CHECKLIST_ID).first()
|
||||||
|
if not checklist:
|
||||||
|
print(f"❌ Checklist con ID {CHECKLIST_ID} no encontrado")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
print(f"✅ Agregando preguntas al checklist: {checklist.name}")
|
||||||
|
|
||||||
|
# Definir todas las preguntas por sección
|
||||||
|
questions_data = [
|
||||||
|
# DOCUMENTACIÓN
|
||||||
|
('Documentación', 'Cédula Verde', 'pass_fail', 2, False),
|
||||||
|
('Documentación', 'Verificación Técnica Vehicular', 'pass_fail', 2, False),
|
||||||
|
('Documentación', 'Póliza de Seguro', 'pass_fail', 2, False),
|
||||||
|
('Documentación', 'Licencia de conducir del chofer', 'pass_fail', 2, False),
|
||||||
|
|
||||||
|
# EXTERIOR DEL VEHÍCULO
|
||||||
|
('Exterior', 'Estado de carrocería', 'good_bad', 1, True),
|
||||||
|
('Exterior', 'Espejo retrovisor izquierdo', 'pass_fail', 1, True),
|
||||||
|
('Exterior', 'Espejo retrovisor derecho', 'pass_fail', 1, True),
|
||||||
|
('Exterior', 'Parabrisas delantero', 'pass_fail', 1, True),
|
||||||
|
('Exterior', 'Luneta trasera', 'pass_fail', 1, True),
|
||||||
|
('Exterior', 'Cristales laterales', 'pass_fail', 1, True),
|
||||||
|
('Exterior', 'Escobillas limpiaparabrisas', 'pass_fail', 1, True),
|
||||||
|
('Exterior', 'Antena', 'pass_fail', 1, False),
|
||||||
|
('Exterior', 'Tapa de combustible', 'pass_fail', 1, True),
|
||||||
|
('Exterior', 'Paragolpes delantero', 'pass_fail', 1, True),
|
||||||
|
('Exterior', 'Paragolpes trasero', 'pass_fail', 1, True),
|
||||||
|
|
||||||
|
# NEUMÁTICOS
|
||||||
|
('Neumáticos', 'Neumático delantero izquierdo - Presión', 'pass_fail', 1, True),
|
||||||
|
('Neumáticos', 'Neumático delantero izquierdo - Banda de rodadura', 'good_bad', 2, True),
|
||||||
|
('Neumáticos', 'Neumático delantero derecho - Presión', 'pass_fail', 1, True),
|
||||||
|
('Neumáticos', 'Neumático delantero derecho - Banda de rodadura', 'good_bad', 2, True),
|
||||||
|
('Neumáticos', 'Neumático trasero izquierdo - Presión', 'pass_fail', 1, True),
|
||||||
|
('Neumáticos', 'Neumático trasero izquierdo - Banda de rodadura', 'good_bad', 2, True),
|
||||||
|
('Neumáticos', 'Neumático trasero derecho - Presión', 'pass_fail', 1, True),
|
||||||
|
('Neumáticos', 'Neumático trasero derecho - Banda de rodadura', 'good_bad', 2, True),
|
||||||
|
('Neumáticos', 'Neumático de auxilio - Estado', 'pass_fail', 1, True),
|
||||||
|
|
||||||
|
# SISTEMA ELÉCTRICO
|
||||||
|
('Sistema Eléctrico', 'Batería - Estado', 'good_bad', 2, True),
|
||||||
|
('Sistema Eléctrico', 'Luces de posición delanteras', 'pass_fail', 1, False),
|
||||||
|
('Sistema Eléctrico', 'Luces bajas', 'pass_fail', 1, False),
|
||||||
|
('Sistema Eléctrico', 'Luces altas', 'pass_fail', 1, False),
|
||||||
|
('Sistema Eléctrico', 'Luces de giro delanteras', 'pass_fail', 1, False),
|
||||||
|
('Sistema Eléctrico', 'Luces de freno', 'pass_fail', 2, False),
|
||||||
|
('Sistema Eléctrico', 'Luces de retroceso', 'pass_fail', 1, False),
|
||||||
|
('Sistema Eléctrico', 'Luces traseras', 'pass_fail', 1, False),
|
||||||
|
('Sistema Eléctrico', 'Luces de giro traseras', 'pass_fail', 1, False),
|
||||||
|
('Sistema Eléctrico', 'Luz de patente', 'pass_fail', 1, False),
|
||||||
|
('Sistema Eléctrico', 'Luz interior', 'pass_fail', 1, False),
|
||||||
|
('Sistema Eléctrico', 'Bocina', 'pass_fail', 1, False),
|
||||||
|
('Sistema Eléctrico', 'Testigos en tablero', 'pass_fail', 1, True),
|
||||||
|
|
||||||
|
# MOTOR
|
||||||
|
('Motor', 'Nivel de aceite', 'pass_fail', 2, True),
|
||||||
|
('Motor', 'Fugas de aceite', 'pass_fail', 2, True),
|
||||||
|
('Motor', 'Filtro de aceite', 'status', 1, True),
|
||||||
|
('Motor', 'Nivel de refrigerante', 'pass_fail', 1, True),
|
||||||
|
('Motor', 'Fugas de refrigerante', 'pass_fail', 2, True),
|
||||||
|
('Motor', 'Estado de mangueras', 'pass_fail', 1, True),
|
||||||
|
('Motor', 'Filtro de aire', 'status', 1, True),
|
||||||
|
('Motor', 'Correa de distribución', 'pass_fail', 2, True),
|
||||||
|
('Motor', 'Correas auxiliares', 'pass_fail', 1, True),
|
||||||
|
|
||||||
|
# FRENOS
|
||||||
|
('Frenos', 'Pastillas/zapatas delanteras', 'good_bad', 2, True),
|
||||||
|
('Frenos', 'Discos/tambores delanteros', 'good_bad', 2, True),
|
||||||
|
('Frenos', 'Pastillas/zapatas traseras', 'good_bad', 2, True),
|
||||||
|
('Frenos', 'Discos/tambores traseros', 'good_bad', 2, True),
|
||||||
|
('Frenos', 'Nivel de líquido de frenos', 'pass_fail', 2, False),
|
||||||
|
('Frenos', 'Porcentaje de humedad en líquido', 'numeric', 1, False),
|
||||||
|
('Frenos', 'Freno de mano', 'pass_fail', 2, False),
|
||||||
|
|
||||||
|
# SUSPENSIÓN Y DIRECCIÓN
|
||||||
|
('Suspensión', 'Amortiguadores delanteros', 'pass_fail', 2, True),
|
||||||
|
('Suspensión', 'Amortiguadores traseros', 'pass_fail', 2, True),
|
||||||
|
('Suspensión', 'Cojinetes de ruedas', 'pass_fail', 2, False),
|
||||||
|
('Suspensión', 'Rótulas', 'pass_fail', 2, True),
|
||||||
|
('Suspensión', 'Bieletas', 'pass_fail', 1, True),
|
||||||
|
('Suspensión', 'Bujes', 'pass_fail', 1, True),
|
||||||
|
('Dirección', 'Nivel de líquido de dirección', 'pass_fail', 1, True),
|
||||||
|
('Dirección', 'Estado de cremallera/caja', 'pass_fail', 2, True),
|
||||||
|
('Dirección', 'Fugas en dirección', 'pass_fail', 2, True),
|
||||||
|
|
||||||
|
# INTERIOR
|
||||||
|
('Interior', 'Tablero de instrumentos', 'pass_fail', 1, True),
|
||||||
|
('Interior', 'Funcionamiento de velocímetro', 'pass_fail', 1, False),
|
||||||
|
('Interior', 'Funcionamiento de cuentakilómetros', 'pass_fail', 1, False),
|
||||||
|
('Interior', 'Cinturones de seguridad delanteros', 'pass_fail', 2, True),
|
||||||
|
('Interior', 'Cinturones de seguridad traseros', 'pass_fail', 2, True),
|
||||||
|
('Interior', 'Asientos', 'pass_fail', 1, True),
|
||||||
|
('Interior', 'Apoyacabezas', 'pass_fail', 1, True),
|
||||||
|
('Interior', 'Aire acondicionado', 'pass_fail', 1, False),
|
||||||
|
('Interior', 'Calefacción', 'pass_fail', 1, False),
|
||||||
|
('Interior', 'Radio/sistema de audio', 'pass_fail', 1, False),
|
||||||
|
('Interior', 'Ceniceros', 'pass_fail', 1, False),
|
||||||
|
('Interior', 'Encendedor', 'pass_fail', 1, False),
|
||||||
|
('Interior', 'Guantera', 'pass_fail', 1, False),
|
||||||
|
('Interior', 'Tapizado', 'good_bad', 1, True),
|
||||||
|
|
||||||
|
# SEGURIDAD
|
||||||
|
('Seguridad', 'Matafuego - Fecha de vencimiento', 'pass_fail', 2, True),
|
||||||
|
('Seguridad', 'Matafuego - Presión', 'pass_fail', 2, True),
|
||||||
|
('Seguridad', 'Balizas triangulares', 'pass_fail', 1, True),
|
||||||
|
('Seguridad', 'Botiquín de primeros auxilios', 'pass_fail', 1, True),
|
||||||
|
('Seguridad', 'Llave de ruedas', 'pass_fail', 1, False),
|
||||||
|
('Seguridad', 'Gato hidráulico', 'pass_fail', 1, False),
|
||||||
|
('Seguridad', 'Chaleco reflectivo', 'pass_fail', 1, False),
|
||||||
|
|
||||||
|
# TRANSMISIÓN
|
||||||
|
('Transmisión', 'Nivel de aceite de transmisión', 'pass_fail', 1, True),
|
||||||
|
('Transmisión', 'Fugas en transmisión', 'pass_fail', 2, True),
|
||||||
|
('Transmisión', 'Estado de embrague', 'pass_fail', 2, False),
|
||||||
|
('Transmisión', 'Funcionamiento de cambios', 'pass_fail', 2, False),
|
||||||
|
|
||||||
|
# ESCAPE
|
||||||
|
('Escape', 'Estado del sistema de escape', 'pass_fail', 1, True),
|
||||||
|
('Escape', 'Fugas en escape', 'pass_fail', 2, True),
|
||||||
|
('Escape', 'Silenciador', 'pass_fail', 1, True),
|
||||||
|
('Escape', 'Catalizador', 'pass_fail', 1, True),
|
||||||
|
|
||||||
|
# PRUEBA DE RUTA
|
||||||
|
('Prueba de Ruta', 'Arranque del motor', 'pass_fail', 2, False),
|
||||||
|
('Prueba de Ruta', 'Ralentí estable', 'pass_fail', 1, False),
|
||||||
|
('Prueba de Ruta', 'Aceleración', 'pass_fail', 2, False),
|
||||||
|
('Prueba de Ruta', 'Frenado', 'pass_fail', 2, False),
|
||||||
|
('Prueba de Ruta', 'Dirección', 'pass_fail', 2, False),
|
||||||
|
('Prueba de Ruta', 'Cambios de marcha', 'pass_fail', 2, False),
|
||||||
|
('Prueba de Ruta', 'Ruidos anormales', 'pass_fail', 2, True),
|
||||||
|
('Prueba de Ruta', 'Vibraciones', 'pass_fail', 1, True),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Agregar todas las preguntas
|
||||||
|
max_score = 0
|
||||||
|
for idx, (section, text, qtype, points, photos) in enumerate(questions_data):
|
||||||
|
question = Question(
|
||||||
|
checklist_id=checklist.id,
|
||||||
|
section=section,
|
||||||
|
text=text,
|
||||||
|
type=qtype,
|
||||||
|
points=points,
|
||||||
|
order=idx + 1,
|
||||||
|
allow_photos=photos,
|
||||||
|
max_photos=3 if photos else 0,
|
||||||
|
requires_comment_on_fail=False
|
||||||
|
)
|
||||||
|
db.add(question)
|
||||||
|
max_score += points
|
||||||
|
|
||||||
|
# Actualizar puntuación máxima del checklist
|
||||||
|
checklist.max_score = max_score
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
print(f'✅ Se agregaron {len(questions_data)} preguntas al checklist')
|
||||||
|
print(f'✅ Puntuación máxima: {max_score} puntos')
|
||||||
|
print(f'\nDesglose por sección:')
|
||||||
|
|
||||||
|
# Contar preguntas por sección
|
||||||
|
from collections import Counter
|
||||||
|
sections = Counter([s for s, _, _, _, _ in questions_data])
|
||||||
|
for section, count in sections.items():
|
||||||
|
section_points = sum(p for s, _, _, p, _ in questions_data if s == section)
|
||||||
|
print(f' - {section}: {count} preguntas ({section_points} puntos)')
|
||||||
|
|
||||||
|
db.close()
|
||||||
180
backend/add_questions.py
Normal file
180
backend/add_questions.py
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
"""
|
||||||
|
Script para agregar preguntas a un checklist basado en el formato del PDF
|
||||||
|
Ejecutar: docker cp add_questions.py checklist-backend:/app/ && docker-compose exec -T backend python /app/add_questions.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
from app.core.database import SessionLocal
|
||||||
|
from app.models import Checklist, Question
|
||||||
|
|
||||||
|
# ID del checklist al que quieres agregar preguntas
|
||||||
|
CHECKLIST_ID = 2 # Cambia este número según el ID de tu checklist
|
||||||
|
|
||||||
|
db = SessionLocal()
|
||||||
|
|
||||||
|
# Verificar que el checklist existe
|
||||||
|
checklist = db.query(Checklist).filter(Checklist.id == CHECKLIST_ID).first()
|
||||||
|
if not checklist:
|
||||||
|
print(f"❌ Checklist con ID {CHECKLIST_ID} no encontrado")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
print(f"✅ Agregando preguntas al checklist: {checklist.name}")
|
||||||
|
|
||||||
|
# Definir todas las preguntas por sección
|
||||||
|
questions_data = [
|
||||||
|
# DOCUMENTACIÓN
|
||||||
|
('Documentación', 'Cédula Verde', 'pass_fail', 2, False),
|
||||||
|
('Documentación', 'Verificación Técnica Vehicular', 'pass_fail', 2, False),
|
||||||
|
('Documentación', 'Póliza de Seguro', 'pass_fail', 2, False),
|
||||||
|
('Documentación', 'Licencia de conducir del chofer', 'pass_fail', 2, False),
|
||||||
|
|
||||||
|
# EXTERIOR DEL VEHÍCULO
|
||||||
|
('Exterior', 'Estado de carrocería', 'good_bad', 1, True),
|
||||||
|
('Exterior', 'Espejo retrovisor izquierdo', 'pass_fail', 1, True),
|
||||||
|
('Exterior', 'Espejo retrovisor derecho', 'pass_fail', 1, True),
|
||||||
|
('Exterior', 'Parabrisas delantero', 'pass_fail', 1, True),
|
||||||
|
('Exterior', 'Luneta trasera', 'pass_fail', 1, True),
|
||||||
|
('Exterior', 'Cristales laterales', 'pass_fail', 1, True),
|
||||||
|
('Exterior', 'Escobillas limpiaparabrisas', 'pass_fail', 1, True),
|
||||||
|
('Exterior', 'Antena', 'pass_fail', 1, False),
|
||||||
|
('Exterior', 'Tapa de combustible', 'pass_fail', 1, True),
|
||||||
|
('Exterior', 'Paragolpes delantero', 'pass_fail', 1, True),
|
||||||
|
('Exterior', 'Paragolpes trasero', 'pass_fail', 1, True),
|
||||||
|
|
||||||
|
# NEUMÁTICOS
|
||||||
|
('Neumáticos', 'Neumático delantero izquierdo - Presión', 'pass_fail', 1, True),
|
||||||
|
('Neumáticos', 'Neumático delantero izquierdo - Banda de rodadura', 'good_bad', 2, True),
|
||||||
|
('Neumáticos', 'Neumático delantero derecho - Presión', 'pass_fail', 1, True),
|
||||||
|
('Neumáticos', 'Neumático delantero derecho - Banda de rodadura', 'good_bad', 2, True),
|
||||||
|
('Neumáticos', 'Neumático trasero izquierdo - Presión', 'pass_fail', 1, True),
|
||||||
|
('Neumáticos', 'Neumático trasero izquierdo - Banda de rodadura', 'good_bad', 2, True),
|
||||||
|
('Neumáticos', 'Neumático trasero derecho - Presión', 'pass_fail', 1, True),
|
||||||
|
('Neumáticos', 'Neumático trasero derecho - Banda de rodadura', 'good_bad', 2, True),
|
||||||
|
('Neumáticos', 'Neumático de auxilio - Estado', 'pass_fail', 1, True),
|
||||||
|
|
||||||
|
# SISTEMA ELÉCTRICO
|
||||||
|
('Sistema Eléctrico', 'Batería - Estado', 'good_bad', 2, True),
|
||||||
|
('Sistema Eléctrico', 'Luces de posición delanteras', 'pass_fail', 1, False),
|
||||||
|
('Sistema Eléctrico', 'Luces bajas', 'pass_fail', 1, False),
|
||||||
|
('Sistema Eléctrico', 'Luces altas', 'pass_fail', 1, False),
|
||||||
|
('Sistema Eléctrico', 'Luces de giro delanteras', 'pass_fail', 1, False),
|
||||||
|
('Sistema Eléctrico', 'Luces de freno', 'pass_fail', 2, False),
|
||||||
|
('Sistema Eléctrico', 'Luces de retroceso', 'pass_fail', 1, False),
|
||||||
|
('Sistema Eléctrico', 'Luces traseras', 'pass_fail', 1, False),
|
||||||
|
('Sistema Eléctrico', 'Luces de giro traseras', 'pass_fail', 1, False),
|
||||||
|
('Sistema Eléctrico', 'Luz de patente', 'pass_fail', 1, False),
|
||||||
|
('Sistema Eléctrico', 'Luz interior', 'pass_fail', 1, False),
|
||||||
|
('Sistema Eléctrico', 'Bocina', 'pass_fail', 1, False),
|
||||||
|
('Sistema Eléctrico', 'Testigos en tablero', 'pass_fail', 1, True),
|
||||||
|
|
||||||
|
# MOTOR
|
||||||
|
('Motor', 'Nivel de aceite', 'pass_fail', 2, True),
|
||||||
|
('Motor', 'Fugas de aceite', 'pass_fail', 2, True),
|
||||||
|
('Motor', 'Filtro de aceite', 'status', 1, True),
|
||||||
|
('Motor', 'Nivel de refrigerante', 'pass_fail', 1, True),
|
||||||
|
('Motor', 'Fugas de refrigerante', 'pass_fail', 2, True),
|
||||||
|
('Motor', 'Estado de mangueras', 'pass_fail', 1, True),
|
||||||
|
('Motor', 'Filtro de aire', 'status', 1, True),
|
||||||
|
('Motor', 'Correa de distribución', 'pass_fail', 2, True),
|
||||||
|
('Motor', 'Correas auxiliares', 'pass_fail', 1, True),
|
||||||
|
|
||||||
|
# FRENOS
|
||||||
|
('Frenos', 'Pastillas/zapatas delanteras', 'good_bad', 2, True),
|
||||||
|
('Frenos', 'Discos/tambores delanteros', 'good_bad', 2, True),
|
||||||
|
('Frenos', 'Pastillas/zapatas traseras', 'good_bad', 2, True),
|
||||||
|
('Frenos', 'Discos/tambores traseros', 'good_bad', 2, True),
|
||||||
|
('Frenos', 'Nivel de líquido de frenos', 'pass_fail', 2, False),
|
||||||
|
('Frenos', 'Porcentaje de humedad en líquido', 'numeric', 1, False),
|
||||||
|
('Frenos', 'Freno de mano', 'pass_fail', 2, False),
|
||||||
|
|
||||||
|
# SUSPENSIÓN Y DIRECCIÓN
|
||||||
|
('Suspensión', 'Amortiguadores delanteros', 'pass_fail', 2, True),
|
||||||
|
('Suspensión', 'Amortiguadores traseros', 'pass_fail', 2, True),
|
||||||
|
('Suspensión', 'Cojinetes de ruedas', 'pass_fail', 2, False),
|
||||||
|
('Suspensión', 'Rótulas', 'pass_fail', 2, True),
|
||||||
|
('Suspensión', 'Bieletas', 'pass_fail', 1, True),
|
||||||
|
('Suspensión', 'Bujes', 'pass_fail', 1, True),
|
||||||
|
('Dirección', 'Nivel de líquido de dirección', 'pass_fail', 1, True),
|
||||||
|
('Dirección', 'Estado de cremallera/caja', 'pass_fail', 2, True),
|
||||||
|
('Dirección', 'Fugas en dirección', 'pass_fail', 2, True),
|
||||||
|
|
||||||
|
# INTERIOR
|
||||||
|
('Interior', 'Tablero de instrumentos', 'pass_fail', 1, True),
|
||||||
|
('Interior', 'Funcionamiento de velocímetro', 'pass_fail', 1, False),
|
||||||
|
('Interior', 'Funcionamiento de cuentakilómetros', 'pass_fail', 1, False),
|
||||||
|
('Interior', 'Cinturones de seguridad delanteros', 'pass_fail', 2, True),
|
||||||
|
('Interior', 'Cinturones de seguridad traseros', 'pass_fail', 2, True),
|
||||||
|
('Interior', 'Asientos', 'pass_fail', 1, True),
|
||||||
|
('Interior', 'Apoyacabezas', 'pass_fail', 1, True),
|
||||||
|
('Interior', 'Aire acondicionado', 'pass_fail', 1, False),
|
||||||
|
('Interior', 'Calefacción', 'pass_fail', 1, False),
|
||||||
|
('Interior', 'Radio/sistema de audio', 'pass_fail', 1, False),
|
||||||
|
('Interior', 'Ceniceros', 'pass_fail', 1, False),
|
||||||
|
('Interior', 'Encendedor', 'pass_fail', 1, False),
|
||||||
|
('Interior', 'Guantera', 'pass_fail', 1, False),
|
||||||
|
('Interior', 'Tapizado', 'good_bad', 1, True),
|
||||||
|
|
||||||
|
# SEGURIDAD
|
||||||
|
('Seguridad', 'Matafuego - Fecha de vencimiento', 'pass_fail', 2, True),
|
||||||
|
('Seguridad', 'Matafuego - Presión', 'pass_fail', 2, True),
|
||||||
|
('Seguridad', 'Balizas triangulares', 'pass_fail', 1, True),
|
||||||
|
('Seguridad', 'Botiquín de primeros auxilios', 'pass_fail', 1, True),
|
||||||
|
('Seguridad', 'Llave de ruedas', 'pass_fail', 1, False),
|
||||||
|
('Seguridad', 'Gato hidráulico', 'pass_fail', 1, False),
|
||||||
|
('Seguridad', 'Chaleco reflectivo', 'pass_fail', 1, False),
|
||||||
|
|
||||||
|
# TRANSMISIÓN
|
||||||
|
('Transmisión', 'Nivel de aceite de transmisión', 'pass_fail', 1, True),
|
||||||
|
('Transmisión', 'Fugas en transmisión', 'pass_fail', 2, True),
|
||||||
|
('Transmisión', 'Estado de embrague', 'pass_fail', 2, False),
|
||||||
|
('Transmisión', 'Funcionamiento de cambios', 'pass_fail', 2, False),
|
||||||
|
|
||||||
|
# ESCAPE
|
||||||
|
('Escape', 'Estado del sistema de escape', 'pass_fail', 1, True),
|
||||||
|
('Escape', 'Fugas en escape', 'pass_fail', 2, True),
|
||||||
|
('Escape', 'Silenciador', 'pass_fail', 1, True),
|
||||||
|
('Escape', 'Catalizador', 'pass_fail', 1, True),
|
||||||
|
|
||||||
|
# PRUEBA DE RUTA
|
||||||
|
('Prueba de Ruta', 'Arranque del motor', 'pass_fail', 2, False),
|
||||||
|
('Prueba de Ruta', 'Ralentí estable', 'pass_fail', 1, False),
|
||||||
|
('Prueba de Ruta', 'Aceleración', 'pass_fail', 2, False),
|
||||||
|
('Prueba de Ruta', 'Frenado', 'pass_fail', 2, False),
|
||||||
|
('Prueba de Ruta', 'Dirección', 'pass_fail', 2, False),
|
||||||
|
('Prueba de Ruta', 'Cambios de marcha', 'pass_fail', 2, False),
|
||||||
|
('Prueba de Ruta', 'Ruidos anormales', 'pass_fail', 2, True),
|
||||||
|
('Prueba de Ruta', 'Vibraciones', 'pass_fail', 1, True),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Agregar todas las preguntas
|
||||||
|
max_score = 0
|
||||||
|
for idx, (section, text, qtype, points, photos) in enumerate(questions_data):
|
||||||
|
question = Question(
|
||||||
|
checklist_id=checklist.id,
|
||||||
|
section=section,
|
||||||
|
text=text,
|
||||||
|
type=qtype,
|
||||||
|
points=points,
|
||||||
|
order=idx + 1,
|
||||||
|
allow_photos=photos,
|
||||||
|
max_photos=3 if photos else 0,
|
||||||
|
requires_comment_on_fail=False
|
||||||
|
)
|
||||||
|
db.add(question)
|
||||||
|
max_score += points
|
||||||
|
|
||||||
|
# Actualizar puntuación máxima del checklist
|
||||||
|
checklist.max_score = max_score
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
print(f'✅ Se agregaron {len(questions_data)} preguntas al checklist')
|
||||||
|
print(f'✅ Puntuación máxima: {max_score} puntos')
|
||||||
|
print(f'\nDesglose por sección:')
|
||||||
|
|
||||||
|
# Contar preguntas por sección
|
||||||
|
from collections import Counter
|
||||||
|
sections = Counter([s for s, _, _, _, _ in questions_data])
|
||||||
|
for section, count in sections.items():
|
||||||
|
section_points = sum(p for s, _, _, p, _ in questions_data if s == section)
|
||||||
|
print(f' - {section}: {count} preguntas ({section_points} puntos)')
|
||||||
|
|
||||||
|
db.close()
|
||||||
@@ -24,7 +24,12 @@ def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
|
|||||||
|
|
||||||
def decode_access_token(token: str):
|
def decode_access_token(token: str):
|
||||||
try:
|
try:
|
||||||
|
print(f"Attempting to decode token: {token[:50]}...") # Debug
|
||||||
|
print(f"Using SECRET_KEY: {settings.SECRET_KEY[:20]}...") # Debug
|
||||||
|
print(f"Using ALGORITHM: {settings.ALGORITHM}") # Debug
|
||||||
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
||||||
|
print(f"Successfully decoded payload: {payload}") # Debug
|
||||||
return payload
|
return payload
|
||||||
except JWTError:
|
except JWTError as e:
|
||||||
|
print(f"JWT decode error: {e}") # Debug
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -34,14 +34,18 @@ def get_current_user(
|
|||||||
):
|
):
|
||||||
token = credentials.credentials
|
token = credentials.credentials
|
||||||
payload = decode_access_token(token)
|
payload = decode_access_token(token)
|
||||||
|
print(f"Token payload: {payload}") # Debug
|
||||||
if payload is None:
|
if payload is None:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail="Token inválido o expirado"
|
detail="Token inválido o expirado"
|
||||||
)
|
)
|
||||||
|
|
||||||
user = db.query(models.User).filter(models.User.id == payload.get("sub")).first()
|
user_id = int(payload.get("sub"))
|
||||||
|
print(f"Looking for user ID: {user_id}") # Debug
|
||||||
|
user = db.query(models.User).filter(models.User.id == user_id).first()
|
||||||
if user is None:
|
if user is None:
|
||||||
|
print(f"User not found with ID: {user_id}") # Debug
|
||||||
raise HTTPException(status_code=404, detail="Usuario no encontrado")
|
raise HTTPException(status_code=404, detail="Usuario no encontrado")
|
||||||
|
|
||||||
return user
|
return user
|
||||||
@@ -79,7 +83,7 @@ def login(user_login: schemas.UserLogin, db: Session = Depends(get_db)):
|
|||||||
detail="Usuario o contraseña incorrectos"
|
detail="Usuario o contraseña incorrectos"
|
||||||
)
|
)
|
||||||
|
|
||||||
access_token = create_access_token(data={"sub": user.id, "role": user.role})
|
access_token = create_access_token(data={"sub": str(user.id), "role": user.role})
|
||||||
return {
|
return {
|
||||||
"access_token": access_token,
|
"access_token": access_token,
|
||||||
"token_type": "bearer",
|
"token_type": "bearer",
|
||||||
@@ -254,7 +258,7 @@ def get_inspection(
|
|||||||
current_user: models.User = Depends(get_current_user)
|
current_user: models.User = Depends(get_current_user)
|
||||||
):
|
):
|
||||||
inspection = db.query(models.Inspection).options(
|
inspection = db.query(models.Inspection).options(
|
||||||
joinedload(models.Inspection.checklist),
|
joinedload(models.Inspection.checklist).joinedload(models.Checklist.questions),
|
||||||
joinedload(models.Inspection.mechanic),
|
joinedload(models.Inspection.mechanic),
|
||||||
joinedload(models.Inspection.answers).joinedload(models.Answer.question),
|
joinedload(models.Inspection.answers).joinedload(models.Answer.question),
|
||||||
joinedload(models.Inspection.answers).joinedload(models.Answer.media_files)
|
joinedload(models.Inspection.answers).joinedload(models.Answer.media_files)
|
||||||
|
|||||||
@@ -167,11 +167,11 @@ class MediaFile(MediaFileBase):
|
|||||||
class ChecklistWithQuestions(Checklist):
|
class ChecklistWithQuestions(Checklist):
|
||||||
questions: List[Question] = []
|
questions: List[Question] = []
|
||||||
|
|
||||||
class InspectionDetail(Inspection):
|
|
||||||
checklist: Checklist
|
|
||||||
mechanic: User
|
|
||||||
answers: List[Answer] = []
|
|
||||||
|
|
||||||
class AnswerWithMedia(Answer):
|
class AnswerWithMedia(Answer):
|
||||||
media_files: List[MediaFile] = []
|
media_files: List[MediaFile] = []
|
||||||
question: Question
|
question: Question
|
||||||
|
|
||||||
|
class InspectionDetail(Inspection):
|
||||||
|
checklist: ChecklistWithQuestions
|
||||||
|
mechanic: User
|
||||||
|
answers: List[AnswerWithMedia] = []
|
||||||
|
|||||||
@@ -1,50 +1,3 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
echo "🚀 Inicializando Sistema de Checklists..."
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Esperar a que PostgreSQL esté listo
|
|
||||||
echo "⏳ Esperando a que PostgreSQL esté listo..."
|
|
||||||
sleep 5
|
|
||||||
|
|
||||||
# Crear usuarios
|
|
||||||
echo "👥 Creando usuarios de prueba..."
|
|
||||||
|
|
||||||
docker-compose exec -T backend python << EOF
|
|
||||||
from app.core.database import SessionLocal
|
|
||||||
from app.models import User
|
|
||||||
from app.core.security import get_password_hash
|
|
||||||
|
|
||||||
db = SessionLocal()
|
|
||||||
|
|
||||||
# Crear admin
|
|
||||||
admin = User(
|
|
||||||
username='admin',
|
|
||||||
password_hash=get_password_hash('admin123'),
|
|
||||||
role='admin',
|
|
||||||
email='admin@taller.com',
|
|
||||||
full_name='Administrador Sistema'
|
|
||||||
)
|
|
||||||
db.add(admin)
|
|
||||||
|
|
||||||
# Crear mecánico
|
|
||||||
mechanic = User(
|
|
||||||
username='mecanico1',
|
|
||||||
password_hash=get_password_hash('mecanico123'),
|
|
||||||
role='mechanic',
|
|
||||||
email='mecanico@taller.com',
|
|
||||||
full_name='Juan Pérez'
|
|
||||||
)
|
|
||||||
db.add(mechanic)
|
|
||||||
|
|
||||||
db.commit()
|
|
||||||
print('✅ Usuarios creados exitosamente')
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Crear checklist de ejemplo
|
|
||||||
echo "📋 Creando checklist de ejemplo..."
|
|
||||||
|
|
||||||
docker-compose exec -T backend python << EOF
|
|
||||||
from app.core.database import SessionLocal
|
from app.core.database import SessionLocal
|
||||||
from app.models import Checklist, Question
|
from app.models import Checklist, Question
|
||||||
|
|
||||||
@@ -117,16 +70,3 @@ db.commit()
|
|||||||
|
|
||||||
print(f'✅ Checklist creado con {len(questions_data)} preguntas')
|
print(f'✅ Checklist creado con {len(questions_data)} preguntas')
|
||||||
print(f'✅ Puntuación máxima: {max_score}')
|
print(f'✅ Puntuación máxima: {max_score}')
|
||||||
EOF
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "✅ Inicialización completada!"
|
|
||||||
echo ""
|
|
||||||
echo "🌐 Accede a la aplicación:"
|
|
||||||
echo " Frontend: http://localhost:5173"
|
|
||||||
echo " API Docs: http://localhost:8000/docs"
|
|
||||||
echo ""
|
|
||||||
echo "👥 Usuarios de prueba:"
|
|
||||||
echo " Admin: admin / admin123"
|
|
||||||
echo " Mecánico: mecanico1 / mecanico123"
|
|
||||||
echo ""
|
|
||||||
@@ -5,8 +5,10 @@ alembic==1.13.1
|
|||||||
psycopg2-binary==2.9.9
|
psycopg2-binary==2.9.9
|
||||||
pydantic==2.5.3
|
pydantic==2.5.3
|
||||||
pydantic-settings==2.1.0
|
pydantic-settings==2.1.0
|
||||||
|
email-validator==2.1.0
|
||||||
python-jose[cryptography]==3.3.0
|
python-jose[cryptography]==3.3.0
|
||||||
passlib[bcrypt]==1.7.4
|
passlib==1.7.4
|
||||||
|
bcrypt==4.0.1
|
||||||
python-multipart==0.0.6
|
python-multipart==0.0.6
|
||||||
openai==1.10.0
|
openai==1.10.0
|
||||||
Pillow==10.2.0
|
Pillow==10.2.0
|
||||||
|
|||||||
72
create_checklist.py
Normal file
72
create_checklist.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
from app.core.database import SessionLocal
|
||||||
|
from app.models import Checklist, Question
|
||||||
|
|
||||||
|
db = SessionLocal()
|
||||||
|
|
||||||
|
# Crear checklist
|
||||||
|
checklist = Checklist(
|
||||||
|
name='Mantenimiento Preventivo',
|
||||||
|
description='Checklist estándar de mantenimiento preventivo',
|
||||||
|
ai_mode='assisted',
|
||||||
|
scoring_enabled=True,
|
||||||
|
is_active=True,
|
||||||
|
created_by=1
|
||||||
|
)
|
||||||
|
db.add(checklist)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(checklist)
|
||||||
|
|
||||||
|
# Crear preguntas por sección
|
||||||
|
questions_data = [
|
||||||
|
# Sistema Eléctrico
|
||||||
|
('Sistema Eléctrico', 'Estado de la batería', 'good_bad', 1, True),
|
||||||
|
('Sistema Eléctrico', 'Bocina', 'pass_fail', 1, False),
|
||||||
|
('Sistema Eléctrico', 'Luces (posición, cruce, carretera)', 'pass_fail', 1, False),
|
||||||
|
('Sistema Eléctrico', 'Testigos en cuadro', 'pass_fail', 1, True),
|
||||||
|
|
||||||
|
# Frenos
|
||||||
|
('Frenos', 'Frenos (pastillas, discos)', 'pass_fail', 2, True),
|
||||||
|
('Frenos', 'Líquido de freno', 'pass_fail', 1, False),
|
||||||
|
('Frenos', 'Porcentaje de humedad', 'numeric', 1, False),
|
||||||
|
|
||||||
|
# Motor
|
||||||
|
('Motor', 'Nivel de aceite', 'pass_fail', 1, True),
|
||||||
|
('Motor', 'Fugas de aceite', 'pass_fail', 2, True),
|
||||||
|
('Motor', 'Filtro de aceite', 'status', 1, True),
|
||||||
|
('Motor', 'Fugas de refrigerante', 'pass_fail', 2, True),
|
||||||
|
|
||||||
|
# Neumáticos
|
||||||
|
('Neumáticos', 'Presión neumáticos', 'pass_fail', 1, False),
|
||||||
|
('Neumáticos', 'Banda de rodadura', 'good_bad', 1, True),
|
||||||
|
|
||||||
|
# Suspensión
|
||||||
|
('Suspensión', 'Amortiguadores', 'pass_fail', 2, True),
|
||||||
|
('Suspensión', 'Cojinetes de ruedas', 'pass_fail', 1, False),
|
||||||
|
|
||||||
|
# Varios
|
||||||
|
('Exterior', 'Estado carrocería', 'good_bad', 1, True),
|
||||||
|
('Exterior', 'Escobillas limpiaparabrisas', 'pass_fail', 1, True),
|
||||||
|
('Interior', 'Aire acondicionado', 'pass_fail', 1, False),
|
||||||
|
('Pruebas', 'Prueba dinámica del vehículo', 'pass_fail', 2, False),
|
||||||
|
]
|
||||||
|
|
||||||
|
max_score = 0
|
||||||
|
for idx, (section, text, qtype, points, photos) in enumerate(questions_data):
|
||||||
|
question = Question(
|
||||||
|
checklist_id=checklist.id,
|
||||||
|
section=section,
|
||||||
|
text=text,
|
||||||
|
type=qtype,
|
||||||
|
points=points,
|
||||||
|
order=idx + 1,
|
||||||
|
allow_photos=photos,
|
||||||
|
max_photos=3
|
||||||
|
)
|
||||||
|
db.add(question)
|
||||||
|
max_score += points
|
||||||
|
|
||||||
|
checklist.max_score = max_score
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
print(f'✅ Checklist creado con {len(questions_data)} preguntas')
|
||||||
|
print(f'✅ Puntuación máxima: {max_score}')
|
||||||
@@ -23,7 +23,7 @@ services:
|
|||||||
container_name: checklist-backend
|
container_name: checklist-backend
|
||||||
environment:
|
environment:
|
||||||
DATABASE_URL: postgresql://checklist_user:checklist_pass_2024@postgres:5432/checklist_db
|
DATABASE_URL: postgresql://checklist_user:checklist_pass_2024@postgres:5432/checklist_db
|
||||||
SECRET_KEY: your-secret-key-change-in-production-min-32-chars
|
SECRET_KEY: ${SECRET_KEY:-your-secret-key-change-in-production-min-32-chars}
|
||||||
OPENAI_API_KEY: ${OPENAI_API_KEY}
|
OPENAI_API_KEY: ${OPENAI_API_KEY}
|
||||||
ENVIRONMENT: development
|
ENVIRONMENT: development
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
1280
frontend/src/App.jsx
1280
frontend/src/App.jsx
File diff suppressed because it is too large
Load Diff
111
frontend/src/Sidebar.jsx
Normal file
111
frontend/src/Sidebar.jsx
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
export default function Sidebar({ user, activeTab, setActiveTab, sidebarOpen, setSidebarOpen, onLogout }) {
|
||||||
|
return (
|
||||||
|
<aside className={`bg-gray-900 text-white transition-all duration-300 ${sidebarOpen ? 'w-64' : 'w-16'} flex flex-col fixed h-full z-10`}>
|
||||||
|
{/* Sidebar Header */}
|
||||||
|
<div className={`p-4 flex items-center ${sidebarOpen ? 'justify-between' : 'justify-center'} border-b border-gray-700`}>
|
||||||
|
{sidebarOpen && <h2 className="text-xl font-bold">Sistema</h2>}
|
||||||
|
<button
|
||||||
|
onClick={() => setSidebarOpen(!sidebarOpen)}
|
||||||
|
className="p-2 rounded-lg hover:bg-gray-800 transition"
|
||||||
|
title={sidebarOpen ? 'Ocultar sidebar' : 'Mostrar sidebar'}
|
||||||
|
>
|
||||||
|
{sidebarOpen ? '☰' : '☰'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Navigation */}
|
||||||
|
<nav className="flex-1 p-2">
|
||||||
|
<ul className="space-y-2">
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('checklists')}
|
||||||
|
className={`w-full flex items-center ${sidebarOpen ? 'gap-3 px-4' : 'justify-center px-2'} py-3 rounded-lg transition ${
|
||||||
|
activeTab === 'checklists'
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'text-gray-300 hover:bg-gray-800'
|
||||||
|
}`}
|
||||||
|
title={!sidebarOpen ? 'Checklists' : ''}
|
||||||
|
>
|
||||||
|
<span className="text-xl">📋</span>
|
||||||
|
{sidebarOpen && <span>Checklists</span>}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('inspections')}
|
||||||
|
className={`w-full flex items-center ${sidebarOpen ? 'gap-3 px-4' : 'justify-center px-2'} py-3 rounded-lg transition ${
|
||||||
|
activeTab === 'inspections'
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'text-gray-300 hover:bg-gray-800'
|
||||||
|
}`}
|
||||||
|
title={!sidebarOpen ? 'Inspecciones' : ''}
|
||||||
|
>
|
||||||
|
<span className="text-xl">🔍</span>
|
||||||
|
{sidebarOpen && <span>Inspecciones</span>}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{user.role === 'admin' && (
|
||||||
|
<>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('users')}
|
||||||
|
className={`w-full flex items-center ${sidebarOpen ? 'gap-3 px-4' : 'justify-center px-2'} py-3 rounded-lg transition ${
|
||||||
|
activeTab === 'users'
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'text-gray-300 hover:bg-gray-800'
|
||||||
|
}`}
|
||||||
|
title={!sidebarOpen ? 'Usuarios' : ''}
|
||||||
|
>
|
||||||
|
<span className="text-xl">👥</span>
|
||||||
|
{sidebarOpen && <span>Usuarios</span>}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('reports')}
|
||||||
|
className={`w-full flex items-center ${sidebarOpen ? 'gap-3 px-4' : 'justify-center px-2'} py-3 rounded-lg transition ${
|
||||||
|
activeTab === 'reports'
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'text-gray-300 hover:bg-gray-800'
|
||||||
|
}`}
|
||||||
|
title={!sidebarOpen ? 'Reportes' : ''}
|
||||||
|
>
|
||||||
|
<span className="text-xl">📊</span>
|
||||||
|
{sidebarOpen && <span>Reportes</span>}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* User Info */}
|
||||||
|
<div className="p-4 border-t border-gray-700">
|
||||||
|
<div className={`flex items-center gap-3 ${!sidebarOpen && 'justify-center'}`}>
|
||||||
|
<div className="w-10 h-10 bg-blue-600 rounded-full flex items-center justify-center text-white font-bold flex-shrink-0">
|
||||||
|
{user.username.charAt(0).toUpperCase()}
|
||||||
|
</div>
|
||||||
|
{sidebarOpen && (
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm font-medium truncate">{user.full_name || user.username}</p>
|
||||||
|
<p className="text-xs text-gray-400">{user.role === 'admin' ? 'Admin' : 'Mecánico'}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={onLogout}
|
||||||
|
className={`mt-3 w-full px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition flex items-center justify-center gap-2`}
|
||||||
|
title={!sidebarOpen ? 'Cerrar Sesión' : ''}
|
||||||
|
>
|
||||||
|
{sidebarOpen ? (
|
||||||
|
<>
|
||||||
|
<span>Cerrar Sesión</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span className="text-lg">🚪</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user