From 443de4ec0eef1d66599a4ffd4088b75506a4995d Mon Sep 17 00:00:00 2001 From: ronalds Date: Tue, 18 Nov 2025 16:46:20 -0300 Subject: [PATCH] Nuevo Commit --- GUIA_MECANICO.md | 131 ++ add_questions.py | 180 +++ backend/add_questions.py | 180 +++ backend/app/core/security.py | 7 +- backend/app/main.py | 10 +- backend/app/schemas.py | 10 +- init-data.sh => backend/create_checklist.py | 60 - backend/requirements.txt | 4 +- create_checklist.py | 72 + docker-compose.yml | 2 +- frontend/src/App.jsx | 1408 +++++++++++++++++-- frontend/src/Sidebar.jsx | 111 ++ 12 files changed, 1969 insertions(+), 206 deletions(-) create mode 100644 GUIA_MECANICO.md create mode 100644 add_questions.py create mode 100644 backend/add_questions.py rename init-data.sh => backend/create_checklist.py (63%) create mode 100644 create_checklist.py create mode 100644 frontend/src/Sidebar.jsx diff --git a/GUIA_MECANICO.md b/GUIA_MECANICO.md new file mode 100644 index 0000000..39d6a34 --- /dev/null +++ b/GUIA_MECANICO.md @@ -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. diff --git a/add_questions.py b/add_questions.py new file mode 100644 index 0000000..f7a7ecc --- /dev/null +++ b/add_questions.py @@ -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() diff --git a/backend/add_questions.py b/backend/add_questions.py new file mode 100644 index 0000000..f7a7ecc --- /dev/null +++ b/backend/add_questions.py @@ -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() diff --git a/backend/app/core/security.py b/backend/app/core/security.py index 8102588..ad0cc33 100644 --- a/backend/app/core/security.py +++ b/backend/app/core/security.py @@ -24,7 +24,12 @@ def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): def decode_access_token(token: str): 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]) + print(f"Successfully decoded payload: {payload}") # Debug return payload - except JWTError: + except JWTError as e: + print(f"JWT decode error: {e}") # Debug return None diff --git a/backend/app/main.py b/backend/app/main.py index 2126ff4..4ef7bbb 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -34,14 +34,18 @@ def get_current_user( ): token = credentials.credentials payload = decode_access_token(token) + print(f"Token payload: {payload}") # Debug if payload is None: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, 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: + print(f"User not found with ID: {user_id}") # Debug raise HTTPException(status_code=404, detail="Usuario no encontrado") return user @@ -79,7 +83,7 @@ def login(user_login: schemas.UserLogin, db: Session = Depends(get_db)): 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 { "access_token": access_token, "token_type": "bearer", @@ -254,7 +258,7 @@ def get_inspection( current_user: models.User = Depends(get_current_user) ): 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.answers).joinedload(models.Answer.question), joinedload(models.Inspection.answers).joinedload(models.Answer.media_files) diff --git a/backend/app/schemas.py b/backend/app/schemas.py index d5c94e4..bd90f1e 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -167,11 +167,11 @@ class MediaFile(MediaFileBase): class ChecklistWithQuestions(Checklist): questions: List[Question] = [] -class InspectionDetail(Inspection): - checklist: Checklist - mechanic: User - answers: List[Answer] = [] - class AnswerWithMedia(Answer): media_files: List[MediaFile] = [] question: Question + +class InspectionDetail(Inspection): + checklist: ChecklistWithQuestions + mechanic: User + answers: List[AnswerWithMedia] = [] diff --git a/init-data.sh b/backend/create_checklist.py similarity index 63% rename from init-data.sh rename to backend/create_checklist.py index bb34abb..fb558d4 100644 --- a/init-data.sh +++ b/backend/create_checklist.py @@ -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.models import Checklist, Question @@ -117,16 +70,3 @@ db.commit() print(f'✅ Checklist creado con {len(questions_data)} preguntas') 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 "" diff --git a/backend/requirements.txt b/backend/requirements.txt index 77681ae..8dd08ef 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -5,8 +5,10 @@ alembic==1.13.1 psycopg2-binary==2.9.9 pydantic==2.5.3 pydantic-settings==2.1.0 +email-validator==2.1.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 openai==1.10.0 Pillow==10.2.0 diff --git a/create_checklist.py b/create_checklist.py new file mode 100644 index 0000000..fb558d4 --- /dev/null +++ b/create_checklist.py @@ -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}') diff --git a/docker-compose.yml b/docker-compose.yml index 9b8be51..6db77ca 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -23,7 +23,7 @@ services: container_name: checklist-backend environment: 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} ENVIRONMENT: development ports: diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 72dfb19..424c9e5 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,5 +1,7 @@ import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom' -import { useState, useEffect } from 'react' +import { useState, useEffect, useRef } from 'react' +import SignatureCanvas from 'react-signature-canvas' +import Sidebar from './Sidebar' function App() { const [user, setUser] = useState(null) @@ -65,6 +67,8 @@ function LoginPage({ setUser }) { throw new Error(data.detail || 'Error al iniciar sesión') } + console.log('Login successful, token:', data.access_token.substring(0, 20) + '...') + // Guardar token y usuario localStorage.setItem('token', data.access_token) localStorage.setItem('user', JSON.stringify(data.user)) @@ -146,6 +150,8 @@ function DashboardPage({ user, setUser }) { const [inspections, setInspections] = useState([]) const [loading, setLoading] = useState(true) const [activeTab, setActiveTab] = useState('checklists') + const [activeInspection, setActiveInspection] = useState(null) + const [sidebarOpen, setSidebarOpen] = useState(true) useEffect(() => { loadData() @@ -156,14 +162,25 @@ function DashboardPage({ user, setUser }) { const token = localStorage.getItem('token') const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000' + console.log('Token:', token ? 'exists' : 'missing') + // Cargar checklists const checklistsRes = await fetch(`${API_URL}/api/checklists?active_only=true`, { headers: { 'Authorization': `Bearer ${token}`, }, }) - const checklistsData = await checklistsRes.json() - setChecklists(checklistsData) + + console.log('Checklists response:', checklistsRes.status) + + if (checklistsRes.ok) { + const checklistsData = await checklistsRes.json() + console.log('Checklists data:', checklistsData) + setChecklists(Array.isArray(checklistsData) ? checklistsData : []) + } else { + console.error('Error loading checklists:', checklistsRes.status) + setChecklists([]) + } // Cargar inspecciones const inspectionsRes = await fetch(`${API_URL}/api/inspections?limit=10`, { @@ -171,11 +188,22 @@ function DashboardPage({ user, setUser }) { 'Authorization': `Bearer ${token}`, }, }) - const inspectionsData = await inspectionsRes.json() - setInspections(inspectionsData) + + console.log('Inspections response:', inspectionsRes.status) + + if (inspectionsRes.ok) { + const inspectionsData = await inspectionsRes.json() + console.log('Inspections data:', inspectionsData) + setInspections(Array.isArray(inspectionsData) ? inspectionsData : []) + } else { + console.error('Error loading inspections:', inspectionsRes.status) + setInspections([]) + } } catch (error) { console.error('Error loading data:', error) + setChecklists([]) + setInspections([]) } finally { setLoading(false) } @@ -188,124 +216,554 @@ function DashboardPage({ user, setUser }) { } return ( -
- {/* Header */} -
-
-
-
-

Checklist Inteligente

-

- Bienvenido, {user.full_name || user.username} ({user.role === 'admin' ? 'Administrador' : 'Mecánico'}) -

+
+ {/* Sidebar */} + + + {/* Main Content */} +
+ {/* Header */} +
+
+

+ {activeTab === 'checklists' && 'Checklists'} + {activeTab === 'inspections' && 'Inspecciones'} + {activeTab === 'users' && 'Usuarios'} + {activeTab === 'reports' && 'Reportes'} +

+
+
+ + {/* Content */} +
+
+
+ {loading ? ( +
+
Cargando datos...
+
+ ) : activeTab === 'checklists' ? ( + + ) : activeTab === 'inspections' ? ( + + ) : activeTab === 'users' ? ( +
+
👥
+
Módulo de Usuarios en desarrollo...
+
+ ) : activeTab === 'reports' ? ( +
+
📊
+
Módulo de Reportes en desarrollo...
+
+ ) : null}
-
-
- {/* Tabs */} -
-
-
- -
- -
- {loading ? ( -
-
Cargando datos...
-
- ) : activeTab === 'checklists' ? ( - - ) : ( - - )} -
-
-
- - {/* Info Card */} -
-
-

📘 Información del MVP

-

- Este es el MVP funcional del sistema de checklists inteligentes. - El frontend completo con todas las funcionalidades se encuentra en desarrollo. - Puedes probar la API completa en: http://localhost:8000/docs -

-
+ {/* Modal de Inspección Activa */} + {activeInspection && ( + setActiveInspection(null)} + onComplete={() => { + setActiveInspection(null) + loadData() + }} + /> + )}
) } -function ChecklistsTab({ checklists, user }) { - if (checklists.length === 0) { - return ( -
-

No hay checklists activos

-

Ejecuta ./init-data.sh para crear datos de ejemplo

-
- ) +function ChecklistsTab({ checklists, user, onChecklistCreated, onStartInspection }) { + const [showCreateModal, setShowCreateModal] = useState(false) + const [creating, setCreating] = useState(false) + const [formData, setFormData] = useState({ + name: '', + description: '', + ai_mode: 'off', + scoring_enabled: true + }) + + const handleCreate = async (e) => { + e.preventDefault() + setCreating(true) + + try { + const token = localStorage.getItem('token') + const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000' + + const response = await fetch(`${API_URL}/api/checklists`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(formData), + }) + + if (response.ok) { + setShowCreateModal(false) + setFormData({ name: '', description: '', ai_mode: 'off', scoring_enabled: true }) + onChecklistCreated() + } else { + alert('Error al crear checklist') + } + } catch (error) { + console.error('Error:', error) + alert('Error al crear checklist') + } finally { + setCreating(false) + } } return (
- {checklists.map((checklist) => ( -
-
-
-

{checklist.name}

-

{checklist.description}

-
- - Puntuación máxima: {checklist.max_score} - - - Modo IA: {checklist.ai_mode} - + {user.role === 'admin' && ( +
+ +
+ )} + + {checklists.length === 0 ? ( +
+

No hay checklists activos

+ {user.role === 'admin' && ( + + )} +
+ ) : ( + checklists.map((checklist) => ( +
+
+
+

{checklist.name}

+

{checklist.description}

+
+ + Puntuación máxima: {checklist.max_score} + + + Modo IA: {checklist.ai_mode} + +
+ {user.role === 'mechanic' && ( + + )} +
+
+ )) + )} + + {/* Modal Crear Checklist */} + {showCreateModal && ( +
+
+
+

Crear Nuevo Checklist

+ +
+
+ + setFormData({ ...formData, name: e.target.value })} + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + placeholder="Ej: Mantenimiento Preventivo" + required + /> +
+ +
+ +