diff --git a/backend/main.py b/backend/main.py
index e19713c..206e973 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -1289,6 +1289,9 @@ async def attendance_records(
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 == '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) &
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 03a7df8..2c9ae9b 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "aars-frontend",
- "version": "1.0.0",
+ "version": "1.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "aars-frontend",
- "version": "1.0.0",
+ "version": "1.2.0",
"dependencies": {
"axios": "^1.7.7",
"lucide-react": "^0.441.0",
diff --git a/frontend/src/components/StatusBadge.jsx b/frontend/src/components/StatusBadge.jsx
index a7c0f2c..77cfcbf 100644
--- a/frontend/src/components/StatusBadge.jsx
+++ b/frontend/src/components/StatusBadge.jsx
@@ -1,18 +1,22 @@
-import { CheckCircle, AlertCircle, Clock, X, AlertTriangle, Briefcase, Stethoscope, Timer, PartyPopper, CalendarX, Palmtree } from 'lucide-react'
+import { CheckCircle, AlertCircle, CalendarX, Palmtree, Briefcase } from 'lucide-react'
/**
* Reusable status badge for attendance records.
- * Renders one badge per status dimension (abnormal / late / early / OT / leave).
+ * Simplified to BASIC status categories — no detailed breakdowns.
+ *
+ * Status mapping (driven by backend status_code / status_text):
+ * - missing → 缺勤 (grey)
+ * - holiday → 公假 (indigo)
+ * - al / sl / cl / mixed_leave → 請假 (violet)
+ * - normal → 正常 (green)
+ * - everything else (late/early/ot/abnormal/late_early/...) → 異常 (orange)
*
* Props:
- * - code: status_code string (used for row-coloring and fallback)
- * - late: late_minutes number
- * - early: early_minutes number
- * - ot: ot_minutes number
- * - text: status_text string (used for leave / holiday / mixed)
+ * - code: status_code string
+ * - text: status_text string (fallback signal)
* - size: 'sm' | 'md' (default 'sm')
*/
-export default function StatusBadge({ code, late = 0, early = 0, ot = 0, text = '', size = 'sm' }) {
+export default function StatusBadge({ code = '', text = '', size = 'sm' }) {
const sizeCls = size === 'md'
? 'px-3 py-1 text-sm'
: 'px-2 py-0.5 text-xs'
@@ -24,53 +28,31 @@ export default function StatusBadge({ code, late = 0, early = 0, ot = 0, text =
)
- // Missing: show grey 缺勤 badge
- if (code === 'missing' || text === '缺勤') {
+ const c = String(code || '').toLowerCase()
+ const t = String(text || '')
+
+ // 缺勤 (missing)
+ if (c === 'missing' || t === '缺勤') {
return badge(, '缺勤', 'bg-gray-200 text-gray-700')
}
- // Holiday: show purple 假期 badge
- if (code === 'holiday' || text === '🏖️公假') {
+ // 公假 (holiday)
+ if (c === 'holiday' || t.includes('公假') || t.includes('🏖️')) {
return badge(, '公假', 'bg-indigo-100 text-indigo-700')
}
- // Annual leave
- if (code === 'al' || text === '年假 AL') {
- return badge(, '年假 AL', 'bg-pink-100 text-pink-700')
+ // 請假 (any leave type)
+ if (['al', 'sl', 'cl', 'mixed_leave'].includes(c)
+ || t.includes('年假') || t.includes('病假') || t.includes('補鐘')
+ || t.includes('混合假')) {
+ return badge(, '請假', 'bg-violet-100 text-violet-700')
}
- // Sick leave
- if (code === 'sl' || text === '病假 SL') {
- return badge(, '病假 SL', 'bg-cyan-100 text-cyan-700')
- }
-
- // Compensation leave
- if (code === 'cl' || text === '補鐘 CL') {
- return badge(, '補鐘 CL', 'bg-amber-100 text-amber-700')
- }
-
- // Mixed leave
- if (code === 'mixed_leave' || text === '混合假') {
- return badge(, '混合假', 'bg-violet-100 text-violet-700')
- }
-
- const parts = []
-
- // Abnormal (any dimension > 30 min)
- if (late > 30 || early > 30 || ot > 30) {
- if (late > 30) parts.push(badge(, `異常-遲到${late}分`, 'bg-red-100 text-red-700'))
- else if (early > 30) parts.push(badge(, `異常-早退${early}分`, 'bg-red-100 text-red-700'))
- else parts.push(badge(, `異常-OT${ot}分`, 'bg-red-100 text-red-700'))
- } else {
- // Individual badges (≤ 30 min each)
- if (late > 0) parts.push(badge(, `遲到${late}分`, 'bg-orange-100 text-orange-700'))
- if (early > 0) parts.push(badge(, `早退${early}分`, 'bg-blue-100 text-blue-700'))
- if (ot > 0) parts.push(badge(, `OT${ot}分`, 'bg-purple-100 text-purple-700'))
- }
-
- if (parts.length === 0) {
+ // 正常
+ if (c === 'normal' && !t) {
return badge(, '正常', 'bg-green-100 text-green-700')
}
- return {parts}
+ // 異常 (late/early/ot/abnormal/late_early/late_ot/early_ot/...)
+ return badge(, '異常', 'bg-orange-100 text-orange-700')
}
\ No newline at end of file
diff --git a/frontend/src/pages/List.jsx b/frontend/src/pages/List.jsx
index 8bab9ea..ecff2fb 100644
--- a/frontend/src/pages/List.jsx
+++ b/frontend/src/pages/List.jsx
@@ -2,7 +2,8 @@ import { useState, useEffect, useMemo } from 'react'
import { Link } from 'react-router-dom'
import api from '../../api'
import toast from 'react-hot-toast'
-import { Search, Eye, ChevronUp, ChevronDown, Filter, X, AlertCircle, Clock, FileCode } from 'lucide-react'
+import { Search, Eye, ChevronUp, ChevronDown, Filter, X, FileCode } from 'lucide-react'
+import StatusBadge from '../components/StatusBadge'
const handleExportHTML = (section) => {
const token = localStorage.getItem('aars_token')
@@ -18,19 +19,18 @@ export default function AttendanceList() {
const [search, setSearch] = useState('')
const [sortKey, setSortKey] = useState(null)
const [sortDir, setSortDir] = useState('asc')
- const [filterKey, setFilterKey] = useState('')
- const [filterValue, setFilterValue] = useState('')
- const [latenessRecords, setLatenessRecords] = useState([])
+ const [statusFilter, setStatusFilter] = useState('')
useEffect(() => {
fetchRecords()
- fetchLateness()
- }, [])
+ }, [statusFilter])
const fetchRecords = async () => {
setLoading(true)
try {
- const { data } = await api.get('/api/attendance?page=1&page_size=1000')
+ const params = { page: 1, page_size: 1000 }
+ if (statusFilter) params.status_text = statusFilter
+ const { data } = await api.get('/api/attendance', { params })
setRecords(data.data || [])
if (data.data && data.data.length > 0) {
@@ -44,37 +44,10 @@ export default function AttendanceList() {
}
}
- const fetchLateness = async () => {
- try {
- const { data } = await api.get('/api/attendance/lateness/records')
- setLatenessRecords(data || [])
- } catch (err) {
- console.error('Failed to load lateness data:', err)
- }
- }
-
- // Create a map of late records for quick lookup
- const latenessMap = useMemo(() => {
- const map = {}
- latenessRecords.forEach(rec => {
- const key = `${rec.staff}-${rec.date}`
- map[key] = rec
- })
- return map
- }, [latenessRecords])
-
- // Filtered and sorted data
+ // Filtered and sorted data (client-side: search + sort only; status handled by backend)
const processedData = useMemo(() => {
let result = [...records]
- // Filter
- if (filterKey && filterValue) {
- result = result.filter(r => {
- const val = String(r[filterKey] || '').toLowerCase()
- return val.includes(filterValue.toLowerCase())
- })
- }
-
// Search
if (search) {
const searchLower = search.toLowerCase()
@@ -104,7 +77,7 @@ export default function AttendanceList() {
}
return result
- }, [records, search, sortKey, sortDir, filterKey, filterValue])
+ }, [records, search, sortKey, sortDir])
const handleSort = (key) => {
if (sortKey === key) {
@@ -115,42 +88,11 @@ export default function AttendanceList() {
}
}
- const handleFilter = (key) => {
- setFilterKey(key)
- setFilterValue('')
- }
-
- const clearFilter = () => {
- setFilterKey('')
- setFilterValue('')
- }
-
const getSortIcon = (key) => {
if (sortKey !== key) return null
return sortDir === 'asc' ? :
}
- // Get lateness info for a record
- const getLatenessInfo = (record) => {
- // Find staff name and date columns
- const staffKey = Object.keys(record).find(k =>
- k.toLowerCase().includes('staff') || k.toLowerCase().includes('name') || k.toLowerCase().includes('員工')
- )
- const dateKey = Object.keys(record).find(k =>
- k.toLowerCase().includes('date') || k.toLowerCase().includes('日期')
- )
-
- if (!staffKey || !dateKey) return null
-
- const staff = record[staffKey]
- const date = record[dateKey]
-
- if (!staff || !date) return null
-
- const key = `${staff}-${String(date).slice(0, 10)}`
- return latenessMap[key]
- }
-
return (
{/* Header */}
@@ -188,43 +130,38 @@ export default function AttendanceList() {
/>
- {/* Column Filter */}
+ {/* Status Filter (basic categories — server-side) */}
- {filterKey && (
- <>
- setFilterValue(e.target.value)}
- placeholder="輸入篩選值..."
- className="text-sm border border-slate-300 rounded-md px-2 py-2 w-32"
- />
-
- >
+ {statusFilter && (
+
)}
{/* Result count */}
{processedData.length} 筆記錄
- {latenessRecords.length > 0 && (
-
- • {latenessRecords.length} 筆遲到
-
+ {statusFilter && (
+ • 已篩選:{statusFilter}
)}
@@ -267,46 +204,34 @@ export default function AttendanceList() {
) : (
- processedData.map((record) => {
- const lateInfo = getLatenessInfo(record)
- const isLate = lateInfo && lateInfo.minutes_late > 0
-
- return (
- window.location.href = `/attendance/${record._row}`}
- >
- | {record._row} |
- {headers.slice(0, 6).map(h => (
-
- {record[h] !== null && record[h] !== undefined ? String(record[h]) : '-'}
- |
- ))}
-
- {isLate ? (
-
-
- {lateInfo.minutes_late}分
-
- ) : (
-
-
- 正常
-
- )}
+ processedData.map((record) => (
+ |
window.location.href = `/attendance/${record._row}`}
+ >
+ | {record._row} |
+ {headers.slice(0, 6).map(h => (
+
+ {record[h] !== null && record[h] !== undefined ? String(record[h]) : '-'}
|
- e.stopPropagation()}>
-
-
-
- |
-
- )
- })
+ ))}
+
+
+ |
+ e.stopPropagation()}>
+
+
+
+ |
+
+ ))
)}