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 @@ -
-
-
🏢 Kunder
- -
-
- {% if customers %} -
- Navn - Rolle - E-mail - Slet -
- {% for customer in customers %} -
-
- - {{ customer.customer_name }} - -
- {{ customer.role or '-' }} - {{ customer.customer_email or '-' }} - -
- {% endfor %} - {% else %} -

Ingen kunder

- {% endif %} -
-
-
-
-
👥 Kontakter
- -
-
- {% if contacts %} -
- Navn - Titel - Kunde - Slet -
- {% for contact in contacts %} -
-
{{ contact.contact_name }}
- {{ contact.title or '-' }} - {{ contact.customer_name or '-' }} - -
- {% endfor %} - {% else %} -

Ingen kontakter

- {% endif %} -
-
@@ -2153,6 +2073,46 @@
+ +
+
+
+ Kommentarer +
+ {{ comments|length if comments else 0 }} +
+ +
+ {% if comments %} + {% for comment in comments %} +
+
+
+ {{ comment.forfatter }} + {{ comment.created_at.strftime('%d/%m-%Y %H:%M') }} +
+
+ {{ comment.indhold|replace('\n', '
')|safe }} +
+
+
+ {% endfor %} + {% else %} +

Ingen kommentarer endnu.

+ {% endif %} +
+ +
+
+
+ + +
+
+
+
@@ -2237,104 +2197,77 @@
- {% macro render_tree(nodes) %} - {% endmacro %} - {# Only show tree when there is more than the lone current case #} {% set has_relations = relation_tree and (relation_tree|length > 1 or (relation_tree|length == 1 and relation_tree[0].children)) %} {% if has_relations %} -
- {{ render_tree(relation_tree) }} +
+ + + + + + + + + + + + + + {{ render_relation_rows(relation_tree) }} + +
Niv.SagTitelStatusTypeRelationHandling
{% else %}

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 @@
+
+
+
🏷️ TAGS
+ +
+
+
+
Indlaeser tags...
+
+
+
Forslag (brand/type)
+
+
Indlaeser forslag...
+
+
+
+
+ +
+
+
🏢 Kunder
+ +
+
+ {% if customers %} +
+ Navn + Rolle + E-mail + Slet +
+ {% for customer in customers %} +
+ + {{ customer.role or '-' }} + {{ customer.customer_email or '-' }} + +
+ {% endfor %} + {% else %} +

Ingen kunder

+ {% endif %} +
+
+ +
+
+
👥 Kontakter
+ +
+
+ {% if contacts %} +
+ Navn + Titel + Kunde + Slet +
+ {% for contact in contacts %} +
+
{{ contact.contact_name }}
+ {{ contact.title or '-' }} + {{ contact.customer_name or '-' }} + +
+ {% endfor %} + {% else %} +

Ingen kontakter

+ {% endif %} +
+
+
💻 Hardware
@@ -4410,34 +4448,6 @@
-
-
-
🏷️ TAGS
- -
-
-
-
Indlaeser tags...
-
-
-
Forslag (brand/type)
-
-
Indlaeser forslag...
-
-
-
-
- - - - - - -
Kunde-wiki
@@ -5207,47 +5217,6 @@
- -
-
-
-
-
Kommentarer
- {{ comments|length if comments else 0 }} -
-
- {% if comments %} - {% for comment in comments %} -
-
-
- {{ comment.forfatter }} - {{ comment.created_at.strftime('%d/%m-%Y %H:%M') }} -
-
- {{ comment.indhold|replace('\n', '
')|safe }} -
-
-
- {% endfor %} - {% else %} -

Ingen kommentarer endnu.

- {% endif %} -
- -
-
-
- +{% block extra_js %} - - +{% endblock %}