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:
@@ -1267,6 +1267,40 @@ async def attendance_records(
|
|||||||
if status:
|
if status:
|
||||||
q = q.filter(AttendanceRecord.status_code == status)
|
q = q.filter(AttendanceRecord.status_code == status)
|
||||||
if status_text:
|
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}%"))
|
q = q.filter(AttendanceRecord.status_text.like(f"%{status_text}%"))
|
||||||
if search:
|
if search:
|
||||||
like = f"%{search}%"
|
like = f"%{search}%"
|
||||||
|
|||||||
@@ -2,25 +2,20 @@ import { CheckCircle, AlertCircle, Clock, X, AlertTriangle, Briefcase, Stethosco
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Reusable status badge for attendance records.
|
* Reusable status badge for attendance records.
|
||||||
* Handles all status codes including enriched leave types:
|
* Renders one badge per status dimension (abnormal / late / early / OT / leave).
|
||||||
* normal / late / late_early / late_ot / early / early_ot / ot / abnormal / missing
|
|
||||||
* holiday / al / sl / cl / mixed_leave
|
|
||||||
*
|
*
|
||||||
* Props:
|
* Props:
|
||||||
* - code: status_code string
|
* - code: status_code string (used for row-coloring and fallback)
|
||||||
* - text: status_text string (already enriched)
|
* - late: late_minutes number
|
||||||
|
* - early: early_minutes number
|
||||||
|
* - ot: ot_minutes number
|
||||||
* - size: 'sm' | 'md' (default 'sm')
|
* - 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'
|
const sizeCls = size === 'md'
|
||||||
? 'px-3 py-1 text-sm'
|
? 'px-3 py-1 text-sm'
|
||||||
: 'px-2 py-0.5 text-xs'
|
: '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) => (
|
const badge = (icon, label, colorCls) => (
|
||||||
<span className={`inline-flex items-center gap-1 ${sizeCls} rounded-full font-medium ${colorCls}`}>
|
<span className={`inline-flex items-center gap-1 ${sizeCls} rounded-full font-medium ${colorCls}`}>
|
||||||
{icon}
|
{icon}
|
||||||
@@ -28,97 +23,23 @@ export default function StatusBadge({ code, text = '', size = 'sm' }) {
|
|||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
|
|
||||||
// ============ Attendance status badges ============
|
const parts = []
|
||||||
if (code === 'normal') {
|
|
||||||
return (
|
// Abnormal (any dimension > 30 min)
|
||||||
<span className="inline-flex items-center gap-1.5">
|
if (late > 30 || early > 30 || ot > 30) {
|
||||||
{badge(<CheckCircle size={12} />, mainText || '正常', 'bg-green-100 text-green-700')}
|
if (late > 30) parts.push(badge(<AlertTriangle size={12} />, `異常-遲到${late}分`, 'bg-red-100 text-red-700'))
|
||||||
{leaveParts.map((lp, i) => (
|
else if (early > 30) parts.push(badge(<AlertTriangle size={12} />, `異常-早退${early}分`, 'bg-red-100 text-red-700'))
|
||||||
<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">
|
else parts.push(badge(<AlertTriangle size={12} />, `異常-OT${ot}分`, 'bg-red-100 text-red-700'))
|
||||||
+ {lp}
|
} else {
|
||||||
</span>
|
// Individual badges (≤ 30 min each)
|
||||||
))}
|
if (late > 0) parts.push(badge(<AlertCircle size={12} />, `遲到${late}分`, 'bg-orange-100 text-orange-700'))
|
||||||
</span>
|
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'))
|
||||||
}
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ Enriched leave status badges ============
|
if (parts.length === 0) {
|
||||||
if (code === 'holiday') {
|
return badge(<CheckCircle size={12} />, '正常', 'bg-green-100 text-green-700')
|
||||||
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')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// fallback
|
return <span className="inline-flex items-center gap-1 flex-wrap">{parts}</span>
|
||||||
return <span className={`inline-flex items-center px-2 py-0.5 rounded text-xs text-slate-500 bg-slate-100`}>{text || code || '-'}</span>
|
|
||||||
}
|
}
|
||||||
@@ -87,7 +87,10 @@ export default function AttendanceList() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getStatusBadge = (record) => {
|
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) => {
|
const getRowClass = (code) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user