"""
Ticket System Frontend Views
HTML template routes for ticket management UI
"""
import logging
from fastapi import APIRouter, Request, HTTPException, Form
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from typing import Optional
from datetime import date
from app.core.database import execute_query, execute_update
logger = logging.getLogger(__name__)
router = APIRouter()
templates = Jinja2Templates(directory="app")
# ============================================================================
# MOCKUP ROUTES (TEMPORARY)
# ============================================================================
@router.get("/mockups/1", response_class=HTMLResponse)
async def mockup_option1(request: Request):
"""Mockup: Split Screen Concept"""
return templates.TemplateResponse("ticket/frontend/mockups/option1_splitscreen.html", {"request": request})
@router.get("/mockups/2", response_class=HTMLResponse)
async def mockup_option2(request: Request):
"""Mockup: Kanban Board Concept"""
return templates.TemplateResponse("ticket/frontend/mockups/option2_kanban.html", {"request": request})
@router.get("/mockups/3", response_class=HTMLResponse)
async def mockup_option3(request: Request):
"""Mockup: Power Table Concept"""
return templates.TemplateResponse("ticket/frontend/mockups/option3_powertable.html", {"request": request})
@router.get("/worklog/review", response_class=HTMLResponse)
async def worklog_review_page(
request: Request,
customer_id: Optional[int] = None,
status: str = "draft"
):
"""
Worklog review page with single-entry approval
Query params:
customer_id: Filter by customer (optional)
status: Filter by status (default: draft)
"""
try:
# Build query with filters
query = """
SELECT
w.id,
w.ticket_id,
w.user_id,
w.work_date,
w.hours,
w.work_type,
w.description,
w.billing_method,
w.status,
w.prepaid_card_id,
w.created_at,
t.ticket_number,
t.subject AS ticket_subject,
t.customer_id,
t.status AS ticket_status,
c.name AS customer_name,
u.username AS user_name,
pc.card_number,
pc.remaining_hours AS card_remaining_hours
FROM tticket_worklog w
INNER JOIN tticket_tickets t ON t.id = w.ticket_id
LEFT JOIN customers c ON c.id = t.customer_id
LEFT JOIN users u ON u.user_id = w.user_id
LEFT JOIN tticket_prepaid_cards pc ON pc.id = w.prepaid_card_id
WHERE w.status = %s
"""
params = [status]
if customer_id:
query += " AND t.customer_id = %s"
params.append(customer_id)
query += " ORDER BY w.work_date DESC, w.created_at DESC"
worklogs = execute_query(query, tuple(params))
# Get customer list for filter dropdown
customers_query = """
SELECT DISTINCT c.id, c.name
FROM customers c
INNER JOIN tticket_tickets t ON t.customer_id = c.id
INNER JOIN tticket_worklog w ON w.ticket_id = t.id
WHERE w.status = %s
ORDER BY c.name
"""
customers = execute_query(customers_query, (status,))
# Calculate totals
total_hours = sum(float(w['hours']) for w in worklogs)
total_billable = sum(
float(w['hours'])
for w in worklogs
if w['billing_method'] == 'invoice'
)
return templates.TemplateResponse(
"ticket/frontend/worklog_review.html",
{
"request": request,
"worklogs": worklogs,
"customers": customers,
"selected_customer_id": customer_id,
"selected_status": status,
"total_hours": total_hours,
"total_billable_hours": total_billable,
"total_entries": len(worklogs)
}
)
except Exception as e:
logger.error(f"❌ Failed to load worklog review page: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/worklog/{worklog_id}/approve")
async def approve_worklog_entry(
worklog_id: int,
redirect_to: Optional[str] = Form(default="/ticket/worklog/review")
):
"""
Approve single worklog entry (change status draft → billable)
Form params:
redirect_to: URL to redirect after approval
"""
try:
# Check entry exists and is draft
check_query = """
SELECT id, status, billing_method
FROM tticket_worklog
WHERE id = %s
"""
entry = execute_query_single(check_query, (worklog_id,))
if not entry:
raise HTTPException(status_code=404, detail="Worklog entry not found")
if entry['status'] != 'draft':
raise HTTPException(
status_code=400,
detail=f"Cannot approve entry with status '{entry['status']}'"
)
# Approve entry
update_query = """
UPDATE tticket_worklog
SET status = 'billable',
updated_at = CURRENT_TIMESTAMP
WHERE id = %s
"""
execute_update(update_query, (worklog_id,))
logger.info(f"✅ Approved worklog entry {worklog_id}")
return RedirectResponse(url=redirect_to, status_code=303)
except HTTPException:
raise
except Exception as e:
logger.error(f"❌ Failed to approve worklog entry: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/worklog/{worklog_id}/reject")
async def reject_worklog_entry(
worklog_id: int,
reason: Optional[str] = Form(default=None),
redirect_to: Optional[str] = Form(default="/ticket/worklog/review")
):
"""
Reject single worklog entry (change status draft → rejected)
Form params:
reason: Rejection reason (optional)
redirect_to: URL to redirect after rejection
"""
try:
# Check entry exists and is draft
check_query = """
SELECT id, status
FROM tticket_worklog
WHERE id = %s
"""
entry = execute_query_single(check_query, (worklog_id,))
if not entry:
raise HTTPException(status_code=404, detail="Worklog entry not found")
if entry['status'] != 'draft':
raise HTTPException(
status_code=400,
detail=f"Cannot reject entry with status '{entry['status']}'"
)
# Reject entry (store reason in description)
update_query = """
UPDATE tticket_worklog
SET status = 'rejected',
description = COALESCE(description, '') ||
CASE WHEN %s IS NOT NULL
THEN E'\n\n[REJECTED: ' || %s || ']'
ELSE E'\n\n[REJECTED]'
END,
updated_at = CURRENT_TIMESTAMP
WHERE id = %s
"""
execute_update(update_query, (reason, reason, worklog_id))
logger.info(f"❌ Rejected worklog entry {worklog_id}" + (f": {reason}" if reason else ""))
return RedirectResponse(url=redirect_to, status_code=303)
except HTTPException:
raise
except Exception as e:
logger.error(f"❌ Failed to reject worklog entry: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/tickets/new", response_class=HTMLResponse)
async def new_ticket_page(request: Request):
"""
New ticket creation page with multi-step wizard
"""
return templates.TemplateResponse("ticket/frontend/ticket_new.html", {"request": request})
@router.get("/dashboard", response_class=HTMLResponse)
async def ticket_dashboard(request: Request):
"""
Ticket system dashboard with statistics
"""
try:
# Get ticket statistics
stats_query = """
SELECT
COUNT(*) FILTER (WHERE status = 'open') AS open_count,
COUNT(*) FILTER (WHERE status = 'in_progress') AS in_progress_count,
COUNT(*) FILTER (WHERE status = 'pending_customer') AS pending_count,
COUNT(*) FILTER (WHERE status = 'resolved') AS resolved_count,
COUNT(*) FILTER (WHERE status = 'closed') AS closed_count,
COUNT(*) AS total_count
FROM tticket_tickets
"""
stats_result = execute_query(stats_query)
stats = stats_result[0] if stats_result else {}
# Get recent tickets
recent_query = """
SELECT
t.id,
t.ticket_number,
t.subject,
t.status,
t.priority,
t.created_at,
c.name AS customer_name
FROM tticket_tickets t
LEFT JOIN customers c ON c.id = t.customer_id
ORDER BY t.created_at DESC
LIMIT 10
"""
recent_tickets = execute_query(recent_query)
# Get worklog statistics
worklog_stats_query = """
SELECT
COUNT(*) FILTER (WHERE status = 'draft') AS draft_count,
COALESCE(SUM(hours) FILTER (WHERE status = 'draft'), 0) AS draft_hours,
COUNT(*) FILTER (WHERE status = 'billable') AS billable_count,
COALESCE(SUM(hours) FILTER (WHERE status = 'billable'), 0) AS billable_hours
FROM tticket_worklog
"""
worklog_stats_result = execute_query(worklog_stats_query)
worklog_stats = worklog_stats_result[0] if worklog_stats_result else {}
return templates.TemplateResponse(
"ticket/frontend/dashboard.html",
{
"request": request,
"stats": stats,
"recent_tickets": recent_tickets or [],
"worklog_stats": worklog_stats
}
)
except Exception as e:
logger.error(f"❌ Failed to load dashboard: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
@router.get("/tickets", response_class=HTMLResponse)
async def ticket_list_page(
request: Request,
status: Optional[str] = None,
priority: Optional[str] = None,
customer_id: Optional[int] = None,
search: Optional[str] = None
):
"""
Ticket list page with filters
"""
try:
# Build query with filters
query = """
SELECT
t.id,
t.ticket_number,
t.subject,
t.status,
t.priority,
t.created_at,
t.updated_at,
c.name AS customer_name,
u.username AS assigned_to_name,
(SELECT COUNT(*) FROM tticket_comments WHERE ticket_id = t.id) AS comment_count,
(SELECT COUNT(*) FROM tticket_worklog WHERE ticket_id = t.id) AS worklog_count
FROM tticket_tickets t
LEFT JOIN customers c ON c.id = t.customer_id
LEFT JOIN users u ON u.user_id = t.assigned_to_user_id
WHERE 1=1
"""
params = []
if status:
query += " AND t.status = %s"
params.append(status)
if priority:
query += " AND t.priority = %s"
params.append(priority)
if customer_id:
query += " AND t.customer_id = %s"
params.append(customer_id)
if search:
query += " AND (t.subject ILIKE %s OR t.description ILIKE %s OR t.ticket_number ILIKE %s)"
search_pattern = f"%{search}%"
params.extend([search_pattern, search_pattern, search_pattern])
query += " ORDER BY t.created_at DESC LIMIT 100"
tickets = execute_query(query, tuple(params)) if params else execute_query(query)
# Get filter options
customers = execute_query(
"""SELECT DISTINCT c.id, c.name
FROM customers c
INNER JOIN tticket_tickets t ON t.customer_id = c.id
ORDER BY c.name"""
)
return templates.TemplateResponse(
"ticket/frontend/ticket_list.html",
{
"request": request,
"tickets": tickets,
"customers": customers,
"selected_status": status,
"selected_priority": priority,
"selected_customer_id": customer_id,
"search_query": search
}
)
except Exception as e:
logger.error(f"❌ Failed to load ticket list: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/tickets/{ticket_id}", response_class=HTMLResponse)
async def ticket_detail_page(request: Request, ticket_id: int):
"""
Ticket detail page with comments and worklog
"""
try:
# Get ticket details
ticket_query = """
SELECT
t.*,
c.name AS customer_name,
c.email AS customer_email,
u.username AS assigned_to_name
FROM tticket_tickets t
LEFT JOIN customers c ON c.id = t.customer_id
LEFT JOIN users u ON u.user_id = t.assigned_to_user_id
WHERE t.id = %s
"""
ticket = execute_query_single(ticket_query, (ticket_id,))
if not ticket:
raise HTTPException(status_code=404, detail="Ticket not found")
# Get comments
comments_query = """
SELECT
c.*,
u.username AS user_name
FROM tticket_comments c
LEFT JOIN users u ON u.user_id = c.user_id
WHERE c.ticket_id = %s
ORDER BY c.created_at ASC
"""
comments = execute_query(comments_query, (ticket_id,))
# Get worklog
worklog_query = """
SELECT
w.*,
u.username AS user_name
FROM tticket_worklog w
LEFT JOIN users u ON u.user_id = w.user_id
WHERE w.ticket_id = %s
ORDER BY w.work_date DESC, w.created_at DESC
"""
worklog = execute_query(worklog_query, (ticket_id,))
# Get attachments
attachments_query = """
SELECT * FROM tticket_attachments
WHERE ticket_id = %s
ORDER BY created_at DESC
"""
attachments = execute_query(attachments_query, (ticket_id,))
return templates.TemplateResponse(
"ticket/frontend/ticket_detail.html",
{
"request": request,
"ticket": ticket,
"comments": comments,
"worklog": worklog,
"attachments": attachments
}
)
except HTTPException:
raise
except Exception as e:
logger.error(f"❌ Failed to load ticket detail: {e}")
raise HTTPException(status_code=500, detail=str(e))