- Updated the case list endpoint to handle filtering and error logging more effectively. - Changed the template directory structure for better organization. - Enhanced the case detail view with improved error handling and customer information retrieval. - Redesigned the index.html template to include a more modern layout and responsive design using Bootstrap. - Implemented dark mode toggle functionality and improved search/filter capabilities in the frontend. - Removed unused code and optimized existing JavaScript for better performance.
322 lines
12 KiB
Python
322 lines
12 KiB
Python
import logging
|
|
from typing import List, Optional
|
|
from fastapi import APIRouter, HTTPException, Query
|
|
from app.core.database import execute_query
|
|
from datetime import datetime
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter()
|
|
|
|
# ============================================================================
|
|
# SAGER - CRUD Operations
|
|
# ============================================================================
|
|
|
|
@router.get("/sag")
|
|
async def list_sager(
|
|
status: Optional[str] = Query(None),
|
|
tag: Optional[str] = Query(None),
|
|
customer_id: Optional[int] = Query(None),
|
|
ansvarlig_bruger_id: Optional[int] = Query(None),
|
|
):
|
|
"""List all cases with optional filtering."""
|
|
try:
|
|
query = "SELECT * FROM sag_sager WHERE deleted_at IS NULL"
|
|
params = []
|
|
|
|
if status:
|
|
query += " AND status = %s"
|
|
params.append(status)
|
|
if customer_id:
|
|
query += " AND customer_id = %s"
|
|
params.append(customer_id)
|
|
if ansvarlig_bruger_id:
|
|
query += " AND ansvarlig_bruger_id = %s"
|
|
params.append(ansvarlig_bruger_id)
|
|
|
|
query += " ORDER BY created_at DESC"
|
|
|
|
cases = execute_query(query, tuple(params))
|
|
|
|
# If tag filter, filter in Python after fetch
|
|
if tag:
|
|
case_ids = [case['id'] for case in cases]
|
|
if case_ids:
|
|
tag_query = "SELECT sag_id FROM sag_tags WHERE tag_navn = %s AND deleted_at IS NULL"
|
|
tagged_cases = execute_query(tag_query, (tag,))
|
|
tagged_ids = set(t['sag_id'] for t in tagged_cases)
|
|
cases = [c for c in cases if c['id'] in tagged_ids]
|
|
|
|
return cases
|
|
except Exception as e:
|
|
logger.error("❌ Error listing cases: %s", e)
|
|
raise HTTPException(status_code=500, detail="Failed to list cases")
|
|
|
|
@router.post("/sag")
|
|
async def create_sag(data: dict):
|
|
"""Create a new case."""
|
|
try:
|
|
if not data.get('titel'):
|
|
raise HTTPException(status_code=400, detail="titel is required")
|
|
if not data.get('customer_id'):
|
|
raise HTTPException(status_code=400, detail="customer_id is required")
|
|
|
|
query = """
|
|
INSERT INTO sag_sager
|
|
(titel, beskrivelse, type, status, customer_id, ansvarlig_bruger_id, deadline)
|
|
VALUES (%s, %s, %s, %s, %s, %s, %s)
|
|
RETURNING *
|
|
"""
|
|
params = (
|
|
data.get('titel'),
|
|
data.get('beskrivelse', ''),
|
|
data.get('type', 'ticket'),
|
|
data.get('status', 'åben'),
|
|
data.get('customer_id'),
|
|
data.get('ansvarlig_bruger_id'),
|
|
data.get('deadline'),
|
|
)
|
|
|
|
result = execute_query(query, params)
|
|
if result:
|
|
logger.info("✅ Case created: %s", result[0]['id'])
|
|
return result[0]
|
|
raise HTTPException(status_code=500, detail="Failed to create case")
|
|
except Exception as e:
|
|
logger.error("❌ Error creating case: %s", e)
|
|
raise HTTPException(status_code=500, detail="Failed to create case")
|
|
|
|
@router.get("/sag/{sag_id}")
|
|
async def get_sag(sag_id: int):
|
|
"""Get a specific case."""
|
|
try:
|
|
query = "SELECT * FROM sag_sager WHERE id = %s AND deleted_at IS NULL"
|
|
result = execute_query(query, (sag_id,))
|
|
if not result:
|
|
raise HTTPException(status_code=404, detail="Case not found")
|
|
return result[0]
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error("❌ Error getting case: %s", e)
|
|
raise HTTPException(status_code=500, detail="Failed to get case")
|
|
|
|
@router.patch("/sag/{sag_id}")
|
|
async def update_sag(sag_id: int, updates: dict):
|
|
"""Update a case."""
|
|
try:
|
|
# Check if case exists
|
|
check = execute_query("SELECT id FROM sag_sager WHERE id = %s AND deleted_at IS NULL", (sag_id,))
|
|
if not check:
|
|
raise HTTPException(status_code=404, detail="Case not found")
|
|
|
|
# Build dynamic update query
|
|
allowed_fields = ['titel', 'beskrivelse', 'type', 'status', 'ansvarlig_bruger_id', 'deadline']
|
|
set_clauses = []
|
|
params = []
|
|
|
|
for field in allowed_fields:
|
|
if field in updates:
|
|
set_clauses.append(f"{field} = %s")
|
|
params.append(updates[field])
|
|
|
|
if not set_clauses:
|
|
raise HTTPException(status_code=400, detail="No valid fields to update")
|
|
|
|
params.append(sag_id)
|
|
query = f"UPDATE sag_sager SET {', '.join(set_clauses)} WHERE id = %s RETURNING *"
|
|
|
|
result = execute_query(query, tuple(params))
|
|
if result:
|
|
logger.info("✅ Case updated: %s", sag_id)
|
|
return result[0]
|
|
raise HTTPException(status_code=500, detail="Failed to update case")
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error("❌ Error updating case: %s", e)
|
|
raise HTTPException(status_code=500, detail="Failed to update case")
|
|
|
|
@router.delete("/sag/{sag_id}")
|
|
async def delete_sag(sag_id: int):
|
|
"""Soft-delete a case."""
|
|
try:
|
|
check = execute_query("SELECT id FROM sag_sager WHERE id = %s AND deleted_at IS NULL", (sag_id,))
|
|
if not check:
|
|
raise HTTPException(status_code=404, detail="Case not found")
|
|
|
|
query = "UPDATE sag_sager SET deleted_at = NOW() WHERE id = %s RETURNING id"
|
|
result = execute_query(query, (sag_id,))
|
|
|
|
if result:
|
|
logger.info("✅ Case soft-deleted: %s", sag_id)
|
|
return {"status": "deleted", "id": sag_id}
|
|
raise HTTPException(status_code=500, detail="Failed to delete case")
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error("❌ Error deleting case: %s", e)
|
|
raise HTTPException(status_code=500, detail="Failed to delete case")
|
|
|
|
# ============================================================================
|
|
# RELATIONER - Case Relations
|
|
# ============================================================================
|
|
|
|
@router.get("/sag/{sag_id}/relationer")
|
|
async def get_relationer(sag_id: int):
|
|
"""Get all relations for a case."""
|
|
try:
|
|
# Check if case exists
|
|
check = execute_query("SELECT id FROM sag_sager WHERE id = %s AND deleted_at IS NULL", (sag_id,))
|
|
if not check:
|
|
raise HTTPException(status_code=404, detail="Case not found")
|
|
|
|
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
|
|
"""
|
|
result = execute_query(query, (sag_id, sag_id))
|
|
return result
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error("❌ Error getting relations: %s", e)
|
|
raise HTTPException(status_code=500, detail="Failed to get relations")
|
|
|
|
@router.post("/sag/{sag_id}/relationer")
|
|
async def create_relation(sag_id: int, data: dict):
|
|
"""Add a relation to another case."""
|
|
try:
|
|
if not data.get('målsag_id') or not data.get('relationstype'):
|
|
raise HTTPException(status_code=400, detail="målsag_id and relationstype required")
|
|
|
|
målsag_id = data.get('målsag_id')
|
|
relationstype = data.get('relationstype')
|
|
|
|
# Validate both cases exist
|
|
check1 = execute_query("SELECT id FROM sag_sager WHERE id = %s AND deleted_at IS NULL", (sag_id,))
|
|
check2 = execute_query("SELECT id FROM sag_sager WHERE id = %s AND deleted_at IS NULL", (målsag_id,))
|
|
|
|
if not check1 or not check2:
|
|
raise HTTPException(status_code=404, detail="One or both cases not found")
|
|
|
|
query = """
|
|
INSERT INTO sag_relationer (kilde_sag_id, målsag_id, relationstype)
|
|
VALUES (%s, %s, %s)
|
|
RETURNING *
|
|
"""
|
|
result = execute_query(query, (sag_id, målsag_id, relationstype))
|
|
|
|
if result:
|
|
logger.info("✅ Relation created: %s -> %s (%s)", sag_id, målsag_id, relationstype)
|
|
return result[0]
|
|
raise HTTPException(status_code=500, detail="Failed to create relation")
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error("❌ Error creating relation: %s", e)
|
|
raise HTTPException(status_code=500, detail="Failed to create relation")
|
|
|
|
@router.delete("/sag/{sag_id}/relationer/{relation_id}")
|
|
async def delete_relation(sag_id: int, relation_id: int):
|
|
"""Soft-delete a relation."""
|
|
try:
|
|
check = execute_query(
|
|
"SELECT id FROM sag_relationer WHERE id = %s AND deleted_at IS NULL AND (kilde_sag_id = %s OR målsag_id = %s)",
|
|
(relation_id, sag_id, sag_id)
|
|
)
|
|
if not check:
|
|
raise HTTPException(status_code=404, detail="Relation not found")
|
|
|
|
query = "UPDATE sag_relationer SET deleted_at = NOW() WHERE id = %s RETURNING id"
|
|
result = execute_query(query, (relation_id,))
|
|
|
|
if result:
|
|
logger.info("✅ Relation soft-deleted: %s", relation_id)
|
|
return {"status": "deleted", "id": relation_id}
|
|
raise HTTPException(status_code=500, detail="Failed to delete relation")
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error("❌ Error deleting relation: %s", e)
|
|
raise HTTPException(status_code=500, detail="Failed to delete relation")
|
|
|
|
# ============================================================================
|
|
# TAGS - Case Tags
|
|
# ============================================================================
|
|
|
|
@router.get("/sag/{sag_id}/tags")
|
|
async def get_tags(sag_id: int):
|
|
"""Get all tags for a case."""
|
|
try:
|
|
check = execute_query("SELECT id FROM sag_sager WHERE id = %s AND deleted_at IS NULL", (sag_id,))
|
|
if not check:
|
|
raise HTTPException(status_code=404, detail="Case not found")
|
|
|
|
query = "SELECT * FROM sag_tags WHERE sag_id = %s AND deleted_at IS NULL ORDER BY created_at DESC"
|
|
result = execute_query(query, (sag_id,))
|
|
return result
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error("❌ Error getting tags: %s", e)
|
|
raise HTTPException(status_code=500, detail="Failed to get tags")
|
|
|
|
@router.post("/sag/{sag_id}/tags")
|
|
async def add_tag(sag_id: int, data: dict):
|
|
"""Add a tag to a case."""
|
|
try:
|
|
if not data.get('tag_navn'):
|
|
raise HTTPException(status_code=400, detail="tag_navn is required")
|
|
|
|
check = execute_query("SELECT id FROM sag_sager WHERE id = %s AND deleted_at IS NULL", (sag_id,))
|
|
if not check:
|
|
raise HTTPException(status_code=404, detail="Case not found")
|
|
|
|
query = """
|
|
INSERT INTO sag_tags (sag_id, tag_navn)
|
|
VALUES (%s, %s)
|
|
RETURNING *
|
|
"""
|
|
result = execute_query(query, (sag_id, data.get('tag_navn')))
|
|
|
|
if result:
|
|
logger.info("✅ Tag added: %s -> %s", sag_id, data.get('tag_navn'))
|
|
return result[0]
|
|
raise HTTPException(status_code=500, detail="Failed to add tag")
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error("❌ Error adding tag: %s", e)
|
|
raise HTTPException(status_code=500, detail="Failed to add tag")
|
|
|
|
@router.delete("/sag/{sag_id}/tags/{tag_id}")
|
|
async def delete_tag(sag_id: int, tag_id: int):
|
|
"""Soft-delete a tag."""
|
|
try:
|
|
check = execute_query(
|
|
"SELECT id FROM sag_tags WHERE id = %s AND sag_id = %s AND deleted_at IS NULL",
|
|
(tag_id, sag_id)
|
|
)
|
|
if not check:
|
|
raise HTTPException(status_code=404, detail="Tag not found")
|
|
|
|
query = "UPDATE sag_tags SET deleted_at = NOW() WHERE id = %s RETURNING id"
|
|
result = execute_query(query, (tag_id,))
|
|
|
|
if result:
|
|
logger.info("✅ Tag soft-deleted: %s", tag_id)
|
|
return {"status": "deleted", "id": tag_id}
|
|
raise HTTPException(status_code=500, detail="Failed to delete tag")
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error("❌ Error deleting tag: %s", e)
|
|
raise HTTPException(status_code=500, detail="Failed to delete tag")
|