StatusBadge: split badges per dimension (abnormal/late/early/OT <=30min); Attendance filter by status_code conditions for abnormal/late/early/OT/normal/leave types

This commit is contained in:
IT狗
2026-07-21 18:10:43 +08:00
parent 4c9db01213
commit 33a5e3b2ad
3 changed files with 61 additions and 103 deletions
+34
View File
@@ -1267,6 +1267,40 @@ 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)
)
elif status_text == 'late':
q = q.filter(
(AttendanceRecord.late_minutes > 0) &
(AttendanceRecord.late_minutes <= 30)
)
elif status_text == 'early':
q = q.filter(
(AttendanceRecord.early_minutes > 0) &
(AttendanceRecord.early_minutes <= 30)
)
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 == 'missing')
elif status_text in ('holiday', 'al', 'sl', 'cl', 'mixed_leave'):
q = q.filter(AttendanceRecord.status_code == status_text)
elif status_text == 'normal':
q = q.filter(
(AttendanceRecord.late_minutes == 0) &
(AttendanceRecord.early_minutes == 0) &
(AttendanceRecord.ot_minutes == 0)
)
else:
# Fallback to text search
q = q.filter(AttendanceRecord.status_text.like(f"%{status_text}%"))
if search:
like = f"%{search}%"
+21 -100
View File
@@ -2,25 +2,20 @@ import { CheckCircle, AlertCircle, Clock, X, AlertTriangle, Briefcase, Stethosco
/**
* Reusable status badge for attendance records.
* Handles all status codes including enriched leave types:
* normal / late / late_early / late_ot / early / early_ot / ot / abnormal / missing
* holiday / al / sl / cl / mixed_leave
* Renders one badge per status dimension (abnormal / late / early / OT / leave).
*
* Props:
* - code: status_code string
* - text: status_text string (already enriched)
* - code: status_code string (used for row-coloring and fallback)
* - late: late_minutes number
* - early: early_minutes number
* - ot: ot_minutes number
* - size: 'sm' | 'md' (default 'sm')
*/
export default function StatusBadge({ code, text = '', size = 'sm' }) {
export default function StatusBadge({ code, late = 0, early = 0, ot = 0, size = 'sm' }) {
const sizeCls = size === 'md'
? 'px-3 py-1 text-sm'
: 'px-2 py-0.5 text-xs'
// Split text into main + leave parts (after ' + ' separator)
const parts = text.split(/\s*\+\s*/).filter(Boolean)
const mainText = parts[0] || ''
const leaveParts = parts.slice(1)
const badge = (icon, label, colorCls) => (
<span className={`inline-flex items-center gap-1 ${sizeCls} rounded-full font-medium ${colorCls}`}>
{icon}
@@ -28,97 +23,23 @@ export default function StatusBadge({ code, text = '', size = 'sm' }) {
</span>
)
// ============ Attendance status badges ============
if (code === 'normal') {
return (
<span className="inline-flex items-center gap-1.5">
{badge(<CheckCircle size={12} />, mainText || '正常', 'bg-green-100 text-green-700')}
{leaveParts.map((lp, i) => (
<span key={i} className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs bg-slate-100 text-slate-600">
+ {lp}
</span>
))}
</span>
)
}
if (code === 'late' || code === 'late_early' || code === 'late_ot') {
return (
<span className="inline-flex items-center gap-1.5">
{badge(<AlertCircle size={12} />, mainText || code, 'bg-orange-100 text-orange-700')}
{leaveParts.map((lp, i) => (
<span key={i} className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs bg-slate-100 text-slate-600">
+ {lp}
</span>
))}
</span>
)
}
if (code === 'early' || code === 'early_ot') {
return (
<span className="inline-flex items-center gap-1.5">
{badge(<Clock size={12} />, mainText || code, 'bg-blue-100 text-blue-700')}
{leaveParts.map((lp, i) => (
<span key={i} className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs bg-slate-100 text-slate-600">
+ {lp}
</span>
))}
</span>
)
}
if (code === 'ot') {
return (
<span className="inline-flex items-center gap-1.5">
{badge(<Clock size={12} />, mainText || '加班', 'bg-purple-100 text-purple-700')}
{leaveParts.map((lp, i) => (
<span key={i} className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs bg-slate-100 text-slate-600">
+ {lp}
</span>
))}
</span>
)
}
if (code === 'abnormal') {
return (
<span className="inline-flex items-center gap-1.5">
{badge(<AlertTriangle size={12} />, mainText || '⚠️異常', 'bg-red-100 text-red-700')}
{leaveParts.map((lp, i) => (
<span key={i} className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs bg-slate-100 text-slate-600">
+ {lp}
</span>
))}
</span>
)
}
if (code === 'missing') {
return (
<span className="inline-flex items-center gap-1.5">
{badge(<X size={12} />, '缺勤', 'bg-gray-200 text-gray-600')}
{leaveParts.map((lp, i) => (
<span key={i} className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs bg-slate-100 text-slate-600">
{lp}
</span>
))}
</span>
)
const parts = []
// Abnormal (any dimension > 30 min)
if (late > 30 || early > 30 || ot > 30) {
if (late > 30) parts.push(badge(<AlertTriangle size={12} />, `異常-遲到${late}`, 'bg-red-100 text-red-700'))
else if (early > 30) parts.push(badge(<AlertTriangle size={12} />, `異常-早退${early}`, 'bg-red-100 text-red-700'))
else parts.push(badge(<AlertTriangle size={12} />, `異常-OT${ot}`, 'bg-red-100 text-red-700'))
} else {
// Individual badges (≤ 30 min each)
if (late > 0) parts.push(badge(<AlertCircle size={12} />, `遲到${late}`, 'bg-orange-100 text-orange-700'))
if (early > 0) parts.push(badge(<Clock size={12} />, `早退${early}`, 'bg-blue-100 text-blue-700'))
if (ot > 0) parts.push(badge(<Clock size={12} />, `OT${ot}`, 'bg-purple-100 text-purple-700'))
}
// ============ Enriched leave status badges ============
if (code === 'holiday') {
return badge(<PartyPopper size={12} />, mainText, 'bg-amber-100 text-amber-700')
}
if (code === 'al') {
return badge(<Briefcase size={12} />, mainText || '年假', 'bg-emerald-100 text-emerald-700')
}
if (code === 'sl') {
return badge(<Stethoscope size={12} />, mainText || '病假', 'bg-pink-100 text-pink-700')
}
if (code === 'cl') {
return badge(<Timer size={12} />, mainText || '補鐘', 'bg-indigo-100 text-indigo-700')
}
if (code === 'mixed_leave') {
return badge(<Timer size={12} />, mainText || '混合假', 'bg-fuchsia-100 text-fuchsia-700')
if (parts.length === 0) {
return badge(<CheckCircle size={12} />, '正常', 'bg-green-100 text-green-700')
}
// fallback
return <span className={`inline-flex items-center px-2 py-0.5 rounded text-xs text-slate-500 bg-slate-100`}>{text || code || '-'}</span>
return <span className="inline-flex items-center gap-1 flex-wrap">{parts}</span>
}
+4 -1
View File
@@ -87,7 +87,10 @@ export default function AttendanceList() {
}
const getStatusBadge = (record) => {
return <StatusBadge code={record.status_code} text={record.status_text} />
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} />
}
const getRowClass = (code) => {