From eb94d8ccfc441eb1440796c4efdc8ef81def319a Mon Sep 17 00:00:00 2001
From: ronalds
Date: Tue, 25 Nov 2025 09:22:38 -0300
Subject: [PATCH] backend crear endpoitns para permisos de checklist por
mecanico, 1.0.30
---
backend/app/main.py | 90 +++++++++++++++-
backend/app/models.py | 15 +++
backend/app/schemas.py | 4 +-
backend/main2.py | 122 ----------------------
frontend/index.html | 4 +-
frontend/src/App.jsx | 127 +++++++++++++++++++++--
migrations/add_checklist_permissions.sql | 26 +++++
7 files changed, 253 insertions(+), 135 deletions(-)
delete mode 100644 backend/main2.py
create mode 100644 migrations/add_checklist_permissions.sql
diff --git a/backend/app/main.py b/backend/app/main.py
index 6a7866b..d4bb0fc 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -617,7 +617,40 @@ def get_checklists(
query = db.query(models.Checklist)
if active_only:
query = query.filter(models.Checklist.is_active == True)
- return query.offset(skip).limit(limit).all()
+
+ # Si es mecánico, solo ver checklists con permiso
+ if current_user.role == "mechanic":
+ # Obtener IDs de checklists con permiso o sin permisos (acceso global)
+ permitted_checklist_ids = db.query(models.ChecklistPermission.checklist_id).filter(
+ models.ChecklistPermission.mechanic_id == current_user.id
+ ).distinct().all()
+ permitted_ids = [id[0] for id in permitted_checklist_ids]
+
+ # Checklists sin permisos = acceso global
+ checklists_without_permissions = db.query(models.Checklist.id).outerjoin(
+ models.ChecklistPermission
+ ).group_by(models.Checklist.id).having(
+ func.count(models.ChecklistPermission.id) == 0
+ ).all()
+ global_ids = [id[0] for id in checklists_without_permissions]
+
+ all_allowed_ids = list(set(permitted_ids + global_ids))
+ if all_allowed_ids:
+ query = query.filter(models.Checklist.id.in_(all_allowed_ids))
+ else:
+ # Si no hay permisos, devolver lista vacía
+ return []
+
+ checklists = query.offset(skip).limit(limit).all()
+
+ # Agregar allowed_mechanics a cada checklist
+ for checklist in checklists:
+ permissions = db.query(models.ChecklistPermission.mechanic_id).filter(
+ models.ChecklistPermission.checklist_id == checklist.id
+ ).all()
+ checklist.allowed_mechanics = [p[0] for p in permissions]
+
+ return checklists
@app.get("/api/checklists/{checklist_id}", response_model=schemas.ChecklistWithQuestions)
@@ -629,6 +662,12 @@ def get_checklist(checklist_id: int, db: Session = Depends(get_db)):
if not checklist:
raise HTTPException(status_code=404, detail="Checklist no encontrado")
+ # Agregar allowed_mechanics
+ permissions = db.query(models.ChecklistPermission.mechanic_id).filter(
+ models.ChecklistPermission.checklist_id == checklist.id
+ ).all()
+ checklist.allowed_mechanics = [p[0] for p in permissions]
+
return checklist
@@ -641,10 +680,28 @@ def create_checklist(
if current_user.role != "admin":
raise HTTPException(status_code=403, detail="No autorizado")
- db_checklist = models.Checklist(**checklist.dict(), created_by=current_user.id)
+ # Extraer mechanic_ids antes de crear el checklist
+ checklist_data = checklist.dict(exclude={'mechanic_ids'})
+ mechanic_ids = checklist.mechanic_ids or []
+
+ db_checklist = models.Checklist(**checklist_data, created_by=current_user.id)
db.add(db_checklist)
+ db.flush() # Para obtener el ID
+
+ # Crear permisos para mecánicos seleccionados
+ for mechanic_id in mechanic_ids:
+ permission = models.ChecklistPermission(
+ checklist_id=db_checklist.id,
+ mechanic_id=mechanic_id
+ )
+ db.add(permission)
+
db.commit()
db.refresh(db_checklist)
+
+ # Agregar allowed_mechanics a la respuesta
+ db_checklist.allowed_mechanics = mechanic_ids
+
return db_checklist
@@ -662,11 +719,38 @@ def update_checklist(
if not db_checklist:
raise HTTPException(status_code=404, detail="Checklist no encontrado")
- for key, value in checklist.dict(exclude_unset=True).items():
+ # Extraer mechanic_ids si se envía
+ update_data = checklist.dict(exclude_unset=True, exclude={'mechanic_ids'})
+ mechanic_ids = checklist.mechanic_ids
+
+ # Actualizar campos del checklist
+ for key, value in update_data.items():
setattr(db_checklist, key, value)
+ # Si se proporcionan mechanic_ids, actualizar permisos
+ if mechanic_ids is not None:
+ # Eliminar permisos existentes
+ db.query(models.ChecklistPermission).filter(
+ models.ChecklistPermission.checklist_id == checklist_id
+ ).delete()
+
+ # Crear nuevos permisos
+ for mechanic_id in mechanic_ids:
+ permission = models.ChecklistPermission(
+ checklist_id=checklist_id,
+ mechanic_id=mechanic_id
+ )
+ db.add(permission)
+
db.commit()
db.refresh(db_checklist)
+
+ # Agregar allowed_mechanics a la respuesta
+ permissions = db.query(models.ChecklistPermission.mechanic_id).filter(
+ models.ChecklistPermission.checklist_id == checklist_id
+ ).all()
+ db_checklist.allowed_mechanics = [p[0] for p in permissions]
+
return db_checklist
diff --git a/backend/app/models.py b/backend/app/models.py
index ad81356..36fe47e 100644
--- a/backend/app/models.py
+++ b/backend/app/models.py
@@ -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):
@@ -186,3 +187,17 @@ class AIConfiguration(Base):
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")
diff --git a/backend/app/schemas.py b/backend/app/schemas.py
index 9fca46d..6ebc0b5 100644
--- a/backend/app/schemas.py
+++ b/backend/app/schemas.py
@@ -70,10 +70,11 @@ 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):
is_active: Optional[bool] = None
+ mechanic_ids: Optional[List[int]] = None # IDs de mecánicos autorizados
class Checklist(ChecklistBase):
id: int
@@ -81,6 +82,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
diff --git a/backend/main2.py b/backend/main2.py
deleted file mode 100644
index 9b0b5ec..0000000
--- a/backend/main2.py
+++ /dev/null
@@ -1,122 +0,0 @@
-from fastapi import FastAPI, File, UploadFile, Form, Depends, HTTPException, status
-from fastapi.middleware.cors import CORSMiddleware
-from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
-from sqlalchemy.orm import Session, joinedload
-from sqlalchemy import func, case
-from typing import List, Optional
-import os
-import boto3
-from botocore.client import Config
-import uuid
-from app.core import config as app_config
-from app.core.database import engine, get_db, Base
-from app.core.security import verify_password, get_password_hash, create_access_token, decode_access_token
-from app import models, schemas
-import shutil
-from datetime import datetime, timedelta
-
-BACKEND_VERSION = "1.0.25"
-app = FastAPI(title="Checklist Inteligente API", version=BACKEND_VERSION)
-
-# S3/MinIO configuration
-S3_ENDPOINT = app_config.MINIO_ENDPOINT
-S3_ACCESS_KEY = app_config.MINIO_ACCESS_KEY
-S3_SECRET_KEY = app_config.MINIO_SECRET_KEY
-S3_IMAGE_BUCKET = app_config.MINIO_IMAGE_BUCKET
-S3_PDF_BUCKET = app_config.MINIO_PDF_BUCKET
-
-s3_client = boto3.client(
- 's3',
- endpoint_url=S3_ENDPOINT,
- aws_access_key_id=S3_ACCESS_KEY,
- aws_secret_access_key=S3_SECRET_KEY,
- config=Config(signature_version='s3v4'),
- region_name='us-east-1'
-)
-
-# Crear tablas
-Base.metadata.create_all(bind=engine)
-
-# Información visual al iniciar el backend
-print("\n================ BACKEND STARTUP INFO ================")
-print(f"Backend version: {BACKEND_VERSION}")
-print(f"Database URL: {app_config.settings.DATABASE_URL}")
-print(f"Environment: {app_config.settings.ENVIRONMENT}")
-print(f"MinIO endpoint: {app_config.MINIO_ENDPOINT}")
-print("====================================================\n", flush=True)
-
-# CORS
-app.add_middleware(
- CORSMiddleware,
- allow_origins=["http://localhost:5173", "http://localhost:3000"],
- allow_credentials=True,
- allow_methods=["*"],
- allow_headers=["*"],
-)
-
-# Simulación de modelos y autenticación para ejemplo
-class User:
- def __init__(self, role):
- self.role = role
-
-class AIConfiguration:
- is_active = True
- logo_url = ""
-
-class models:
- User = User
- AIConfiguration = AIConfiguration
-
-# Simulación de get_db y get_current_user
-
-def get_db():
- # Aquí iría la lógica real de SQLAlchemy
- class DummyDB:
- def query(self, model):
- return self
- def filter(self, *args, **kwargs):
- return self
- def first(self):
- return models.AIConfiguration()
- def commit(self):
- pass
- def refresh(self, obj):
- pass
- return DummyDB()
-
-def get_current_user():
- # Aquí iría la lógica real de autenticación
- return models.User(role="admin")
-
-# Endpoint para subir el logo
-@app.post("/api/config/logo", response_model=dict)
-async def upload_logo(
- file: UploadFile = File(...),
- db: Session = Depends(get_db),
- current_user: models.User = Depends(get_current_user)
-):
- if current_user.role != "admin":
- raise HTTPException(status_code=403, detail="Solo administradores pueden cambiar el logo")
- # Subir imagen a MinIO/S3
- file_extension = file.filename.split(".")[-1]
- now = datetime.now()
- folder = "logo"
- file_name = f"logo_{now.strftime('%Y%m%d_%H%M%S')}.{file_extension}"
- s3_key = f"{folder}/{file_name}"
- # s3_client.upload_fileobj(file.file, S3_IMAGE_BUCKET, s3_key, ExtraArgs={"ContentType": file.content_type})
- logo_url = f"https://minio.example.com/bucket/{s3_key}" # Ajusta según tu config
- # Actualiza la configuración en la base de datos
- # config = db.query(models.AIConfiguration).filter(models.AIConfiguration.is_active == True).first()
- # if config:
- # config.logo_url = logo_url
- # db.commit()
- return {"logo_url": logo_url}
-
-# Endpoint para obtener el logo
-@app.get("/api/config/logo", response_model=dict)
-def get_logo_url(db: Session = Depends(get_db)):
- # config = db.query(models.AIConfiguration).filter(models.AIConfiguration.is_active == True).first()
- # if config and getattr(config, "logo_url", None):
- # return {"logo_url": config.logo_url}
- # return {"logo_url": "https://minio.example.com/bucket/logo/default_logo.png"}
- return {"logo_url": "https://minio.example.com/bucket/logo/default_logo.png"}
\ No newline at end of file
diff --git a/frontend/index.html b/frontend/index.html
index 99a1d1b..9557828 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -4,8 +4,8 @@
- Syntria - Sistema Inteligente de Inspecciones
-
+ AYUTEC - Sistema Inteligente de Inspecciones
+
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 6b2a026..46b94cf 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -106,7 +106,7 @@ function LoginPage({ setUser }) {
Sin logo
)}
- Syntria
+ AYUTEC
Sistema Inteligente de Inspecciones
@@ -296,7 +296,7 @@ function DashboardPage({ user, setUser }) {
Sin logo
)}
-
Syntria
+
AYUTEC
Sistema Inteligente de Inspecciones
@@ -887,7 +887,7 @@ function APITokensTab({ user }) {
Incluye el token en el header Authorization de tus requests:
- Authorization: Bearer syntria_tu_token_aqui
+ Authorization: Bearer AYUTEC_tu_token_aqui
@@ -1346,13 +1346,39 @@ function ChecklistsTab({ checklists, user, onChecklistCreated, onStartInspection
const [showQuestionsModal, setShowQuestionsModal] = useState(false)
const [selectedChecklist, setSelectedChecklist] = useState(null)
const [creating, setCreating] = useState(false)
+ const [mechanics, setMechanics] = useState([])
const [formData, setFormData] = useState({
name: '',
description: '',
ai_mode: 'off',
- scoring_enabled: true
+ scoring_enabled: true,
+ mechanic_ids: []
})
+ useEffect(() => {
+ loadMechanics()
+ }, [])
+
+ const loadMechanics = async () => {
+ try {
+ const token = localStorage.getItem('token')
+ const API_URL = import.meta.env.VITE_API_URL || ''
+ const response = await fetch(`${API_URL}/api/users`, {
+ headers: { 'Authorization': `Bearer ${token}` }
+ })
+ if (response.ok) {
+ const data = await response.json()
+ // Filtrar solo mecánicos activos
+ const mechanicUsers = data.filter(u =>
+ (u.role === 'mechanic' || u.role === 'mecanico') && u.is_active
+ )
+ setMechanics(mechanicUsers)
+ }
+ } catch (error) {
+ console.error('Error loading mechanics:', error)
+ }
+ }
+
const handleCreate = async (e) => {
e.preventDefault()
setCreating(true)
@@ -1372,7 +1398,13 @@ function ChecklistsTab({ checklists, user, onChecklistCreated, onStartInspection
if (response.ok) {
setShowCreateModal(false)
- setFormData({ name: '', description: '', ai_mode: 'off', scoring_enabled: true })
+ setFormData({
+ name: '',
+ description: '',
+ ai_mode: 'off',
+ scoring_enabled: true,
+ mechanic_ids: []
+ })
onChecklistCreated()
} else {
alert('Error al crear checklist')
@@ -1414,7 +1446,7 @@ function ChecklistsTab({ checklists, user, onChecklistCreated, onStartInspection
checklists.map((checklist) => (
-
+
{checklist.name}
{checklist.description}
@@ -1425,6 +1457,22 @@ function ChecklistsTab({ checklists, user, onChecklistCreated, onStartInspection
Modo IA: {checklist.ai_mode}
+ {/* Mostrar permisos de mecánicos */}
+ {user.role === 'admin' && (
+
+ {!checklist.allowed_mechanics || checklist.allowed_mechanics.length === 0 ? (
+
+ 🌍 Acceso Global - Todos los mecánicos
+
+ ) : (
+
+
+ 🔐 Restringido - {checklist.allowed_mechanics.length} mecánico{checklist.allowed_mechanics.length !== 1 ? 's' : ''}
+
+
+ )}
+
+ )}
{user.role === 'admin' && (
@@ -1558,6 +1606,65 @@ function ChecklistsTab({ checklists, user, onChecklistCreated, onStartInspection
+ {/* Selector de Mecánicos Autorizados */}
+
+
+ 🔐 Mecánicos Autorizados
+
+
+ {mechanics.length === 0 ? (
+
No hay mecánicos disponibles
+ ) : (
+
+ )}
+
+
+ 💡 Si no seleccionas ningún mecánico, todos podrán usar este checklist.
+ Si seleccionas mecánicos específicos, solo ellos tendrán acceso.
+
+
+
ℹ️ Después de crear el checklist, podrás agregar preguntas desde la API o directamente en la base de datos.
@@ -1569,7 +1676,13 @@ function ChecklistsTab({ checklists, user, onChecklistCreated, onStartInspection
type="button"
onClick={() => {
setShowCreateModal(false)
- setFormData({ name: '', description: '', ai_mode: 'off', scoring_enabled: true })
+ setFormData({
+ name: '',
+ description: '',
+ ai_mode: 'off',
+ scoring_enabled: true,
+ mechanic_ids: []
+ })
}}
className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition"
disabled={creating}
diff --git a/migrations/add_checklist_permissions.sql b/migrations/add_checklist_permissions.sql
new file mode 100644
index 0000000..82f0622
--- /dev/null
+++ b/migrations/add_checklist_permissions.sql
@@ -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;