From 4b967f792071ebe225eb3da032a4e84c3a3bd72a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?IT=E7=8B=97?= Date: Tue, 21 Jul 2026 21:33:42 +0800 Subject: [PATCH] =?UTF-8?q?StatusBadge:=20simplify=20to=205=20basic=20cate?= =?UTF-8?q?gories=20(=E6=AD=A3=E5=B8=B8/=E7=95=B0=E5=B8=B8/=E7=BC=BA?= =?UTF-8?q?=E5=8B=A4/=E5=85=AC=E5=81=87/=E8=AB=8B=E5=81=87)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - StatusBadge no longer shows detailed late/early/OT minute breakdowns nor AL/SL/CL sub-types; collapse to single basic-status badge - List page: replace text-input column filter with status dropdown (server-side ?status_text= filter; backend now supports 'leave' IN-clause for al/sl/cl/mixed_leave group) - List page: badge now driven by backend status_code/status_text (remove frontend latenessMap which contradicted backend status) --- backend/main.py | 3 + frontend/package-lock.json | 4 +- frontend/src/components/StatusBadge.jsx | 74 ++++------ frontend/src/pages/List.jsx | 185 +++++++----------------- 4 files changed, 88 insertions(+), 178 deletions(-) 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()}> + + + + + + )) )}