Initial commit: AARS backend + frontend + holiday/leave phase 1+2
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
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, FileText, AlertTriangle } from 'lucide-react'
|
||||
|
||||
const SEVERITY_COLORS = {
|
||||
'1': { bg: '#D1FAE5', fg: '#065F46', label: '低 Low' },
|
||||
'2': { bg: '#DBEAFE', fg: '#1E40AF', label: '中 Medium' },
|
||||
'3': { bg: '#FEF3C7', fg: '#92400E', label: '高 High' },
|
||||
'4': { bg: '#FED7AA', fg: '#9A3412', label: '嚴重 Severe' },
|
||||
'5': { bg: '#FEE2E2', fg: '#7F1D1D', label: '極嚴重 Critical' },
|
||||
}
|
||||
|
||||
const FIELDS = [
|
||||
{ key: 'date', label: '日期 Date' },
|
||||
{ key: 'time', label: '時間 Time' },
|
||||
{ key: 'severity', label: '嚴重程度 Severity', badge: true },
|
||||
{ key: 'location', label: '地點 Location' },
|
||||
{ key: 'employee_name', label: '員工/報告人 Employee' },
|
||||
{ key: 'department', label: '部門 Department' },
|
||||
{ key: 'description', label: '事件描述 Description' },
|
||||
{ key: 'medical_report', label: '醫療報告 Medical Report' },
|
||||
{ key: 'action_taken', label: '處理情況 Action Taken' },
|
||||
{ key: 'responsible_person', label: '負責人 Responsible Person' },
|
||||
{ key: 'created_at', label: '建立時間 Created At' },
|
||||
]
|
||||
|
||||
export default function AccidentDetail() {
|
||||
const { id } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const [record, setRecord] = useState(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
const numericId = parseInt(id, 10)
|
||||
if (isNaN(numericId) || numericId <= 0) {
|
||||
navigate('/accident/list', { replace: true })
|
||||
return
|
||||
}
|
||||
fetchRecord(numericId)
|
||||
}, [id])
|
||||
|
||||
const fetchRecord = async (recordId) => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const { data } = await api.get(`/api/accident/${recordId}`)
|
||||
setRecord(data)
|
||||
} catch (err) {
|
||||
console.error('Error fetching record:', err)
|
||||
const detail = err.response?.data?.detail
|
||||
// FastAPI 422 returns detail as array of validation errors — stringify safely
|
||||
let msg = 'Failed to load record'
|
||||
if (typeof detail === 'string') {
|
||||
msg = detail
|
||||
} else if (Array.isArray(detail)) {
|
||||
msg = `Invalid request: ${detail.map(d => d.msg || JSON.stringify(d)).join('; ')}`
|
||||
} else if (detail && typeof detail === 'object') {
|
||||
msg = JSON.stringify(detail)
|
||||
} else if (err.response?.status === 404) {
|
||||
msg = 'Record not found'
|
||||
}
|
||||
setError(msg)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
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">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link
|
||||
to="/accident/list"
|
||||
className="flex items-center gap-2 text-slate-600 hover:text-slate-900"
|
||||
>
|
||||
<ArrowLeft size={20} />
|
||||
返回列表
|
||||
</Link>
|
||||
</div>
|
||||
<div className="card p-8 text-center">
|
||||
<AlertTriangle size={48} className="mx-auto text-red-400 mb-2" />
|
||||
<div className="text-red-500 mb-2">{error}</div>
|
||||
<button
|
||||
onClick={() => navigate('/accident/list')}
|
||||
className="mt-4 px-4 py-2 text-sm text-primary-600 hover:text-primary-700"
|
||||
>
|
||||
返回列表
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const sev = String(record.severity || '')
|
||||
const sevColor = SEVERITY_COLORS[sev] || { bg: '#F3F4F6', fg: '#374151', label: sev || '-' }
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 mb-4 sm:mb-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link
|
||||
to="/accident/list"
|
||||
className="flex items-center gap-2 text-slate-600 hover:text-slate-900"
|
||||
>
|
||||
<ArrowLeft size={20} />
|
||||
返回
|
||||
</Link>
|
||||
<h1 className="text-xl sm:text-2xl font-bold text-slate-900">
|
||||
意外記錄 #{id}
|
||||
</h1>
|
||||
</div>
|
||||
<span
|
||||
className="inline-flex items-center px-3 py-1 rounded-full text-xs font-bold"
|
||||
style={{ backgroundColor: sevColor.bg, color: sevColor.fg }}
|
||||
>
|
||||
Severity {sev} · {sevColor.label}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="card overflow-hidden">
|
||||
<div className="p-4 border-b border-slate-200 bg-slate-50">
|
||||
<div className="flex items-center gap-3">
|
||||
<FileText size={24} className="text-slate-400" />
|
||||
<div>
|
||||
<div className="font-medium text-slate-900">記錄詳情</div>
|
||||
<div className="text-sm text-slate-500">AARS 意外事故記錄</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-slate-100">
|
||||
{FIELDS.map(({ key, label, badge }) => {
|
||||
const val = record[key]
|
||||
const display = val !== null && val !== undefined && val !== '' ? String(val) : '-'
|
||||
return (
|
||||
<div key={key} className="grid grid-cols-1 sm:grid-cols-12">
|
||||
<div className="sm:col-span-3 px-4 py-3 text-sm font-medium text-slate-500 bg-slate-50">
|
||||
{label}
|
||||
</div>
|
||||
<div className="sm:col-span-9 px-4 py-3 text-sm text-slate-900 break-words">
|
||||
{badge && sev !== '' ? (
|
||||
<span
|
||||
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-bold"
|
||||
style={{ backgroundColor: sevColor.bg, color: sevColor.fg }}
|
||||
>
|
||||
{sev} · {sevColor.label}
|
||||
</span>
|
||||
) : (
|
||||
display
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user