CHM_attendance/app/templates/student/attendance_details.html
2025-09-14 02:00:19 +08:00

578 lines
25 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('student.attendance') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left me-2"></i>返回列表
</a>
</div>
</div>
<!-- 基本信息卡片 -->
<div class="row mb-4">
<div class="col-md-6">
<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-6">
<p><strong>学号:</strong> {{ weekly_record.student_number }}</p>
<p><strong>姓名:</strong> {{ weekly_record.name }}</p>
{% if student %}
<p><strong>年级:</strong> {{ student.grade }}</p>
<p><strong>学院:</strong> {{ student.college or '未设置' }}</p>
{% endif %}
</div>
<div class="col-6">
{% if student %}
<p><strong>专业:</strong> {{ student.major or '未设置' }}</p>
<p><strong>导师:</strong> {{ student.supervisor or '未设置' }}</p>
<p><strong>学位类型:</strong> {{ student.degree_type or '未设置' }}</p>
<p><strong>状态:</strong>
<span class="badge bg-{{ 'success' if student.status == '在读' else 'secondary' }}">
{{ student.status }}
</span>
</p>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card shadow">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-calendar-week me-2"></i>考勤周期
</h6>
</div>
<div class="card-body">
<div class="row">
<div class="col-6">
<p><strong>开始日期:</strong> {{ weekly_record.week_start_date.strftime('%Y年%m月%d日') }}</p>
<p><strong>结束日期:</strong> {{ weekly_record.week_end_date.strftime('%Y年%m月%d日') }}</p>
</div>
<div class="col-6">
<p><strong>创建时间:</strong> {{ weekly_record.created_at.strftime('%Y-%m-%d %H:%M') }}</p>
<p><strong>更新时间:</strong> {{ weekly_record.updated_at.strftime('%Y-%m-%d %H:%M') }}</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 统计数据卡片 -->
<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">
{{ "%.1f"|format(weekly_record.actual_work_hours) }}小时
</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-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(weekly_record.class_work_hours) }}小时
</div>
</div>
<div class="col-auto">
<i class="fas fa-briefcase 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">
{# 🔥 修改:使用 weekly_record.absent_count单位改为“次” #}
{{ weekly_record.absent_count if weekly_record.absent_count is defined else 0 }}次
</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(weekly_record.overtime_hours) }}小时
</div>
</div>
<div class="col-auto">
<i class="fas fa-moon fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</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-list me-2"></i>每日考勤明细
<small class="text-muted">(点击日期查看详细时段信息)</small>
</h6>
</div>
<div class="card-body">
{% if daily_details %}
<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>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for detail in daily_details %}
<tr class="{% if '迟到' in detail.status %}table-warning{% elif detail.status == '缺勤' %}table-danger{% endif %}">
<td>{{ detail.attendance_date.strftime('%m-%d') }}</td>
<td>
{% set weekday = detail.attendance_date.weekday() %}
{% if weekday == 0 %}周一
{% elif weekday == 1 %}周二
{% elif weekday == 2 %}周三
{% elif weekday == 3 %}周四
{% elif weekday == 4 %}周五
{% elif weekday == 5 %}周六
{% else %}周日
{% endif %}
</td>
<td>
{% if detail.status == '正常' %}
<span class="badge bg-success">{{ detail.status }}</span>
{% elif '迟到' in detail.status %}
<span class="badge bg-warning">{{ detail.status }}</span>
{% elif detail.status == '缺勤' %}
<span class="badge bg-danger">{{ detail.status }}</span>
{% elif detail.status == '请假' %}
<span class="badge bg-orange">{{ detail.status }}</span>
{% elif detail.status == '休息' %}
<span class="badge bg-info">{{ detail.status }}</span>
{% elif detail.status == '加班' %}
<span class="badge bg-primary">{{ detail.status }}</span>
{% else %}
<span class="badge bg-secondary">{{ detail.status }}</span>
{% endif %}
</td>
<td>
{% if detail.check_in_time %}
{{ detail.check_in_time.strftime('%H:%M') }}
{% else %}
<span class="text-muted">未打卡</span>
{% endif %}
</td>
<td>
{% if detail.check_out_time %}
{{ detail.check_out_time.strftime('%H:%M') }}
{% else %}
<span class="text-muted">未打卡</span>
{% endif %}
</td>
<td>
{% if detail.duration_hours %}
<span class="badge bg-primary">{{ detail.duration_hours }}h</span>
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
<td>
{% if detail.summary_remarks %}
<small class="text-muted">{{ detail.summary_remarks }}</small>
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
<td>
{% if detail.status not in ['休息', '缺勤'] and detail.detailed_info %}
<button type="button" class="btn btn-sm btn-outline-primary"
onclick="showDetailModal('{{ detail.detail_id }}', '{{ detail.attendance_date.strftime('%Y-%m-%d') }}', '{{ detail.remarks|escape }}')"
title="查看详细时段">
<i class="fas fa-eye"></i>
</button>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-3">
<i class="fas fa-calendar-times fa-3x text-muted mb-3"></i>
<h5 class="text-muted">暂无每日考勤明细</h5>
<p class="text-muted">该考勤周期内没有详细的打卡记录</p>
</div>
{% endif %}
</div>
</div>
<!-- 统计分析和历史对比的其他部分... -->
<div class="row mb-4">
<div class="col-md-6">
<div class="card shadow">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-chart-pie me-2"></i>本周考勤统计
</h6>
</div>
<div class="card-body">
<div class="row text-center">
<div class="col-3">
<div class="border-end">
<h6 class="text-success">{{ present_days }}</h6>
<small class="text-muted">正常天数</small>
</div>
</div>
<div class="col-3">
<div class="border-end">
<h6 class="text-warning">{{ late_days }}</h6>
<small class="text-muted">迟到天数</small>
</div>
</div>
<div class="col-3">
<div class="border-end">
{# 🔥 修改:标题改为“缺勤次数”,数据源改为 weekly_record.absent_count #}
<h6 class="text-danger">{{ weekly_record.absent_count if weekly_record.absent_count is defined else 0 }}</h6>
<small class="text-muted">缺勤次数</small>
</div>
</div>
<div class="col-3">
<h6 class="text-info">{{ "%.1f"|format(avg_daily_hours) }}h</h6>
<small class="text-muted">日均时长</small>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card shadow">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-history me-2"></i>最近记录对比
</h6>
</div>
<div class="card-body">
{% if historical_records %}
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>周期</th>
<th>出勤时长</th>
{# 🔥 修改:表头改为“缺勤次数” #}
<th>缺勤次数</th>
</tr>
</thead>
<tbody>
{% for record_item in historical_records %}
<tr>
<td>
<small>{{ record_item.week_start_date.strftime('%m-%d') }}</small>
</td>
<td>
<span class="badge bg-primary">{{ "%.1f"|format(record_item.actual_work_hours) }}h</span>
</td>
<td>
{# 🔥 修改:使用 record_item.absent_count单位改为“次” #}
{% set absent_count = record_item.absent_count if record_item.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>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-muted text-center mb-0">暂无历史记录</p>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<!-- 详细时段信息模态框 -->
<div class="modal fade" id="detailModal" tabindex="-1" aria-labelledby="detailModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered"> <!-- 添加 modal-dialog-centered -->
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="detailModalLabel">
<i class="fas fa-clock me-2"></i>详细打卡时段 - <span id="modalDate"></span>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div id="detailContent">
<!-- 内容将通过JavaScript动态填充 -->
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
</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;
}
.border-end {
border-right: 1px solid #dee2e6;
}
.text-xs {
font-size: 0.7rem;
}
.card {
border: 0;
box-shadow: 0 0.15rem 1.75rem 0 rgba(58, 59, 69, 0.15) !important;
}
.period-card {
border: 1px solid #e3e6f0;
border-radius: 0.35rem;
padding: 1rem;
margin-bottom: 1rem;
}
.period-header {
font-weight: 600;
color: #5a5c69;
margin-bottom: 0.5rem;
}
.badge.bg-orange {
background-color: #fd7e14 !important;
}
.time-info {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>
{% endblock %}
{% block extra_js %}
<script>
function showDetailModal(detailId, date, remarksJson) {
document.getElementById('modalDate').textContent = date;
console.log('调用showDetailModal', detailId, date, remarksJson); // 调试信息
let detailsData = null;
try {
if (remarksJson && remarksJson.startsWith('{')) {
const parsed = JSON.parse(remarksJson);
detailsData = parsed.details;
console.log('解析的详细数据:', detailsData); // 调试信息
}
} catch (e) {
console.error('解析详细信息失败:', e);
}
if (!detailsData) {
document.getElementById('detailContent').innerHTML =
'<p class="text-muted">暂无详细时段信息</p><p class="text-muted">原始数据: ' + remarksJson + '</p>';
} else {
let html = '';
// 显示各个时段的详情
const periods = [
{ key: 'morning', name: '早上时段', time: '09:45-11:30' },
{ key: 'afternoon', name: '下午时段', time: '13:30-18:30' },
{ key: 'evening', name: '晚上时段', time: '19:00-23:30' }
];
periods.forEach(period => {
if (detailsData[period.key]) {
const data = detailsData[period.key];
html += `
<div class="period-card">
<div class="period-header">
<i class="fas fa-clock me-2"></i>${period.name} (${period.time})
</div>
<div class="time-info">
<div>
<strong>签到:</strong>
<span class="badge ${getStatusClass(data.status, 'in')}">
${data.in || '未打卡'}
</span>
${data.late_minutes ? `<small class="text-warning">(迟到${data.late_minutes}分钟)</small>` : ''}
</div>
<div>
<strong>签退:</strong>
<span class="badge ${getStatusClass(data.status, 'out')}">
${data.out || '未打卡'}
</span>
${data.early_minutes ? `<small class="text-warning">(早退${data.early_minutes}分钟)</small>` : ''}
</div>
<div>
<strong>工时:</strong>
${calculatePeriodHours(data.in, data.out)}
</div>
</div>
</div>
`;
}
});
// 如果是周末加班
if (detailsData.overtime) {
html += `
<div class="period-card">
<div class="period-header">
<i class="fas fa-moon me-2"></i>周末加班
</div>
<div class="time-info">
<div>
<strong>开始:</strong>
<span class="badge bg-info">${detailsData.overtime.in || '未记录'}</span>
</div>
<div>
<strong>结束:</strong>
<span class="badge bg-info">${detailsData.overtime.out || '未记录'}</span>
</div>
<div>
<strong>加班时长:</strong>
${calculatePeriodHours(detailsData.overtime.in, detailsData.overtime.out)}
</div>
</div>
</div>
`;
}
if (html === '') {
html = '<p class="text-muted">该日期没有详细的时段打卡信息</p>';
}
document.getElementById('detailContent').innerHTML = html;
}
const modal = new bootstrap.Modal(document.getElementById('detailModal'));
modal.show();
}
function getStatusClass(status, type) {
if (status === 'normal') return 'bg-success';
if (status === 'late' && type === 'in') return 'bg-warning';
if (status === 'early_leave' && type === 'out') return 'bg-warning';
if (status === 'missing') return 'bg-secondary';
return 'bg-secondary';
}
function calculatePeriodHours(startTime, endTime) {
if (!startTime || !endTime) return '<span class="text-muted">-</span>';
try {
const start = new Date(`2000-01-01 ${startTime}:00`);
const end = new Date(`2000-01-01 ${endTime}:00`);
const diff = (end - start) / (1000 * 60 * 60);
if (diff > 0) {
return `<span class="badge bg-primary">${diff.toFixed(1)}h</span>`;
}
} catch (e) {
console.error('计算时长失败:', e);
}
return '<span class="text-muted">-</span>';
}
// 测试函数
function testModal() {
console.log('测试模态框');
document.getElementById('modalDate').textContent = '测试日期';
document.getElementById('detailContent').innerHTML = '<p>测试内容</p>';
const modal = new bootstrap.Modal(document.getElementById('detailModal'));
modal.show();
}
</script>
{% endblock %}