CHM_attendance/app/templates/admin/attendance_management.html
superlishunqin 77b57a9876 0914
2025-09-14 01:28:47 +08:00

716 lines
32 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% 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_count_desc" {{ 'selected' if sort_by == 'absent_count_desc' or sort_by == 'absent_days_desc' else '' }}>缺勤次数↓</option>
<option value="absent_count_asc" {{ 'selected' if sort_by == 'absent_count_asc' or 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>
<!-- 🔥 新增:高级搜索区域 -->
<div id="advancedSearchArea" class="col-12 mt-3" style="display: none;">
<div class="border-top pt-3">
<h6 class="text-muted mb-3">
<i class="fas fa-filter me-2"></i>高级搜索
</h6>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">导师(多选)</label>
<div class="supervisor-checkboxes" style="max-height: 200px; overflow-y: auto; border: 1px solid #ced4da; border-radius: 0.375rem; padding: 10px;">
{% for supervisor in all_supervisors %}
<div class="form-check">
<input class="form-check-input" type="checkbox" name="supervisor"
value="{{ supervisor }}" id="supervisor_{{ loop.index }}"
{{ 'checked' if supervisor in selected_supervisors else '' }}>
<label class="form-check-label" for="supervisor_{{ loop.index }}">
{{ supervisor }}
</label>
</div>
{% endfor %}
</div>
<div class="mt-2">
<button type="button" class="btn btn-sm btn-outline-primary me-2" onclick="selectAllSupervisors()">全选</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="clearAllSupervisors()">清空</button>
</div>
</div>
<div class="col-md-6">
<label class="form-label">年级(多选)</label>
<div class="grade-checkboxes" style="max-height: 200px; overflow-y: auto; border: 1px solid #ced4da; border-radius: 0.375rem; padding: 10px;">
{% for grade in all_grades %}
<div class="form-check">
<input class="form-check-input" type="checkbox" name="grade"
value="{{ grade }}" id="grade_{{ grade }}"
{{ 'checked' if grade|string in selected_grades else '' }}>
<label class="form-check-label" for="grade_{{ grade }}">
{% if grade >= 20 %}
{{ grade }}级
{% else %}
{{ grade }}年级
{% endif %}
</label>
</div>
{% endfor %}
</div>
<div class="mt-2">
<button type="button" class="btn btn-sm btn-outline-primary me-2" onclick="selectAllGrades()">全选</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="clearAllGrades()">清空</button>
</div>
</div>
</div>
</div>
</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_count">
缺勤次数
<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>
{# 🔥 修改:使用 absent_count 而不是 absent_days单位改为"次" #}
{% set absent_count = record.absent_count if record.absent_count is defined else 0 %}
{% if absent_count > 0 %}
<span class="badge bg-warning">{{ absent_count }}次</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">
{# 🔥 修改:使用 absent_count 统计总缺勤次数 #}
{% set total_absent_count = attendance_records|sum(attribute='absent_count') if attendance_records[0].absent_count is defined else 0 %}
{{ total_absent_count }}
</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 class="col-6 mb-2">
<button class="btn btn-outline-warning btn-block" onclick="clearAllFilters()">
<i class="fas fa-eraser 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() {
// 使用 admin.js 中的 exportCurrentFilter 函数
exportCurrentFilter();
}
// 自动设置结束日期为开始日期的一周后
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); // 调试用
}
}
}
}
// 高级搜索切换功能已在 admin.js 中实现
// DOM加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
console.log('页面加载完成,初始化排序功能'); // 调试信息
setupTableSorting();
});
</script>
<script src="{{ url_for('static', filename='js/admin.js') }}"></script>
{% endblock %}