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),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(get_current_user),
|
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 datetime import datetime as _dt
|
||||||
from sqlalchemy import or_, and_
|
from sqlalchemy import or_, and_
|
||||||
|
|
||||||
@@ -1267,39 +1274,6 @@ async def attendance_records(
|
|||||||
q = q.filter(AttendanceRecord.employee_name == staff)
|
q = q.filter(AttendanceRecord.employee_name == staff)
|
||||||
if status:
|
if status:
|
||||||
q = q.filter(AttendanceRecord.status_code == 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:
|
if search:
|
||||||
like = f"%{search}%"
|
like = f"%{search}%"
|
||||||
q = q.filter(or_(
|
q = q.filter(or_(
|
||||||
@@ -1308,20 +1282,62 @@ async def attendance_records(
|
|||||||
AttendanceRecord.shift_code.like(like),
|
AttendanceRecord.shift_code.like(like),
|
||||||
))
|
))
|
||||||
|
|
||||||
# sort
|
# Pre-sort by raw date so enrich-then-filter ordering is deterministic.
|
||||||
sortable = {
|
sortable_raw = {
|
||||||
"date": AttendanceRecord.date,
|
"date": AttendanceRecord.date,
|
||||||
"employee_name": AttendanceRecord.employee_name,
|
"employee_name": AttendanceRecord.employee_name,
|
||||||
"weekday": AttendanceRecord.weekday,
|
"weekday": AttendanceRecord.weekday,
|
||||||
"shift_code": AttendanceRecord.shift_code,
|
"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())
|
q = q.order_by(col.desc() if sort_order == "desc" else col.asc())
|
||||||
|
|
||||||
total = q.count()
|
all_rows = q.all()
|
||||||
rows = q.offset((page - 1) * per_page).limit(per_page).all()
|
if len(all_rows) > 5000:
|
||||||
enrich_attendance_with_leave(rows, db)
|
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):
|
def to_dict(r):
|
||||||
return {
|
return {
|
||||||
@@ -1347,7 +1363,7 @@ async def attendance_records(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"records": [to_dict(r) for r in rows],
|
"records": [to_dict(r) for r in page_rows],
|
||||||
"total": total,
|
"total": total,
|
||||||
"page": page,
|
"page": page,
|
||||||
"per_page": per_page,
|
"per_page": per_page,
|
||||||
|
|||||||
Reference in New Issue
Block a user