Attendance filter: enrich-then-filter so badge matches result set
The previous filter applied status_code conditions on raw DB rows, then enriched the page of results. That meant a missing record with a leave request would surface under '缺勤' (raw code='missing') but render a '請假'/'公假' badge (enriched status_code). Move status_text filter to AFTER enrichment so it matches the badge. status_text sort_by='status_code' also moves to Python sort since post-enrich codes aren't in the DB. Log a warning if the in-memory filter scans >5000 rows so we know when to revisit.
This commit is contained in:
+58
-42
@@ -1248,7 +1248,14 @@ async def attendance_records(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Paginated records from attendance_records (SQL)."""
|
||||
"""Paginated records from attendance_records (SQL).
|
||||
|
||||
status_text filter runs AFTER enrichment so it matches the badge rendered
|
||||
from post-enrich status_code. Otherwise missing-day-with-leave records
|
||||
would surface under '缺勤' but render as '請假'/'公假' badges (mismatch).
|
||||
Trade-off: status_text/sort_by filter runs in Python after enrich instead
|
||||
of SQL — fine while dataset stays small (~80 records), log if it grows.
|
||||
"""
|
||||
from datetime import datetime as _dt
|
||||
from sqlalchemy import or_, and_
|
||||
|
||||
@@ -1267,39 +1274,6 @@ async def attendance_records(
|
||||
q = q.filter(AttendanceRecord.employee_name == staff)
|
||||
if status:
|
||||
q = q.filter(AttendanceRecord.status_code == status)
|
||||
if status_text:
|
||||
# Map status_text filter to status_code conditions so the result set
|
||||
# stays aligned with the badge rendered from (post-enrich) status_code.
|
||||
# Previously used late/early/OT thresholds which mismatched because
|
||||
# the import path can leave late_minutes=0 while status_code='abnormal'
|
||||
# (e.g. act_in=18:00 vs exp_in=11:00), and missing records also have
|
||||
# all three == 0 despite status_code='missing'.
|
||||
# Accept both English status_code values AND the Chinese basic-status
|
||||
# labels returned by /api/attendance/status_texts dropdown.
|
||||
abnormal_codes = ('abnormal', 'late', 'early', 'ot',
|
||||
'late_early', 'late_ot', 'early_ot')
|
||||
if status_text in ('abnormal', '異常'):
|
||||
q = q.filter(AttendanceRecord.status_code.in_(abnormal_codes))
|
||||
elif status_text == 'late':
|
||||
q = q.filter(AttendanceRecord.status_code.in_(('late', 'late_early', 'late_ot')))
|
||||
elif status_text == 'early':
|
||||
q = q.filter(AttendanceRecord.status_code.in_(('early', 'late_early', 'early_ot')))
|
||||
elif status_text == 'ot':
|
||||
q = q.filter(AttendanceRecord.status_code.in_(('ot', 'late_ot', 'early_ot')))
|
||||
elif status_text in ('missing', '缺勤'):
|
||||
q = q.filter(AttendanceRecord.status_code == 'missing')
|
||||
elif status_text in ('holiday', '公假'):
|
||||
q = q.filter(AttendanceRecord.status_code == 'holiday')
|
||||
elif status_text in ('al', 'sl', 'cl', 'mixed_leave'):
|
||||
q = q.filter(AttendanceRecord.status_code == status_text)
|
||||
elif status_text in ('leave', '請假'):
|
||||
# Any leave type — group filter
|
||||
q = q.filter(AttendanceRecord.status_code.in_(['al', 'sl', 'cl', 'mixed_leave']))
|
||||
elif status_text in ('normal', '正常'):
|
||||
q = q.filter(AttendanceRecord.status_code == 'normal')
|
||||
else:
|
||||
# Fallback to text search
|
||||
q = q.filter(AttendanceRecord.status_text.like(f"%{status_text}%"))
|
||||
if search:
|
||||
like = f"%{search}%"
|
||||
q = q.filter(or_(
|
||||
@@ -1308,20 +1282,62 @@ async def attendance_records(
|
||||
AttendanceRecord.shift_code.like(like),
|
||||
))
|
||||
|
||||
# sort
|
||||
sortable = {
|
||||
# Pre-sort by raw date so enrich-then-filter ordering is deterministic.
|
||||
sortable_raw = {
|
||||
"date": AttendanceRecord.date,
|
||||
"employee_name": AttendanceRecord.employee_name,
|
||||
"weekday": AttendanceRecord.weekday,
|
||||
"shift_code": AttendanceRecord.shift_code,
|
||||
"status_code": AttendanceRecord.status_code,
|
||||
}
|
||||
col = sortable.get(sort_by, AttendanceRecord.date)
|
||||
col = sortable_raw.get(sort_by, AttendanceRecord.date)
|
||||
q = q.order_by(col.desc() if sort_order == "desc" else col.asc())
|
||||
|
||||
total = q.count()
|
||||
rows = q.offset((page - 1) * per_page).limit(per_page).all()
|
||||
enrich_attendance_with_leave(rows, db)
|
||||
all_rows = q.all()
|
||||
if len(all_rows) > 5000:
|
||||
logger.warning(
|
||||
"attendance_records: in-memory filter over %d rows — consider prefiltering via date range",
|
||||
len(all_rows),
|
||||
)
|
||||
|
||||
# Enrich ALL rows so we can filter by post-enrich status_code.
|
||||
enrich_attendance_with_leave(all_rows, db)
|
||||
|
||||
# Apply status_text filter on enriched status_code (post-leave/holiday override)
|
||||
if status_text:
|
||||
abnormal_codes = ('abnormal', 'late', 'early', 'ot',
|
||||
'late_early', 'late_ot', 'early_ot')
|
||||
leave_codes = ('al', 'sl', 'cl', 'mixed_leave')
|
||||
if status_text in ('abnormal', '異常'):
|
||||
rows = [r for r in all_rows if (r.status_code or '').lower() in abnormal_codes]
|
||||
elif status_text == 'late':
|
||||
rows = [r for r in all_rows if (r.status_code or '').lower() in ('late', 'late_early', 'late_ot')]
|
||||
elif status_text == 'early':
|
||||
rows = [r for r in all_rows if (r.status_code or '').lower() in ('early', 'late_early', 'early_ot')]
|
||||
elif status_text == 'ot':
|
||||
rows = [r for r in all_rows if (r.status_code or '').lower() in ('ot', 'late_ot', 'early_ot')]
|
||||
elif status_text in ('missing', '缺勤'):
|
||||
rows = [r for r in all_rows if (r.status_code or '').lower() == 'missing']
|
||||
elif status_text in ('holiday', '公假'):
|
||||
rows = [r for r in all_rows if (r.status_code or '').lower() == 'holiday']
|
||||
elif status_text in leave_codes:
|
||||
rows = [r for r in all_rows if (r.status_code or '').lower() == status_text]
|
||||
elif status_text in ('leave', '請假'):
|
||||
rows = [r for r in all_rows if (r.status_code or '').lower() in leave_codes]
|
||||
elif status_text in ('normal', '正常'):
|
||||
rows = [r for r in all_rows if (r.status_code or '').lower() == 'normal']
|
||||
else:
|
||||
# Fallback to raw status_text substring search
|
||||
rows = [r for r in all_rows
|
||||
if r.status_text and status_text in r.status_text]
|
||||
else:
|
||||
rows = all_rows
|
||||
|
||||
# Python sort by status_code when requested (raw DB only handled the basic columns)
|
||||
if sort_by == 'status_code':
|
||||
rows.sort(key=lambda r: (r.status_code or ''), reverse=(sort_order == 'desc'))
|
||||
|
||||
total = len(rows)
|
||||
page_rows = rows[(page - 1) * per_page: (page - 1) * per_page + per_page]
|
||||
|
||||
def to_dict(r):
|
||||
return {
|
||||
@@ -1347,7 +1363,7 @@ async def attendance_records(
|
||||
}
|
||||
|
||||
return {
|
||||
"records": [to_dict(r) for r in rows],
|
||||
"records": [to_dict(r) for r in page_rows],
|
||||
"total": total,
|
||||
"page": page,
|
||||
"per_page": per_page,
|
||||
|
||||
Reference in New Issue
Block a user