578 lines
25 KiB
HTML
578 lines
25 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('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 %}
|