From 3387f6c475432bea18b28263e47c12cb9e832177 Mon Sep 17 00:00:00 2001 From: Christian Date: Sat, 3 Oct 2026 14:06:51 +0200 Subject: [PATCH] feat: enhance solution management with customer selection and improve call logging URLs - Added customer search functionality in the solution management editor, allowing users to select a customer. - Updated the API endpoint for incoming call logging to use the local recipient extension. - Implemented a new case number navigation feature in the global search. - Modified case creation logic to include relations and adjust module visibility. - Added tests for new features including customer selection in solutions and call logging URL changes. --- app/dashboard/backend/mission_router.py | 2 +- app/dashboard/backend/mission_service.py | 11 +- app/modules/locations/backend/router.py | 20 +- app/modules/locations/frontend/views.py | 24 + app/modules/locations/templates/detail.html | 39 ++ app/modules/planner/backend/router.py | 17 +- app/modules/sag/backend/router.py | 121 ++++ app/modules/sag/backend/solutions.py | 2 +- app/modules/sag/frontend/views.py | 9 +- app/modules/sag/templates/create.html | 21 +- app/modules/sag/templates/detail_v4.html | 555 ++++++++++++++++-- app/modules/sag/templates/detail_v5.html | 41 ++ .../sag/templates/solutions_management.html | 11 +- app/settings/frontend/settings.html | 2 +- app/shared/frontend/base.html | 16 + design_prototypes/sag_v5/arbejdsrum.html | 456 ++++++++++++++ design_prototypes/sag_v5/journal.html | 470 +++++++++++++++ design_prototypes/sag_v5/sagsbord.html | 449 ++++++++++++++ migrations/1053_sag_internal_comments.sql | 4 + static/css/case-v5.css | 49 ++ static/js/case-create.js | 13 +- static/js/case-v5.js | 269 +++++++++ tests/test_mission_call_state.py | 58 ++ tests/test_planner.py | 2 + tests/test_sag_module.py | 78 ++- tests/test_sag_solution_knowledge.py | 9 + tests/test_sag_v5.py | 163 +++++ tests/test_telefoni_call_logging.py | 2 +- 28 files changed, 2820 insertions(+), 93 deletions(-) create mode 100644 app/modules/sag/templates/detail_v5.html create mode 100644 design_prototypes/sag_v5/arbejdsrum.html create mode 100644 design_prototypes/sag_v5/journal.html create mode 100644 design_prototypes/sag_v5/sagsbord.html create mode 100644 migrations/1053_sag_internal_comments.sql create mode 100644 static/css/case-v5.css create mode 100644 static/js/case-v5.js create mode 100644 tests/test_mission_call_state.py create mode 100644 tests/test_sag_v5.py diff --git a/app/dashboard/backend/mission_router.py b/app/dashboard/backend/mission_router.py index 950e4fb..9469b5f 100644 --- a/app/dashboard/backend/mission_router.py +++ b/app/dashboard/backend/mission_router.py @@ -752,7 +752,7 @@ async def mission_telefoni_ringing(event: MissionCallEvent, request: Request, to state = 'ringing', ended_at = NULL, answered_at = NULL, - started_at = LEAST(mission_call_state.started_at, EXCLUDED.started_at), + started_at = EXCLUDED.started_at, updated_at = NOW(), last_payload = jsonb_set( EXCLUDED.last_payload, diff --git a/app/dashboard/backend/mission_service.py b/app/dashboard/backend/mission_service.py index 248ecb2..e46d596 100644 --- a/app/dashboard/backend/mission_service.py +++ b/app/dashboard/backend/mission_service.py @@ -27,9 +27,9 @@ class MissionService: @staticmethod def get_ring_timeout_seconds() -> int: # A Yealink "Incoming call" event has no matching event when the - # caller abandons the call. Keep it briefly, then remove it from the - # live boards so stale ringing phones cannot remain visible. - return 30 + # caller abandons the call. Phones may not emit another event while + # ringing, so allow a full ring window before clearing a stale entry. + return 120 @staticmethod def expire_stale_ringing_calls() -> None: @@ -44,7 +44,7 @@ class MissionService: ended_at = COALESCE(ended_at, NOW()), updated_at = NOW() WHERE state = 'ringing' - AND started_at < (NOW() - make_interval(secs => %s)) + AND updated_at < (NOW() - make_interval(secs => %s)) """, (timeout_seconds,), ) @@ -211,7 +211,6 @@ class MissionService: logger.warning("Mission table missing: mission_call_state (active calls unavailable)") return [] - 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, @@ -1475,7 +1474,7 @@ class MissionService: return { "kpis": MissionService._safe("kpis", MissionService.get_kpis, kpis_default), - "active_calls": MissionService._safe("active_calls", MissionService.get_active_calls, []), + "active_calls": MissionService._safe("active_calls", MissionService.get_active_calls, None), "employee_deadlines": MissionService._safe("employee_deadlines", MissionService.get_employee_deadlines, []), "active_alerts": MissionService._safe("active_alerts", MissionService.get_active_alerts, []), "live_feed": MissionService._safe("live_feed", lambda: MissionService.get_live_feed(20), []), diff --git a/app/modules/locations/backend/router.py b/app/modules/locations/backend/router.py index ff27b61..fc8bb5b 100644 --- a/app/modules/locations/backend/router.py +++ b/app/modules/locations/backend/router.py @@ -1546,7 +1546,8 @@ async def get_location_audit( @router.get("/locations/search", response_model=LocationSearchResponse) async def search_locations( q: str = Query(..., min_length=1, max_length=255, description="Search query (name or address)"), - limit: int = Query(10, ge=1, le=100) + limit: int = Query(10, ge=1, le=100), + customer_id: Optional[int] = Query(None, ge=1), ): """ Search for locations by name or address. @@ -1562,31 +1563,38 @@ async def search_locations( search_term = f"%{q}%" # Search locations_locations (case-insensitive match on name, street, city) - search_query = """ + customer_filter = " AND customer_id = %s" if customer_id is not None else "" + search_query = f""" SELECT * FROM locations_locations WHERE deleted_at IS NULL + {customer_filter} AND ( name ILIKE %s OR address_street ILIKE %s + OR address_postal_code ILIKE %s OR address_city ILIKE %s ) ORDER BY name ASC LIMIT %s """ - - results = execute_query(search_query, (search_term, search_term, search_term, limit)) + + params = ([customer_id] if customer_id is not None else []) + [search_term] * 4 + [limit] + results = execute_query(search_query, tuple(params)) # Get total count (without limit) for the search - count_query = """ + count_query = f""" SELECT COUNT(*) as total FROM locations_locations WHERE deleted_at IS NULL + {customer_filter} AND ( name ILIKE %s OR address_street ILIKE %s + OR address_postal_code ILIKE %s OR address_city ILIKE %s ) """ - count_result = execute_query(count_query, (search_term, search_term, search_term)) + count_params = ([customer_id] if customer_id is not None else []) + [search_term] * 4 + count_result = execute_query(count_query, tuple(count_params)) total_count = count_result[0]['total'] if count_result else 0 locations = [Location(**row) for row in results] diff --git a/app/modules/locations/frontend/views.py b/app/modules/locations/frontend/views.py index 5845804..6605e0a 100644 --- a/app/modules/locations/frontend/views.py +++ b/app/modules/locations/frontend/views.py @@ -588,6 +588,29 @@ def detail_location_view(id: int = Path(..., gt=0)): location = location[0] # Get first result + related_cases = execute_query( + """ + WITH RECURSIVE location_tree AS ( + SELECT id, parent_location_id, ARRAY[id] AS path + FROM locations_locations + WHERE id = %s AND deleted_at IS NULL + UNION ALL + SELECT child.id, child.parent_location_id, parent.path || child.id + FROM locations_locations child + JOIN location_tree parent ON child.parent_location_id = parent.id + WHERE child.deleted_at IS NULL AND NOT child.id = ANY(parent.path) + ) + SELECT DISTINCT s.id, s.titel, s.status, s.template_key, s.updated_at, + s.customer_id, ll.id AS matched_location_id, ll.name AS matched_location_name + FROM location_tree tree + JOIN sag_lokationer sl ON sl.location_id = tree.id AND sl.deleted_at IS NULL + JOIN sag_sager s ON s.id = sl.sag_id AND s.deleted_at IS NULL + JOIN locations_locations ll ON ll.id = tree.id + ORDER BY s.updated_at DESC NULLS LAST, s.id DESC + """, + (id,), + ) or [] + hierarchy = [] current_parent_id = location.get("parent_location_id") while current_parent_id: @@ -907,6 +930,7 @@ def detail_location_view(id: int = Path(..., gt=0)): location["hierarchy"] = hierarchy location["children"] = children location["relation_tree"] = relation_tree + location["related_cases"] = related_cases location["direct_child_nodes"] = direct_child_nodes location["related_location_count"] = max(len(relation_rows) - 1, 0) location["same_customer_locations"] = same_customer_locations diff --git a/app/modules/locations/templates/detail.html b/app/modules/locations/templates/detail.html index 87c3edc..a9c99b1 100644 --- a/app/modules/locations/templates/detail.html +++ b/app/modules/locations/templates/detail.html @@ -506,6 +506,12 @@ Relationer +