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.commit?.slice(0, 8)}
{version.date && <> · {new Date(version.date).toLocaleString("zh-HK", {timeZone:"Asia/Hong_Kong"})}>}
手動建立快照,或從過往備份還原
⚠️ 還原將覆蓋當前資料庫。輸入 {restoreConfirm} 確認:
重設使用者密碼。Admin 限。
為 {resetTarget.email} 設定新密碼
以下操作將永久刪除資料,無法復原。請確認後再執行。
{section.description}