- 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.
286 lines
15 KiB
HTML
286 lines
15 KiB
HTML
{% extends "shared/frontend/base.html" %}
|
|
|
|
{% block title %}Fastpris Rapporter - BMC Hub{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid py-4">
|
|
<!-- Header -->
|
|
<div class="row mb-4">
|
|
<div class="col">
|
|
<nav aria-label="breadcrumb">
|
|
<ol class="breadcrumb">
|
|
<li class="breadcrumb-item"><a href="/fixed-price-agreements">Fastpris Aftaler</a></li>
|
|
<li class="breadcrumb-item active">Rapporter</li>
|
|
</ol>
|
|
</nav>
|
|
<h1 class="h3 mb-0">📊 Fastpris Rapporter</h1>
|
|
<p class="text-muted">Profitabilitet og performance analyse</p>
|
|
</div>
|
|
</div>
|
|
|
|
{% if error %}
|
|
<div class="alert alert-danger">
|
|
<i class="bi bi-exclamation-triangle me-2"></i>
|
|
<strong>Fejl:</strong> {{ error }}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Summary Stats -->
|
|
<div class="row g-3 mb-4">
|
|
<div class="col-md-3">
|
|
<div class="card border-0 shadow-sm">
|
|
<div class="card-body">
|
|
<h6 class="text-muted mb-2">Aktive Aftaler</h6>
|
|
<h3 class="mb-0">{{ stats.active_agreements or 0 }}</h3>
|
|
<small class="text-muted">af {{ stats.total_agreements or 0 }} total</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card border-0 shadow-sm">
|
|
<div class="card-body">
|
|
<h6 class="text-muted mb-2">Total Omsætning</h6>
|
|
<h3 class="mb-0">{{ "{:,.0f}".format(stats.total_revenue or 0) }} kr</h3>
|
|
<small class="text-muted">Månedlig værdi</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card border-0 shadow-sm">
|
|
<div class="card-body">
|
|
<h6 class="text-muted mb-2">Estimeret Profit</h6>
|
|
<h3 class="mb-0">{{ "{:,.0f}".format(stats.estimated_profit or 0) }} kr</h3>
|
|
<small class="text-muted">Ved 300 kr/t kostpris</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card border-0 shadow-sm">
|
|
<div class="card-body">
|
|
<h6 class="text-muted mb-2">Profit Margin</h6>
|
|
{% set profit_margin = ((stats.estimated_profit|float / stats.total_revenue|float * 100)|round(1)) if stats.total_revenue and stats.total_revenue > 0 else 0 %}
|
|
<h3 class="mb-0">{{ profit_margin }}%</h3>
|
|
<small class="text-muted">Gennemsnitlig margin</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tabs -->
|
|
<ul class="nav nav-tabs mb-3" role="tablist">
|
|
<li class="nav-item">
|
|
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#performance-tab">Performance</button>
|
|
</li>
|
|
<li class="nav-item">
|
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#trends-tab">Trends</button>
|
|
</li>
|
|
<li class="nav-item">
|
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#customers-tab">Kunder</button>
|
|
</li>
|
|
</ul>
|
|
|
|
<!-- Tab Content -->
|
|
<div class="tab-content">
|
|
<!-- Performance Tab -->
|
|
<div class="tab-pane fade show active" id="performance-tab">
|
|
<div class="card border-0 shadow-sm">
|
|
<div class="card-header bg-white">
|
|
<h5 class="mb-0">Aftale Performance</h5>
|
|
<small class="text-muted">Sorteret efter profit margin</small>
|
|
</div>
|
|
<div class="card-body">
|
|
{% if performance %}
|
|
<div class="table-responsive">
|
|
<table class="table table-hover">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>Aftale</th>
|
|
<th>Kunde</th>
|
|
<th class="text-end">Total Timer</th>
|
|
<th class="text-end">Månedlig Værdi</th>
|
|
<th class="text-end">Profit</th>
|
|
<th class="text-end">Margin</th>
|
|
<th class="text-end">Udnyttelse</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for agr in performance %}
|
|
<tr>
|
|
<td>
|
|
<a href="/fixed-price-agreements/{{ agr.agreement_id }}">
|
|
<strong>{{ agr.agreement_number }}</strong>
|
|
</a>
|
|
</td>
|
|
<td>{{ agr.customer_name }}</td>
|
|
<td class="text-end">{{ '%.1f'|format(agr.total_used_hours or 0) }}t</td>
|
|
<td class="text-end">{{ "{:,.0f}".format(agr.total_base_revenue or 0) }} kr</td>
|
|
<td class="text-end">
|
|
{% if agr.estimated_profit and agr.estimated_profit > 0 %}
|
|
<span class="text-success">{{ "{:,.0f}".format(agr.estimated_profit) }} kr</span>
|
|
{% else %}
|
|
<span class="text-danger">{{ "{:,.0f}".format(agr.estimated_profit or 0) }} kr</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="text-end">
|
|
{% if agr.profit_margin and agr.profit_margin >= 30 %}
|
|
<span class="badge bg-success">{{ '%.1f'|format(agr.profit_margin) }}%</span>
|
|
{% elif agr.profit_margin and agr.profit_margin >= 15 %}
|
|
<span class="badge bg-warning">{{ '%.1f'|format(agr.profit_margin) }}%</span>
|
|
{% else %}
|
|
<span class="badge bg-danger">{{ '%.1f'|format(agr.profit_margin or 0) }}%</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="text-end">
|
|
{% set utilization = ((agr.total_used_hours or 0) / (agr.total_allocated_hours or 1) * 100) if agr.total_allocated_hours else 0 %}
|
|
{% if utilization >= 80 %}
|
|
<span class="badge bg-success">{{ '%.0f'|format(utilization) }}%</span>
|
|
{% elif utilization >= 50 %}
|
|
<span class="badge bg-info">{{ '%.0f'|format(utilization) }}%</span>
|
|
{% else %}
|
|
<span class="badge bg-secondary">{{ '%.0f'|format(utilization) }}%</span>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<div class="text-center text-muted py-5">
|
|
<i class="bi bi-graph-down fs-1 mb-3"></i>
|
|
<p>Ingen performance data tilgængelig</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Trends Tab -->
|
|
<div class="tab-pane fade" id="trends-tab">
|
|
<div class="card border-0 shadow-sm">
|
|
<div class="card-header bg-white">
|
|
<h5 class="mb-0">Månedlige Trends</h5>
|
|
<small class="text-muted">Seneste 12 måneder</small>
|
|
</div>
|
|
<div class="card-body">
|
|
{% if trends %}
|
|
<div class="table-responsive">
|
|
<table class="table table-hover">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>Måned</th>
|
|
<th class="text-end">Aktive Aftaler</th>
|
|
<th class="text-end">Brugte Timer</th>
|
|
<th class="text-end">Overtid Timer</th>
|
|
<th class="text-end">Total Værdi</th>
|
|
<th class="text-end">Profit</th>
|
|
<th class="text-end">Margin</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for trend in trends %}
|
|
<tr>
|
|
<td><strong>{{ trend.period_month }}</strong></td>
|
|
<td class="text-end">{{ trend.active_agreements }}</td>
|
|
<td class="text-end">{{ '%.1f'|format(trend.total_used_hours or 0) }}t</td>
|
|
<td class="text-end">
|
|
{% if trend.total_overtime_hours and trend.total_overtime_hours > 0 %}
|
|
<span class="text-warning">{{ '%.1f'|format(trend.total_overtime_hours) }}t</span>
|
|
{% else %}
|
|
<span class="text-muted">-</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="text-end">{{ "{:,.0f}".format(trend.monthly_total_revenue or 0) }} kr</td>
|
|
<td class="text-end">
|
|
{% if trend.total_profit and trend.total_profit > 0 %}
|
|
<span class="text-success">{{ "{:,.0f}".format(trend.total_profit) }} kr</span>
|
|
{% else %}
|
|
<span class="text-danger">{{ "{:,.0f}".format(trend.total_profit or 0) }} kr</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="text-end">
|
|
{% if trend.avg_profit_margin and trend.avg_profit_margin >= 30 %}
|
|
<span class="badge bg-success">{{ '%.1f'|format(trend.avg_profit_margin) }}%</span>
|
|
{% elif trend.avg_profit_margin and trend.avg_profit_margin >= 15 %}
|
|
<span class="badge bg-warning">{{ '%.1f'|format(trend.avg_profit_margin) }}%</span>
|
|
{% else %}
|
|
<span class="badge bg-danger">{{ '%.1f'|format(trend.avg_profit_margin or 0) }}%</span>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<div class="text-center text-muted py-5">
|
|
<i class="bi bi-calendar3 fs-1 mb-3"></i>
|
|
<p>Ingen trend data tilgængelig</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Customers Tab -->
|
|
<div class="tab-pane fade" id="customers-tab">
|
|
<div class="card border-0 shadow-sm">
|
|
<div class="card-header bg-white">
|
|
<h5 class="mb-0">Top Kunder</h5>
|
|
<small class="text-muted">Sorteret efter total forbrug</small>
|
|
</div>
|
|
<div class="card-body">
|
|
{% if customers %}
|
|
<div class="table-responsive">
|
|
<table class="table table-hover">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>Kunde</th>
|
|
<th class="text-end">Aftaler</th>
|
|
<th class="text-end">Total Timer</th>
|
|
<th class="text-end">Overtid</th>
|
|
<th class="text-end">Total Værdi</th>
|
|
<th class="text-end">Avg Margin</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for customer in customers %}
|
|
<tr>
|
|
<td><strong>{{ customer.customer_name }}</strong></td>
|
|
<td class="text-end">{{ customer.agreement_count }}</td>
|
|
<td class="text-end">{{ '%.1f'|format(customer.total_used_hours or 0) }}t</td>
|
|
<td class="text-end">
|
|
{% if customer.total_overtime_hours and customer.total_overtime_hours > 0 %}
|
|
<span class="text-warning">{{ '%.1f'|format(customer.total_overtime_hours) }}t</span>
|
|
{% else %}
|
|
<span class="text-muted">-</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="text-end">{{ "{:,.0f}".format(customer.total_revenue or 0) }} kr</td>
|
|
<td class="text-end">
|
|
{% if customer.avg_profit_margin and customer.avg_profit_margin >= 30 %}
|
|
<span class="badge bg-success">{{ '%.1f'|format(customer.avg_profit_margin) }}%</span>
|
|
{% elif customer.avg_profit_margin and customer.avg_profit_margin >= 15 %}
|
|
<span class="badge bg-warning">{{ '%.1f'|format(customer.avg_profit_margin) }}%</span>
|
|
{% else %}
|
|
<span class="badge bg-danger">{{ '%.1f'|format(customer.avg_profit_margin or 0) }}%</span>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<div class="text-center text-muted py-5">
|
|
<i class="bi bi-people fs-1 mb-3"></i>
|
|
<p>Ingen kunde data tilgængelig</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|