438 lines
17 KiB
Python
438 lines
17 KiB
Python
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify
|
|
from flask_login import login_required, current_user
|
|
from app.models import db, Student, WeeklyAttendance, DailyAttendanceDetail, LeaveRecord
|
|
from app.utils.auth_helpers import student_required
|
|
from app.utils.database import safe_add_and_commit
|
|
from datetime import datetime, timedelta
|
|
from sqlalchemy import and_, or_, desc
|
|
|
|
student_bp = Blueprint('student', __name__)
|
|
|
|
|
|
@student_bp.route('/dashboard')
|
|
@student_required
|
|
def dashboard():
|
|
"""学生主页"""
|
|
if current_user.is_admin():
|
|
return redirect(url_for('admin.dashboard'))
|
|
|
|
student = Student.query.filter_by(student_number=current_user.student_number).first()
|
|
if not student:
|
|
flash('学生信息不存在,请联系管理员', 'error')
|
|
return redirect(url_for('auth.logout'))
|
|
|
|
# 获取最近的考勤记录
|
|
recent_attendance = WeeklyAttendance.query.filter_by(
|
|
student_number=current_user.student_number
|
|
).order_by(desc(WeeklyAttendance.week_start_date)).limit(5).all()
|
|
|
|
# 统计数据
|
|
total_records = WeeklyAttendance.query.filter_by(
|
|
student_number=current_user.student_number
|
|
).count()
|
|
|
|
total_work_hours = db.session.query(
|
|
db.func.sum(WeeklyAttendance.actual_work_hours)
|
|
).filter_by(student_number=current_user.student_number).scalar() or 0
|
|
|
|
total_absent_days = db.session.query(
|
|
db.func.sum(WeeklyAttendance.absent_days)
|
|
).filter_by(student_number=current_user.student_number).scalar() or 0
|
|
|
|
# 获取未审批的请假记录
|
|
pending_leaves = LeaveRecord.query.filter_by(
|
|
student_number=current_user.student_number,
|
|
status='待审批'
|
|
).order_by(desc(LeaveRecord.created_at)).all()
|
|
|
|
return render_template('student/dashboard.html',
|
|
student=student,
|
|
recent_attendance=recent_attendance,
|
|
total_records=total_records,
|
|
total_work_hours=float(total_work_hours),
|
|
total_absent_days=int(total_absent_days),
|
|
pending_leaves=pending_leaves)
|
|
|
|
|
|
@student_bp.route('/attendance')
|
|
@student_required
|
|
def attendance():
|
|
"""考勤记录页面"""
|
|
from sqlalchemy import desc, func, case, or_
|
|
|
|
page = request.args.get('page', 1, type=int)
|
|
per_page = 20
|
|
|
|
# 日期筛选
|
|
start_date = request.args.get('start_date')
|
|
end_date = request.args.get('end_date')
|
|
|
|
# 构建基础查询,同时计算迟到次数
|
|
query = db.session.query(
|
|
WeeklyAttendance,
|
|
func.coalesce(
|
|
func.sum(
|
|
case(
|
|
(DailyAttendanceDetail.status.like('%迟到%'), 1),
|
|
else_=0
|
|
)
|
|
), 0
|
|
).label('late_count')
|
|
).outerjoin(
|
|
DailyAttendanceDetail,
|
|
WeeklyAttendance.record_id == DailyAttendanceDetail.weekly_record_id
|
|
).filter(
|
|
WeeklyAttendance.student_number == current_user.student_number
|
|
).group_by(WeeklyAttendance.record_id)
|
|
|
|
# 应用筛选条件
|
|
if start_date:
|
|
try:
|
|
start_date_obj = datetime.strptime(start_date, '%Y-%m-%d').date()
|
|
query = query.filter(WeeklyAttendance.week_start_date >= start_date_obj)
|
|
except ValueError:
|
|
flash('开始日期格式错误', 'error')
|
|
|
|
if end_date:
|
|
try:
|
|
end_date_obj = datetime.strptime(end_date, '%Y-%m-%d').date()
|
|
query = query.filter(WeeklyAttendance.week_end_date <= end_date_obj)
|
|
except ValueError:
|
|
flash('结束日期格式错误', 'error')
|
|
|
|
# 执行分页查询
|
|
pagination = query.order_by(desc(WeeklyAttendance.week_start_date)).paginate(
|
|
page=page, per_page=per_page, error_out=False
|
|
)
|
|
|
|
# 处理结果,将迟到次数添加到记录对象中
|
|
attendance_records = []
|
|
for record, late_count in pagination.items:
|
|
record.late_count = int(late_count) if late_count else 0
|
|
attendance_records.append(record)
|
|
|
|
# 更新pagination对象的items
|
|
pagination.items = attendance_records
|
|
|
|
# 计算总体统计
|
|
total_stats = None
|
|
if attendance_records:
|
|
# 计算所有记录的统计信息
|
|
all_records_query = db.session.query(
|
|
WeeklyAttendance,
|
|
func.coalesce(
|
|
func.sum(
|
|
case(
|
|
(DailyAttendanceDetail.status.like('%迟到%'), 1),
|
|
else_=0
|
|
)
|
|
), 0
|
|
).label('late_count')
|
|
).outerjoin(
|
|
DailyAttendanceDetail,
|
|
WeeklyAttendance.record_id == DailyAttendanceDetail.weekly_record_id
|
|
).filter(
|
|
WeeklyAttendance.student_number == current_user.student_number
|
|
).group_by(WeeklyAttendance.record_id)
|
|
|
|
# 应用相同的筛选条件
|
|
if start_date:
|
|
start_date_obj = datetime.strptime(start_date, '%Y-%m-%d').date()
|
|
all_records_query = all_records_query.filter(WeeklyAttendance.week_start_date >= start_date_obj)
|
|
if end_date:
|
|
end_date_obj = datetime.strptime(end_date, '%Y-%m-%d').date()
|
|
all_records_query = all_records_query.filter(WeeklyAttendance.week_end_date <= end_date_obj)
|
|
|
|
all_records = all_records_query.all()
|
|
|
|
total_actual_hours = sum(record.actual_work_hours for record, _ in all_records)
|
|
total_class_hours = sum(record.class_work_hours for record, _ in all_records)
|
|
total_absent_days = sum(record.absent_days for record, _ in all_records)
|
|
total_overtime_hours = sum(record.overtime_hours for record, _ in all_records)
|
|
total_late_count = sum(late_count for _, late_count in all_records)
|
|
|
|
# 计算请假天数
|
|
record_ids = [record.record_id for record, _ in all_records]
|
|
total_leave_days = 0
|
|
if record_ids:
|
|
total_leave_days = DailyAttendanceDetail.query.filter(
|
|
DailyAttendanceDetail.weekly_record_id.in_(record_ids),
|
|
DailyAttendanceDetail.status == '请假'
|
|
).count()
|
|
|
|
total_stats = {
|
|
'total_weeks': len(all_records),
|
|
'total_actual_hours': total_actual_hours,
|
|
'total_class_hours': total_class_hours,
|
|
'total_absent_days': total_absent_days,
|
|
'total_overtime_hours': total_overtime_hours,
|
|
'total_late_count': total_late_count,
|
|
'total_leave_days': total_leave_days,
|
|
'avg_weekly_hours': total_actual_hours / max(len(all_records), 1)
|
|
}
|
|
|
|
return render_template('student/attendance.html',
|
|
attendance_records=attendance_records,
|
|
pagination=pagination,
|
|
start_date=start_date,
|
|
end_date=end_date,
|
|
total_stats=total_stats)
|
|
|
|
|
|
@student_bp.route('/attendance/<int:record_id>/details')
|
|
@student_required
|
|
def attendance_details(record_id):
|
|
"""考勤详细信息"""
|
|
from datetime import datetime, timedelta
|
|
import json
|
|
|
|
# 获取周考勤汇总记录(确保只能查看自己的记录)
|
|
record = WeeklyAttendance.query.filter_by(
|
|
record_id=record_id,
|
|
student_number=current_user.student_number
|
|
).first_or_404()
|
|
|
|
# 获取学生信息
|
|
student = Student.query.filter_by(student_number=current_user.student_number).first()
|
|
|
|
# 获取该周的每日考勤明细
|
|
daily_details = DailyAttendanceDetail.query.filter_by(
|
|
weekly_record_id=record_id
|
|
).order_by(DailyAttendanceDetail.attendance_date).all()
|
|
|
|
# 处理每日详情,计算工作时长和解析详细信息
|
|
processed_daily_details = []
|
|
for detail in daily_details:
|
|
processed_detail = {
|
|
'detail_id': detail.detail_id,
|
|
'attendance_date': detail.attendance_date,
|
|
'status': detail.status,
|
|
'check_in_time': detail.check_in_time,
|
|
'check_out_time': detail.check_out_time,
|
|
'remarks': detail.remarks,
|
|
'duration_hours': None,
|
|
'detailed_info': None
|
|
}
|
|
|
|
# 计算工作时长
|
|
if detail.check_in_time and detail.check_out_time:
|
|
try:
|
|
# 创建完整的datetime对象
|
|
start_datetime = datetime.combine(detail.attendance_date, detail.check_in_time)
|
|
end_datetime = datetime.combine(detail.attendance_date, detail.check_out_time)
|
|
|
|
# 如果结束时间小于开始时间,说明跨天了
|
|
if end_datetime < start_datetime:
|
|
end_datetime += timedelta(days=1)
|
|
|
|
duration = (end_datetime - start_datetime).total_seconds() / 3600
|
|
processed_detail['duration_hours'] = round(duration, 1)
|
|
except Exception as e:
|
|
print(f"计算工作时长失败: {e}")
|
|
processed_detail['duration_hours'] = None
|
|
|
|
# 解析详细信息
|
|
if detail.remarks:
|
|
try:
|
|
if detail.remarks.startswith('{'):
|
|
remarks_data = json.loads(detail.remarks)
|
|
processed_detail['detailed_info'] = remarks_data.get('details')
|
|
processed_detail['summary_remarks'] = remarks_data.get('summary', detail.remarks)
|
|
else:
|
|
processed_detail['summary_remarks'] = detail.remarks
|
|
except:
|
|
processed_detail['summary_remarks'] = detail.remarks
|
|
|
|
processed_daily_details.append(processed_detail)
|
|
|
|
# 计算统计数据
|
|
total_days = len(processed_daily_details)
|
|
present_days = len([d for d in processed_daily_details if d['status'] == '正常'])
|
|
late_days = len([d for d in processed_daily_details if '迟到' in d['status']])
|
|
absent_days = len([d for d in processed_daily_details if d['status'] == '缺勤'])
|
|
|
|
# 计算平均每日工作时长
|
|
if processed_daily_details:
|
|
avg_daily_hours = record.actual_work_hours / max(present_days, 1)
|
|
else:
|
|
avg_daily_hours = 0
|
|
|
|
# 获取该学生最近的其他考勤记录(用于对比)
|
|
recent_records = WeeklyAttendance.query.filter_by(
|
|
student_number=current_user.student_number
|
|
).filter(WeeklyAttendance.record_id != record_id).order_by(
|
|
desc(WeeklyAttendance.week_start_date)
|
|
).limit(5).all()
|
|
|
|
return render_template('student/attendance_details.html',
|
|
record=record,
|
|
student=student,
|
|
daily_details=processed_daily_details,
|
|
total_days=total_days,
|
|
present_days=present_days,
|
|
late_days=late_days,
|
|
absent_days=absent_days,
|
|
avg_daily_hours=avg_daily_hours,
|
|
recent_records=recent_records)
|
|
|
|
|
|
@student_bp.route('/statistics')
|
|
@login_required
|
|
def statistics():
|
|
"""学生个人统计"""
|
|
from sqlalchemy import desc, func, case
|
|
from datetime import datetime, timedelta
|
|
|
|
# 获取当前学生信息
|
|
student = Student.query.filter_by(student_number=current_user.student_number).first_or_404()
|
|
|
|
# 获取筛选参数
|
|
start_date = request.args.get('start_date', '')
|
|
end_date = request.args.get('end_date', '')
|
|
|
|
# 构建考勤记录查询
|
|
attendance_query = WeeklyAttendance.query.filter_by(
|
|
student_number=current_user.student_number
|
|
)
|
|
|
|
# 应用日期筛选
|
|
if start_date:
|
|
try:
|
|
start_date_obj = datetime.strptime(start_date, '%Y-%m-%d').date()
|
|
attendance_query = attendance_query.filter(WeeklyAttendance.week_start_date >= start_date_obj)
|
|
except ValueError:
|
|
flash('开始日期格式错误', 'error')
|
|
|
|
if end_date:
|
|
try:
|
|
end_date_obj = datetime.strptime(end_date, '%Y-%m-%d').date()
|
|
attendance_query = attendance_query.filter(WeeklyAttendance.week_end_date <= end_date_obj)
|
|
except ValueError:
|
|
flash('结束日期格式错误', 'error')
|
|
|
|
# 获取考勤记录,按周排序
|
|
attendance_records = attendance_query.order_by(desc(WeeklyAttendance.week_start_date)).all()
|
|
|
|
# 计算统计数据
|
|
total_stats = {
|
|
'total_work_hours': sum(record.actual_work_hours for record in attendance_records),
|
|
'total_class_hours': sum(record.class_work_hours for record in attendance_records),
|
|
'total_overtime_hours': sum(record.overtime_hours for record in attendance_records),
|
|
'total_absent_days': sum(record.absent_days for record in attendance_records),
|
|
'attendance_weeks': len(attendance_records)
|
|
}
|
|
|
|
# 计算迟到次数
|
|
total_late_count = db.session.query(
|
|
func.sum(
|
|
case(
|
|
(DailyAttendanceDetail.status.like('%迟到%'), 1),
|
|
else_=0
|
|
)
|
|
)
|
|
).join(
|
|
WeeklyAttendance, DailyAttendanceDetail.weekly_record_id == WeeklyAttendance.record_id
|
|
).filter(WeeklyAttendance.student_number == current_user.student_number)
|
|
|
|
if start_date:
|
|
try:
|
|
start_date_obj = datetime.strptime(start_date, '%Y-%m-%d').date()
|
|
total_late_count = total_late_count.filter(WeeklyAttendance.week_start_date >= start_date_obj)
|
|
except ValueError:
|
|
pass
|
|
|
|
if end_date:
|
|
try:
|
|
end_date_obj = datetime.strptime(end_date, '%Y-%m-%d').date()
|
|
total_late_count = total_late_count.filter(WeeklyAttendance.week_end_date <= end_date_obj)
|
|
except ValueError:
|
|
pass
|
|
|
|
total_stats['total_late_count'] = int(total_late_count.scalar() or 0)
|
|
|
|
# 计算平均值
|
|
if total_stats['attendance_weeks'] > 0:
|
|
total_stats['avg_weekly_hours'] = round(total_stats['total_work_hours'] / total_stats['attendance_weeks'], 1)
|
|
total_stats['avg_weekly_class_hours'] = round(
|
|
total_stats['total_class_hours'] / total_stats['attendance_weeks'], 1)
|
|
else:
|
|
total_stats['avg_weekly_hours'] = 0
|
|
total_stats['avg_weekly_class_hours'] = 0
|
|
|
|
# 按月统计
|
|
monthly_stats = db.session.query(
|
|
func.date_format(WeeklyAttendance.week_start_date, '%Y-%m').label('month'),
|
|
func.count(WeeklyAttendance.record_id).label('record_count'),
|
|
func.sum(WeeklyAttendance.actual_work_hours).label('total_hours'),
|
|
func.sum(WeeklyAttendance.class_work_hours).label('class_hours'),
|
|
func.sum(WeeklyAttendance.overtime_hours).label('overtime_hours'),
|
|
func.sum(WeeklyAttendance.absent_days).label('absent_days')
|
|
).filter_by(student_number=current_user.student_number)
|
|
|
|
if start_date:
|
|
try:
|
|
start_date_obj = datetime.strptime(start_date, '%Y-%m-%d').date()
|
|
monthly_stats = monthly_stats.filter(WeeklyAttendance.week_start_date >= start_date_obj)
|
|
except ValueError:
|
|
pass
|
|
|
|
if end_date:
|
|
try:
|
|
end_date_obj = datetime.strptime(end_date, '%Y-%m-%d').date()
|
|
monthly_stats = monthly_stats.filter(WeeklyAttendance.week_end_date <= end_date_obj)
|
|
except ValueError:
|
|
pass
|
|
|
|
monthly_stats = monthly_stats.group_by('month').order_by('month').all()
|
|
|
|
# 最近几周的趋势数据
|
|
recent_weeks = attendance_query.order_by(desc(WeeklyAttendance.week_start_date)).limit(12).all()
|
|
recent_weeks.reverse() # 按时间正序排列
|
|
|
|
# 计算入学以来的总体表现
|
|
all_time_stats = None
|
|
if student.enrollment_date:
|
|
all_time_query = WeeklyAttendance.query.filter_by(
|
|
student_number=current_user.student_number
|
|
)
|
|
|
|
all_records = all_time_query.all()
|
|
if all_records:
|
|
# 计算入学以来的总统计
|
|
enrollment_weeks = (datetime.now().date() - student.enrollment_date).days // 7
|
|
all_time_stats = {
|
|
'total_work_hours': sum(record.actual_work_hours for record in all_records),
|
|
'total_class_hours': sum(record.class_work_hours for record in all_records),
|
|
'total_overtime_hours': sum(record.overtime_hours for record in all_records),
|
|
'total_absent_days': sum(record.absent_days for record in all_records),
|
|
'attendance_weeks': len(all_records),
|
|
'enrollment_weeks': enrollment_weeks,
|
|
'attendance_rate': round(len(all_records) / max(enrollment_weeks, 1) * 100,
|
|
1) if enrollment_weeks > 0 else 0
|
|
}
|
|
|
|
# 计算入学以来的迟到次数
|
|
all_time_late_count = db.session.query(
|
|
func.sum(
|
|
case(
|
|
(DailyAttendanceDetail.status.like('%迟到%'), 1),
|
|
else_=0
|
|
)
|
|
)
|
|
).join(
|
|
WeeklyAttendance, DailyAttendanceDetail.weekly_record_id == WeeklyAttendance.record_id
|
|
).filter(WeeklyAttendance.student_number == current_user.student_number).scalar()
|
|
|
|
all_time_stats['total_late_count'] = int(all_time_late_count or 0)
|
|
|
|
return render_template('student/statistics.html',
|
|
student=student,
|
|
attendance_records=attendance_records,
|
|
total_stats=total_stats,
|
|
monthly_stats=monthly_stats,
|
|
recent_weeks=recent_weeks,
|
|
all_time_stats=all_time_stats,
|
|
start_date=start_date,
|
|
end_date=end_date)
|
|
|