Add Settings page, List page, report template
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
import { useState } from "react"
|
||||
import { Trash2, AlertTriangle, Shield } from "lucide-react"
|
||||
import api from "../api"
|
||||
|
||||
const RESET_SECTIONS = [
|
||||
{
|
||||
key: "attendance",
|
||||
label: "出勤 Attendance",
|
||||
description: "刪除所有出勤記錄(attendance_records 全表清空)",
|
||||
color: "red",
|
||||
},
|
||||
{
|
||||
key: "accident",
|
||||
label: "意外 Accident",
|
||||
description: "刪除所有意外記錄(accidents 全表清空)",
|
||||
color: "orange",
|
||||
},
|
||||
{
|
||||
key: "holidays",
|
||||
label: "假期 Holidays + Leave",
|
||||
description: "刪除所有公眾假期 + 員工請假記錄",
|
||||
color: "amber",
|
||||
},
|
||||
]
|
||||
|
||||
export default function Settings() {
|
||||
const [confirmText, setConfirmText] = useState({})
|
||||
const [loading, setLoading] = useState({})
|
||||
const [result, setResult] = useState({})
|
||||
|
||||
const handleReset = async (section) => {
|
||||
const confirmVal = confirmText[section] || ""
|
||||
if (confirmVal !== section) {
|
||||
setResult((prev) => ({ ...prev, [section]: { error: `請輸入 "${section}" 以確認刪除` } }))
|
||||
return
|
||||
}
|
||||
|
||||
setLoading((prev) => ({ ...prev, [section]: true }))
|
||||
setResult((prev) => ({ ...prev, [section]: null }))
|
||||
|
||||
try {
|
||||
const res = await api.post(`/api/admin/reset/${section}`, { confirm: section })
|
||||
setResult((prev) => ({
|
||||
...prev,
|
||||
[section]: {
|
||||
success: `已刪除 ${res.data.deleted} 條記錄`,
|
||||
},
|
||||
}))
|
||||
setConfirmText((prev) => ({ ...prev, [section]: "" }))
|
||||
} catch (err) {
|
||||
setResult((prev) => ({
|
||||
...prev,
|
||||
[section]: {
|
||||
error: err.response?.data?.detail || err.message || "刪除失敗",
|
||||
},
|
||||
}))
|
||||
} finally {
|
||||
setLoading((prev) => ({ ...prev, [section]: false }))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto space-y-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<Shield className="w-6 h-6 text-slate-700" />
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-900">系統管理</h1>
|
||||
<p className="text-sm text-slate-500 mt-0.5">
|
||||
危險操作,請小心使用
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Danger Zone */}
|
||||
<div className="bg-white border border-red-200 rounded-xl overflow-hidden">
|
||||
<div className="px-5 py-4 border-b border-red-100 bg-red-50/50">
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertTriangle className="w-5 h-5 text-red-600" />
|
||||
<h2 className="text-lg font-semibold text-red-800">Danger Zone — 資料庫重置</h2>
|
||||
</div>
|
||||
<p className="text-sm text-red-600 mt-1">
|
||||
以下操作將永久刪除資料,無法復原。請確認後再執行。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-slate-100">
|
||||
{RESET_SECTIONS.map((section) => {
|
||||
const res = result[section.key]
|
||||
const isLoading = loading[section.key]
|
||||
|
||||
return (
|
||||
<div key={section.key} className="px-5 py-4">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold text-slate-900">{section.label}</h3>
|
||||
<p className="text-sm text-slate-500 mt-0.5">{section.description}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="text"
|
||||
className="w-32 px-3 py-2 text-sm border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-red-500 focus:border-red-500 placeholder:text-slate-400"
|
||||
placeholder={`輸入 "${section.key}"`}
|
||||
value={confirmText[section.key] || ""}
|
||||
onChange={(e) => {
|
||||
setConfirmText((prev) => ({ ...prev, [section.key]: e.target.value }))
|
||||
setResult((prev) => ({ ...prev, [section.key]: null }))
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") handleReset(section.key)
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleReset(section.key)}
|
||||
disabled={isLoading || (confirmText[section.key] || "") !== section.key}
|
||||
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-700 disabled:bg-slate-300 disabled:cursor-not-allowed rounded-md transition-colors"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
{isLoading ? "刪除中..." : "重置"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Result message */}
|
||||
{res && (
|
||||
<div
|
||||
className={`mt-3 text-sm px-3 py-2 rounded-md ${
|
||||
res.error
|
||||
? "bg-red-50 text-red-700 border border-red-200"
|
||||
: "bg-green-50 text-green-700 border border-green-200"
|
||||
}`}
|
||||
>
|
||||
{res.error ? `❌ ${res.error}` : `✅ ${res.success}`}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user