572 lines
24 KiB
HTML
572 lines
24 KiB
HTML
{% extends 'layout/base.html' %}
|
|
|
|
{% block title %}考勤详情 - CHM考勤管理系统{% 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.attendance_management') }}" 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_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(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>
|
|
<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">
|
|
<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>
|
|
</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 in historical_records %}
|
|
<tr>
|
|
<td>
|
|
<small>{{ record.week_start_date.strftime('%m-%d') }}</small>
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-primary">{{ "%.1f"|format(record.actual_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>
|
|
</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 %}
|