bmc_hub/app/modules/sag/frontend/views.py

381 lines
16 KiB
Python
Raw Normal View History

import logging
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()
# Setup template directory
templates = Jinja2Templates(directory="app")
@router.get("/sag", response_class=HTMLResponse)
async def sager_liste(
request: Request,
status: str = Query(None),
tag: str = Query(None),
customer_id: int = Query(None),
):
"""Display list of all cases."""
try:
query = """
SELECT s.*,
c.name as customer_name,
CONCAT(COALESCE(cont.first_name, ''), ' ', COALESCE(cont.last_name, '')) as kontakt_navn
FROM sag_sager s
LEFT JOIN customers c ON s.customer_id = c.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
WHERE s.deleted_at IS NULL
"""
params = []
if status:
query += " AND s.status = %s"
params.append(status)
if customer_id:
query += " AND s.customer_id = %s"
params.append(customer_id)
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
statuses = execute_query("SELECT DISTINCT status FROM sag_sager WHERE deleted_at IS NULL ORDER BY status", ())
all_tags = execute_query("SELECT DISTINCT tag_navn FROM sag_tags WHERE deleted_at IS NULL ORDER BY tag_navn", ())
return templates.TemplateResponse("modules/sag/templates/index.html", {
"request": request,
"sager": sager,
"relations_map": relations_map,
"child_ids": list(child_ids),
"statuses": [s['status'] for s in statuses],
"all_tags": [t['tag_navn'] for t in all_tags],
"current_status": status,
"current_tag": tag,
})
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})
@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 * 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")
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:
# 1. Get all connected case IDs (Recursive CTE)
tree_ids_query = """
WITH RECURSIVE CaseTree AS (
SELECT id FROM sag_sager WHERE id = %s
UNION
SELECT CASE WHEN sr.kilde_sag_id = ct.id THEN sr.målsag_id ELSE sr.kilde_sag_id END
FROM sag_relationer sr
JOIN CaseTree ct ON sr.kilde_sag_id = ct.id OR sr.målsag_id = ct.id
WHERE sr.deleted_at IS NULL
)
SELECT id FROM CaseTree LIMIT 50;
"""
tree_ids_rows = execute_query(tree_ids_query, (sag_id,))
tree_ids = [r['id'] for r in tree_ids_rows]
if tree_ids:
# 2. Fetch details
placeholders = ','.join(['%s'] * len(tree_ids))
tree_cases_query = f"SELECT id, titel, status 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 edges
tree_edges_query = f"""
SELECT id, kilde_sag_id, målsag_id, relationstype
FROM sag_relationer
WHERE deleted_at IS NULL
AND kilde_sag_id IN ({placeholders})
AND målsag_id IN ({placeholders})
"""
tree_edges = execute_query(tree_edges_query, tuple(tree_ids) * 2)
# 4. Build Graph
children_map = {cid: [] for cid in tree_ids}
parents_map = {cid: [] for cid in tree_ids}
for edge in tree_edges:
k, m, rtype = edge['kilde_sag_id'], edge['målsag_id'], edge['relationstype'].lower()
parent, child = k, m # Default (e.g. Relateret til)
if rtype == 'afledt af': # m is parent of k
parent, child = m, k
elif rtype == 'årsag til': # k is parent of m
parent, child = k, m
if parent in children_map:
children_map[parent].append({
'id': child,
'type': edge['relationstype'],
'rel_id': edge['id']
})
if child in parents_map:
parents_map[child].append(parent)
# 5. Identify Roots and Build
roots = [cid for cid in tree_ids if not parents_map[cid]]
if not roots and tree_ids: roots = [min(tree_ids)] # Fallback
def build_tree_node(cid, visited):
if cid in visited: return None
visited.add(cid)
node_case = tree_cases.get(cid)
if not node_case: return None
children_nodes = []
for child_info in children_map.get(cid, []):
c_node = build_tree_node(child_info['id'], visited.copy())
if c_node:
c_node['relation_type'] = child_info['type']
c_node['relation_id'] = child_info['rel_id']
children_nodes.append(c_node)
return {
'case': node_case,
'children': children_nodes,
'is_current': cid == sag_id
}
relation_tree = [build_tree_node(r, set()) for r in roots]
relation_tree = [n for n in relation_tree if n]
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 customer via contact_companies
kontakt_query = """
SELECT c.*
FROM contacts c
JOIN contact_companies cc ON c.id = cc.contact_id
WHERE cc.customer_id = %s
ORDER BY cc.is_primary DESC, c.id ASC
LIMIT 1
"""
kontakt_result = execute_query(kontakt_query, (sag['customer_id'],))
if kontakt_result:
hovedkontakt = kontakt_result[0]
# Fetch prepaid cards for customer
# Cast remaining_hours to float to avoid Jinja formatting issues with Decimal
# DEBUG: Logging 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
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}")
else:
prepaid_cards = []
# 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
FROM sag_kontakter sk
JOIN contacts c ON sk.contact_id = c.id
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,))
# 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}")
return templates.TemplateResponse("modules/sag/templates/detail.html", {
"request": request,
"case": sag,
"customer": customer,
"hovedkontakt": hovedkontakt,
"contacts": contacts,
"customers": customers,
"prepaid_cards": prepaid_cards,
"tags": tags,
"relationer": relationer,
"relation_tree": relation_tree,
"comments": comments,
"solution": solution,
"time_entries": time_entries,
"is_nextcloud": is_nextcloud,
"nextcloud_instance": nextcloud_instance,
})
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],
})
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")