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), include_deferred: bool = Query(False), ): """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 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 (s.deferred_until IS NULL OR s.deferred_until <= NOW())" query += " AND (s.deferred_until_case_id IS NULL OR s.deferred_until_status IS NULL OR ds.status = s.deferred_until_status)" 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", ()) 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": [s['status'] for s in statuses], "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, }) 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 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 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 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,)) # 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 = [] statuses = execute_query("SELECT DISTINCT status FROM sag_sager WHERE deleted_at IS NULL ORDER BY status", ()) 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, "related_case_options": related_case_options, "status_options": [s["status"] for s in statuses], }) 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")