Files
aars/frontend/src/pages/roster/Roster.jsx
T

191 lines
6.2 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect } from 'react'
import { Upload, CheckCircle, Trash2, Clock, Users, RefreshCw } from 'lucide-react'
import api from '../../api'
import toast from 'react-hot-toast'
export default function RosterPage() {
const [rosterData, setRosterData] = useState({ headers: [], rows: [] })
const [rosterInfo, setRosterInfo] = useState({ uploaded: false })
const [file, setFile] = useState(null)
const [uploading, setUploading] = useState(false)
const [loading, setLoading] = useState(true)
const loadRosterData = async () => {
try {
const [infoRes, dataRes] = await Promise.all([
api.get('/api/roster/info'),
api.get('/api/roster/data').catch(() => ({ data: { headers: [], rows: [] } }))
])
setRosterInfo(infoRes.data)
setRosterData(dataRes.data || { headers: [], rows: [] })
} catch (err) {
console.error('Load error:', err)
} finally {
setLoading(false)
}
}
useEffect(() => {
loadRosterData()
}, [])
const handleUpload = async (e) => {
e.preventDefault()
if (!file) return
setUploading(true)
const formData = new FormData()
formData.append('file', file)
try {
await api.post('/api/roster/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
})
toast.success('上傳成功!')
setFile(null)
loadRosterData()
} catch (err) {
toast.error(err.response?.data?.detail || '上傳失敗')
} finally {
setUploading(false)
}
}
const handleDelete = async () => {
if (!confirm('確定要刪除班次表嗎?')) return
try {
await api.delete('/api/roster')
toast.success('已刪除')
setRosterData({ headers: [], rows: [] })
setRosterInfo({ uploaded: false })
} catch (err) {
toast.error('刪除失敗')
}
}
const handleRecalc = async () => {
setLoading(true)
try {
await Promise.all([
api.get('/api/roster/info'),
api.get('/api/roster/data').catch(() => ({ data: { headers: [], rows: [] } }))
])
toast.success('已重新計算')
} catch {
toast.error('重新計算失敗')
} finally {
loadRosterData()
}
}
if (loading) {
return <div className="p-8 text-center">載入中...</div>
}
return (
<div className="p-6">
<h1 className="text-xl sm:text-2xl font-bold mb-4 sm:mb-6">班次管理 Roster</h1>
{/* Upload Section */}
<div className="card p-6 mb-6">
<h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
<Upload size={20} />
上傳班次表
</h2>
{!rosterInfo.uploaded ? (
<form onSubmit={handleUpload} className="flex gap-4 items-end">
<div className="flex-1">
<label className="block text-sm font-medium text-slate-700 mb-1">
選擇班次表 Excel 檔案
</label>
<input
type="file"
accept=".xlsx,.xls"
onChange={(e) => setFile(e.target.files[0])}
className="input-field"
/>
</div>
<button type="submit" disabled={!file || uploading} className="btn-primary">
{uploading ? '上傳中...' : '上傳'}
</button>
</form>
) : (
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<CheckCircle className="text-green-500" size={24} />
<div>
<p className="font-medium">已上傳班次表</p>
<p className="text-sm text-slate-500">
{rosterInfo.rows} 個班次{rosterInfo.columns}
</p>
</div>
</div>
<div className="flex gap-2">
<button onClick={handleRecalc} className="btn-primary">
<RefreshCw size={16} className="inline mr-1" />
RECALC
</button>
<button onClick={handleDelete} className="btn-danger">
<Trash2 size={16} className="inline mr-1" />
刪除
</button>
</div>
</div>
)}
</div>
{/* Roster Table */}
{rosterData.headers.length > 0 && (
<div className="card overflow-hidden">
<div className="p-4 border-b border-slate-200">
<h2 className="text-lg font-semibold flex items-center gap-2">
<Clock size={20} />
班次表
</h2>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead className="bg-slate-100 border-b border-slate-200">
<tr>
{rosterData.headers.map((header, idx) => (
<th key={idx} className="px-3 py-2 text-left text-xs font-semibold text-slate-600">
{header}
</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{rosterData.rows.map((row, rowIdx) => (
<tr key={rowIdx} className="hover:bg-slate-50">
{rosterData.headers.map((header, colIdx) => (
<td key={colIdx} className="px-3 py-2 text-slate-700">
{row[colIdx] !== null && row[colIdx] !== undefined
? String(row[colIdx])
: '-'}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* Instructions */}
{!rosterInfo.uploaded && (
<div className="card p-6">
<h2 className="text-lg font-semibold mb-4">如何使用</h2>
<ol className="list-decimal list-inside space-y-2 text-slate-600">
<li>上傳包含班次資料的 Excel 檔案</li>
<li>Excel 需要有班次描述</li>
<li>上傳後可以睇到所有班次</li>
<li> Attendance Excel 入面加班次填入員工所屬班次代碼</li>
</ol>
</div>
)}
</div>
)
}