diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 3329590..e00bf26 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -1,5 +1,4 @@ from pydantic_settings import BaseSettings -from typing import Optional class Settings(BaseSettings): # Database @@ -11,7 +10,7 @@ class Settings(BaseSettings): ACCESS_TOKEN_EXPIRE_MINUTES: int = 10080 # 7 días # OpenAI - OPENAI_API_KEY: Optional[str] = None + OPENAI_API_KEY: str | None = None # Environment ENVIRONMENT: str = "development" diff --git a/backend/app/main.py b/backend/app/main.py index c89aeea..64d240b 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -96,6 +96,230 @@ def get_me(current_user: models.User = Depends(get_current_user)): return current_user +# ============= USER ENDPOINTS ============= +@app.get("/api/users", response_model=List[schemas.User]) +def get_users( + skip: int = 0, + limit: int = 100, + active_only: bool = True, + db: Session = Depends(get_db), + current_user: models.User = Depends(get_current_user) +): + # Solo admin puede ver todos los usuarios + if current_user.role != "admin": + raise HTTPException(status_code=403, detail="No tienes permisos para ver usuarios") + + query = db.query(models.User) + if active_only: + query = query.filter(models.User.is_active == True) + + return query.offset(skip).limit(limit).all() + + +@app.get("/api/users/{user_id}", response_model=schemas.User) +def get_user( + user_id: int, + db: Session = Depends(get_db), + current_user: models.User = Depends(get_current_user) +): + # Solo admin puede ver otros usuarios + if current_user.role != "admin" and current_user.id != user_id: + raise HTTPException(status_code=403, detail="No tienes permisos para ver este usuario") + + user = db.query(models.User).filter(models.User.id == user_id).first() + if not user: + raise HTTPException(status_code=404, detail="Usuario no encontrado") + + return user + + +@app.post("/api/users", response_model=schemas.User) +def create_user( + user: schemas.UserCreate, + db: Session = Depends(get_db), + current_user: models.User = Depends(get_current_user) +): + # Solo admin puede crear usuarios + if current_user.role != "admin": + raise HTTPException(status_code=403, detail="No tienes permisos para crear usuarios") + + # Verificar si usuario existe + db_user = db.query(models.User).filter(models.User.username == user.username).first() + if db_user: + raise HTTPException(status_code=400, detail="Usuario ya existe") + + # Verificar si email existe + if user.email: + db_email = db.query(models.User).filter(models.User.email == user.email).first() + if db_email: + raise HTTPException(status_code=400, detail="Email ya está en uso") + + # Crear usuario + hashed_password = get_password_hash(user.password) + db_user = models.User( + username=user.username, + email=user.email, + full_name=user.full_name, + role=user.role, + password_hash=hashed_password, + is_active=True + ) + db.add(db_user) + db.commit() + db.refresh(db_user) + return db_user + + +@app.put("/api/users/{user_id}", response_model=schemas.User) +def update_user( + user_id: int, + user_update: schemas.UserUpdate, + db: Session = Depends(get_db), + current_user: models.User = Depends(get_current_user) +): + # Solo admin puede actualizar otros usuarios + if current_user.role != "admin" and current_user.id != user_id: + raise HTTPException(status_code=403, detail="No tienes permisos para actualizar este usuario") + + db_user = db.query(models.User).filter(models.User.id == user_id).first() + if not db_user: + raise HTTPException(status_code=404, detail="Usuario no encontrado") + + # Actualizar campos + if user_update.email is not None: + # Verificar si email está en uso + existing = db.query(models.User).filter( + models.User.email == user_update.email, + models.User.id != user_id + ).first() + if existing: + raise HTTPException(status_code=400, detail="Email ya está en uso") + db_user.email = user_update.email + + if user_update.full_name is not None: + db_user.full_name = user_update.full_name + + # Solo admin puede cambiar roles + if user_update.role is not None: + if current_user.role != "admin": + raise HTTPException(status_code=403, detail="No tienes permisos para cambiar roles") + db_user.role = user_update.role + + db.commit() + db.refresh(db_user) + return db_user + + +@app.patch("/api/users/{user_id}/deactivate") +def deactivate_user( + user_id: int, + db: Session = Depends(get_db), + current_user: models.User = Depends(get_current_user) +): + # Solo admin puede inactivar usuarios + if current_user.role != "admin": + raise HTTPException(status_code=403, detail="No tienes permisos para inactivar usuarios") + + # No permitir auto-inactivación + if current_user.id == user_id: + raise HTTPException(status_code=400, detail="No puedes inactivar tu propio usuario") + + db_user = db.query(models.User).filter(models.User.id == user_id).first() + if not db_user: + raise HTTPException(status_code=404, detail="Usuario no encontrado") + + db_user.is_active = False + db.commit() + + return {"message": "Usuario inactivado correctamente", "user_id": user_id} + + +@app.patch("/api/users/{user_id}/activate") +def activate_user( + user_id: int, + db: Session = Depends(get_db), + current_user: models.User = Depends(get_current_user) +): + # Solo admin puede activar usuarios + if current_user.role != "admin": + raise HTTPException(status_code=403, detail="No tienes permisos para activar usuarios") + + db_user = db.query(models.User).filter(models.User.id == user_id).first() + if not db_user: + raise HTTPException(status_code=404, detail="Usuario no encontrado") + + db_user.is_active = True + db.commit() + + return {"message": "Usuario activado correctamente", "user_id": user_id} + + +@app.patch("/api/users/{user_id}/password") +def change_user_password( + user_id: int, + password_update: schemas.AdminPasswordUpdate, + db: Session = Depends(get_db), + current_user: models.User = Depends(get_current_user) +): + # Solo admin puede cambiar contraseñas de otros usuarios + if current_user.role != "admin": + raise HTTPException(status_code=403, detail="No tienes permisos para cambiar contraseñas") + + db_user = db.query(models.User).filter(models.User.id == user_id).first() + if not db_user: + raise HTTPException(status_code=404, detail="Usuario no encontrado") + + # Cambiar contraseña + db_user.password_hash = get_password_hash(password_update.new_password) + db.commit() + + return {"message": "Contraseña actualizada correctamente"} + + +@app.patch("/api/users/me/password") +def change_my_password( + password_update: schemas.UserPasswordUpdate, + db: Session = Depends(get_db), + current_user: models.User = Depends(get_current_user) +): + # Verificar contraseña actual + if not verify_password(password_update.current_password, current_user.password_hash): + raise HTTPException(status_code=400, detail="Contraseña actual incorrecta") + + # Cambiar contraseña + current_user.password_hash = get_password_hash(password_update.new_password) + db.commit() + + return {"message": "Contraseña actualizada correctamente"} + + +@app.put("/api/users/me", response_model=schemas.User) +def update_my_profile( + user_update: schemas.UserUpdate, + db: Session = Depends(get_db), + current_user: models.User = Depends(get_current_user) +): + # Actualizar email + if user_update.email is not None: + # Verificar si email está en uso + existing = db.query(models.User).filter( + models.User.email == user_update.email, + models.User.id != current_user.id + ).first() + if existing: + raise HTTPException(status_code=400, detail="Email ya está en uso") + current_user.email = user_update.email + + # Actualizar nombre + if user_update.full_name is not None: + current_user.full_name = user_update.full_name + + # No permitir cambio de rol desde perfil + db.commit() + db.refresh(current_user) + return current_user + + # ============= CHECKLIST ENDPOINTS ============= @app.get("/api/checklists", response_model=List[schemas.Checklist]) def get_checklists( @@ -233,11 +457,16 @@ def get_inspections( limit: int = 100, vehicle_plate: str = None, status: str = None, + show_inactive: bool = False, db: Session = Depends(get_db), current_user: models.User = Depends(get_current_user) ): query = db.query(models.Inspection) + # Por defecto, solo mostrar inspecciones activas + if not show_inactive: + query = query.filter(models.Inspection.is_active == True) + # Mecánicos solo ven sus inspecciones if current_user.role == "mechanic": query = query.filter(models.Inspection.mechanic_id == current_user.id) @@ -346,6 +575,32 @@ def complete_inspection( return inspection +@app.patch("/api/inspections/{inspection_id}/deactivate") +def deactivate_inspection( + inspection_id: int, + db: Session = Depends(get_db), + current_user: models.User = Depends(get_current_user) +): + # Solo admin puede inactivar + if current_user.role != "admin": + raise HTTPException(status_code=403, detail="No tienes permisos para inactivar inspecciones") + + inspection = db.query(models.Inspection).filter( + models.Inspection.id == inspection_id + ).first() + + if not inspection: + raise HTTPException(status_code=404, detail="Inspección no encontrada") + + inspection.is_active = False + inspection.status = "inactive" + + db.commit() + db.refresh(inspection) + + return {"message": "Inspección inactivada correctamente", "inspection_id": inspection_id} + + # ============= ANSWER ENDPOINTS ============= @app.post("/api/answers", response_model=schemas.Answer) def create_answer( diff --git a/backend/app/models.py b/backend/app/models.py index f51452c..080b6ff 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -87,7 +87,8 @@ class Inspection(Base): flagged_items_count = Column(Integer, default=0) # Estado - status = Column(String(20), default="draft") # draft, completed + status = Column(String(20), default="draft") # draft, completed, inactive + is_active = Column(Boolean, default=True) # Firma signature_data = Column(Text) # Base64 de la firma diff --git a/backend/app/schemas.py b/backend/app/schemas.py index d9be0e0..ef6d827 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -12,6 +12,18 @@ class UserBase(BaseModel): class UserCreate(UserBase): password: str +class UserUpdate(BaseModel): + email: Optional[EmailStr] = None + full_name: Optional[str] = None + role: Optional[str] = None + +class UserPasswordUpdate(BaseModel): + current_password: str + new_password: str + +class AdminPasswordUpdate(BaseModel): + new_password: str + class UserLogin(BaseModel): username: str password: str diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 0ac2732..82561de 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -331,7 +331,7 @@ function DashboardPage({ user, setUser }) { onStartInspection={setActiveInspection} /> ) : activeTab === 'inspections' ? ( - + ) : activeTab === 'settings' ? ( ) : activeTab === 'users' ? ( @@ -996,17 +996,25 @@ function ChecklistsTab({ checklists, user, onChecklistCreated, onStartInspection
{user.role === 'admin' && ( - + <> + + + )} - {user.role === 'mechanic' && ( + {(user.role === 'mechanic' || user.role === 'mecanico') && ( +
+ {user?.role === 'admin' && ( + <> + + + )} + +
) } -function InspectionsTab({ inspections, user }) { +function InspectionsTab({ inspections, user, onUpdate }) { const [selectedInspection, setSelectedInspection] = useState(null) if (inspections.length === 0) { @@ -1494,7 +1539,9 @@ function InspectionsTab({ inspections, user }) { {selectedInspection && ( setSelectedInspection(null)} + onUpdate={onUpdate} /> )}