- Updated the index.html template to include a new column for "Næste todo" in the sag table. - Added new JavaScript functions to load and manage case statuses in settings.html, including normalization and rendering of statuses. - Introduced a new tag search feature in tags_admin.html, allowing users to filter tags by name, type, and module with pagination support. - Enhanced the backend router.py to include a new endpoint for listing tag usage across modules with server-side filtering and pagination. - Improved the overall UI and UX of the tag administration page, including responsive design adjustments and better error handling.
581 lines
22 KiB
Python
581 lines
22 KiB
Python
import logging
|
|
import json
|
|
from datetime import date, datetime
|
|
from typing import Optional
|
|
from fastapi import APIRouter, HTTPException, Query, Request
|
|
from fastapi.responses import HTMLResponse
|
|
from fastapi.templating import Jinja2Templates
|
|
from pathlib import Path
|
|
from app.core.database import execute_query
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter()
|
|
|
|
|
|
def _is_deadline_overdue(deadline_value) -> bool:
|
|
if not deadline_value:
|
|
return False
|
|
if isinstance(deadline_value, datetime):
|
|
return deadline_value.date() < date.today()
|
|
if isinstance(deadline_value, date):
|
|
return deadline_value < date.today()
|
|
return False
|
|
|
|
# Setup template directory
|
|
templates = Jinja2Templates(directory="app")
|
|
|
|
|
|
def _fetch_assignment_users():
|
|
return execute_query(
|
|
"""
|
|
SELECT user_id, COALESCE(full_name, username) AS display_name
|
|
FROM users
|
|
ORDER BY display_name
|
|
""",
|
|
()
|
|
) or []
|
|
|
|
|
|
def _fetch_assignment_groups():
|
|
return execute_query(
|
|
"""
|
|
SELECT id, name
|
|
FROM groups
|
|
ORDER BY name
|
|
""",
|
|
()
|
|
) or []
|
|
|
|
|
|
def _coerce_optional_int(value: Optional[str]) -> Optional[int]:
|
|
"""Convert empty strings and None to None, otherwise parse as int."""
|
|
if value is None or value == "":
|
|
return None
|
|
try:
|
|
return int(value)
|
|
except (TypeError, ValueError):
|
|
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,
|
|
status: str = Query(None),
|
|
tag: str = Query(None),
|
|
customer_id: str = Query(None),
|
|
ansvarlig_bruger_id: str = Query(None),
|
|
assigned_group_id: str = Query(None),
|
|
include_deferred: bool = Query(False),
|
|
):
|
|
"""Display list of all cases."""
|
|
try:
|
|
# Coerce string params to optional ints
|
|
customer_id_int = _coerce_optional_int(customer_id)
|
|
ansvarlig_bruger_id_int = _coerce_optional_int(ansvarlig_bruger_id)
|
|
assigned_group_id_int = _coerce_optional_int(assigned_group_id)
|
|
query = """
|
|
SELECT s.*,
|
|
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,
|
|
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
|
|
LEFT JOIN groups g ON g.id = s.assigned_group_id
|
|
LEFT JOIN LATERAL (
|
|
SELECT cc.contact_id
|
|
FROM contact_companies cc
|
|
WHERE cc.customer_id = c.id
|
|
ORDER BY cc.is_primary DESC NULLS LAST, cc.id ASC
|
|
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
|
|
"""
|
|
params = []
|
|
|
|
if not include_deferred:
|
|
query += " AND ("
|
|
query += "s.deferred_until IS NULL"
|
|
query += " OR s.deferred_until <= NOW()"
|
|
query += " OR (s.deferred_until_case_id IS NOT NULL AND s.deferred_until_status IS NOT NULL AND ds.status = s.deferred_until_status)"
|
|
query += ")"
|
|
|
|
if status:
|
|
query += " AND s.status = %s"
|
|
params.append(status)
|
|
if customer_id_int:
|
|
query += " AND s.customer_id = %s"
|
|
params.append(customer_id_int)
|
|
if ansvarlig_bruger_id_int:
|
|
query += " AND s.ansvarlig_bruger_id = %s"
|
|
params.append(ansvarlig_bruger_id_int)
|
|
if assigned_group_id_int:
|
|
query += " AND s.assigned_group_id = %s"
|
|
params.append(assigned_group_id_int)
|
|
|
|
query += " ORDER BY s.created_at DESC"
|
|
sager = execute_query(query, tuple(params))
|
|
|
|
# Fetch relations for all cases
|
|
relations_query = """
|
|
SELECT
|
|
sr.kilde_sag_id,
|
|
sr.målsag_id,
|
|
sr.relationstype,
|
|
sr.id as relation_id
|
|
FROM sag_relationer sr
|
|
WHERE sr.deleted_at IS NULL
|
|
"""
|
|
all_relations = execute_query(relations_query, ())
|
|
child_ids = set()
|
|
|
|
# Build relations map: {sag_id: [list of related sag_ids]}
|
|
relations_map = {}
|
|
for rel in all_relations or []:
|
|
if rel.get('målsag_id') is not None:
|
|
child_ids.add(rel['målsag_id'])
|
|
# Add forward relation
|
|
if rel['kilde_sag_id'] not in relations_map:
|
|
relations_map[rel['kilde_sag_id']] = []
|
|
relations_map[rel['kilde_sag_id']].append({
|
|
'target_id': rel['målsag_id'],
|
|
'type': rel['relationstype'],
|
|
'direction': 'forward'
|
|
})
|
|
# Add backward relation
|
|
if rel['målsag_id'] not in relations_map:
|
|
relations_map[rel['målsag_id']] = []
|
|
relations_map[rel['målsag_id']].append({
|
|
'target_id': rel['kilde_sag_id'],
|
|
'type': rel['relationstype'],
|
|
'direction': 'backward'
|
|
})
|
|
|
|
# Filter by tag if provided
|
|
if tag and sager:
|
|
sag_ids = [s['id'] for s in sager]
|
|
tag_query = "SELECT sag_id FROM sag_tags WHERE tag_navn = %s AND deleted_at IS NULL"
|
|
tagged = execute_query(tag_query, (tag,))
|
|
tagged_ids = set(t['sag_id'] for t in tagged)
|
|
sager = [s for s in sager if s['id'] in tagged_ids]
|
|
|
|
# Fetch all distinct statuses and tags for filters
|
|
status_options = _fetch_case_status_options()
|
|
|
|
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)
|
|
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(
|
|
request.url.include_query_params(include_deferred="0" if include_deferred else "1")
|
|
)
|
|
|
|
return templates.TemplateResponse("modules/sag/templates/index.html", {
|
|
"request": request,
|
|
"sager": sager,
|
|
"relations_map": relations_map,
|
|
"child_ids": list(child_ids),
|
|
"statuses": status_options,
|
|
"all_tags": [t['tag_navn'] for t in all_tags],
|
|
"current_status": status,
|
|
"current_tag": tag,
|
|
"include_deferred": include_deferred,
|
|
"toggle_include_deferred_url": toggle_include_deferred_url,
|
|
"assignment_users": _fetch_assignment_users(),
|
|
"assignment_groups": _fetch_assignment_groups(),
|
|
"current_ansvarlig_bruger_id": ansvarlig_bruger_id_int,
|
|
"current_assigned_group_id": assigned_group_id_int,
|
|
})
|
|
except Exception as e:
|
|
logger.error("❌ Error displaying case list: %s", e)
|
|
raise HTTPException(status_code=500, detail="Failed to load case list")
|
|
|
|
@router.get("/sag/new", response_class=HTMLResponse)
|
|
async def opret_sag_side(request: Request):
|
|
"""Show create case form."""
|
|
return templates.TemplateResponse("modules/sag/templates/create.html", {
|
|
"request": request,
|
|
"assignment_users": _fetch_assignment_users(),
|
|
"assignment_groups": _fetch_assignment_groups(),
|
|
})
|
|
|
|
@router.get("/sag/varekob-salg", response_class=HTMLResponse)
|
|
async def sag_varekob_salg(request: Request):
|
|
"""Display orders overview for all purchases and sales."""
|
|
return templates.TemplateResponse("modules/sag/templates/varekob_salg.html", {
|
|
"request": request,
|
|
})
|
|
|
|
@router.get("/sag/{sag_id}", response_class=HTMLResponse)
|
|
async def sag_detaljer(request: Request, sag_id: int):
|
|
"""Display case details."""
|
|
try:
|
|
# Fetch main case
|
|
sag_query = """
|
|
SELECT s.*,
|
|
COALESCE(u.full_name, u.username) AS ansvarlig_navn,
|
|
g.name AS assigned_group_name
|
|
FROM sag_sager s
|
|
LEFT JOIN users u ON u.user_id = s.ansvarlig_bruger_id
|
|
LEFT JOIN groups g ON g.id = s.assigned_group_id
|
|
WHERE s.id = %s AND s.deleted_at IS NULL
|
|
"""
|
|
sag_result = execute_query(sag_query, (sag_id,))
|
|
|
|
if not sag_result:
|
|
raise HTTPException(status_code=404, detail="Case not found")
|
|
|
|
sag = sag_result[0]
|
|
|
|
# Fetch tags (Support both Legacy sag_tags and New entity_tags)
|
|
# First try the new system (entity_tags) which the valid frontend uses
|
|
tags_query = """
|
|
SELECT t.name as tag_navn
|
|
FROM tags t
|
|
JOIN entity_tags et ON t.id = et.tag_id
|
|
WHERE et.entity_type = 'case' AND et.entity_id = %s
|
|
"""
|
|
tags = execute_query(tags_query, (sag_id,))
|
|
|
|
# If empty, try legacy table fallback
|
|
if not tags:
|
|
tags_query_legacy = "SELECT * FROM sag_tags WHERE sag_id = %s AND deleted_at IS NULL ORDER BY created_at DESC"
|
|
tags = execute_query(tags_query_legacy, (sag_id,))
|
|
|
|
# Fetch relations
|
|
relationer_query = """
|
|
SELECT sr.*,
|
|
ss_kilde.titel as kilde_titel,
|
|
ss_mål.titel as mål_titel
|
|
FROM sag_relationer sr
|
|
JOIN sag_sager ss_kilde ON sr.kilde_sag_id = ss_kilde.id
|
|
JOIN sag_sager ss_mål ON sr.målsag_id = ss_mål.id
|
|
WHERE (sr.kilde_sag_id = %s OR sr.målsag_id = %s)
|
|
AND sr.deleted_at IS NULL
|
|
ORDER BY sr.created_at DESC
|
|
"""
|
|
relationer = execute_query(relationer_query, (sag_id, sag_id))
|
|
|
|
# --- Relation Tree Construction ---
|
|
relation_tree = []
|
|
try:
|
|
from app.modules.sag.services.relation_service import RelationService
|
|
relation_tree = RelationService.get_relation_tree(sag_id)
|
|
except Exception as e:
|
|
logger.error(f"Error building relation tree: {e}")
|
|
relation_tree = []
|
|
except Exception as e:
|
|
logger.error(f"Error building relation tree: {e}")
|
|
relation_tree = []
|
|
|
|
# Fetch customer info if customer_id exists
|
|
customer = None
|
|
hovedkontakt = None
|
|
if sag.get('customer_id'):
|
|
customer_query = "SELECT * FROM customers WHERE id = %s"
|
|
customer_result = execute_query(customer_query, (sag['customer_id'],))
|
|
if customer_result:
|
|
customer = customer_result[0]
|
|
|
|
# Fetch hovedkontakt (primary contact) for case via sag_kontakter
|
|
kontakt_query = """
|
|
SELECT c.*
|
|
FROM contacts c
|
|
JOIN sag_kontakter sk ON c.id = sk.contact_id
|
|
WHERE sk.sag_id = %s AND sk.deleted_at IS NULL AND sk.is_primary = TRUE
|
|
LIMIT 1
|
|
"""
|
|
kontakt_result = execute_query(kontakt_query, (sag_id,))
|
|
if kontakt_result:
|
|
hovedkontakt = kontakt_result[0]
|
|
else:
|
|
fallback_query = """
|
|
SELECT c.*
|
|
FROM contacts c
|
|
JOIN sag_kontakter sk ON c.id = sk.contact_id
|
|
WHERE sk.sag_id = %s AND sk.deleted_at IS NULL
|
|
ORDER BY sk.created_at ASC
|
|
LIMIT 1
|
|
"""
|
|
fallback_result = execute_query(fallback_query, (sag_id,))
|
|
if fallback_result:
|
|
hovedkontakt = fallback_result[0]
|
|
|
|
# Fetch prepaid cards for customer
|
|
# Cast remaining_hours to float to avoid Jinja formatting issues with Decimal
|
|
# DEBUG: Logging customer ID
|
|
prepaid_cards = []
|
|
if sag.get('customer_id'):
|
|
cid = sag.get('customer_id')
|
|
logger.info(f"🔎 Looking up prepaid cards for Sag {sag_id}, Customer ID: {cid} (Type: {type(cid)})")
|
|
|
|
pc_query = """
|
|
SELECT id, card_number, CAST(remaining_hours AS FLOAT) as remaining_hours, expires_at
|
|
FROM tticket_prepaid_cards
|
|
WHERE customer_id = %s
|
|
AND status = 'active'
|
|
AND remaining_hours > 0
|
|
ORDER BY created_at DESC
|
|
"""
|
|
prepaid_cards = execute_query(pc_query, (cid,))
|
|
logger.info(f"💳 Found {len(prepaid_cards)} prepaid cards for customer {cid}")
|
|
|
|
# Fetch fixed-price agreements for customer
|
|
fixed_price_agreements = []
|
|
if sag.get('customer_id'):
|
|
cid = sag.get('customer_id')
|
|
logger.info(f"🔎 Looking up fixed-price agreements for Sag {sag_id}, Customer ID: {cid}")
|
|
|
|
fpa_query = """
|
|
SELECT
|
|
a.id,
|
|
a.agreement_number,
|
|
a.monthly_hours,
|
|
COALESCE(bp.remaining_hours, a.monthly_hours) as remaining_hours_this_month
|
|
FROM customer_fixed_price_agreements a
|
|
LEFT JOIN fixed_price_billing_periods bp ON (
|
|
a.id = bp.agreement_id
|
|
AND bp.period_start <= CURRENT_DATE
|
|
AND bp.period_end >= CURRENT_DATE
|
|
)
|
|
WHERE a.customer_id = %s
|
|
AND a.status = 'active'
|
|
AND (a.end_date IS NULL OR a.end_date >= CURRENT_DATE)
|
|
ORDER BY a.created_at DESC
|
|
"""
|
|
fixed_price_agreements = execute_query(fpa_query, (cid,))
|
|
logger.info(f"📋 Found {len(fixed_price_agreements)} fixed-price agreements for customer {cid}")
|
|
|
|
# Fetch Nextcloud Instance for this customer
|
|
nextcloud_instance = None
|
|
if customer:
|
|
nc_query = "SELECT * FROM nextcloud_instances WHERE customer_id = %s AND deleted_at IS NULL"
|
|
nc_result = execute_query(nc_query, (customer['id'],))
|
|
if nc_result:
|
|
nextcloud_instance = nc_result[0]
|
|
|
|
# Fetch linked contacts
|
|
contacts_query = """
|
|
SELECT
|
|
sk.*,
|
|
c.first_name || ' ' || c.last_name as contact_name,
|
|
c.email as contact_email,
|
|
c.phone,
|
|
c.mobile,
|
|
c.title,
|
|
company.customer_name
|
|
FROM sag_kontakter sk
|
|
JOIN contacts c ON sk.contact_id = c.id
|
|
LEFT JOIN LATERAL (
|
|
SELECT cu.name AS customer_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, cu.name
|
|
LIMIT 1
|
|
) company ON TRUE
|
|
WHERE sk.sag_id = %s AND sk.deleted_at IS NULL
|
|
"""
|
|
contacts = execute_query(contacts_query, (sag_id,))
|
|
|
|
# Fetch linked customers
|
|
customers_query = """
|
|
SELECT sk.*, c.name as customer_name, c.email as customer_email
|
|
FROM sag_kunder sk
|
|
JOIN customers c ON sk.customer_id = c.id
|
|
WHERE sk.sag_id = %s AND sk.deleted_at IS NULL
|
|
"""
|
|
customers = execute_query(customers_query, (sag_id,))
|
|
|
|
# Fetch comments
|
|
comments_query = "SELECT * FROM sag_kommentarer WHERE sag_id = %s AND deleted_at IS NULL ORDER BY created_at ASC"
|
|
comments = execute_query(comments_query, (sag_id,))
|
|
|
|
# Fetch Solution
|
|
solution_query = "SELECT * FROM sag_solutions WHERE sag_id = %s"
|
|
solution_res = execute_query(solution_query, (sag_id,))
|
|
solution = solution_res[0] if solution_res else None
|
|
|
|
# Fetch Time Entries
|
|
time_query = "SELECT * FROM tmodule_times WHERE sag_id = %s ORDER BY worked_date DESC"
|
|
time_entries = execute_query(time_query, (sag_id,))
|
|
|
|
# Fetch linked telephony call history
|
|
call_history_query = """
|
|
SELECT
|
|
t.id,
|
|
t.callid,
|
|
t.direction,
|
|
t.ekstern_nummer,
|
|
t.started_at,
|
|
t.ended_at,
|
|
t.duration_sec,
|
|
u.username,
|
|
u.full_name,
|
|
CONCAT(COALESCE(c.first_name, ''), ' ', COALESCE(c.last_name, '')) AS contact_name
|
|
FROM telefoni_opkald t
|
|
LEFT JOIN users u ON u.user_id = t.bruger_id
|
|
LEFT JOIN contacts c ON c.id = t.kontakt_id
|
|
WHERE t.sag_id = %s
|
|
ORDER BY t.started_at DESC
|
|
LIMIT 200
|
|
"""
|
|
call_history = execute_query(call_history_query, (sag_id,))
|
|
|
|
# Check for nextcloud integration (case-insensitive, insensitive to whitespace)
|
|
logger.info(f"Checking tags for Nextcloud on case {sag_id}: {tags}")
|
|
is_nextcloud = any(t['tag_navn'] and t['tag_navn'].strip().lower() == 'nextcloud' for t in tags)
|
|
logger.info(f"is_nextcloud result: {is_nextcloud}")
|
|
|
|
related_case_options = []
|
|
try:
|
|
related_ids = set()
|
|
for rel in relationer or []:
|
|
related_ids.add(rel["kilde_sag_id"])
|
|
related_ids.add(rel["målsag_id"])
|
|
related_ids.discard(sag_id)
|
|
if related_ids:
|
|
placeholders = ",".join(["%s"] * len(related_ids))
|
|
related_query = f"SELECT id, titel, status FROM sag_sager WHERE id IN ({placeholders}) AND deleted_at IS NULL"
|
|
related_case_options = execute_query(related_query, tuple(related_ids))
|
|
except Exception as e:
|
|
logger.error("❌ Error building related case options: %s", e)
|
|
related_case_options = []
|
|
|
|
pipeline_stages = []
|
|
try:
|
|
pipeline_stages = execute_query(
|
|
"SELECT id, name, color, sort_order FROM pipeline_stages ORDER BY sort_order ASC, id ASC",
|
|
(),
|
|
)
|
|
except Exception as e:
|
|
logger.warning("⚠️ Could not load pipeline stages: %s", e)
|
|
pipeline_stages = []
|
|
|
|
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", {
|
|
"request": request,
|
|
"case": sag,
|
|
"customer": customer,
|
|
"hovedkontakt": hovedkontakt,
|
|
"contacts": contacts,
|
|
"customers": customers,
|
|
"prepaid_cards": prepaid_cards,
|
|
"fixed_price_agreements": fixed_price_agreements,
|
|
"tags": tags,
|
|
|
|
"relationer": relationer,
|
|
"relation_tree": relation_tree,
|
|
"comments": comments,
|
|
"solution": solution,
|
|
"time_entries": time_entries,
|
|
"call_history": call_history,
|
|
"is_nextcloud": is_nextcloud,
|
|
"nextcloud_instance": nextcloud_instance,
|
|
"related_case_options": related_case_options,
|
|
"pipeline_stages": pipeline_stages,
|
|
"status_options": status_options,
|
|
"is_deadline_overdue": is_deadline_overdue,
|
|
"assignment_users": _fetch_assignment_users(),
|
|
"assignment_groups": _fetch_assignment_groups(),
|
|
})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error("❌ Error displaying case details: %s", e)
|
|
raise HTTPException(status_code=500, detail="Failed to load case details")
|
|
|
|
|
|
@router.get("/sag/{sag_id}/edit", response_class=HTMLResponse)
|
|
async def sag_rediger(request: Request, sag_id: int):
|
|
"""Display edit case form."""
|
|
try:
|
|
sag_query = "SELECT * FROM sag_sager WHERE id = %s AND deleted_at IS NULL"
|
|
sag_result = execute_query(sag_query, (sag_id,))
|
|
|
|
if not sag_result:
|
|
raise HTTPException(status_code=404, detail="Case not found")
|
|
|
|
return templates.TemplateResponse("modules/sag/templates/edit.html", {
|
|
"request": request,
|
|
"case": sag_result[0],
|
|
"assignment_users": _fetch_assignment_users(),
|
|
"assignment_groups": _fetch_assignment_groups(),
|
|
})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error("❌ Error loading edit case page: %s", e)
|
|
raise HTTPException(status_code=500, detail="Failed to load edit case page")
|