""" 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(check_query, (worklog_id,), fetchone=True) 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(check_query, (worklog_id,), fetchone=True) 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("/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 = execute_query(stats_query, fetchone=True) # 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 = execute_query(worklog_stats_query, fetchone=True) return templates.TemplateResponse( "ticket/frontend/dashboard.html", { "request": request, "stats": stats, "recent_tickets": recent_tickets, "worklog_stats": worklog_stats } ) except Exception as e: logger.error(f"❌ Failed to load dashboard: {e}") 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(ticket_query, (ticket_id,), fetchone=True) 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))