CHM_attendance/app/templates/admin/attendance_management.html
superlishunqin 3e6c8d353c SmartDSP
2025-06-12 00:38:27 +08:00

657 lines
28 KiB
HTML

{% extends 'layout/base.html' %}
{% block title %}考勤管理 - SmartDSP考勤管理系统{% endblock %}
{% block content %}
<div class="container-fluid mt-4">
<!-- 页面标题 -->
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 mb-0">
<i class="fas fa-calendar-check me-2"></i>考勤管理
</h1>
<div>
<a href="{{ url_for('admin.upload_attendance') }}" class="btn btn-primary me-2">
<i class="fas fa-upload me-2"></i>上传数据
</a>
<a href="{{ url_for('admin.dashboard') }}" class="btn btn-secondary">
<i class="fas fa-home me-2"></i>返回首页
</a>
</div>
</div>
<!-- 搜索和筛选 -->
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-search me-2"></i>搜索筛选
</h6>
</div>
<div class="card-body">
<form method="GET" class="row g-3" id="searchForm">
<div class="col-md-2">
<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-2">
<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-3">
<label for="student_search" class="form-label">学生姓名/学号</label>
<input type="text" class="form-control" id="student_search" name="student_search"
placeholder="输入姓名或学号" value="{{ student_search or '' }}">
</div>
<div class="col-md-2">
<label for="sort_by" class="form-label">排序方式</label>
<select class="form-select" id="sort_by" name="sort_by">
<option value="created_at_desc" {{ 'selected' if sort_by == 'created_at_desc' else '' }}>最新记录</option>
<option value="created_at_asc" {{ 'selected' if sort_by == 'created_at_asc' else '' }}>最早记录</option>
<option value="actual_work_hours_desc" {{ 'selected' if sort_by == 'actual_work_hours_desc' else '' }}>出勤时长↓</option>
<option value="actual_work_hours_asc" {{ 'selected' if sort_by == 'actual_work_hours_asc' else '' }}>出勤时长↑</option>
<option value="class_work_hours_desc" {{ 'selected' if sort_by == 'class_work_hours_desc' else '' }}>班内工作↓</option>
<option value="class_work_hours_asc" {{ 'selected' if sort_by == 'class_work_hours_asc' else '' }}>班内工作↑</option>
<option value="absent_days_desc" {{ 'selected' if sort_by == 'absent_days_desc' else '' }}>旷工天数↓</option>
<option value="absent_days_asc" {{ 'selected' if sort_by == 'absent_days_asc' else '' }}>旷工天数↑</option>
<option value="late_count_desc" {{ 'selected' if sort_by == 'late_count_desc' else '' }}>迟到次数↓</option>
<option value="late_count_asc" {{ 'selected' if sort_by == 'late_count_asc' else '' }}>迟到次数↑</option>
<option value="overtime_hours_desc" {{ 'selected' if sort_by == 'overtime_hours_desc' else '' }}>加班时长↓</option>
<option value="overtime_hours_asc" {{ 'selected' if sort_by == 'overtime_hours_asc' else '' }}>加班时长↑</option>
</select>
</div>
<div class="col-md-3 d-flex align-items-end">
<button type="submit" class="btn btn-primary me-2">
<i class="fas fa-search me-1"></i>搜索
</button>
<a href="{{ url_for('admin.attendance_management') }}" class="btn btn-outline-secondary me-2">
<i class="fas fa-refresh"></i>
</a>
<button type="button" class="btn btn-outline-info" onclick="toggleAdvancedSearch()" title="高级搜索">
<i class="fas fa-cog"></i>
</button>
</div>
<!-- 隐藏字段保持分页状态 -->
<input type="hidden" name="page" value="{{ pagination.page if pagination else 1 }}">
</form>
</div>
</div>
<!-- 考勤记录表格 -->
<div class="card shadow mb-4">
<div class="card-header py-3 d-flex justify-content-between align-items-center">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-table me-2"></i>考勤记录
</h6>
{% if attendance_records %}
<span class="badge bg-info">
共 {{ pagination.total }} 条记录
</span>
{% endif %}
</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 class="sortable" data-sort="actual_work_hours">
出勤时长
<i class="fas fa-sort ms-1 text-muted sort-icon"></i>
</th>
<th class="sortable" data-sort="class_work_hours">
班内工作
<i class="fas fa-sort ms-1 text-muted sort-icon"></i>
</th>
<th class="sortable" data-sort="absent_days">
旷工天数
<i class="fas fa-sort ms-1 text-muted sort-icon"></i>
</th>
<th class="sortable" data-sort="late_count">
迟到次数
<i class="fas fa-sort ms-1 text-muted sort-icon"></i>
</th>
<th class="sortable" data-sort="overtime_hours">
加班时长
<i class="fas fa-sort ms-1 text-muted sort-icon"></i>
</th>
<th class="sortable" data-sort="created_at">
记录时间
<i class="fas fa-sort ms-1 text-muted sort-icon"></i>
</th>
<th class="text-center">操作</th>
</tr>
</thead>
<tbody>
{% for record in attendance_records %}
<tr>
<td>
<a href="{{ url_for('admin.student_detail', student_number=record.student_number) }}"
class="text-decoration-none">
{{ record.student_number }}
</a>
</td>
<td>{{ record.name }}</td>
<td>
<small>
{{ record.week_start_date.strftime('%Y-%m-%d') }}<br>
至 {{ record.week_end_date.strftime('%Y-%m-%d') }}
</small>
</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.absent_days > 0 %}
<span class="badge bg-warning">{{ record.absent_days }}天</span>
{% else %}
<span class="badge bg-success">0天</span>
{% endif %}
</td>
<td>
{% set late_count = record.late_count if record.late_count is defined else 0 %}
{% if late_count > 0 %}
<span class="badge bg-warning">{{ late_count }}次</span>
{% else %}
<span class="badge bg-success">0次</span>
{% endif %}
</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>
<small class="text-muted">
{{ record.created_at.strftime('%m-%d %H:%M') }}
</small>
</td>
<td class="text-center">
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-outline-primary"
onclick="viewDetails({{ record.record_id }})"
title="查看详情">
<i class="fas fa-eye"></i>
</button>
<button type="button" class="btn btn-outline-danger"
onclick="deleteRecord({{ record.record_id }}, '{{ record.name }}')"
title="删除">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- 分页 -->
{% if pagination.pages > 1 %}
<nav aria-label="考勤记录分页">
<ul class="pagination justify-content-center">
{% if pagination.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('admin.attendance_management',
page=pagination.prev_num,
start_date=start_date,
end_date=end_date,
student_search=student_search,
sort_by=sort_by) }}">
<i class="fas fa-chevron-left"></i>
</a>
</li>
{% endif %}
{% for page_num in pagination.iter_pages() %}
{% if page_num %}
{% if page_num != pagination.page %}
<li class="page-item">
<a class="page-link" href="{{ url_for('admin.attendance_management',
page=page_num,
start_date=start_date,
end_date=end_date,
student_search=student_search,
sort_by=sort_by) }}">
{{ page_num }}
</a>
</li>
{% else %}
<li class="page-item active">
<span class="page-link">{{ page_num }}</span>
</li>
{% endif %}
{% else %}
<li class="page-item disabled">
<span class="page-link"></span>
</li>
{% endif %}
{% endfor %}
{% if pagination.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('admin.attendance_management',
page=pagination.next_num,
start_date=start_date,
end_date=end_date,
student_search=student_search,
sort_by=sort_by) }}">
<i class="fas fa-chevron-right"></i>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<!-- 空状态 -->
<div class="text-center py-5">
<i class="fas fa-inbox fa-4x text-muted mb-3"></i>
<h5 class="text-muted">暂无考勤记录</h5>
<p class="text-muted mb-4">还没有上传任何考勤数据</p>
<a href="{{ url_for('admin.upload_attendance') }}" class="btn btn-primary">
<i class="fas fa-upload me-2"></i>立即上传考勤数据
</a>
</div>
{% endif %}
</div>
</div>
<!-- 统计信息卡片 -->
{% if attendance_records %}
<div class="row">
<div class="col-md-6">
<div class="card shadow mb-4">
<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">
<div class="row text-center">
<div class="col-2">
<div class="border-end">
<h6 class="text-primary">{{ pagination.total }}</h6>
<small class="text-muted">总记录</small>
</div>
</div>
<div class="col-2">
<div class="border-end">
<h6 class="text-success">
{{ attendance_records|sum(attribute='actual_work_hours')|round(1) }}h
</h6>
<small class="text-muted">总出勤</small>
</div>
</div>
<div class="col-1">
<div class="border-end">
<h6 class="text-danger">
{{ attendance_records|sum(attribute='absent_days') }}
</h6>
<small class="text-muted">旷工</small>
</div>
</div>
<div class="col-1">
<div class="border-end">
<h6 class="text-warning">
{% set total_leave = statistics.total_leave_days if statistics and statistics.total_leave_days else 0 %}
{{ total_leave }}
</h6>
<small class="text-muted">请假</small>
</div>
</div>
<div class="col-2">
<div class="border-end">
<h6 class="text-warning">
{% set total_late = attendance_records|sum(attribute='late_count') if attendance_records[0].late_count is defined else 0 %}
{{ total_late }}
</h6>
<small class="text-muted">迟到次数</small>
</div>
</div>
<div class="col-2">
<div class="border-end">
<h6 class="text-info">
{{ attendance_records|sum(attribute='overtime_hours')|round(1) }}h
</h6>
<small class="text-muted">总加班</small>
</div>
</div>
<div class="col-2">
<h6 class="text-secondary">
{{ "%.1f"|format((attendance_records|sum(attribute='actual_work_hours')) / (attendance_records|length) if attendance_records|length > 0 else 0) }}h
</h6>
<small class="text-muted">平均出勤</small>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card shadow mb-4">
<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-6 mb-2">
<a href="{{ url_for('admin.upload_attendance') }}"
class="btn btn-outline-primary btn-block">
<i class="fas fa-upload me-2"></i>上传新数据
</a>
</div>
<div class="col-6 mb-2">
<a href="{{ url_for('admin.student_list') }}"
class="btn btn-outline-success btn-block">
<i class="fas fa-users me-2"></i>学生管理
</a>
</div>
<div class="col-6 mb-2">
<a href="{{ url_for('admin.statistics') }}"
class="btn btn-outline-info btn-block">
<i class="fas fa-chart-line me-2"></i>统计报表
</a>
</div>
<div class="col-6 mb-2">
<button class="btn btn-outline-secondary btn-block" onclick="exportData()">
<i class="fas fa-download me-2"></i>导出数据
</button>
</div>
</div>
</div>
</div>
</div>
</div>
{% endif %}
</div>
<!-- 删除确认模态框 -->
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteModalLabel">
<i class="fas fa-exclamation-triangle text-warning me-2"></i>确认删除
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>您确定要删除 <strong id="studentName"></strong> 的这条考勤记录吗?</p>
<p class="text-danger"><small>此操作不可撤销!</small></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-danger" id="confirmDelete">确认删除</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_css %}
<style>
.btn-block {
display: block;
width: 100%;
}
.border-end {
border-right: 1px solid #dee2e6;
}
.table th {
background-color: #f8f9fc;
border-top: none;
font-weight: 600;
font-size: 0.85rem;
color: #5a5c69;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.badge {
font-size: 0.75rem;
}
.btn-group-sm > .btn {
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
}
/* 排序功能样式 */
.sortable {
cursor: pointer;
user-select: none;
position: relative;
transition: background-color 0.2s ease;
}
.sortable:hover {
background-color: #e9ecef !important;
}
.sort-icon {
font-size: 0.7rem;
transition: all 0.2s ease;
}
.sortable:hover .sort-icon {
color: #007bff !important;
}
.sortable.sort-active {
background-color: #e7f1ff !important;
}
.sortable.sort-active .sort-icon {
color: #007bff !important;
}
.sortable.sort-asc .sort-icon:before {
content: "\f0de"; /* fa-sort-up */
}
.sortable.sort-desc .sort-icon:before {
content: "\f0dd"; /* fa-sort-down */
}
/* 响应式调整 */
@media (max-width: 768px) {
.col-2 {
flex: 0 0 33.333333%;
max-width: 33.333333%;
margin-bottom: 1rem;
}
.table-responsive {
font-size: 0.8rem;
}
.badge {
font-size: 0.65rem;
}
}
</style>
{% endblock %}
{% block extra_js %}
<script>
let recordToDelete = null;
function viewDetails(recordId) {
window.location.href = `/admin/attendance/${recordId}/details`;
}
function editRecord(recordId) {
window.location.href = `/admin/attendance/${recordId}/edit`;
}
function deleteRecord(recordId, studentName) {
recordToDelete = recordId;
document.getElementById('studentName').textContent = studentName;
const modal = new bootstrap.Modal(document.getElementById('deleteModal'));
modal.show();
}
document.getElementById('confirmDelete').addEventListener('click', function() {
if (recordToDelete) {
const form = document.createElement('form');
form.method = 'POST';
form.action = `/admin/attendance/${recordToDelete}/delete`;
const csrfToken = document.querySelector('meta[name="csrf-token"]');
if (csrfToken) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'csrf_token';
input.value = csrfToken.getAttribute('content');
form.appendChild(input);
}
document.body.appendChild(form);
form.submit();
}
});
function exportData() {
const params = new URLSearchParams(window.location.search);
params.set('export', 'excel');
window.location.href = '{{ url_for("admin.attendance_management") }}?' + params.toString();
}
// 自动设置结束日期为开始日期的一周后
document.getElementById('start_date').addEventListener('change', function() {
const startDate = new Date(this.value);
const endDate = new Date(startDate);
endDate.setDate(startDate.getDate() + 6);
document.getElementById('end_date').value = endDate.toISOString().split('T')[0];
});
// 排序选择器变化时自动提交表单
document.getElementById('sort_by').addEventListener('change', function() {
// 重置到第一页
const pageInput = document.querySelector('input[name="page"]');
if (pageInput) {
pageInput.value = 1;
}
document.getElementById('searchForm').submit();
});
// 获取当前URL参数的函数
function getUrlParams() {
const params = new URLSearchParams(window.location.search);
return {
start_date: params.get('start_date') || '',
end_date: params.get('end_date') || '',
student_search: params.get('student_search') || '',
sort_by: params.get('sort_by') || 'created_at_desc'
};
}
// 构建新的排序URL
function buildSortUrl(sortField, currentParams) {
const currentSort = currentParams.sort_by;
let newSort;
if (currentSort === `${sortField}_asc`) {
newSort = `${sortField}_desc`;
} else {
newSort = `${sortField}_asc`;
}
const url = new URL(window.location.origin + window.location.pathname);
url.searchParams.set('sort_by', newSort);
if (currentParams.start_date) url.searchParams.set('start_date', currentParams.start_date);
if (currentParams.end_date) url.searchParams.set('end_date', currentParams.end_date);
if (currentParams.student_search) url.searchParams.set('student_search', currentParams.student_search);
// 重置到第一页
url.searchParams.set('page', '1');
return url.toString();
}
// 表头排序功能
function setupTableSorting() {
const sortableHeaders = document.querySelectorAll('.sortable');
const currentParams = getUrlParams();
console.log('当前URL参数:', currentParams); // 调试信息
sortableHeaders.forEach(header => {
header.addEventListener('click', function(e) {
e.preventDefault();
const sortField = this.getAttribute('data-sort');
console.log('点击排序字段:', sortField); // 调试信息
const newUrl = buildSortUrl(sortField, currentParams);
console.log('新URL:', newUrl); // 调试信息
window.location.href = newUrl;
});
});
// 更新当前排序状态的显示
const currentSortBy = currentParams.sort_by;
console.log('当前排序:', currentSortBy); // 调试用
if (currentSortBy && currentSortBy.includes('_')) {
// 移除所有现有的排序状态
sortableHeaders.forEach(header => {
header.classList.remove('sort-active', 'sort-asc', 'sort-desc');
});
// 解析排序字段和方向
const lastUnderscoreIndex = currentSortBy.lastIndexOf('_');
if (lastUnderscoreIndex > 0) {
const field = currentSortBy.substring(0, lastUnderscoreIndex);
const direction = currentSortBy.substring(lastUnderscoreIndex + 1);
console.log('解析排序:', field, direction); // 调试用
const header = document.querySelector(`[data-sort="${field}"]`);
if (header) {
header.classList.add('sort-active');
if (direction === 'desc') {
header.classList.add('sort-desc');
} else {
header.classList.add('sort-asc');
}
console.log('设置排序状态成功:', header.textContent.trim()); // 调试用
} else {
console.log('未找到排序表头:', field); // 调试用
}
}
}
}
// 高级搜索切换(预留功能)
function toggleAdvancedSearch() {
alert('高级搜索功能开发中...');
}
// DOM加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
console.log('页面加载完成,初始化排序功能'); // 调试信息
setupTableSorting();
});
</script>
{% endblock %}