mirror of
https://github.com/TronoSfera/Law.git
synced 2026-05-18 18:13:46 +03:00
241 lines
9.5 KiB
Python
241 lines
9.5 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import datetime, timezone
|
|
from decimal import Decimal
|
|
from uuid import UUID
|
|
|
|
from fastapi import APIRouter, Depends
|
|
from sqlalchemy import func
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.core.deps import require_role
|
|
from app.db.session import get_db
|
|
from app.models.admin_user import AdminUser
|
|
from app.models.request import Request
|
|
from app.models.status import Status
|
|
from app.models.status_history import StatusHistory
|
|
from app.services.sla_metrics import compute_sla_snapshot
|
|
|
|
router = APIRouter()
|
|
|
|
DEFAULT_TERMINAL_STATUS_CODES = {"RESOLVED", "CLOSED", "REJECTED"}
|
|
PAID_STATUS_CODES = {"PAID", "ОПЛАЧЕНО"}
|
|
|
|
|
|
def _terminal_status_codes(db: Session) -> set[str]:
|
|
rows = db.query(Status.code).filter(Status.is_terminal.is_(True)).all()
|
|
codes = {str(code).strip() for (code,) in rows if code}
|
|
return codes or set(DEFAULT_TERMINAL_STATUS_CODES)
|
|
|
|
|
|
def _paid_status_codes() -> set[str]:
|
|
return set(PAID_STATUS_CODES)
|
|
|
|
|
|
def _month_bounds(now_utc: datetime) -> tuple[datetime, datetime]:
|
|
start = datetime(now_utc.year, now_utc.month, 1, tzinfo=timezone.utc)
|
|
if now_utc.month == 12:
|
|
end = datetime(now_utc.year + 1, 1, 1, tzinfo=timezone.utc)
|
|
else:
|
|
end = datetime(now_utc.year, now_utc.month + 1, 1, tzinfo=timezone.utc)
|
|
return start, end
|
|
|
|
|
|
def _to_float(value) -> float:
|
|
if value is None:
|
|
return 0.0
|
|
if isinstance(value, Decimal):
|
|
return float(value)
|
|
try:
|
|
return float(value)
|
|
except (TypeError, ValueError):
|
|
return 0.0
|
|
|
|
|
|
def _uuid_or_none(value: str | None) -> UUID | None:
|
|
try:
|
|
return UUID(str(value or ""))
|
|
except ValueError:
|
|
return None
|
|
|
|
|
|
@router.get("/overview")
|
|
def overview(db: Session = Depends(get_db), admin=Depends(require_role("ADMIN", "LAWYER"))):
|
|
role = str(admin.get("role") or "").upper()
|
|
actor_id = str(admin.get("sub") or "").strip()
|
|
actor_uuid = _uuid_or_none(actor_id)
|
|
|
|
terminal_codes = _terminal_status_codes(db)
|
|
paid_codes = _paid_status_codes()
|
|
now_utc = datetime.now(timezone.utc)
|
|
month_start, next_month_start = _month_bounds(now_utc)
|
|
|
|
unread_for_clients = (
|
|
db.query(func.count(Request.id))
|
|
.filter(Request.client_has_unread_updates.is_(True))
|
|
.scalar()
|
|
or 0
|
|
)
|
|
unread_for_lawyers = (
|
|
db.query(func.count(Request.id))
|
|
.filter(Request.lawyer_has_unread_updates.is_(True))
|
|
.scalar()
|
|
or 0
|
|
)
|
|
|
|
active_load_rows = (
|
|
db.query(Request.assigned_lawyer_id, func.count(Request.id))
|
|
.filter(Request.assigned_lawyer_id.is_not(None))
|
|
.filter(Request.status_code.notin_(terminal_codes))
|
|
.group_by(Request.assigned_lawyer_id)
|
|
.all()
|
|
)
|
|
total_load_rows = (
|
|
db.query(Request.assigned_lawyer_id, func.count(Request.id))
|
|
.filter(Request.assigned_lawyer_id.is_not(None))
|
|
.group_by(Request.assigned_lawyer_id)
|
|
.all()
|
|
)
|
|
active_amount_rows = (
|
|
db.query(Request.assigned_lawyer_id, func.coalesce(func.sum(func.coalesce(Request.invoice_amount, 0)), 0))
|
|
.filter(Request.assigned_lawyer_id.is_not(None))
|
|
.filter(Request.status_code.notin_(terminal_codes))
|
|
.group_by(Request.assigned_lawyer_id)
|
|
.all()
|
|
)
|
|
paid_rows = (
|
|
db.query(
|
|
Request.assigned_lawyer_id,
|
|
func.count(StatusHistory.id),
|
|
func.coalesce(func.sum(func.coalesce(Request.invoice_amount, 0)), 0),
|
|
)
|
|
.join(StatusHistory, StatusHistory.request_id == Request.id)
|
|
.filter(Request.assigned_lawyer_id.is_not(None))
|
|
.filter(StatusHistory.created_at >= month_start, StatusHistory.created_at < next_month_start)
|
|
.filter(func.upper(StatusHistory.to_status).in_(paid_codes))
|
|
.group_by(Request.assigned_lawyer_id)
|
|
.all()
|
|
)
|
|
|
|
active_load_map = {str(lawyer_id): int(count) for lawyer_id, count in active_load_rows if lawyer_id}
|
|
total_load_map = {str(lawyer_id): int(count) for lawyer_id, count in total_load_rows if lawyer_id}
|
|
active_amount_map = {str(lawyer_id): _to_float(amount) for lawyer_id, amount in active_amount_rows if lawyer_id}
|
|
paid_events_map = {str(lawyer_id): int(events) for lawyer_id, events, _ in paid_rows if lawyer_id}
|
|
monthly_gross_map = {str(lawyer_id): _to_float(gross) for lawyer_id, _, gross in paid_rows if lawyer_id}
|
|
|
|
lawyers = (
|
|
db.query(AdminUser)
|
|
.filter(AdminUser.role == "LAWYER", AdminUser.is_active.is_(True))
|
|
.all()
|
|
)
|
|
lawyer_loads = []
|
|
for lawyer in lawyers:
|
|
lawyer_id = str(lawyer.id)
|
|
salary_percent = _to_float(lawyer.salary_percent)
|
|
monthly_paid_gross = monthly_gross_map.get(lawyer_id, 0.0)
|
|
monthly_salary = monthly_paid_gross * salary_percent / 100.0
|
|
lawyer_loads.append(
|
|
{
|
|
"lawyer_id": lawyer_id,
|
|
"name": lawyer.name,
|
|
"email": lawyer.email,
|
|
"avatar_url": lawyer.avatar_url,
|
|
"primary_topic_code": lawyer.primary_topic_code,
|
|
"default_rate": _to_float(lawyer.default_rate),
|
|
"salary_percent": salary_percent,
|
|
"active_load": active_load_map.get(lawyer_id, 0),
|
|
"total_assigned": total_load_map.get(lawyer_id, 0),
|
|
"active_amount": round(active_amount_map.get(lawyer_id, 0.0), 2),
|
|
"monthly_paid_events": paid_events_map.get(lawyer_id, 0),
|
|
"monthly_paid_gross": round(monthly_paid_gross, 2),
|
|
"monthly_salary": round(monthly_salary, 2),
|
|
}
|
|
)
|
|
lawyer_loads.sort(key=lambda row: (-row["active_load"], row["name"] or "", row["email"] or ""))
|
|
|
|
if role == "LAWYER" and actor_uuid is not None:
|
|
scoped_by_status_rows = (
|
|
db.query(Request.status_code, func.count(Request.id))
|
|
.filter(Request.assigned_lawyer_id == str(actor_uuid))
|
|
.group_by(Request.status_code)
|
|
.all()
|
|
)
|
|
by_status = {status: int(count) for status, count in scoped_by_status_rows}
|
|
assigned_total = int(sum(by_status.values()))
|
|
active_assigned_total = int(
|
|
db.query(func.count(Request.id))
|
|
.filter(Request.assigned_lawyer_id == str(actor_uuid))
|
|
.filter(Request.status_code.notin_(terminal_codes))
|
|
.scalar()
|
|
or 0
|
|
)
|
|
unassigned_total = int(db.query(func.count(Request.id)).filter(Request.assigned_lawyer_id.is_(None)).scalar() or 0)
|
|
my_unread_updates = int(
|
|
db.query(func.count(Request.id))
|
|
.filter(
|
|
Request.assigned_lawyer_id == str(actor_uuid),
|
|
Request.lawyer_has_unread_updates.is_(True),
|
|
)
|
|
.scalar()
|
|
or 0
|
|
)
|
|
my_unread_by_event_rows = (
|
|
db.query(Request.lawyer_unread_event_type, func.count(Request.id))
|
|
.filter(
|
|
Request.assigned_lawyer_id == str(actor_uuid),
|
|
Request.lawyer_has_unread_updates.is_(True),
|
|
Request.lawyer_unread_event_type.is_not(None),
|
|
)
|
|
.group_by(Request.lawyer_unread_event_type)
|
|
.all()
|
|
)
|
|
my_unread_by_event = {str(event_type): int(count) for event_type, count in my_unread_by_event_rows if event_type}
|
|
scoped_lawyer_loads = [row for row in lawyer_loads if str(row["lawyer_id"]) == str(actor_uuid)]
|
|
elif role == "LAWYER":
|
|
by_status = {}
|
|
assigned_total = 0
|
|
active_assigned_total = 0
|
|
unassigned_total = int(db.query(func.count(Request.id)).filter(Request.assigned_lawyer_id.is_(None)).scalar() or 0)
|
|
my_unread_updates = 0
|
|
my_unread_by_event = {}
|
|
scoped_lawyer_loads = []
|
|
else:
|
|
scoped_by_status_rows = db.query(Request.status_code, func.count(Request.id)).group_by(Request.status_code).all()
|
|
by_status = {status: int(count) for status, count in scoped_by_status_rows}
|
|
assigned_total = int(
|
|
db.query(func.count(Request.id))
|
|
.filter(Request.assigned_lawyer_id.is_not(None))
|
|
.scalar()
|
|
or 0
|
|
)
|
|
active_assigned_total = int(
|
|
db.query(func.count(Request.id))
|
|
.filter(Request.assigned_lawyer_id.is_not(None))
|
|
.filter(Request.status_code.notin_(terminal_codes))
|
|
.scalar()
|
|
or 0
|
|
)
|
|
unassigned_total = int(db.query(func.count(Request.id)).filter(Request.assigned_lawyer_id.is_(None)).scalar() or 0)
|
|
my_unread_updates = 0
|
|
my_unread_by_event = {}
|
|
scoped_lawyer_loads = lawyer_loads
|
|
|
|
sla_snapshot = compute_sla_snapshot(db)
|
|
return {
|
|
"scope": role if role in {"ADMIN", "LAWYER"} else "ADMIN",
|
|
"new": int(by_status.get("NEW", 0)),
|
|
"by_status": by_status,
|
|
"assigned_total": assigned_total,
|
|
"active_assigned_total": active_assigned_total,
|
|
"unassigned_total": unassigned_total,
|
|
"my_unread_updates": my_unread_updates,
|
|
"my_unread_by_event": my_unread_by_event,
|
|
"frt_avg_minutes": sla_snapshot.get("frt_avg_minutes"),
|
|
"sla_overdue": sla_snapshot.get("overdue_total", 0),
|
|
"overdue_by_status": sla_snapshot.get("overdue_by_status", {}),
|
|
"overdue_by_transition": sla_snapshot.get("overdue_by_transition", {}),
|
|
"avg_time_in_status_hours": sla_snapshot.get("avg_time_in_status_hours", {}),
|
|
"unread_for_clients": int(unread_for_clients),
|
|
"unread_for_lawyers": int(unread_for_lawyers),
|
|
"lawyer_loads": scoped_lawyer_loads,
|
|
}
|