bmc_hub/app/fixed_price/frontend/views.py
Christian e4b9091a1b feat: Implement fixed-price agreements frontend views and related templates
- Added views for listing fixed-price agreements, displaying agreement details, and a reporting dashboard.
- Created HTML templates for listing, detailing, and reporting on fixed-price agreements.
- Introduced API endpoint to fetch active customers for agreement creation.
- Added migration scripts for creating necessary database tables and views for fixed-price agreements, billing periods, and reporting.
- Implemented triggers for auto-generating agreement numbers and updating timestamps.
- Enhanced ticket management with archived ticket views and filtering capabilities.
2026-02-08 01:45:00 +01:00

221 lines
7.3 KiB
Python

"""
Fixed-Price Agreement Frontend Views
"""
from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.templating import Jinja2Templates
from app.core.database import execute_query
import logging
logger = logging.getLogger(__name__)
router = APIRouter()
templates = Jinja2Templates(directory="app")
@router.get("/fixed-price-agreements", response_class=HTMLResponse)
async def list_agreements(request: Request):
"""List all fixed-price agreements"""
# Load customers for the create modal
try:
customers_query = """
SELECT
id,
name,
cvr_number,
email,
phone,
city,
is_active
FROM customers
WHERE deleted_at IS NULL
AND is_active = true
ORDER BY name
LIMIT 1000
"""
customers = execute_query(customers_query)
logger.info(f"📋 Loaded {len(customers)} customers for modal")
except Exception as e:
logger.error(f"❌ Error loading customers: {e}")
customers = []
return templates.TemplateResponse("fixed_price/frontend/list.html", {
"request": request,
"customers": customers
})
@router.get("/fixed-price-agreements/{agreement_id}", response_class=HTMLResponse)
async def agreement_detail(request: Request, agreement_id: int):
"""Agreement detail page with periods and related sager"""
from fastapi import HTTPException
try:
# Fetch agreement
agr_query = """
SELECT a.*,
COALESCE(p.used_hours, 0) as current_used_hours,
COALESCE(p.remaining_hours, a.monthly_hours) as current_remaining_hours,
p.status as current_period_status
FROM customer_fixed_price_agreements a
LEFT JOIN fixed_price_billing_periods p ON p.agreement_id = a.id
AND p.period_start <= CURRENT_DATE
AND p.period_end >= CURRENT_DATE
WHERE a.id = %s
"""
agreement = execute_query(agr_query, (agreement_id,))
if not agreement:
raise HTTPException(status_code=404, detail="Aftale ikke fundet")
agreement = agreement[0]
# Fetch all billing periods
periods_query = """
SELECT * FROM fixed_price_billing_periods
WHERE agreement_id = %s
ORDER BY period_start DESC
"""
periods = execute_query(periods_query, (agreement_id,))
# Fetch related sager
sager_query = """
SELECT DISTINCT s.id, s.titel, s.status, s.created_at
FROM sag_sager s
INNER JOIN tmodule_times t ON t.sag_id = s.id
WHERE t.fixed_price_agreement_id = %s
AND s.deleted_at IS NULL
ORDER BY s.created_at DESC
LIMIT 50
"""
sager = execute_query(sager_query, (agreement_id,))
# Fetch time entries
time_query = """
SELECT t.*, s.titel as sag_titel
FROM tmodule_times t
LEFT JOIN sag_sager s ON s.id = t.sag_id
WHERE t.fixed_price_agreement_id = %s
ORDER BY t.created_at DESC
LIMIT 100
"""
time_entries = execute_query(time_query, (agreement_id,))
return templates.TemplateResponse("fixed_price/frontend/detail.html", {
"request": request,
"agreement": agreement,
"periods": periods,
"sager": sager,
"time_entries": time_entries
})
except Exception as e:
logger.error(f"❌ Error loading agreement detail: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/fixed-price-agreements/reports/dashboard", response_class=HTMLResponse)
async def reports_dashboard(request: Request):
"""Reporting dashboard with profitability analysis"""
try:
# Get summary stats
stats_query = """
SELECT
COUNT(*) FILTER (WHERE status = 'active') as active_agreements,
COUNT(*) as total_agreements,
SUM(monthly_hours * hourly_rate) as total_revenue,
SUM(monthly_hours * (hourly_rate - 300)) as estimated_profit
FROM customer_fixed_price_agreements
"""
stats = execute_query(stats_query)
# Get performance data from view
performance_query = """
SELECT
*,
CASE
WHEN total_revenue > 0 THEN (total_profit / total_revenue * 100)
ELSE 0
END as profit_margin
FROM fixed_price_agreement_performance
ORDER BY total_profit DESC
LIMIT 50
"""
performance = execute_query(performance_query)
# Get monthly trends
trends_query = """
SELECT
*,
month as period_month,
CASE
WHEN monthly_total_revenue > 0 THEN (monthly_profit / monthly_total_revenue * 100)
ELSE 0
END as avg_profit_margin
FROM fixed_price_monthly_trends
ORDER BY month DESC
LIMIT 12
"""
trends = execute_query(trends_query)
# Get customer breakdown
customer_query = """
SELECT
*,
total_hours_used as total_used_hours,
CASE
WHEN total_revenue > 0 THEN (total_profit / total_revenue * 100)
ELSE 0
END as avg_profit_margin
FROM fixed_price_customer_summary
ORDER BY total_used_hours DESC
LIMIT 20
"""
customers = execute_query(customer_query)
return templates.TemplateResponse("fixed_price/frontend/reports.html", {
"request": request,
"stats": stats[0] if stats else {},
"performance": performance,
"trends": trends,
"customers": customers
})
except Exception as e:
logger.error(f"❌ Error loading reports: {e}")
return templates.TemplateResponse("fixed_price/frontend/reports.html", {
"request": request,
"stats": {},
"performance": [],
"trends": [],
"customers": [],
"error": str(e)
})
@router.get("/api/fixed-price-agreements/customers")
async def get_customers_for_agreements():
"""Get all active customers for fixed-price agreement creation"""
try:
query = """
SELECT
id,
name,
cvr_number,
email,
phone,
city,
is_active
FROM customers
WHERE deleted_at IS NULL
AND is_active = true
ORDER BY name
LIMIT 1000
"""
customers = execute_query(query)
logger.info(f"📋 Loaded {len(customers)} customers for fixed-price agreements")
return customers
except Exception as e:
logger.error(f"❌ Error loading customers: {e}")
return []