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

674 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">
<div>
<h2 class="fw-bold text-primary mb-0">
<i class="fas fa-calendar-check 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"><a href="{{ url_for('student.attendance') }}">考勤记录</a></li>
<li class="breadcrumb-item active">考勤详情</li>
</ol>
</nav>
</div>
<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> {{ record.student_number }}</p>
<p><strong>姓名:</strong> {{ 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> {{ record.week_start_date.strftime('%Y年%m月%d日') }}</p>
<p><strong>结束日期:</strong> {{ record.week_end_date.strftime('%Y年%m月%d日') }}</p>
</div>
<div class="col-6">
<p><strong>创建时间:</strong> {{ record.created_at.strftime('%Y-%m-%d %H:%M') }}</p>
<p><strong>更新时间:</strong> {{ 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(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(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">
{{ record.absent_days }}天
</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(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>
<strong>{{ detail.attendance_date.strftime('%m-%d') }}</strong>
<small class="d-block text-muted">{{ detail.attendance_date.strftime('%Y') }}</small>
</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 %}
{% if weekday >= 5 %}
<small class="badge bg-info">休息日</small>
{% 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 %}
<span class="badge bg-primary">{{ detail.check_in_time.strftime('%H:%M') }}</span>
{% else %}
<span class="text-muted">未打卡</span>
{% endif %}
</td>
<td>
{% if detail.check_out_time %}
<span class="badge bg-success">{{ detail.check_out_time.strftime('%H:%M') }}</span>
{% 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-5">
<i class="fas fa-calendar-times fa-4x 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">
<h6 class="text-danger">{{ absent_days }}</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>
<!-- 出勤率计算 -->
<hr>
<div class="row text-center">
<div class="col-4">
<h6 class="text-primary">{{ "%.1f"|format((present_days / max(total_days, 1) * 100)) }}%</h6>
<small class="text-muted">出勤率</small>
</div>
<div class="col-4">
<h6 class="text-success">{{ "%.1f"|format((record.class_work_hours / max(record.actual_work_hours, 1) * 100)) }}%</h6>
<small class="text-muted">班内工作率</small>
</div>
<div class="col-4">
<h6 class="text-info">{{ total_days }}</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 recent_records %}
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>周期</th>
<th>出勤时长</th>
<th>旷工天数</th>
<th>对比</th>
</tr>
</thead>
<tbody>
{% for record_item in recent_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>
{% if record_item.absent_days > 0 %}
<span class="badge bg-warning">{{ record_item.absent_days }}</span>
{% else %}
<span class="badge bg-success">0</span>
{% endif %}
</td>
<td>
{% set diff = record.actual_work_hours - record_item.actual_work_hours %}
{% if diff > 0 %}
<small class="text-success">+{{ "%.1f"|format(diff) }}h</small>
{% elif diff < 0 %}
<small class="text-danger">{{ "%.1f"|format(diff) }}h</small>
{% else %}
<small class="text-muted">-</small>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-muted text-center mb-0">暂无历史记录</p>
{% endif %}
</div>
</div>
</div>
</div>
<!-- 操作建议 -->
{% if late_days > 0 or absent_days > 0 %}
<div class="alert alert-warning" role="alert">
<h6 class="alert-heading"><i class="fas fa-exclamation-triangle me-2"></i>考勤提醒</h6>
<p class="mb-0">
{% if late_days > 0 %}
本周有 <strong>{{ late_days }}</strong> 天迟到,
{% endif %}
{% if absent_days > 0 %}
<strong>{{ absent_days }}</strong> 天缺勤,
{% endif %}
请注意调整作息时间,保持良好的考勤记录。
</p>
</div>
{% endif %}
</div>
<!-- 详细时段信息模态框 -->
<div class="modal fade" id="detailModal" tabindex="-1" aria-labelledby="detailModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg 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;
background-color: #f8f9fc;
}
.period-header {
font-weight: 600;
color: #5a5c69;
margin-bottom: 0.75rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid #e3e6f0;
}
.time-info {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 1rem;
}
.badge.bg-orange {
background-color: #fd7e14 !important;
color: white;
}
.time-info > div {
flex: 1;
min-width: 120px;
}
/* 高亮迟到和缺勤行 */
.table-warning {
--bs-table-accent-bg: rgba(255, 193, 7, 0.1);
}
.table-danger {
--bs-table-accent-bg: rgba(220, 53, 69, 0.1);
}
@media (max-width: 768px) {
.time-info {
flex-direction: column;
align-items: flex-start;
}
.time-info > div {
width: 100%;
margin-bottom: 0.5rem;
}
}
</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 =
'<div class="alert alert-info"><i class="fas fa-info-circle me-2"></i>暂无详细时段信息</div>';
} else {
let html = '';
// 显示各个时段的详情
const periods = [
{ key: 'morning', name: '早上时段', time: '09:45-11:30', icon: 'fa-sun' },
{ key: 'afternoon', name: '下午时段', time: '13:30-18:30', icon: 'fa-cloud-sun' },
{ key: 'evening', name: '晚上时段', time: '19:00-23:30', icon: 'fa-moon' }
];
periods.forEach(period => {
if (detailsData[period.key]) {
const data = detailsData[period.key];
html += `
<div class="period-card">
<div class="period-header">
<i class="fas ${period.icon} me-2"></i>${period.name}
<small class="text-muted">(${period.time})</small>
</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 d-block">(迟到${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 d-block">(早退${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-business-time 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 = '<div class="alert alert-warning"><i class="fas fa-exclamation-triangle me-2"></i>该日期没有详细的时段打卡信息</div>';
}
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`);
let diff = (end - start) / (1000 * 60 * 60);
// 处理跨天情况
if (diff < 0) {
diff += 24;
}
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>';
}
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
console.log('学生考勤详情页面已加载');
// 显示考勤统计
console.log('考勤统计:', {
正常天数: {{ present_days }},
迟到天数: {{ late_days }},
缺勤天数: {{ absent_days }},
日均时长: {{ "%.1f"|format(avg_daily_hours) }}
});
});
</script>
{% endblock %}