Fix attendance filter: align with badge via status_code (not late/early/OT thresholds)

The previous filter used 'late_minutes == 0 AND early_minutes == 0 AND
ot_minutes == 0' for '正常', which incorrectly included records whose
import-time late_minutes was 0 but post-enrich status_code was 'abnormal'
(e.g. act_in=18:00 vs exp_in=11:00) or 'missing' (act_in='-').

Switch filter to status_code conditions so the result set matches the
badge rendered from status_code. Also accept the Chinese basic-status
labels (正常/異常/缺勤/公假/請假) as filter values, and return only those
5 from /api/attendance/status_texts so the dropdown vocabulary stays
in sync.

Frontend attendance list: simplify getStatusBadge to drop now-unused
late/early/ot props; align row highlight with the basic-status grouping.
This commit is contained in:
IT狗
2026-07-21 21:55:55 +08:00
parent d302fa9beb
commit 7702fe4ae5
2 changed files with 41 additions and 40 deletions
+30 -31
View File
@@ -1099,9 +1099,14 @@ async def attendance_status_texts(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Return distinct status_text values for filter dropdown."""
rows = db.query(AttendanceRecord.status_text).distinct().all()
return [r[0] for r in rows if r[0]]
"""Return BASIC status categories for filter dropdown.
The frontend renders only 5 basic statuses (正常/異常/缺勤/公假/請假),
so the dropdown should offer those exact values rather than every
raw status_text variant (e.g. '異常-遲到39分'). Keeping it server-driven
so client and server agree on the filter vocabulary.
"""
return ['正常', '異常', '缺勤', '公假', '請假']
@app.get("/api/attendance/stats")
async def attendance_stats(
@@ -1263,41 +1268,35 @@ async def attendance_records(
if status:
q = q.filter(AttendanceRecord.status_code == status)
if status_text:
# Map status_text filter to actual conditions
if status_text == 'abnormal':
q = q.filter(
(AttendanceRecord.late_minutes > 30) |
(AttendanceRecord.early_minutes > 30) |
(AttendanceRecord.ot_minutes > 30)
)
# 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.late_minutes > 0) &
(AttendanceRecord.late_minutes <= 30)
)
q = q.filter(AttendanceRecord.status_code.in_(('late', 'late_early', 'late_ot')))
elif status_text == 'early':
q = q.filter(
(AttendanceRecord.early_minutes > 0) &
(AttendanceRecord.early_minutes <= 30)
)
q = q.filter(AttendanceRecord.status_code.in_(('early', 'late_early', 'early_ot')))
elif status_text == 'ot':
q = q.filter(
(AttendanceRecord.ot_minutes > 0) &
(AttendanceRecord.ot_minutes <= 30)
)
elif status_text == 'missing':
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', 'al', 'sl', 'cl', 'mixed_leave'):
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 == 'leave':
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 == 'normal':
q = q.filter(
(AttendanceRecord.late_minutes == 0) &
(AttendanceRecord.early_minutes == 0) &
(AttendanceRecord.ot_minutes == 0)
)
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}%"))
+11 -9
View File
@@ -87,18 +87,20 @@ export default function AttendanceList() {
}
const getStatusBadge = (record) => {
const lm = record.late_minutes || 0
const em = record.early_minutes || 0
const om = record.ot_minutes || 0
return <StatusBadge code={record.status_code} late={lm} early={em} ot={om} text={record.status_text} />
// StatusBadge now drives from status_code + status_text only (basic 5 categories).
return <StatusBadge code={record.status_code} text={record.status_text} />
}
const getRowClass = (code) => {
if (code === 'abnormal') return 'bg-red-50 hover:bg-red-100'
if (code === 'missing') return 'bg-gray-100 hover:bg-gray-200'
if (code === 'late' || code === 'late_early' || code === 'late_ot') return 'bg-orange-50 hover:bg-orange-100'
if (code === 'early' || code === 'early_ot') return 'bg-blue-50 hover:bg-blue-100'
if (code === 'ot') return 'bg-purple-50 hover:bg-purple-100'
// Match the basic-status grouping so highlights line up with the badge.
const c = String(code || '').toLowerCase()
if (c === 'abnormal' || c === 'late' || c === 'early' || c === 'ot'
|| c === 'late_early' || c === 'late_ot' || c === 'early_ot') {
return 'bg-orange-50 hover:bg-orange-100'
}
if (c === 'missing') return 'bg-gray-100 hover:bg-gray-200'
if (c === 'holiday') return 'bg-indigo-50 hover:bg-indigo-100'
if (['al', 'sl', 'cl', 'mixed_leave'].includes(c)) return 'bg-violet-50 hover:bg-violet-100'
return 'hover:bg-slate-50'
}