253 lines
8.8 KiB
PHP
253 lines
8.8 KiB
PHP
<?php
|
||
/**
|
||
* 班级操行分管理系统 - 管理端考勤管理
|
||
*
|
||
* 开发者: Canglan
|
||
* 联系方式: admin@sea-studio.top
|
||
* 版权归属: Sea Network Technology Studio
|
||
* 许可证: MIT License
|
||
*
|
||
* 版权所有 © Sea Network Technology Studio
|
||
*/
|
||
|
||
require_once __DIR__ . '/../config.php';
|
||
|
||
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'admin') {
|
||
header('Location: /index.php');
|
||
exit();
|
||
}
|
||
|
||
$page_title = '考勤管理';
|
||
$role = $_SESSION['role'] ?? '';
|
||
|
||
if (!in_array($role, ['班主任', '考勤委员'])) {
|
||
header('Location: /admin/dashboard.php');
|
||
exit();
|
||
}
|
||
|
||
include __DIR__ . '/../includes/header.php';
|
||
?>
|
||
|
||
<?php include __DIR__ . '/../includes/nav.php'; ?>
|
||
|
||
<div class="container">
|
||
<!-- 考勤操作工具栏 -->
|
||
<div class="card">
|
||
<div class="attendance-toolbar">
|
||
<div class="form-group" style="margin:0">
|
||
<label>日期</label>
|
||
<input type="date" id="attendanceDate" value="<?php echo date('Y-m-d'); ?>">
|
||
</div>
|
||
<div class="status-group">
|
||
<button class="status-btn active" data-status="absent" onclick="selectStatus(this)" data-default-deduction="3">缺勤</button>
|
||
<button class="status-btn" data-status="late" onclick="selectStatus(this)" data-default-deduction="1">迟到</button>
|
||
<button class="status-btn" data-status="leave" onclick="selectStatus(this)" data-default-deduction="0">请假</button>
|
||
<input type="number" id="customDeduction" placeholder="自定义扣分" min="0" max="10" style="width:100px;margin-left:10px;" title="留空或0使用默认值">
|
||
</div>
|
||
<input type="text" id="attendanceReason" placeholder="原因(可选)" style="flex:1;min-width:150px;">
|
||
<button class="btn btn-primary" onclick="selectAllStudents()">全选</button>
|
||
<button class="btn" onclick="deselectAllStudents()">取消全选</button>
|
||
<button class="btn btn-danger" onclick="submitAttendance()">提交考勤</button>
|
||
<button class="btn btn-secondary" onclick="loadAttendanceRecords()">查询记录</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 学生方格网格 -->
|
||
<div class="card">
|
||
<div class="card-title">点击选择有考勤异常的学生</div>
|
||
<div class="student-grid" id="studentGrid">
|
||
<!-- JS 动态生成 -->
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 历史考勤记录 -->
|
||
<div class="card">
|
||
<div class="card-title">考勤记录</div>
|
||
<div class="table-wrapper">
|
||
<table class="table">
|
||
<thead>
|
||
<tr><th>学号</th><th>姓名</th><th>状态</th><th>原因</th><th>记录人</th><th>扣分</th></tr>
|
||
</thead>
|
||
<tbody id="attendanceList"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
let currentStatus = 'absent';
|
||
let studentsData = [];
|
||
let existingRecords = [];
|
||
|
||
// 选择考勤状态
|
||
function selectStatus(btn) {
|
||
document.querySelectorAll('.status-btn').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
currentStatus = btn.dataset.status;
|
||
// 自动设置默认扣分值
|
||
const defaultDeduction = btn.dataset.defaultDeduction;
|
||
if (defaultDeduction && defaultDeduction !== '0') {
|
||
document.getElementById('customDeduction').value = defaultDeduction;
|
||
} else {
|
||
document.getElementById('customDeduction').value = '';
|
||
}
|
||
}
|
||
|
||
// 加载学生列表
|
||
async function loadStudents() {
|
||
const res = await apiGet('/api/admin/students', {page_size: 1000});
|
||
if (res && res.success) {
|
||
studentsData = res.data.students;
|
||
renderStudentGrid();
|
||
// 同时加载当天已有考勤记录
|
||
await loadExistingRecords();
|
||
} else {
|
||
document.getElementById('studentGrid').innerHTML = '<div style="text-align:center;padding:20px;color:#999;">加载学生列表失败</div>';
|
||
}
|
||
}
|
||
|
||
// 渲染学生方格
|
||
function renderStudentGrid() {
|
||
let html = '';
|
||
studentsData.forEach(student => {
|
||
const hasRecord = existingRecords.some(r => r.student_id === student.student_id);
|
||
html += `<div class="student-cell${hasRecord ? ' has-record' : ''}"
|
||
data-id="${student.student_id}"
|
||
data-name="${escapeHtml(student.name)}"
|
||
onclick="toggleStudent(this)">
|
||
${escapeHtml(student.name)}
|
||
</div>`;
|
||
});
|
||
if (studentsData.length === 0) {
|
||
html = '<div style="text-align:center;padding:20px;color:#999;width:100%;">暂无学生数据</div>';
|
||
}
|
||
document.getElementById('studentGrid').innerHTML = html;
|
||
}
|
||
|
||
// 切换学生选中状态
|
||
function toggleStudent(cell) {
|
||
cell.classList.toggle('selected');
|
||
}
|
||
|
||
// 全选
|
||
function selectAllStudents() {
|
||
document.querySelectorAll('.student-cell:not(.has-record)').forEach(cell => {
|
||
cell.classList.add('selected');
|
||
});
|
||
}
|
||
|
||
// 取消全选
|
||
function deselectAllStudents() {
|
||
document.querySelectorAll('.student-cell').forEach(cell => {
|
||
cell.classList.remove('selected');
|
||
});
|
||
}
|
||
|
||
// 加载当天已有考勤记录(用于标记 .has-record)
|
||
async function loadExistingRecords() {
|
||
const date = document.getElementById('attendanceDate').value;
|
||
const res = await apiGet('/api/admin/attendance/records', { date });
|
||
if (res && res.success) {
|
||
existingRecords = res.data.records || [];
|
||
renderStudentGrid(); // 重新渲染以标记 has-record
|
||
}
|
||
}
|
||
|
||
// 提交考勤
|
||
async function submitAttendance() {
|
||
const selectedCells = document.querySelectorAll('.student-cell.selected');
|
||
if (selectedCells.length === 0) {
|
||
showToast('请先选择有考勤异常的学生', 'warning');
|
||
return;
|
||
}
|
||
|
||
const date = document.getElementById('attendanceDate').value;
|
||
const reason = document.getElementById('attendanceReason').value;
|
||
const customDeduction = document.getElementById('customDeduction').value;
|
||
const customDeductionValue = customDeduction ? parseInt(customDeduction) : null;
|
||
|
||
// 检查是否有已存在记录的学生
|
||
const hasRecordStudents = [];
|
||
selectedCells.forEach(cell => {
|
||
if (cell.classList.contains('has-record')) {
|
||
hasRecordStudents.push(cell.dataset.name);
|
||
}
|
||
});
|
||
|
||
if (hasRecordStudents.length > 0) {
|
||
const confirmed = confirm(`以下学生已有考勤记录:${hasRecordStudents.join('、')},是否继续提交?`);
|
||
if (!confirmed) return;
|
||
}
|
||
|
||
// 批量提交
|
||
const promises = [];
|
||
selectedCells.forEach(cell => {
|
||
const studentId = parseInt(cell.dataset.id);
|
||
const payload = {
|
||
student_id: studentId,
|
||
date: date,
|
||
status: currentStatus,
|
||
reason: reason,
|
||
apply_deduction: true
|
||
};
|
||
// 只有设置了自定义扣分时才发送
|
||
if (customDeductionValue !== null && customDeductionValue > 0) {
|
||
payload.custom_deduction = customDeductionValue;
|
||
}
|
||
promises.push(apiPost('/api/admin/attendance', payload));
|
||
});
|
||
|
||
const results = await Promise.allSettled(promises);
|
||
const succeeded = results.filter(r => r.status === 'fulfilled' && r.value?.success).length;
|
||
const failed = results.length - succeeded;
|
||
|
||
if (failed === 0) {
|
||
showToast(`考勤提交成功(${succeeded}条)`);
|
||
} else {
|
||
showToast(`提交完成:成功${succeeded}条,失败${failed}条`, 'error');
|
||
}
|
||
|
||
// 刷新
|
||
deselectAllStudents();
|
||
await loadExistingRecords();
|
||
loadAttendanceRecords();
|
||
}
|
||
|
||
// 查询并渲染考勤记录
|
||
async function loadAttendanceRecords() {
|
||
const date = document.getElementById('attendanceDate').value;
|
||
const res = await apiGet('/api/admin/attendance/records', { date });
|
||
if (res && res.success) {
|
||
let html = '';
|
||
const records = res.data.records || [];
|
||
records.forEach(record => {
|
||
html += `<tr>
|
||
<td>${escapeHtml(record.student_no)}</td>
|
||
<td>${escapeHtml(record.student_name)}</td>
|
||
<td>${getStatusBadge(record.status, 'attendance')}</td>
|
||
<td>${escapeHtml(record.reason || '-')}</td>
|
||
<td>${escapeHtml(record.recorder_name || '-')}</td>
|
||
<td>${record.deduction_applied ? '已扣分' : '-'}</td>
|
||
</tr>`;
|
||
});
|
||
if (records.length === 0) {
|
||
html = '<tr><td colspan="6" style="text-align:center;">暂无考勤记录</td></tr>';
|
||
}
|
||
document.getElementById('attendanceList').innerHTML = html;
|
||
}
|
||
}
|
||
|
||
// 日期变化时重新加载
|
||
document.getElementById('attendanceDate').addEventListener('change', function() {
|
||
loadExistingRecords();
|
||
loadAttendanceRecords();
|
||
});
|
||
|
||
// 页面初始化
|
||
loadStudents();
|
||
loadAttendanceRecords();
|
||
</script>
|
||
<script src="/assets/js/admin.js"></script>
|
||
|
||
<?php include __DIR__ . '/../includes/footer.php'; ?>
|