Nuevo Commit

This commit is contained in:
2025-11-18 16:46:20 -03:00
parent be30b3ca18
commit 443de4ec0e
12 changed files with 1969 additions and 206 deletions

131
GUIA_MECANICO.md Normal file
View 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
View 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
View 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()

View File

@@ -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

View File

@@ -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)

View File

@@ -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] = []

View File

@@ -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 ""

View File

@@ -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
View 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}')

View File

@@ -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:

File diff suppressed because it is too large Load Diff

111
frontend/src/Sidebar.jsx Normal file
View 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>
)
}