Files
service_manager/backend/app/api/v1/endpoints/tickets.py
2026-01-19 14:04:55 -07:00

427 lines
13 KiB
Python

"""
Tickets endpoints - ServiceManagerWeb
"""
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File
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 pydantic import BaseModel
import uuid
router = APIRouter()
# ===================================
# SCHEMAS
# ===================================
class TicketCreate(BaseModel):
subject: str
description: str
category_id: Optional[str] = None
system_id: Optional[str] = None
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
description: str
status: str
priority: str
category_id: Optional[str] = None
system_id: Optional[str] = None
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.system_id) if ticket.system_id else None
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,
system_id=system_uuid,
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)
# Convertir a respuesta
return {
"id": str(db_ticket.id),
"ticket_number": db_ticket.ticket_number,
"subject": 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,
"system_id": str(db_ticket.system_id) if db_ticket.system_id else None,
"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()
return [
{
"id": str(t.id),
"ticket_number": t.ticket_number,
"subject": 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,
"system_id": str(t.system_id) if t.system_id else None,
"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"
)
return {
"id": str(ticket.id),
"ticket_number": ticket.ticket_number,
"subject": 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,
"system_id": str(ticket.system_id) if ticket.system_id else None,
"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)
return {
"id": str(db_ticket.id),
"ticket_number": db_ticket.ticket_number,
"subject": 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,
"system_id": str(db_ticket.system_id) if db_ticket.system_id else None,
"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)
return {
"id": str(db_ticket.id),
"ticket_number": db_ticket.ticket_number,
"subject": 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,
"system_id": str(db_ticket.system_id) if db_ticket.system_id else None,
"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 (placeholder)
# ===================================
@router.get("/{ticket_id}/comments")
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
"""
return []
@router.post("/{ticket_id}/comments", status_code=status.HTTP_201_CREATED)
async def create_comment(
ticket_id: str,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Agregar un comentario a un ticket
"""
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="Comments not yet implemented"
)
# ===================================
# 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"
)