Files
aars/frontend/src/pages/attendance/Detail.jsx
T

229 lines
7.9 KiB
React
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 { useParams, useNavigate, Link } from 'react-router-dom'
import api from '../../api'
import toast from 'react-hot-toast'
import { ArrowLeft, Save, X } from 'lucide-react'
export default function AttendanceDetail() {
const { id } = useParams()
const navigate = useNavigate()
const [record, setRecord] = useState(null)
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [editMode, setEditMode] = useState(false)
const [formData, setFormData] = useState({})
const [error, setError] = useState(null)
useEffect(() => {
fetchRecord()
}, [id])
const fetchRecord = async () => {
setLoading(true)
setError(null)
try {
// Fetch all records and find by id
const { data } = await api.get('/api/attendance/records?per_page=1000')
const found = (data.records || []).find(r => r.id === parseInt(id))
if (found) {
setRecord(found)
setFormData({
employee_name: found.employee_name || '',
company: found.company || '',
date: found.date || '',
weekday: found.weekday || '',
shift_code: found.shift_code || '',
status_code: found.status_code || '',
status_text: found.status_text || '',
late_minutes: found.late_minutes || 0,
early_minutes: found.early_minutes || 0,
ot_minutes: found.ot_minutes || 0,
expected_in: found.expected_in || '',
expected_out: found.expected_out || '',
actual_in: found.actual_in || '',
actual_out: found.actual_out || '',
is_manually_edited: found.is_manually_edited || false,
})
} else {
setError('找不到記錄')
}
} catch (err) {
console.error('Error:', err)
setError('載入失敗')
} finally {
setLoading(false)
}
}
const handleSave = async () => {
setSaving(true)
try {
await api.put(`/api/attendance/${id}`, formData)
toast.success('保存成功')
setEditMode(false)
fetchRecord()
} catch (err) {
toast.error('保存失敗')
} finally {
setSaving(false)
}
}
const handleCancel = () => {
setEditMode(false)
if (record) {
setFormData({
employee_name: record.employee_name || '',
company: record.company || '',
date: record.date || '',
weekday: record.weekday || '',
shift_code: record.shift_code || '',
status_code: record.status_code || '',
status_text: record.status_text || '',
late_minutes: record.late_minutes || 0,
early_minutes: record.early_minutes || 0,
ot_minutes: record.ot_minutes || 0,
expected_in: record.expected_in || '',
expected_out: record.expected_out || '',
actual_in: record.actual_in || '',
actual_out: record.actual_out || '',
})
}
}
const getStatusBadge = (code, text) => {
const badges = {
normal: 'bg-green-100 text-green-700',
late: 'bg-orange-100 text-orange-700',
early: 'bg-blue-100 text-blue-700',
ot: 'bg-purple-100 text-purple-700',
abnormal: 'bg-red-100 text-red-700',
missing: 'bg-gray-200 text-gray-600',
late_early: 'bg-orange-100 text-orange-700',
late_ot: 'bg-orange-100 text-orange-700',
early_ot: 'bg-blue-100 text-blue-700',
}
return (
<span className={`px-3 py-1 rounded-full text-xs font-medium ${badges[code] || 'bg-gray-100 text-gray-600'}`}>
{text || code}
</span>
)
}
const displayFields = [
{ key: 'employee_name', label: '員工姓名' },
{ key: 'company', label: '公司' },
{ key: 'date', label: '日期' },
{ key: 'weekday', label: '星期' },
{ key: 'shift_code', label: '班次' },
{ key: 'expected_in', label: '應上班時間' },
{ key: 'expected_out', label: '應下班時間' },
{ key: 'actual_in', label: '實際上班' },
{ key: 'actual_out', label: '實際下班' },
{ key: 'late_minutes', label: '遲到分鐘' },
{ key: 'early_minutes', label: '早退分鐘' },
{ key: 'ot_minutes', label: 'OT分鐘' },
{ key: 'status_code', label: '狀態代碼' },
{ key: 'status_text', label: '狀態' },
]
if (loading) {
return (
<div className="flex items-center justify-center py-12">
<div className="text-slate-500">載入中...</div>
</div>
)
}
if (error) {
return (
<div className="space-y-4">
<Link to="/attendance" className="flex items-center gap-2 text-slate-600 hover:text-slate-900">
<ArrowLeft size={20} /> 返回列表
</Link>
<div className="card p-8 text-center text-red-500">{error}</div>
</div>
)
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Link to="/attendance" className="flex items-center gap-2 text-slate-600 hover:text-slate-900">
<ArrowLeft size={20} /> 返回
</Link>
<h1 className="text-xl font-bold text-slate-900">
出勤記錄 - {record?.employee_name}
</h1>
</div>
<div className="flex items-center gap-3">
{record && getStatusBadge(record.status_code, record.status_text)}
{!editMode ? (
<button
onClick={() => setEditMode(true)}
className="px-4 py-2 bg-blue-600 text-white text-sm rounded-md hover:bg-blue-700"
>
編輯
</button>
) : (
<>
<button
onClick={handleCancel}
className="flex items-center gap-2 px-4 py-2 border border-slate-300 text-slate-700 text-sm rounded-md hover:bg-slate-50"
>
<X size={16} /> 取消
</button>
<button
onClick={handleSave}
disabled={saving}
className="flex items-center gap-2 px-4 py-2 bg-green-600 text-white text-sm rounded-md hover:bg-green-700 disabled:opacity-50"
>
<Save size={16} /> {saving ? '保存中...' : '保存'}
</button>
</>
)}
</div>
</div>
{/* Manual edit warning */}
{record?.is_manually_edited && (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg px-4 py-3 text-sm text-yellow-800">
此記錄曾被手動修改之後重新導入 Excel 不會覆蓋此記錄
</div>
)}
{/* Record Details */}
<div className="card">
<div className="p-4 border-b border-slate-200 bg-slate-50">
<h2 className="font-semibold text-slate-700">記錄詳情</h2>
</div>
<div className="divide-y divide-slate-100">
{displayFields.map(({ key, label }) => (
<div key={key} className="grid grid-cols-12 items-center">
<div className="col-span-3 px-4 py-3 text-sm font-medium text-slate-500 bg-slate-50">
{label}
</div>
<div className="col-span-9 px-4 py-3">
{editMode && ['status_code', 'late_minutes', 'early_minutes', 'ot_minutes', 'actual_in', 'actual_out', 'shift_code'].includes(key) ? (
<input
type="text"
value={formData[key]}
onChange={(e) => setFormData({ ...formData, [key]: e.target.value })}
className="w-full px-3 py-1.5 text-sm border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
) : (
<span className="text-sm text-slate-900">
{record?.[key] !== null && record?.[key] !== undefined ? String(record[key]) : '-'}
</span>
)}
</div>
</div>
))}
</div>
</div>
</div>
)
}