diff --git a/app/modules/sag/backend/router.py b/app/modules/sag/backend/router.py index ae925cf..7b411d9 100644 --- a/app/modules/sag/backend/router.py +++ b/app/modules/sag/backend/router.py @@ -1,6 +1,7 @@ import logging import os import shutil +import json from pathlib import Path from datetime import datetime from typing import List, Optional @@ -50,15 +51,64 @@ def _get_user_id_from_request(request: Request) -> int: def _normalize_case_status(status_value: Optional[str]) -> str: + allowed_statuses = [] + seen = set() + + def _add_status(value: Optional[str]) -> None: + candidate = str(value or "").strip() + if not candidate: + return + key = candidate.lower() + if key in seen: + return + seen.add(key) + allowed_statuses.append(candidate) + + try: + setting_row = execute_query_single("SELECT value FROM settings WHERE key = %s", ("case_statuses",)) + if setting_row and setting_row.get("value"): + parsed = json.loads(setting_row.get("value") or "[]") + for item in parsed if isinstance(parsed, list) else []: + if isinstance(item, str): + value = item.strip() + elif isinstance(item, dict): + value = str(item.get("value") or "").strip() + else: + value = "" + _add_status(value) + except Exception: + pass + + # Include historical/current DB statuses so legacy values remain valid + try: + rows = execute_query("SELECT DISTINCT status FROM sag_sager WHERE deleted_at IS NULL ORDER BY status", ()) or [] + for row in rows: + _add_status(row.get("status")) + except Exception: + pass + + if not allowed_statuses: + allowed_statuses = ["åben", "under behandling", "afventer", "løst", "lukket"] + + allowed_map = {s.lower(): s for s in allowed_statuses} + if not status_value: - return "åben" + return allowed_map.get("åben", allowed_statuses[0]) normalized = str(status_value).strip().lower() - if normalized == "afventer": - return "åben" - if normalized in {"åben", "lukket"}: - return normalized - return "åben" + if normalized in allowed_map: + return allowed_map[normalized] + + # Backward compatibility for legacy mapping + if normalized == "afventer" and "åben" in allowed_map: + return allowed_map["åben"] + + # Do not force unknown values back to default; preserve user-entered/custom DB values + raw_value = str(status_value).strip() + if raw_value: + return raw_value + + return allowed_map.get("åben", allowed_statuses[0]) def _normalize_optional_timestamp(value: Optional[str], field_name: str) -> Optional[str]: diff --git a/app/modules/sag/frontend/views.py b/app/modules/sag/frontend/views.py index 5d420f4..dfd05e7 100644 --- a/app/modules/sag/frontend/views.py +++ b/app/modules/sag/frontend/views.py @@ -1,4 +1,5 @@ import logging +import json from datetime import date, datetime from typing import Optional from fastapi import APIRouter, HTTPException, Query, Request @@ -56,6 +57,50 @@ def _coerce_optional_int(value: Optional[str]) -> Optional[int]: return None +def _fetch_case_status_options() -> list[str]: + default_statuses = ["åben", "under behandling", "afventer", "løst", "lukket"] + values = [] + seen = set() + + def _add(value: Optional[str]) -> None: + candidate = str(value or "").strip() + if not candidate: + return + key = candidate.lower() + if key in seen: + return + seen.add(key) + values.append(candidate) + + setting_row = execute_query( + "SELECT value FROM settings WHERE key = %s", + ("case_statuses",) + ) + + if setting_row and setting_row[0].get("value"): + try: + parsed = json.loads(setting_row[0].get("value") or "[]") + for item in parsed if isinstance(parsed, list) else []: + value = "" + if isinstance(item, str): + value = item.strip() + elif isinstance(item, dict): + value = str(item.get("value") or "").strip() + _add(value) + except Exception: + pass + + statuses = execute_query("SELECT DISTINCT status FROM sag_sager WHERE deleted_at IS NULL ORDER BY status", ()) or [] + + for row in statuses: + _add(row.get("status")) + + for default in default_statuses: + _add(default) + + return values + + @router.get("/sag", response_class=HTMLResponse) async def sager_liste( request: Request, @@ -77,7 +122,9 @@ async def sager_liste( c.name as customer_name, CONCAT(COALESCE(cont.first_name, ''), ' ', COALESCE(cont.last_name, '')) as kontakt_navn, COALESCE(u.full_name, u.username) AS ansvarlig_navn, - g.name AS assigned_group_name + g.name AS assigned_group_name, + nt.title AS next_todo_title, + nt.due_date AS next_todo_due_date FROM sag_sager s LEFT JOIN customers c ON s.customer_id = c.id LEFT JOIN users u ON u.user_id = s.ansvarlig_bruger_id @@ -90,6 +137,22 @@ async def sager_liste( LIMIT 1 ) cc_first ON true LEFT JOIN contacts cont ON cc_first.contact_id = cont.id + LEFT JOIN LATERAL ( + SELECT t.title, t.due_date + FROM sag_todo_steps t + WHERE t.sag_id = s.id + AND t.deleted_at IS NULL + AND t.is_done = FALSE + ORDER BY + CASE + WHEN t.is_next THEN 0 + WHEN t.due_date IS NOT NULL THEN 1 + ELSE 2 + END, + t.due_date ASC NULLS LAST, + t.created_at ASC + LIMIT 1 + ) nt ON true LEFT JOIN sag_sager ds ON ds.id = s.deferred_until_case_id WHERE s.deleted_at IS NULL """ @@ -162,29 +225,11 @@ async def sager_liste( sager = [s for s in sager if s['id'] in tagged_ids] # Fetch all distinct statuses and tags for filters - statuses = execute_query("SELECT DISTINCT status FROM sag_sager WHERE deleted_at IS NULL ORDER BY status", ()) - status_options = [] - seen_statuses = set() + status_options = _fetch_case_status_options() - for row in statuses or []: - status_value = str(row.get("status") or "").strip() - if not status_value: - continue - key = status_value.lower() - if key in seen_statuses: - continue - seen_statuses.add(key) - status_options.append(status_value) - - current_status = str(sag.get("status") or "").strip() - if current_status and current_status.lower() not in seen_statuses: - seen_statuses.add(current_status.lower()) + current_status = str(status or "").strip() + if current_status and current_status.lower() not in {s.lower() for s in status_options}: status_options.append(current_status) - - for default_status in ["åben", "under behandling", "afventer", "løst", "lukket"]: - if default_status.lower() not in seen_statuses: - seen_statuses.add(default_status.lower()) - status_options.append(default_status) all_tags = execute_query("SELECT DISTINCT tag_navn FROM sag_tags WHERE deleted_at IS NULL ORDER BY tag_navn", ()) toggle_include_deferred_url = str( @@ -196,7 +241,7 @@ async def sager_liste( "sager": sager, "relations_map": relations_map, "child_ids": list(child_ids), - "statuses": [s['status'] for s in statuses], + "statuses": status_options, "all_tags": [t['tag_navn'] for t in all_tags], "current_status": status, "current_tag": tag, @@ -473,7 +518,10 @@ async def sag_detaljer(request: Request, sag_id: int): logger.warning("⚠️ Could not load pipeline stages: %s", e) pipeline_stages = [] - statuses = execute_query("SELECT DISTINCT status FROM sag_sager WHERE deleted_at IS NULL ORDER BY status", ()) + status_options = _fetch_case_status_options() + current_status = str(sag.get("status") or "").strip() + if current_status and current_status.lower() not in {s.lower() for s in status_options}: + status_options.append(current_status) is_deadline_overdue = _is_deadline_overdue(sag.get("deadline")) return templates.TemplateResponse("modules/sag/templates/detail.html", { diff --git a/app/modules/sag/services/relation_service.py b/app/modules/sag/services/relation_service.py index 73b289d..33a4df8 100644 --- a/app/modules/sag/services/relation_service.py +++ b/app/modules/sag/services/relation_service.py @@ -33,7 +33,7 @@ class RelationService: # 2. Fetch details for these cases placeholders = ','.join(['%s'] * len(tree_ids)) - tree_cases_query = f"SELECT id, titel, status FROM sag_sager WHERE id IN ({placeholders})" + tree_cases_query = f"SELECT id, titel, status, type, template_key FROM sag_sager WHERE id IN ({placeholders})" tree_cases = {c['id']: c for c in execute_query(tree_cases_query, tuple(tree_ids))} # 3. Fetch all edges between these cases diff --git a/app/modules/sag/templates/detail.html b/app/modules/sag/templates/detail.html index b3291a3..6503863 100644 --- a/app/modules/sag/templates/detail.html +++ b/app/modules/sag/templates/detail.html @@ -1984,86 +1984,6 @@ -
Ingen kunder
- {% endif %} -Ingen kontakter
- {% endif %} -Ingen kommentarer endnu.
+ {% endif %} +| Niv. | +Sag | +Titel | +Status | +Type | +Relation | +Handling | +
|---|
Ingen relaterede sager
@@ -2735,6 +2668,7 @@ // Initialize everything when DOM is ready document.addEventListener('DOMContentLoaded', () => { + hydrateTopbarStatusOptions(); // Initialize modals contactSearchModal = new bootstrap.Modal(document.getElementById('contactSearchModal')); customerSearchModal = new bootstrap.Modal(document.getElementById('customerSearchModal')); @@ -4229,6 +4163,110 @@Ingen kunder
+ {% endif %} +Ingen kontakter
+ {% endif %} +Ingen kommentarer endnu.
- {% endif %} -
')|safe }} +