- 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.
221 lines
7.3 KiB
Python
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 []
|