564 lines
21 KiB
HTML
564 lines
21 KiB
HTML
{% extends 'layout/base.html' %}
|
|
|
|
{% block title %}个人统计 - SmartDSP考勤管理系统{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid mt-4">
|
|
<!-- 页面标题 -->
|
|
<div class="row mb-4">
|
|
<div class="col">
|
|
<h2 class="fw-bold text-primary">
|
|
<i class="fas fa-chart-line me-2"></i>个人统计分析
|
|
</h2>
|
|
<nav aria-label="breadcrumb">
|
|
<ol class="breadcrumb">
|
|
<li class="breadcrumb-item"><a href="{{ url_for('student.dashboard') }}">首页</a></li>
|
|
<li class="breadcrumb-item active">个人统计</li>
|
|
</ol>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 学生基本信息 -->
|
|
<div class="row mb-4">
|
|
<div class="col">
|
|
<div class="card shadow">
|
|
<div class="card-header py-3">
|
|
<h6 class="m-0 font-weight-bold text-primary">
|
|
<i class="fas fa-user me-2"></i>基本信息
|
|
</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-md-3">
|
|
<strong>学号:</strong> {{ student.student_number }}
|
|
</div>
|
|
<div class="col-md-3">
|
|
<strong>姓名:</strong> {{ student.name }}
|
|
</div>
|
|
<div class="col-md-3">
|
|
<strong>年级:</strong> {{ student.grade }}级
|
|
</div>
|
|
<div class="col-md-3">
|
|
<strong>入学日期:</strong> {{ student.enrollment_date.strftime('%Y-%m-%d') if student.enrollment_date else '未设置' }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 筛选器 -->
|
|
<div class="card mb-4">
|
|
<div class="card-header py-3">
|
|
<h6 class="m-0 font-weight-bold text-primary">
|
|
<i class="fas fa-filter me-2"></i>筛选条件
|
|
</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<form method="GET" action="{{ url_for('student.statistics') }}" class="row g-3">
|
|
<div class="col-md-4">
|
|
<label for="start_date" class="form-label">开始日期</label>
|
|
<input type="date" class="form-control" id="start_date" name="start_date"
|
|
value="{{ start_date or '' }}">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label for="end_date" class="form-label">结束日期</label>
|
|
<input type="date" class="form-control" id="end_date" name="end_date"
|
|
value="{{ end_date or '' }}">
|
|
</div>
|
|
<div class="col-md-4 d-flex align-items-end">
|
|
<div class="d-grid gap-2 d-md-flex w-100">
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="fas fa-search me-1"></i>筛选
|
|
</button>
|
|
<a href="{{ url_for('student.statistics') }}" class="btn btn-outline-secondary">
|
|
<i class="fas fa-refresh me-1"></i>重置
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 入学以来总体统计 -->
|
|
{% if all_time_stats %}
|
|
<div class="row mb-4">
|
|
<div class="col">
|
|
<div class="card shadow border-left-primary">
|
|
<div class="card-header py-3">
|
|
<h6 class="m-0 font-weight-bold text-primary">
|
|
<i class="fas fa-graduation-cap me-2"></i>入学以来总体表现
|
|
</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-md-2">
|
|
<div class="text-center">
|
|
<h4 class="text-primary">{{ all_time_stats.attendance_weeks }}</h4>
|
|
<small class="text-muted">总考勤周数</small>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<div class="text-center">
|
|
<h4 class="text-success">{{ "%.1f"|format(all_time_stats.total_work_hours) }}</h4>
|
|
<small class="text-muted">总工作时长(h)</small>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<div class="text-center">
|
|
<h4 class="text-info">{{ "%.1f"|format(all_time_stats.total_class_hours) }}</h4>
|
|
<small class="text-muted">班内工作(h)</small>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<div class="text-center">
|
|
<h4 class="text-warning">{{ all_time_stats.total_late_count }}</h4>
|
|
<small class="text-muted">迟到次数</small>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<div class="text-center">
|
|
<h4 class="text-danger">{{ all_time_stats.total_absent_days }}</h4>
|
|
<small class="text-muted">旷工天数</small>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<div class="text-center">
|
|
<h4 class="text-secondary">{{ "%.1f"|format(all_time_stats.attendance_rate) }}%</h4>
|
|
<small class="text-muted">出勤率</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- 当前筛选条件下的统计 -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-3">
|
|
<div class="card border-left-primary shadow h-100 py-2">
|
|
<div class="card-body">
|
|
<div class="row no-gutters align-items-center">
|
|
<div class="col mr-2">
|
|
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
|
|
筛选期间考勤周数
|
|
</div>
|
|
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
|
{{ total_stats.attendance_weeks }}周
|
|
</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<i class="fas fa-calendar-week fa-2x text-gray-300"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-3">
|
|
<div class="card border-left-success shadow h-100 py-2">
|
|
<div class="card-body">
|
|
<div class="row no-gutters align-items-center">
|
|
<div class="col mr-2">
|
|
<div class="text-xs font-weight-bold text-success text-uppercase mb-1">
|
|
总出勤时长
|
|
</div>
|
|
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
|
{{ "%.1f"|format(total_stats.total_work_hours) }}h
|
|
</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<i class="fas fa-clock fa-2x text-gray-300"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-3">
|
|
<div class="card border-left-warning shadow h-100 py-2">
|
|
<div class="card-body">
|
|
<div class="row no-gutters align-items-center">
|
|
<div class="col mr-2">
|
|
<div class="text-xs font-weight-bold text-warning text-uppercase mb-1">
|
|
迟到次数
|
|
</div>
|
|
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
|
{{ total_stats.total_late_count }}次
|
|
</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<i class="fas fa-exclamation-triangle fa-2x text-gray-300"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-3">
|
|
<div class="card border-left-info shadow h-100 py-2">
|
|
<div class="card-body">
|
|
<div class="row no-gutters align-items-center">
|
|
<div class="col mr-2">
|
|
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">
|
|
周均工作时长
|
|
</div>
|
|
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
|
{{ "%.1f"|format(total_stats.avg_weekly_hours) }}h
|
|
</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<i class="fas fa-chart-line fa-2x text-gray-300"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 图表展示区域 -->
|
|
<div class="row mb-4">
|
|
<!-- 月度统计图表 -->
|
|
<div class="col-lg-8">
|
|
<div class="card shadow">
|
|
<div class="card-header py-3">
|
|
<h6 class="m-0 font-weight-bold text-primary">
|
|
<i class="fas fa-chart-bar me-2"></i>月度考勤统计
|
|
</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<canvas id="monthlyChart" width="400" height="200"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 最近趋势图表 -->
|
|
<div class="col-lg-4">
|
|
<div class="card shadow">
|
|
<div class="card-header py-3">
|
|
<h6 class="m-0 font-weight-bold text-primary">
|
|
<i class="fas fa-chart-line me-2"></i>最近12周趋势
|
|
</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<canvas id="trendChart" width="400" height="200"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 详细记录表格 -->
|
|
<div class="card shadow">
|
|
<div class="card-header py-3">
|
|
<h6 class="m-0 font-weight-bold text-primary">
|
|
<i class="fas fa-table me-2"></i>详细考勤记录
|
|
</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
{% if attendance_records %}
|
|
<div class="table-responsive">
|
|
<table class="table table-hover">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>周次</th>
|
|
<th>实际工作时长</th>
|
|
<th>班内工作时长</th>
|
|
<th>加班时长</th>
|
|
<th>旷工天数</th>
|
|
<th>考勤状态</th>
|
|
<th>操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for record in attendance_records %}
|
|
<tr>
|
|
<td>
|
|
<div>
|
|
<strong>{{ record.week_start_date.strftime('%Y-%m-%d') }}</strong>
|
|
<small class="d-block text-muted">
|
|
至 {{ record.week_end_date.strftime('%Y-%m-%d') }}
|
|
</small>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-primary">{{ "%.1f"|format(record.actual_work_hours) }}h</span>
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-success">{{ "%.1f"|format(record.class_work_hours) }}h</span>
|
|
</td>
|
|
<td>
|
|
{% if record.overtime_hours > 0 %}
|
|
<span class="badge bg-info">{{ "%.1f"|format(record.overtime_hours) }}h</span>
|
|
{% else %}
|
|
<span class="badge bg-secondary">0h</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{% if record.absent_days > 0 %}
|
|
<span class="badge bg-danger">{{ record.absent_days }}天</span>
|
|
{% else %}
|
|
<span class="badge bg-success">0天</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{% set performance_score = (record.actual_work_hours / 40 * 100) if record.actual_work_hours else 0 %}
|
|
{% if performance_score >= 80 %}
|
|
<span class="badge bg-success">优秀</span>
|
|
{% elif performance_score >= 60 %}
|
|
<span class="badge bg-warning">良好</span>
|
|
{% else %}
|
|
<span class="badge bg-danger">待改善</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<a href="{{ url_for('student.attendance_details', record_id=record.record_id) }}"
|
|
class="btn btn-sm btn-outline-primary">
|
|
<i class="fas fa-eye me-1"></i>详情
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<div class="text-center py-5">
|
|
<i class="fas fa-chart-line fa-4x text-muted mb-3"></i>
|
|
<h5 class="text-muted">暂无统计数据</h5>
|
|
<p class="text-muted">当前筛选条件下没有找到考勤记录</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 快捷操作 -->
|
|
<div class="row mt-4">
|
|
<div class="col">
|
|
<div class="card shadow">
|
|
<div class="card-header py-3">
|
|
<h6 class="m-0 font-weight-bold text-primary">
|
|
<i class="fas fa-tools me-2"></i>快捷操作
|
|
</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-md-3 mb-2">
|
|
<a href="{{ url_for('student.attendance') }}" class="btn btn-outline-primary w-100">
|
|
<i class="fas fa-calendar-check me-2"></i>考勤记录
|
|
</a>
|
|
</div>
|
|
<div class="col-md-3 mb-2">
|
|
<a href="{{ url_for('auth.profile') }}" class="btn btn-outline-secondary w-100">
|
|
<i class="fas fa-user me-2"></i>个人资料
|
|
</a>
|
|
</div>
|
|
<div class="col-md-3 mb-2">
|
|
<button type="button" class="btn btn-outline-success w-100" onclick="exportStatistics()">
|
|
<i class="fas fa-download me-2"></i>导出统计
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.border-left-primary {
|
|
border-left: 0.25rem solid #4e73df !important;
|
|
}
|
|
|
|
.border-left-success {
|
|
border-left: 0.25rem solid #1cc88a !important;
|
|
}
|
|
|
|
.border-left-info {
|
|
border-left: 0.25rem solid #36b9cc !important;
|
|
}
|
|
|
|
.border-left-warning {
|
|
border-left: 0.25rem solid #f6c23e !important;
|
|
}
|
|
|
|
.text-xs {
|
|
font-size: 0.7rem;
|
|
}
|
|
|
|
.card {
|
|
border: 0;
|
|
box-shadow: 0 0.15rem 1.75rem 0 rgba(58, 59, 69, 0.15) !important;
|
|
}
|
|
|
|
.table th {
|
|
background-color: #f8f9fc;
|
|
border-top: none;
|
|
font-weight: 600;
|
|
font-size: 0.85rem;
|
|
color: #5a5c69;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
/* 图表容器样式 */
|
|
canvas {
|
|
max-height: 300px;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script>
|
|
// 月度统计图表
|
|
const monthlyData = {
|
|
labels: [
|
|
{% for stat in monthly_stats %}
|
|
'{{ stat.month }}',
|
|
{% endfor %}
|
|
],
|
|
datasets: [{
|
|
label: '总工作时长',
|
|
data: [
|
|
{% for stat in monthly_stats %}
|
|
{{ stat.total_hours or 0 }},
|
|
{% endfor %}
|
|
],
|
|
backgroundColor: 'rgba(78, 115, 223, 0.2)',
|
|
borderColor: 'rgba(78, 115, 223, 1)',
|
|
borderWidth: 2,
|
|
fill: true
|
|
}, {
|
|
label: '班内工作时长',
|
|
data: [
|
|
{% for stat in monthly_stats %}
|
|
{{ stat.class_hours or 0 }},
|
|
{% endfor %}
|
|
],
|
|
backgroundColor: 'rgba(28, 200, 138, 0.2)',
|
|
borderColor: 'rgba(28, 200, 138, 1)',
|
|
borderWidth: 2,
|
|
fill: true
|
|
}]
|
|
};
|
|
|
|
const monthlyCtx = document.getElementById('monthlyChart').getContext('2d');
|
|
const monthlyChart = new Chart(monthlyCtx, {
|
|
type: 'line',
|
|
data: monthlyData,
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
title: {
|
|
display: true,
|
|
text: '工作时长 (小时)'
|
|
}
|
|
},
|
|
x: {
|
|
title: {
|
|
display: true,
|
|
text: '月份'
|
|
}
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
display: true,
|
|
position: 'top'
|
|
},
|
|
title: {
|
|
display: true,
|
|
text: '月度工作时长趋势'
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// 最近12周趋势图表
|
|
const trendData = {
|
|
labels: [
|
|
{% for week in recent_weeks %}
|
|
'{{ week.week_start_date.strftime("%m/%d") }}',
|
|
{% endfor %}
|
|
],
|
|
datasets: [{
|
|
label: '周工作时长',
|
|
data: [
|
|
{% for week in recent_weeks %}
|
|
{{ week.actual_work_hours }},
|
|
{% endfor %}
|
|
],
|
|
backgroundColor: 'rgba(54, 185, 204, 0.2)',
|
|
borderColor: 'rgba(54, 185, 204, 1)',
|
|
borderWidth: 2,
|
|
tension: 0.1
|
|
}]
|
|
};
|
|
|
|
const trendCtx = document.getElementById('trendChart').getContext('2d');
|
|
const trendChart = new Chart(trendCtx, {
|
|
type: 'line',
|
|
data: trendData,
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
title: {
|
|
display: true,
|
|
text: '时长(h)'
|
|
}
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
},
|
|
title: {
|
|
display: true,
|
|
text: '近期趋势'
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// 导出统计功能
|
|
function exportStatistics() {
|
|
const params = new URLSearchParams(window.location.search);
|
|
params.set('export', 'excel');
|
|
|
|
const exportUrl = '{{ url_for("student.statistics") }}?' + params.toString();
|
|
|
|
const link = document.createElement('a');
|
|
link.href = exportUrl;
|
|
link.download = '个人考勤统计.xlsx';
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
}
|
|
|
|
// 页面加载完成后的初始化
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
console.log('个人统计页面已加载');
|
|
|
|
// 显示统计摘要
|
|
{% if total_stats %}
|
|
console.log('统计摘要:', {
|
|
考勤周数: {{ total_stats.attendance_weeks }},
|
|
总工作时长: {{ total_stats.total_work_hours }},
|
|
迟到次数: {{ total_stats.total_late_count }},
|
|
周均时长: {{ total_stats.avg_weekly_hours }}
|
|
});
|
|
{% endif %}
|
|
});
|
|
</script>
|
|
{% endblock %}
|