Initial commit: AARS backend + frontend + holiday/leave phase 1+2

This commit is contained in:
IT Dog
2026-07-19 20:10:15 +08:00
commit bbc0048c24
49 changed files with 13375 additions and 0 deletions
+170
View File
@@ -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>
)
}