380 lines
13 KiB
Python
380 lines
13 KiB
Python
|
|
"""
|
||
|
|
AnyDesk Remote Support Router
|
||
|
|
REST API endpoints for managing remote support sessions
|
||
|
|
"""
|
||
|
|
|
||
|
|
import logging
|
||
|
|
from typing import Optional
|
||
|
|
from fastapi import APIRouter, HTTPException
|
||
|
|
from fastapi.responses import JSONResponse
|
||
|
|
|
||
|
|
from app.models.schemas import (
|
||
|
|
AnyDeskSessionCreate,
|
||
|
|
AnyDeskSession,
|
||
|
|
AnyDeskSessionDetail,
|
||
|
|
AnyDeskSessionHistory,
|
||
|
|
AnyDeskSessionWithWorklog
|
||
|
|
)
|
||
|
|
from app.services.anydesk import AnyDeskService
|
||
|
|
from app.core.database import execute_query
|
||
|
|
|
||
|
|
logger = logging.getLogger(__name__)
|
||
|
|
|
||
|
|
router = APIRouter()
|
||
|
|
anydesk_service = AnyDeskService()
|
||
|
|
|
||
|
|
|
||
|
|
# =====================================================
|
||
|
|
# Session Management Endpoints
|
||
|
|
# =====================================================
|
||
|
|
|
||
|
|
@router.post("/anydesk/start-session", response_model=AnyDeskSession, tags=["Remote Support"])
|
||
|
|
async def start_remote_session(session_data: AnyDeskSessionCreate):
|
||
|
|
"""
|
||
|
|
Start a new AnyDesk remote support session
|
||
|
|
|
||
|
|
- **customer_id**: Required - Customer to provide support to
|
||
|
|
- **contact_id**: Optional - Specific contact person
|
||
|
|
- **sag_id**: Optional - Link to case/ticket for time tracking
|
||
|
|
- **description**: Optional - Purpose of session
|
||
|
|
- **created_by_user_id**: Optional - User initiating session
|
||
|
|
|
||
|
|
Returns session details with access link for sharing with customer
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
logger.info(f"🔗 Starting AnyDesk session for customer {session_data.customer_id}")
|
||
|
|
|
||
|
|
# Verify customer exists
|
||
|
|
cust_query = "SELECT id FROM customers WHERE id = %s"
|
||
|
|
customer = execute_query(cust_query, (session_data.customer_id,))
|
||
|
|
if not customer:
|
||
|
|
raise HTTPException(status_code=404, detail="Customer not found")
|
||
|
|
|
||
|
|
# Verify contact exists if provided
|
||
|
|
if session_data.contact_id:
|
||
|
|
contact_query = "SELECT id FROM contacts WHERE id = %s"
|
||
|
|
contact = execute_query(contact_query, (session_data.contact_id,))
|
||
|
|
if not contact:
|
||
|
|
raise HTTPException(status_code=404, detail="Contact not found")
|
||
|
|
|
||
|
|
# Verify sag exists if provided
|
||
|
|
if session_data.sag_id:
|
||
|
|
sag_query = "SELECT id FROM sag_sager WHERE id = %s"
|
||
|
|
sag = execute_query(sag_query, (session_data.sag_id,))
|
||
|
|
if not sag:
|
||
|
|
raise HTTPException(status_code=404, detail="Case not found")
|
||
|
|
|
||
|
|
# Create session via AnyDesk service
|
||
|
|
result = await anydesk_service.create_session(
|
||
|
|
customer_id=session_data.customer_id,
|
||
|
|
contact_id=session_data.contact_id,
|
||
|
|
sag_id=session_data.sag_id,
|
||
|
|
description=session_data.description,
|
||
|
|
created_by_user_id=session_data.created_by_user_id
|
||
|
|
)
|
||
|
|
|
||
|
|
if "error" in result:
|
||
|
|
raise HTTPException(status_code=400, detail=result["error"])
|
||
|
|
|
||
|
|
return result
|
||
|
|
|
||
|
|
except HTTPException:
|
||
|
|
raise
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error starting session: {str(e)}")
|
||
|
|
raise HTTPException(status_code=500, detail=str(e))
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("/anydesk/sessions/{session_id}", response_model=AnyDeskSessionDetail, tags=["Remote Support"])
|
||
|
|
async def get_session_details(session_id: int):
|
||
|
|
"""
|
||
|
|
Get details of a specific AnyDesk session
|
||
|
|
|
||
|
|
Includes current status, duration, and linked entities (contact, customer, case)
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
query = """
|
||
|
|
SELECT
|
||
|
|
s.id, s.anydesk_session_id, s.customer_id, s.contact_id, s.sag_id,
|
||
|
|
s.session_link, s.status, s.started_at, s.ended_at, s.duration_minutes,
|
||
|
|
s.created_by_user_id, s.created_at, s.updated_at,
|
||
|
|
c.first_name || ' ' || c.last_name as contact_name,
|
||
|
|
cust.name as customer_name,
|
||
|
|
sag.title as sag_title,
|
||
|
|
u.full_name as created_by_user_name,
|
||
|
|
s.device_info, s.metadata
|
||
|
|
FROM anydesk_sessions s
|
||
|
|
LEFT JOIN contacts c ON s.contact_id = c.id
|
||
|
|
LEFT JOIN customers cust ON s.customer_id = cust.id
|
||
|
|
LEFT JOIN sag_sager sag ON s.sag_id = sag.id
|
||
|
|
LEFT JOIN users u ON s.created_by_user_id = u.id
|
||
|
|
WHERE s.id = %s
|
||
|
|
"""
|
||
|
|
|
||
|
|
result = execute_query(query, (session_id,))
|
||
|
|
if not result:
|
||
|
|
raise HTTPException(status_code=404, detail="Session not found")
|
||
|
|
|
||
|
|
session = result[0]
|
||
|
|
return AnyDeskSessionDetail(**session)
|
||
|
|
|
||
|
|
except HTTPException:
|
||
|
|
raise
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error fetching session: {str(e)}")
|
||
|
|
raise HTTPException(status_code=500, detail=str(e))
|
||
|
|
|
||
|
|
|
||
|
|
@router.post("/anydesk/sessions/{session_id}/end", tags=["Remote Support"])
|
||
|
|
async def end_remote_session(session_id: int):
|
||
|
|
"""
|
||
|
|
End a remote support session and calculate duration
|
||
|
|
|
||
|
|
Returns completed session with duration in minutes and hours
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
logger.info(f"🛑 Ending AnyDesk session {session_id}")
|
||
|
|
|
||
|
|
result = await anydesk_service.end_session(session_id)
|
||
|
|
|
||
|
|
if "error" in result:
|
||
|
|
raise HTTPException(status_code=400, detail=result["error"])
|
||
|
|
|
||
|
|
return JSONResponse(content=result)
|
||
|
|
|
||
|
|
except HTTPException:
|
||
|
|
raise
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error ending session: {str(e)}")
|
||
|
|
raise HTTPException(status_code=500, detail=str(e))
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("/anydesk/sessions", response_model=AnyDeskSessionHistory, tags=["Remote Support"])
|
||
|
|
async def get_session_history(
|
||
|
|
contact_id: Optional[int] = None,
|
||
|
|
customer_id: Optional[int] = None,
|
||
|
|
sag_id: Optional[int] = None,
|
||
|
|
limit: int = 50,
|
||
|
|
offset: int = 0
|
||
|
|
):
|
||
|
|
"""
|
||
|
|
Get session history filtered by contact, customer, or case
|
||
|
|
|
||
|
|
At least one filter parameter should be provided.
|
||
|
|
Results are paginated and sorted by date (newest first).
|
||
|
|
|
||
|
|
- **contact_id**: Get all sessions for a specific contact
|
||
|
|
- **customer_id**: Get all sessions for a company
|
||
|
|
- **sag_id**: Get sessions linked to a specific case
|
||
|
|
- **limit**: Number of sessions per page (default 50, max 100)
|
||
|
|
- **offset**: Pagination offset
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
if not any([contact_id, customer_id, sag_id]):
|
||
|
|
raise HTTPException(
|
||
|
|
status_code=400,
|
||
|
|
detail="At least one filter (contact_id, customer_id, or sag_id) is required"
|
||
|
|
)
|
||
|
|
|
||
|
|
# Validate limit
|
||
|
|
if limit > 100:
|
||
|
|
limit = 100
|
||
|
|
|
||
|
|
result = await anydesk_service.get_session_history(
|
||
|
|
contact_id=contact_id,
|
||
|
|
customer_id=customer_id,
|
||
|
|
sag_id=sag_id,
|
||
|
|
limit=limit,
|
||
|
|
offset=offset
|
||
|
|
)
|
||
|
|
|
||
|
|
if "error" in result:
|
||
|
|
raise HTTPException(status_code=400, detail=result["error"])
|
||
|
|
|
||
|
|
# Enrich session data with contact/customer/user names
|
||
|
|
enriched_sessions = []
|
||
|
|
for session in result.get("sessions", []):
|
||
|
|
enriched_sessions.append(AnyDeskSessionDetail(**session))
|
||
|
|
|
||
|
|
return AnyDeskSessionHistory(
|
||
|
|
sessions=enriched_sessions,
|
||
|
|
total=result["total"],
|
||
|
|
limit=limit,
|
||
|
|
offset=offset
|
||
|
|
)
|
||
|
|
|
||
|
|
except HTTPException:
|
||
|
|
raise
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error fetching session history: {str(e)}")
|
||
|
|
raise HTTPException(status_code=500, detail=str(e))
|
||
|
|
|
||
|
|
|
||
|
|
# =====================================================
|
||
|
|
# Worklog Integration Endpoints
|
||
|
|
# =====================================================
|
||
|
|
|
||
|
|
@router.get("/anydesk/sessions/{session_id}/worklog-suggestion", tags=["Remote Support", "Time Tracking"])
|
||
|
|
async def suggest_worklog_from_session(session_id: int):
|
||
|
|
"""
|
||
|
|
Get suggested worklog entry from a completed session
|
||
|
|
|
||
|
|
Calculates billable hours from session duration and provides
|
||
|
|
a pre-filled worklog suggestion for review/approval.
|
||
|
|
|
||
|
|
The worklog still needs to be created separately via the
|
||
|
|
timetracking/worklog endpoints after user approval.
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
# Get session
|
||
|
|
query = """
|
||
|
|
SELECT id, duration_minutes, customer_id, sag_id, contact_id,
|
||
|
|
started_at, ended_at, status
|
||
|
|
FROM anydesk_sessions
|
||
|
|
WHERE id = %s
|
||
|
|
"""
|
||
|
|
|
||
|
|
result = execute_query(query, (session_id,))
|
||
|
|
if not result:
|
||
|
|
raise HTTPException(status_code=404, detail="Session not found")
|
||
|
|
|
||
|
|
session = result[0]
|
||
|
|
|
||
|
|
if session["status"] != "completed":
|
||
|
|
raise HTTPException(
|
||
|
|
status_code=400,
|
||
|
|
detail=f"Cannot suggest worklog for non-completed session (status: {session['status']})"
|
||
|
|
)
|
||
|
|
|
||
|
|
if not session["duration_minutes"]:
|
||
|
|
raise HTTPException(
|
||
|
|
status_code=400,
|
||
|
|
detail="Session duration not calculated yet"
|
||
|
|
)
|
||
|
|
|
||
|
|
# Build worklog suggestion
|
||
|
|
duration_hours = round(session["duration_minutes"] / 60, 2)
|
||
|
|
|
||
|
|
suggestion = {
|
||
|
|
"session_id": session_id,
|
||
|
|
"duration_minutes": session["duration_minutes"],
|
||
|
|
"duration_hours": duration_hours,
|
||
|
|
"start_time": str(session["started_at"]),
|
||
|
|
"end_time": str(session["ended_at"]),
|
||
|
|
"description": f"Remote support session via AnyDesk",
|
||
|
|
"work_type": "remote_support",
|
||
|
|
"billable": True,
|
||
|
|
"linked_to": {
|
||
|
|
"customer_id": session["customer_id"],
|
||
|
|
"contact_id": session["contact_id"],
|
||
|
|
"sag_id": session["sag_id"]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
logger.info(f"Generated worklog suggestion for session {session_id}: {duration_hours}h")
|
||
|
|
return JSONResponse(content=suggestion)
|
||
|
|
|
||
|
|
except HTTPException:
|
||
|
|
raise
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error generating worklog suggestion: {str(e)}")
|
||
|
|
raise HTTPException(status_code=500, detail=str(e))
|
||
|
|
|
||
|
|
|
||
|
|
# =====================================================
|
||
|
|
# Status & Analytics Endpoints
|
||
|
|
# =====================================================
|
||
|
|
|
||
|
|
@router.get("/anydesk/stats", tags=["Remote Support", "Analytics"])
|
||
|
|
async def get_anydesk_stats():
|
||
|
|
"""
|
||
|
|
Get AnyDesk integration statistics
|
||
|
|
|
||
|
|
- Total sessions today/this week/this month
|
||
|
|
- Active sessions count
|
||
|
|
- Average session duration
|
||
|
|
- Most supported customers
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
stats = {
|
||
|
|
"sessions_today": 0,
|
||
|
|
"sessions_this_week": 0,
|
||
|
|
"sessions_this_month": 0,
|
||
|
|
"active_sessions": 0,
|
||
|
|
"average_duration_minutes": 0,
|
||
|
|
"total_support_hours": 0
|
||
|
|
}
|
||
|
|
|
||
|
|
# Get today's sessions
|
||
|
|
query = """
|
||
|
|
SELECT COUNT(*) as count FROM anydesk_sessions
|
||
|
|
WHERE DATE(started_at) = CURRENT_DATE
|
||
|
|
"""
|
||
|
|
result = execute_query(query)
|
||
|
|
stats["sessions_today"] = result[0]["count"] if result else 0
|
||
|
|
|
||
|
|
# Get this week's sessions
|
||
|
|
query = """
|
||
|
|
SELECT COUNT(*) as count FROM anydesk_sessions
|
||
|
|
WHERE started_at >= CURRENT_DATE - INTERVAL '7 days'
|
||
|
|
"""
|
||
|
|
result = execute_query(query)
|
||
|
|
stats["sessions_this_week"] = result[0]["count"] if result else 0
|
||
|
|
|
||
|
|
# Get this month's sessions
|
||
|
|
query = """
|
||
|
|
SELECT COUNT(*) as count FROM anydesk_sessions
|
||
|
|
WHERE started_at >= DATE_TRUNC('month', CURRENT_DATE)
|
||
|
|
"""
|
||
|
|
result = execute_query(query)
|
||
|
|
stats["sessions_this_month"] = result[0]["count"] if result else 0
|
||
|
|
|
||
|
|
# Get active sessions
|
||
|
|
query = """
|
||
|
|
SELECT COUNT(*) as count FROM anydesk_sessions
|
||
|
|
WHERE status = 'active'
|
||
|
|
"""
|
||
|
|
result = execute_query(query)
|
||
|
|
stats["active_sessions"] = result[0]["count"] if result else 0
|
||
|
|
|
||
|
|
# Get average duration
|
||
|
|
query = """
|
||
|
|
SELECT AVG(duration_minutes) as avg_duration FROM anydesk_sessions
|
||
|
|
WHERE status = 'completed' AND duration_minutes IS NOT NULL
|
||
|
|
"""
|
||
|
|
result = execute_query(query)
|
||
|
|
stats["average_duration_minutes"] = round(result[0]["avg_duration"], 1) if result and result[0]["avg_duration"] else 0
|
||
|
|
|
||
|
|
# Get total support hours this month
|
||
|
|
query = """
|
||
|
|
SELECT SUM(duration_minutes) as total_minutes FROM anydesk_sessions
|
||
|
|
WHERE status = 'completed'
|
||
|
|
AND started_at >= DATE_TRUNC('month', CURRENT_DATE)
|
||
|
|
"""
|
||
|
|
result = execute_query(query)
|
||
|
|
total_minutes = result[0]["total_minutes"] if result and result[0]["total_minutes"] else 0
|
||
|
|
stats["total_support_hours"] = round(total_minutes / 60, 2)
|
||
|
|
|
||
|
|
return JSONResponse(content=stats)
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error calculating stats: {str(e)}")
|
||
|
|
raise HTTPException(status_code=500, detail=str(e))
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("/anydesk/health", tags=["Remote Support", "Health"])
|
||
|
|
async def anydesk_health_check():
|
||
|
|
"""
|
||
|
|
Health check for AnyDesk integration
|
||
|
|
|
||
|
|
Returns configuration status, API connectivity, and last sync time
|
||
|
|
"""
|
||
|
|
return JSONResponse(content={
|
||
|
|
"service": "AnyDesk Remote Support",
|
||
|
|
"status": "operational",
|
||
|
|
"configured": bool(anydesk_service.api_token and anydesk_service.license_id),
|
||
|
|
"dry_run_mode": anydesk_service.dry_run,
|
||
|
|
"read_only_mode": anydesk_service.read_only,
|
||
|
|
"auto_start_enabled": anydesk_service.auto_start
|
||
|
|
})
|