import { useState, useEffect } from "react" import { Trash2, AlertTriangle, Shield, Download, Upload, RefreshCw, Database, Info, KeyRound, User as UserIcon } from "lucide-react" import api from "../api" import toast from "react-hot-toast" const RESET_SECTIONS = [ { key: "attendance", label: "出勤 Attendance", description: "刪除所有出勤記錄(attendance_records 全表清空)", color: "red", }, { key: "accident", label: "意外 Accident", description: "刪除所有意外記錄(accidents 全表清空)", color: "orange", }, { key: "holidays", label: "假期 Holidays", description: "刪除所有公眾假期記錄", color: "amber", }, { key: "leaves", label: "請假 Leaves", description: "刪除所有員工請假記錄", color: "lime", }, ] export default function Settings() { const [confirmText, setConfirmText] = useState({}) const [loading, setLoading] = useState({}) const [result, setResult] = useState({}) const [version, setVersion] = useState(null) const [backups, setBackups] = useState([]) const [loadingBackups, setLoadingBackups] = useState(false) const [creatingBackup, setCreatingBackup] = useState(false) const [restoring, setRestoring] = useState(null) const [restoreConfirm, setRestoreConfirm] = useState("") // User management (admin only) const [users, setUsers] = useState([]) const [loadingUsers, setLoadingUsers] = useState(false) const [resetTarget, setResetTarget] = useState(null) const [newPassword, setNewPassword] = useState("") const [resetting, setResetting] = useState(false) const [resetError, setResetError] = useState(null) useEffect(() => { fetchVersion() fetchBackups() fetchUsers() }, []) const fetchUsers = async () => { setLoadingUsers(true) try { const { data } = await api.get("/api/auth/users") setUsers(data || []) } catch (e) { // 403 if not admin — leave list empty setUsers([]) } finally { setLoadingUsers(false) } } const openResetDialog = (u) => { setResetTarget(u) setNewPassword("") setResetError(null) } const closeResetDialog = () => { if (resetting) return setResetTarget(null) setNewPassword("") setResetError(null) } const handleResetPassword = async () => { if (!resetTarget || !newPassword) return if (newPassword.length < 6) { setResetError("密碼至少需要 6 個字元") return } setResetting(true) setResetError(null) try { await api.post("/api/auth/reset-password", { email: resetTarget.email, new_password: newPassword, }) toast.success(`已重設 ${resetTarget.email} 嘅密碼`) closeResetDialog() } catch (e) { setResetError(e.response?.data?.detail || e.message) } finally { setResetting(false) } } const fetchVersion = async () => { try { const { data } = await api.get("/api/version") setVersion(data) } catch (e) { // ignore } } const fetchBackups = async () => { setLoadingBackups(true) try { const { data } = await api.get("/api/admin/backup/list") setBackups(data || []) } catch (e) { console.error("Failed to fetch backups:", e) } finally { setLoadingBackups(false) } } const handleCreateBackup = async () => { setCreatingBackup(true) try { const { data } = await api.post("/api/admin/backup/create") toast.success(`Backup created: ${data.saved}`) fetchBackups() } catch (e) { toast.error("Backup failed: " + (e.response?.data?.detail || e.message)) } finally { setCreatingBackup(false) } } const handleRestore = async (filename) => { if (restoreConfirm !== filename) { toast.error("Please type the filename to confirm restore") return } setRestoring(filename) try { const { data } = await api.post("/api/admin/backup/restore", { filename }) toast.success(`Restored from ${filename}. Auto-backup: ${data.auto_backup}`) setRestoreConfirm("") fetchBackups() } catch (e) { toast.error("Restore failed: " + (e.response?.data?.detail || e.message)) } finally { setRestoring(null) } } const handleDownload = (filename) => { const token = localStorage.getItem("aars_token") if (!token) return window.open(`/api/admin/backup/download/${filename}?token=${token}`, "_blank") } 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 })) } } const formatSize = (bytes) => { if (!bytes) return "—" if (bytes < 1024) return bytes + " B" if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB" return (bytes / 1024 / 1024).toFixed(1) + " MB" } const formatDate = (iso) => { if (!iso) return "—" try { const d = new Date(iso) return d.toLocaleString("zh-HK", { timeZone: "Asia/Hong_Kong" }) } catch { return iso } } return (
{/* Version Info */} {version && (
AARS v{version.version}
Commit: {version.commit?.slice(0, 8)} {version.date && <> · {new Date(version.date).toLocaleString("zh-HK", {timeZone:"Asia/Hong_Kong"})}}
)} {/* Backup / Restore */}

💾 資料庫 Backup / Restore

手動建立快照,或從過往備份還原

{loadingBackups ? (
載入中...
) : backups.length === 0 ? (
尚無備份記錄
) : ( backups.map((b) => (
{b.filename}
{formatDate(b.created)} · {formatSize(b.size)}
)) )}
{/* Restore confirmation */} {restoreConfirm && (

⚠️ 還原將覆蓋當前資料庫。輸入 {restoreConfirm} 確認:

setRestoreConfirm(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && restoreConfirm === restoreConfirm) handleRestore(restoreConfirm) }} />
)}
{/* User Management — admin only */}

使用者管理 User Management

重設使用者密碼。Admin 限。

{loadingUsers && (
載入中…
)} {!loadingUsers && users.length === 0 && (
沒有使用者(或你唔係 admin)
)} {!loadingUsers && users.map((u) => (
{u.name || u.email}
{u.email} {u.role} {!u.is_active && ( inactive )}
))}
{/* Reset Password Modal */} {resetTarget && (
e.stopPropagation()} >

重設密碼

{resetTarget.email} 設定新密碼

{resetError && (
❌ {resetError}
)}
)} {/* Danger Zone */}

Danger Zone — 資料庫重置

以下操作將永久刪除資料,無法復原。請確認後再執行。

{RESET_SECTIONS.map((section) => { const res = result[section.key] const isLoading = loading[section.key] return (

{section.label}

{section.description}

{ setConfirmText((prev) => ({ ...prev, [section.key]: e.target.value })) setResult((prev) => ({ ...prev, [section.key]: null })) }} onKeyDown={(e) => { if (e.key === "Enter") handleReset(section.key) }} />
{res && (
{res.error ? `❌ ${res.error}` : `✅ ${res.success}`}
)}
) })}
) }