develop #1
375
AUDITORIA_INSPECCIONES.md
Normal file
375
AUDITORIA_INSPECCIONES.md
Normal 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
283
PERMISOS_CHECKLIST.md
Normal 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.
|
||||||
@@ -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
|
from pydantic_settings import BaseSettings
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
@@ -15,6 +25,9 @@ class Settings(BaseSettings):
|
|||||||
# Environment
|
# Environment
|
||||||
ENVIRONMENT: str = "development"
|
ENVIRONMENT: str = "development"
|
||||||
|
|
||||||
|
# Notificaciones
|
||||||
|
NOTIFICACION_ENDPOINT: str = ""
|
||||||
|
|
||||||
# CORS - Orígenes permitidos separados por coma
|
# CORS - Orígenes permitidos separados por coma
|
||||||
ALLOWED_ORIGINS: str = "http://localhost:3000,http://localhost:5173"
|
ALLOWED_ORIGINS: str = "http://localhost:3000,http://localhost:5173"
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,7 @@ class User(Base):
|
|||||||
username = Column(String(50), unique=True, index=True, nullable=False)
|
username = Column(String(50), unique=True, index=True, nullable=False)
|
||||||
email = Column(String(100), unique=True, index=True)
|
email = Column(String(100), unique=True, index=True)
|
||||||
password_hash = Column(String(255), nullable=False)
|
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))
|
full_name = Column(String(100))
|
||||||
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())
|
||||||
@@ -55,6 +55,7 @@ class Checklist(Base):
|
|||||||
creator = relationship("User", back_populates="checklists_created")
|
creator = relationship("User", back_populates="checklists_created")
|
||||||
questions = relationship("Question", back_populates="checklist", cascade="all, delete-orphan")
|
questions = relationship("Question", back_populates="checklist", cascade="all, delete-orphan")
|
||||||
inspections = relationship("Inspection", back_populates="checklist")
|
inspections = relationship("Inspection", back_populates="checklist")
|
||||||
|
permissions = relationship("ChecklistPermission", back_populates="checklist", cascade="all, delete-orphan")
|
||||||
|
|
||||||
|
|
||||||
class Question(Base):
|
class Question(Base):
|
||||||
@@ -71,11 +72,22 @@ class Question(Base):
|
|||||||
allow_photos = Column(Boolean, default=True)
|
allow_photos = Column(Boolean, default=True)
|
||||||
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)
|
||||||
|
|
||||||
|
# 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())
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
checklist = relationship("Checklist", back_populates="questions")
|
checklist = relationship("Checklist", back_populates="questions")
|
||||||
answers = relationship("Answer", back_populates="question")
|
answers = relationship("Answer", back_populates="question")
|
||||||
|
subquestions = relationship("Question", backref="parent", remote_side=[id])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Inspection(Base):
|
class Inspection(Base):
|
||||||
@@ -116,6 +128,7 @@ class Inspection(Base):
|
|||||||
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())
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||||
|
|
||||||
|
pdf_url = Column(String(500)) # URL del PDF en S3
|
||||||
# Relationships
|
# Relationships
|
||||||
checklist = relationship("Checklist", back_populates="inspections")
|
checklist = relationship("Checklist", back_populates="inspections")
|
||||||
mechanic = relationship("User", back_populates="inspections")
|
mechanic = relationship("User", back_populates="inspections")
|
||||||
@@ -169,7 +182,44 @@ class AIConfiguration(Base):
|
|||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
provider = Column(String(50), nullable=False) # openai, gemini
|
provider = Column(String(50), nullable=False) # openai, gemini
|
||||||
api_key = Column(Text, nullable=False)
|
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)
|
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())
|
||||||
updated_at = Column(DateTime(timezone=True), onupdate=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")
|
||||||
|
|||||||
@@ -70,10 +70,16 @@ class ChecklistBase(BaseModel):
|
|||||||
logo_url: Optional[str] = None
|
logo_url: Optional[str] = None
|
||||||
|
|
||||||
class ChecklistCreate(ChecklistBase):
|
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
|
is_active: Optional[bool] = None
|
||||||
|
mechanic_ids: Optional[List[int]] = None # IDs de mecánicos autorizados
|
||||||
|
|
||||||
class Checklist(ChecklistBase):
|
class Checklist(ChecklistBase):
|
||||||
id: int
|
id: int
|
||||||
@@ -81,6 +87,7 @@ class Checklist(ChecklistBase):
|
|||||||
is_active: bool
|
is_active: bool
|
||||||
created_by: int
|
created_by: int
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
|
allowed_mechanics: Optional[List[int]] = [] # IDs de mecánicos permitidos
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
from_attributes = True
|
from_attributes = True
|
||||||
@@ -97,6 +104,10 @@ class QuestionBase(BaseModel):
|
|||||||
allow_photos: bool = True
|
allow_photos: bool = True
|
||||||
max_photos: int = 3
|
max_photos: int = 3
|
||||||
requires_comment_on_fail: bool = False
|
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):
|
class QuestionCreate(QuestionBase):
|
||||||
checklist_id: int
|
checklist_id: int
|
||||||
@@ -113,6 +124,7 @@ class Question(QuestionBase):
|
|||||||
from_attributes = True
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Inspection Schemas
|
# Inspection Schemas
|
||||||
class InspectionBase(BaseModel):
|
class InspectionBase(BaseModel):
|
||||||
or_number: Optional[str] = None
|
or_number: Optional[str] = None
|
||||||
@@ -212,7 +224,7 @@ class InspectionDetail(Inspection):
|
|||||||
class AIConfigurationBase(BaseModel):
|
class AIConfigurationBase(BaseModel):
|
||||||
provider: str # openai, gemini
|
provider: str # openai, gemini
|
||||||
api_key: str
|
api_key: str
|
||||||
model_name: str
|
model_name: Optional[str] = None
|
||||||
|
|
||||||
class AIConfigurationCreate(AIConfigurationBase):
|
class AIConfigurationCreate(AIConfigurationBase):
|
||||||
pass
|
pass
|
||||||
@@ -236,3 +248,72 @@ class AIModelInfo(BaseModel):
|
|||||||
name: str
|
name: str
|
||||||
provider: str
|
provider: str
|
||||||
description: Optional[str] = None
|
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
25
backend/docker.ps1
Normal 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
|
||||||
32
backend/migrate_ai_prompt.py
Normal file
32
backend/migrate_ai_prompt.py
Normal 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)
|
||||||
45
backend/migrate_conditional_questions.py
Normal file
45
backend/migrate_conditional_questions.py
Normal 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()
|
||||||
@@ -15,3 +15,5 @@ google-generativeai==0.3.2
|
|||||||
Pillow==10.2.0
|
Pillow==10.2.0
|
||||||
reportlab==4.0.9
|
reportlab==4.0.9
|
||||||
python-dotenv==1.0.0
|
python-dotenv==1.0.0
|
||||||
|
boto3==1.34.89
|
||||||
|
requests==2.31.0
|
||||||
52
backend/s3test.py
Normal file
52
backend/s3test.py
Normal 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
30
backend/test_minio.py
Normal 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)
|
||||||
@@ -20,7 +20,7 @@ services:
|
|||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
backend:
|
backend:
|
||||||
image: dymai/syntria-backend:latest
|
image: dymai/syntria-backend:1.0.15
|
||||||
container_name: syntria-backend-prod
|
container_name: syntria-backend-prod
|
||||||
restart: always
|
restart: always
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -38,7 +38,7 @@ services:
|
|||||||
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4
|
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: dymai/syntria-frontend:latest
|
image: dymai/syntria-frontend:1.0.24
|
||||||
container_name: syntria-frontend-prod
|
container_name: syntria-frontend-prod
|
||||||
restart: always
|
restart: always
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
27
frontend/buildFront.ps1
Normal file
27
frontend/buildFront.ps1
Normal 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
|
||||||
@@ -4,8 +4,8 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Syntria - Sistema Inteligente de Inspecciones</title>
|
<title>AYUTEC - Sistema Inteligente de Inspecciones</title>
|
||||||
<meta name="description" content="Syntria: Sistema avanzado de inspecciones vehiculares con inteligencia artificial" />
|
<meta name="description" content="AYUTEC: Sistema avanzado de inspecciones vehiculares con inteligencia artificial" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
1813
frontend/src/App.jsx
1813
frontend/src/App.jsx
File diff suppressed because it is too large
Load Diff
@@ -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">
|
<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>
|
<span className="text-white font-bold text-lg">S</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
@@ -67,6 +67,9 @@ export default function Sidebar({ user, activeTab, setActiveTab, sidebarOpen, se
|
|||||||
{sidebarOpen && <span>Usuarios</span>}
|
{sidebarOpen && <span>Usuarios</span>}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{(user.role === 'admin' || user.role === 'asesor') && (
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab('reports')}
|
onClick={() => setActiveTab('reports')}
|
||||||
@@ -81,6 +84,9 @@ export default function Sidebar({ user, activeTab, setActiveTab, sidebarOpen, se
|
|||||||
{sidebarOpen && <span>Reportes</span>}
|
{sidebarOpen && <span>Reportes</span>}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
)}
|
||||||
|
{user.role === 'admin' && (
|
||||||
|
<>
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab('api-tokens')}
|
onClick={() => setActiveTab('api-tokens')}
|
||||||
@@ -123,7 +129,9 @@ export default function Sidebar({ user, activeTab, setActiveTab, sidebarOpen, se
|
|||||||
{sidebarOpen && (
|
{sidebarOpen && (
|
||||||
<div className="flex-1 min-w-0">
|
<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-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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
26
gitUpdate.ps1
Normal file
26
gitUpdate.ps1
Normal 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
|
||||||
26
migrations/add_checklist_permissions.sql
Normal file
26
migrations/add_checklist_permissions.sql
Normal 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;
|
||||||
39
migrations/add_inspection_audit_log.sql
Normal file
39
migrations/add_inspection_audit_log.sql
Normal 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
|
||||||
Reference in New Issue
Block a user