diff --git a/app/customers/backend/router.py b/app/customers/backend/router.py index cededca..424ec31 100644 --- a/app/customers/backend/router.py +++ b/app/customers/backend/router.py @@ -56,6 +56,7 @@ class CustomerUpdate(BaseModel): is_active: Optional[bool] = None invoice_email: Optional[str] = None mobile_phone: Optional[str] = None + department: Optional[str] = None class ContactCreate(BaseModel): @@ -568,11 +569,15 @@ async def update_customer(customer_id: int, update: CustomerUpdate): "SELECT * FROM customers WHERE id = %s", (customer_id,)) return updated - except Exception as e: logger.error(f"❌ Failed to update customer {customer_id}: {e}") raise HTTPException(status_code=500, detail=str(e)) +@router.patch("/customers/{customer_id}") +async def patch_customer(customer_id: int, update: CustomerUpdate): + """Partially update customer information (same as PUT)""" + return await update_customer(customer_id, update) + @router.get("/customers/{customer_id}/data-consistency") async def check_customer_data_consistency(customer_id: int): diff --git a/app/dashboard/backend/router.py b/app/dashboard/backend/router.py index 45c3077..72ef10b 100644 --- a/app/dashboard/backend/router.py +++ b/app/dashboard/backend/router.py @@ -124,6 +124,46 @@ async def global_search(q: str): return {"customers": [], "contacts": [], "vendors": []} +@router.get("/search/sag", response_model=List[Dict[str, Any]]) +async def search_sag(q: str): + """ + Search for cases (sager) with customer information + """ + if not q or len(q) < 2: + return [] + + search_term = f"%{q}%" + + try: + # Search cases with customer names + sager = execute_query(""" + SELECT + s.id, + s.titel, + s.beskrivelse, + s.status, + s.created_at, + s.customer_id, + c.name as customer_name + FROM sag_sager s + LEFT JOIN customers c ON s.customer_id = c.id + WHERE s.deleted_at IS NULL + AND ( + CAST(s.id AS TEXT) ILIKE %s OR + s.titel ILIKE %s OR + s.beskrivelse ILIKE %s OR + c.name ILIKE %s + ) + ORDER BY s.created_at DESC + LIMIT 20 + """, (search_term, search_term, search_term, search_term)) + + return sager or [] + except Exception as e: + logger.error(f"❌ Error searching sager: {e}", exc_info=True) + return [] + + @router.get("/live-stats", response_model=Dict[str, Any]) async def get_live_stats(): """ diff --git a/app/modules/sag/backend/router.py b/app/modules/sag/backend/router.py index dae1951..904ed45 100644 --- a/app/modules/sag/backend/router.py +++ b/app/modules/sag/backend/router.py @@ -319,3 +319,49 @@ async def delete_tag(sag_id: int, tag_id: int): except Exception as e: logger.error("❌ Error deleting tag: %s", e) raise HTTPException(status_code=500, detail="Failed to delete tag") + + +# ============================================================================ +# HARDWARE - Placeholder endpoints for frontend compatibility +# ============================================================================ + +@router.get("/sag/{sag_id}/hardware") +async def list_case_hardware(sag_id: int): + """List hardware associated with a case. Placeholder endpoint.""" + # TODO: Implement when hardware-case relation is defined + return [] + +@router.post("/sag/{sag_id}/hardware") +async def add_case_hardware(sag_id: int): + """Add hardware to case. Placeholder endpoint.""" + # TODO: Implement when hardware-case relation is defined + return {"message": "Hardware endpoint not yet implemented"} + +@router.delete("/sag/{sag_id}/hardware/{hardware_id}") +async def remove_case_hardware(sag_id: int, hardware_id: int): + """Remove hardware from case. Placeholder endpoint.""" + # TODO: Implement when hardware-case relation is defined + return {"message": "Hardware endpoint not yet implemented"} + + +# ============================================================================ +# LOCATIONS - Placeholder endpoints for frontend compatibility +# ============================================================================ + +@router.get("/sag/{sag_id}/locations") +async def list_case_locations(sag_id: int): + """List locations associated with a case. Placeholder endpoint.""" + # TODO: Implement when location-case relation is defined + return [] + +@router.post("/sag/{sag_id}/locations") +async def add_case_location(sag_id: int): + """Add location to case. Placeholder endpoint.""" + # TODO: Implement when location-case relation is defined + return {"message": "Location endpoint not yet implemented"} + +@router.delete("/sag/{sag_id}/locations/{location_id}") +async def remove_case_location(sag_id: int, location_id: int): + """Remove location from case. Placeholder endpoint.""" + # TODO: Implement when location-case relation is defined + return {"message": "Location endpoint not yet implemented"} diff --git a/app/modules/sag/frontend/views.py b/app/modules/sag/frontend/views.py index fd8925b..5cef803 100644 --- a/app/modules/sag/frontend/views.py +++ b/app/modules/sag/frontend/views.py @@ -1,5 +1,5 @@ import logging -from fastapi import APIRouter, HTTPException, Query +from fastapi import APIRouter, HTTPException, Query, Request from fastapi.responses import HTMLResponse from fastapi.templating import Jinja2Templates from pathlib import Path @@ -9,31 +9,80 @@ logger = logging.getLogger(__name__) router = APIRouter() # Setup template directory -template_dir = Path(__file__).parent.parent / "templates" -templates = Jinja2Templates(directory=str(template_dir)) +templates = Jinja2Templates(directory="app") @router.get("/sag", response_class=HTMLResponse) async def sager_liste( - request, + request: Request, status: str = Query(None), tag: str = Query(None), customer_id: int = Query(None), ): """Display list of all cases.""" try: - query = "SELECT * FROM sag_sager WHERE deleted_at IS NULL" + 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 status = %s" + query += " AND s.status = %s" params.append(status) if customer_id: - query += " AND customer_id = %s" + query += " AND s.customer_id = %s" params.append(customer_id) - query += " ORDER BY created_at DESC" + 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] @@ -46,9 +95,11 @@ async def sager_liste( 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("index.html", { + 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, @@ -59,7 +110,7 @@ async def sager_liste( raise HTTPException(status_code=500, detail="Failed to load case list") @router.get("/sag/{sag_id}", response_class=HTMLResponse) -async def sag_detaljer(request, sag_id: int): +async def sag_detaljer(request: Request, sag_id: int): """Display case details.""" try: # Fetch main case @@ -91,16 +142,31 @@ async def sag_detaljer(request, sag_id: int): # 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] - return templates.TemplateResponse("detail.html", { + return templates.TemplateResponse("modules/sag/templates/detail.html", { "request": request, - "sag": sag, + "case": sag, "customer": customer, + "hovedkontakt": hovedkontakt, "tags": tags, "relationer": relationer, }) diff --git a/app/modules/sag/templates/detail.html b/app/modules/sag/templates/detail.html index 1d4bbca..520c2d6 100644 --- a/app/modules/sag/templates/detail.html +++ b/app/modules/sag/templates/detail.html @@ -1,236 +1,1370 @@ - - -
- - -{{ sag.beskrivelse or 'Ingen beskrivelse' }}
-{{ sag.status }}
-{{ sag.type }}
-{{ sag.deadline[:10] if sag.deadline else 'Ikke sat' }}
-{{ sag.created_at }}
-Ingen kontakter
+ {% endif %} +Ingen kunder
+ {% endif %} +Ingen relaterede sager
+ {% endif %} +Klik "Vis" for at se alle
+SAG kompatible moduler
+