import json from typing import Any, Dict, Optional from app.core.database import execute_query, execute_query_single class MissionService: @staticmethod def get_ring_timeout_seconds() -> int: raw = MissionService.get_setting_value("mission_call_ring_timeout_seconds", "180") or "180" try: value = int(raw) except (TypeError, ValueError): value = 180 return max(30, min(value, 3600)) @staticmethod def expire_stale_ringing_calls() -> None: timeout_seconds = MissionService.get_ring_timeout_seconds() execute_query( """ UPDATE mission_call_state SET state = 'hangup', ended_at = COALESCE(ended_at, NOW()), updated_at = NOW() WHERE state = 'ringing' AND started_at < (NOW() - make_interval(secs => %s)) """, (timeout_seconds,), ) @staticmethod def get_setting_value(key: str, default: Optional[str] = None) -> Optional[str]: row = execute_query_single("SELECT value FROM settings WHERE key = %s", (key,)) if not row: return default value = row.get("value") if value is None or value == "": return default return str(value) @staticmethod def parse_json_setting(key: str, default: Any) -> Any: raw = MissionService.get_setting_value(key, None) if raw is None: return default try: return json.loads(raw) except Exception: return default @staticmethod def build_alert_key(service_name: str, customer_name: Optional[str]) -> str: customer_part = (customer_name or "").strip().lower() or "global" return f"{service_name.strip().lower()}::{customer_part}" @staticmethod def resolve_contact_context(caller_number: Optional[str]) -> Dict[str, Optional[str]]: if not caller_number: return {"contact_name": None, "company_name": None, "customer_tag": None} query = """ SELECT c.id, c.first_name, c.last_name, ( SELECT cu.name FROM contact_companies cc JOIN customers cu ON cu.id = cc.customer_id WHERE cc.contact_id = c.id ORDER BY cc.is_primary DESC NULLS LAST, cc.id ASC LIMIT 1 ) AS company_name, ( SELECT t.name FROM entity_tags et JOIN tags t ON t.id = et.tag_id WHERE et.entity_type = 'contact' AND et.entity_id = c.id AND LOWER(t.name) IN ('vip', 'serviceaftale', 'service agreement') ORDER BY t.name LIMIT 1 ) AS customer_tag FROM contacts c WHERE RIGHT(regexp_replace(COALESCE(c.phone, ''), '\\D', '', 'g'), 8) = RIGHT(regexp_replace(%s, '\\D', '', 'g'), 8) OR RIGHT(regexp_replace(COALESCE(c.mobile, ''), '\\D', '', 'g'), 8) = RIGHT(regexp_replace(%s, '\\D', '', 'g'), 8) LIMIT 1 """ row = execute_query_single(query, (caller_number, caller_number)) if not row: return {"contact_name": None, "company_name": None, "customer_tag": None} contact_name = f"{(row.get('first_name') or '').strip()} {(row.get('last_name') or '').strip()}".strip() or None return { "contact_name": contact_name, "company_name": row.get("company_name"), "customer_tag": row.get("customer_tag"), } @staticmethod def insert_event( *, event_type: str, title: str, severity: str = "info", source: Optional[str] = None, customer_name: Optional[str] = None, payload: Optional[Dict[str, Any]] = None, ) -> Dict[str, Any]: rows = execute_query( """ INSERT INTO mission_events (event_type, severity, title, source, customer_name, payload) VALUES (%s, %s, %s, %s, %s, %s::jsonb) RETURNING id, event_type, severity, title, source, customer_name, payload, created_at """, (event_type, severity, title, source, customer_name, json.dumps(payload or {})), ) return rows[0] if rows else {} @staticmethod def get_kpis() -> Dict[str, int]: query = """ SELECT COUNT(*) FILTER (WHERE deleted_at IS NULL AND LOWER(status) <> 'afsluttet') AS open_cases, COUNT(*) FILTER (WHERE deleted_at IS NULL AND LOWER(status) = 'åben' AND ansvarlig_bruger_id IS NULL) AS new_cases, COUNT(*) FILTER (WHERE deleted_at IS NULL AND LOWER(status) <> 'afsluttet' AND ansvarlig_bruger_id IS NULL) AS unassigned_cases, COUNT(*) FILTER (WHERE deleted_at IS NULL AND LOWER(status) <> 'afsluttet' AND deadline IS NOT NULL AND deadline::date = CURRENT_DATE) AS deadlines_today, COUNT(*) FILTER (WHERE deleted_at IS NULL AND LOWER(status) <> 'afsluttet' AND deadline IS NOT NULL AND deadline::date < CURRENT_DATE) AS overdue_deadlines FROM sag_sager """ row = execute_query_single(query) or {} return { "open_cases": int(row.get("open_cases") or 0), "new_cases": int(row.get("new_cases") or 0), "unassigned_cases": int(row.get("unassigned_cases") or 0), "deadlines_today": int(row.get("deadlines_today") or 0), "overdue_deadlines": int(row.get("overdue_deadlines") or 0), } @staticmethod def get_employee_deadlines() -> list[Dict[str, Any]]: rows = execute_query( """ SELECT COALESCE(u.full_name, u.username, 'Ukendt') AS employee_name, COUNT(*) FILTER (WHERE s.deadline::date = CURRENT_DATE) AS deadlines_today, COUNT(*) FILTER (WHERE s.deadline::date < CURRENT_DATE) AS overdue_deadlines FROM sag_sager s LEFT JOIN users u ON u.user_id = s.ansvarlig_bruger_id WHERE s.deleted_at IS NULL AND LOWER(s.status) <> 'afsluttet' AND s.deadline IS NOT NULL GROUP BY COALESCE(u.full_name, u.username, 'Ukendt') HAVING COUNT(*) FILTER (WHERE s.deadline::date = CURRENT_DATE) > 0 OR COUNT(*) FILTER (WHERE s.deadline::date < CURRENT_DATE) > 0 ORDER BY overdue_deadlines DESC, deadlines_today DESC, employee_name ASC """ ) or [] return [ { "employee_name": row.get("employee_name"), "deadlines_today": int(row.get("deadlines_today") or 0), "overdue_deadlines": int(row.get("overdue_deadlines") or 0), } for row in rows ] @staticmethod def get_active_calls() -> list[Dict[str, Any]]: MissionService.expire_stale_ringing_calls() rows = execute_query( """ SELECT call_id, queue_name, caller_number, contact_name, company_name, customer_tag, state, started_at, answered_at, ended_at, updated_at FROM mission_call_state WHERE state = 'ringing' ORDER BY started_at DESC """ ) return rows or [] @staticmethod def get_active_alerts() -> list[Dict[str, Any]]: rows = execute_query( """ SELECT alert_key, service_name, customer_name, status, is_active, started_at, resolved_at, updated_at FROM mission_uptime_alerts WHERE is_active = TRUE ORDER BY started_at ASC NULLS LAST """ ) return rows or [] @staticmethod def get_live_feed(limit: int = 20) -> list[Dict[str, Any]]: rows = execute_query( """ SELECT id, event_type, severity, title, source, customer_name, payload, created_at FROM mission_events ORDER BY created_at DESC LIMIT %s """, (limit,), ) return rows or [] @staticmethod def get_state() -> Dict[str, Any]: return { "kpis": MissionService.get_kpis(), "active_calls": MissionService.get_active_calls(), "employee_deadlines": MissionService.get_employee_deadlines(), "active_alerts": MissionService.get_active_alerts(), "live_feed": MissionService.get_live_feed(20), "config": { "display_queues": MissionService.parse_json_setting("mission_display_queues", []), "sound_enabled": str(MissionService.get_setting_value("mission_sound_enabled", "true")).lower() == "true", "sound_volume": int(MissionService.get_setting_value("mission_sound_volume", "70") or 70), "sound_events": MissionService.parse_json_setting("mission_sound_events", ["incoming_call", "uptime_down", "critical_event"]), "kpi_visible": MissionService.parse_json_setting( "mission_kpi_visible", ["open_cases", "new_cases", "unassigned_cases", "deadlines_today", "overdue_deadlines"], ), "customer_filter": MissionService.get_setting_value("mission_customer_filter", "") or "", }, }