backend crear endpoitns para permisos de checklist por mecanico, 1.0.30

This commit is contained in:
2025-11-25 09:22:38 -03:00
parent ad59152cce
commit eb94d8ccfc
7 changed files with 253 additions and 135 deletions

View File

@@ -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

View File

@@ -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")

View File

@@ -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

View File

@@ -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"}

View File

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

View File

@@ -106,7 +106,7 @@ function LoginPage({ setUser }) {
<div className="w-24 h-24 bg-white rounded-2xl flex items-center justify-center shadow-lg text-gray-400">Sin logo</div>
)}
</div>
<h1 className="text-4xl font-bold text-white mb-2">Syntria</h1>
<h1 className="text-4xl font-bold text-white mb-2">AYUTEC</h1>
<p className="text-indigo-100 text-sm">Sistema Inteligente de Inspecciones</p>
</div>
@@ -296,7 +296,7 @@ function DashboardPage({ user, setUser }) {
<div className="w-12 h-12 bg-white rounded-xl flex items-center justify-center shadow-lg text-gray-400">Sin logo</div>
)}
<div>
<h1 className="text-2xl font-bold text-white">Syntria</h1>
<h1 className="text-2xl font-bold text-white">AYUTEC</h1>
<p className="text-xs text-indigo-200">Sistema Inteligente de Inspecciones</p>
</div>
</div>
@@ -887,7 +887,7 @@ function APITokensTab({ user }) {
Incluye el token en el header <code className="bg-blue-100 px-1 py-0.5 rounded">Authorization</code> de tus requests:
</p>
<code className="block bg-blue-100 p-2 rounded text-xs mt-2">
Authorization: Bearer syntria_tu_token_aqui
Authorization: Bearer AYUTEC_tu_token_aqui
</code>
</div>
</div>
@@ -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) => (
<div key={checklist.id} className="border border-gray-200 rounded-lg p-4 hover:shadow-md transition">
<div className="flex justify-between items-start">
<div>
<div className="flex-1">
<h3 className="text-lg font-semibold text-gray-900">{checklist.name}</h3>
<p className="text-sm text-gray-600 mt-1">{checklist.description}</p>
<div className="flex gap-4 mt-3 text-sm">
@@ -1425,6 +1457,22 @@ function ChecklistsTab({ checklists, user, onChecklistCreated, onStartInspection
Modo IA: <strong className="capitalize">{checklist.ai_mode}</strong>
</span>
</div>
{/* Mostrar permisos de mecánicos */}
{user.role === 'admin' && (
<div className="mt-2">
{!checklist.allowed_mechanics || checklist.allowed_mechanics.length === 0 ? (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
🌍 Acceso Global - Todos los mecánicos
</span>
) : (
<div className="flex items-center gap-2 flex-wrap">
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-indigo-100 text-indigo-800">
🔐 Restringido - {checklist.allowed_mechanics.length} mecánico{checklist.allowed_mechanics.length !== 1 ? 's' : ''}
</span>
</div>
)}
</div>
)}
</div>
<div className="flex gap-2">
{user.role === 'admin' && (
@@ -1558,6 +1606,65 @@ function ChecklistsTab({ checklists, user, onChecklistCreated, onStartInspection
</label>
</div>
{/* Selector de Mecánicos Autorizados */}
<div className="border-t pt-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
🔐 Mecánicos Autorizados
</label>
<div className="bg-gray-50 border border-gray-300 rounded-lg p-3 max-h-48 overflow-y-auto">
{mechanics.length === 0 ? (
<p className="text-sm text-gray-500">No hay mecánicos disponibles</p>
) : (
<div className="space-y-2">
<div className="flex items-center pb-2 border-b">
<input
type="checkbox"
checked={formData.mechanic_ids.length === 0}
onChange={(e) => {
if (e.target.checked) {
setFormData({ ...formData, mechanic_ids: [] })
}
}}
className="w-4 h-4 text-green-600 border-gray-300 rounded focus:ring-green-500"
/>
<label className="ml-2 text-sm font-semibold text-green-700">
🌍 Todos los mecánicos (acceso global)
</label>
</div>
{mechanics.map((mechanic) => (
<div key={mechanic.id} className="flex items-center">
<input
type="checkbox"
checked={formData.mechanic_ids.includes(mechanic.id)}
onChange={(e) => {
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"
/>
<label className="ml-2 text-sm text-gray-700">
{mechanic.full_name || mechanic.username} ({mechanic.email})
</label>
</div>
))}
</div>
)}
</div>
<p className="mt-2 text-xs text-gray-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.
</p>
</div>
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mt-4">
<p className="text-sm text-yellow-800">
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}

View File

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