develop #1

Merged
gitea merged 44 commits from develop into main 2025-11-26 01:15:20 +00:00
20 changed files with 3736 additions and 430 deletions

375
AUDITORIA_INSPECCIONES.md Normal file
View File

@@ -0,0 +1,375 @@
# Sistema de Auditoría y Edición de Inspecciones
## ✅ Implementación Completa
Se ha implementado un sistema completo de auditoría que permite a los administradores editar inspecciones completadas y mantener un registro detallado de todos los cambios realizados.
---
## 🎯 Características Implementadas
### **Backend**
1. **Modelo de Auditoría**
- Tabla `inspection_audit_log` que registra todos los cambios
- Campos: inspection_id, answer_id, user_id, action, entity_type, field_name, old_value, new_value, comment, created_at
- Relaciones con inspections, answers y users
2. **Endpoints de Auditoría**
- `GET /api/inspections/{id}/audit-log` - Obtener historial de cambios (solo admin)
- `PUT /api/answers/{id}/admin-edit` - Editar respuesta con registro automático (solo admin)
3. **Registro Automático**
- Cada cambio registra: qué se cambió, valor anterior, valor nuevo, quién lo cambió, cuándo y por qué
- Recalcula puntos automáticamente si cambia el status
- Registra múltiples cambios en una sola edición
### **Frontend**
1. **Edición de Respuestas (Solo Admin)**
- Botón "✏️ Editar" en cada respuesta de inspecciones completadas
- Formulario inline con campos editables:
- Estado (OK, Advertencia, Crítico, N/A)
- Valor de respuesta (según tipo de pregunta)
- Observación
- Marcador de señalamiento
- Motivo del cambio (obligatorio)
- Validación: requiere explicar el motivo del cambio
2. **Modal de Historial de Cambios**
- Botón "📜 Ver Historial de Cambios" en el footer del modal de inspección
- Lista cronológica de todos los cambios (más reciente primero)
- Para cada cambio muestra:
- Quién lo hizo (nombre del usuario)
- Cuándo (fecha y hora)
- Qué acción realizó
- Qué campo modificó
- Valor anterior vs valor nuevo (visual con colores)
- Motivo del cambio
- Iconos visuales según tipo de acción (➕✏️🗑️🔄)
3. **Restricciones de Seguridad**
- Solo administradores pueden editar respuestas
- Solo administradores pueden ver el historial
- Solo inspecciones completadas pueden editarse
- Cada cambio requiere justificación obligatoria
---
## 📋 Instrucciones de Uso
### **Para Administradores**
#### 1. Editar una Respuesta
1. Abre el detalle de una inspección completada
2. Busca la respuesta que quieres modificar
3. Haz clic en el botón "✏️ Editar" junto a la respuesta
4. Modifica los campos necesarios:
- **Estado**: Cambia entre OK, Advertencia, Crítico o N/A
- **Valor de respuesta**: Solo si la pregunta no es pass/fail
- **Observación**: Agrega o modifica comentarios
- **Señalado**: Marca o desmarca el flag de atención
5. **Importante**: Escribe el motivo del cambio en el campo "Motivo del cambio"
6. Haz clic en "Guardar Cambios"
7. El sistema:
- Actualiza la respuesta
- Recalcula los puntos automáticamente
- Registra cada cambio en el log de auditoría
- Recarga la inspección con los datos actualizados
**Nota**: No puedes guardar sin escribir un motivo del cambio.
#### 2. Ver Historial de Cambios
1. Abre el detalle de cualquier inspección
2. En el footer, haz clic en "📜 Ver Historial de Cambios"
3. Se abrirá un modal con la bitácora completa
4. Revisa:
- Todos los cambios realizados por administradores
- Orden cronológico (más recientes primero)
- Detalles completos de cada modificación
- Quién, cuándo, qué y por qué
#### 3. Tipos de Cambios Registrados
El sistema registra automáticamente:
- **answer_value**: Cambio en la respuesta
- **status**: Cambio en el estado (OK/Advertencia/Crítico/N/A)
- **comment**: Cambio en las observaciones
- **is_flagged**: Marcado o desmarcado de señalamiento
- **points_earned**: Recálculo automático de puntos
### **Para Mecánicos**
- No pueden editar inspecciones completadas
- No pueden ver el historial de cambios
- Solo ven el estado final de las respuestas
---
## 🗄️ Migración de Base de Datos
### **Ejecutar SQL**
```bash
# Usando psql
psql -U tu_usuario -d tu_database -f migrations/add_inspection_audit_log.sql
# O directamente
psql -U tu_usuario -d tu_database
```
Luego ejecuta:
```sql
-- Crear tabla de auditoría
CREATE TABLE IF NOT EXISTS inspection_audit_log (
id SERIAL PRIMARY KEY,
inspection_id INTEGER NOT NULL REFERENCES inspections(id) ON DELETE CASCADE,
answer_id INTEGER REFERENCES answers(id) ON DELETE CASCADE,
user_id INTEGER NOT NULL REFERENCES users(id),
action VARCHAR(50) NOT NULL,
entity_type 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
);
-- Crear índices
CREATE INDEX idx_audit_log_inspection ON inspection_audit_log(inspection_id);
CREATE INDEX idx_audit_log_answer ON inspection_audit_log(answer_id);
CREATE INDEX idx_audit_log_user ON inspection_audit_log(user_id);
CREATE INDEX idx_audit_log_created_at ON inspection_audit_log(created_at DESC);
```
### **Reiniciar Backend**
```bash
# Si usas Docker
docker-compose restart backend
# Si corres directamente
# Ctrl+C y volver a ejecutar
python -m uvicorn app.main:app --reload
```
---
## 🔍 Ejemplos de Uso
### Ejemplo 1: Corregir Estado de Respuesta
**Escenario**: Un mecánico marcó "Crítico" por error cuando debía ser "Advertencia"
**Pasos**:
1. Admin abre la inspección
2. Encuentra la respuesta con estado "Crítico"
3. Clic en "✏️ Editar"
4. Cambia Estado a "Advertencia"
5. En "Motivo del cambio": "Error del mecánico, no era crítico sino advertencia menor"
6. Guarda cambios
7. El sistema:
- Actualiza el status de "critical" a "warning"
- Recalcula puntos (de 0 a 50% del total)
- Registra: field_name="status", old_value="critical", new_value="warning"
- Registra: field_name="points_earned", old_value="0", new_value="5"
**Resultado en Auditoría**:
```
✏️ Juan Pérez (Admin) • 25 de noviembre de 2025, 14:30
Acción: updated en answer (Respuesta #45)
Campo modificado: status
Valor anterior: critical
Valor nuevo: warning
Campo modificado: points_earned
Valor anterior: 0
Valor nuevo: 5
Motivo: Error del mecánico, no era crítico sino advertencia menor
```
---
### Ejemplo 2: Agregar Observación Faltante
**Escenario**: El mecánico no dejó observaciones en un item señalado
**Pasos**:
1. Admin edita la respuesta
2. Agrega en "Observación": "Necesita cambio de aceite urgente"
3. Mantiene el señalamiento activado
4. Motivo: "Agregando observación faltante para clarity"
5. Guarda
**Resultado**: Se registra el cambio de observación de vacío a texto.
---
### Ejemplo 3: Revisión de Historial
**Escenario**: Auditoría mensual de cambios en inspecciones
**Pasos**:
1. Admin abre inspección
2. Clic en "📜 Ver Historial de Cambios"
3. Revisa todos los cambios del mes
4. Verifica justificaciones
5. Identifica patrones de errores comunes
---
## 📊 Base de Datos
### Estructura de `inspection_audit_log`
| Campo | Tipo | Descripción |
|-------|------|-------------|
| id | SERIAL | ID único del registro |
| inspection_id | INTEGER | ID de la inspección modificada |
| answer_id | INTEGER | ID de la respuesta modificada (nullable) |
| user_id | INTEGER | ID del usuario que hizo el cambio |
| action | VARCHAR(50) | Tipo de acción (created, updated, deleted, status_changed) |
| entity_type | VARCHAR(50) | Tipo de entidad (inspection, answer) |
| field_name | VARCHAR(100) | Nombre del campo modificado |
| old_value | TEXT | Valor anterior |
| new_value | TEXT | Valor nuevo |
| comment | TEXT | Motivo del cambio |
| created_at | TIMESTAMP | Fecha y hora del cambio |
### Consultas Útiles
```sql
-- Ver todos los cambios de una inspección
SELECT u.full_name, ial.action, ial.field_name, ial.old_value, ial.new_value, ial.comment, ial.created_at
FROM inspection_audit_log ial
JOIN users u ON ial.user_id = u.id
WHERE ial.inspection_id = 123
ORDER BY ial.created_at DESC;
-- Ver cambios realizados por un admin específico
SELECT i.id as inspection_id, i.vehicle_plate, ial.field_name, ial.comment, ial.created_at
FROM inspection_audit_log ial
JOIN inspections i ON ial.inspection_id = i.id
WHERE ial.user_id = 5
ORDER BY ial.created_at DESC;
-- Contar cambios por tipo
SELECT action, COUNT(*) as total
FROM inspection_audit_log
GROUP BY action
ORDER BY total DESC;
-- Ver respuestas más editadas
SELECT answer_id, COUNT(*) as ediciones
FROM inspection_audit_log
WHERE answer_id IS NOT NULL
GROUP BY answer_id
ORDER BY ediciones DESC
LIMIT 10;
-- Cambios en los últimos 7 días
SELECT i.id, i.vehicle_plate, u.full_name, ial.field_name, ial.created_at
FROM inspection_audit_log ial
JOIN inspections i ON ial.inspection_id = i.id
JOIN users u ON ial.user_id = u.id
WHERE ial.created_at >= NOW() - INTERVAL '7 days'
ORDER BY ial.created_at DESC;
```
---
## ⚠️ Notas Importantes
1. **Solo Admins Pueden Editar**
- Los mecánicos NO pueden editar inspecciones completadas
- Solo usuarios con rol `admin` tienen acceso
2. **Solo Inspecciones Completadas**
- No se pueden editar inspecciones en estado "draft"
- El botón de editar solo aparece en inspecciones completadas
3. **Motivo Obligatorio**
- Cada cambio DEBE tener una justificación
- El campo "Motivo del cambio" es obligatorio
- No se puede guardar sin completarlo
4. **Recalculo Automático**
- Al cambiar el status, los puntos se recalculan automáticamente
- OK = 100% de puntos
- Warning = 50% de puntos
- Critical/NA = 0% de puntos
5. **Registro Completo**
- Cada campo modificado genera un registro separado
- Se guarda el valor anterior y el nuevo
- Se registra quién y cuándo hizo el cambio
- No se pueden borrar registros de auditoría
6. **Cascada en Borrado**
- Si se borra una inspección, se borran sus logs
- Si se borra una respuesta, se borran sus logs
- Los logs del usuario permanecen aunque se borre el usuario
---
## 🐛 Troubleshooting
### Problema: "No puedo editar una respuesta"
**Solución**:
1. Verificar que eres admin: `SELECT role FROM users WHERE id = X;`
2. Verificar que la inspección está completada: `SELECT status FROM inspections WHERE id = Y;`
3. Verificar que el botón "✏️ Editar" aparece
4. Revisar consola del navegador para errores
### Problema: "Error al guardar cambios"
**Solución**:
1. Verificar que completaste el campo "Motivo del cambio"
2. Verificar token de autenticación válido
3. Revisar logs del backend para errores específicos
4. Verificar que la tabla `inspection_audit_log` existe
### Problema: "No veo el historial de cambios"
**Solución**:
1. Verificar que eres admin
2. Verificar que hay cambios registrados: `SELECT * FROM inspection_audit_log WHERE inspection_id = X;`
3. Limpiar caché del navegador (Ctrl+Shift+R)
4. Revisar consola del navegador para errores de API
### Problema: "Los puntos no se recalculan correctamente"
**Solución**:
1. Verificar la lógica en el backend (main.py, admin_edit_answer)
2. Revisar que la pregunta tiene `points` configurados
3. Verificar logs de auditoría para ver si se registró el cambio de puntos
4. Recalcular manualmente si es necesario
---
## 🎉 Resumen
**Backend**: Sistema completo de auditoría con registro automático
**Frontend**: Edición inline + modal de historial
**Base de Datos**: Tabla de auditoría con índices optimizados
**Seguridad**: Solo admins, motivo obligatorio, registro inmutable
**Documentación**: Completa con ejemplos y troubleshooting
El sistema está listo para usar después de ejecutar la migración SQL y reiniciar el backend.
---
## 📈 Beneficios
1. **Trazabilidad Completa**: Saber quién cambió qué y cuándo
2. **Auditoría**: Cumplimiento de normas de calidad y transparencia
3. **Corrección de Errores**: Admins pueden corregir errores sin perder datos
4. **Accountability**: Cada cambio requiere justificación documentada
5. **Historial Inmutable**: Los registros no se pueden borrar ni modificar
6. **Reportes**: Base de datos lista para generar reportes de cambios

283
PERMISOS_CHECKLIST.md Normal file
View File

@@ -0,0 +1,283 @@
# Sistema de Permisos por Mecánico - Checklists
## ✅ Implementación Completa
Se ha implementado un sistema completo de permisos que permite controlar qué mecánicos pueden usar cada checklist.
---
## 🎯 Características Implementadas
### **Backend**
1. **Nueva Tabla de Permisos**
- Tabla `checklist_permissions` con relación many-to-many
- Constraint UNIQUE para evitar duplicados
- Índices optimizados para consultas rápidas
2. **Lógica de Acceso**
- **Acceso Global**: Si un checklist NO tiene permisos registrados, todos los mecánicos pueden usarlo
- **Acceso Restringido**: Si tiene permisos, solo esos mecánicos específicos pueden verlo
- **Admins**: Siempre ven todos los checklists
3. **Endpoints Actualizados**
- `GET /api/checklists`: Filtra automáticamente por permisos del mecánico
- `POST /api/checklists`: Guarda permisos al crear
- `PUT /api/checklists/{id}`: Actualiza permisos al editar
- Incluye `allowed_mechanics` en las respuestas
### **Frontend**
1. **Creación de Checklists**
- Selector de mecánicos con checkboxes
- Opción "Acceso Global" (no seleccionar ninguno)
- Interfaz clara con íconos 🔐 y 🌍
2. **Visualización**
- Badge verde "🌍 Acceso Global" para checklists sin restricciones
- Badge índigo "🔐 Restringido - X mecánicos" para checklists restringidos
- Solo visible para administradores
3. **Edición de Permisos**
- Botón "🔐 Permisos" en cada checklist (solo admins)
- Modal dedicado para editar permisos
- Cambios se aplican inmediatamente
4. **Mensajes Mejorados**
- Mensaje específico para mecánicos sin checklists disponibles
- Instrucciones claras para contactar al administrador
---
## 📋 Instrucciones de Uso
### **Para Administradores**
#### 1. Crear Checklist con Permisos
1. Ve a la pestaña "Checklists"
2. Haz clic en "+ Crear Checklist"
3. Completa los datos del checklist
4. En la sección "🔐 Mecánicos Autorizados":
- **Para acceso global**: No selecciones ningún mecánico (deja todo sin marcar)
- **Para acceso restringido**: Marca los mecánicos que tendrán acceso
5. Haz clic en "Crear Checklist"
#### 2. Editar Permisos de Checklist Existente
1. Ve a la pestaña "Checklists"
2. Busca el checklist que quieres modificar
3. Haz clic en el botón "🔐 Permisos"
4. Modifica la selección de mecánicos:
- Marca "🌍 Todos los mecánicos" para acceso global
- O selecciona mecánicos específicos
5. Haz clic en "Guardar Permisos"
**Nota**: Los cambios son inmediatos. Los mecánicos que pierdan acceso dejarán de ver el checklist instantáneamente.
#### 3. Ver Estado de Permisos
Cada tarjeta de checklist muestra:
- **🌍 Acceso Global - Todos los mecánicos**: Sin restricciones
- **🔐 Restringido - X mecánicos**: Solo esos mecánicos tienen acceso
### **Para Mecánicos**
- Solo verás los checklists donde tienes permiso
- Si no ves ningún checklist, contacta al administrador
- No puedes modificar permisos (solo el admin puede hacerlo)
---
## 🗄️ Migración de Base de Datos
### **Ejecutar SQL**
```bash
# Usando psql
psql -U tu_usuario -d tu_database -f migrations/add_checklist_permissions.sql
# O directamente
psql -U tu_usuario -d tu_database
```
Luego ejecuta:
```sql
-- Crear tabla de permisos checklist-mecánico
CREATE TABLE IF NOT EXISTS checklist_permissions (
id SERIAL PRIMARY KEY,
checklist_id INTEGER NOT NULL REFERENCES checklists(id) ON DELETE CASCADE,
mechanic_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
UNIQUE(checklist_id, mechanic_id)
);
-- Crear índices
CREATE INDEX idx_checklist_permissions_checklist ON checklist_permissions(checklist_id);
CREATE INDEX idx_checklist_permissions_mechanic ON checklist_permissions(mechanic_id);
-- Comentarios
COMMENT ON TABLE checklist_permissions IS 'Control de acceso de mecánicos a checklists. Si no hay registros para un checklist, todos los mecánicos tienen acceso.';
```
### **Reiniciar Backend**
Después de ejecutar el SQL, reinicia el backend para que los cambios en los modelos tomen efecto:
```bash
# Si usas Docker
docker-compose restart backend
# Si corres directamente
# Ctrl+C y volver a ejecutar
python -m uvicorn app.main:app --reload
```
---
## 🔍 Ejemplos de Uso
### Ejemplo 1: Checklist para Todos los Mecánicos
**Escenario**: Checklist de "Revisión Básica" que todos pueden usar
**Configuración**:
- Al crear/editar: No seleccionar ningún mecánico
- El sistema muestra: "🌍 Acceso Global - Todos los mecánicos"
**Resultado**: Todos los mecánicos ven este checklist
---
### Ejemplo 2: Checklist Especializado
**Escenario**: Checklist de "Mantenimiento Eléctrico" solo para mecánicos certificados
**Configuración**:
1. Al crear/editar: Seleccionar solo mecánicos con certificación eléctrica
2. El sistema muestra: "🔐 Restringido - 3 mecánicos"
**Resultado**: Solo esos 3 mecánicos ven este checklist
---
### Ejemplo 3: Cambio de Permisos
**Escenario**: Un mecánico nuevo se certifica en electricidad
**Pasos**:
1. Admin hace clic en "🔐 Permisos" del checklist "Mantenimiento Eléctrico"
2. Marca al nuevo mecánico en la lista
3. Guarda cambios
4. El mecánico inmediatamente ve el checklist en su lista
---
## 📊 Base de Datos
### Estructura de `checklist_permissions`
| Campo | Tipo | Descripción |
|-------|------|-------------|
| id | SERIAL | ID único de la relación |
| checklist_id | INTEGER | ID del checklist |
| mechanic_id | INTEGER | ID del mecánico autorizado |
| created_at | TIMESTAMP | Fecha de creación del permiso |
### Consultas Útiles
```sql
-- Ver permisos de un checklist específico
SELECT u.full_name, u.email
FROM checklist_permissions cp
JOIN users u ON cp.mechanic_id = u.id
WHERE cp.checklist_id = 1;
-- Ver qué checklists puede usar un mecánico
SELECT c.name, c.description
FROM checklist_permissions cp
JOIN checklists c ON cp.checklist_id = c.id
WHERE cp.mechanic_id = 5;
-- Checklists sin restricciones (acceso global)
SELECT c.id, c.name
FROM checklists c
LEFT JOIN checklist_permissions cp ON c.id = cp.checklist_id
WHERE cp.id IS NULL;
-- Dar acceso a un mecánico a un checklist
INSERT INTO checklist_permissions (checklist_id, mechanic_id)
VALUES (1, 5)
ON CONFLICT (checklist_id, mechanic_id) DO NOTHING;
-- Quitar acceso
DELETE FROM checklist_permissions
WHERE checklist_id = 1 AND mechanic_id = 5;
-- Convertir checklist a acceso global (borrar todos los permisos)
DELETE FROM checklist_permissions WHERE checklist_id = 1;
```
---
## ⚠️ Notas Importantes
1. **Permisos Vacíos = Acceso Global**
- Si un checklist NO tiene registros en `checklist_permissions`, TODOS los mecánicos pueden usarlo
- Es el comportamiento por defecto
2. **Los Admins Siempre Ven Todo**
- Los usuarios con rol `admin` ven todos los checklists sin importar los permisos
- Útil para gestión y supervisión
3. **Cambios Inmediatos**
- Al editar permisos, los cambios se aplican instantáneamente
- No requiere logout/login
4. **Cascada en Borrado**
- Si borras un checklist, sus permisos se borran automáticamente
- Si borras un mecánico, sus permisos se borran automáticamente
5. **Inspecciones Existentes**
- Las inspecciones ya creadas NO se ven afectadas por cambios de permisos
- Solo afecta la creación de nuevas inspecciones
---
## 🐛 Troubleshooting
### Problema: "Mecánico no ve ningún checklist"
**Solución**:
1. Verificar que el mecánico esté activo: `SELECT is_active FROM users WHERE id = X;`
2. Verificar permisos: `SELECT * FROM checklist_permissions WHERE mechanic_id = X;`
3. Verificar si hay checklists con acceso global (sin permisos)
4. Verificar rol del usuario: debe ser `mechanic` o `mecanico`
### Problema: "Error al crear/editar permisos"
**Solución**:
1. Verificar que la tabla `checklist_permissions` existe
2. Verificar que los IDs de mecánicos son válidos
3. Revisar logs del backend para errores específicos
4. Verificar que el usuario es admin
### Problema: "Los permisos no se aplican"
**Solución**:
1. Hacer logout y login nuevamente
2. Verificar que el backend se reinició después de la migración
3. Limpiar caché del navegador (Ctrl+Shift+R)
4. Verificar en la base de datos que los permisos se guardaron correctamente
---
## 🎉 Resumen
**Backend**: Filtrado automático por permisos
**Frontend**: Interfaz completa para gestionar permisos
**Base de Datos**: Migración lista para ejecutar
**Documentación**: Completa con ejemplos
El sistema está listo para usar después de ejecutar la migración SQL y reiniciar el backend.

View File

@@ -1,3 +1,13 @@
import os
# Variables de conexión S3/MinIO
MINIO_HOST = os.getenv('MINIO_HOST', 'localhost')
MINIO_SECURE = os.getenv('MINIO_SECURE', 'false').lower() == 'true'
MINIO_PORT = int(os.getenv('MINIO_PORT', '9000'))
MINIO_ACCESS_KEY = os.getenv('MINIO_ACCESS_KEY', 'minioadmin')
MINIO_SECRET_KEY = os.getenv('MINIO_SECRET_KEY', 'minioadmin')
MINIO_IMAGE_BUCKET = os.getenv('MINIO_IMAGE_BUCKET', 'images')
MINIO_PDF_BUCKET = os.getenv('MINIO_PDF_BUCKET', 'pdfs')
MINIO_ENDPOINT = f"{'https' if MINIO_SECURE else 'http'}://{MINIO_HOST}:{MINIO_PORT}"
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
@@ -15,6 +25,9 @@ class Settings(BaseSettings):
# Environment
ENVIRONMENT: str = "development"
# Notificaciones
NOTIFICACION_ENDPOINT: str = ""
# CORS - Orígenes permitidos separados por coma
ALLOWED_ORIGINS: str = "http://localhost:3000,http://localhost:5173"

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,7 @@ class User(Base):
username = Column(String(50), unique=True, index=True, nullable=False)
email = Column(String(100), unique=True, index=True)
password_hash = Column(String(255), nullable=False)
role = Column(String(20), nullable=False) # admin, mechanic
role = Column(String(20), nullable=False) # admin, mechanic, asesor
full_name = Column(String(100))
is_active = Column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
@@ -55,6 +55,7 @@ class Checklist(Base):
creator = relationship("User", back_populates="checklists_created")
questions = relationship("Question", back_populates="checklist", cascade="all, delete-orphan")
inspections = relationship("Inspection", back_populates="checklist")
permissions = relationship("ChecklistPermission", back_populates="checklist", cascade="all, delete-orphan")
class Question(Base):
@@ -71,11 +72,22 @@ class Question(Base):
allow_photos = Column(Boolean, default=True)
max_photos = Column(Integer, default=3)
requires_comment_on_fail = Column(Boolean, default=False)
send_notification = Column(Boolean, default=False)
# Conditional logic
parent_question_id = Column(Integer, ForeignKey("questions.id"), nullable=True)
show_if_answer = Column(String(50), nullable=True) # Valor que dispara esta pregunta
# AI Analysis
ai_prompt = Column(Text, nullable=True) # Prompt personalizado para análisis de IA de esta pregunta
created_at = Column(DateTime(timezone=True), server_default=func.now())
# Relationships
checklist = relationship("Checklist", back_populates="questions")
answers = relationship("Answer", back_populates="question")
subquestions = relationship("Question", backref="parent", remote_side=[id])
class Inspection(Base):
@@ -116,6 +128,7 @@ class Inspection(Base):
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
pdf_url = Column(String(500)) # URL del PDF en S3
# Relationships
checklist = relationship("Checklist", back_populates="inspections")
mechanic = relationship("User", back_populates="inspections")
@@ -169,7 +182,44 @@ class AIConfiguration(Base):
id = Column(Integer, primary_key=True, index=True)
provider = Column(String(50), nullable=False) # openai, gemini
api_key = Column(Text, nullable=False)
model_name = Column(String(100), nullable=False)
model_name = Column(String(100), nullable=True)
logo_url = Column(Text, nullable=True) # URL del logo configurable
is_active = Column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
class ChecklistPermission(Base):
"""Tabla intermedia para permisos de checklist por mecánico"""
__tablename__ = "checklist_permissions"
id = Column(Integer, primary_key=True, index=True)
checklist_id = Column(Integer, ForeignKey("checklists.id", ondelete="CASCADE"), nullable=False)
mechanic_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
# Relationships
checklist = relationship("Checklist", back_populates="permissions")
mechanic = relationship("User")
class InspectionAuditLog(Base):
"""Registro de auditoría para cambios en inspecciones y respuestas"""
__tablename__ = "inspection_audit_log"
id = Column(Integer, primary_key=True, index=True)
inspection_id = Column(Integer, ForeignKey("inspections.id", ondelete="CASCADE"), nullable=False)
answer_id = Column(Integer, ForeignKey("answers.id", ondelete="CASCADE"), nullable=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
action = Column(String(50), nullable=False) # created, updated, deleted, status_changed
entity_type = Column(String(50), nullable=False) # inspection, answer
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
inspection = relationship("Inspection")
answer = relationship("Answer")
user = relationship("User")

View File

@@ -70,10 +70,16 @@ class ChecklistBase(BaseModel):
logo_url: Optional[str] = None
class ChecklistCreate(ChecklistBase):
pass
mechanic_ids: Optional[List[int]] = [] # IDs de mecánicos autorizados
class ChecklistUpdate(ChecklistBase):
class ChecklistUpdate(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
ai_mode: Optional[str] = None
scoring_enabled: Optional[bool] = None
logo_url: Optional[str] = None
is_active: Optional[bool] = None
mechanic_ids: Optional[List[int]] = None # IDs de mecánicos autorizados
class Checklist(ChecklistBase):
id: int
@@ -81,6 +87,7 @@ class Checklist(ChecklistBase):
is_active: bool
created_by: int
created_at: datetime
allowed_mechanics: Optional[List[int]] = [] # IDs de mecánicos permitidos
class Config:
from_attributes = True
@@ -97,6 +104,10 @@ class QuestionBase(BaseModel):
allow_photos: bool = True
max_photos: int = 3
requires_comment_on_fail: bool = False
send_notification: bool = False
parent_question_id: Optional[int] = None
show_if_answer: Optional[str] = None
ai_prompt: Optional[str] = None
class QuestionCreate(QuestionBase):
checklist_id: int
@@ -113,6 +124,7 @@ class Question(QuestionBase):
from_attributes = True
# Inspection Schemas
class InspectionBase(BaseModel):
or_number: Optional[str] = None
@@ -212,7 +224,7 @@ class InspectionDetail(Inspection):
class AIConfigurationBase(BaseModel):
provider: str # openai, gemini
api_key: str
model_name: str
model_name: Optional[str] = None
class AIConfigurationCreate(AIConfigurationBase):
pass
@@ -236,3 +248,72 @@ class AIModelInfo(BaseModel):
name: str
provider: str
description: Optional[str] = None
# Reports Schemas
class InspectionStats(BaseModel):
total_inspections: int
completed_inspections: int
pending_inspections: int
completion_rate: float
avg_score: float
total_flagged_items: int
class MechanicRanking(BaseModel):
mechanic_id: int
mechanic_name: str
total_inspections: int
avg_score: float
completion_rate: float
class ChecklistStats(BaseModel):
checklist_id: int
checklist_name: str
total_inspections: int
avg_score: float
class DashboardData(BaseModel):
stats: InspectionStats
mechanic_ranking: List[MechanicRanking]
checklist_stats: List[ChecklistStats]
inspections_by_date: dict
pass_fail_ratio: dict
class InspectionListItem(BaseModel):
id: int
vehicle_plate: str
checklist_name: str
mechanic_name: str
status: str
score: Optional[int]
max_score: Optional[int]
flagged_items: int
started_at: Optional[datetime]
completed_at: Optional[datetime]
# Audit Log Schemas
class AuditLogBase(BaseModel):
action: str
entity_type: str
field_name: Optional[str] = None
old_value: Optional[str] = None
new_value: Optional[str] = None
comment: Optional[str] = None
class AuditLog(AuditLogBase):
id: int
inspection_id: int
answer_id: Optional[int] = None
user_id: int
user_name: Optional[str] = None
created_at: datetime
class Config:
from_attributes = True
class AnswerEdit(BaseModel):
answer_value: Optional[str] = None
status: Optional[str] = None
comment: Optional[str] = None
is_flagged: Optional[bool] = None
edit_comment: Optional[str] = None # Comentario del admin sobre por qué editó

25
backend/docker.ps1 Normal file
View File

@@ -0,0 +1,25 @@
Clear-Host
# Input
$version = Read-Host "Ingrese el numero de version (ej: 1.0.34)"
Write-Host "`n=== Construyendo imagen dymai/syntria-backend:$version ===`n"
docker build -t "dymai/syntria-backend:$version" .
if ($LASTEXITCODE -ne 0) {
Write-Host "`nERROR: El build fallo. No se realizara el push." -ForegroundColor Red
pause
exit 1
}
Write-Host "`n=== Subiendo imagen a Docker Hub ===`n"
docker push "dymai/syntria-backend:$version"
if ($LASTEXITCODE -ne 0) {
Write-Host "`nERROR: El push fallo." -ForegroundColor Red
pause
exit 1
}
Write-Host "`n=== Proceso completado exitosamente ===`n" -ForegroundColor Green
pause

View File

@@ -0,0 +1,32 @@
"""
Migration: Add ai_prompt column to questions table
Date: 2025-11-21
Description: Adds ai_prompt TEXT column for custom AI analysis prompts per question
"""
# SQL Migration Script
sql_statements = [
# Add ai_prompt column
"""
ALTER TABLE questions
ADD COLUMN ai_prompt TEXT;
""",
]
# To apply this migration, run these SQL statements in your PostgreSQL database:
if __name__ == "__main__":
print("=" * 80)
print("MIGRATION: Add ai_prompt to questions table")
print("=" * 80)
print("\nExecute the following SQL statements in your PostgreSQL database:\n")
for i, statement in enumerate(sql_statements, 1):
print(f"-- Statement {i}")
print(statement.strip())
print()
print("=" * 80)
print("\nTo verify the migration:")
print("SELECT column_name, data_type FROM information_schema.columns")
print("WHERE table_name = 'questions' AND column_name = 'ai_prompt';")
print("=" * 80)

View File

@@ -0,0 +1,45 @@
"""
Migration script to add conditional questions support
Run this script to add parent_question_id and show_if_answer columns
"""
from sqlalchemy import create_engine, text
import os
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://checklist_user:checklist_pass_2024@localhost:5432/checklist_db")
engine = create_engine(DATABASE_URL)
migrations = [
"""
ALTER TABLE questions
ADD COLUMN IF NOT EXISTS parent_question_id INTEGER REFERENCES questions(id) ON DELETE CASCADE;
""",
"""
ALTER TABLE questions
ADD COLUMN IF NOT EXISTS show_if_answer VARCHAR(50);
""",
"""
CREATE INDEX IF NOT EXISTS idx_questions_parent
ON questions(parent_question_id);
"""
]
def run_migration():
print("🔄 Starting migration for conditional questions...")
with engine.connect() as conn:
for i, migration in enumerate(migrations, 1):
try:
conn.execute(text(migration))
conn.commit()
print(f"✅ Migration {i}/{len(migrations)} completed")
except Exception as e:
print(f"❌ Error in migration {i}: {e}")
conn.rollback()
return False
print("✅ All migrations completed successfully!")
return True
if __name__ == "__main__":
run_migration()

View File

@@ -15,3 +15,5 @@ google-generativeai==0.3.2
Pillow==10.2.0
reportlab==4.0.9
python-dotenv==1.0.0
boto3==1.34.89
requests==2.31.0

52
backend/s3test.py Normal file
View File

@@ -0,0 +1,52 @@
import boto3
from botocore.client import Config
from botocore.exceptions import ClientError
MINIO_ENDPOINT = "minioapi.rshtech.com.py"
MINIO_ACCESS_KEY = "6uEIJyKR2Fi4UXiSgIeG"
MINIO_SECRET_KEY = "8k0kYuvxD9ePuvjdxvDk8WkGhhlaaee8BxU1mqRW"
MINIO_IMAGE_BUCKET = "images"
MINIO_PDF_BUCKET = "pdfs"
MINIO_SECURE = True # HTTPS
MINIO_PORT = 443
def main():
try:
endpoint_url = f"https://{MINIO_ENDPOINT}:{MINIO_PORT}" if MINIO_SECURE \
else f"http://{MINIO_ENDPOINT}:{MINIO_PORT}"
# Crear cliente S3 compatible para MinIO
s3 = boto3.client(
"s3",
endpoint_url=endpoint_url,
aws_access_key_id=MINIO_ACCESS_KEY,
aws_secret_access_key=MINIO_SECRET_KEY,
config=Config(signature_version="s3v4"),
region_name="us-east-1"
)
print("🔍 Probando conexión…")
# Listar buckets
response = s3.list_buckets()
print("✅ Conexión exitosa. Buckets disponibles:")
for bucket in response.get("Buckets", []):
print(f" - {bucket['Name']}")
# Verificar acceso a buckets específicos
for bucket_name in [MINIO_IMAGE_BUCKET, MINIO_PDF_BUCKET]:
try:
s3.head_bucket(Bucket=bucket_name)
print(f"✔ Acceso OK al bucket: {bucket_name}")
except ClientError:
print(f"❌ No se pudo acceder al bucket: {bucket_name}")
print("🎉 Test finalizado correctamente.")
except Exception as e:
print("❌ Error:", e)
if __name__ == "__main__":
main()

30
backend/test_minio.py Normal file
View File

@@ -0,0 +1,30 @@
import os
from app.core import config as app_config
import boto3
from botocore.client import Config
scheme = 'https' if app_config.MINIO_SECURE else 'http'
endpoint = f"{scheme}://{os.getenv('MINIO_ENDPOINT', 'localhost')}:{app_config.MINIO_PORT}"
access_key = os.getenv('MINIO_ACCESS_KEY', 'minioadmin')
secret_key = os.getenv('MINIO_SECRET_KEY', 'minioadmin')
bucket = os.getenv('MINIO_IMAGE_BUCKET', 'images')
s3 = boto3.client(
's3',
endpoint_url=endpoint,
aws_access_key_id=access_key,
aws_secret_access_key=secret_key,
config=Config(signature_version='s3v4'),
region_name='us-east-1'
)
try:
# List buckets
response = s3.list_buckets()
print('Buckets:', [b['Name'] for b in response['Buckets']])
# Upload test file
with open('test_minio.py', 'rb') as f:
s3.upload_fileobj(f, bucket, 'test_minio.py')
print(f'Archivo subido a bucket {bucket} correctamente.')
except Exception as e:
print('Error:', e)

View File

@@ -20,7 +20,7 @@ services:
retries: 5
backend:
image: dymai/syntria-backend:latest
image: dymai/syntria-backend:1.0.15
container_name: syntria-backend-prod
restart: always
depends_on:
@@ -38,7 +38,7 @@ services:
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4
frontend:
image: dymai/syntria-frontend:latest
image: dymai/syntria-frontend:1.0.24
container_name: syntria-frontend-prod
restart: always
depends_on:

27
frontend/buildFront.ps1 Normal file
View File

@@ -0,0 +1,27 @@
Clear-Host
# Pedir version
$version = Read-Host "Ingrese el numero de version (ej: 1.0.34)"
Write-Host "`n=== Construyendo imagen dymai/syntria-frontend:$version ===`n"
docker build -f Dockerfile.prod -t "dymai/syntria-frontend:$version" .
# Si build falla, no continuar
if ($LASTEXITCODE -ne 0) {
Write-Host "`nERROR: El build fallo. No se realizara el push." -ForegroundColor Red
pause
exit 1
}
Write-Host "`n=== Subiendo imagen a Docker Hub ===`n"
docker push "dymai/syntria-frontend:$version"
# Si push falla, mostrar error
if ($LASTEXITCODE -ne 0) {
Write-Host "`nERROR: El push fallo." -ForegroundColor Red
pause
exit 1
}
Write-Host "`n=== Proceso completado exitosamente ===`n" -ForegroundColor Green
pause

View File

@@ -4,8 +4,8 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Syntria - Sistema Inteligente de Inspecciones</title>
<meta name="description" content="Syntria: Sistema avanzado de inspecciones vehiculares con inteligencia artificial" />
<title>AYUTEC - Sistema Inteligente de Inspecciones</title>
<meta name="description" content="AYUTEC: Sistema avanzado de inspecciones vehiculares con inteligencia artificial" />
</head>
<body>
<div id="root"></div>

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ export default function Sidebar({ user, activeTab, setActiveTab, sidebarOpen, se
<div className="w-8 h-8 bg-gradient-to-br from-indigo-500 to-purple-500 rounded-lg flex items-center justify-center">
<span className="text-white font-bold text-lg">S</span>
</div>
<h2 className="text-xl font-bold bg-gradient-to-r from-indigo-400 to-purple-400 bg-clip-text text-transparent">Syntria</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>
)}
<button
@@ -67,20 +67,26 @@ export default function Sidebar({ user, activeTab, setActiveTab, sidebarOpen, se
{sidebarOpen && <span>Usuarios</span>}
</button>
</li>
<li>
<button
onClick={() => setActiveTab('reports')}
className={`w-full flex items-center ${sidebarOpen ? 'gap-3 px-4' : 'justify-center px-2'} py-3 rounded-lg transition ${
activeTab === 'reports'
? 'bg-gradient-to-r from-indigo-600 to-purple-600 text-white shadow-lg'
: 'text-indigo-200 hover:bg-indigo-900/50'
}`}
title={!sidebarOpen ? 'Reportes' : ''}
>
<span className="text-xl">📊</span>
{sidebarOpen && <span>Reportes</span>}
</button>
</li>
</>
)}
{(user.role === 'admin' || user.role === 'asesor') && (
<li>
<button
onClick={() => setActiveTab('reports')}
className={`w-full flex items-center ${sidebarOpen ? 'gap-3 px-4' : 'justify-center px-2'} py-3 rounded-lg transition ${
activeTab === 'reports'
? 'bg-gradient-to-r from-indigo-600 to-purple-600 text-white shadow-lg'
: 'text-indigo-200 hover:bg-indigo-900/50'
}`}
title={!sidebarOpen ? 'Reportes' : ''}
>
<span className="text-xl">📊</span>
{sidebarOpen && <span>Reportes</span>}
</button>
</li>
)}
{user.role === 'admin' && (
<>
<li>
<button
onClick={() => setActiveTab('api-tokens')}
@@ -123,7 +129,9 @@ export default function Sidebar({ user, activeTab, setActiveTab, sidebarOpen, se
{sidebarOpen && (
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate text-white">{user.full_name || user.username}</p>
<p className="text-xs text-indigo-300">{user.role === 'admin' ? '👑 Admin' : '🔧 Mecánico'}</p>
<p className="text-xs text-indigo-300">
{user.role === 'admin' ? '👑 Admin' : user.role === 'asesor' ? '📊 Asesor' : '🔧 Mecánico'}
</p>
</div>
)}
</div>

26
gitUpdate.ps1 Normal file
View File

@@ -0,0 +1,26 @@
Clear-Host
# Pedir mensaje de commit
$mensaje = Read-Host "Ingrese el mensaje de commit"
Write-Host "Agregando archivos..."
git add .
Write-Host "Creando commit..."
git commit -m "$mensaje"
Write-Host "Haciendo push a la rama develop..."
$pushOutput = git push origin develop 2>&1
# Revisar si fallo la autenticacion
if ($pushOutput -match "Authentication failed" -or $pushOutput -match "Failed to authenticate") {
Write-Host "`nERROR: Fallo la autenticacion. Ejecutando git init para reconfigurar..." -ForegroundColor Red
git init
Write-Host "Intentando push nuevamente..."
git push origin develop
}
Write-Host "`nProceso finalizado."
pause

View File

@@ -0,0 +1,26 @@
-- Migración: Agregar sistema de permisos por mecánico para checklists
-- Fecha: 2025-11-25
-- Descripción: Crea tabla intermedia para controlar qué mecánicos pueden usar cada checklist
-- Crear tabla de permisos checklist-mecánico
CREATE TABLE IF NOT EXISTS checklist_permissions (
id SERIAL PRIMARY KEY,
checklist_id INTEGER NOT NULL REFERENCES checklists(id) ON DELETE CASCADE,
mechanic_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
-- Constraint para evitar duplicados
UNIQUE(checklist_id, mechanic_id)
);
-- Crear índices para mejorar rendimiento
CREATE INDEX idx_checklist_permissions_checklist ON checklist_permissions(checklist_id);
CREATE INDEX idx_checklist_permissions_mechanic ON checklist_permissions(mechanic_id);
-- Comentarios para documentación
COMMENT ON TABLE checklist_permissions IS 'Control de acceso de mecánicos a checklists. Si no hay registros para un checklist, todos los mecánicos tienen acceso.';
COMMENT ON COLUMN checklist_permissions.checklist_id IS 'ID del checklist restringido';
COMMENT ON COLUMN checklist_permissions.mechanic_id IS 'ID del mecánico autorizado';
-- Verificar que la migración se ejecutó correctamente
SELECT 'Tabla checklist_permissions creada exitosamente' AS status;

View File

@@ -0,0 +1,39 @@
-- Migración: Agregar sistema de auditoría para edición de inspecciones
-- Fecha: 2025-11-25
-- Descripción: Crea tabla de auditoría para rastrear todos los cambios en inspecciones y respuestas
-- Crear tabla de auditoría
CREATE TABLE IF NOT EXISTS inspection_audit_log (
id SERIAL PRIMARY KEY,
inspection_id INTEGER NOT NULL REFERENCES inspections(id) ON DELETE CASCADE,
answer_id INTEGER REFERENCES answers(id) ON DELETE CASCADE,
user_id INTEGER NOT NULL REFERENCES users(id),
action VARCHAR(50) NOT NULL, -- created, updated, deleted, status_changed
entity_type VARCHAR(50) NOT NULL, -- inspection, answer
field_name VARCHAR(100), -- Campo modificado
old_value TEXT, -- Valor anterior
new_value TEXT, -- Valor nuevo
comment TEXT, -- Comentario del cambio
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Crear índices para mejorar rendimiento
CREATE INDEX idx_audit_log_inspection ON inspection_audit_log(inspection_id);
CREATE INDEX idx_audit_log_answer ON inspection_audit_log(answer_id);
CREATE INDEX idx_audit_log_user ON inspection_audit_log(user_id);
CREATE INDEX idx_audit_log_created_at ON inspection_audit_log(created_at DESC);
-- Comentarios para documentación
COMMENT ON TABLE inspection_audit_log IS 'Registro de auditoría de cambios en inspecciones y respuestas. Registra quién, cuándo y qué cambió.';
COMMENT ON COLUMN inspection_audit_log.action IS 'Tipo de acción: created, updated, deleted, status_changed';
COMMENT ON COLUMN inspection_audit_log.entity_type IS 'Tipo de entidad modificada: inspection, answer';
COMMENT ON COLUMN inspection_audit_log.field_name IS 'Nombre del campo que fue modificado';
COMMENT ON COLUMN inspection_audit_log.old_value IS 'Valor anterior del campo';
COMMENT ON COLUMN inspection_audit_log.new_value IS 'Valor nuevo del campo';
COMMENT ON COLUMN inspection_audit_log.comment IS 'Comentario del administrador sobre por qué realizó el cambio';
-- Verificar que la migración se ejecutó correctamente
SELECT 'Tabla inspection_audit_log creada exitosamente' AS status;
-- Opcional: Ver estructura de la tabla
\d inspection_audit_log