Compare commits

124 Commits

Author SHA1 Message Date
49d3ef9db1 Frontend v1.3.5:
CORREGIDO: Validación de fotos obligatorias ahora usa photo_requirement === 'required' en lugar de allow_photos
Las preguntas con photo_requirement configurado como 'none' o 'optional' ya NO exigirán fotos obligatorias
Aplica para ambos botones: "Siguiente →" y "Finalizar Inspección"
Problema corregido:
Antes: La validación usaba currentQuestion.allow_photos (campo antiguo booleano)
Ahora: Usa currentQuestion.photo_requirement === 'required' (nuevo sistema de 3 estados)
2025-12-05 06:19:28 -03:00
7111550fb7 Cambios realizados v1.3.4 + v1.2.9:
Backend v1.2.9:

Mejorada la estructura del system prompt para evitar que la IA repita literalmente las instrucciones
Agregada instrucción explícita: "NUNCA repitas las instrucciones del sistema como respuesta"
Separación clara entre "ROL Y COMPORTAMIENTO" y "REGLAS DE RESPUESTA"
Frontend v1.3.4:

CORREGIDO: Las imágenes del chat ahora usan URLs de S3 permanentes en lugar de blob URLs temporales
El mensaje del usuario se agrega DESPUÉS de recibir la respuesta del servidor con las URLs de S3
Los blob URLs se liberan correctamente después de obtener las URLs permanentes
Ahora las imágenes se pueden visualizar en tamaño completo (lightbox) sin errores ERR_FILE_NOT_FOUND
Cómo funciona ahora:

Usuario adjunta imagen y envía mensaje
Inputs se limpian inmediatamente (mejor UX)
Imagen se sube a S3 en el backend
Backend retorna URLs de S3 en attached_files
Mensaje del usuario se crea con URLs de S3 (no blobs)
Ambos mensajes (usuario + asistente) se agregan juntos
Las imágenes persisten después de refrescar la página
El lightbox funciona correctamente con URLs permanentes
2025-12-05 06:11:46 -03:00
f73319046e Cambios realizados v1.2.9:
Mejoré la estructura del system prompt para evitar que la IA repita literalmente las instrucciones del
2025-12-05 05:58:08 -03:00
e9a184f087 Backend v1.2.8
Fix aplicado:

Corregido el acceso a las variables de configuración de MinIO/S3:
2025-12-05 05:48:53 -03:00
954c5b4a7b Frontend v1.3.3
Fix aplicado:

Agregado optional chaining (?.) para prevenir crash cuando inspection.checklist.questions es undefined:
2025-12-05 05:42:49 -03:00
6455d351dd Frontend v1.3.1
Fix aplicado:

Ahora cuando envías una imagen en el chat:

Se sube al servidor (S3)
El servidor devuelve la URL permanente en attached_files
El frontend actualiza el mensaje del usuario reemplazando la blob URL con la URL de S3
Se libera la blob URL antigua para evitar memory leaks
2025-12-05 05:38:05 -03:00
ae3a50054a Backend v1.2.7 / Frontend v1.3.0
Problema de blob URLs solucionado:

Cambios implementados:
Backend:

Las imágenes del chat ahora se suben a S3 automáticamente
Se genera una URL permanente para cada imagen
La respuesta incluye url para cada archivo adjunto
Frontend:

Las imágenes en mensajes del asistente usan URLs de S3 (permanentes)
Ya no depende de blob URLs que desaparecen al refrescar
Las imágenes del usuario siguen usando blob (temporal solo para preview)
Las imágenes del asistente se muestran con URLs persistentes
Resultado:

 Las imágenes del chat ahora funcionan después de refrescar
 Las URLs son permanentes y accesibles
 No más errores 404 con blob: URLs
 Las imágenes quedan guardadas en S3 para historial
2025-12-04 16:47:00 -03:00
387897acfc Problema solucionado - Backend v1.2.6 / Frontend v1.2.8
Cambios implementados:

Frontend:
Ahora envía context_answers con todas las respuestas de preguntas anteriores (texto + observaciones)
Incluye el texto de la pregunta para mejor contexto
Funciona con la configuración de IDs de preguntas o todas las anteriores
Backend:
Recibe y procesa context_answers
Agrega sección "RESPUESTAS DE PREGUNTAS ANTERIORES" al system prompt
Muestra pregunta, respuesta y observaciones de cada pregunta previa
2025-12-04 14:35:58 -03:00
56decba945 Backend v1.2.5
Cambios implementados:

Streaming habilitado para OpenAI: Ahora usa stream=True en las llamadas al chat
Procesamiento en tiempo real: El servidor recibe chunks de la respuesta y los concatena
Mejor experiencia: Las respuestas largas se generan más rápido (el servidor empieza a recibir antes)
Cómo funciona:
2025-12-04 14:25:16 -03:00
65a74cf754 Veo el problema. La versión de openai instalada (1.10.0) es muy antigua. La documentación que compartiste es de una versión mucho más nueva que usa responses.create() y streaming diferente.
El error de 'proxies' viene de un conflicto entre versiones de httpx (cliente HTTP interno de OpenAI).

Solución: Actualizar openai a la última versión
2025-12-04 14:24:04 -03:00
289b4b6b93 Backend v1.2.4
Problema solucionado:

Actualizada toda la API de OpenAI de la versión antigua (openai.ChatCompletion.create) a la nueva (client.chat.completions.create)
El error 'proxies' ocurría porque la versión antigua intentaba usar parámetros incompatibles
Ahora funciona con openai>=1.0.0 que es la versión instalada (1.10.0)
Cambios aplicados:

Análisis de imágenes/PDF en inspecciones
Chat del asistente (ya estaba actualizado)
Análisis automático de fotos
2025-12-04 14:12:01 -03:00
a1ab955556 Backend v1.2.3
Cambio implementado:

Ahora el prompt del asistente configurado en la pregunta es el system prompt completo. El sistema solo agrega:

Contexto automático: Vehículo, OR, pregunta actual
Instrucciones técnicas básicas: No inventar datos, pedir valores técnicos cuando falten
Longitud de respuesta: Según configuración
Beneficios:

 Tu prompt de "Omar" funciona exactamente como lo escribiste
 Mantienes control total del comportamiento del asistente
 El sistema solo complementa con contexto, no reemplaza tu prompt
 Más flexible para diferentes tipos de asistentes (diagnóstico, checklist, inspección, etc.)
2025-12-04 14:05:32 -03:00
14d5027170 Solucionado
El error ocurría porque faltaba el caso de Anthropic en la función de análisis de imágenes. Ahora Anthropic Claude puede:

Analizar imágenes con vision
Analizar documentos PDF
Usar el formato correcto de mensajes con system separado
Backend actualizado a v1.2.2
2025-12-04 14:01:58 -03:00
a8afaa044f Frontend actualizado a v1.2.7
Cambios en esta versión:

Fix: Selección visual de modelos ahora se muestra inmediatamente al hacer click en un proveedor
Los modelos se muestran seleccionados en el formulario antes de guardar la configuración
2025-12-04 11:58:45 -03:00
7f2e9add29 Resumen de Cambios Implementados
Backend v1.2.1
Mejoras en gestión de API keys multi-proveedor:

Nuevo endpoint /api/ai/api-keys: Retorna todas las API keys guardadas por proveedor (enmascaradas para seguridad)

Formato: {"openai": {"has_key": true, "masked_key": "sk-proj...xyz", "is_active": false}}
Solo administradores pueden acceder
Endpoint /api/ai/configuration mejorado:

Ahora preserva API keys existentes cuando se cambia de proveedor
Si ya existe configuración para un proveedor, solo actualiza el modelo y activa ese proveedor
Solo requiere API key nueva si no existe configuración previa para ese proveedor
Validación: no acepta API keys vacías para nuevos proveedores
Persistencia de configuraciones:

Cada proveedor (OpenAI, Anthropic, Gemini) mantiene su registro en la base de datos
Solo uno tiene is_active=True a la vez
Al cambiar de proveedor, se desactiva el anterior pero NO se elimina
Frontend v1.2.6
UX mejorada para configuración de IA:

Indicadores visuales en botones de proveedor:

Badge "✓ ACTIVO" en verde para el proveedor actualmente activo
Badge "Configurado" en gris para proveedores con API key guardada pero inactivos
Sin badges para proveedores no configurados
Selector de modelos inteligente:

Solo muestra modelo seleccionado si el proveedor está activo
Al hacer click en un proveedor inactivo, NO se pre-selecciona ningún modelo
Solo al GUARDAR se activa el proveedor con el modelo seleccionado
Input de API key con contexto:

Muestra key enmascarada si ya existe: ✓ Ya tienes una API key guardada: sk-proj...xyz
Permite dejar vacío para mantener la key actual
Solo requiere key nueva si el proveedor no tiene una guardada
Flujo de trabajo mejorado:

Click en proveedor → Cambia tab de formulario
Si ya tiene key guardada → Se muestra enmascarada, puede mantenerla
Seleccionar modelo → Click en "Guardar Configuración"
Solo entonces se ACTIVA ese proveedor y modelo
Beneficios
No re-ingresar API keys: Al cambiar entre proveedores, las keys se preservan
Claridad visual: Solo el proveedor activo muestra badge verde y modelo seleccionado
Seguridad: API keys enmascaradas en la UI (sk-proj...xyz)
Flexibilidad: Configurar los 3 proveedores y cambiar entre ellos sin perder configuración
Versiones actualizadas:

Backend: 1.2.0 → 1.2.1
Frontend: 1.2.5 → 1.2.6
Service Worker: cache v1.2.6
2025-12-04 11:52:38 -03:00
24eb039302 IMPLEMENTADO - Soporte para Anthropic Claude y Corrección de Configuración
🔧 Backend v1.2.0 | Frontend v1.2.5
He implementado completamente el soporte para Anthropic Claude y corregido el problema de visualización en el módulo de configuración:

🎯 Cambios Implementados:
1. Backend - Soporte para Anthropic Claude
Nuevos modelos agregados:

Claude Sonnet 4.5 (Recomendado) - Equilibrio perfecto
Claude Opus 4.5 - Máxima capacidad
Claude Haiku 4.5 - Ultra rápido y económico
Integración completa:

Chat Assistant con Anthropic
Generación de resúmenes PDF con Anthropic
Manejo correcto de formato de mensajes (system separado)
Configuración por defecto: claude-sonnet-4.5
2025-12-04 11:38:48 -03:00
9de059e9ca v1.1.2 - Fix campo photo_requirement en schemas
BACKEND:
- Agregado campo photo_requirement a QuestionBase schema
- Ahora acepta valores: 'none', 'optional', 'required'
- Permite crear/actualizar preguntas con configuración de adjuntos
- Marcado allow_photos como DEPRECATED (mantener por compatibilidad)
- Backend v1.1.2

FRONTEND:
- Sin cambios (v1.2.4) - ya enviaba correctamente el campo
2025-12-04 11:30:22 -03:00
9ed41c9ee4 v1.2.4 - Lightbox para visualizar imágenes en Chat Assistant
FRONTEND:
- Implementado visor de imágenes (lightbox) dentro del modal de chat
- Click en imagen abre pantalla completa en lugar de nueva pestaña
- Fondo negro translúcido (95%) con z-index 200
- Botón de cerrar (×) en esquina superior derecha
- Click en imagen/fondo cierra el lightbox
- Etiqueta flotante con nombre de archivo
- Cursor zoom-in en miniaturas para indicar ampliación
- Responsive: max 95vw/95vh con scroll automático
- Frontend v1.2.4

BACKEND:
- Sin cambios (v1.1.1)
2025-12-04 11:28:03 -03:00
b191030321 v1.1.1 - Fix error OpenAI Client proxies argument
BACKEND:
- Corregido error "Client.init() got unexpected argument 'proxies'"
- Cambio de import: from openai import OpenAI → import openai
- Instanciación explícita: openai.OpenAI(api_key=...)
- Compatible con versiones recientes de librería openai (>= 1.0.0)
- Backend v1.1.1

FRONTEND:
- Sin cambios (v1.2.3)
2025-12-04 11:15:48 -03:00
023a004c53 IMPLEMENTADO - Previsualización de Imágenes en Chat Assistant
📸 Frontend actualizado a v1.2.3
He implementado un sistema completo de previsualización de imágenes en el chat assistant:

🎨 Características Implementadas:
1. Preview Antes de Enviar (Zona de Input)
Miniaturas 20x20px con superposición de nombre
Botón de eliminar en esquina superior derecha (rojo con X)
Fondo oscuro translúcido para nombre del archivo
Hover effects para mejor UX

// Vista previa antes de enviar:
┌─────────────────────────────┐
│  [IMG]  [IMG]  📄 file.pdf  │  ← Miniaturas clickeables
│   ✕      ✕       ✕          │
└─────────────────────────────┘

2. Imágenes en Mensajes del Chat
Renderizado completo de imágenes en mensajes del usuario
Máximo 256px de altura (responsive)
Click para abrir en nueva pestaña (full size)
Metadata bajo la imagen (nombre + tamaño)
Esquina redondeada para mejor diseño
Transición hover (opacity 90%)

// Mensaje del usuario con imagen:
┌────────────────────────────┐
│ [Texto del mensaje]        │
│                            │
│ ┌────────────────────────┐ │
│ │                        │ │
│ │    [IMAGEN PREVIEW]    │ │ ← Click para ampliar
│ │                        │ │
│ └────────────────────────┘ │
│ 🖼️ photo.jpg (128.5 KB)   │
│                            │
│ 10:45                      │
└────────────────────────────┘

3. Gestión de Memoria
URLs temporales con URL.createObjectURL()
Limpieza automática al eliminar archivo
useEffect cleanup al desmontar modal
No memory leaks garantizados
2025-12-04 11:03:19 -03:00
59a0f56b99 IMPLEMENTACIÓN COMPLETADA - Informes Personalizados para Chat Assistant
📊 Backend actualizado a v1.1.0
He implementado un sistema inteligente de generación de informes para preguntas con chat assistant:

🔧 Cambios Implementados:
1. Nueva función generate_chat_summary() (líneas ~1450)
Funcionalidad:

Recibe el chat_history completo de una conversación
Usa OpenAI o Gemini (según configuración activa) para analizar la conversación
Genera un resumen estructurado en JSON con:
problema_identificado: Descripción del problema principal
hallazgos: Lista de observaciones técnicas
diagnostico: Conclusión del diagnóstico
recomendaciones: Pasos sugeridos
Características:

Temperature: 0.3 (respuestas consistentes)
Max tokens: 800
Response format: JSON
Manejo robusto de errores con fallback
2025-12-04 10:56:00 -03:00
3bf8b44581 Problema solucionado
El error 422 ocurría porque:

Al continuar una inspección existente, las fotos se cargan como URLs de string
El código intentaba subirlas de nuevo como si fueran archivos File
El backend rechazaba la petición porque no recibía un archivo válido → 422 Unprocessable Entity
La solución:

Verificar si cada elemento en answer.photos es un File o Blob (archivo nuevo)
Solo subir archivos nuevos que aún no están en el servidor
Ignorar URLs de string porque ya están subidas y almacenadas
Ahora al continuar una inspección:

 Las fotos existentes se muestran correctamente (fix anterior)
 No se intentan subir de nuevo (fix actual)
 Solo se suben fotos nuevas que agregues
2025-12-04 09:59:10 -03:00
311d363e31 Versiones actualizadas:
Frontend: v1.2.1 → v1.2.2
Backend: v1.0.97 → v1.0.98
Cambios en v1.2.2 / v1.0.98:

 Fix crítico: Error createObjectURL al continuar inspecciones existentes
 Soporte para fotos como File (nuevas) y URL string (existentes)
2025-12-04 09:19:13 -03:00
d3676172e1 Versiones actualizadas:
Frontend: v1.2.0 → v1.2.1

package.json
Sidebar.jsx
service-worker.js (PWA)
Backend: v1.0.96 → v1.0.97

main.py
Cambios incluidos en v1.2.1 / v1.0.97:

 Paginación de inspecciones (ya estaba implementada con 10 items)
 Limpieza de sesiones de chat al cambiar de pregunta
 Campo "Número de OR" obligatorio
 Eliminación de referencias "IA" → "Asistente Ayutec"
2025-12-04 08:20:02 -03:00
e3524b32d4 Ahora funciona así:
Inspección nueva, Pregunta 1 (Frenos):

Abro chat → aiChatMessages = [] (vacío)
Pregunto sobre frenos
Cierro → se guarda en answers[pregunta1].chatHistory
Misma inspección, Pregunta 2 (Neumáticos):

Abro chat → aiChatMessages = [] (vacío, porque esta pregunta no tiene historial)
Chat limpio, sin mezclar con frenos 
Vuelvo a Pregunta 1:

Abro chat → aiChatMessages = [historial guardado]
Veo mi conversación anterior sobre frenos 
Nueva inspección en otro momento:

Todas las preguntas empiezan con aiChatMessages = []
No se mezcla con inspecciones anteriores 
2025-12-04 07:37:21 -03:00
44cd81956f Validación de Coherencia IA Implementada
Cambios en el Backend (v1.0.96)
Nuevo campo expected_answer en el análisis de IA:

La IA ahora retorna cuál debería ser la respuesta correcta según lo que observa en la imagen
Se incluyen las opciones de respuesta disponibles en el prompt para que la IA elija la correcta
Extracción de opciones de pregunta:

El sistema extrae las opciones disponibles (Buen Estado, Mal Estado, etc.)
Las envía a la IA para que determine cuál es la respuesta esperada
Cambios en el Frontend
Validación antes de continuar:

Cuando el mecánico intenta avanzar a la siguiente pregunta o firmar
El sistema compara su respuesta con expected_answer del análisis de IA
Si NO coinciden, aparece un popup con:
2025-12-03 10:40:33 -03:00
58bf1bfc69 -MOdificar Formato de PDF para informe
se saco el esatdo y porcentaje
2025-12-03 01:21:11 -03:00
50909e4499 Mejoras Implementadas en Extracción de PDFs con IA
He mejorado significativamente el sistema de extracción de texto de PDFs para el análisis con IA. Aquí están los cambios principales:

🎯 Problemas Resueltos
Límites muy pequeños → Aumentados significativamente según capacidad del modelo
No extraía todo el documento → Ahora procesa hasta 100k caracteres
Duplicación de contenido → Detecta y elimina páginas repetidas
Sin información de estado → Reporta páginas procesadas, truncado, etc.
2025-12-03 00:55:11 -03:00
582114a55a -FrontEnd actualizado
*Actualziacion de Logos
2025-12-02 22:42:51 -03:00
c4f5d960de Nueva Funcionalidad: 3 Estados para Adjuntos (Ninguno/Opcional/Obligatorio)
He implementado el sistema de 3 estados para el requisito de fotos/archivos que solicitaste.

Problema Original:
Solo había 2 estados:

 Permitir fotos (checkbox activado)
 No permitir fotos (checkbox desactivado)
Faltaba: Fotos opcionales vs obligatorias

Solución Implementada:
3 Estados disponibles:

🚫 No permitir adjuntos (photo_requirement = 'none')

No se muestra el input de fotos
El mecánico NO puede adjuntar archivos
📎 Opcional (photo_requirement = 'optional')

Se muestra el input de fotos
El mecánico PUEDE adjuntar si lo desea
No es obligatorio para continuar
⚠️ Obligatorio (photo_requirement = 'required')

Se muestra el input de fotos con etiqueta "OBLIGATORIO"
El mecánico DEBE adjuntar al menos 1 archivo
Validación bloquea continuar sin adjuntos
2025-12-02 22:22:51 -03:00
35b419a654 Bug Corregido: Orden Automático de Subpreguntas
He identificado y solucionado el bug que causaba que las subpreguntas recién creadas aparecieran al principio (asociadas a la primera pregunta) hasta que las arrastraras.

Problema Identificado:
Cuando creabas una subpregunta, el backend asignaba order = 0 por defecto, lo que hacía que:

La subpregunta apareciera al principio de la lista
Al arrastrarla, el sistema recalculaba el orden y la colocaba correctamente debajo de su padre
Solución Implementada:
Backend (v1.0.93) - main.py:

Modifiqué el endpoint POST /api/questions para calcular automáticamente el order correcto al crear una pregunta:
2025-12-02 22:03:00 -03:00
fce31467d8 Frontend (v1.0.98):
Formulario mejorado (App.jsx):

Selector de pregunta padre SIEMPRE disponible para cualquier tipo
Selector de condición opcional (solo si el padre es boolean/single_choice)
Indicadores visuales claros:
Verde: "Esta subpregunta aparecerá SIEMPRE"
Azul: " Condición aplicada"
Visualización distinguible:

Badge verde 📎 para subpreguntas siempre visibles
Badge azul  para subpreguntas condicionales
Texto explicativo debajo de cada subpregunta
2025-12-02 17:58:43 -03:00
c6a6ba976e Versiones actualizadas:
Frontend: 1.0.96 → 1.0.97
Service Worker: 1.0.96 → 1.0.97
Backend: 1.0.92 (sin cambios)
Resultado:
 Las preguntas padre se mueven CON todos sus hijos
 Los hijos mantienen su orden relativo al padre
 No hay conflictos de orden entre preguntas
 El sistema usa espaciado inteligente (0, 10, 20...) para evitar colisiones
 Las subpreguntas solo se mueven dentro de su padre
2025-12-02 17:22:55 -03:00
31f5edae84 Mejoras Visuales del Drag-and-Drop
Efectos Visuales Añadidos:
Al iniciar el arrastre:

La pregunta se vuelve semi-transparente (40% opacidad)
Se aplica un ligero zoom out (scale 0.98)
Aparece un borde morado brillante (ring-2 ring-purple-500)
Durante el arrastre sobre una pregunta:

La zona de destino se resalta con fondo morado claro
Borde superior morado grueso (4px)
Sombra pronunciada para destacar la zona
Indicador visual claro: Badge flotante que dice "Se moverá ANTES de esta pregunta" con ícono de cruz
Transiciones suaves:

Todas las transformaciones tienen duration-200 para animaciones fluidas
El elemento arrastrado mantiene su escala reducida durante el movimiento
Al soltar:

Se restauran todos los estilos originales
La pregunta regresa a opacidad 100% y escala normal
2025-12-02 15:57:55 -03:00
de5f09a351 Frontend (v1.0.95)
Ordenamiento consistente de preguntas (App.jsx):

Las preguntas ahora se ordenan por el campo order antes de agruparse por sección
Esto asegura que el orden se mantenga exactamente como está en el backend
Ordenamiento de secciones (App.jsx):

Las secciones se ordenan por el order mínimo de sus preguntas
Garantiza que las secciones aparezcan en orden lógico y consistente
Mejora en drag-and-drop (App.jsx):

Al reordenar, ahora se ordenan las preguntas por order antes de calcular nuevas posiciones
Los nuevos valores de order se asignan correctamente preservando el orden relativo
Funciona correctamente con una sola sección y con subpreguntas
Ordenamiento en modo inspección (App.jsx):

getVisibleQuestions() ahora ordena las preguntas visibles por su campo order
Mantiene el orden correcto durante la ejecución de inspecciones
Backend (v1.0.92)
No se requirieron cambios en el backend (ya estaba ordenando correctamente con order_by(models.Question.order))
2025-12-02 15:50:22 -03:00
7f50bfd8c6 Renderizado Markdown agregado al chat
Cambios:

 Agregada dependencia react-markdown v9.0.1
 Import de ReactMarkdown en App.jsx
 Mensajes del asistente ahora renderizan Markdown
 Mensajes del usuario siguen en texto plano
 Estilos Tailwind prose para formato limpio
 Soporte para: negrita, cursiva, listas, código, encabezados, etc.
Versión: Frontend 1.0.93 → 1.0.94
2025-12-02 15:23:59 -03:00
c374909fa8 Chat AI Assistant con Archivos Adjuntos Implementado
🎯 Nueva Funcionalidad Completa
Se ha implementado un sistema de chat conversacional con IA que permite adjuntar archivos (imágenes y PDFs), similar a ChatGPT, con prompt personalizable y envío completo al webhook.

📋 Características Implementadas
1. Adjuntar Archivos en el Chat
 Botón 📎 para adjuntar archivos
 Soporte para imágenes (JPG, PNG, etc.) y PDFs
 Preview de archivos adjuntos antes de enviar
 Eliminación individual de archivos adjuntos
 Múltiples archivos por mensaje
 Validación de tipos de archivo
2. Procesamiento Backend de Archivos
 Endpoint modificado para recibir FormData con archivos
 PDFs: Extracción automática de texto con pypdf
 Imágenes: Conversión a base64 para Vision AI
 Análisis combinado de texto + imágenes
 Límite de 2000 caracteres por PDF para optimizar
3. Integración con IA
 OpenAI Vision: Soporte multimodal (texto + imágenes)
 Gemini: Soporte de imágenes y texto
 Contexto enriquecido con archivos adjuntos
 Prompts adaptados según tipo de archivo
4. Custom Prompt por Checklist
 Campo assistant_prompt configurable por pregunta
 Campo assistant_instructions para instrucciones adicionales
 Control de longitud de respuesta (short/medium/long)
 Contexto automático del vehículo en cada mensaje
5. Persistencia del Chat
 Nuevo campo chat_history en modelo Answer
 Migración SQL: add_chat_history_to_answers.sql
 Guardado automático del historial completo
 Restauración del chat al reabrir
6. Envío al Webhook (n8n)
 Todos los chats incluidos en send_completed_inspection_to_n8n()
 Campo chat_history en cada respuesta del webhook
 Incluye metadata de archivos adjuntos
 Tipo de pregunta identificado en webhook
 Datos completos para análisis posterior
2025-12-02 11:22:21 -03:00
bf30b1a2bf Soporte para PDFs agregado al sistema de análisis con IA
📋 Cambios Implementados
Frontend:

 Input acepta image/*,application/pdf
 Label actualizado: "Fotografías / Documentos PDF *"
 Preview diferenciado: PDFs muestran icono 📝 rojo en lugar de imagen
 Nombre de archivo PDF visible en el preview
 Contador genérico: "archivo(s) cargado(s)"
Backend:

 Agregado pypdf==4.0.1 a requirements.txt
 Detección automática de PDFs por content_type o extensión
 Extracción de texto de PDFs usando pypdf.PdfReader
 Validación de PDFs vacíos (sin texto extraíble)
 Prompts adaptados automáticamente para PDFs
 Soporte en OpenAI y Gemini (análisis de texto en lugar de vision)
 Límite de 4000 caracteres del PDF para análisis
🎯 Funcionamiento
Usuario sube PDF → Sistema detecta tipo de archivo
Extrae texto → PyPDF lee todas las páginas
Análisis IA → Envía texto al modelo (no usa Vision API)
Respuesta → Misma estructura JSON que con imágenes
⚠️ Limitaciones
PDFs escaneados sin OCR no funcionarán (requieren texto seleccionable)
Máximo 4000 caracteres del PDF enviados al AI
📦 Versiones
Frontend: 1.0.91 → 1.0.92
Backend: 1.0.89 → 1.0.90
2025-12-02 09:40:44 -03:00
d51d912962 Removido el efecto animate-bounce del modal de actualización. Ahora aparece estático sin saltos. 2025-12-01 01:40:47 -03:00
1450d443d4 Front v1.0.91 2025-12-01 01:36:16 -03:00
1988ec95da Corregido: Problema de timing en recálculo de max_score
🐛 Problema
Al crear 1 pregunta → max_score quedaba en 0
Al crear 2 preguntas → max_score mostraba 1
Al quedar 1 pregunta → max_score mostraba 0
🔍 Causa
Llamaba a recalculate_checklist_max_score() ANTES del db.commit(), entonces la consulta SQL no encontraba las preguntas recién agregadas/modificadas porque aún no estaban persistidas en la base de datos.
2025-12-01 00:27:20 -03:00
d86a216766 Problema Resuelto: max_score no se actualizaba al eliminar preguntas
🐛 Problema
Cuando se eliminaban preguntas de un checklist, la Puntuación máxima no se recalculaba, quedando con valores incorrectos (más altos de lo que debería).

🔍 Causa
Al CREAR pregunta: solo se sumaban puntos (max_score += points)
Al ACTUALIZAR pregunta: no se recalculaba el max_score
Al ELIMINAR pregunta: no se restaban los puntos del max_score
 Solución
Nueva función helper recalculate_checklist_max_score():

Suma puntos de todas las preguntas NO eliminadas desde la base de datos
Garantiza consistencia siempre
Aplicada en 3 operaciones:

POST /api/questions - Al crear pregunta (línea ~960)
PUT /api/questions/{id} - Al actualizar puntos (línea ~1038)
DELETE /api/questions/{id} - Al eliminar pregunta (línea ~1167)
📦 Versión
Backend: 1.0.87 → 1.0.88
2025-12-01 00:18:05 -03:00
4174774702 Cambios Adicionales
 Importado or_ de SQLAlchemy para query del reporte
 Backend: 1.0.86 → 1.0.87
🎯 Resultado
 Inspecciones solo muestran preguntas activas del checklist
 PDFs correctos sin preguntas eliminadas
 Cálculo de score preciso (solo preguntas vigentes)
 Webhooks envían solo datos relevantes
 Reportes con métricas correctas
 Respuestas huérfanas de preguntas eliminadas se ignoran automáticamente
2025-12-01 00:10:06 -03:00
54006d5756 Campo de Observaciones Opcional
 Agregado checkbox "Agregar campo observaciones" en QuestionTypeEditor.jsx (sección "Opciones Generales")
 Por defecto está marcado (compatibilidad con preguntas existentes)
 El campo de observaciones solo se muestra si show_observations !== false
 El admin ahora tiene control total sobre si mostrar o no las observaciones
2. Botón "Consultar Asistente IA" Siempre Visible
 El botón ahora aparece siempre para preguntas tipo ai_assistant
 No depende de que la pregunta tenga fotos habilitadas
 Movido a una sección independiente (fuera del bloque de fotos)
 Removido el botón duplicado que estaba dentro de la sección de fotos
3. Versiones Actualizadas
Frontend: 1.0.89 → 1.0.90
Service Worker: ayutec-v1.0.89 → ayutec-v1.0.90
Backend: Sin cambios (no fue necesario)
📋 Detalles Técnicos
App.jsx:

Campo de respuesta oculto para photo_only y ai_assistant
Botón de Asistente IA en sección dedicada (siempre visible para ai_assistant)
Observaciones solo si show_observations !== false y no es photo_only ni ai_assistant
QuestionTypeEditor.jsx:

Nueva sección "⚙️ Opciones Generales" con checkbox azul
Texto de ayuda: "Si está marcado, el mecánico podrá agregar notas adicionales"
2025-11-30 23:51:04 -03:00
c226fbd34b Corregido Error de Chat IA (Backend v1.0.86)
Problema
Causa
El modelo de base de datos usa model_name pero el código intentaba acceder a model.

Solución
 Cambiado ai_config.model → ai_config.model_name en:

Llamada a OpenAI: model=ai_config.model_name or "gpt-4"
Llamada a Gemini: GenerativeModel(ai_config.model_name or 'gemini-pro')
Response del endpoint: "model": ai_config.model_name
2025-11-30 23:35:39 -03:00
b2398efead Actualización PWA Manual (v1.0.89)
Cambios Realizados
Service Worker (public/service-worker.js)
 Removido skipWaiting() automático en install
 Removido claim() automático en activate
 Solo se activa cuando recibe mensaje SKIP_WAITING del usuario
App.jsx
 Modal se muestra cuando hay actualización
 Nueva versión se instala en segundo plano
⏸️ Espera confirmación del usuario
 Solo actualiza cuando el usuario presiona el botón
 Protección contra recargas múltiples (refreshing flag)
Flujo Actualizado
Deploy de nueva versión (ej: v1.0.89)
Usuario abre la app con versión antigua (v1.0.88)
Service Worker detecta nueva versión
Descarga en segundo plano la nueva versión
Modal aparece → "¡Nueva Actualización!"
⏸️ La app sigue funcionando normalmente
👆 Usuario presiona "🚀 ACTUALIZAR AHORA"
Service Worker se activa (skipWaiting)
Página se recarga automáticamente
 Nueva versión activa
Ventajas
 Usuario tiene control total
 No interrumpe trabajo en curso
 Puede terminar inspección antes de actualizar
 Modal bloqueante asegura que eventualmente actualice
 Actualización instantánea al presionar botón
Ahora el usuario DEBE presionar el botón para actualizar! 🎯
2025-11-30 23:30:57 -03:00
14a64778b8 Nuevo Tipo de Pregunta: Asistente IA (Chat) 🤖💬
Frontend (v1.0.88)
QuestionTypeEditor.jsx
 Nuevo tipo: ai_assistant con icono 💬
 Configuración completa:
assistant_prompt: Define rol y comportamiento del asistente
context_questions: IDs de preguntas anteriores cuyas fotos usar (o todas)
assistant_instructions: Reglas específicas de diagnóstico
max_messages: Límite de mensajes en el chat
response_length: Corta/Media/Larga
QuestionAnswerInput.jsx
 Mensaje informativo para tipo ai_assistant
 Indica que el chat se abre con botón separado
App.jsx - Modal de Chat IA
 Modal full-screen responsive con:

Header con gradiente purple/blue
Área de mensajes con scroll
Input de texto + botón enviar
Soporte Enter para enviar
Indicador de "pensando..."
Timestamps en mensajes
Confianza de la IA
Límite de mensajes
 Botón "💬 Consultar Asistente IA" al lado de "Cargar Documentos"

 Contador de mensajes en el botón si ya hay historial

 Historial guardado en answers[questionId].chatHistory

 Auto-marca como completada cuando se abre el chat

Backend (v1.0.85)
Endpoint /api/ai/chat-assistant
 Recibe:

Mensaje del usuario
Historial del chat
Fotos de preguntas anteriores con sus análisis
Configuración del asistente
Info del vehículo
 Contexto inteligente:

Información del vehículo (marca, modelo, km, placa)
Análisis previos de fotos (hasta 10 fotos más relevantes)
Instrucciones personalizadas por pregunta
Historial conversacional (últimos 10 mensajes)
 Soporte multi-proveedor:

OpenAI (GPT-4, GPT-3.5)
Google Gemini
 Respuestas adaptables:

Longitud configurable (200/400/800 tokens)
Temperature 0.7 para balance creatividad/precisión
Cómo Funciona
Configuración (Admin):

- Crear pregunta tipo "Asistente IA (Chat)"
- Definir prompt: "Eres experto en sistemas de frenos..."
- Seleccionar preguntas anteriores: "5,8,12" (fotos de frenos)
- Instrucciones: "Si pastillas < 3mm → cambio inmediato"
- Longitud: Media
- Max mensajes: 20

2 Durante Inspección (Mecánico):

- Llega a pregunta tipo AI Assistant
- Ve botón "💬 Consultar Asistente IA"
- Clic → Abre chat full-screen
- La IA YA vio las fotos de preguntas 5, 8, 12
- Mecánico pregunta: "¿Las pastillas están bien?"
- IA responde basado en fotos previas + contexto vehículo
- Conversación natural con historial
- Al cerrar → se guarda automáticamente

3 Ejemplo de Conversación:

Mecánico: "Vi desgaste en las pastillas, ¿qué recomiendas?"

IA: "Basándome en las fotos que analicé:

     Pastilla delantera izquierda: ~2.5mm de espesor
     Pastilla delantera derecha: ~2.8mm

     ⚠️ CRÍTICO: Ambas pastillas están por debajo del
     mínimo de 3mm. Con 45,000 km, es normal este desgaste.

     RECOMENDACIÓN:
     - Cambio INMEDIATO de pastillas delanteras
     - Verificar estado de discos (posible rectificado)
     - Revisar líquido de frenos (última foto muestra nivel bajo)

     ¿Necesitas el código de pieza para este Toyota Corolla 2019?"

Casos de Uso
Diagnóstico de Frenos

assistant_prompt: "Eres especialista en sistemas de frenos.
                   Analiza desgaste, fugas, vibraciones."
context_questions: "10,11,12,13" // Fotos de pastillas, discos, líquido

assistant_prompt: "Experto en motores. Detecta fugas, ruidos anormales,
                   consumo excesivo."
context_questions: "5,6,7,8,9" // Motor, aceite, correa, filtros

assistant_prompt: "Especialista en sistemas eléctricos y electrónicos."
context_questions: "20,21,22" // Batería, luces, tablero
instructions: "Siempre pedir código OBD2 si hay check engine"

Ventajas
 Contextual: La IA ve fotos previas, no pregunta "¿puedes mostrarme?"
 Especializado: Un asistente POR tema (frenos, motor, eléctrico)
 Conversacional: El mecánico puede hacer follow-up questions
 Guiado: Instrucciones específicas por tipo de inspección
 Historial: No repite info, mantiene contexto de la conversación
 Móvil-friendly: Modal responsive, fácil de usar en celular
2025-11-30 23:23:43 -03:00
a692948a6f Sistema de Actualización PWA Implementado (v1.0.87)
Frontend (v1.0.87)
Service Worker (public/service-worker.js)
 Cache versionado dinámico: ayutec-v1.0.87
 Estrategia Network-First con fallback a cache
 Auto-limpieza de caches antiguos en activación
 Skip waiting para activación inmediata
 Soporte para mensaje SKIP_WAITING desde cliente
Detección de Actualizaciones (App.jsx)
 Registro automático de Service Worker
 Listener de updatefound para detectar nuevas versiones
 Listener de controllerchange para recarga automática
 Estado updateAvailable y waitingWorker
Modal de Actualización
 Diseño grande y llamativo con animación bounce
 Overlay bloqueante (z-index 9999, no se puede cerrar)
 Botón enorme: "🚀 ACTUALIZAR AHORA"
 Gradiente indigo/purple, responsive
 Texto claro: "Nueva versión disponible"
 Recarga automática al actualizar
PWA Manifest (site.webmanifest)
 Agregado start_url y scope
 Configurado orientation: portrait
 Display standalone para app nativa
HTML Metatags (index.html)
 theme-color para barra de navegación
 apple-mobile-web-app-capable para iOS
 mobile-web-app-capable para Android
 Viewport con user-scalable=no para PWA
Automatización
 Script PowerShell update-version.ps1:
Incrementa versión automáticamente (patch)
Actualiza package.json
Actualiza service-worker.js
Sincroniza ambos archivos
 Guía completa PWA-UPDATE-GUIDE.md
Flujo de Actualización
Desarrollador ejecuta update-version.ps1
Build y deploy de nueva versión
Usuario abre la app
Service Worker detecta nueva versión
Modal aparece automáticamente bloqueando UI
Usuario presiona "ACTUALIZAR AHORA"
Service Worker se activa
Página se recarga automáticamente
Usuario usa nueva versión
Backend (v1.0.84)
Sin cambios
Ahora la PWA se actualiza automáticamente mostrando un modal imposible de ignorar! 🚀📱
2025-11-30 23:11:33 -03:00
45ad650bac Mejoras de Responsividad Móvil (v1.0.86)
Sidebar
 Oculto por defecto en móvil (window.innerWidth < 1024px)
 Overlay oscuro cuando está abierto en móvil (se cierra al tocar fuera)
 Deslizable desde la izquierda con transiciones suaves
 Siempre visible en desktop (lg: breakpoint)
Header
 Botón hamburguesa visible solo en móvil (lg:hidden)
 Logo escalable: 50px en móvil → 70px en desktop
 Título oculto en móvil para ahorrar espacio
 Indicador de sección: icono solo en móvil, texto completo en desktop
 Padding adaptable: 3px móvil → 4px tablet → 8px desktop
Contenido Principal
 Sin margin-left en móvil (el sidebar es overlay)
 Padding responsive: 3px → 4px → 6px según tamaño
 Border-radius adaptable: xl en móvil → 2xl en desktop
Modal de Inspección
 Ancho completo en móvil con padding mínimo (2px)
 Título responsive: lg (móvil) → xl (tablet) → 2xl (desktop)
 Altura máxima: 95vh móvil → 90vh desktop
Navegador de Preguntas
 Botones más pequeños en móvil: 7px/8px círculos
 Overflow horizontal con scroll para muchas preguntas
 Números responsive: texto sm en móvil → lg en desktop
 Gaps reducidos: 1px móvil → 2px desktop
Botones de Navegación
 Solo flechas en móvil (← →)
 Texto completo en desktop ("← Anterior", "Siguiente →")
 Padding y texto adaptables: text-sm móvil → text-base desktop
 Mejor uso del espacio horizontal
Formularios
 Espaciado adaptive: space-y-3 móvil → space-y-6 desktop
 Labels y texto responsive: xs → sm → base
 Banner de modo IA con wrap en móvil
La interfaz ahora es completamente funcional en móviles sin scroll horizontal, con todos los elementos accesibles y legibles! 📱
2025-11-30 22:59:38 -03:00
7820f143ac feat: Ocultar referencias a IA en interfaz de mecánico
Frontend (v1.0.85):
- Reemplazado "Modo IA COMPLETO/ASISTIDO" por "Modo AUTOCOMPLETADO/ASISTIDO"
- Cambiado "La IA completará/sugerirá" por "El sistema completará/sugerirá"
- Reemplazado "🤖 Análisis automático" por "📋 Procesamiento automático"
- Eliminadas todas las menciones explícitas de IA visibles para mecánicos
- Terminología neutral: "el sistema" en lugar de "la IA"

Backend (v1.0.84):
- Sin cambios

Mecánicos usan funcionalidad IA sin saber que existe, mejora UX profesional
2025-11-30 22:44:51 -03:00
2db2833f27 feat: Validación inteligente de contexto en análisis de imágenes IA
Backend (v1.0.84):
- Agregado campo 'context_match' en respuesta JSON de análisis IA
- IA evalúa si imagen corresponde al contexto de la pregunta
- Tres niveles de validación: prompt personalizado, pregunta específica, análisis general
- Detección automática de imágenes fuera de contexto con recomendaciones específicas

Frontend (v1.0.84):
- Sistema de alertas cuando IA detecta imágenes que no corresponden
- Popup de advertencia muestra qué imágenes no coinciden y por qué
- Opción para eliminar automáticamente fotos incorrectas y cargar nuevas
- Validación con frases clave: "no corresponde", "no coincide", "no relacionad"
- Previene que mecánicos carguen imágenes irrelevantes a las preguntas

Evita errores en inspecciones al garantizar que cada foto corresponda a su pregunta específica
2025-11-30 22:35:31 -03:00
7b39648be5 Con IA configurada y funcionando → Análisis + popup
 Sin IA configurada → Solo popup (sin análisis)
 Error en el backend → Popup con mensaje de sin análisis
 Timeout o fallo de red → Documentos marcados como cargados
2025-11-30 22:23:40 -03:00
c76f803871 ACtualizacion en campos de tipo foto blouqeaba por no contestar,
Actualziacion de estados del DOM de las imagenes
2025-11-29 11:55:30 -03:00
b6440130ac Frontend v1.0.82
1. Eliminado campo duplicado de "Observaciones":

 Antes: Había 2 campos de observaciones (uno general y uno dentro de fotos)
 Ahora: Solo 1 campo de observaciones (el general)
2. Nuevo tipo de pregunta "📸 Solo Fotografía" (photo_only):

Comportamiento: Solo muestra el título de la pregunta y el campo para adjuntar fotos
NO muestra: Campo de respuesta ni campo de observaciones
Ideal para: Documentación fotográfica pura (ej: "Fotografía del VIN", "Estado general del vehículo")
2025-11-29 11:25:14 -03:00
886f0bafbd Cambios Completados - IA Oculta al Mecánico
🎭 Frontend v1.0.81
1. Botón renombrado:

 Antes: "🤖 Analizar Pregunta"
 Ahora: "📁 Cargar Documentos"
Estado procesando: "Procesando..." (sin mencionar IA)
2. Análisis IA separado de observaciones:

El análisis NO se escribe en el campo de observaciones
Se guarda en aiAnalysis (campo separado)
Mecánico escribe observaciones manualmente
Se agrega flag documentsLoaded: true al procesar
3. Popup de confirmación:

Después de cargar documentos: " Documentos cargados correctamente"
NO muestra el análisis al mecánico
4. Validación obligatoria:

Si hay fotos adjuntas y el checklist tiene IA activada
DEBE presionar "Cargar Documentos" antes de continuar
Mensaje: "⚠️ Debes presionar 'Cargar Documentos' antes de continuar"
5. Referencias a IA eliminadas:

 Removido: "Analizando X imagen(es) con IA..."
 Removido: "✓ Analizada"
 Removido: "guardada automáticamente"
 Ahora: "Procesando X documento(s)..."
 Ahora: "Respuesta guardada"
6. Análisis IA solo visible para admin:

En el modal de detalle de inspección
Sección morada "🤖 Análisis de IA"
Muestra: estado, observaciones, recomendación, confianza
Solo visible si user.role === 'admin'
🔧 Backend v1.0.83
Sin cambios (el campo ai_analysis ya existía en JSON)
2025-11-29 08:40:14 -03:00
00218a1a92 Frontend v1.0.80:
El botón "📄 Exportar PDF" ahora solo es visible para admin y asesor
Los mecánicos (role === 'mechanic') pueden ver el modal de inspección pero NO pueden exportar el PDF
Backend v1.0.82 (sin cambios adicionales)
Resumen de permisos:

 Admin: Ver inspección + Exportar PDF + Ver historial + Inactivar
 Asesor: Ver inspección + Exportar PDF
 Mecánico: Solo ver inspección + Continuar incompletas
2025-11-28 14:54:28 -03:00
ed037ef4cc agregar logs 2025-11-28 10:11:42 -03:00
37daf6b8d3 Backend v1.0.82: PDF con logos dual (empresa + checklist)
- Generación de PDF ahora muestra dos logos en el encabezado
- Logo izquierda: logo de la empresa (AIConfiguration)
- Logo derecha: logo del checklist específico (o empresa como fallback)
- Nueva función helper load_logo() para reutilización
- Layout horizontal con tabla de 3 columnas para separación visual
- Frontend: sin cambios (v1.0.79)
2025-11-28 10:00:08 -03:00
f57d7328e1 Backend v1.0.82: PDF con logos dual (empresa + checklist)
- Generación de PDF ahora muestra dos logos en el encabezado
- Logo izquierda: logo de la empresa (AIConfiguration)
- Logo derecha: logo del checklist específico (o empresa como fallback)
- Nueva función helper load_logo() para reutilización
- Layout horizontal con tabla de 3 columnas para separación visual
- Frontend: sin cambios (v1.0.79)
2025-11-28 09:58:18 -03:00
6d006d3b13 Frontend v1.0.79: Renombrar "Borrador" a "Incompleta" y agregar funcionalidad para continuar inspecciones
- Cambio de terminología: "Borrador" → "Incompleta" en toda la UI
- Nuevo botón "Continuar Inspección" en modal de detalle (solo para incompletas)
- InspectionModal ahora carga y continúa inspecciones existentes
- Carga automática de respuestas previas al continuar
- Backend: status por defecto cambiado de "draft" a "incomplete"
2025-11-28 09:36:29 -03:00
96426a4259 Merge branch 'develop' of https://git.rshtech.com.py/gitea/checklist into develop 2025-11-28 09:32:20 -03:00
16f431cbad Cambios Completados
He implementado exitosamente la funcionalidad solicitada. Aquí está el resumen:

🔄 Cambios en el Frontend (App.jsx)
Renombrado de "Borrador" a "Incompleta":

Actualizado el filtro de estado: draft → incomplete
Cambiado el texto del selector: "Borradores" → "Incompletas"
Actualizado el badge de estado en la lista de inspecciones
Botón "Continuar Inspección":

Agregado botón verde "▶️ Continuar Inspección" en InspectionDetailModal
Solo aparece cuando inspection.status !== 'completed'
Cierra el modal de detalle y abre el modal de inspección para continuar
Soporte para continuar inspecciones:

Modificado InspectionModal para recibir existingInspection
Carga automática de datos del vehículo existentes
Carga de respuestas previas desde el backend
Si hay respuestas existentes, salta directamente al paso 2 (Preguntas)
No crea una nueva inspección si ya existe, solo continúa la actual
Actualización de versión:

package.json: v1.0.78 → v1.0.79
🔧 Cambios en el Backend (models.py)
Renombrado del estado por defecto:
status = Column(String(20), default="draft") → default="incomplete"
Comentario actualizado: # draft, completed, inactive → # incomplete, completed, inactive
2025-11-28 09:26:35 -03:00
91711ac95f Corrección aplicada:
🔧 Patrones que ahora elimina:
 Análisis Automático (90% confianza):
 Análisis IA (95% confianza):
 🤖 Análisis Automático (98% confianza):
 🤖 Análisis IA (100% confianza):
Backend actualizado a v1.0.81
2025-11-28 08:09:05 -03:00
4e70f1f9b0 Cambios de terminología:
📝 Selector de modo (admin):
 "Modo IA" →  "Modo de Asistencia"
 "Sin IA - Control manual total" →  "Manual - Control total del operario"
 "IA Asistida - Sugerencias en fotos" →  "Asistido - Sugerencias automáticas"
 "IA Completa - Análisis automático" →  "Automático - Análisis completo"
📋 Descripciones:
 "Sin IA: El mecánico completa..." →  "Modo Manual: El operario completa..."
 "IA Asistida: Cuando se suben fotos, la IA analiza..." →  "Modo Asistido: ...el sistema analiza..."
 "IA Completa: El mecánico solo toma fotos y la IA responde..." →  "Modo Automático: ...el sistema responde..."
 "Requiere OPENAI_API_KEY configurada" →  "Requiere configuración de API externa"
🔍 Durante inspección:
 "🤖 Análisis IA disponible" →  "🤖 Análisis automático disponible"
💬 En observaciones/comentarios:
 "🤖 Análisis IA (98% confianza):" →  "Análisis Automático (98% confianza):"
 "🤖 Análisis IA:" →  "Análisis Automático:"
 "🤖 Análisis IA de X imágenes:" →  "Análisis Automático de X imágenes:"
🎯 Resultado:
Los mecánicos ahora ven:

"Asistente automático" en lugar de "Inteligencia Artificial"
"Sistema" en lugar de "IA"
"Análisis automático" en lugar de "Análisis IA"
Terminología más neutral y profesional
Frontend actualizado a v1.0.78
2025-11-27 18:31:15 -03:00
14b3376a4a Interfaz consistente entre crear y editar
 Etiquetas legibles en lugar de valores técnicos
 Más fácil e intuitivo para administradores
 Reutiliza el mismo componente configurable
Frontend actualizado a v1.0.77
2025-11-27 17:57:43 -03:00
185b9fc631 Cambio aplicado:
📸 Logo del PDF:
Usa exclusivamente el logo de "⚙️ Configuración"
No usa el logo del checklist (ese es para otra funcionalidad)
Más simple y consistente
Backend v1.0.79
2025-11-27 17:45:30 -03:00
320f41c0ff Mejoras implementadas:
🔍 Mejor debugging:
Logs detallados en consola para ver qué está pasando
Muestra la URL del logo que intenta cargar
Indica el código HTTP de respuesta
Stack trace completo si hay error
📐 Ajuste automático de tamaño:
Antes: Forzaba 40mm x 40mm (distorsionaba la imagen)
Ahora: Mantiene proporciones (aspect ratio)
Ancho máximo: 50mm
Alto máximo: 40mm
Se ajusta automáticamente al que limite primero
 Validaciones adicionales:
Verifica que el checklist exista
Verifica que tenga logo_url configurado
Mensajes informativos en cada caso
Backend v1.0.79
2025-11-27 17:40:23 -03:00
e79aa1f212 Backend actualizado a v1.0.78
 Cambios aplicados:
📏 Nuevos tamaños de letra:
Preguntas: 11pt en negrita (más grandes y destacadas)
Respuestas: 10pt (tamaño medio legible)
Comentarios: 9pt con indentación (diferenciados visualmente)
🤖 Formato mejorado de comentarios IA:
 Removido prefijo "Análisis IA (98% confianza): "
 Salto de línea doble antes de "Recomendaciones:"
 "Recomendaciones:" ahora en negrita
 Indentación de 10mm en comentarios para mejor jerarquía visual
2025-11-27 17:29:02 -03:00
34221c4726 Cambios implementados:
🔄 Función getReadableAnswer() en Frontend:
Convierte valores técnicos a etiquetas legibles dinámicamente
Lee la configuración question.options (que tú defines al crear preguntas)
Busca en el array choices la etiqueta correspondiente al valor
📋 Conversiones soportadas:
Boolean: "yes" → "Sí", "pass" → "Pasa", "good" → "Bueno"
Single Choice: "option1" → "Opción 1", "excellent" → "Excelente"
Multiple Choice: "lights,wipers" → "Luces, Limpiaparabrisas"
Scale/Text/Number/Date/Time: Se muestran tal cual (ya son legibles)
🎯 Dónde se aplica:
Modal de detalle de inspección al ver respuestas completadas
Respeta las configuraciones dinámicas que defines en el editor de preguntas
Funciona con todas las plantillas predefinidas y configuraciones personalizadas
⚙️ Funcionamiento dinámico:
Como los tipos de pregunta son configurables por ti en el frontend,
 la función lee directamente de question.options.choices el array que tú configuraste,
 por lo que funcionará automáticamente con cualquier configuración que crees.

Versiones actualizadas:

Frontend: 1.0.76 - Backend: 1.0.77

Ahora tanto el PDF como el modal de inspecciones mostrarán las etiquetas legibles en lugar de los valores técnicos.
2025-11-27 17:19:50 -03:00
58672c52d7 Backend actualizada a 1.0.77
🖼️ Logo en la portada del PDF:
Se carga el logo desde checklist.logo_url (configurado en la administración)
Ubicación: Arriba del título, centrado
Tamaño: 40mm x 40mm (tamaño estándar para logos corporativos)
Manejo de errores: Si el logo no carga, continúa generando el PDF sin bloquearse
Si no hay logo configurado, simplemente no se muestra (no rompe el PDF)
📄 Estructura de la portada:
Logo del checklist (si existe)
Título "📋 INFORME DE INSPECCIÓN VEHICULAR"
Número de inspección
Cuadros de información del vehículo e inspección
Resumen de evaluación
2025-11-27 17:15:47 -03:00
416588a327 Backend v1.0.76:
- 🎨 Rediseñado PDF primera página con diseño moderno y profesional (cuadros con encabezados separados y coloreados, bordes redondeados, separadores internos, mejor jerarquía visual)
- 🔒 Eliminado nombre de mecánico del PDF por privacidad (solo código de operario)
- 🐛 Corregido bug: PDF mostraba valores técnicos en lugar de etiquetas legibles (implementada función get_readable_answer() que convierte "option1" → "Bueno", "pass" → "Pasa", soporta boolean, single_choice y multiple_choice)

Frontend v1.0.75:
-  Sin cambios
2025-11-27 17:12:45 -03:00
32c7f79dd6 Cambios implementados:
🎨 Diseño Visual Mejorado:
Portada más espaciada - Espaciado superior aumentado para mejor presentación
Cuadros con encabezados separados - Cada sección tiene un header coloreado profesional
Bordes redondeados - Esquinas suavizadas para un look más moderno
Separadores internos - Líneas delgadas entre filas para mejor legibilidad
Etiquetas diferenciadas - Labels en gris claro, valores en negrita oscura
🚗 Cuadro de Vehículo:
Header azul (#2563eb) con "🚗 INFORMACIÓN DEL VEHÍCULO"
Contenido blanco con bordes redondeados
Layout limpio: etiqueta arriba, valor abajo por campo
📄 Cuadro de Inspección:
Header verde (#16a34a) con "📄 INFORMACIÓN DE LA INSPECCIÓN"
Nombre de mecánico eliminado - Solo código de operario por privacidad
Campos: Nº Pedido, OR Nº, Cód. Operario, Fecha
📊 Resumen de Evaluación:
Título "📊 RESUMEN DE EVALUACIÓN" centrado
Grid de 4 métricas: Puntuación, Porcentaje, Estado, Ítems Críticos
Borde dinámico según resultado (verde/amarillo/rojo)
Estado textual: EXCELENTE/ACEPTABLE/DEFICIENTE
Separadores internos para cada métrica
Versión Backend actualizada a 1.0.75
2025-11-27 17:06:09 -03:00
1c9d7348ed Auto-Scroll Implementado en Drag & Drop
Frontend v1.0.75
Nueva Funcionalidad:

 Auto-scroll automático cuando arrastras cerca de los bordes del modal
 Zona de activación: 100 pixeles desde arriba/abajo
 Scroll suave: 10 pixeles cada 16ms (~60fps)
 Limpieza automática: Detiene el scroll cuando sueltas o sales del área
Cómo Funciona:

Arrastras una pregunta cerca del borde superior → scroll automático hacia arriba
Arrastras cerca del borde inferior → scroll automático hacia abajo
Alejas del borde → scroll se detiene
Sueltas la pregunta → scroll se limpia
2025-11-27 16:52:35 -03:00
ce151631ab Corregido Drag & Drop con Validación de Niveles
Cambios v1.0.74
Lógica Implementada:

Preguntas padre solo se pueden reordenar entre sí
Subpreguntas solo se pueden reordenar con otras subpreguntas del mismo padre
No se permite arrastrar una pregunta padre dentro de subpreguntas o viceversa
Validaciones:

 En handleDragOver: Cursor none si intentas arrastrar entre diferentes niveles
 En handleDrop: Mensajes de error claros si intentas mezclar niveles
 Filtrado inteligente: Solo reordena el grupo correcto de preguntas
2025-11-27 16:47:05 -03:00
2d520e03d6 Frontend v1.0.74:
- Implementado drag & drop nativo HTML5 para reordenar preguntas
- Agregados estados draggedQuestion y dragOverQuestion
- Handlers: handleDragStart, handleDragEnd, handleDragOver, handleDrop
- Indicador visual: línea azul en drop zone
- Icono de agarre (⋮⋮) con tooltip "Arrastra para reordenar"
- Opacidad 50% en elemento arrastrado
- Cursor 'move' indica elemento arrastrable
- Mantiene función moveQuestion para compatibilidad
- Reordenamiento automático al soltar
2025-11-27 16:43:45 -03:00
bd2b11d543 Frontend v1.0.73:
- Implementado drag & drop nativo HTML5 para reordenar preguntas
- Agregados estados draggedQuestion y dragOverQuestion
- Handlers: handleDragStart, handleDragEnd, handleDragOver, handleDrop
- Indicador visual: línea azul en drop zone
- Icono de agarre (⋮⋮) con tooltip "Arrastra para reordenar"
- Opacidad 50% en elemento arrastrado
- Cursor 'move' indica elemento arrastrable
- Mantiene función moveQuestion para compatibilidad
- Reordenamiento automático al soltar
2025-11-27 16:43:14 -03:00
97c5aab93d Backend v1.0.73:
- Implementado sistema de reordenamiento de preguntas
- Nuevo endpoint PATCH /api/checklists/{id}/questions/reorder
- Schema QuestionReorder para validar datos de reorden
- Actualización en lote de campo 'order' en preguntas
- Auditoría automática de cambios de orden
- Validación de permisos y existencia de checklist

Frontend v1.0.73:
- Agregada funcionalidad de reordenamiento de preguntas
- Botones ▲ ▼ para mover preguntas arriba/abajo
- Función moveQuestion() para gestionar reordenamiento
- Interfaz visual mejorada con separadores
- Tooltips descriptivos en botones de orden
- Recarga automática tras reordenar
2025-11-27 16:17:45 -03:00
d6c0f117a1 Backend v1.0.73:
- Implementado sistema de reordenamiento de preguntas
- Nuevo endpoint PATCH /api/checklists/{id}/questions/reorder
- Schema QuestionReorder para validar datos de reorden
- Actualización en lote de campo 'order' en preguntas
- Auditoría automática de cambios de orden
- Validación de permisos y existencia de checklist

Frontend v1.0.71:
- Agregada funcionalidad de reordenamiento de preguntas
- Botones ▲ ▼ para mover preguntas arriba/abajo
- Función moveQuestion() para gestionar reordenamiento
- Interfaz visual mejorada con separadores
- Tooltips descriptivos en botones de orden
- Recarga automática tras reordenar
2025-11-27 16:17:02 -03:00
651aa138cf Backend v1.0.73:
- Implementado sistema de reordenamiento de preguntas
- Nuevo endpoint PATCH /api/checklists/{id}/questions/reorder
- Schema QuestionReorder para validar datos de reorden
- Actualización en lote de campo 'order' en preguntas
- Auditoría automática de cambios de orden
- Validación de permisos y existencia de checklist

Frontend v1.0.71:
- Agregada funcionalidad de reordenamiento de preguntas
- Botones ▲ ▼ para mover preguntas arriba/abajo
- Función moveQuestion() para gestionar reordenamiento
- Interfaz visual mejorada con separadores
- Tooltips descriptivos en botones de orden
- Recarga automática tras reordenar
2025-11-27 16:15:20 -03:00
826c5fce5e Backend v1.0.71:
- Implementado soft delete para preguntas
- Nuevas columnas: is_deleted (boolean), updated_at (timestamp)
- Migración SQL: add_soft_delete_to_questions.sql
- Endpoint DELETE marca preguntas como eliminadas en lugar de borrarlas
- GET /api/checklists/{id} filtra preguntas eliminadas (is_deleted=false)
- Validación de subpreguntas activas antes de eliminar
- Índices agregados para optimizar queries
- Mantiene integridad de respuestas históricas y PDFs generados
- Permite limpiar checklists sin afectar inspecciones completadas
2025-11-27 15:34:19 -03:00
ed3f513075 Backend v1.0.71:
- Implementado soft delete para preguntas
- Nuevas columnas: is_deleted (boolean), updated_at (timestamp)
- Migración SQL: add_soft_delete_to_questions.sql
- Endpoint DELETE marca preguntas como eliminadas en lugar de borrarlas
- GET /api/checklists/{id} filtra preguntas eliminadas (is_deleted=false)
- Validación de subpreguntas activas antes de eliminar
- Índices agregados para optimizar queries
- Mantiene integridad de respuestas históricas y PDFs generados
- Permite limpiar checklists sin afectar inspecciones completadas
2025-11-27 15:32:56 -03:00
027f22551c Frontend v1.0.69:
- Agregado debug logging para investigar problema de carga de ai_prompt al editar preguntas
- Console.log muestra el objeto de pregunta completo y el campo ai_prompt específico

Backend v1.0.69:
- Sincronización de versión con frontend
- Schema ya incluye ai_prompt en QuestionBase y Question
2025-11-27 11:39:25 -03:00
0117ba34f8 Frontend v1.0.68:
- Agregada funcionalidad de edición de checklists
- Nuevo modal para editar nombre, descripción, modo IA y scoring
- Botón "✏️ Editar" en cada checklist (solo admins)
- Mejora en la gestión de checklists en el panel de administración

Backend v1.0.68:
- Actualización de versión para sincronizar con frontend
- Endpoint PUT /api/checklists/{id} ya soportaba la funcionalidad
2025-11-27 11:19:48 -03:00
efbf57e6bc v1.0.67 Backend / v1.0.67 Frontend - Ordenamiento consistente de checklists e inspecciones
Frontend (1.0.67):
- 🔧 Checklists e inspecciones se ordenan por ID descendente (más recientes primero)
- Mantiene posición de elementos después de editar/actualizar
- Ya no se mueven al final de la lista tras modificaciones
- Orden consistente en todas las recargas de datos
- Mejora UX al preservar contexto visual del usuario

Backend (1.0.67):
- Sin cambios (mantiene versión actual)
2025-11-27 03:01:06 -03:00
afe57fba1d v1.0.67 Backend / v1.0.66 Frontend - Filtro de usuarios incluye administradores en Informes
Frontend (1.0.66):
- 🔧 Filtro de usuarios ahora incluye admin además de mechanic/mecanico
- Los administradores aparecen en el filtro ya que también pueden hacer inspecciones
- Formato mejorado: "{full_name || username} ({role})"
- Mayor visibilidad de todas las inspecciones realizadas por cualquier usuario autorizado

Backend (1.0.67):
- Sin cambios (mantiene versión actual)
2025-11-27 02:56:55 -03:00
409cbd437a v1.0.67 Backend / v1.0.65 Frontend - Mejora en filtro de mecánicos en pestaña Informes
Frontend (1.0.65):
- 🔧 Filtro de mecánicos muestra nombre completo con rol
- Formato: "{full_name || username} ({role})"
- Ejemplo: "Ron 1 Admin (admin)" en lugar de solo "Ron 1"
- Fallback a username si full_name no está disponible
- Mayor claridad para identificar usuarios en reportes

Backend (1.0.67):
- Sin cambios (mantiene versión actual)
2025-11-27 02:51:57 -03:00
ac17c26c66 test 2025-11-27 02:44:15 -03:00
ef9c37dcdd v1.0.67 Backend / v1.0.64 Frontend - Paginación de 10 elementos en todas las pestañas
Frontend (1.0.64):
- 📄 Paginación en InspectionsTab (10 inspecciones/página)
- 📄 Paginación en ChecklistsTab (10 checklists/página)
- 📊 Paginación en ReportsTab (10 informes/página)
- Auto-reset a página 1 cuando cambian filtros de búsqueda
- Navegación inteligente con puntos suspensivos para rangos grandes
- Muestra primera, última y páginas cercanas (actual ± 1)
- Contador 'Mostrando X-Y de Z' en cada pestaña
- Botones Anterior/Siguiente con estados deshabilitados
- useEffect para sincronizar currentPage con filtros

Mejoras de UX:
- Navegación directa por número de página
- Diseño consistente en las 3 pestañas
- Controles responsive con hover states
- Indicadores visuales claros de página actual

Backend (1.0.67):
- Sin cambios (mantiene versión actual)

Documentación:
- 📝 Agregada sección 'Control de Versiones' en README.md
- Instrucciones detalladas para commits con versiones
- Formato estándar para mensajes de commit
- Tipos de commit (feat, fix, refactor, etc.)
- Reglas de Semantic Versioning
- Ubicación de archivos de versión"
2025-11-27 02:31:20 -03:00
e3ac1c84d7 Listo! Logs eliminados. Frontend v1.0.63. 2025-11-27 02:22:06 -03:00
aa35c8f2eb v1.0.67 Backend / v1.0.61 Frontend - Fix 422: ai_analysis ahora acepta lista
Backend (1.0.67):
- 🐛 Fix: ai_analysis cambió de dict a list en schemas
- Soporta múltiples análisis de IA (una por cada imagen)
- AnswerCreate.ai_analysis: Optional[list] = None
- Answer.ai_analysis: Optional[list] = None
- Compatible con campo JSON en base de datos

Frontend (1.0.61):
- Sin cambios (ya enviaba ai_analysis como array)
- Formato: [{ success, analysis, raw_response, model, provider, imageIndex, fileName }]

Causa del error 422:
- Frontend enviaba: ai_analysis: [{ imageIndex: 1, ... }]
- Backend esperaba: ai_analysis: { ... } (dict)
- Ahora backend acepta: ai_analysis: [{ ... }, { ... }] (list)

Beneficio:
- Ahora se almacenan TODOS los análisis de múltiples imágenes
- Cada elemento del array tiene imageIndex para identificación
- Mantiene trazabilidad completa del análisis IA
2025-11-27 02:11:56 -03:00
d1b4d10257 v1.0.66 Backend / v1.0.60 Frontend - Fix error 422 en análisis IA sin respuesta
Backend (1.0.66):
- 🐛 Fix: answer_value ahora es Optional en AnswerBase schema
- Permite guardar respuestas con solo análisis IA y fotos
- Permite guardar observaciones sin answer_value
- Ya no rechaza con 422 cuando answer_value es null/vacío

Frontend (1.0.60):
- 🐛 Fix: saveAnswer ahora permite guardar si hay:
  * Valor de respuesta, O
  * Observaciones de IA, O
  * Fotos cargadas
- Mejorada lógica de determinación de status
- Solo calcula status si hay answer.value
- Permite guardar análisis IA antes de seleccionar respuesta

Flujo mejorado:
1. Usuario sube fotos
2. Click "Analizar con IA" → genera observaciones
3. Puede avanzar sin seleccionar respuesta (guardará solo observaciones)
4. O puede seleccionar respuesta después → actualiza el record

Causa del error 422:
- answer_value era required en schema
- Al analizar fotos sin seleccionar respuesta se enviaba answer_value=""
- Backend rechazaba con 422 Unprocessable Entity
- Ahora answer_value es opcional y acepta null/vacío
2025-11-27 02:07:17 -03:00
7fb2e40a1e v1.0.65 Backend / v1.0.59 Frontend - Fix client_name + Mejoras en carga de fotos
Backend (1.0.65):
- Fix: Todas las referencias client_name cambiadas a order_number
- Actualizado webhook n8n: "cliente" → "pedido"
- Actualizado contexto IA: "Cliente" → "Nº Pedido"
- PDF ahora muestra "Nº de Pedido" en lugar de "Cliente"

Frontend (1.0.59):
- 📸 NUEVO: Vista previa de fotos cargadas (grid 3 columnas con thumbnails)
- 📸 NUEVO: Botón "✕" para eliminar fotos individuales
- 📸 NUEVO: Botón manual "🤖 Analizar con IA" (no auto-análisis)
- 📸 MEJORA: Permite cargar múltiples fotos respetando max_photos
- 📸 MEJORA: Input file solo required si no hay fotos cargadas
- 📸 MEJORA: Muestra contador "X foto(s) cargada(s)"
- 🔧 Fix: Ya no analiza automáticamente al subir (espera click en botón)
- 🔧 Fix: Permite re-cargar fotos eliminando las anteriores
- 🔧 Fix: Previene exceder max_photos mostrando alerta

UX Improvements:
- Usuario sube 1-3 fotos y las ve en preview
- Puede eliminar individualmente con hover + click en ✕
- Click en "Analizar con IA" procesa todas las fotos juntas
- Análisis secuencial con summary multi-imagen

Nota: No requiere migración (ya ejecutada en v1.0.64)
2025-11-27 01:58:08 -03:00
fdad7b10ad v1.0.64 Backend / v1.0.58 Frontend - Renombrar cliente a N° de Pedido
Backend (1.0.64):
- Renombrado campo client_name a order_number en modelo Inspection
- Actualizado InspectionBase schema con nuevo campo order_number
- Comentario descriptivo: "Número de pedido asociado a la inspección"

Frontend (1.0.58):
- Renombrado client_name a order_number en toda la aplicación
- Actualizado label: "Nombre del Cliente" → "Nº de Pedido"
- Actualizado placeholder: "Juan Pérez" → "PED-12345"
- Actualizado título sección: "Información del Cliente" → "Información del Pedido"
- Actualizado filtro de búsqueda para incluir número de pedido
- Actualizado texto de búsqueda: "cliente" → "Nº pedido"

Database:
- Script de migración: rename_client_name_to_order_number.sql
- Comando: ALTER TABLE inspections RENAME COLUMN client_name TO order_number

Nota: Ejecutar migración SQL antes de usar esta versión
2025-11-27 01:49:42 -03:00
162b278044 ultima correccion con Script de Actualizacion de Git 2025-11-27 01:38:19 -03:00
d8f1c3de10 UPdate Script de Git 2025-11-27 01:37:03 -03:00
e3adb34960 v1.0.63 Backend / v1.0.57 Frontend - Edición y auditoría de preguntas
Backend (1.0.63):
- Agregado modelo QuestionAuditLog para historial de cambios
- Implementado registro de auditoría en create/update/delete de preguntas
- Nuevos endpoints: GET /api/questions/{id}/audit y GET /api/checklists/{id}/questions/audit
- Tracking a nivel de campo con valores antes/después
- Script de migración: add_question_audit_log.sql

Frontend (1.0.57):
- Agregado botón "Editar" en preguntas de checklists
- Implementado formulario de edición con datos pre-cargados
- Agregado botón "Historial" para ver cambios de preguntas
- Modal de auditoría con timeline de cambios y comparación lado a lado
- Fix: Error "firstResult is not defined" en análisis multi-imagen IA
- UI con códigos de color para acciones (crear/modificar/eliminar)
2025-11-27 01:34:54 -03:00
1e5ba305ae v1.0.63 Backend / v1.0.57 Frontend - Edición y auditoría de preguntas 2025-11-27 01:30:34 -03:00
da2469b39e v1.0.63 Backend / v1.0.57 Frontend - Edición y auditoría de preguntas 2025-11-27 01:29:01 -03:00
cdd1b3507b v1.0.63 Backend / v1.0.57 Frontend - Edición y auditoría de preguntas 2025-11-27 01:26:15 -03:00
6f3a6d40f4 Edicion de preguntas 2025-11-27 01:14:21 -03:00
f4b33ce014 Actualizar resultado de analisis de imagenes con ia 2025-11-26 18:17:45 -03:00
822615d2ba Refactorizacion de logica de analisis de las preguntas con la IA y que acepte y respete las reglas de imagenes 2025-11-26 17:58:15 -03:00
6999416be9 Backend corregido para subir el logo de chelists 1.0.61 2025-11-26 17:45:27 -03:00
16a25799e9 trabajar por logos para checklists 2025-11-26 17:42:06 -03:00
637b21d85f Se borraron los archivos temporales de los favicons y tambien se re configuro el logo y la configuracion del logo 2025-11-26 17:31:38 -03:00
aa2b196b53 Se agregaron los favIcons 2025-11-26 17:27:58 -03:00
ac53303930 agregar fiiltros de busqueda en el front 2025-11-26 17:02:26 -03:00
4d3ac4bb5c Cooreegido la exportacion de pdf cuando se edita una checklist ahora si se edita algo de la inspeccion hecha se actualiza el PDF 2025-11-26 14:08:49 -03:00
70f984bfdf Actualziar modelo de PDF 2025-11-26 09:23:35 -03:00
d35f0343e1 bACKEND AY ENVIA NOTIFICACION A N8N ESTA TODO ESTABLE 2025-11-26 01:59:54 -03:00
38dbb07eec BACKEND EN VERSION 1.0.55 CORREGIR ERROR PARA OBENTER IAMGENES LINK PAR ENVIAR A N8N 2025-11-26 01:50:56 -03:00
0987becc25 Agregar envio a n8n usando .env para enviar cuando se guarda una inspeccion 2025-11-26 01:42:25 -03:00
5b82418f0a Agregar en el Modal campo de Nro Operario back y front 2025-11-26 01:35:03 -03:00
822ab5a1cb Actualziacion de analisis de ia con las imagenes y se agrega el campo de cod operario en el front y en el back 2025-11-26 01:20:26 -03:00
cbfab59222 De nuevo actulizar configuraciones con los modelos 2025-11-26 00:31:25 -03:00
83e76f02a2 Corregir modelos de ia en configuraciones 2025-11-26 00:29:05 -03:00
26ed0eb4f0 ACTUALZIAR EL PROMT PRINCIPAL CON EL CUSTOM 2025-11-26 00:20:46 -03:00
b867f11450 custom promtp no esstaba funcionando 2025-11-26 00:13:32 -03:00
98f0d94564 Atualizar campo de IA obseervaciones back 1.0.43 y front 1.0.41 2025-11-25 23:55:34 -03:00
683e260c79 IA va a tener Contexto de la primera plantilal de las Inspecciones backend 1.0.42 frontend 1.0.40 2025-11-25 23:45:20 -03:00
1c4bd93105 elimine los logs algunso logs de debug 2025-11-25 23:27:59 -03:00
773a9336ef Ajuste de mensajes de notificaciones y resolucion de notificaciones duplicadas backend 1.0.40 2025-11-25 23:19:16 -03:00
4c8938a24e back 1.0.38 y front 1.0.39 cambios en condicionales de sub preguntas, solo boleanos 2025-11-25 22:43:14 -03:00
1ef07ad2c5 Cambios Grandes, editro nuevo de preguntas, logica nueva con mas opciones de pregutnas con preguntas hijos hasta 5 niveles 2025-11-25 22:23:21 -03:00
39 changed files with 8372 additions and 765 deletions

View File

@@ -1,7 +1,7 @@
# Database # Database
DATABASE_URL=postgresql://checklist_user:checklist_pass_2024@localhost:5432/checklist_db DATABASE_URL=postgresql://checklist_user:checklist_pass_2024@localhost:5432/checklist_db
# Backend # Backend
SECRET_KEY=your-super-secret-key-min-32-characters-change-this SECRET_KEY=your-super-secret-key-min-32-characters-change-this
ALGORITHM=HS256 ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=10080 ACCESS_TOKEN_EXPIRE_MINUTES=10080

133
.github/copilot-instructions.md vendored Normal file
View File

@@ -0,0 +1,133 @@
# No olvides dar lo comentarios de los cambios que se hicieron para el backend y el front en para los comentarios de git
# Siempre actuliza la version del front y del back la version del front esta en el archivo package.json y la del backend en el archivo main.py en una variable llamada BACKEND_VERSION
# Si el FrontEnd no sufre modificaciones no es necesario actualizar su version, al igual que el backend, solo poner en el comentario de git que no se hicieron cambios en el front o en el backend segun sea el caso
# Ayudetec - Intelligent Checklist System for Automotive Workshops
## Architecture Overview
**Tech Stack**: FastAPI (Python 3.11) + React 18 + PostgreSQL 15 + MinIO/S3
**Deployment**: Docker Compose for dev, Docker Stack for production
**Key Feature**: AI-powered inspection analysis with OpenAI/Gemini integration
### Service Structure
- `backend/` - FastAPI monolith with JWT auth, S3 file storage, PDF generation
- `frontend/` - React SPA with Vite, TailwindCSS, client-side routing
- `postgres` - Main data store with checklist templates, inspections, answers
- MinIO/S3 - Image and PDF storage (configurable endpoint)
### Core Data Model
**Users** (`role`: admin/mechanic/asesor) → **Checklists** (templates with questions) → **Inspections** (mechanic executions) → **Answers** (responses with photos/scores)
- **Permissions**: `ChecklistPermission` table controls mechanic access (empty = global access)
- **Nested Questions**: 5-level deep conditional subquestions via `parent_question_id` + `show_if_answer`
- **Scoring**: Auto-calculated from answer points, stored in `Inspection.score/percentage`
- **Question Types**: boolean, single_choice, multiple_choice, scale, text, number, date, time (config in `Question.options` JSON)
## Development Workflows
### Running Locally
```powershell
docker-compose up -d # Start all services
docker-compose logs backend # Debug backend issues
```
**URLs**: Frontend `http://localhost:5173`, Backend API `http://localhost:8000/docs`
### Database Initialization
Use `init_users.py` to create default admin/mechanic users:
```powershell
docker-compose exec backend python init_users.py
```
**Default credentials**: `admin/admin123`, `mecanico/mec123`
### Migrations
Manual SQL scripts in `migrations/` - run via:
```powershell
docker-compose exec backend python -c "
from app.core.database import engine
with open('migrations/your_migration.sql') as f:
engine.execute(f.read())
"
```
### Building for Production
```powershell
.\build-and-push.ps1 # Builds images, pushes to Docker Hub (configured in script)
```
Then deploy with `docker-compose.prod.yml` or `docker-stack.yml` (Swarm mode)
## Project-Specific Conventions
### API Architecture (`backend/app/main.py`)
- **Single 2800+ line file** - all endpoints in main.py (no routers/controllers split)
- Auth via `get_current_user()` dependency returning `models.User`
- File uploads use boto3 S3 client configured from `app/core/config.py` (MinIO compatible)
- PDF generation inline with ReportLab (starts ~line 1208, function `generate_pdf`)
### AI Integration Modes (see `AI_FUNCTIONALITY.md`)
- `ai_mode` on Checklist: `"off"` (manual) | `"assisted"` (suggestions) | `"copilot"` (auto-complete)
- AI analysis triggered on photo upload, stored in `Answer.ai_analysis` JSON
- Webhook notifications to n8n on completion: `send_completed_inspection_to_n8n()`
### Frontend Patterns (`frontend/src/App.jsx`)
- **5400+ line single-file component** - all views in App.jsx (Login, Dashboard, Admin panels)
- Auth state in `localStorage` (token + user object)
- API calls use `fetch()` with `import.meta.env.VITE_API_URL` base
- Signature capture via `react-signature-canvas` (saved as base64)
### Permission Model
When fetching checklists:
- Admins see all checklists
- Mechanics only see checklists with either:
- No `ChecklistPermission` records (global access), OR
- A permission record linking that mechanic
- Implement via JOIN in `GET /api/checklists` endpoint
### Environment Variables
Critical settings in `.env`:
- `DATABASE_URL` - Postgres connection string
- `SECRET_KEY` - JWT signing (min 32 chars)
- `OPENAI_API_KEY` / `GEMINI_API_KEY` - Optional for AI features
- `MINIO_*` vars - S3-compatible storage (MinIO/AWS S3)
- `NOTIFICACION_ENDPOINT` - n8n webhook for inspection events
- `ALLOWED_ORIGINS` - Comma-separated CORS origins
## Key Integration Points
### S3/MinIO Storage
- Configured globally in `main.py` via `boto3.client()` + `Config(signature_version='s3v4')`
- Two buckets: `MINIO_IMAGE_BUCKET` (photos), `MINIO_PDF_BUCKET` (reports)
- Upload pattern: generate UUID filename, `s3_client.upload_fileobj()`, store URL in DB
### PDF Generation
- ReportLab library generates inspection reports with photos, signatures, scoring
- Triggered on inspection completion, stored to S3, URL saved in `Inspection.pdf_url`
- Uses checklist logo from `Checklist.logo_url` if available
### Webhook Notifications
When `Question.send_notification = true`, answering triggers `send_answer_notification()` to `NOTIFICACION_ENDPOINT`
On inspection completion, sends full data via `send_completed_inspection_to_n8n()`
## Common Gotchas
- **No Alembic auto-migrations** - use manual SQL scripts in `migrations/`
- **Token expiry is 7 days** - set in `config.ACCESS_TOKEN_EXPIRE_MINUTES = 10080`
- **Photos stored as S3 URLs** - not base64 in DB (except signatures)
- **Nested questions limited to 5 levels** - enforced in `Question.depth_level`
- **PowerShell scripts** - Windows-first project (see `.ps1` build scripts)
- **Frontend has no state management** - uses React `useState` only, no Redux/Context
## Checklist Management
### Editing Checklists
- Admins can edit checklist name, description, AI mode, and scoring settings via "✏️ Editar" button
- Edit modal (`showEditChecklistModal`) uses PUT `/api/checklists/{id}` endpoint
- Backend endpoint supports partial updates via `exclude_unset=True` in Pydantic model
- Logo and permissions have separate management modals for focused UI
## Example Tasks
**Add new question type**: Update `Question.type` validation, modify `QuestionTypeEditor.jsx` UI, handle in `QuestionAnswerInput.jsx`
**Change scoring logic**: Edit `backend/app/main.py` answer submission endpoint, recalculate `Inspection.score`
**Add new user role**: Update `User.role` enum, modify `get_current_user()` checks, adjust frontend role conditionals in `App.jsx`
**Edit checklist properties**: Use existing PUT endpoint, add fields to `editChecklistData` state, update modal form

2
.gitignore vendored
View File

@@ -29,6 +29,8 @@ dist-ssr/
*.swp *.swp
*.swo *.swo
*~ *~
__pycache__/
*.pyc
# OS # OS
.DS_Store .DS_Store

View File

@@ -360,6 +360,54 @@ UPDATE checklists SET max_score = (
MIT License - Uso libre para proyectos comerciales y personales MIT License - Uso libre para proyectos comerciales y personales
## 📝 Control de Versiones
### Instrucciones para commits de Git
**IMPORTANTE**: Siempre incluir la versión actualizada en los mensajes de commit.
Formato recomendado:
```bash
git add .
git commit -m "tipo: descripción del cambio
- Detalle 1
- Detalle 2
- Frontend vX.X.XX / Backend vX.X.XX"
```
Tipos de commit:
- `feat`: Nueva funcionalidad
- `fix`: Corrección de bugs
- `refactor`: Refactorización de código
- `style`: Cambios de formato/estilo
- `docs`: Actualización de documentación
- `perf`: Mejoras de rendimiento
- `test`: Añadir o actualizar tests
**Ejemplo real**:
```bash
git add .
git commit -m "feat: Add pagination (10 items/page) to all main tabs
- Pagination for Inspections, Checklists, and Reports
- Auto-reset on filter changes
- Smart page navigation with ellipsis
- Result counters showing X-Y of Z items
- Frontend v1.0.64"
```
### Versionado
Seguir **Semantic Versioning** (MAJOR.MINOR.PATCH):
- **MAJOR**: Cambios incompatibles en la API
- **MINOR**: Nueva funcionalidad compatible con versiones anteriores
- **PATCH**: Correcciones de bugs
Ubicación de versiones:
- Frontend: `frontend/package.json``"version": "X.X.XX"`
- Backend: `backend/app/main.py``version="X.X.XX"` en FastAPI app
## 🆘 Soporte ## 🆘 Soporte
Para problemas o preguntas: Para problemas o preguntas:

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,7 @@ class User(Base):
password_hash = Column(String(255), nullable=False) password_hash = Column(String(255), nullable=False)
role = Column(String(20), nullable=False) # admin, mechanic, asesor role = Column(String(20), nullable=False) # admin, mechanic, asesor
full_name = Column(String(100)) full_name = Column(String(100))
employee_code = Column(String(50)) # Nro Operario - código de otro sistema
is_active = Column(Boolean, default=True) is_active = Column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), server_default=func.now()) created_at = Column(DateTime(timezone=True), server_default=func.now())
@@ -65,23 +66,29 @@ class Question(Base):
checklist_id = Column(Integer, ForeignKey("checklists.id"), nullable=False) checklist_id = Column(Integer, ForeignKey("checklists.id"), nullable=False)
section = Column(String(100)) # Sistema eléctrico, Frenos, etc section = Column(String(100)) # Sistema eléctrico, Frenos, etc
text = Column(Text, nullable=False) text = Column(Text, nullable=False)
type = Column(String(30), nullable=False) # pass_fail, good_bad, text, etc type = Column(String(30), nullable=False) # boolean, single_choice, multiple_choice, scale, text, number, date, time
points = Column(Integer, default=1) points = Column(Integer, default=1)
options = Column(JSON) # Para multiple choice options = Column(JSON) # Configuración flexible según tipo de pregunta
order = Column(Integer, default=0) order = Column(Integer, default=0)
allow_photos = Column(Boolean, default=True) allow_photos = Column(Boolean, default=True) # DEPRECATED: usar photo_requirement
photo_requirement = Column(String(20), default='optional') # none, optional, required
max_photos = Column(Integer, default=3) max_photos = Column(Integer, default=3)
requires_comment_on_fail = Column(Boolean, default=False) requires_comment_on_fail = Column(Boolean, default=False)
send_notification = Column(Boolean, default=False) send_notification = Column(Boolean, default=False)
# Conditional logic # Conditional logic - Subpreguntas anidadas hasta 5 niveles
parent_question_id = Column(Integer, ForeignKey("questions.id"), nullable=True) parent_question_id = Column(Integer, ForeignKey("questions.id"), nullable=True)
show_if_answer = Column(String(50), nullable=True) # Valor que dispara esta pregunta show_if_answer = Column(String(50), nullable=True) # Valor que dispara esta pregunta
depth_level = Column(Integer, default=0) # 0=principal, 1-5=subpreguntas anidadas
# AI Analysis # AI Analysis
ai_prompt = Column(Text, nullable=True) # Prompt personalizado para análisis de IA de esta pregunta ai_prompt = Column(Text, nullable=True) # Prompt personalizado para análisis de IA de esta pregunta
# Soft Delete
is_deleted = Column(Boolean, default=False) # Soft delete: mantiene integridad de respuestas históricas
created_at = Column(DateTime(timezone=True), server_default=func.now()) created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Relationships # Relationships
checklist = relationship("Checklist", back_populates="questions") checklist = relationship("Checklist", back_populates="questions")
@@ -106,7 +113,10 @@ class Inspection(Base):
vehicle_brand = Column(String(50)) vehicle_brand = Column(String(50))
vehicle_model = Column(String(100)) vehicle_model = Column(String(100))
vehicle_km = Column(Integer) vehicle_km = Column(Integer)
client_name = Column(String(200)) order_number = Column(String(200)) # Nº de Pedido
# Datos del mecánico
mechanic_employee_code = Column(String(50)) # Código de operario del mecánico
# Scoring # Scoring
score = Column(Integer, default=0) score = Column(Integer, default=0)
@@ -115,7 +125,7 @@ class Inspection(Base):
flagged_items_count = Column(Integer, default=0) flagged_items_count = Column(Integer, default=0)
# Estado # Estado
status = Column(String(20), default="draft") # draft, completed, inactive status = Column(String(20), default="incomplete") # incomplete, completed, inactive
is_active = Column(Boolean, default=True) is_active = Column(Boolean, default=True)
# Firma # Firma
@@ -148,6 +158,7 @@ class Answer(Base):
comment = Column(Text) # Comentarios adicionales comment = Column(Text) # Comentarios adicionales
ai_analysis = Column(JSON) # Análisis de IA si aplica ai_analysis = Column(JSON) # Análisis de IA si aplica
chat_history = Column(JSON) # Historial de chat con AI Assistant (para tipo ai_assistant)
is_flagged = Column(Boolean, default=False) # Si requiere atención is_flagged = Column(Boolean, default=False) # Si requiere atención
created_at = Column(DateTime(timezone=True), server_default=func.now()) created_at = Column(DateTime(timezone=True), server_default=func.now())
@@ -203,6 +214,27 @@ class ChecklistPermission(Base):
mechanic = relationship("User") mechanic = relationship("User")
class QuestionAuditLog(Base):
"""Registro de auditoría para cambios en preguntas de checklists"""
__tablename__ = "question_audit_log"
id = Column(Integer, primary_key=True, index=True)
question_id = Column(Integer, ForeignKey("questions.id", ondelete="CASCADE"), nullable=False)
checklist_id = Column(Integer, ForeignKey("checklists.id", ondelete="CASCADE"), nullable=False)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
action = Column(String(50), nullable=False) # created, updated, deleted
field_name = Column(String(100), nullable=True) # Campo modificado
old_value = Column(Text, nullable=True) # Valor anterior
new_value = Column(Text, nullable=True) # Valor nuevo
comment = Column(Text, nullable=True) # Comentario del cambio
created_at = Column(DateTime(timezone=True), server_default=func.now())
# Relationships
question = relationship("Question")
checklist = relationship("Checklist")
user = relationship("User")
class InspectionAuditLog(Base): class InspectionAuditLog(Base):
"""Registro de auditoría para cambios en inspecciones y respuestas""" """Registro de auditoría para cambios en inspecciones y respuestas"""
__tablename__ = "inspection_audit_log" __tablename__ = "inspection_audit_log"

View File

@@ -7,6 +7,7 @@ class UserBase(BaseModel):
username: str username: str
email: Optional[EmailStr] = None email: Optional[EmailStr] = None
full_name: Optional[str] = None full_name: Optional[str] = None
employee_code: Optional[str] = None # Nro Operario - código de otro sistema
role: str = "mechanic" role: str = "mechanic"
class UserCreate(UserBase): class UserCreate(UserBase):
@@ -16,6 +17,7 @@ class UserUpdate(BaseModel):
username: Optional[str] = None username: Optional[str] = None
email: Optional[EmailStr] = None email: Optional[EmailStr] = None
full_name: Optional[str] = None full_name: Optional[str] = None
employee_code: Optional[str] = None
role: Optional[str] = None role: Optional[str] = None
class UserPasswordUpdate(BaseModel): class UserPasswordUpdate(BaseModel):
@@ -31,6 +33,7 @@ class UserLogin(BaseModel):
class User(UserBase): class User(UserBase):
id: int id: int
employee_code: Optional[str] = None
is_active: bool is_active: bool
created_at: datetime created_at: datetime
@@ -94,20 +97,37 @@ class Checklist(ChecklistBase):
# Question Schemas # Question Schemas
# Tipos de preguntas soportados:
# - boolean: Dos opciones personalizables (ej: Sí/No, Pasa/Falla)
# - single_choice: Selección única con N opciones
# - multiple_choice: Selección múltiple
# - scale: Escala numérica (1-5, 1-10, etc.)
# - text: Texto libre
# - number: Valor numérico
# - date: Fecha
# - time: Hora
class QuestionBase(BaseModel): class QuestionBase(BaseModel):
section: Optional[str] = None section: Optional[str] = None
text: str text: str
type: str type: str # boolean, single_choice, multiple_choice, scale, text, number, date, time
points: int = 1 points: int = 1
options: Optional[dict] = None options: Optional[dict] = None # Configuración flexible según tipo
# Estructura de options:
# Boolean: {"type": "boolean", "choices": [{"value": "yes", "label": "Sí", "points": 1, "status": "ok"}, ...]}
# Single/Multiple Choice: {"type": "single_choice", "choices": [{"value": "opt1", "label": "Opción 1", "points": 2}, ...]}
# Scale: {"type": "scale", "min": 1, "max": 5, "step": 1, "labels": {"min": "Muy malo", "max": "Excelente"}}
# Text: {"type": "text", "multiline": true, "max_length": 500}
order: int = 0 order: int = 0
allow_photos: bool = True allow_photos: bool = True # DEPRECATED: mantener por compatibilidad
photo_requirement: Optional[str] = 'optional' # none, optional, required
max_photos: int = 3 max_photos: int = 3
requires_comment_on_fail: bool = False requires_comment_on_fail: bool = False
send_notification: bool = False send_notification: bool = False
parent_question_id: Optional[int] = None parent_question_id: Optional[int] = None
show_if_answer: Optional[str] = None show_if_answer: Optional[str] = None
ai_prompt: Optional[str] = None ai_prompt: Optional[str] = None
is_deleted: bool = False
class QuestionCreate(QuestionBase): class QuestionCreate(QuestionBase):
checklist_id: int checklist_id: int
@@ -115,15 +135,37 @@ class QuestionCreate(QuestionBase):
class QuestionUpdate(QuestionBase): class QuestionUpdate(QuestionBase):
pass pass
class QuestionReorder(BaseModel):
question_id: int
new_order: int
class Question(QuestionBase): class Question(QuestionBase):
id: int id: int
checklist_id: int checklist_id: int
created_at: datetime created_at: datetime
updated_at: Optional[datetime] = None
class Config: class Config:
from_attributes = True from_attributes = True
# Question Audit Schemas
class QuestionAuditLog(BaseModel):
id: int
question_id: int
checklist_id: int
user_id: int
action: str
field_name: Optional[str] = None
old_value: Optional[str] = None
new_value: Optional[str] = None
comment: Optional[str] = None
created_at: datetime
user: Optional['User'] = None
class Config:
from_attributes = True
# Inspection Schemas # Inspection Schemas
class InspectionBase(BaseModel): class InspectionBase(BaseModel):
@@ -133,7 +175,8 @@ class InspectionBase(BaseModel):
vehicle_brand: Optional[str] = None vehicle_brand: Optional[str] = None
vehicle_model: Optional[str] = None vehicle_model: Optional[str] = None
vehicle_km: Optional[int] = None vehicle_km: Optional[int] = None
client_name: Optional[str] = None order_number: Optional[str] = None # Nº de Pedido
mechanic_employee_code: Optional[str] = None
class InspectionCreate(InspectionBase): class InspectionCreate(InspectionBase):
checklist_id: int checklist_id: int
@@ -149,6 +192,7 @@ class Inspection(InspectionBase):
id: int id: int
checklist_id: int checklist_id: int
mechanic_id: int mechanic_id: int
mechanic_employee_code: Optional[str] = None
score: int score: int
max_score: int max_score: int
percentage: float percentage: float
@@ -163,7 +207,7 @@ class Inspection(InspectionBase):
# Answer Schemas # Answer Schemas
class AnswerBase(BaseModel): class AnswerBase(BaseModel):
answer_value: str answer_value: Optional[str] = None # Opcional para permitir guardar solo análisis IA
status: str = "ok" status: str = "ok"
comment: Optional[str] = None comment: Optional[str] = None
is_flagged: bool = False is_flagged: bool = False
@@ -171,6 +215,8 @@ class AnswerBase(BaseModel):
class AnswerCreate(AnswerBase): class AnswerCreate(AnswerBase):
inspection_id: int inspection_id: int
question_id: int question_id: int
ai_analysis: Optional[list] = None # Lista de análisis de IA (soporta múltiples imágenes)
chat_history: Optional[list] = None # Historial de chat con AI Assistant
class AnswerUpdate(AnswerBase): class AnswerUpdate(AnswerBase):
pass pass
@@ -180,7 +226,8 @@ class Answer(AnswerBase):
inspection_id: int inspection_id: int
question_id: int question_id: int
points_earned: int points_earned: int
ai_analysis: Optional[dict] = None ai_analysis: Optional[list] = None # Lista de análisis de IA
chat_history: Optional[list] = None # Historial de chat con AI Assistant
created_at: datetime created_at: datetime
class Config: class Config:
@@ -222,9 +269,10 @@ class InspectionDetail(Inspection):
# AI Configuration Schemas # AI Configuration Schemas
class AIConfigurationBase(BaseModel): class AIConfigurationBase(BaseModel):
provider: str # openai, gemini provider: str # openai, gemini, anthropic
api_key: str api_key: str
model_name: Optional[str] = None model_name: Optional[str] = None
logo_url: Optional[str] = None
class AIConfigurationCreate(AIConfigurationBase): class AIConfigurationCreate(AIConfigurationBase):
pass pass
@@ -233,6 +281,7 @@ class AIConfigurationUpdate(BaseModel):
provider: Optional[str] = None provider: Optional[str] = None
api_key: Optional[str] = None api_key: Optional[str] = None
model_name: Optional[str] = None model_name: Optional[str] = None
logo_url: Optional[str] = None
is_active: Optional[bool] = None is_active: Optional[bool] = None
class AIConfiguration(AIConfigurationBase): class AIConfiguration(AIConfigurationBase):

View File

@@ -22,4 +22,4 @@ if ($LASTEXITCODE -ne 0) {
} }
Write-Host "`n=== Proceso completado exitosamente ===`n" -ForegroundColor Green Write-Host "`n=== Proceso completado exitosamente ===`n" -ForegroundColor Green
pause

View File

@@ -0,0 +1,44 @@
-- Migration: Add question_audit_log table
-- Date: 2025-11-27
-- Description: Add audit logging for question changes
CREATE TABLE IF NOT EXISTS question_audit_log (
id SERIAL PRIMARY KEY,
question_id INTEGER NOT NULL,
checklist_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
action VARCHAR(50) NOT NULL,
field_name VARCHAR(100),
old_value TEXT,
new_value TEXT,
comment TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
-- Foreign keys
CONSTRAINT fk_question_audit_question
FOREIGN KEY (question_id)
REFERENCES questions(id)
ON DELETE CASCADE,
CONSTRAINT fk_question_audit_checklist
FOREIGN KEY (checklist_id)
REFERENCES checklists(id)
ON DELETE CASCADE,
CONSTRAINT fk_question_audit_user
FOREIGN KEY (user_id)
REFERENCES users(id)
);
-- Create indexes for better query performance
CREATE INDEX idx_question_audit_question_id ON question_audit_log(question_id);
CREATE INDEX idx_question_audit_checklist_id ON question_audit_log(checklist_id);
CREATE INDEX idx_question_audit_created_at ON question_audit_log(created_at);
CREATE INDEX idx_question_audit_action ON question_audit_log(action);
-- Add comment to table
COMMENT ON TABLE question_audit_log IS 'Registro de auditoría para cambios en preguntas de checklists';
COMMENT ON COLUMN question_audit_log.action IS 'Tipo de acción: created, updated, deleted';
COMMENT ON COLUMN question_audit_log.field_name IS 'Nombre del campo modificado (solo para updates)';
COMMENT ON COLUMN question_audit_log.old_value IS 'Valor anterior del campo';
COMMENT ON COLUMN question_audit_log.new_value IS 'Valor nuevo del campo';

View File

@@ -0,0 +1,10 @@
-- Migration: Rename client_name to order_number
-- Date: 2025-11-27
-- Description: Cambiar campo client_name a order_number en tabla inspections
-- Renombrar la columna
ALTER TABLE inspections
RENAME COLUMN client_name TO order_number;
-- Actualizar comentario de la columna
COMMENT ON COLUMN inspections.order_number IS 'Número de pedido asociado a la inspección';

View File

@@ -10,10 +10,12 @@ python-jose[cryptography]==3.3.0
passlib==1.7.4 passlib==1.7.4
bcrypt==4.0.1 bcrypt==4.0.1
python-multipart==0.0.6 python-multipart==0.0.6
openai==1.10.0 openai==1.57.4
anthropic==0.40.0
google-generativeai==0.3.2 google-generativeai==0.3.2
Pillow==10.2.0 Pillow==10.2.0
reportlab==4.0.9 reportlab==4.0.9
pypdf==4.0.1
python-dotenv==1.0.0 python-dotenv==1.0.0
boto3==1.34.89 boto3==1.34.89
requests==2.31.0 requests==2.31.0

0
docker.ps1 Normal file
View File

View File

@@ -0,0 +1,155 @@
# Mejoras en la Extracción de PDFs con IA
## Versión Backend: 1.0.95
## Problema Original
El sistema tenía limitaciones al procesar PDFs con IA:
1. **Límites muy pequeños**: Solo extraía 2,000-4,000 caracteres
2. **Sin manejo de duplicaciones**: Páginas repetidas se procesaban múltiples veces
3. **No aprovechaba contextos largos**: Los modelos modernos soportan millones de tokens
4. **Falta de información**: No reportaba páginas procesadas o si el contenido fue truncado
## Solución Implementada
### 1. Función Centralizada de Extracción
Nueva función `extract_pdf_text_smart()` que:
- ✅ Extrae texto de forma inteligente
- ✅ Detecta y evita páginas duplicadas
- ✅ Maneja límites configurables
- ✅ Reporta estadísticas completas (páginas, caracteres, truncado)
- ✅ Manejo robusto de errores
```python
pdf_result = extract_pdf_text_smart(pdf_content, max_chars=50000)
# Retorna:
# {
# 'text': '...',
# 'pages': 10,
# 'pages_processed': 9, # Si una página estaba duplicada
# 'total_chars': 45000,
# 'truncated': False,
# 'success': True
# }
```
### 2. Límites Optimizados por Caso de Uso
| Endpoint | Límite Anterior | Límite Nuevo | Modelo Objetivo |
|----------|----------------|--------------|-----------------|
| `/api/analyze-image` (OpenAI) | 4,000 chars | 30,000 chars | GPT-4 (128k tokens) |
| `/api/analyze-image` (Gemini) | 4,000 chars | 100,000 chars | Gemini 1.5/2.0 (2M tokens) |
| `/api/ai/chat-assistant` | 2,000 chars | 50,000 chars | Equilibrado para contexto |
### 3. Detección de Duplicaciones
El sistema ahora verifica si el contenido de una página ya existe antes de agregarlo:
```python
if page_text.strip() not in full_text:
full_text += f"\n--- Página {page_num}/{total_pages} ---\n{page_text.strip()}\n"
```
Esto previene:
- PDFs con páginas idénticas repetidas
- Documentos mal generados con contenido duplicado
- Uso innecesario de tokens en el análisis IA
### 4. Información Mejorada
El sistema ahora reporta:
- **Páginas totales**: Total de páginas en el PDF
- **Páginas procesadas**: Páginas únicas con contenido
- **Caracteres totales**: Tamaño real del texto extraído
- **Indicador de truncado**: Si el PDF fue limitado
Ejemplo de output:
```
📄 PDF procesado: manual-vehiculo.pdf - 87450 caracteres, 8/10 páginas (TRUNCADO)
```
## Capacidades de Contexto por Modelo
### OpenAI GPT-4
- **Contexto**: ~128,000 tokens (~500,000 caracteres)
- **Límite aplicado**: 30,000 caracteres
- **Razón**: Balance entre contexto útil y costo
### Gemini 1.5/2.0 Pro
- **Contexto**: 2,000,000 tokens (~8,000,000 caracteres)
- **Límite aplicado**: 100,000 caracteres
- **Razón**: Aprovechar contexto masivo sin sobrecargar
### Chat Assistant
- **Límite**: 50,000 caracteres
- **Razón**: Incluye historial + contexto de fotos + PDF
## Casos de Uso Soportados
### ✅ PDFs Pequeños (1-5 páginas)
Extracción completa sin truncado
### ✅ PDFs Medianos (5-20 páginas)
Extracción completa o parcial según contenido
### ✅ PDFs Grandes (20+ páginas)
Extracción inteligente con truncado después de límite
### ✅ PDFs con Páginas Duplicadas
Detección automática y eliminación
### ✅ Múltiples PDFs en Chat
Cada uno procesado independientemente con su límite
## Indicadores de Estado
### En Logs del Servidor
```
📄 PDF procesado: documento.pdf - 25000 caracteres, 10/10 páginas
📄 PDF procesado: manual.pdf - 50000 caracteres, 15/20 páginas (TRUNCADO)
```
### En Respuesta al Cliente
```json
{
"attached_files": [
{
"filename": "manual.pdf",
"type": "application/pdf",
"pages": 20,
"pages_processed": 15,
"total_chars": 75000,
"truncated": true
}
]
}
```
## Próximas Mejoras Potenciales
1. **Chunking Inteligente**: Para PDFs muy grandes, dividir en chunks semánticos
2. **OCR Integrado**: Detectar PDFs escaneados y aplicar OCR automático
3. **Resumen Automático**: Para PDFs grandes, generar resumen antes de análisis
4. **Cache de Extracciones**: Guardar texto extraído en DB para reutilización
## Migración
No requiere migración de base de datos. Los cambios son retrocompatibles.
## Testing
Para probar las mejoras:
1. **PDF pequeño** (< 10 páginas): Debe procesarse completo
2. **PDF grande** (> 50 páginas): Debe truncarse y reportar info
3. **PDF con duplicados**: Debe eliminar páginas repetidas
4. **Múltiples PDFs**: Cada uno procesado independientemente
## Notas Técnicas
- La función `extract_pdf_text_smart()` está en `main.py` línea ~210
- Usa `pypdf.PdfReader` para extracción
- Maneja encoding UTF-8 automáticamente
- Thread-safe (usa BytesIO)

177
docs/pdf-regeneration.md Normal file
View File

@@ -0,0 +1,177 @@
# Regeneración Automática de PDF al Editar Respuestas
## Descripción General
Se ha implementado la funcionalidad de regeneración automática del PDF de inspección cuando se editan respuestas en inspecciones completadas.
## Cambios Implementados
### 1. Nueva Función Reutilizable: `generate_inspection_pdf()`
**Ubicación**: `backend/app/main.py` (línea ~1046)
**Propósito**: Generar el PDF de una inspección y subirlo a S3.
**Parámetros**:
- `inspection_id: int` - ID de la inspección
- `db: Session` - Sesión de base de datos
**Retorna**: `str` - URL del PDF generado en S3
**Características**:
- Genera PDF profesional con diseño A4
- Incluye toda la información de la inspección
- Sube automáticamente a S3/MinIO
- Sobrescribe PDF existente si ya existe
- Maneja errores y excepciones
### 2. Actualización de `complete_inspection()`
**Ubicación**: `backend/app/main.py` (línea ~1358)
**Cambios**:
- Removido código duplicado de generación de PDF
- Ahora usa la función `generate_inspection_pdf()`
- Código más limpio y mantenible
**Antes**:
```python
# 300+ líneas de código de generación de PDF inline
```
**Después**:
```python
# Generar PDF usando función reutilizable
pdf_url = generate_inspection_pdf(inspection_id, db)
inspection.pdf_url = pdf_url
```
### 3. Actualización de `update_answer()`
**Ubicación**: `backend/app/main.py` (línea ~1497)
**Nuevas Funcionalidades**:
1. **Verificación de Estado**: Comprueba si la inspección está completada
2. **Recálculo de Puntuación**: Actualiza score, porcentaje y contadores
3. **Regeneración de PDF**: Genera nuevo PDF con los cambios
4. **Manejo de Errores**: No interrumpe la actualización si falla la generación del PDF
**Flujo de Trabajo**:
```python
1. Usuario edita respuesta
2. Backend actualiza Answer en BD
3. Backend verifica si inspection.status == "completed"
4. Si está completada:
a. Recalcula score total
b. Recalcula porcentaje
c. Recalcula items críticos
d. Genera nuevo PDF
e. Actualiza inspection.pdf_url
5. Retorna Answer actualizado
```
## Casos de Uso
### Caso 1: Editar Respuesta en Inspección en Progreso
```
- Usuario edita respuesta
- Respuesta se actualiza
- PDF NO se regenera (inspección no completada)
```
### Caso 2: Editar Respuesta en Inspección Completada
```
- Usuario edita respuesta
- Respuesta se actualiza
- Sistema detecta que inspección está completada
- Score se recalcula automáticamente
- PDF se regenera con los nuevos datos
- PDF anterior es sobrescrito en S3
```
## Ventajas de la Nueva Implementación
1. **DRY (Don't Repeat Yourself)**: Código de generación de PDF existe una sola vez
2. **Mantenibilidad**: Cambios al PDF solo se hacen en un lugar
3. **Automatización**: PDFs siempre reflejan el estado actual
4. **Consistencia**: Mismo diseño profesional en todas partes
5. **Robustez**: Manejo de errores sin interrumpir flujo principal
## Estructura del PDF Generado
El PDF incluye:
### Portada
- Título e ID de inspección
- Cuadro de información del vehículo (azul)
- Cuadro de información del cliente y mecánico (verde)
- Resumen de puntuación con colores según porcentaje
### Detalle de Inspección
- Agrupado por secciones
- Cada pregunta con:
- Icono de estado (✓ ok, ⚠ warning, ✕ critical)
- Respuesta y estado
- Comentarios
- Galería de imágenes (6 por fila)
### Footer
- Timestamp de generación
## Logs y Debugging
El sistema imprime logs útiles:
```python
# Al regenerar PDF
🔄 Regenerando PDF para inspección completada #123
# Al completar regeneración
PDF generado y subido a S3: https://...
# Si hay error
Error regenerando PDF: [detalle]
```
## Versión del Backend
**Versión actual**: `1.0.26`
Se incrementó la versión para reflejar esta nueva funcionalidad.
## Notas Técnicas
### S3/MinIO
- Los PDFs sobrescriben el archivo anterior con el mismo nombre
- Ruta: `{año}/{mes}/inspeccion_{id}_{placa}.pdf`
- Content-Type: `application/pdf`
### Base de Datos
- Campo `inspection.pdf_url` se actualiza automáticamente
- Score, porcentaje y flagged_items_count se recalculan
- Todo en una sola transacción
### Manejo de Errores
- Si falla la generación del PDF, se registra el error
- La actualización de la respuesta NO se revierte
- Se imprime traceback completo para debugging
## Próximos Pasos Sugeridos
1. ✅ Implementar regeneración de PDF (COMPLETADO)
2. ⏳ Ejecutar migraciones SQL para employee_code
3. ⏳ Probar flujo completo en ambiente de desarrollo
4. ⏳ Considerar notificación a n8n cuando se edita inspección completada
5. ⏳ Agregar campo `updated_at` a inspecciones para tracking de cambios
## Testing
Para probar la funcionalidad:
1. Completar una inspección
2. Verificar que se genera el PDF
3. Editar una respuesta (cambiar status, comentario, etc.)
4. Verificar en logs que se regenera el PDF
5. Descargar el PDF y confirmar que refleja los cambios
6. Verificar que el score se recalculó correctamente

196
docs/webhook-n8n.md Normal file
View File

@@ -0,0 +1,196 @@
# Documentación de Webhook - n8n
## Endpoint
El endpoint configurado en `.env`:
```
NOTIFICACION_ENDPOINT=https://n8nw.comercialarmin.com.py/webhook/53284540-edc4-418f-b1bf-a70a805f8212
```
## Evento: Inspección Completada
### Cuándo se envía
Cuando se completa una inspección (endpoint: `POST /api/inspections/{id}/complete`)
### Estructura del JSON
```json
{
"tipo": "inspeccion_completada",
"inspeccion": {
"id": 123,
"estado": "completed",
"or_number": "OR-001",
"work_order_number": "WO-123",
"vehiculo": {
"placa": "ABC-123",
"marca": "Toyota",
"modelo": "Corolla 2020",
"kilometraje": 50000
},
"cliente": "Juan Pérez",
"mecanico": {
"id": 5,
"nombre": "Carlos Méndez",
"email": "carlos@example.com",
"codigo_operario": "OPR-001"
},
"checklist": {
"id": 1,
"nombre": "Inspección Vehicular Completa"
},
"puntuacion": {
"obtenida": 85,
"maxima": 100,
"porcentaje": 85.0,
"items_criticos": 2
},
"fechas": {
"inicio": "2025-11-26T10:30:00",
"completado": "2025-11-26T11:45:00"
},
"pdf_url": "https://minioapi.ayutec.es/pdfs/2025/11/inspeccion_123_ABC-123.pdf",
"firma": "data:image/png;base64,..."
},
"respuestas": [
{
"id": 1,
"pregunta": {
"id": 10,
"texto": "¿Estado de los neumáticos?",
"seccion": "Neumáticos",
"orden": 1
},
"respuesta": "ok",
"estado": "ok",
"comentario": "Neumáticos en buen estado",
"observaciones": "Presión correcta en las 4 ruedas",
"puntos_obtenidos": 1,
"es_critico": false,
"imagenes": [
{
"id": 100,
"url": "https://minioapi.ayutec.es/images/2025/11/foto1.jpg",
"filename": "neumatico_delantero.jpg"
},
{
"id": 101,
"url": "https://minioapi.ayutec.es/images/2025/11/foto2.jpg",
"filename": "neumatico_trasero.jpg"
}
],
"ai_analysis": {
"status": "ok",
"observations": "Los neumáticos presentan un desgaste uniforme...",
"recommendation": "Continuar con el mantenimiento preventivo",
"confidence": 0.95
}
},
{
"id": 2,
"pregunta": {
"id": 11,
"texto": "¿Luces delanteras funcionan?",
"seccion": "Iluminación",
"orden": 2
},
"respuesta": "warning",
"estado": "warning",
"comentario": "Faro izquierdo opaco",
"observaciones": "Requiere restauración de faro",
"puntos_obtenidos": 0.5,
"es_critico": true,
"imagenes": [
{
"id": 102,
"url": "https://minioapi.ayutec.es/images/2025/11/foto3.jpg",
"filename": "faro_izquierdo.jpg"
}
],
"ai_analysis": {
"status": "minor",
"observations": "Se detecta opacidad en el faro izquierdo...",
"recommendation": "Pulir o restaurar el lente del faro",
"confidence": 0.9
}
}
],
"timestamp": "2025-11-26T11:45:30.123456"
}
```
## Campos Importantes
### Imágenes
- Cada respuesta incluye un array `imagenes` con:
- `id`: ID del archivo en la base de datos
- `url`: **URL directa** de la imagen en MinIO (lista para descargar/mostrar)
- `filename`: Nombre original del archivo
### AI Analysis
- Si la pregunta fue analizada por IA, incluye:
- `status`: ok/minor/critical
- `observations`: Observaciones del análisis
- `recommendation`: Recomendaciones
- `confidence`: Nivel de confianza (0-1)
### Código de Operario
- Se incluye en `inspeccion.mecanico.codigo_operario`
- Se copia automáticamente del perfil del mecánico al crear la inspección
### PDF
- URL del PDF generado en `inspeccion.pdf_url`
- Incluye miniaturas de todas las imágenes
## Uso en n8n
### 1. Webhook Trigger
Configura un nodo Webhook con la URL del archivo `.env`
### 2. Filtrar por tipo
```javascript
// Verificar si es una inspección completada
{{ $json.tipo === "inspeccion_completada" }}
```
### 3. Acceder a las imágenes
```javascript
// Obtener todas las URLs de imágenes
{{ $json.respuestas.map(r => r.imagenes.map(i => i.url)).flat() }}
// Primera imagen de cada respuesta
{{ $json.respuestas.map(r => r.imagenes[0]?.url) }}
// Imágenes de respuestas críticas
{{ $json.respuestas.filter(r => r.es_critico).map(r => r.imagenes).flat() }}
```
### 4. Descargar imágenes
Las URLs son públicas y directas, se pueden:
- Descargar con HTTP Request
- Enviar por email como adjuntos
- Procesar con Computer Vision
- Subir a otro servicio (Google Drive, Dropbox, etc.)
### 5. Ejemplo: Enviar por email
```javascript
// En un nodo Email
To: {{ $json.inspeccion.cliente_email }}
Subject: Inspección Completada - {{ $json.inspeccion.vehiculo.placa }}
Attachments: {{ $json.inspeccion.pdf_url }}
```
## Logs
El backend imprime logs detallados:
```
🚀 Enviando inspección #123 a n8n...
📤 Enviando 15 respuestas con imágenes a n8n...
✅ Inspección #123 enviada exitosamente a n8n
- 15 respuestas
- 23 imágenes
```
## Seguridad
- El webhook es HTTPS
- Las URLs de imágenes son públicas en MinIO
- No se envían passwords ni tokens
- Se incluyen solo datos relevantes de la inspección

View File

@@ -0,0 +1,168 @@
# Sistema de Actualización PWA - AYUTEC
## 🚀 Características
-**Detección automática** de nuevas versiones
-**Modal de actualización** grande y visible
-**Service Worker** con estrategia Network-First
-**Cache inteligente** para funcionamiento offline
-**Actualización forzada** al usuario cuando hay nueva versión
## 📱 Instalación como PWA
### En Android/iOS:
1. Abre la app en Chrome/Safari
2. Toca el menú (⋮)
3. Selecciona "Agregar a pantalla de inicio"
4. Confirma la instalación
### En Desktop:
1. Abre la app en Chrome/Edge
2. Haz clic en el ícono de instalación () en la barra de direcciones
3. Confirma "Instalar"
## 🔄 Proceso de Actualización
### Para el Usuario:
1. Cuando hay una actualización, aparece automáticamente un **modal grande**
2. El modal muestra: "¡Nueva Actualización!"
3. Botón grande: **"🚀 ACTUALIZAR AHORA"**
4. Al presionar, la app se recarga con la nueva versión
### Para el Desarrollador:
#### Opción 1: Script Automático (Recomendado)
```powershell
cd frontend
.\update-version.ps1
```
Este script:
- Incrementa automáticamente la versión patch (1.0.87 → 1.0.88)
- Actualiza `package.json`
- Actualiza `public/service-worker.js`
#### Opción 2: Manual
1. **Actualizar `package.json`:**
```json
"version": "1.0.88" // Incrementar número
```
2. **Actualizar `public/service-worker.js`:**
```javascript
const CACHE_NAME = 'ayutec-v1.0.88'; // Mismo número
```
3. **Hacer build y deploy:**
```powershell
npm run build
docker build -t tu-registry/checklist-frontend:latest .
docker push tu-registry/checklist-frontend:latest
```
## 🔧 Cómo Funciona
### 1. Service Worker
- Registrado en `App.jsx`
- Cache con nombre versionado: `ayutec-v1.0.87`
- Estrategia: **Network First, Cache Fallback**
- Al cambiar la versión, se crea nuevo cache
### 2. Detección de Actualización
```javascript
// En App.jsx
registration.addEventListener('updatefound', () => {
// Nueva versión detectada
setUpdateAvailable(true)
})
```
### 3. Modal de Actualización
- Overlay negro semi-transparente (z-index: 9999)
- Modal animado con bounce
- Botón grande con gradiente
- **No se puede cerrar** - obliga a actualizar
### 4. Aplicación de Actualización
```javascript
waitingWorker.postMessage({ type: 'SKIP_WAITING' });
// Activa el nuevo service worker
// Recarga la página automáticamente
```
## 📊 Versionado
Seguimos **Semantic Versioning**:
- **MAJOR**: Cambios incompatibles (1.0.0 → 2.0.0)
- **MINOR**: Nueva funcionalidad compatible (1.0.0 → 1.1.0)
- **PATCH**: Correcciones de bugs (1.0.0 → 1.0.1)
El script `update-version.ps1` incrementa automáticamente **PATCH**.
## 🧪 Probar Localmente
1. **Compilar en modo producción:**
```bash
npm run build
npm run preview
```
2. **Simular actualización:**
- Abre la app en navegador
- Incrementa versión en `service-worker.js`
- Recarga la página (Ctrl+F5)
- Debe aparecer el modal de actualización
## 🐛 Troubleshooting
### El modal no aparece
- Verifica que el service worker esté registrado (F12 → Application → Service Workers)
- Asegúrate de cambiar el `CACHE_NAME` en `service-worker.js`
- Desregistra el SW antiguo: `Application → Service Workers → Unregister`
### La app no se actualiza
- Fuerza actualización: Ctrl+Shift+R (hard reload)
- Limpia cache del navegador
- Verifica que la nueva versión esté deployada
### PWA no se instala
- Verifica que `site.webmanifest` esté accesible
- Requiere HTTPS (excepto localhost)
- Verifica íconos en `/public/`
## 📝 Checklist de Deploy
- [ ] Incrementar versión con `update-version.ps1`
- [ ] Verificar que ambos archivos tengan la misma versión
- [ ] Hacer commit: `git commit -m "chore: bump version to X.X.X"`
- [ ] Build de producción: `npm run build`
- [ ] Build de Docker: `docker build -t frontend:vX.X.X .`
- [ ] Push a registry
- [ ] Deploy en servidor
- [ ] Verificar que usuarios vean el modal de actualización
## 🎯 Mejores Prácticas
1. **Siempre** incrementar versión antes de deploy
2. **Nunca** reutilizar números de versión
3. **Probar** localmente antes de deploy
4. **Documentar** cambios en commit message
5. **Notificar** a usuarios si es actualización crítica
## 🔐 Seguridad
- Service Worker solo funciona en HTTPS
- Manifest require `start_url` y `scope` correctos
- Cache no almacena datos sensibles (solo assets estáticos)
## 📱 Compatibilidad
- ✅ Chrome/Edge (Desktop y Mobile)
- ✅ Safari (iOS 11.3+)
- ✅ Firefox (Desktop y Mobile)
- ✅ Samsung Internet
- ⚠️ IE11 no soportado
---
**Versión actual:** 1.0.87
**Última actualización:** 2025-11-30

View File

@@ -24,4 +24,4 @@ if ($LASTEXITCODE -ne 0) {
} }
Write-Host "`n=== Proceso completado exitosamente ===`n" -ForegroundColor Green Write-Host "`n=== Proceso completado exitosamente ===`n" -ForegroundColor Green
pause

View File

@@ -2,8 +2,16 @@
<html lang="es"> <html lang="es">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="theme-color" content="#4f46e5" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="mobile-web-app-capable" content="yes" />
<title>AYUTEC - Sistema Inteligente de Inspecciones</title> <title>AYUTEC - Sistema Inteligente de Inspecciones</title>
<meta name="description" content="AYUTEC: Sistema avanzado de inspecciones vehiculares con inteligencia artificial" /> <meta name="description" content="AYUTEC: Sistema avanzado de inspecciones vehiculares con inteligencia artificial" />
</head> </head>

View File

@@ -1,7 +1,7 @@
{ {
"name": "checklist-frontend", "name": "checklist-frontend",
"private": true, "private": true,
"version": "1.0.0", "version": "1.3.5",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@@ -15,7 +15,8 @@
"axios": "^1.6.5", "axios": "^1.6.5",
"react-signature-canvas": "^1.0.6", "react-signature-canvas": "^1.0.6",
"lucide-react": "^0.303.0", "lucide-react": "^0.303.0",
"clsx": "^2.1.0" "clsx": "^2.1.0",
"react-markdown": "^9.0.1"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.2.48", "@types/react": "^18.2.48",
@@ -26,4 +27,4 @@
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"vite": "^5.0.11" "vite": "^5.0.11"
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

BIN
frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="28" height="28" viewBox="0 0 28 28"><image width="28" height="28" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAMAAABF0y+mAAAAVFBMVEVHcEwKJTcJKT0ULT4FJzwOIzQYLj0BHCwCJDcILEAcKTQJJDcQIzISAABTk8tSkcdVls4AJj00Z5AmUnVCeKdMiLwrWn86bplGgLEMNVAcRmYAHTJrIwFaAAAADnRSTlMAp9Va5pRHxvDGPH4oDQ8oGsYAAAD8SURBVCiRxZLJkoMgEEDVoCGaQHezyPL//zl0Y0wOM5dUpeYdpPAVvcEw/DPr8qdaJlS3k3F9VwoLRjqJOD/VRSGBq9acQMB7VxrJgsECXVgGyiYSqf0wqbpd8IeUuEuGtjERUQt157MGLyznyBvI8zpOqoGBXeo5tWcZcEJHLSoFORgVuztyCrNjDvCqx/ZWtgKSH62XeL2RrB8sbyRhKgE1aXwLTQV1nxByDuOrBZfAZC5ZXbdHn0AOKaXgctgdxVh7P0rKGUbnGwmjT1SCPyhTj+q5glBT+7aejsnmuTcCDPFi4DV1udotOybLEp84lILG66+Mn76mr/IDIP4ZVqH5o/IAAAAASUVORK5CYII="></image><style>@media (prefers-color-scheme: light) { :root { filter: none; } }
@media (prefers-color-scheme: dark) { :root { filter: none; } }
</style></svg>

After

Width:  |  Height:  |  Size: 940 B

View File

@@ -0,0 +1,67 @@
// Service Worker para PWA con detección de actualizaciones
// IMPORTANTE: Actualizar esta versión cada vez que se despliegue una nueva versión
const CACHE_NAME = 'ayutec-v1.3.5';
const urlsToCache = [
'/',
'/index.html'
];
// Instalación del service worker
self.addEventListener('install', (event) => {
console.log('Service Worker: Installing version', CACHE_NAME);
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => {
console.log('Service Worker: Caching files');
return cache.addAll(urlsToCache);
})
// NO hacer skipWaiting automáticamente - esperar a que el usuario lo active
);
});
// Activación del service worker
self.addEventListener('activate', (event) => {
console.log('Service Worker: Activating...');
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (cacheName !== CACHE_NAME) {
console.log('Service Worker: Deleting old cache:', cacheName);
return caches.delete(cacheName);
}
})
);
})
// NO hacer claim automáticamente - solo cuando el usuario actualice manualmente
);
});
// Estrategia: Network First, fallback to Cache
self.addEventListener('fetch', (event) => {
event.respondWith(
fetch(event.request)
.then((response) => {
// Clone la respuesta
const responseToCache = response.clone();
// Actualizar cache con la nueva respuesta
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseToCache);
});
return response;
})
.catch(() => {
// Si falla la red, usar cache
return caches.match(event.request);
})
);
});
// Mensaje para notificar actualización
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});

View File

@@ -0,0 +1,24 @@
{
"name": "AYUTEC - Sistema de Inspecciones",
"short_name": "AYUTEC",
"start_url": "/",
"scope": "/",
"icons": [
{
"src": "/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#4f46e5",
"background_color": "#ffffff",
"display": "standalone",
"orientation": "portrait"
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,332 @@
import React from 'react'
/**
* Renderizador Dinámico de Campos de Respuesta
* Renderiza el input apropiado según la configuración de la pregunta
*/
export function QuestionAnswerInput({ question, value, onChange, onSave }) {
const config = question.options || {}
const questionType = config.type || question.type
// BOOLEAN (2 opciones)
if (questionType === 'boolean' && config.choices?.length === 2) {
const [choice1, choice2] = config.choices
return (
<div className="flex gap-4">
<label className="flex items-center cursor-pointer px-4 py-3 border-2 rounded-lg transition hover:bg-gray-50">
<input
type="radio"
value={choice1.value}
checked={value === choice1.value}
onChange={(e) => {
onChange(e.target.value)
onSave?.()
}}
className="mr-3"
/>
<span className={`font-medium ${choice1.status === 'ok' ? 'text-green-600' : 'text-red-600'}`}>
{choice1.status === 'ok' ? '✓' : '✗'} {choice1.label}
</span>
</label>
<label className="flex items-center cursor-pointer px-4 py-3 border-2 rounded-lg transition hover:bg-gray-50">
<input
type="radio"
value={choice2.value}
checked={value === choice2.value}
onChange={(e) => {
onChange(e.target.value)
onSave?.()
}}
className="mr-3"
/>
<span className={`font-medium ${choice2.status === 'ok' ? 'text-green-600' : 'text-red-600'}`}>
{choice2.status === 'ok' ? '✓' : '✗'} {choice2.label}
</span>
</label>
</div>
)
}
// SINGLE CHOICE (selección única)
if (questionType === 'single_choice' && config.choices) {
return (
<div className="space-y-2">
{config.choices.map((choice, idx) => (
<label
key={idx}
className="flex items-center cursor-pointer px-4 py-3 border-2 rounded-lg transition hover:bg-gray-50"
>
<input
type="radio"
value={choice.value}
checked={value === choice.value}
onChange={(e) => {
onChange(e.target.value)
onSave?.()
}}
className="mr-3"
/>
<span className="flex-1 font-medium">{choice.label}</span>
{choice.points > 0 && (
<span className="text-sm text-blue-600">+{choice.points} pts</span>
)}
</label>
))}
{config.allow_other && (
<div className="pl-7">
<label className="flex items-center">
<input
type="radio"
value="__other__"
checked={value && !config.choices.find(c => c.value === value)}
onChange={(e) => onChange('')}
className="mr-3"
/>
<span>Otro:</span>
<input
type="text"
value={value && !config.choices.find(c => c.value === value) ? value : ''}
onChange={(e) => onChange(e.target.value)}
onBlur={onSave}
className="ml-2 flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
placeholder="Especificar..."
/>
</label>
</div>
)}
</div>
)
}
// MULTIPLE CHOICE (selección múltiple)
if (questionType === 'multiple_choice' && config.choices) {
const selectedValues = value ? (Array.isArray(value) ? value : value.split(',')) : []
const handleToggle = (choiceValue) => {
let newValues
if (selectedValues.includes(choiceValue)) {
newValues = selectedValues.filter(v => v !== choiceValue)
} else {
newValues = [...selectedValues, choiceValue]
}
onChange(newValues.join(','))
onSave?.()
}
return (
<div className="space-y-2">
{config.choices.map((choice, idx) => (
<label
key={idx}
className="flex items-center cursor-pointer px-4 py-3 border-2 rounded-lg transition hover:bg-gray-50"
>
<input
type="checkbox"
checked={selectedValues.includes(choice.value)}
onChange={() => handleToggle(choice.value)}
className="mr-3 w-4 h-4"
/>
<span className="flex-1 font-medium">{choice.label}</span>
{choice.points > 0 && (
<span className="text-sm text-blue-600">+{choice.points} pts</span>
)}
</label>
))}
</div>
)
}
// SCALE (escala numérica)
if (questionType === 'scale') {
const min = config.min || 1
const max = config.max || 5
const step = config.step || 1
const labels = config.labels || {}
const options = []
for (let i = min; i <= max; i += step) {
options.push(i)
}
return (
<div className="space-y-3">
<div className="flex justify-between text-sm text-gray-600 mb-2">
{labels.min && <span>{labels.min}</span>}
{labels.max && <span>{labels.max}</span>}
</div>
<div className="flex gap-2 justify-center">
{options.map(num => (
<button
key={num}
type="button"
onClick={() => {
onChange(String(num))
onSave?.()
}}
className={`w-12 h-12 rounded-full font-bold transition ${
value === String(num)
? 'bg-blue-600 text-white scale-110'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
}`}
>
{num}
</button>
))}
</div>
<div className="text-center">
{value && (
<div className="inline-flex items-center gap-2 px-4 py-2 bg-blue-50 rounded-lg">
<span className="text-sm text-gray-600">Seleccionado:</span>
<span className="font-bold text-blue-600 text-lg">{value}</span>
<span className="text-sm text-gray-600">/ {max}</span>
</div>
)}
</div>
</div>
)
}
// TEXT (texto libre)
if (questionType === 'text') {
const multiline = config.multiline !== false
const maxLength = config.max_length || 500
if (multiline) {
return (
<div>
<textarea
value={value || ''}
onChange={(e) => onChange(e.target.value)}
onBlur={onSave}
maxLength={maxLength}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
rows="4"
placeholder="Ingrese su respuesta..."
/>
<div className="text-xs text-gray-500 mt-1 text-right">
{(value?.length || 0)} / {maxLength} caracteres
</div>
</div>
)
} else {
return (
<input
type="text"
value={value || ''}
onChange={(e) => onChange(e.target.value)}
onBlur={onSave}
maxLength={maxLength}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
placeholder="Ingrese su respuesta..."
/>
)
}
}
// NUMBER (valor numérico)
if (questionType === 'number') {
const min = config.min ?? 0
const max = config.max ?? 100
const unit = config.unit || ''
return (
<div className="flex items-center gap-2">
<input
type="number"
value={value || ''}
onChange={(e) => onChange(e.target.value)}
onBlur={onSave}
min={min}
max={max}
step="any"
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
placeholder={`${min} - ${max}`}
/>
{unit && <span className="text-gray-600 font-medium">{unit}</span>}
</div>
)
}
// DATE (fecha)
if (questionType === 'date') {
return (
<input
type="date"
value={value || ''}
onChange={(e) => {
onChange(e.target.value)
onSave?.()
}}
min={config.min_date}
max={config.max_date}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
/>
)
}
// TIME (hora)
if (questionType === 'time') {
return (
<input
type="time"
value={value || ''}
onChange={(e) => {
onChange(e.target.value)
onSave?.()
}}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
/>
)
}
// PHOTO_ONLY (solo foto, sin campo de respuesta)
if (questionType === 'photo_only') {
return (
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
<p className="text-sm text-blue-800">
📸 Esta pregunta solo requiere fotografías. Adjunta las imágenes en la sección de fotos abajo.
</p>
</div>
)
}
// AI_ASSISTANT (Chat con asistente)
if (questionType === 'ai_assistant') {
return (
<div className="p-4 bg-gradient-to-r from-purple-50 to-blue-50 border-2 border-purple-200 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<span className="text-2xl">💬</span>
<h4 className="font-semibold text-purple-900">Asistente Disponible</h4>
</div>
<p className="text-sm text-purple-700 mb-2">
Haz clic en el botón "💬 Consultar Asistente" debajo para abrir el chat.
El asistente ha analizado las fotos anteriores y está listo para ayudarte.
</p>
<div className="text-xs text-purple-600 bg-white/50 rounded px-2 py-1">
No requiere respuesta manual - el chat se guarda automáticamente
</div>
</div>
)
}
// Fallback para tipos desconocidos
return (
<div className="p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
<p className="text-sm text-yellow-800">
Tipo de pregunta no reconocido: <code>{questionType}</code>
</p>
<input
type="text"
value={value || ''}
onChange={(e) => onChange(e.target.value)}
onBlur={onSave}
className="mt-2 w-full px-3 py-2 border border-gray-300 rounded-lg"
placeholder="Respuesta de texto libre..."
/>
</div>
)
}
export default QuestionAnswerInput

View File

@@ -0,0 +1,658 @@
import React, { useState, useEffect } from 'react'
/**
* Editor de Tipos de Preguntas Configurables (estilo Google Forms)
*
* Tipos soportados:
* - boolean: Dos opciones personalizables
* - single_choice: Selección única con N opciones
* - multiple_choice: Selección múltiple
* - scale: Escala numérica
* - text: Texto libre
* - number: Valor numérico
* - date: Fecha
* - time: Hora
*/
const QUESTION_TYPES = [
{ value: 'boolean', label: '✓✗ Booleana (2 opciones)', icon: '🔘' },
{ value: 'single_choice', label: '◎ Selección Única', icon: '⚪' },
{ value: 'multiple_choice', label: '☑ Selección Múltiple', icon: '✅' },
{ value: 'scale', label: '⭐ Escala Numérica', icon: '📊' },
{ value: 'text', label: '📝 Texto Libre', icon: '✏️' },
{ value: 'number', label: '🔢 Número', icon: '#️⃣' },
{ value: 'date', label: '📅 Fecha', icon: '📆' },
{ value: 'time', label: '🕐 Hora', icon: '⏰' },
{ value: 'photo_only', label: '📸 Solo Fotografía', icon: '📷' },
{ value: 'ai_assistant', label: '🤖 Asistente (Chat)', icon: '💬' }
]
const STATUS_OPTIONS = [
{ value: 'ok', label: 'OK (Verde)', color: 'green' },
{ value: 'warning', label: 'Advertencia (Amarillo)', color: 'yellow' },
{ value: 'critical', label: 'Crítico (Rojo)', color: 'red' },
{ value: 'info', label: 'Informativo (Azul)', color: 'blue' }
]
// Plantillas predefinidas para tipos booleanos
const BOOLEAN_TEMPLATES = [
{
name: 'Pasa/Falla',
choices: [
{ value: 'pass', label: 'Pasa', points: 1, status: 'ok' },
{ value: 'fail', label: 'Falla', points: 0, status: 'critical' }
]
},
{
name: 'Sí/No',
choices: [
{ value: 'yes', label: 'Sí', points: 1, status: 'ok' },
{ value: 'no', label: 'No', points: 0, status: 'critical' }
]
},
{
name: 'Bueno/Malo',
choices: [
{ value: 'good', label: 'Bueno', points: 1, status: 'ok' },
{ value: 'bad', label: 'Malo', points: 0, status: 'critical' }
]
},
{
name: 'Aprobado/Rechazado',
choices: [
{ value: 'approved', label: 'Aprobado', points: 1, status: 'ok' },
{ value: 'rejected', label: 'Rechazado', points: 0, status: 'critical' }
]
},
{
name: 'Funciona/No Funciona',
choices: [
{ value: 'works', label: 'Funciona', points: 1, status: 'ok' },
{ value: 'not_works', label: 'No Funciona', points: 0, status: 'critical' }
]
},
{
name: 'Personalizado',
choices: [
{ value: 'option1', label: 'Opción 1', points: 1, status: 'ok' },
{ value: 'option2', label: 'Opción 2', points: 0, status: 'critical' }
]
}
]
export function QuestionTypeEditor({ value, onChange, maxPoints = 1 }) {
const [config, setConfig] = useState(value || {
type: 'boolean',
choices: BOOLEAN_TEMPLATES[0].choices
})
useEffect(() => {
if (value && value.type) {
setConfig(value)
}
}, [value])
const handleTypeChange = (newType) => {
let newConfig = { type: newType }
// Inicializar con valores por defecto según el tipo
switch (newType) {
case 'boolean':
newConfig.choices = [...BOOLEAN_TEMPLATES[0].choices]
break
case 'single_choice':
case 'multiple_choice':
newConfig.choices = [
{ value: 'option1', label: 'Opción 1', status: 'ok' },
{ value: 'option2', label: 'Opción 2', status: 'warning' },
{ value: 'option3', label: 'Opción 3', status: 'critical' }
]
newConfig.allow_other = false
break
case 'scale':
newConfig.min = 1
newConfig.max = 5
newConfig.step = 1
newConfig.labels = { min: 'Muy malo', max: 'Excelente' }
break
case 'text':
newConfig.multiline = true
newConfig.max_length = 500
break
case 'number':
newConfig.min = 0
newConfig.max = 100
newConfig.unit = ''
break
case 'date':
newConfig.min_date = null
newConfig.max_date = null
break
case 'time':
newConfig.format = '24h'
break
}
setConfig(newConfig)
onChange(newConfig)
}
const updateConfig = (updates) => {
const newConfig = { ...config, ...updates }
setConfig(newConfig)
onChange(newConfig)
}
const updateChoice = (index, field, value) => {
const newChoices = [...config.choices]
newChoices[index] = { ...newChoices[index], [field]: value }
updateConfig({ choices: newChoices })
}
const addChoice = () => {
const newChoices = [...config.choices, {
value: `option${config.choices.length + 1}`,
label: `Opción ${config.choices.length + 1}`,
status: 'ok'
}]
updateConfig({ choices: newChoices })
}
const removeChoice = (index) => {
if (config.type === 'boolean' && config.choices.length <= 2) {
alert('Las preguntas booleanas deben tener exactamente 2 opciones')
return
}
const newChoices = config.choices.filter((_, i) => i !== index)
updateConfig({ choices: newChoices })
}
const applyBooleanTemplate = (template) => {
updateConfig({ choices: [...template.choices] })
}
return (
<div className="space-y-4">
{/* Selector de Tipo de Pregunta */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Tipo de Pregunta
</label>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
{QUESTION_TYPES.map(type => (
<button
key={type.value}
type="button"
onClick={() => handleTypeChange(type.value)}
className={`p-3 border-2 rounded-lg text-left transition ${
config.type === type.value
? 'border-purple-600 bg-purple-50'
: 'border-gray-300 hover:border-purple-300'
}`}
>
<div className="text-2xl mb-1">{type.icon}</div>
<div className="text-xs font-medium">{type.label}</div>
</button>
))}
</div>
</div>
{/* Opciones Globales */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h4 className="font-semibold text-blue-900 mb-3 text-sm"> Opciones Generales</h4>
<div className="space-y-2">
{/* Checkbox para campo de observaciones */}
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={config.show_observations !== false}
onChange={(e) => updateConfig({ show_observations: e.target.checked })}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700">
📝 Mostrar campo de observaciones al mecánico
</span>
</label>
<p className="text-xs text-gray-500 ml-6">
Si está marcado, el mecánico podrá agregar notas adicionales en esta pregunta
</p>
</div>
</div>
{/* Configuración específica según tipo */}
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4">
{/* BOOLEAN */}
{config.type === 'boolean' && (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Plantilla Predefinida
</label>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
{BOOLEAN_TEMPLATES.map((template, idx) => (
<button
key={idx}
type="button"
onClick={() => applyBooleanTemplate(template)}
className="px-3 py-2 border border-gray-300 rounded-lg hover:border-purple-500 hover:bg-purple-50 text-sm transition"
>
{template.name}
</button>
))}
</div>
</div>
<div className="grid grid-cols-2 gap-4">
{config.choices?.map((choice, idx) => (
<div key={idx} className="bg-white border border-gray-300 rounded-lg p-3">
<label className="block text-xs text-gray-600 mb-1">
Opción {idx + 1}
</label>
<input
type="text"
value={choice.label}
onChange={(e) => updateChoice(idx, 'label', e.target.value)}
className="w-full px-2 py-1 border border-gray-300 rounded mb-2 text-sm"
placeholder="Texto de la opción"
/>
<div>
<label className="block text-xs text-gray-500 mb-1">Estado (Determina Puntuación)</label>
<select
value={choice.status || 'ok'}
onChange={(e) => updateChoice(idx, 'status', e.target.value)}
className="w-full px-2 py-1 border border-gray-300 rounded text-sm"
>
<option value="ok"> OK (1pt)</option>
<option value="warning"> Advertencia (0.5pt)</option>
<option value="critical"> Crítico (0pt)</option>
</select>
</div>
</div>
))}
</div>
</div>
)}
{/* SINGLE CHOICE / MULTIPLE CHOICE */}
{(config.type === 'single_choice' || config.type === 'multiple_choice') && (
<div className="space-y-3">
<div className="flex justify-between items-center">
<label className="text-sm font-medium text-gray-700">
Opciones ({config.choices?.length || 0})
</label>
<button
type="button"
onClick={addChoice}
className="px-3 py-1 bg-purple-600 text-white rounded text-sm hover:bg-purple-700"
>
+ Agregar Opción
</button>
</div>
<div className="space-y-2">
{config.choices?.map((choice, idx) => (
<div key={idx} className="flex gap-2 items-start bg-white border border-gray-300 rounded-lg p-2">
<div className="flex-shrink-0 mt-2">
{config.type === 'single_choice' ? '⚪' : '☑️'}
</div>
<div className="flex-1 grid grid-cols-3 gap-2">
<div className="col-span-2">
<input
type="text"
value={choice.label}
onChange={(e) => updateChoice(idx, 'label', e.target.value)}
className="w-full px-2 py-1 border border-gray-300 rounded text-sm"
placeholder={`Opción ${idx + 1}`}
/>
</div>
<div>
<select
value={choice.status || 'ok'}
onChange={(e) => updateChoice(idx, 'status', e.target.value)}
className="w-full px-2 py-1 border border-gray-300 rounded text-sm"
title="Define la puntuación: OK=1pt, Advertencia=0.5pt, Crítico=0pt"
>
<option value="ok"> OK (1pt)</option>
<option value="warning"> Advertencia (0.5pt)</option>
<option value="critical"> Crítico (0pt)</option>
</select>
</div>
</div>
<button
type="button"
onClick={() => removeChoice(idx)}
className="flex-shrink-0 px-2 py-1 text-red-600 hover:bg-red-50 rounded"
>
🗑
</button>
</div>
))}
</div>
<div className="flex items-center gap-2">
<input
type="checkbox"
id="allow_other"
checked={config.allow_other || false}
onChange={(e) => updateConfig({ allow_other: e.target.checked })}
className="rounded border-gray-300"
/>
<label htmlFor="allow_other" className="text-sm text-gray-700">
Permitir opción "Otro" con texto libre
</label>
</div>
</div>
)}
{/* SCALE */}
{config.type === 'scale' && (
<div className="space-y-3">
<div className="grid grid-cols-3 gap-4">
<div>
<label className="block text-sm text-gray-700 mb-1">Mínimo</label>
<input
type="number"
value={config.min || 1}
onChange={(e) => updateConfig({ min: parseInt(e.target.value) || 1 })}
className="w-full px-2 py-1 border border-gray-300 rounded"
/>
</div>
<div>
<label className="block text-sm text-gray-700 mb-1">Máximo</label>
<input
type="number"
value={config.max || 5}
onChange={(e) => updateConfig({ max: parseInt(e.target.value) || 5 })}
className="w-full px-2 py-1 border border-gray-300 rounded"
/>
</div>
<div>
<label className="block text-sm text-gray-700 mb-1">Incremento</label>
<input
type="number"
value={config.step || 1}
onChange={(e) => updateConfig({ step: parseInt(e.target.value) || 1 })}
className="w-full px-2 py-1 border border-gray-300 rounded"
min="1"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm text-gray-700 mb-1">Etiqueta Mínimo</label>
<input
type="text"
value={config.labels?.min || ''}
onChange={(e) => updateConfig({
labels: { ...config.labels, min: e.target.value }
})}
className="w-full px-2 py-1 border border-gray-300 rounded"
placeholder="Ej: Muy malo"
/>
</div>
<div>
<label className="block text-sm text-gray-700 mb-1">Etiqueta Máximo</label>
<input
type="text"
value={config.labels?.max || ''}
onChange={(e) => updateConfig({
labels: { ...config.labels, max: e.target.value }
})}
className="w-full px-2 py-1 border border-gray-300 rounded"
placeholder="Ej: Excelente"
/>
</div>
</div>
<div className="bg-blue-50 border border-blue-200 rounded p-3">
<p className="text-xs text-blue-800">
Vista previa: {config.min} {config.labels?.min} {config.max} {config.labels?.max}
</p>
</div>
</div>
)}
{/* TEXT */}
{config.type === 'text' && (
<div className="space-y-3">
<div className="flex items-center gap-4">
<input
type="checkbox"
id="multiline"
checked={config.multiline !== false}
onChange={(e) => updateConfig({ multiline: e.target.checked })}
className="rounded border-gray-300"
/>
<label htmlFor="multiline" className="text-sm text-gray-700">
Permitir múltiples líneas
</label>
</div>
<div>
<label className="block text-sm text-gray-700 mb-1">
Longitud máxima (caracteres)
</label>
<input
type="number"
value={config.max_length || 500}
onChange={(e) => updateConfig({ max_length: parseInt(e.target.value) || 500 })}
className="w-full px-2 py-1 border border-gray-300 rounded"
min="1"
max="5000"
/>
</div>
</div>
)}
{/* NUMBER */}
{config.type === 'number' && (
<div className="space-y-3">
<div className="grid grid-cols-3 gap-4">
<div>
<label className="block text-sm text-gray-700 mb-1">Mínimo</label>
<input
type="number"
value={config.min ?? 0}
onChange={(e) => updateConfig({ min: parseFloat(e.target.value) })}
className="w-full px-2 py-1 border border-gray-300 rounded"
/>
</div>
<div>
<label className="block text-sm text-gray-700 mb-1">Máximo</label>
<input
type="number"
value={config.max ?? 100}
onChange={(e) => updateConfig({ max: parseFloat(e.target.value) })}
className="w-full px-2 py-1 border border-gray-300 rounded"
/>
</div>
<div>
<label className="block text-sm text-gray-700 mb-1">Unidad</label>
<input
type="text"
value={config.unit || ''}
onChange={(e) => updateConfig({ unit: e.target.value })}
className="w-full px-2 py-1 border border-gray-300 rounded"
placeholder="Ej: km, kg, °C"
/>
</div>
</div>
</div>
)}
{/* DATE */}
{config.type === 'date' && (
<div className="space-y-3">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm text-gray-700 mb-1">Fecha mínima (opcional)</label>
<input
type="date"
value={config.min_date || ''}
onChange={(e) => updateConfig({ min_date: e.target.value })}
className="w-full px-2 py-1 border border-gray-300 rounded"
/>
</div>
<div>
<label className="block text-sm text-gray-700 mb-1">Fecha máxima (opcional)</label>
<input
type="date"
value={config.max_date || ''}
onChange={(e) => updateConfig({ max_date: e.target.value })}
className="w-full px-2 py-1 border border-gray-300 rounded"
/>
</div>
</div>
</div>
)}
{/* TIME */}
{config.type === 'time' && (
<div>
<label className="block text-sm text-gray-700 mb-2">Formato</label>
<div className="flex gap-4">
<label className="flex items-center gap-2">
<input
type="radio"
value="12h"
checked={config.format === '12h'}
onChange={(e) => updateConfig({ format: e.target.value })}
className="border-gray-300"
/>
<span className="text-sm">12 horas (AM/PM)</span>
</label>
<label className="flex items-center gap-2">
<input
type="radio"
value="24h"
checked={config.format !== '12h'}
onChange={(e) => updateConfig({ format: e.target.value })}
className="border-gray-300"
/>
<span className="text-sm">24 horas</span>
</label>
</div>
</div>
)}
{/* AI ASSISTANT (CHAT) */}
{config.type === 'ai_assistant' && (
<div className="space-y-4">
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="flex items-start gap-3">
<span className="text-3xl">💬</span>
<div>
<h4 className="font-semibold text-blue-900 mb-1">Asistente Conversacional</h4>
<p className="text-sm text-blue-700">
El mecánico podrá chatear con el asistente usando fotos de preguntas anteriores como contexto.
Configura qué preguntas anteriores usar y el comportamiento del asistente.
</p>
</div>
</div>
</div>
{/* Prompt del asistente */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
🎯 Prompt del Asistente (Rol y Comportamiento)
</label>
<textarea
value={config.assistant_prompt || ''}
onChange={(e) => updateConfig({ assistant_prompt: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 font-mono text-sm"
rows="6"
placeholder="Ejemplo: Eres un experto mecánico especializado en sistemas de frenos. Ayuda al mecánico a diagnosticar problemas basándote en las fotos que has visto. Sé directo y técnico. Si ves algo anormal en las fotos, menciónalo proactivamente."
/>
<p className="text-xs text-gray-500 mt-1">
Define cómo debe comportarse el asistente: su rol, tono, especialidad, etc.
</p>
</div>
{/* Preguntas de contexto */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
📸 Preguntas Anteriores para Contexto
</label>
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3 mb-3">
<p className="text-xs text-yellow-800">
<strong>💡 Cómo funciona:</strong> El asistente verá las fotos de las preguntas que selecciones abajo.
Elige preguntas cuyas fotos sean relevantes para el diagnóstico (ej: si es asistente de frenos, selecciona preguntas sobre pastillas, discos, líquido de frenos, etc.)
</p>
</div>
<textarea
value={config.context_questions || ''}
onChange={(e) => updateConfig({ context_questions: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 text-sm"
rows="3"
placeholder="IDs de preguntas separados por comas. Ejemplo: 5,8,12,15
Dejar vacío para usar TODAS las preguntas anteriores con fotos."
/>
<p className="text-xs text-gray-500 mt-1">
Especifica los IDs de preguntas anteriores cuyas fotos debe analizar el asistente, o déjalo vacío para usar todas.
</p>
</div>
{/* Instrucciones adicionales */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
📋 Instrucciones Adicionales (Opcional)
</label>
<textarea
value={config.assistant_instructions || ''}
onChange={(e) => updateConfig({ assistant_instructions: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 text-sm"
rows="4"
placeholder="Instrucciones específicas adicionales.
Ejemplo:
- Si detectas pastillas con menos de 3mm, recomienda cambio inmediato
- Siempre verifica si hay fugas de líquido
- Menciona el código OBD2 si es relevante"
/>
<p className="text-xs text-gray-500 mt-1">
Reglas o criterios específicos que el asistente debe seguir al dar consejos.
</p>
</div>
{/* Configuración de respuestas */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
💬 Mensajes Máximos
</label>
<input
type="number"
min="1"
max="50"
value={config.max_messages || 20}
onChange={(e) => updateConfig({ max_messages: parseInt(e.target.value) })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
/>
<p className="text-xs text-gray-500 mt-1">
Límite de mensajes en el chat
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
📏 Longitud de Respuesta
</label>
<select
value={config.response_length || 'medium'}
onChange={(e) => updateConfig({ response_length: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
>
<option value="short">Corta (concisa)</option>
<option value="medium">Media (balanceada)</option>
<option value="long">Larga (detallada)</option>
</select>
</div>
</div>
</div>
)}
</div>
</div>
)
}
export default QuestionTypeEditor

View File

@@ -1,19 +1,35 @@
export default function Sidebar({ user, activeTab, setActiveTab, sidebarOpen, setSidebarOpen, onLogout }) { export default function Sidebar({ user, activeTab, setActiveTab, sidebarOpen, setSidebarOpen, onLogout }) {
return ( return (
<aside className={`bg-gradient-to-b from-gray-900 via-indigo-950 to-purple-950 text-white transition-all duration-300 ${sidebarOpen ? 'w-64' : 'w-16'} flex flex-col fixed h-full z-10 shadow-2xl`}> <>
{/* Overlay para cerrar sidebar en móvil */}
{sidebarOpen && (
<div
className="fixed inset-0 bg-black/50 z-20 lg:hidden"
onClick={() => setSidebarOpen(false)}
/>
)}
{/* Sidebar */}
<aside className={`bg-gradient-to-b from-gray-900 via-indigo-950 to-purple-950 text-white transition-all duration-300 flex flex-col fixed h-full shadow-2xl
${sidebarOpen ? 'w-64' : 'w-16'}
${sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}
z-30 lg:z-10
`}>
{/* Sidebar Header */} {/* Sidebar Header */}
<div className={`p-4 flex items-center ${sidebarOpen ? 'justify-between' : 'justify-center'} border-b border-indigo-800/50`}> <div className={`p-4 flex items-center ${sidebarOpen ? 'justify-between' : 'justify-center'} border-b border-indigo-800/50`}>
{sidebarOpen && ( {sidebarOpen && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-8 h-8 bg-gradient-to-br from-indigo-500 to-purple-500 rounded-lg flex items-center justify-center"> <img
<span className="text-white font-bold text-lg">S</span> src="/ayutec_logo.png"
</div> alt="Ayutec"
className="w-8 h-8 object-contain bg-white rounded-lg p-1"
/>
<h2 className="text-xl font-bold bg-gradient-to-r from-indigo-400 to-purple-400 bg-clip-text text-transparent">Ayutec</h2> <h2 className="text-xl font-bold bg-gradient-to-r from-indigo-400 to-purple-400 bg-clip-text text-transparent">Ayutec</h2>
</div> </div>
)} )}
<button <button
onClick={() => setSidebarOpen(!sidebarOpen)} onClick={() => setSidebarOpen(!sidebarOpen)}
className="p-2 rounded-lg hover:bg-indigo-800/50 transition" className="p-2 rounded-lg hover:bg-indigo-800/50 transition lg:block"
title={sidebarOpen ? 'Ocultar sidebar' : 'Mostrar sidebar'} title={sidebarOpen ? 'Ocultar sidebar' : 'Mostrar sidebar'}
> >
{sidebarOpen ? '☰' : '☰'} {sidebarOpen ? '☰' : '☰'}
@@ -122,6 +138,27 @@ export default function Sidebar({ user, activeTab, setActiveTab, sidebarOpen, se
{/* User Info */} {/* User Info */}
<div className="p-4 border-t border-indigo-800/50"> <div className="p-4 border-t border-indigo-800/50">
{/* Versión */}
{sidebarOpen && (
<div className="mb-3 px-2 py-1.5 bg-indigo-900/30 rounded-lg border border-indigo-700/30">
<a
href="https://ayutec.es"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center gap-2 hover:opacity-80 transition-opacity"
>
<img
src="/ayutec_logo.webp"
alt="Ayutec"
className="w-10 h-10 object-contain bg-white rounded p-1"
/>
<p className="text-xs text-indigo-300 font-medium hover:text-indigo-200">
Ayutec v1.3.5
</p>
</a>
</div>
)}
<div className={`flex items-center gap-3 ${!sidebarOpen && 'justify-center'}`}> <div className={`flex items-center gap-3 ${!sidebarOpen && 'justify-center'}`}>
<div className="w-10 h-10 bg-gradient-to-br from-indigo-500 to-purple-500 rounded-full flex items-center justify-center text-white font-bold flex-shrink-0 shadow-lg"> <div className="w-10 h-10 bg-gradient-to-br from-indigo-500 to-purple-500 rounded-full flex items-center justify-center text-white font-bold flex-shrink-0 shadow-lg">
{user.username.charAt(0).toUpperCase()} {user.username.charAt(0).toUpperCase()}
@@ -150,5 +187,6 @@ export default function Sidebar({ user, activeTab, setActiveTab, sidebarOpen, se
</button> </button>
</div> </div>
</aside> </aside>
</>
) )
} }

View File

@@ -0,0 +1,41 @@
# Script para actualizar la versión del frontend y service worker automáticamente
Write-Host "🔄 Actualizando versión del frontend..." -ForegroundColor Cyan
# Leer package.json
$packageJson = Get-Content "package.json" -Raw | ConvertFrom-Json
$currentVersion = $packageJson.version
Write-Host "📦 Versión actual: $currentVersion" -ForegroundColor Yellow
# Separar versión en partes (major.minor.patch)
$versionParts = $currentVersion -split '\.'
$major = [int]$versionParts[0]
$minor = [int]$versionParts[1]
$patch = [int]$versionParts[2]
# Incrementar patch
$patch++
$newVersion = "$major.$minor.$patch"
Write-Host "✨ Nueva versión: $newVersion" -ForegroundColor Green
# Actualizar package.json
$packageJsonContent = Get-Content "package.json" -Raw
$packageJsonContent = $packageJsonContent -replace """version"": ""$currentVersion""", """version"": ""$newVersion"""
Set-Content "package.json" -Value $packageJsonContent -NoNewline
Write-Host "✅ package.json actualizado" -ForegroundColor Green
# Actualizar service-worker.js
$swPath = "public\service-worker.js"
$swContent = Get-Content $swPath -Raw
$swContent = $swContent -replace "ayutec-v$currentVersion", "ayutec-v$newVersion"
Set-Content $swPath -Value $swContent -NoNewline
Write-Host "✅ service-worker.js actualizado" -ForegroundColor Green
Write-Host ""
Write-Host "🎉 Versión actualizada exitosamente a: $newVersion" -ForegroundColor Magenta
Write-Host ""
Write-Host "📝 Recuerda hacer commit de los cambios:" -ForegroundColor Yellow
Write-Host " git add package.json public/service-worker.js" -ForegroundColor Gray
Write-Host " git commit -m 'chore: bump version to $newVersion'" -ForegroundColor Gray

View File

@@ -1,13 +1,32 @@
Clear-Host Clear-Host
# Pedir mensaje de commit # Crear archivo temporal para el mensaje
$mensaje = Read-Host "Ingrese el mensaje de commit" $tempFile = [System.IO.Path]::GetTempFileName()
Write-Host "Agregando archivos..." Write-Host "Abriendo editor de texto..." -ForegroundColor Cyan
Write-Host "1. Pegue su mensaje de commit"
Write-Host "2. Guarde el archivo (Ctrl+S)"
Write-Host "3. Cierre el editor (Alt+F4 o X)" -ForegroundColor Green
Write-Host ""
# Abrir notepad con el archivo temporal
notepad++.exe $tempFile | Out-Null
# Verificar que el archivo tenga contenido
if (-not (Test-Path $tempFile) -or (Get-Item $tempFile).Length -eq 0) {
Write-Host "No se ingreso ningun mensaje. Abortando..." -ForegroundColor Red
Remove-Item $tempFile -ErrorAction SilentlyContinue
exit
}
Write-Host "`nAgregando archivos..."
git add . git add .
Write-Host "Creando commit..." Write-Host "Creando commit..."
git commit -m "$mensaje" git commit -F $tempFile
# Eliminar archivo temporal después del commit
Remove-Item $tempFile -ErrorAction SilentlyContinue
Write-Host "Haciendo push a la rama develop..." Write-Host "Haciendo push a la rama develop..."
$pushOutput = git push origin develop 2>&1 $pushOutput = git push origin develop 2>&1
@@ -23,4 +42,4 @@ if ($pushOutput -match "Authentication failed" -or $pushOutput -match "Failed to
} }
Write-Host "`nProceso finalizado." Write-Host "`nProceso finalizado."
pause

View File

@@ -0,0 +1,28 @@
-- Migración: Asegurar que ai_analysis existe en la tabla answers
-- Fecha: 2025-11-26
-- Descripción: Agrega la columna ai_analysis si no existe (para guardar el resultado del análisis de IA)
-- Agregar columna ai_analysis si no existe
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'answers'
AND column_name = 'ai_analysis'
) THEN
ALTER TABLE answers ADD COLUMN ai_analysis JSONB;
COMMENT ON COLUMN answers.ai_analysis IS 'Resultado del análisis de IA: {status, observations, recommendation, confidence, model, provider}';
END IF;
END $$;
-- Verificar que la columna existe
SELECT
column_name,
data_type,
is_nullable,
column_default
FROM information_schema.columns
WHERE table_name = 'answers'
AND column_name = 'ai_analysis';
SELECT '✅ Columna ai_analysis verificada/creada en tabla answers' as status;

View File

@@ -0,0 +1,6 @@
-- Agregar campo chat_history a la tabla answers
-- Fecha: 2025-12-02
ALTER TABLE answers ADD COLUMN IF NOT EXISTS chat_history JSON;
COMMENT ON COLUMN answers.chat_history IS 'Historial de conversación con AI Assistant para preguntas tipo ai_assistant';

View File

@@ -0,0 +1,16 @@
-- Migración: Agregar campo employee_code (Nro Operario) a la tabla users
-- Fecha: 2025-11-26
-- Descripción: Agrega un campo opcional para almacenar el código de operario de otro sistema
-- Agregar columna employee_code a la tabla users
ALTER TABLE users
ADD COLUMN IF NOT EXISTS employee_code VARCHAR(50);
-- Comentario descriptivo
COMMENT ON COLUMN users.employee_code IS 'Número de operario - código de identificación de otro sistema';
-- Verificar que la columna se agregó correctamente
SELECT column_name, data_type, character_maximum_length
FROM information_schema.columns
WHERE table_name = 'users'
AND column_name = 'employee_code';

View File

@@ -0,0 +1,153 @@
-- Migración: Sistema de tipos de preguntas configurables (estilo Google Forms)
-- Fecha: 2025-11-25
-- Descripción: Permite crear tipos de preguntas personalizables con opciones definidas por el usuario
-- PASO 1: Agregar comentario explicativo a la columna options
COMMENT ON COLUMN questions.options IS 'Configuración JSON de la pregunta con choices personalizables. Ejemplos:
Boolean: {"type": "boolean", "choices": [{"value": "yes", "label": "Sí", "points": 1, "status": "ok"}, {"value": "no", "label": "No", "points": 0, "status": "critical"}]}
Single Choice: {"type": "single_choice", "choices": [{"value": "excellent", "label": "Excelente", "points": 3}, {"value": "good", "label": "Bueno", "points": 2}]}
Multiple Choice: {"type": "multiple_choice", "choices": [{"value": "lights", "label": "Luces"}, {"value": "wipers", "label": "Limpiaparabrisas"}]}
Scale: {"type": "scale", "min": 1, "max": 5, "step": 1, "labels": {"min": "Muy malo", "max": "Excelente"}}';
-- PASO 2: Actualizar el comentario de la columna type
COMMENT ON COLUMN questions.type IS 'Tipo de pregunta:
- boolean: Dos opciones personalizables (ej: Sí/No, Pasa/Falla, Bueno/Malo)
- single_choice: Selección única con opciones personalizadas
- multiple_choice: Selección múltiple con opciones personalizadas
- scale: Escala numérica personalizable (1-5, 1-10, etc.)
- text: Texto libre
- number: Valor numérico
- date: Fecha
- time: Hora';
-- PASO 3: Migrar datos existentes de pass_fail y good_bad al nuevo formato
-- Actualizar preguntas tipo pass_fail
UPDATE questions
SET
type = 'boolean',
options = jsonb_build_object(
'type', 'boolean',
'choices', jsonb_build_array(
jsonb_build_object(
'value', 'pass',
'label', 'Pasa',
'points', points,
'status', 'ok'
),
jsonb_build_object(
'value', 'fail',
'label', 'Falla',
'points', 0,
'status', 'critical'
)
)
)
WHERE type = 'pass_fail';
-- Actualizar preguntas tipo good_bad
UPDATE questions
SET
type = 'boolean',
options = jsonb_build_object(
'type', 'boolean',
'choices', jsonb_build_array(
jsonb_build_object(
'value', 'good',
'label', 'Bueno',
'points', points,
'status', 'ok'
),
jsonb_build_object(
'value', 'bad',
'label', 'Malo',
'points', 0,
'status', 'critical'
)
)
)
WHERE type = 'good_bad';
-- Actualizar preguntas tipo good_bad_regular (3 opciones)
UPDATE questions
SET
type = 'single_choice',
options = jsonb_build_object(
'type', 'single_choice',
'choices', jsonb_build_array(
jsonb_build_object(
'value', 'good',
'label', 'Bueno',
'points', points,
'status', 'ok'
),
jsonb_build_object(
'value', 'regular',
'label', 'Regular',
'points', FLOOR(points / 2),
'status', 'warning'
),
jsonb_build_object(
'value', 'bad',
'label', 'Malo',
'points', 0,
'status', 'critical'
)
)
)
WHERE type = 'good_bad_regular';
-- PASO 4: Actualizar preguntas de tipo text sin opciones
UPDATE questions
SET
options = jsonb_build_object(
'type', 'text',
'multiline', true,
'max_length', 500
)
WHERE type = 'text' AND (options IS NULL OR options::text = '{}');
-- PASO 5: Crear función helper para validar estructura de options
CREATE OR REPLACE FUNCTION validate_question_options()
RETURNS TRIGGER AS $$
BEGIN
-- Validar que options tenga el campo type
IF NEW.options IS NOT NULL AND NOT (NEW.options ? 'type') THEN
RAISE EXCEPTION 'El campo options debe contener una propiedad "type"';
END IF;
-- Validar que boolean y single_choice tengan choices
IF NEW.type IN ('boolean', 'single_choice', 'multiple_choice') THEN
IF NEW.options IS NULL OR NOT (NEW.options ? 'choices') THEN
RAISE EXCEPTION 'Las preguntas de tipo % requieren un array "choices" en options', NEW.type;
END IF;
-- Validar que boolean tenga exactamente 2 opciones
IF NEW.type = 'boolean' AND jsonb_array_length(NEW.options->'choices') != 2 THEN
RAISE EXCEPTION 'Las preguntas de tipo boolean deben tener exactamente 2 opciones';
END IF;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- PASO 6: Crear trigger para validación (opcional, comentado por si causa problemas)
-- DROP TRIGGER IF EXISTS validate_options_trigger ON questions;
-- CREATE TRIGGER validate_options_trigger
-- BEFORE INSERT OR UPDATE ON questions
-- FOR EACH ROW
-- EXECUTE FUNCTION validate_question_options();
-- PASO 7: Índice para búsquedas por tipo de pregunta
CREATE INDEX IF NOT EXISTS idx_questions_type ON questions(type);
-- Verificación
SELECT
type,
COUNT(*) as total,
COUNT(CASE WHEN options IS NOT NULL THEN 1 END) as with_options
FROM questions
GROUP BY type
ORDER BY type;
SELECT 'Migración de tipos de preguntas flexibles completada exitosamente' AS status;

View File

@@ -0,0 +1,23 @@
-- Migración: Agregar campo mechanic_employee_code a la tabla inspections
-- Fecha: 2025-11-26
-- Descripción: Agrega un campo para almacenar el código de operario del mecánico que realiza la inspección
-- Agregar columna mechanic_employee_code a la tabla inspections
ALTER TABLE inspections
ADD COLUMN IF NOT EXISTS mechanic_employee_code VARCHAR(50);
-- Comentario descriptivo
COMMENT ON COLUMN inspections.mechanic_employee_code IS 'Código de operario del mecánico que realiza la inspección (copiado del perfil de usuario)';
-- Actualizar inspecciones existentes con el employee_code del mecánico correspondiente
UPDATE inspections i
SET mechanic_employee_code = u.employee_code
FROM users u
WHERE i.mechanic_id = u.id
AND i.mechanic_employee_code IS NULL;
-- Verificar que la columna se agregó correctamente
SELECT column_name, data_type, character_maximum_length
FROM information_schema.columns
WHERE table_name = 'inspections'
AND column_name = 'mechanic_employee_code';

View File

@@ -0,0 +1,172 @@
-- Migración: Subpreguntas anidadas hasta 5 niveles
-- Fecha: 2025-11-25
-- Descripción: Agrega soporte y validación para subpreguntas anidadas hasta 5 niveles de profundidad
-- Agregar columna para tracking de nivel (opcional pero útil)
ALTER TABLE questions
ADD COLUMN IF NOT EXISTS depth_level INTEGER DEFAULT 0;
-- Comentarios
COMMENT ON COLUMN questions.parent_question_id IS 'ID de la pregunta padre. NULL = pregunta principal. Soporta anidamiento hasta 5 niveles.';
COMMENT ON COLUMN questions.depth_level IS 'Nivel de profundidad: 0=principal, 1-5=subpreguntas anidadas';
-- Función para calcular profundidad de una pregunta
CREATE OR REPLACE FUNCTION calculate_question_depth(question_id INTEGER)
RETURNS INTEGER AS $$
DECLARE
current_parent_id INTEGER;
depth INTEGER := 0;
max_iterations INTEGER := 10; -- Protección contra loops infinitos
BEGIN
-- Obtener el parent_id de la pregunta
SELECT parent_question_id INTO current_parent_id
FROM questions
WHERE id = question_id;
-- Si no tiene padre, es nivel 0
IF current_parent_id IS NULL THEN
RETURN 0;
END IF;
-- Subir por la jerarquía contando niveles
WHILE current_parent_id IS NOT NULL AND depth < max_iterations LOOP
depth := depth + 1;
SELECT parent_question_id INTO current_parent_id
FROM questions
WHERE id = current_parent_id;
END LOOP;
RETURN depth;
END;
$$ LANGUAGE plpgsql;
-- Función trigger para validar profundidad máxima
CREATE OR REPLACE FUNCTION validate_question_depth()
RETURNS TRIGGER AS $$
DECLARE
calculated_depth INTEGER;
parent_depth INTEGER;
BEGIN
-- Si no tiene padre, es nivel 0
IF NEW.parent_question_id IS NULL THEN
NEW.depth_level := 0;
RETURN NEW;
END IF;
-- Validar que el padre existe y no es la misma pregunta
IF NEW.parent_question_id = NEW.id THEN
RAISE EXCEPTION 'Una pregunta no puede ser su propio padre';
END IF;
-- Calcular profundidad del padre
SELECT depth_level INTO parent_depth
FROM questions
WHERE id = NEW.parent_question_id;
IF parent_depth IS NULL THEN
-- Si el padre no tiene depth_level, calcularlo
parent_depth := calculate_question_depth(NEW.parent_question_id);
END IF;
-- La nueva pregunta es un nivel más profundo que su padre
calculated_depth := parent_depth + 1;
-- Validar que no excede 5 niveles
IF calculated_depth > 5 THEN
RAISE EXCEPTION 'No se permiten subpreguntas con profundidad mayor a 5. Esta pregunta tendría profundidad %, máximo permitido: 5', calculated_depth;
END IF;
-- Asignar el nivel calculado
NEW.depth_level := calculated_depth;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Crear trigger para INSERT y UPDATE
DROP TRIGGER IF EXISTS validate_depth_trigger ON questions;
CREATE TRIGGER validate_depth_trigger
BEFORE INSERT OR UPDATE OF parent_question_id ON questions
FOR EACH ROW
EXECUTE FUNCTION validate_question_depth();
-- Actualizar depth_level para preguntas existentes
UPDATE questions
SET depth_level = calculate_question_depth(id);
-- Crear índice para mejorar queries de subpreguntas
CREATE INDEX IF NOT EXISTS idx_questions_parent ON questions(parent_question_id) WHERE parent_question_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_questions_depth ON questions(depth_level);
-- Función helper para obtener árbol de subpreguntas
CREATE OR REPLACE FUNCTION get_question_tree(root_question_id INTEGER)
RETURNS TABLE (
id INTEGER,
parent_question_id INTEGER,
text TEXT,
type VARCHAR(30),
depth_level INTEGER,
show_if_answer VARCHAR(50),
path TEXT
) AS $$
BEGIN
RETURN QUERY
WITH RECURSIVE question_tree AS (
-- Pregunta raíz
SELECT
q.id,
q.parent_question_id,
q.text,
q.type,
q.depth_level,
q.show_if_answer,
q.id::TEXT as path
FROM questions q
WHERE q.id = root_question_id
UNION ALL
-- Subpreguntas recursivas
SELECT
q.id,
q.parent_question_id,
q.text,
q.type,
q.depth_level,
q.show_if_answer,
qt.path || ' > ' || q.id::TEXT
FROM questions q
INNER JOIN question_tree qt ON q.parent_question_id = qt.id
WHERE q.depth_level <= 5 -- Límite de seguridad
)
SELECT * FROM question_tree
ORDER BY depth_level, id;
END;
$$ LANGUAGE plpgsql;
-- Verificar estructura actual
SELECT
COUNT(*) as total_preguntas,
COUNT(CASE WHEN parent_question_id IS NULL THEN 1 END) as principales,
COUNT(CASE WHEN parent_question_id IS NOT NULL THEN 1 END) as subpreguntas,
MAX(depth_level) as max_profundidad
FROM questions;
-- Ver distribución por profundidad
SELECT
depth_level,
COUNT(*) as cantidad,
CASE
WHEN depth_level = 0 THEN 'Principales'
WHEN depth_level = 1 THEN 'Nivel 1'
WHEN depth_level = 2 THEN 'Nivel 2'
WHEN depth_level = 3 THEN 'Nivel 3'
WHEN depth_level = 4 THEN 'Nivel 4'
WHEN depth_level = 5 THEN 'Nivel 5'
END as descripcion
FROM questions
GROUP BY depth_level
ORDER BY depth_level;
SELECT '✓ Migración de subpreguntas anidadas completada' AS status;

View File

@@ -0,0 +1,25 @@
-- Migración: Agregar soft delete a preguntas
-- Fecha: 2025-11-27
-- Descripción: Permite eliminar preguntas sin romper la integridad de respuestas históricas
-- Agregar columna is_deleted a la tabla questions
ALTER TABLE questions ADD COLUMN IF NOT EXISTS is_deleted BOOLEAN DEFAULT FALSE;
-- Agregar columna updated_at si no existe
ALTER TABLE questions ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW();
-- Crear índice para mejorar queries que filtran por is_deleted
CREATE INDEX IF NOT EXISTS idx_questions_is_deleted ON questions(is_deleted);
-- Crear índice compuesto para mejorar queries de preguntas activas por checklist
CREATE INDEX IF NOT EXISTS idx_questions_checklist_active ON questions(checklist_id, is_deleted);
-- Actualizar preguntas existentes como no eliminadas
UPDATE questions SET is_deleted = FALSE WHERE is_deleted IS NULL;
-- Actualizar updated_at en preguntas existentes
UPDATE questions SET updated_at = created_at WHERE updated_at IS NULL;
-- Comentarios en las columnas
COMMENT ON COLUMN questions.is_deleted IS 'Soft delete: marca pregunta como eliminada sin borrarla físicamente, manteniendo integridad de respuestas históricas';
COMMENT ON COLUMN questions.updated_at IS 'Timestamp de última actualización de la pregunta';

View File

@@ -0,0 +1,22 @@
-- Migración: Cambiar allow_photos de Boolean a String con 3 estados
-- Fecha: 2025-12-02
-- Descripción: Agregar soporte para fotos opcionales/obligatorias/no permitidas
-- Paso 1: Agregar nueva columna
ALTER TABLE questions ADD COLUMN photo_requirement VARCHAR(20) DEFAULT 'optional';
-- Paso 2: Migrar datos existentes
UPDATE questions
SET photo_requirement = CASE
WHEN allow_photos = TRUE THEN 'optional'
WHEN allow_photos = FALSE THEN 'none'
ELSE 'optional'
END;
-- Paso 3: Eliminar columna antigua (opcional, comentar si quieres mantener compatibilidad)
-- ALTER TABLE questions DROP COLUMN allow_photos;
-- Nota: Los valores válidos son:
-- 'none' = No se permiten fotos
-- 'optional' = Fotos opcionales (puede adjuntar o no)
-- 'required' = Fotos obligatorias (debe adjuntar al menos 1)

View File

@@ -0,0 +1,468 @@
#!/usr/bin/env python3
"""
Script de Migración de Tipos de Preguntas
==========================================
Migra preguntas existentes (pass_fail, good_bad) al nuevo formato configurable.
Requisitos:
pip install psycopg2-binary
Uso:
python migrate_question_types.py
Base de Datos:
Host: portianerp.rshtech.com.py
Database: syntria_db
User: syntria_user
"""
import psycopg2
import json
from datetime import datetime
from typing import Dict, List, Any
# Configuración de la base de datos
DB_CONFIG = {
'host': 'portianerp.rshtech.com.py',
'database': 'syntria_db',
'user': 'syntria_user',
'password': 'syntria_secure_2024',
'port': 5432
}
# Plantillas de conversión
MIGRATION_TEMPLATES = {
'pass_fail': {
'new_type': 'boolean',
'config': {
'type': 'boolean',
'choices': [
{
'value': 'pass',
'label': 'Pasa',
'points': None, # Se asignará dinámicamente
'status': 'ok'
},
{
'value': 'fail',
'label': 'Falla',
'points': 0,
'status': 'critical'
}
]
}
},
'good_bad': {
'new_type': 'boolean',
'config': {
'type': 'boolean',
'choices': [
{
'value': 'good',
'label': 'Bueno',
'points': None, # Se asignará dinámicamente
'status': 'ok'
},
{
'value': 'bad',
'label': 'Malo',
'points': 0,
'status': 'critical'
}
]
}
},
'good_bad_regular': {
'new_type': 'single_choice',
'config': {
'type': 'single_choice',
'choices': [
{
'value': 'good',
'label': 'Bueno',
'points': None, # Se asignará dinámicamente
'status': 'ok'
},
{
'value': 'regular',
'label': 'Regular',
'points': None, # Se calculará como points/2
'status': 'warning'
},
{
'value': 'bad',
'label': 'Malo',
'points': 0,
'status': 'critical'
}
],
'allow_other': False
}
},
'text': {
'new_type': 'text',
'config': {
'type': 'text',
'multiline': True,
'max_length': 500
}
},
'number': {
'new_type': 'number',
'config': {
'type': 'number',
'min': 0,
'max': 100,
'unit': ''
}
}
}
class QuestionMigrator:
def __init__(self):
self.conn = None
self.cursor = None
self.stats = {
'total_questions': 0,
'migrated': 0,
'skipped': 0,
'errors': 0,
'by_type': {}
}
def connect(self):
"""Conectar a la base de datos"""
try:
print(f"🔌 Conectando a {DB_CONFIG['host']}...")
self.conn = psycopg2.connect(**DB_CONFIG)
self.cursor = self.conn.cursor()
print("✅ Conexión exitosa\n")
return True
except Exception as e:
print(f"❌ Error de conexión: {e}")
return False
def disconnect(self):
"""Cerrar conexión"""
if self.cursor:
self.cursor.close()
if self.conn:
self.conn.close()
print("\n🔌 Conexión cerrada")
def backup_questions_table(self):
"""Crear backup de la tabla questions"""
try:
print("💾 Creando backup de la tabla questions...")
# Crear tabla de backup con timestamp
backup_table = f"questions_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
self.cursor.execute(f"""
CREATE TABLE {backup_table} AS
SELECT * FROM questions;
""")
self.cursor.execute(f"SELECT COUNT(*) FROM {backup_table}")
count = self.cursor.fetchone()[0]
self.conn.commit()
print(f"✅ Backup creado: {backup_table} ({count} registros)\n")
return backup_table
except Exception as e:
print(f"❌ Error creando backup: {e}")
return None
def get_questions_to_migrate(self) -> List[Dict[str, Any]]:
"""Obtener todas las preguntas que necesitan migración"""
try:
print("📊 Obteniendo preguntas para migrar...")
self.cursor.execute("""
SELECT
id,
checklist_id,
section,
text,
type,
points,
options,
allow_photos,
max_photos,
requires_comment_on_fail,
send_notification,
parent_question_id,
show_if_answer,
ai_prompt
FROM questions
ORDER BY id
""")
questions = []
for row in self.cursor.fetchall():
questions.append({
'id': row[0],
'checklist_id': row[1],
'section': row[2],
'text': row[3],
'type': row[4],
'points': row[5],
'options': row[6],
'allow_photos': row[7],
'max_photos': row[8],
'requires_comment_on_fail': row[9],
'send_notification': row[10],
'parent_question_id': row[11],
'show_if_answer': row[12],
'ai_prompt': row[13]
})
self.stats['total_questions'] = len(questions)
print(f"✅ Se encontraron {len(questions)} preguntas\n")
return questions
except Exception as e:
print(f"❌ Error obteniendo preguntas: {e}")
return []
def migrate_question(self, question: Dict[str, Any]) -> bool:
"""Migrar una pregunta al nuevo formato"""
try:
old_type = question['type']
# Si ya está en el nuevo formato, saltar
if old_type in ['boolean', 'single_choice', 'multiple_choice', 'scale', 'text', 'number', 'date', 'time']:
if question['options'] and isinstance(question['options'], dict) and 'type' in question['options']:
self.stats['skipped'] += 1
return True
# Obtener template de migración
if old_type not in MIGRATION_TEMPLATES:
print(f" ⚠️ Tipo desconocido '{old_type}' para pregunta #{question['id']}")
self.stats['skipped'] += 1
return True
template = MIGRATION_TEMPLATES[old_type]
new_type = template['new_type']
new_config = json.loads(json.dumps(template['config'])) # Deep copy
# Asignar puntos dinámicamente
if 'choices' in new_config:
for choice in new_config['choices']:
if choice['points'] is None:
choice['points'] = question['points']
# Para good_bad_regular, calcular puntos intermedios
if old_type == 'good_bad_regular':
new_config['choices'][1]['points'] = max(1, question['points'] // 2)
# Actualizar la pregunta
self.cursor.execute("""
UPDATE questions
SET
type = %s,
options = %s
WHERE id = %s
""", (new_type, json.dumps(new_config), question['id']))
# Actualizar estadísticas
self.stats['migrated'] += 1
if old_type not in self.stats['by_type']:
self.stats['by_type'][old_type] = 0
self.stats['by_type'][old_type] += 1
return True
except Exception as e:
print(f" ❌ Error migrando pregunta #{question['id']}: {e}")
self.stats['errors'] += 1
return False
def verify_migration(self):
"""Verificar que la migración fue exitosa"""
print("\n🔍 Verificando migración...")
try:
# Contar por tipo nuevo
self.cursor.execute("""
SELECT
type,
COUNT(*) as total,
COUNT(CASE WHEN options IS NOT NULL THEN 1 END) as with_config
FROM questions
GROUP BY type
ORDER BY type
""")
print("\n📊 Distribución de preguntas migradas:")
print("-" * 60)
for row in self.cursor.fetchall():
tipo, total, with_config = row
print(f" {tipo:20} | Total: {total:4} | Con config: {with_config:4}")
print("-" * 60)
# Verificar que todas las boolean tengan 2 choices
# Usar CAST para compatibilidad con JSON y JSONB
self.cursor.execute("""
SELECT id, text, options
FROM questions
WHERE type = 'boolean'
AND (
options IS NULL
OR json_array_length((options::json)->'choices') != 2
)
LIMIT 5
""")
invalid = self.cursor.fetchall()
if invalid:
print(f"\n⚠️ Advertencia: {len(invalid)} preguntas boolean con configuración inválida:")
for q_id, text, opts in invalid:
print(f" - #{q_id}: {text[:50]}...")
else:
print("\n✅ Todas las preguntas boolean tienen configuración válida")
return True
except Exception as e:
print(f"❌ Error en verificación: {e}")
return False
def print_statistics(self):
"""Imprimir estadísticas de la migración"""
print("\n" + "=" * 60)
print("📈 ESTADÍSTICAS DE MIGRACIÓN")
print("=" * 60)
print(f"Total de preguntas: {self.stats['total_questions']}")
print(f"Migradas exitosamente: {self.stats['migrated']}")
print(f"Omitidas: {self.stats['skipped']}")
print(f"Errores: {self.stats['errors']}")
print("\nPor tipo original:")
for tipo, count in self.stats['by_type'].items():
print(f" - {tipo:20}: {count:4} preguntas")
print("=" * 60)
def run(self, dry_run=False):
"""Ejecutar la migración completa"""
print("=" * 60)
print("🚀 MIGRACIÓN DE TIPOS DE PREGUNTAS")
print("=" * 60)
print(f"Modo: {'🔍 DRY RUN (sin cambios)' if dry_run else '✍️ MIGRACIÓN REAL'}")
print("=" * 60 + "\n")
if not self.connect():
return False
try:
# Crear backup
if not dry_run:
backup_table = self.backup_questions_table()
if not backup_table:
print("⚠️ No se pudo crear backup. ¿Continuar de todos modos? (y/n): ", end='')
if input().lower() != 'y':
return False
# Obtener preguntas
questions = self.get_questions_to_migrate()
if not questions:
print("❌ No se encontraron preguntas para migrar")
return False
# Migrar cada pregunta
print("🔄 Migrando preguntas...\n")
for i, question in enumerate(questions, 1):
old_type = question['type']
if self.migrate_question(question):
if i % 10 == 0:
print(f" Progreso: {i}/{len(questions)} preguntas procesadas...")
# Commit o rollback según modo
if dry_run:
self.conn.rollback()
print("\n🔍 DRY RUN completado - Cambios revertidos")
else:
self.conn.commit()
print("\n✅ Migración completada - Cambios guardados")
# Verificar migración
self.verify_migration()
# Mostrar estadísticas
self.print_statistics()
return True
except Exception as e:
print(f"\n❌ Error durante la migración: {e}")
if self.conn:
self.conn.rollback()
print("🔄 Cambios revertidos")
return False
finally:
self.disconnect()
def main():
"""Función principal"""
print("\n" + "=" * 60)
print(" MIGRACIÓN DE TIPOS DE PREGUNTAS - Sistema Configurable")
print(" Base de datos: syntria_db @ portianerp.rshtech.com.py")
print("=" * 60 + "\n")
print("Este script migrará las preguntas existentes al nuevo formato:")
print(" • pass_fail → boolean (Pasa/Falla)")
print(" • good_bad → boolean (Bueno/Malo)")
print(" • good_bad_regular → single_choice (Bueno/Regular/Malo)")
print(" • text → text (con configuración)")
print(" • number → number (con configuración)\n")
# Preguntar modo
print("Seleccione el modo de ejecución:")
print(" 1. DRY RUN - Ver cambios sin aplicarlos (recomendado primero)")
print(" 2. MIGRACIÓN REAL - Aplicar cambios permanentes")
print("\nOpción (1/2): ", end='')
try:
option = input().strip()
dry_run = (option != '2')
if not dry_run:
print("\n⚠️ ADVERTENCIA: Esto modificará la base de datos de producción.")
print("Se creará un backup automático antes de continuar.")
print("\n¿Continuar? (escriba 'SI' para confirmar): ", end='')
confirm = input().strip()
if confirm != 'SI':
print("\n❌ Migración cancelada por el usuario")
return
# Ejecutar migración
migrator = QuestionMigrator()
success = migrator.run(dry_run=dry_run)
if success:
if not dry_run:
print("\n✅ Migración completada exitosamente!")
print("\nPróximos pasos:")
print(" 1. Reiniciar el servidor backend")
print(" 2. Probar crear nuevas preguntas con el editor visual")
print(" 3. Verificar que las inspecciones existentes sigan funcionando")
else:
print("\n✅ DRY RUN completado!")
print("\nPara aplicar los cambios, ejecute nuevamente y seleccione opción 2")
else:
print("\n❌ La migración falló. Revise los errores arriba.")
except KeyboardInterrupt:
print("\n\n⚠️ Migración cancelada por el usuario")
except Exception as e:
print(f"\n❌ Error inesperado: {e}")
if __name__ == '__main__':
main()