274 lines
12 KiB
HTML
274 lines
12 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %}统计报表{% endblock %}
|
||
{% block content %}
|
||
<div class="page-toolbar">
|
||
<div>
|
||
<h4 class="page-title"><i class="bi bi-bar-chart-fill"></i> 统计报表</h4>
|
||
<p class="page-sub text-muted">
|
||
数据来源:课时核对表 Excel · 当前 <strong class="text-primary">{{ period_label }}</strong>
|
||
({{ month_accounts }} 条学员×课程)
|
||
</p>
|
||
</div>
|
||
{% if current_user.has_permission('statistics_export') %}
|
||
<a href="{{ url_for('statistics_export', year=year, month=month) }}" class="btn btn-neo-primary btn-sm">
|
||
<i class="bi bi-download"></i> 导出
|
||
</a>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<div class="neo-surface p-3 mb-4">
|
||
<form method="GET" action="{{ url_for('statistics') }}" class="row g-3 align-items-end" id="statsPeriodForm">
|
||
<div class="col-md-4 col-lg-3">
|
||
<label class="form-label fw-semibold mb-1"><i class="bi bi-calendar3"></i> 选择统计月份</label>
|
||
{% if available_periods %}
|
||
<select class="form-select form-select-lg" name="period" id="statsPeriodSelect" aria-label="统计月份">
|
||
{% for p in available_periods %}
|
||
<option value="{{ p.year }}-{{ p.month }}"
|
||
{% if year == p.year and month == p.month %}selected{% endif %}
|
||
{% if p.count < 20 %}data-warn="1"{% endif %}>
|
||
{{ p.year }} 年 {{ p.month }} 月({{ p.count }} 条)
|
||
</option>
|
||
{% endfor %}
|
||
</select>
|
||
{% else %}
|
||
<select class="form-select" disabled><option>尚未导入月度数据</option></select>
|
||
{% endif %}
|
||
</div>
|
||
<div class="col-auto">
|
||
<button type="submit" class="btn btn-neo-primary"><i class="bi bi-arrow-repeat"></i> 查看</button>
|
||
</div>
|
||
{% if available_periods|length > 1 %}
|
||
<div class="col-12">
|
||
<div class="small text-muted mb-2">快捷切换</div>
|
||
<div class="d-flex flex-wrap gap-2">
|
||
{% for p in available_periods %}
|
||
<a href="{{ url_for('statistics', year=p.year, month=p.month) }}"
|
||
class="btn btn-sm {% if year == p.year and month == p.month %}btn-primary{% elif p.count < 20 %}btn-outline-warning{% else %}btn-outline-secondary{% endif %}">
|
||
{{ p.year }}-{{ '%02d'|format(p.month) }}
|
||
<span class="badge {% if year == p.year and month == p.month %}bg-light text-primary{% else %}bg-secondary{% endif %} ms-1">{{ p.count }}</span>
|
||
</a>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
</form>
|
||
{% if not available_periods %}
|
||
<p class="text-muted small mb-0 mt-2">请先在 <a href="{{ url_for('keshibiao_index') }}">课时核对表</a> 导入 Excel。</p>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- 当月概览 -->
|
||
<div class="stat-grid mb-4">
|
||
<div class="neo-surface neo-stat">
|
||
<div class="stat-icon green"><i class="bi bi-cash-stack"></i></div>
|
||
<div>
|
||
<p class="stat-value">¥{{ "%.0f"|format(month_recharge) }}</p>
|
||
<p class="stat-label">{{ period_label }} 充值</p>
|
||
</div>
|
||
</div>
|
||
<div class="neo-surface neo-stat">
|
||
<div class="stat-icon blue"><i class="bi bi-clock-history"></i></div>
|
||
<div>
|
||
<p class="stat-value">{{ "%.0f"|format(month_consumption) }}</p>
|
||
<p class="stat-label">{{ period_label }} 消课(节)</p>
|
||
</div>
|
||
</div>
|
||
<div class="neo-surface neo-stat">
|
||
<div class="stat-icon yellow"><i class="bi bi-currency-yen"></i></div>
|
||
<div>
|
||
<p class="stat-value">¥{{ "%.0f"|format(month_consumed_amount) }}</p>
|
||
<p class="stat-label">{{ period_label }} 已消金额</p>
|
||
</div>
|
||
</div>
|
||
<div class="neo-surface neo-stat">
|
||
<div class="stat-icon purple"><i class="bi bi-arrow-return-left"></i></div>
|
||
<div>
|
||
<p class="stat-value">¥{{ "%.0f"|format(month_refund) }}</p>
|
||
<p class="stat-label">{{ period_label }} 退费</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stat-grid mb-4">
|
||
<div class="neo-surface neo-stat">
|
||
<div class="stat-icon green"><i class="bi bi-wallet2"></i></div>
|
||
<div>
|
||
<p class="stat-value">¥{{ "%.0f"|format(month_end_balance) }}</p>
|
||
<p class="stat-label">{{ period_label }} 月末余额合计</p>
|
||
</div>
|
||
</div>
|
||
<div class="neo-surface neo-stat">
|
||
<div class="stat-icon blue"><i class="bi bi-person-plus"></i></div>
|
||
<div>
|
||
<p class="stat-value">{{ month_new_signups }}</p>
|
||
<p class="stat-label">{{ period_label }} 新签学员</p>
|
||
</div>
|
||
</div>
|
||
<div class="neo-surface neo-stat">
|
||
<div class="stat-icon purple"><i class="bi bi-people-fill"></i></div>
|
||
<div>
|
||
<p class="stat-value">{{ active_students }}</p>
|
||
<p class="stat-label">在读学员</p>
|
||
</div>
|
||
</div>
|
||
<div class="neo-surface neo-stat">
|
||
<div class="stat-icon yellow"><i class="bi bi-calendar-range"></i></div>
|
||
<div>
|
||
<p class="stat-value">¥{{ "%.0f"|format(year_recharge) }}</p>
|
||
<p class="stat-label">{{ year }} 年度充值累计</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{% if monthly_trend %}
|
||
<div class="neo-surface chart-card mb-4">
|
||
<div class="chart-header">
|
||
<div>
|
||
<h5 class="mb-1 fw-bold">月度趋势</h5>
|
||
<span class="text-muted" style="font-size:0.85rem;">已导入月份充值金额</span>
|
||
</div>
|
||
</div>
|
||
<div class="chart-bars">
|
||
{% set max_r = monthly_trend|map(attribute='recharge')|max %}
|
||
{% for item in monthly_trend %}
|
||
<div class="chart-col">
|
||
<div class="chart-bar-wrap">
|
||
<div class="chart-bar {% if item.month == period_label %}active{% endif %}"
|
||
style="height: {{ (item.recharge / max_r * 100) if max_r > 0 else 0 }}%;"></div>
|
||
</div>
|
||
<span class="chart-label">{{ item.label }}</span>
|
||
<span class="chart-val">¥{{ "%.0f"|format(item.recharge) }}</span>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<div class="row g-4">
|
||
<div class="col-lg-6">
|
||
<div class="neo-surface p-4 h-100">
|
||
<h6 class="fw-bold mb-3"><i class="bi bi-book"></i> 各课程统计({{ period_label }})</h6>
|
||
{% if course_stats %}
|
||
<div class="table-responsive">
|
||
<table class="table table-sm table-hover mb-0">
|
||
<thead class="table-light">
|
||
<tr>
|
||
<th>课程</th>
|
||
<th class="text-end">充值</th>
|
||
<th class="text-end">消课</th>
|
||
<th class="text-end">退费</th>
|
||
<th class="text-end">月末余额</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for c in course_stats %}
|
||
<tr>
|
||
<td><span class="badge bg-secondary-subtle text-dark border">{{ c.code }}</span></td>
|
||
<td class="text-end text-success">¥{{ "%.0f"|format(c.recharge) }}</td>
|
||
<td class="text-end">{{ "%.0f"|format(c.consumed_hours) }}节</td>
|
||
<td class="text-end text-danger">{% if c.refund %}¥{{ "%.0f"|format(c.refund) }}{% else %}-{% endif %}</td>
|
||
<td class="text-end fw-semibold">¥{{ "%.0f"|format(c.end_balance) }}</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
{% else %}
|
||
<p class="text-muted mb-0">该月暂无课程数据,请先在「课时核对表」导入 Excel。</p>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="col-lg-6">
|
||
<div class="neo-surface p-4 h-100">
|
||
<h6 class="fw-bold mb-3"><i class="bi bi-arrow-return-left"></i> 退费统计({{ period_label }})</h6>
|
||
{% if refund_rows %}
|
||
<div class="table-responsive">
|
||
<table class="table table-sm table-hover mb-0">
|
||
<thead class="table-light">
|
||
<tr><th>课程</th><th class="text-end">笔数</th><th class="text-end">退费课时</th><th class="text-end">金额</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for r in refund_rows %}
|
||
<tr>
|
||
<td>{{ r.name }}</td>
|
||
<td class="text-end">{{ r.count }}</td>
|
||
<td class="text-end">{{ "%.0f"|format(r.lessons) }}</td>
|
||
<td class="text-end text-danger fw-semibold">¥{{ "%.2f"|format(r.amount) }}</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
{% else %}
|
||
<p class="text-muted mb-0">该月无退费记录。</p>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="col-lg-6">
|
||
<div class="neo-surface p-4 h-100">
|
||
<h6 class="fw-bold mb-3"><i class="bi bi-calendar-check"></i> 上课登记({{ period_label }})</h6>
|
||
{% if class_activity %}
|
||
<div class="table-responsive">
|
||
<table class="table table-sm table-hover mb-0">
|
||
<thead class="table-light">
|
||
<tr><th>课程</th><th class="text-end">登记次数</th><th class="text-end">课时</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for a in class_activity %}
|
||
<tr>
|
||
<td>{{ a.name }}</td>
|
||
<td class="text-end">{{ a.sessions }}</td>
|
||
<td class="text-end">{{ "%.0f"|format(a.lessons) }}</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
{% else %}
|
||
<p class="text-muted mb-0">该月暂无上课登记解析数据。</p>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="col-lg-6">
|
||
<div class="neo-surface p-4 h-100">
|
||
<h6 class="fw-bold mb-3"><i class="bi bi-pie-chart"></i> 财务营收对账(已导入全周期)</h6>
|
||
<div class="row g-3 text-center">
|
||
<div class="col-4">
|
||
<p class="text-muted small mb-1">累计充值</p>
|
||
<p class="fs-4 fw-bold text-success mb-0">¥{{ "%.0f"|format(total_recharge) }}</p>
|
||
</div>
|
||
<div class="col-4">
|
||
<p class="text-muted small mb-1">累计退费</p>
|
||
<p class="fs-4 fw-bold text-danger mb-0">¥{{ "%.0f"|format(total_refund) }}</p>
|
||
</div>
|
||
<div class="col-4">
|
||
<p class="text-muted small mb-1">净收入</p>
|
||
<p class="fs-4 fw-bold text-primary mb-0">¥{{ "%.0f"|format(net_income) }}</p>
|
||
</div>
|
||
</div>
|
||
<hr>
|
||
<p class="text-muted small mb-0 text-center">
|
||
累计已消课金额 ¥{{ "%.0f"|format(total_consumed_amount) }}
|
||
· {{ year }} 年度消课 {{ "%.0f"|format(year_consumption) }} 节
|
||
· 年度退费 ¥{{ "%.0f"|format(year_refund) }}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block extra_js %}
|
||
<script>
|
||
(function () {
|
||
var sel = document.getElementById('statsPeriodSelect');
|
||
if (!sel) return;
|
||
sel.addEventListener('change', function () {
|
||
document.getElementById('statsPeriodForm').submit();
|
||
});
|
||
})();
|
||
</script>
|
||
{% endblock %}
|