Self-hosted P&L tracking app with component-level pricing. Offers: Atlas/Atlas+/Rif/Rif+ with granular cost breakdown. API + MCP + multi-user auth.
160 lines
5.6 KiB
HTML
160 lines
5.6 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Dashboard — Vela Platform{% endblock %}
|
|
{% block content %}
|
|
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h2 class="mb-0"><i class="bi bi-speedometer2"></i> Dashboard</h2>
|
|
<span class="badge bg-primary fs-6">{{ pnl.month }}</span>
|
|
</div>
|
|
|
|
<!-- P&L Summary Cards -->
|
|
<div class="row g-3 mb-4">
|
|
<div class="col-md-3 col-6">
|
|
<div class="card card-stat h-100">
|
|
<div class="card-body">
|
|
<small class="text-muted">Revenue</small>
|
|
<h4 class="mt-1">{{ "%.0f"|format(pnl.total_revenue) }} MAD</h4>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3 col-6">
|
|
<div class="card card-stat h-100">
|
|
<div class="card-body">
|
|
<small class="text-muted">Cost</small>
|
|
<h4 class="mt-1">{{ "%.0f"|format(pnl.total_cost) }} MAD</h4>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3 col-6">
|
|
<div class="card card-stat green h-100">
|
|
<div class="card-body">
|
|
<small class="text-muted">Gross Profit</small>
|
|
<h4 class="mt-1">{{ "%.0f"|format(pnl.gross_profit) }} MAD</h4>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3 col-6">
|
|
<div class="card card-stat {{ 'green' if pnl.net_profit >= 0 else 'red' }} h-100">
|
|
<div class="card-body">
|
|
<small class="text-muted">Net Profit</small>
|
|
<h4 class="mt-1">{{ "%.0f"|format(pnl.net_profit) }} MAD</h4>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-3 mb-4">
|
|
<div class="col-md-6">
|
|
<div class="card h-100">
|
|
<div class="card-header">Margin Breakdown</div>
|
|
<div class="card-body">
|
|
<table class="table table-sm mb-0">
|
|
<tr><td>Gross Margin</td><td class="text-end">{{ "%.1f"|format(pnl.gross_margin_pct) }}%</td></tr>
|
|
<tr><td>OpEx (fixed)</td><td class="text-end">{{ "%.0f"|format(pnl.opex) }} MAD</td></tr>
|
|
<tr class="fw-bold"><td>Net Margin</td><td class="text-end">{{ "%.1f"|format(pnl.net_margin_pct) }}%</td></tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="card h-100">
|
|
<div class="card-header">Active Offers — Cost Breakdown</div>
|
|
<div class="card-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-sm mb-0">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>Offer</th>
|
|
<th class="text-end">Sell</th>
|
|
<th class="text-end">Cost</th>
|
|
<th class="text-end">Margin</th>
|
|
<th class="text-end">Components</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for s in services %}
|
|
<tr>
|
|
<td><strong>{{ s.name }}</strong></td>
|
|
<td class="text-end">{{ "%.0f"|format(s.sell_price) }} MAD</td>
|
|
<td class="text-end text-muted">{{ "%.0f"|format(s.cost_price) }} MAD</td>
|
|
<td class="text-end {{ 'text-success' if (s.sell_price - s.cost_price) > 0 else 'text-danger' }}">
|
|
{{ "%.0f"|format(s.sell_price - s.cost_price) }} MAD
|
|
</td>
|
|
<td class="text-end text-muted">
|
|
<small>{{ s.components|length }} items</small>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 12-Month Trend Chart -->
|
|
<div class="card mb-4">
|
|
<div class="card-header">12-Month Trend</div>
|
|
<div class="card-body">
|
|
<canvas id="trendChart" height="80"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
{% endblock %}
|
|
{% block scripts %}
|
|
<script>
|
|
const trendData = {{ trend | tojson }};
|
|
|
|
const labels = trendData.map(d => d.month);
|
|
const revenue = trendData.map(d => d.total_revenue);
|
|
const cost = trendData.map(d => d.total_cost);
|
|
const netProfit = trendData.map(d => d.net_profit);
|
|
|
|
new Chart(document.getElementById('trendChart'), {
|
|
type: 'line',
|
|
data: {
|
|
labels: labels,
|
|
datasets: [
|
|
{
|
|
label: 'Revenue',
|
|
data: revenue,
|
|
borderColor: '#0d6efd',
|
|
backgroundColor: 'rgba(13,110,253,0.1)',
|
|
fill: true,
|
|
tension: 0.3,
|
|
},
|
|
{
|
|
label: 'Cost',
|
|
data: cost,
|
|
borderColor: '#dc3545',
|
|
backgroundColor: 'rgba(220,53,69,0.1)',
|
|
fill: true,
|
|
tension: 0.3,
|
|
},
|
|
{
|
|
label: 'Net Profit',
|
|
data: netProfit,
|
|
borderColor: '#198754',
|
|
backgroundColor: 'rgba(25,135,84,0.1)',
|
|
fill: true,
|
|
tension: 0.3,
|
|
borderWidth: 2,
|
|
},
|
|
],
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
plugins: {
|
|
legend: { position: 'bottom' },
|
|
},
|
|
scales: {
|
|
y: {
|
|
ticks: { callback: v => v + ' MAD' },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
</script>
|
|
{% endblock %}
|