665 lines
22 KiB
Python
665 lines
22 KiB
Python
|
|
"""Task template backend API (MVP)."""
|
||
|
|
|
||
|
|
from datetime import date, datetime, timedelta
|
||
|
|
from typing import Any, Dict, List, Optional, Tuple
|
||
|
|
import logging
|
||
|
|
|
||
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||
|
|
|
||
|
|
from app.core.auth_dependencies import require_permission
|
||
|
|
from app.core.database import execute_query, execute_query_single
|
||
|
|
from app.modules.task_templates.backend.models import (
|
||
|
|
TaskTemplate,
|
||
|
|
TaskTemplateCreate,
|
||
|
|
TaskTemplateItem,
|
||
|
|
TaskTemplateItemCreate,
|
||
|
|
TaskTemplateItemUpdate,
|
||
|
|
TaskTemplateUpdate,
|
||
|
|
TemplatePreviewRequest,
|
||
|
|
TemplateRunRequest,
|
||
|
|
)
|
||
|
|
|
||
|
|
router = APIRouter()
|
||
|
|
logger = logging.getLogger(__name__)
|
||
|
|
|
||
|
|
|
||
|
|
def _validate_company_template_payload(template_type: str, customer_id: Optional[int]) -> None:
|
||
|
|
if template_type == "company" and not customer_id:
|
||
|
|
raise HTTPException(status_code=400, detail="customer_id is required for company templates")
|
||
|
|
|
||
|
|
if template_type in ("global", "internal", "deactivated") and customer_id is not None:
|
||
|
|
raise HTTPException(
|
||
|
|
status_code=400,
|
||
|
|
detail="customer_id must be null unless template_type is 'company'",
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def _fetch_case(case_id: int) -> Dict[str, Any]:
|
||
|
|
case_row = execute_query_single(
|
||
|
|
"""
|
||
|
|
SELECT id, titel, customer_id, ansvarlig_bruger_id
|
||
|
|
FROM sag_sager
|
||
|
|
WHERE id = %s AND deleted_at IS NULL
|
||
|
|
""",
|
||
|
|
(case_id,),
|
||
|
|
)
|
||
|
|
if not case_row:
|
||
|
|
raise HTTPException(status_code=404, detail="Case not found")
|
||
|
|
return case_row
|
||
|
|
|
||
|
|
|
||
|
|
def _fetch_template_for_case(template_id: int, case_customer_id: Optional[int]) -> Dict[str, Any]:
|
||
|
|
template = execute_query_single(
|
||
|
|
"""
|
||
|
|
SELECT t.*
|
||
|
|
FROM task_templates t
|
||
|
|
WHERE t.id = %s
|
||
|
|
AND t.deleted_at IS NULL
|
||
|
|
AND t.is_active = TRUE
|
||
|
|
AND t.template_type != 'deactivated'
|
||
|
|
AND (
|
||
|
|
t.template_type IN ('global', 'internal')
|
||
|
|
OR (t.template_type = 'company' AND t.customer_id = %s)
|
||
|
|
)
|
||
|
|
""",
|
||
|
|
(template_id, case_customer_id),
|
||
|
|
)
|
||
|
|
if not template:
|
||
|
|
raise HTTPException(status_code=404, detail="Template not available for this case")
|
||
|
|
return template
|
||
|
|
|
||
|
|
|
||
|
|
def _fetch_template_items(template_id: int) -> List[Dict[str, Any]]:
|
||
|
|
return execute_query(
|
||
|
|
"""
|
||
|
|
SELECT *
|
||
|
|
FROM task_template_items
|
||
|
|
WHERE template_id = %s
|
||
|
|
AND is_active = TRUE
|
||
|
|
ORDER BY sort_order ASC, id ASC
|
||
|
|
""",
|
||
|
|
(template_id,),
|
||
|
|
) or []
|
||
|
|
|
||
|
|
|
||
|
|
def _resolve_assignee(
|
||
|
|
item: Dict[str, Any],
|
||
|
|
assignee_mode: str,
|
||
|
|
assignee_user_id: Optional[int],
|
||
|
|
assignee_role_id: Optional[int],
|
||
|
|
) -> Tuple[Optional[int], Optional[int]]:
|
||
|
|
if assignee_mode == "specific_user":
|
||
|
|
return assignee_user_id, None
|
||
|
|
if assignee_mode == "specific_role":
|
||
|
|
return None, assignee_role_id
|
||
|
|
return item.get("default_assignee_user_id"), item.get("default_assignee_role_id")
|
||
|
|
|
||
|
|
|
||
|
|
def _build_preview(
|
||
|
|
items: List[Dict[str, Any]],
|
||
|
|
start_date: date,
|
||
|
|
mode: str,
|
||
|
|
assignee_mode: str,
|
||
|
|
assignee_user_id: Optional[int],
|
||
|
|
assignee_role_id: Optional[int],
|
||
|
|
) -> Dict[str, Any]:
|
||
|
|
preview_items: List[Dict[str, Any]] = []
|
||
|
|
summary = {
|
||
|
|
"subcases": 0,
|
||
|
|
"tasks": 0,
|
||
|
|
"assignments": 0,
|
||
|
|
"deadlines": 0,
|
||
|
|
}
|
||
|
|
|
||
|
|
for item in items:
|
||
|
|
item_type = item.get("item_type")
|
||
|
|
if mode == "tasks" and item_type != "task":
|
||
|
|
continue
|
||
|
|
if mode == "subcases" and item_type != "subcase":
|
||
|
|
continue
|
||
|
|
|
||
|
|
days_offset = int(item.get("days_offset") or 0)
|
||
|
|
due_date = start_date + timedelta(days=days_offset)
|
||
|
|
assigned_user_id, assigned_role_id = _resolve_assignee(
|
||
|
|
item,
|
||
|
|
assignee_mode,
|
||
|
|
assignee_user_id,
|
||
|
|
assignee_role_id,
|
||
|
|
)
|
||
|
|
|
||
|
|
preview_items.append(
|
||
|
|
{
|
||
|
|
"template_item_id": item.get("id"),
|
||
|
|
"title": item.get("title"),
|
||
|
|
"description": item.get("description"),
|
||
|
|
"item_type": item_type,
|
||
|
|
"sort_order": item.get("sort_order") or 0,
|
||
|
|
"days_offset": days_offset,
|
||
|
|
"planned_due_date": due_date.isoformat(),
|
||
|
|
"is_required": bool(item.get("is_required")),
|
||
|
|
"assigned_user_id": assigned_user_id,
|
||
|
|
"assigned_role_id": assigned_role_id,
|
||
|
|
}
|
||
|
|
)
|
||
|
|
|
||
|
|
if item_type == "subcase":
|
||
|
|
summary["subcases"] += 1
|
||
|
|
else:
|
||
|
|
summary["tasks"] += 1
|
||
|
|
|
||
|
|
if assigned_user_id or assigned_role_id:
|
||
|
|
summary["assignments"] += 1
|
||
|
|
|
||
|
|
summary["deadlines"] += 1
|
||
|
|
|
||
|
|
return {"items": preview_items, "summary": summary}
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("/task-templates", response_model=List[TaskTemplate])
|
||
|
|
async def list_task_templates(
|
||
|
|
company_id: Optional[int] = Query(None),
|
||
|
|
category: Optional[str] = Query(None),
|
||
|
|
source: str = Query("all", description="all|company|global|internal"),
|
||
|
|
current_user: dict = Depends(require_permission("templates.view")),
|
||
|
|
):
|
||
|
|
del current_user
|
||
|
|
|
||
|
|
where_parts = ["t.deleted_at IS NULL", "t.is_active = TRUE", "t.template_type != 'deactivated'"]
|
||
|
|
params: List[Any] = []
|
||
|
|
|
||
|
|
if category:
|
||
|
|
where_parts.append("t.category = %s")
|
||
|
|
params.append(category)
|
||
|
|
|
||
|
|
if source == "company":
|
||
|
|
where_parts.append("t.template_type = 'company'")
|
||
|
|
elif source == "global":
|
||
|
|
where_parts.append("t.template_type = 'global'")
|
||
|
|
elif source == "internal":
|
||
|
|
where_parts.append("t.template_type = 'internal'")
|
||
|
|
elif source != "all":
|
||
|
|
raise HTTPException(status_code=400, detail="Invalid source. Use all|company|global|internal")
|
||
|
|
|
||
|
|
if company_id is not None:
|
||
|
|
where_parts.append(
|
||
|
|
"(t.template_type IN ('global', 'internal') OR (t.template_type = 'company' AND t.customer_id = %s))"
|
||
|
|
)
|
||
|
|
params.append(company_id)
|
||
|
|
|
||
|
|
where_clause = " AND ".join(where_parts)
|
||
|
|
query = f"""
|
||
|
|
SELECT t.id, t.name, t.description, t.template_type, t.customer_id,
|
||
|
|
t.category, t.is_active, t.created_by, t.created_at, t.updated_at
|
||
|
|
FROM task_templates t
|
||
|
|
WHERE {where_clause}
|
||
|
|
ORDER BY
|
||
|
|
CASE
|
||
|
|
WHEN %s IS NOT NULL AND t.template_type = 'company' AND t.customer_id = %s THEN 0
|
||
|
|
WHEN t.template_type = 'company' THEN 1
|
||
|
|
WHEN t.template_type = 'global' THEN 2
|
||
|
|
ELSE 3
|
||
|
|
END,
|
||
|
|
t.name ASC
|
||
|
|
"""
|
||
|
|
params.extend([company_id, company_id])
|
||
|
|
|
||
|
|
rows = execute_query(query, tuple(params)) or []
|
||
|
|
return [TaskTemplate(**row) for row in rows]
|
||
|
|
|
||
|
|
|
||
|
|
@router.post("/task-templates", response_model=TaskTemplate)
|
||
|
|
async def create_task_template(
|
||
|
|
payload: TaskTemplateCreate,
|
||
|
|
current_user: dict = Depends(require_permission("templates.create")),
|
||
|
|
):
|
||
|
|
_validate_company_template_payload(payload.template_type, payload.customer_id)
|
||
|
|
|
||
|
|
created = execute_query_single(
|
||
|
|
"""
|
||
|
|
INSERT INTO task_templates (name, description, template_type, customer_id, category, is_active, created_by)
|
||
|
|
VALUES (%s, %s, %s, %s, %s, %s, %s)
|
||
|
|
RETURNING id, name, description, template_type, customer_id, category, is_active, created_by, created_at, updated_at
|
||
|
|
""",
|
||
|
|
(
|
||
|
|
payload.name.strip(),
|
||
|
|
payload.description,
|
||
|
|
payload.template_type,
|
||
|
|
payload.customer_id,
|
||
|
|
payload.category,
|
||
|
|
payload.is_active,
|
||
|
|
current_user.get("id"),
|
||
|
|
),
|
||
|
|
)
|
||
|
|
if not created:
|
||
|
|
raise HTTPException(status_code=500, detail="Failed to create task template")
|
||
|
|
return TaskTemplate(**created)
|
||
|
|
|
||
|
|
|
||
|
|
@router.patch("/task-templates/{template_id}", response_model=TaskTemplate)
|
||
|
|
async def update_task_template(
|
||
|
|
template_id: int,
|
||
|
|
payload: TaskTemplateUpdate,
|
||
|
|
current_user: dict = Depends(require_permission("templates.edit")),
|
||
|
|
):
|
||
|
|
del current_user
|
||
|
|
|
||
|
|
current = execute_query_single(
|
||
|
|
"SELECT * FROM task_templates WHERE id = %s AND deleted_at IS NULL",
|
||
|
|
(template_id,),
|
||
|
|
)
|
||
|
|
if not current:
|
||
|
|
raise HTTPException(status_code=404, detail="Template not found")
|
||
|
|
|
||
|
|
new_template_type = payload.template_type or current.get("template_type")
|
||
|
|
if new_template_type in ("global", "internal", "deactivated"):
|
||
|
|
new_customer_id = None
|
||
|
|
elif payload.customer_id is not None:
|
||
|
|
new_customer_id = payload.customer_id
|
||
|
|
else:
|
||
|
|
new_customer_id = current.get("customer_id")
|
||
|
|
_validate_company_template_payload(new_template_type, new_customer_id)
|
||
|
|
|
||
|
|
updates = []
|
||
|
|
params: List[Any] = []
|
||
|
|
|
||
|
|
if payload.name is not None:
|
||
|
|
updates.append("name = %s")
|
||
|
|
params.append(payload.name.strip())
|
||
|
|
if payload.description is not None:
|
||
|
|
updates.append("description = %s")
|
||
|
|
params.append(payload.description)
|
||
|
|
if payload.template_type is not None:
|
||
|
|
updates.append("template_type = %s")
|
||
|
|
params.append(payload.template_type)
|
||
|
|
if payload.customer_id is not None or payload.template_type in ("global", "internal", "deactivated"):
|
||
|
|
updates.append("customer_id = %s")
|
||
|
|
params.append(new_customer_id)
|
||
|
|
if payload.category is not None:
|
||
|
|
updates.append("category = %s")
|
||
|
|
params.append(payload.category)
|
||
|
|
if payload.is_active is not None:
|
||
|
|
updates.append("is_active = %s")
|
||
|
|
params.append(payload.is_active)
|
||
|
|
|
||
|
|
if not updates:
|
||
|
|
raise HTTPException(status_code=400, detail="No fields to update")
|
||
|
|
|
||
|
|
updates.append("updated_at = NOW()")
|
||
|
|
params.append(template_id)
|
||
|
|
|
||
|
|
updated = execute_query_single(
|
||
|
|
f"""
|
||
|
|
UPDATE task_templates
|
||
|
|
SET {', '.join(updates)}
|
||
|
|
WHERE id = %s AND deleted_at IS NULL
|
||
|
|
RETURNING id, name, description, template_type, customer_id, category, is_active, created_by, created_at, updated_at
|
||
|
|
""",
|
||
|
|
tuple(params),
|
||
|
|
)
|
||
|
|
if not updated:
|
||
|
|
raise HTTPException(status_code=404, detail="Template not found")
|
||
|
|
return TaskTemplate(**updated)
|
||
|
|
|
||
|
|
|
||
|
|
@router.delete("/task-templates/{template_id}")
|
||
|
|
async def deactivate_task_template(
|
||
|
|
template_id: int,
|
||
|
|
current_user: dict = Depends(require_permission("templates.delete")),
|
||
|
|
):
|
||
|
|
del current_user
|
||
|
|
|
||
|
|
row = execute_query_single(
|
||
|
|
"""
|
||
|
|
UPDATE task_templates
|
||
|
|
SET is_active = FALSE,
|
||
|
|
template_type = 'deactivated',
|
||
|
|
customer_id = NULL,
|
||
|
|
updated_at = NOW()
|
||
|
|
WHERE id = %s AND deleted_at IS NULL
|
||
|
|
RETURNING id
|
||
|
|
""",
|
||
|
|
(template_id,),
|
||
|
|
)
|
||
|
|
if not row:
|
||
|
|
raise HTTPException(status_code=404, detail="Template not found")
|
||
|
|
|
||
|
|
return {"status": "deactivated", "template_id": template_id}
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("/task-templates/{template_id}/items", response_model=List[TaskTemplateItem])
|
||
|
|
async def list_task_template_items(
|
||
|
|
template_id: int,
|
||
|
|
current_user: dict = Depends(require_permission("templates.view")),
|
||
|
|
):
|
||
|
|
del current_user
|
||
|
|
|
||
|
|
rows = _fetch_template_items(template_id)
|
||
|
|
return [TaskTemplateItem(**row) for row in rows]
|
||
|
|
|
||
|
|
|
||
|
|
@router.post("/task-templates/{template_id}/items", response_model=TaskTemplateItem)
|
||
|
|
async def create_task_template_item(
|
||
|
|
template_id: int,
|
||
|
|
payload: TaskTemplateItemCreate,
|
||
|
|
current_user: dict = Depends(require_permission("templates.edit")),
|
||
|
|
):
|
||
|
|
del current_user
|
||
|
|
|
||
|
|
template_exists = execute_query_single(
|
||
|
|
"SELECT id FROM task_templates WHERE id = %s AND deleted_at IS NULL",
|
||
|
|
(template_id,),
|
||
|
|
)
|
||
|
|
if not template_exists:
|
||
|
|
raise HTTPException(status_code=404, detail="Template not found")
|
||
|
|
|
||
|
|
created = execute_query_single(
|
||
|
|
"""
|
||
|
|
INSERT INTO task_template_items (
|
||
|
|
template_id, title, description, item_type, default_assignee_user_id,
|
||
|
|
default_assignee_role_id, days_offset, sort_order, is_required, is_active
|
||
|
|
)
|
||
|
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||
|
|
RETURNING id, template_id, title, description, item_type, default_assignee_user_id,
|
||
|
|
default_assignee_role_id, days_offset, sort_order, is_required, is_active,
|
||
|
|
created_at, updated_at
|
||
|
|
""",
|
||
|
|
(
|
||
|
|
template_id,
|
||
|
|
payload.title.strip(),
|
||
|
|
payload.description,
|
||
|
|
payload.item_type,
|
||
|
|
payload.default_assignee_user_id,
|
||
|
|
payload.default_assignee_role_id,
|
||
|
|
payload.days_offset,
|
||
|
|
payload.sort_order,
|
||
|
|
payload.is_required,
|
||
|
|
payload.is_active,
|
||
|
|
),
|
||
|
|
)
|
||
|
|
if not created:
|
||
|
|
raise HTTPException(status_code=500, detail="Failed to create template item")
|
||
|
|
|
||
|
|
return TaskTemplateItem(**created)
|
||
|
|
|
||
|
|
|
||
|
|
@router.patch("/task-template-items/{item_id}", response_model=TaskTemplateItem)
|
||
|
|
async def update_task_template_item(
|
||
|
|
item_id: int,
|
||
|
|
payload: TaskTemplateItemUpdate,
|
||
|
|
current_user: dict = Depends(require_permission("templates.edit")),
|
||
|
|
):
|
||
|
|
del current_user
|
||
|
|
|
||
|
|
updates = []
|
||
|
|
params: List[Any] = []
|
||
|
|
|
||
|
|
if payload.title is not None:
|
||
|
|
updates.append("title = %s")
|
||
|
|
params.append(payload.title.strip())
|
||
|
|
if payload.description is not None:
|
||
|
|
updates.append("description = %s")
|
||
|
|
params.append(payload.description)
|
||
|
|
if payload.item_type is not None:
|
||
|
|
updates.append("item_type = %s")
|
||
|
|
params.append(payload.item_type)
|
||
|
|
if payload.default_assignee_user_id is not None:
|
||
|
|
updates.append("default_assignee_user_id = %s")
|
||
|
|
params.append(payload.default_assignee_user_id)
|
||
|
|
if payload.default_assignee_role_id is not None:
|
||
|
|
updates.append("default_assignee_role_id = %s")
|
||
|
|
params.append(payload.default_assignee_role_id)
|
||
|
|
if payload.days_offset is not None:
|
||
|
|
updates.append("days_offset = %s")
|
||
|
|
params.append(payload.days_offset)
|
||
|
|
if payload.sort_order is not None:
|
||
|
|
updates.append("sort_order = %s")
|
||
|
|
params.append(payload.sort_order)
|
||
|
|
if payload.is_required is not None:
|
||
|
|
updates.append("is_required = %s")
|
||
|
|
params.append(payload.is_required)
|
||
|
|
if payload.is_active is not None:
|
||
|
|
updates.append("is_active = %s")
|
||
|
|
params.append(payload.is_active)
|
||
|
|
|
||
|
|
if not updates:
|
||
|
|
raise HTTPException(status_code=400, detail="No fields to update")
|
||
|
|
|
||
|
|
updates.append("updated_at = NOW()")
|
||
|
|
params.append(item_id)
|
||
|
|
|
||
|
|
row = execute_query_single(
|
||
|
|
f"""
|
||
|
|
UPDATE task_template_items
|
||
|
|
SET {', '.join(updates)}
|
||
|
|
WHERE id = %s
|
||
|
|
RETURNING id, template_id, title, description, item_type, default_assignee_user_id,
|
||
|
|
default_assignee_role_id, days_offset, sort_order, is_required, is_active,
|
||
|
|
created_at, updated_at
|
||
|
|
""",
|
||
|
|
tuple(params),
|
||
|
|
)
|
||
|
|
if not row:
|
||
|
|
raise HTTPException(status_code=404, detail="Template item not found")
|
||
|
|
|
||
|
|
return TaskTemplateItem(**row)
|
||
|
|
|
||
|
|
|
||
|
|
@router.post("/cases/{case_id}/template-preview")
|
||
|
|
async def preview_template_for_case(
|
||
|
|
case_id: int,
|
||
|
|
payload: TemplatePreviewRequest,
|
||
|
|
current_user: dict = Depends(require_permission("templates.view")),
|
||
|
|
):
|
||
|
|
del current_user
|
||
|
|
|
||
|
|
case_row = _fetch_case(case_id)
|
||
|
|
template = _fetch_template_for_case(payload.template_id, case_row.get("customer_id"))
|
||
|
|
|
||
|
|
start_date = payload.start_date or date.today()
|
||
|
|
items = _fetch_template_items(payload.template_id)
|
||
|
|
preview = _build_preview(
|
||
|
|
items,
|
||
|
|
start_date,
|
||
|
|
payload.mode,
|
||
|
|
payload.assignee_mode,
|
||
|
|
payload.assignee_user_id,
|
||
|
|
payload.assignee_role_id,
|
||
|
|
)
|
||
|
|
|
||
|
|
return {
|
||
|
|
"case_id": case_id,
|
||
|
|
"template": {
|
||
|
|
"id": template.get("id"),
|
||
|
|
"name": template.get("name"),
|
||
|
|
"template_type": template.get("template_type"),
|
||
|
|
"category": template.get("category"),
|
||
|
|
},
|
||
|
|
"mode": payload.mode,
|
||
|
|
"assignee_mode": payload.assignee_mode,
|
||
|
|
"start_date": start_date.isoformat(),
|
||
|
|
"summary": preview["summary"],
|
||
|
|
"items": preview["items"],
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
@router.post("/cases/{case_id}/run-template")
|
||
|
|
async def run_template_for_case(
|
||
|
|
case_id: int,
|
||
|
|
payload: TemplateRunRequest,
|
||
|
|
current_user: dict = Depends(require_permission("templates.run")),
|
||
|
|
):
|
||
|
|
case_row = _fetch_case(case_id)
|
||
|
|
template = _fetch_template_for_case(payload.template_id, case_row.get("customer_id"))
|
||
|
|
|
||
|
|
start_date = payload.start_date or date.today()
|
||
|
|
items = _fetch_template_items(payload.template_id)
|
||
|
|
preview = _build_preview(
|
||
|
|
items,
|
||
|
|
start_date,
|
||
|
|
payload.mode,
|
||
|
|
payload.assignee_mode,
|
||
|
|
payload.assignee_user_id,
|
||
|
|
payload.assignee_role_id,
|
||
|
|
)
|
||
|
|
|
||
|
|
run_row = execute_query_single(
|
||
|
|
"""
|
||
|
|
INSERT INTO case_template_runs (case_id, template_id, started_by, started_at, status)
|
||
|
|
VALUES (%s, %s, %s, NOW(), 'running')
|
||
|
|
RETURNING id
|
||
|
|
""",
|
||
|
|
(case_id, payload.template_id, current_user.get("id")),
|
||
|
|
)
|
||
|
|
if not run_row:
|
||
|
|
raise HTTPException(status_code=500, detail="Failed to start template run")
|
||
|
|
|
||
|
|
run_id = run_row.get("id")
|
||
|
|
created_task_ids: List[int] = []
|
||
|
|
created_case_ids: List[int] = []
|
||
|
|
|
||
|
|
try:
|
||
|
|
for item in preview["items"]:
|
||
|
|
created_task_id = None
|
||
|
|
created_case_id = None
|
||
|
|
|
||
|
|
if item["item_type"] == "task":
|
||
|
|
task_row = execute_query_single(
|
||
|
|
"""
|
||
|
|
INSERT INTO sag_todo_steps (sag_id, title, description, due_date, created_by_user_id)
|
||
|
|
VALUES (%s, %s, %s, %s, %s)
|
||
|
|
RETURNING id
|
||
|
|
""",
|
||
|
|
(
|
||
|
|
case_id,
|
||
|
|
item["title"],
|
||
|
|
item.get("description"),
|
||
|
|
item.get("planned_due_date"),
|
||
|
|
current_user.get("id"),
|
||
|
|
),
|
||
|
|
)
|
||
|
|
created_task_id = task_row.get("id") if task_row else None
|
||
|
|
if created_task_id:
|
||
|
|
created_task_ids.append(created_task_id)
|
||
|
|
|
||
|
|
elif item["item_type"] == "subcase":
|
||
|
|
subcase_row = execute_query_single(
|
||
|
|
"""
|
||
|
|
INSERT INTO sag_sager (
|
||
|
|
titel,
|
||
|
|
beskrivelse,
|
||
|
|
template_key,
|
||
|
|
status,
|
||
|
|
customer_id,
|
||
|
|
ansvarlig_bruger_id,
|
||
|
|
created_by_user_id,
|
||
|
|
deadline
|
||
|
|
)
|
||
|
|
VALUES (%s, %s, %s, 'åben', %s, %s, %s, %s::date + time '09:00')
|
||
|
|
RETURNING id
|
||
|
|
""",
|
||
|
|
(
|
||
|
|
item["title"],
|
||
|
|
item.get("description"),
|
||
|
|
"task_template",
|
||
|
|
case_row.get("customer_id"),
|
||
|
|
case_row.get("ansvarlig_bruger_id"),
|
||
|
|
current_user.get("id"),
|
||
|
|
item.get("planned_due_date"),
|
||
|
|
),
|
||
|
|
)
|
||
|
|
created_case_id = subcase_row.get("id") if subcase_row else None
|
||
|
|
if created_case_id:
|
||
|
|
created_case_ids.append(created_case_id)
|
||
|
|
execute_query(
|
||
|
|
"""
|
||
|
|
INSERT INTO sag_relationer (kilde_sag_id, målsag_id, relationstype)
|
||
|
|
VALUES (%s, %s, %s)
|
||
|
|
""",
|
||
|
|
(case_id, created_case_id, "undersag"),
|
||
|
|
)
|
||
|
|
|
||
|
|
execute_query(
|
||
|
|
"""
|
||
|
|
INSERT INTO case_template_run_items (
|
||
|
|
template_run_id, template_item_id, created_case_id, created_task_id, status
|
||
|
|
)
|
||
|
|
VALUES (%s, %s, %s, %s, %s)
|
||
|
|
""",
|
||
|
|
(
|
||
|
|
run_id,
|
||
|
|
item.get("template_item_id"),
|
||
|
|
created_case_id,
|
||
|
|
created_task_id,
|
||
|
|
"created" if (created_case_id or created_task_id) else "skipped",
|
||
|
|
),
|
||
|
|
)
|
||
|
|
|
||
|
|
execute_query(
|
||
|
|
"UPDATE case_template_runs SET status = 'completed', updated_at = NOW() WHERE id = %s",
|
||
|
|
(run_id,),
|
||
|
|
)
|
||
|
|
except Exception as exc:
|
||
|
|
logger.error("❌ Template run %s failed: %s", run_id, exc)
|
||
|
|
execute_query(
|
||
|
|
"UPDATE case_template_runs SET status = 'failed', error_message = %s, updated_at = NOW() WHERE id = %s",
|
||
|
|
(str(exc), run_id),
|
||
|
|
)
|
||
|
|
raise HTTPException(status_code=500, detail="Template run failed")
|
||
|
|
|
||
|
|
return {
|
||
|
|
"status": "completed",
|
||
|
|
"run_id": run_id,
|
||
|
|
"case_id": case_id,
|
||
|
|
"template": {
|
||
|
|
"id": template.get("id"),
|
||
|
|
"name": template.get("name"),
|
||
|
|
},
|
||
|
|
"summary": preview["summary"],
|
||
|
|
"created": {
|
||
|
|
"task_ids": created_task_ids,
|
||
|
|
"subcase_ids": created_case_ids,
|
||
|
|
},
|
||
|
|
"links": {
|
||
|
|
"original_case_id": case_id,
|
||
|
|
"related_subcases": created_case_ids,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("/cases/{case_id}/template-runs")
|
||
|
|
async def list_template_runs_for_case(
|
||
|
|
case_id: int,
|
||
|
|
limit: int = Query(10, ge=1, le=50),
|
||
|
|
current_user: dict = Depends(require_permission("templates.view")),
|
||
|
|
):
|
||
|
|
del current_user
|
||
|
|
|
||
|
|
_fetch_case(case_id)
|
||
|
|
|
||
|
|
rows = execute_query(
|
||
|
|
"""
|
||
|
|
SELECT
|
||
|
|
r.id,
|
||
|
|
r.case_id,
|
||
|
|
r.template_id,
|
||
|
|
t.name AS template_name,
|
||
|
|
r.started_by,
|
||
|
|
r.started_at,
|
||
|
|
r.status,
|
||
|
|
r.error_message,
|
||
|
|
COUNT(ri.id) AS item_count,
|
||
|
|
COUNT(ri.id) FILTER (WHERE ri.created_task_id IS NOT NULL) AS created_tasks,
|
||
|
|
COUNT(ri.id) FILTER (WHERE ri.created_case_id IS NOT NULL) AS created_subcases
|
||
|
|
FROM case_template_runs r
|
||
|
|
LEFT JOIN task_templates t ON t.id = r.template_id
|
||
|
|
LEFT JOIN case_template_run_items ri ON ri.template_run_id = r.id
|
||
|
|
WHERE r.case_id = %s
|
||
|
|
GROUP BY r.id, t.name
|
||
|
|
ORDER BY r.started_at DESC, r.id DESC
|
||
|
|
LIMIT %s
|
||
|
|
""",
|
||
|
|
(case_id, limit),
|
||
|
|
) or []
|
||
|
|
|
||
|
|
return rows
|