""" Tickets endpoints - ServiceManagerWeb """ from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File from pydantic import BaseModel from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func from typing import List, Optional from datetime import datetime from app.core.database import get_db from app.api.deps import get_current_user from app.models.ticket import Ticket, TicketStatus, TicketPriority from app.models.user import User from app.models.category import Category # ✅ CORREGIDO: Era TicketCategory from app.models.system import System from app.models.comment import TicketComment import uuid router = APIRouter() # =================================== # SCHEMAS # =================================== class TicketCreate(BaseModel): subject: str description: str category_id: Optional[str] = None affected_system_id: Optional[str] = None # ✅ CORREGIDO: Era system_id priority: str = "MEDIUM" class TicketUpdate(BaseModel): subject: Optional[str] = None description: Optional[str] = None status: Optional[str] = None priority: Optional[str] = None assigned_to: Optional[str] = None class TicketResponse(BaseModel): id: str ticket_number: str subject: str title: str description: str status: str priority: str category_id: Optional[str] = None affected_system_id: Optional[str] = None # ✅ CORREGIDO: Era system_id created_by: str assigned_to: Optional[str] = None created_at: datetime updated_at: datetime class Config: from_attributes = True class TicketCloseRequest(BaseModel): resolution: Optional[str] = None # =================================== # TICKET ENDPOINTS # =================================== @router.post("/", response_model=TicketResponse, status_code=status.HTTP_201_CREATED) async def create_ticket( ticket: TicketCreate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """ Crear un nuevo ticket """ try: # Generar número de ticket único result = await db.execute( select(func.count(Ticket.id)).where(Ticket.tenant_id == current_user.tenant_id) ) count = result.scalar() or 0 ticket_number = f"TK-{count + 1:06d}" # Convertir IDs de string a UUID si son proporcionados category_uuid = uuid.UUID(ticket.category_id) if ticket.category_id else None system_uuid = uuid.UUID(ticket.affected_system_id) if ticket.affected_system_id else None # ✅ CORREGIDO # ✅ CORREGIDO: Validar en la tabla correcta con el nombre correcto del modelo if category_uuid: category = await db.get(Category, category_uuid) # ✅ Category, no TicketCategory if not category: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"La categoría con ID {ticket.category_id} no existe." ) # Validar si el system_id existe en la tabla affected_systems if system_uuid: system = await db.get(System, system_uuid) if not system: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"El sistema con ID {ticket.affected_system_id} no existe." ) db_ticket = Ticket( id=uuid.uuid4(), tenant_id=current_user.tenant_id, ticket_number=ticket_number, subject=ticket.subject, description=ticket.description, category_id=category_uuid, affected_system_id=system_uuid, # ✅ CORREGIDO: Nombre correcto del campo priority=TicketPriority[ticket.priority.upper()], created_by=current_user.id, status=TicketStatus.NEW, created_at=datetime.utcnow(), updated_at=datetime.utcnow() ) db.add(db_ticket) await db.commit() await db.refresh(db_ticket) # ✅ CORREGIDO: Usar affected_system_id en respuesta return { "id": str(db_ticket.id), "ticket_number": db_ticket.ticket_number, "subject": db_ticket.subject, "title": db_ticket.subject, "description": db_ticket.description, "status": db_ticket.status.value, "priority": db_ticket.priority.value, "category_id": str(db_ticket.category_id) if db_ticket.category_id else None, "affected_system_id": str(db_ticket.affected_system_id) if db_ticket.affected_system_id else None, # ✅ CORREGIDO "created_by": str(db_ticket.created_by), "assigned_to": str(db_ticket.assigned_to) if db_ticket.assigned_to else None, "created_at": db_ticket.created_at, "updated_at": db_ticket.updated_at } except ValueError as e: await db.rollback() raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Invalid UUID format: {str(e)}" ) except Exception as e: await db.rollback() raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Error creating ticket: {str(e)}" ) @router.get("/", response_model=List[TicketResponse]) async def get_tickets( skip: int = 0, limit: int = 100, status_filter: Optional[str] = None, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """ Obtener tickets del usuario actual """ query = select(Ticket).where( Ticket.tenant_id == current_user.tenant_id, Ticket.created_by == current_user.id ) if status_filter: try: status_enum = TicketStatus[status_filter.upper()] query = query.where(Ticket.status == status_enum) except KeyError: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Invalid status: {status_filter}" ) query = query.order_by(Ticket.created_at.desc()).offset(skip).limit(limit) result = await db.execute(query) tickets = result.scalars().all() # ✅ CORREGIDO: Usar affected_system_id return [ { "id": str(t.id), "ticket_number": t.ticket_number, "subject": t.subject, "title": t.subject, "description": t.description, "status": t.status.value, "priority": t.priority.value, "category_id": str(t.category_id) if t.category_id else None, "affected_system_id": str(t.affected_system_id) if t.affected_system_id else None, # ✅ CORREGIDO "created_by": str(t.created_by), "assigned_to": str(t.assigned_to) if t.assigned_to else None, "created_at": t.created_at, "updated_at": t.updated_at } for t in tickets ] @router.get("/{ticket_id}", response_model=TicketResponse) async def get_ticket( ticket_id: str, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """ Obtener un ticket específico """ try: ticket_uuid = uuid.UUID(ticket_id) except ValueError: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid ticket ID format" ) query = select(Ticket).where( Ticket.id == ticket_uuid, Ticket.tenant_id == current_user.tenant_id, Ticket.created_by == current_user.id ) result = await db.execute(query) ticket = result.scalars().first() if not ticket: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Ticket {ticket_id} not found" ) # ✅ CORREGIDO: Usar affected_system_id return { "id": str(ticket.id), "ticket_number": ticket.ticket_number, "subject": ticket.subject, "title": ticket.subject, "description": ticket.description, "status": ticket.status.value, "priority": ticket.priority.value, "category_id": str(ticket.category_id) if ticket.category_id else None, "affected_system_id": str(ticket.affected_system_id) if ticket.affected_system_id else None, # ✅ CORREGIDO "created_by": str(ticket.created_by), "assigned_to": str(ticket.assigned_to) if ticket.assigned_to else None, "created_at": ticket.created_at, "updated_at": ticket.updated_at } @router.patch("/{ticket_id}", response_model=TicketResponse) async def update_ticket( ticket_id: str, ticket_update: TicketUpdate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """ Actualizar un ticket """ try: ticket_uuid = uuid.UUID(ticket_id) except ValueError: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid ticket ID format" ) query = select(Ticket).where( Ticket.id == ticket_uuid, Ticket.tenant_id == current_user.tenant_id, Ticket.created_by == current_user.id ) result = await db.execute(query) db_ticket = result.scalars().first() if not db_ticket: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Ticket {ticket_id} not found" ) try: update_data = ticket_update.dict(exclude_unset=True) for field, value in update_data.items(): if field == "status" and value: setattr(db_ticket, field, TicketStatus[value.upper()]) elif field == "priority" and value: setattr(db_ticket, field, TicketPriority[value.upper()]) elif field == "assigned_to" and value: setattr(db_ticket, field, uuid.UUID(value)) else: setattr(db_ticket, field, value) db_ticket.updated_at = datetime.utcnow() await db.commit() await db.refresh(db_ticket) # ✅ CORREGIDO: Usar affected_system_id return { "id": str(db_ticket.id), "ticket_number": db_ticket.ticket_number, "subject": db_ticket.subject, "title": db_ticket.subject, "description": db_ticket.description, "status": db_ticket.status.value, "priority": db_ticket.priority.value, "category_id": str(db_ticket.category_id) if db_ticket.category_id else None, "affected_system_id": str(db_ticket.affected_system_id) if db_ticket.affected_system_id else None, # ✅ CORREGIDO "created_by": str(db_ticket.created_by), "assigned_to": str(db_ticket.assigned_to) if db_ticket.assigned_to else None, "created_at": db_ticket.created_at, "updated_at": db_ticket.updated_at } except Exception as e: await db.rollback() raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Error updating ticket: {str(e)}" ) @router.patch("/{ticket_id}/close", response_model=TicketResponse) async def close_ticket( ticket_id: str, close_request: TicketCloseRequest, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """ Cerrar un ticket """ try: ticket_uuid = uuid.UUID(ticket_id) except ValueError: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid ticket ID format" ) query = select(Ticket).where( Ticket.id == ticket_uuid, Ticket.tenant_id == current_user.tenant_id, Ticket.created_by == current_user.id ) result = await db.execute(query) db_ticket = result.scalars().first() if not db_ticket: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Ticket {ticket_id} not found" ) try: db_ticket.status = TicketStatus.CLOSED db_ticket.updated_at = datetime.utcnow() await db.commit() await db.refresh(db_ticket) # ✅ CORREGIDO: Usar affected_system_id return { "id": str(db_ticket.id), "ticket_number": db_ticket.ticket_number, "subject": db_ticket.subject, "title": db_ticket.subject, "description": db_ticket.description, "status": db_ticket.status.value, "priority": db_ticket.priority.value, "category_id": str(db_ticket.category_id) if db_ticket.category_id else None, "affected_system_id": str(db_ticket.affected_system_id) if db_ticket.affected_system_id else None, # ✅ CORREGIDO "created_by": str(db_ticket.created_by), "assigned_to": str(db_ticket.assigned_to) if db_ticket.assigned_to else None, "created_at": db_ticket.created_at, "updated_at": db_ticket.updated_at } except Exception as e: await db.rollback() raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Error closing ticket: {str(e)}" ) ## =================================== # COMMENT ENDPOINTS # =================================== class CommentCreate(BaseModel): content: str is_internal: bool = False class CommentResponse(BaseModel): id: str ticket_id: str author_id: str author_name: str content: str is_internal: bool created_at: datetime updated_at: datetime class Config: from_attributes = True @router.get("/{ticket_id}/comments", response_model=List[CommentResponse]) async def get_ticket_comments( ticket_id: str, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """ Obtener comentarios de un ticket """ try: ticket_uuid = uuid.UUID(ticket_id) except ValueError: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid ticket ID format" ) # Verificar que el ticket existe y el usuario tiene acceso ticket_query = select(Ticket).where( Ticket.id == ticket_uuid, Ticket.tenant_id == current_user.tenant_id ) ticket_result = await db.execute(ticket_query) ticket = ticket_result.scalars().first() if not ticket: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Ticket {ticket_id} not found" ) # Obtener comentarios comments_query = select(TicketComment, User).join( User, TicketComment.author_id == User.id ).where( TicketComment.ticket_id == ticket_uuid ).order_by(TicketComment.created_at.asc()) result = await db.execute(comments_query) comments_with_users = result.all() # Formatear respuesta return [ { "id": str(comment.id), "ticket_id": str(comment.ticket_id), "author_id": str(comment.author_id), "author_name": f"{user.first_name} {user.last_name}", "content": comment.content, "is_internal": comment.is_internal, "created_at": comment.created_at, "updated_at": comment.updated_at } for comment, user in comments_with_users ] @router.post("/{ticket_id}/comments", response_model=CommentResponse, status_code=status.HTTP_201_CREATED) async def create_comment( ticket_id: str, comment: CommentCreate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """ Agregar un comentario a un ticket """ try: ticket_uuid = uuid.UUID(ticket_id) except ValueError: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid ticket ID format" ) # Verificar que el ticket existe y el usuario tiene acceso ticket_query = select(Ticket).where( Ticket.id == ticket_uuid, Ticket.tenant_id == current_user.tenant_id ) ticket_result = await db.execute(ticket_query) ticket_obj = ticket_result.scalars().first() if not ticket_obj: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Ticket {ticket_id} not found" ) # Crear comentario new_comment = TicketComment( id=uuid.uuid4(), ticket_id=ticket_uuid, author_id=current_user.id, content=comment.content, is_internal=comment.is_internal, created_at=datetime.utcnow(), updated_at=datetime.utcnow() ) db.add(new_comment) # Actualizar el ticket updated_at ticket_obj.updated_at = datetime.utcnow() await db.commit() await db.refresh(new_comment) # Retornar con el nombre del autor return { "id": str(new_comment.id), "ticket_id": str(new_comment.ticket_id), "author_id": str(new_comment.author_id), "author_name": f"{current_user.first_name} {current_user.last_name}", "content": new_comment.content, "is_internal": new_comment.is_internal, "created_at": new_comment.created_at, "updated_at": new_comment.updated_at } @router.delete("/{ticket_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_ticket( ticket_id: str, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """ Eliminar un ticket (solo admin/manager) """ try: ticket_uuid = uuid.UUID(ticket_id) except ValueError: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid ticket ID format" ) query = select(Ticket).where( Ticket.id == ticket_uuid, Ticket.tenant_id == current_user.tenant_id ) result = await db.execute(query) db_ticket = result.scalars().first() if not db_ticket: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Ticket {ticket_id} not found" ) await db.delete(db_ticket) await db.commit() return {"message": "Ticket deleted successfully"} # =================================== # ATTACHMENT ENDPOINTS (placeholder) # =================================== @router.get("/{ticket_id}/attachments") async def get_ticket_attachments( ticket_id: str, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """ Obtener adjuntos de un ticket """ return [] @router.post("/{ticket_id}/attachments", status_code=status.HTTP_201_CREATED) async def upload_attachment( ticket_id: str, file: UploadFile = File(...), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """ Subir un archivo adjunto a un ticket """ raise HTTPException( status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="File uploads not yet implemented" )