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 */} +
+ +
+ {mechanics.length === 0 ? ( +

No hay mecánicos disponibles

+ ) : ( +
+
+ { + if (e.target.checked) { + setFormData({ ...formData, mechanic_ids: [] }) + } + }} + className="w-4 h-4 text-green-600 border-gray-300 rounded focus:ring-green-500" + /> + +
+ {mechanics.map((mechanic) => ( +
+ { + if (e.target.checked) { + setFormData({ + ...formData, + mechanic_ids: [...formData.mechanic_ids, mechanic.id] + }) + } else { + setFormData({ + ...formData, + mechanic_ids: formData.mechanic_ids.filter(id => id !== mechanic.id) + }) + } + }} + className="w-4 h-4 text-indigo-600 border-gray-300 rounded focus:ring-indigo-500" + /> + +
+ ))} +
+ )} +
+

+ 💡 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;