345 lines
17 KiB
HTML
345 lines
17 KiB
HTML
{% extends "base.html" %}
|
||
{% from 'macros/sortable.html' import sort_th %}
|
||
{% set detail_params = {'id': student.id} %}
|
||
{% block title %}学员详情 - 学生课程管理系统{% endblock %}
|
||
{% block content %}
|
||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||
<h4><i class="bi bi-person-badge"></i> 学员详情 - {{ student.name }}</h4>
|
||
<div>
|
||
{% if current_user.has_permission('student_account_adjust') %}
|
||
<a href="{{ url_for('account_adjust') }}" class="btn btn-outline-warning btn-sm me-2"><i class="bi bi-sliders"></i> 调课时</a>
|
||
{% endif %}
|
||
<a href="{{ url_for('student_list') }}" class="btn btn-outline-secondary"><i class="bi bi-arrow-left"></i> 返回列表</a>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 基本信息 -->
|
||
<div class="card shadow-sm mb-3">
|
||
<div class="card-header bg-primary text-white"><i class="bi bi-person"></i> 基本信息</div>
|
||
<div class="card-body">
|
||
<div class="row">
|
||
<div class="col-md-4"><strong>姓名:</strong>{{ student.name }}</div>
|
||
<div class="col-md-4"><strong>来源:</strong>{{ student.source or '新学员' }}</div>
|
||
<div class="col-md-4"><strong>状态:</strong>
|
||
{% if student.status == 1 %}<span class="badge bg-success">在读</span>{% else %}<span class="badge bg-secondary">停用</span>{% endif %}
|
||
</div>
|
||
<div class="col-md-4"><strong>出生日期:</strong>{{ student.birthday or '-' }}</div>
|
||
<div class="col-md-4"><strong>注册时间:</strong>{{ student.created_at.strftime('%Y-%m-%d') if student.created_at else '-' }}</div>
|
||
<div class="col-md-12"><strong>备注:</strong>{{ student.remark or '-' }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 课时信息 -->
|
||
<div class="card shadow-sm mb-3">
|
||
<div class="card-header bg-success text-white d-flex justify-content-between align-items-center">
|
||
<span><i class="bi bi-wallet2"></i> 课时信息{% if ledger_period %} <small class="opacity-75">(Excel {{ ledger_period }})</small>{% endif %}</span>
|
||
{% if current_user.has_permission('keshibiao_view') %}
|
||
<a href="{{ url_for('keshibiao_balances') }}" class="btn btn-sm btn-light">全部学员余额表 →</a>
|
||
{% endif %}
|
||
</div>
|
||
<div class="card-body">
|
||
{% if accounts %}
|
||
<div class="table-responsive">
|
||
<table class="table table-bordered table-hover">
|
||
<thead class="table-light">
|
||
<tr>
|
||
<th>{{ sort_th('student_detail', 'course', '课程', sort, order, detail_params, '') }}</th>
|
||
<th>{{ sort_th('student_detail', 'end_lessons', '正课剩余', sort, order, detail_params, '') }}</th>
|
||
<th>{{ sort_th('student_detail', 'end_gift', '赠课剩余', sort, order, detail_params, '') }}</th>
|
||
<th>{{ sort_th('student_detail', 'end_total', '总剩余', sort, order, detail_params, '') }}</th>
|
||
<th>
|
||
{{ sort_th('student_detail', 'end_balance', '账户余额', sort, order, detail_params, '', title='折后口径账面余额,非退费金额') }}
|
||
<i class="bi bi-info-circle text-muted" data-bs-toggle="tooltip" title="折后口径账面余额,非退费金额"></i>
|
||
</th>
|
||
<th>
|
||
{{ sort_th('student_detail', 'refund_due', '应退金额', sort, order, detail_params, '', title='原价单价×剩余正课,不含赠课') }}
|
||
<i class="bi bi-info-circle text-muted" data-bs-toggle="tooltip" title="原价单价×剩余正课,不含赠课"></i>
|
||
</th>
|
||
<th>{{ sort_th('student_detail', 'unit_price', '单价/节', sort, order, detail_params, '') }}</th>
|
||
<th>数据月份</th>
|
||
<th>状态</th>
|
||
{% if current_user.has_permission('keshibiao_view') %}<th>课时表</th>{% endif %}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for a in accounts %}
|
||
{% set lg = account_ledger.get(a.id, {}) %}
|
||
{% set rp = refund_previews.get(a.id, {}) if refund_previews else {} %}
|
||
{% set total_h = lg.get('end_total_lessons') %}
|
||
{% if total_h is none %}{% set total_h = a.total_hours %}{% endif %}
|
||
<tr>
|
||
<td>{{ a.course.name if a.course else '—' }}{% if a.course and a.course.course_code %} <small class="text-muted">({{ a.course.course_code }})</small>{% endif %}</td>
|
||
<td><strong>{{ fmt_hours(lg.get('end_lessons')) }}</strong></td>
|
||
<td>{{ fmt_hours(lg.get('end_gift_lessons')) }}</td>
|
||
<td class="text-primary fw-semibold">{{ fmt_hours(lg.get('end_total_lessons')) }}</td>
|
||
<td class="text-success">{{ fmt_money(lg.get('end_balance')) }}</td>
|
||
<td>
|
||
{% if rp and rp.has_original and not rp.refund_label %}
|
||
<strong class="text-danger">{{ fmt_money(rp.refund_due) }}</strong>
|
||
{% elif rp and rp.refund_label %}
|
||
<span class="text-muted">{{ rp.refund_label }}</span>
|
||
{% else %}
|
||
<span class="text-muted">-</span>
|
||
{% endif %}
|
||
</td>
|
||
<td>{{ fmt_price(lg.get('unit_price')) }}</td>
|
||
<td><small class="text-muted">{{ lg.get('period') or '—' }}</small></td>
|
||
<td>
|
||
{% if a.is_stopped %}
|
||
<span class="badge bg-warning">停课中</span>
|
||
{% elif total_h <= 5 and total_h > 0 %}
|
||
<span class="badge bg-danger">课时不足</span>
|
||
{% elif total_h == 0 %}
|
||
<span class="badge bg-secondary">已用完</span>
|
||
{% else %}
|
||
<span class="badge bg-success">正常</span>
|
||
{% endif %}
|
||
{% if a.account_status == 'graduated' %}<span class="badge bg-info">结课</span>{% endif %}
|
||
</td>
|
||
{% if current_user.has_permission('keshibiao_view') %}
|
||
<td><a href="{{ url_for('keshibiao_account', account_id=a.id) }}" class="btn btn-sm btn-outline-primary">流水</a></td>
|
||
{% endif %}
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
{% else %}
|
||
<p class="text-muted">暂无课时账户</p>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
{% if accounts and refund_previews %}
|
||
{% for a in accounts %}
|
||
{% set rp = refund_previews.get(a.id, {}) %}
|
||
{% if rp and rp.has_original %}
|
||
<div class="refund-preview-card mb-3">
|
||
<div class="d-flex justify-content-between align-items-start mb-3">
|
||
<h6 class="fw-bold mb-0"><i class="bi bi-calculator"></i> 退费预览(仅供参考)— {{ rp.course_name }}</h6>
|
||
<span class="badge bg-secondary-subtle text-muted border">原价口径</span>
|
||
</div>
|
||
<div class="row g-3 small">
|
||
<div class="col-md-4"><span class="text-muted">原价总额</span><div class="fw-semibold">{{ fmt_money(rp.original_total) }}</div></div>
|
||
<div class="col-md-4"><span class="text-muted">实付总额</span><div class="fw-semibold">{{ fmt_money(rp.paid_total) }}</div></div>
|
||
<div class="col-md-4"><span class="text-muted">优惠金额</span><div class="fw-semibold">{{ fmt_money(rp.discount) }}</div></div>
|
||
<div class="col-md-4"><span class="text-muted">原价单价/节</span><div>{{ fmt_price(rp.original_unit) }}</div></div>
|
||
<div class="col-md-4"><span class="text-muted">消课单价/节</span><div>{{ fmt_price(rp.consume_unit) }} <small class="text-muted">(实付÷总课时)</small></div></div>
|
||
<div class="col-md-4"><span class="text-muted">正课已上</span><div>{{ fmt_hours(rp.consumed_normal) }} 节</div></div>
|
||
<div class="col-md-4"><span class="text-muted">正课剩余</span><div>{{ fmt_hours(rp.regular_remaining) }} 节</div></div>
|
||
<div class="col-md-4"><span class="text-muted">赠课剩余</span><div>{{ fmt_hours(rp.gift_remaining) }} 节 <small class="text-muted">(退费时清零)</small></div></div>
|
||
<div class="col-md-4"><span class="text-muted">账户余额</span><div class="text-success">{{ fmt_money(rp.balance) }} <small class="text-muted">(折后口径)</small></div></div>
|
||
</div>
|
||
<div class="refund-preview-highlight mt-3 p-3 rounded">
|
||
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||
<span class="fw-bold fs-5">★ 如退费应退</span>
|
||
<span class="refund-due-amount">
|
||
{% if rp.refund_label %}{{ rp.refund_label }}{% else %}{{ fmt_money(rp.refund_due) }}{% endif %}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<p class="text-muted small mb-0 mt-2">退费金额 = 原价单价 × 剩余正课数,赠课不退款。最终退费金额以实际审批为准。</p>
|
||
</div>
|
||
{% elif rp and not rp.has_original %}
|
||
<div class="refund-preview-card mb-3">
|
||
<h6 class="fw-bold text-muted"><i class="bi bi-calculator"></i> 退费预览 — {{ rp.course_name }}</h6>
|
||
<p class="text-muted small mb-0">无原价数据(历史数据未录原价),应退相关字段无法计算。</p>
|
||
</div>
|
||
{% endif %}
|
||
{% endfor %}
|
||
{% endif %}
|
||
|
||
<!-- 所在班级 -->
|
||
<div class="card shadow-sm mb-3">
|
||
<div class="card-header bg-info text-white"><i class="bi bi-grid"></i> 所在班级</div>
|
||
<div class="card-body">
|
||
{% if class_relations %}
|
||
<div class="table-responsive">
|
||
<table class="table table-bordered table-hover">
|
||
<thead class="table-light">
|
||
<tr><th>班级</th><th>课程</th><th>老师</th><th>入班日期</th><th>状态</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for cr in class_relations %}
|
||
<tr>
|
||
<td>{{ cr.class_.name }}</td>
|
||
<td>{{ cr.class_.course.name }}</td>
|
||
<td>{{ cr.class_.teacher.name }}</td>
|
||
<td>{{ cr.join_date }}</td>
|
||
<td>
|
||
{% if cr.status == 1 %}<span class="badge bg-success">在读</span>
|
||
{% elif cr.status == 2 %}<span class="badge bg-warning">已转出</span>
|
||
{% else %}<span class="badge bg-info">已毕业</span>{% endif %}
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
{% else %}
|
||
<p class="text-muted">暂无班级信息</p>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 充值记录 -->
|
||
<div class="card shadow-sm mb-3">
|
||
<div class="card-header"><i class="bi bi-cash-stack"></i> 充值记录</div>
|
||
<div class="card-body">
|
||
{% if recharge_records %}
|
||
<div class="table-responsive">
|
||
<table class="table table-bordered table-hover table-sm">
|
||
<thead class="table-light">
|
||
<tr><th>时间</th><th>课程</th><th>金额</th><th>正课课时</th><th>赠课课时</th><th>支付方式</th><th>备注</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for r in recharge_records %}
|
||
<tr>
|
||
<td>{{ r.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
|
||
<td>{{ r.course.name }}</td>
|
||
<td class="text-success">¥{{ r.amount }}</td>
|
||
<td>{{ r.normal_hours }}</td>
|
||
<td class="text-danger">+{{ r.gifted_hours }}</td>
|
||
<td>{{ r.payment_method or '-' }}</td>
|
||
<td>{{ r.remark or '-' }}</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
{% else %}
|
||
<p class="text-muted">暂无充值记录</p>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 消课记录 -->
|
||
<div class="card shadow-sm mb-3">
|
||
<div class="card-header"><i class="bi bi-journal-check"></i> 消课记录</div>
|
||
<div class="card-body">
|
||
{% if consumption_records %}
|
||
<div class="table-responsive">
|
||
<table class="table table-bordered table-hover table-sm">
|
||
<thead class="table-light">
|
||
<tr><th>日期</th><th>课程</th><th>消课课时</th><th>正课消耗</th><th>赠课消耗</th><th>类型</th><th>备注</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for c in consumption_records %}
|
||
<tr>
|
||
<td>{{ c.consume_date }}</td>
|
||
<td>{{ c.course.name }}</td>
|
||
<td>{{ c.hours_consumed }}</td>
|
||
<td>{{ c.normal_consumed }}</td>
|
||
<td>{{ c.gifted_consumed }}</td>
|
||
<td>
|
||
{% if c.consume_type == 'trial' %}<span class="badge bg-secondary">试听</span>
|
||
{% elif c.consume_type == 'normal' %}<span class="badge bg-primary">正课</span>
|
||
{% else %}<span class="badge bg-warning">混合</span>{% endif %}
|
||
{% if c.is_makeup %}<span class="badge bg-info">补录</span>{% endif %}
|
||
</td>
|
||
<td>{{ c.remark or '-' }}</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
{% else %}
|
||
<p class="text-muted">暂无消课记录</p>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 退费记录 -->
|
||
<div class="card shadow-sm mb-3">
|
||
<div class="card-header"><i class="bi bi-arrow-return-left"></i> 退费记录</div>
|
||
<div class="card-body">
|
||
{% if refund_records %}
|
||
<div class="table-responsive">
|
||
<table class="table table-bordered table-hover table-sm">
|
||
<thead class="table-light">
|
||
<tr><th>时间</th><th>课程</th><th>退费金额</th><th>扣回赠课</th><th>已消课时金额</th><th>材料费</th><th>原因</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for r in refund_records %}
|
||
<tr>
|
||
<td>{{ r.created_at.strftime('%Y-%m-%d') }}</td>
|
||
<td>{{ r.course.name }}</td>
|
||
<td class="text-danger">¥{{ r.refund_amount }}</td>
|
||
<td>{{ r.deduct_gifted_hours }}</td>
|
||
<td>¥{{ r.consumed_hours_value }}</td>
|
||
<td>¥{{ '%.2f'|format((r.material_fee_per_quarter or 0) * (r.quarters or 0)) }}</td>
|
||
<td>{{ r.reason or '-' }}</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
{% else %}
|
||
<p class="text-muted">暂无退费记录</p>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 停课记录 -->
|
||
<div class="card shadow-sm mb-3">
|
||
<div class="card-header"><i class="bi bi-pause-circle"></i> 停课记录</div>
|
||
<div class="card-body">
|
||
{% if stop_records %}
|
||
<div class="table-responsive">
|
||
<table class="table table-bordered table-hover table-sm">
|
||
<thead class="table-light">
|
||
<tr><th>开始日期</th><th>结束日期</th><th>课程</th><th>状态</th><th>原因</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for s in stop_records %}
|
||
<tr>
|
||
<td>{{ s.start_date }}</td>
|
||
<td>{{ s.end_date }}</td>
|
||
<td>{{ s.course.name }}</td>
|
||
<td>
|
||
{% if s.status == 1 %}<span class="badge bg-warning">停课中</span>
|
||
{% elif s.status == 2 %}<span class="badge bg-success">已复课</span>
|
||
{% else %}<span class="badge bg-secondary">已取消</span>{% endif %}
|
||
</td>
|
||
<td>{{ s.reason or '-' }}</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
{% else %}
|
||
<p class="text-muted">暂无停课记录</p>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block extra_css %}
|
||
<style>
|
||
.refund-preview-card {
|
||
background: #FFFBEB;
|
||
border: 1px solid #FCD34D;
|
||
border-radius: 12px;
|
||
padding: 1.25rem;
|
||
}
|
||
.refund-preview-highlight {
|
||
background: #FEF3C7;
|
||
border: 1px solid #F59E0B;
|
||
}
|
||
.refund-due-amount {
|
||
color: #dc3545;
|
||
font-weight: 800;
|
||
font-size: 1.5rem;
|
||
}
|
||
</style>
|
||
{% endblock %}
|
||
|
||
{% block extra_js %}
|
||
<script>
|
||
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(function (el) {
|
||
new bootstrap.Tooltip(el);
|
||
});
|
||
</script>
|
||
{% endblock %}
|