feat(fase3): add supervisor dashboard API
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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"]
|
||||
|
||||
201
services/api-gateway/app/routers/supervisor.py
Normal file
201
services/api-gateway/app/routers/supervisor.py
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user