From 5746ad42e5331ad3584b8437a6b6261bdb6e25db Mon Sep 17 00:00:00 2001 From: Claude AI Date: Thu, 29 Jan 2026 10:54:51 +0000 Subject: [PATCH] feat(fase3): add supervisor dashboard API Co-Authored-By: Claude Opus 4.5 --- services/api-gateway/app/main.py | 3 +- services/api-gateway/app/routers/__init__.py | 5 +- .../api-gateway/app/routers/supervisor.py | 201 ++++++++++++++++++ 3 files changed, 206 insertions(+), 3 deletions(-) create mode 100644 services/api-gateway/app/routers/supervisor.py diff --git a/services/api-gateway/app/main.py b/services/api-gateway/app/main.py index b15f56f..e3cf2da 100644 --- a/services/api-gateway/app/main.py +++ b/services/api-gateway/app/main.py @@ -2,7 +2,7 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from app.core.config import get_settings from app.core.database import engine, Base -from app.routers import auth, whatsapp, flows +from app.routers import auth, whatsapp, flows, supervisor settings = get_settings() @@ -29,6 +29,7 @@ app.add_middleware( app.include_router(auth.router) app.include_router(whatsapp.router) app.include_router(flows.router) +app.include_router(supervisor.router) @app.get("/health") diff --git a/services/api-gateway/app/routers/__init__.py b/services/api-gateway/app/routers/__init__.py index b1d4d82..78c4fc2 100644 --- a/services/api-gateway/app/routers/__init__.py +++ b/services/api-gateway/app/routers/__init__.py @@ -1,5 +1,6 @@ from app.routers.auth import router as auth_router -from app.routers.whatsapp import router as whatsapp_router from app.routers.flows import router as flows_router +from app.routers.supervisor import router as supervisor_router +from app.routers.whatsapp import router as whatsapp_router -__all__ = ["auth_router", "whatsapp_router", "flows_router"] +__all__ = ["auth_router", "flows_router", "supervisor_router", "whatsapp_router"] diff --git a/services/api-gateway/app/routers/supervisor.py b/services/api-gateway/app/routers/supervisor.py new file mode 100644 index 0000000..bbc5dd0 --- /dev/null +++ b/services/api-gateway/app/routers/supervisor.py @@ -0,0 +1,201 @@ +from datetime import datetime, timedelta + +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy import func +from sqlalchemy.orm import Session + +from app.core.database import get_db +from app.core.security import get_current_user +from app.models.queue import Queue +from app.models.user import User, UserRole, UserStatus +from app.models.whatsapp import Conversation, ConversationStatus, Message + +router = APIRouter(prefix="/api/supervisor", tags=["supervisor"]) + + +def require_supervisor(current_user: User = Depends(get_current_user)) -> User: + if current_user.role not in [UserRole.ADMIN, UserRole.SUPERVISOR]: + raise HTTPException(status_code=403, detail="Supervisor or Admin required") + return current_user + + +@router.get("/dashboard") +def get_dashboard( + db: Session = Depends(get_db), + current_user: User = Depends(require_supervisor), +) -> dict: + """Get supervisor dashboard data""" + now = datetime.utcnow() + today_start = now.replace(hour=0, minute=0, second=0, microsecond=0) + + status_counts = {} + for status in ConversationStatus: + count = db.query(Conversation).filter(Conversation.status == status).count() + status_counts[status.value] = count + + waiting_conversations = db.query(Conversation).filter( + Conversation.status == ConversationStatus.WAITING + ).count() + + active_agents = db.query(User).filter( + User.status == UserStatus.ONLINE, + User.is_active == True, + ).count() + + total_agents = db.query(User).filter( + User.role.in_([UserRole.AGENT, UserRole.SUPERVISOR]), + User.is_active == True, + ).count() + + resolved_today = db.query(Conversation).filter( + Conversation.resolved_at >= today_start + ).count() + + messages_today = db.query(Message).filter( + Message.created_at >= today_start + ).count() + + csat_result = db.query(func.avg(Conversation.csat_score)).filter( + Conversation.csat_score != None, + Conversation.resolved_at >= today_start, + ).scalar() + avg_csat = round(float(csat_result), 2) if csat_result else None + + return { + "conversations": { + "by_status": status_counts, + "waiting": waiting_conversations, + "resolved_today": resolved_today, + }, + "agents": { + "online": active_agents, + "total": total_agents, + }, + "messages_today": messages_today, + "avg_csat": avg_csat, + } + + +@router.get("/agents") +def get_agents_status( + db: Session = Depends(get_db), + current_user: User = Depends(require_supervisor), +) -> list[dict]: + """Get detailed agent status for supervisor view""" + agents = db.query(User).filter( + User.role.in_([UserRole.AGENT, UserRole.SUPERVISOR]), + User.is_active == True, + ).all() + + today_start = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0) + result = [] + + for agent in agents: + active = db.query(Conversation).filter( + Conversation.assigned_to == agent.id, + Conversation.status == ConversationStatus.ACTIVE, + ).count() + + waiting = db.query(Conversation).filter( + Conversation.assigned_to == agent.id, + Conversation.status == ConversationStatus.WAITING, + ).count() + + resolved_today = db.query(Conversation).filter( + Conversation.assigned_to == agent.id, + Conversation.resolved_at >= today_start, + ).count() + + result.append({ + "id": str(agent.id), + "name": agent.name, + "email": agent.email, + "role": agent.role.value, + "status": agent.status.value, + "active_conversations": active, + "waiting_conversations": waiting, + "resolved_today": resolved_today, + }) + + return result + + +@router.get("/queues") +def get_queues_status( + db: Session = Depends(get_db), + current_user: User = Depends(require_supervisor), +) -> list[dict]: + """Get queue status for supervisor view""" + queues = db.query(Queue).filter(Queue.is_active == True).all() + + result = [] + for queue in queues: + waiting = db.query(Conversation).filter( + Conversation.queue_id == queue.id, + Conversation.status == ConversationStatus.WAITING, + ).count() + + active = db.query(Conversation).filter( + Conversation.queue_id == queue.id, + Conversation.status == ConversationStatus.ACTIVE, + ).count() + + online_agents = sum( + 1 for qa in queue.agents + if qa.user and qa.user.status == UserStatus.ONLINE + ) + + result.append({ + "id": str(queue.id), + "name": queue.name, + "waiting_conversations": waiting, + "active_conversations": active, + "online_agents": online_agents, + "total_agents": len(queue.agents), + "sla_first_response": queue.sla_first_response, + }) + + return result + + +@router.get("/conversations/critical") +def get_critical_conversations( + db: Session = Depends(get_db), + current_user: User = Depends(require_supervisor), +) -> list[dict]: + """Get conversations that need attention""" + now = datetime.utcnow() + + long_wait = db.query(Conversation).filter( + Conversation.status == ConversationStatus.WAITING, + Conversation.last_message_at < now - timedelta(minutes=5), + ).all() + + high_priority = db.query(Conversation).filter( + Conversation.priority.in_(["high", "urgent"]), + Conversation.status.in_([ConversationStatus.WAITING, ConversationStatus.ACTIVE]), + ).all() + + long_wait_ids = {conv.id for conv in long_wait} + result = [] + seen_ids = set() + + for conv in long_wait + high_priority: + if conv.id in seen_ids: + continue + seen_ids.add(conv.id) + + reason = "long_wait" if conv.id in long_wait_ids else "high_priority" + + result.append({ + "id": str(conv.id), + "contact_name": conv.contact.name if conv.contact else "Unknown", + "contact_phone": conv.contact.phone_number if conv.contact else "", + "status": conv.status.value, + "priority": conv.priority, + "assigned_to": str(conv.assigned_to) if conv.assigned_to else None, + "last_message_at": conv.last_message_at.isoformat() if conv.last_message_at else None, + "reason": reason, + }) + + return result