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 fastapi.middleware.cors import CORSMiddleware
|
||||||
from app.core.config import get_settings
|
from app.core.config import get_settings
|
||||||
from app.core.database import engine, Base
|
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()
|
settings = get_settings()
|
||||||
|
|
||||||
@@ -29,6 +29,7 @@ app.add_middleware(
|
|||||||
app.include_router(auth.router)
|
app.include_router(auth.router)
|
||||||
app.include_router(whatsapp.router)
|
app.include_router(whatsapp.router)
|
||||||
app.include_router(flows.router)
|
app.include_router(flows.router)
|
||||||
|
app.include_router(supervisor.router)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from app.routers.auth import router as auth_router
|
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.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