(() => { const { Card, Button, IconButton, Badge, Avatar, Modal, Field, Input, Select, DataTable, EmptyState, Icon } = window.TaskFlowDesignSystem_dd53c2; /* Satu-satunya tempat kelola akun (halaman Setting lama dihapus, isinya pindah ke sini). Password TIDAK pernah disimpan di klien: form hanya mengirim perintah ke server (password_hash + flag must_change). */ const DEFAULT_POSITIONS = ["Manager", "Staff"]; const GRANTABLE_PAGES = [ { id: "dashboard", label: "Dashboard", icon: "layout-dashboard" }, { id: "tasks", label: "Task List", icon: "clipboard-list" }, { id: "calendar", label: "Calendar", icon: "calendar-days" }, { id: "document", label: "Document", icon: "folder-open" }, { id: "report", label: "Report (lihat saja)", icon: "chart-pie" }, { id: "profile", label: "Profile", icon: "user" }, { id: "absen", label: "Absen Kunjungan", icon: "map-pin-check" }, ]; const ADMIN_ONLY_PAGES = ["Task Verification", "Add New Task", "Log Activity", "Manajemen User"]; const DEFAULT_ROLES = [ { id: "user", name: "User", pages: ["dashboard", "tasks", "calendar", "document", "profile"], locked: true }, { id: "marketing", name: "Marketing", pages: ["dashboard", "tasks", "calendar", "document", "profile", "absen"], locked: true }, ]; const DIREKTUR = DB.direktur; function UserManagement({ users, tasks = [], onSave, onToggleActive, onResetPassword, onDelete }) { const narrow = useBreakpoint() !== "desktop"; const [q, setQ] = React.useState(""); const [positions, setPositions] = React.useState(DEFAULT_POSITIONS); const [posEdit, setPosEdit] = React.useState(null); const [posErr, setPosErr] = React.useState(""); const [roles, setRoles] = React.useState(DEFAULT_ROLES); const [roleEdit, setRoleEdit] = React.useState(null); const [roleErr, setRoleErr] = React.useState(""); const [form, setForm] = React.useState(null); // {mode,id,values} const [reset, setReset] = React.useState(null); const [wipe, setWipe] = React.useState(false); const [confirm, setConfirm] = React.useState(null); const [err, setErr] = React.useState(""); const load = React.useMemo(() => { const m = {}; tasks.forEach((t) => { m[t.user] = m[t.user] || { total: 0, open: 0, late: 0 }; m[t.user].total++; if (t.status !== "done") m[t.user].open++; if (t.status !== "done" && isOverdue(t.target)) m[t.user].late++; }); return m; }, [tasks]); const rows = React.useMemo(() => { const s = q.trim().toLowerCase(); return [DIREKTUR, ...users].filter((u) => !s || u.name.toLowerCase().includes(s) || u.email.toLowerCase().includes(s) || String(u.role).toLowerCase().includes(s) || String(u.position || "").toLowerCase().includes(s)); }, [users, q]); const openAdd = () => { setErr(""); setForm({ mode: "add", id: null, values: { name: "", email: "", role: positions[0] || "Staff", position: positions[0] || "Staff", access: "user", password: "" } }); }; const openEdit = (u) => { setErr(""); setForm({ mode: "edit", id: u.id, values: { name: u.name, email: u.email, role: u.role, position: u.position || positions[0] || "Staff", access: u.access || "user", password: "" } }); }; const submit = () => { const v = form.values; if (!v.name.trim()) return setErr("Nama wajib diisi."); if (!/^[^\s@]+@[^\s@]+\.[a-z]{2,}$/i.test(v.email.trim())) return setErr("Format email tidak valid."); if ([DIREKTUR, ...users].some((u) => u.id !== form.id && u.email.toLowerCase() === v.email.trim().toLowerCase())) return setErr("Email sudah dipakai akun lain."); if (form.mode === "add" && !v.password) return setErr("Password awal wajib diisi (diberikan ke user)."); if (form.mode === "add" && !(v.password.length >= 8 && /[A-Za-z]/.test(v.password) && /[0-9]/.test(v.password))) return setErr("Password awal minimal 8 karakter dan memuat huruf serta angka."); onSave(form.mode, form.id, { name: v.name.trim(), email: v.email.trim().toLowerCase(), role: v.role, position: v.position, access: v.access, pass: v.password || undefined }); setErr(""); setForm(null); }; const savePosition = () => { const v = (posEdit.value || "").trim(); if (!v) return setPosErr("Nama jabatan wajib diisi."); if (positions.some((p, i) => p.toLowerCase() === v.toLowerCase() && i !== posEdit.index)) return setPosErr("Jabatan sudah ada."); setPositions((p) => (posEdit.mode === "add" ? [...p, v] : p.map((x, i) => (i === posEdit.index ? v : x)))); setPosErr(""); setPosEdit(null); }; const removePosition = (i) => { const name = positions[i]; if (users.some((u) => u.position === name)) return setPosErr(`Jabatan "${name}" masih dipakai user.`); setPositions((p) => p.filter((_, x) => x !== i)); setPosErr(""); }; const togglePage = (id) => setRoleEdit((r) => ({ ...r, pages: r.pages.includes(id) ? r.pages.filter((p) => p !== id) : [...r.pages, id] })); const saveRole = () => { const v = (roleEdit.name || "").trim(); if (!v) return setRoleErr("Nama role wajib diisi."); if (v.toLowerCase() === "direktur" || v.toLowerCase() === "admin") return setRoleErr("Nama itu dipakai sistem — pakai nama lain."); if (roles.some((r) => r.name.toLowerCase() === v.toLowerCase() && r.id !== roleEdit.id)) return setRoleErr("Role sudah ada."); if (roleEdit.pages.length === 0) return setRoleErr("Pilih minimal satu halaman."); setRoles((r) => (roleEdit.mode === "add" ? [...r, { id: v.toLowerCase().replace(/\s+/g, "-"), name: v, pages: roleEdit.pages }] : r.map((x) => (x.id === roleEdit.id ? { ...x, name: v, pages: roleEdit.pages } : x)))); setRoleErr(""); setRoleEdit(null); }; return (
setQ(e.target.value)} />
{users.filter((u) => u.active !== false).length} aktif {users.filter((u) => u.active === false).length} nonaktif

Kelola User

User tidak bisa mendaftar sendiri — semua akun dibuat di halaman ini.

{rows.length} akun
{rows.length === 0 ? ( Add User} /> ) : (
{ const boss = u.id === 0; const l = load[u.id] || { total: 0, open: 0, late: 0 }; const off = u.active === false; const roleName = boss ? "Direktur" : (roles.find((r) => r.id === (u.access || "user")) || roles[0]).name; return { who: ( {u.name} {u.email} · {u.role} ), pos: {u.position || "—"}, acc: {roleName}, load: boss ? — : ( 5 ? "var(--coral-600)" : "var(--text-strong)" }}>{l.open}/{l.total} {l.late > 0 && {l.late} lewat target} ), st: boss ? Aktif : off ? Nonaktif : (u.mustChange ? Wajib ganti password : Aktif), act: ( openEdit(u)} /> {!boss && setReset(u)} />} {!boss && onToggleActive(u)} />} {!boss && onDelete && setConfirm(u)} />} ), }; })} />
)}

Kelola Role & Akses

Tentukan halaman yang boleh dibuka tiap role. Maksimal setara role User.

{roles.map((r) => (
{r.name} {r.locked && role bawaan} {r.pages.length} halaman · {users.filter((u) => (u.access || "user") === r.id).length} user {GRANTABLE_PAGES.filter((p) => r.pages.includes(p.id)).map((p) => ( {p.label} ))} { setRoleErr(""); setRoleEdit({ mode: "edit", id: r.id, name: r.name, pages: [...r.pages], locked: r.locked }); }} /> {!r.locked && setRoles((x) => x.filter((y) => y.id !== r.id))} />}
))} Tidak bisa diberikan ke role apa pun: {ADMIN_ONLY_PAGES.join(" · ")} — khusus Direktur.

Kelola Jabatan

Jabatan ini yang muncul sebagai pilihan pada form user.

{positions.map((p, i) => (
{p} {users.filter((u) => u.position === p).length} user { setPosErr(""); setPosEdit({ mode: "edit", value: p, index: i }); }} /> removePosition(i)} />
))} {posErr && {posErr}}

Aturan Akun

Reset data — kosongkan seluruh user, task, dokumen, absen, dan log; sistem kembali seperti instalasi baru (hanya akun Direktur). Dipakai setelah masa uji coba.
{wipe && ( setWipe(false)} description="Seluruh user, task, dokumen, absen, notifikasi, dan log activity dihapus permanen. Hanya akun Direktur yang tersisa. Tindakan ini tidak bisa dibatalkan." footer={<> } /> )} {form && ( setForm(null)} title={form.mode === "add" ? "Add User" : "Edit User"} description={form.mode === "add" ? "Akun dibuat oleh Direktur. User wajib mengganti password saat login pertama." : "Perubahan tercatat di Log Activity."} footer={<> }>
setForm({ ...form, values: { ...form.values, name: e.target.value } })} /> setForm({ ...form, values: { ...form.values, email: e.target.value } })} />
setForm({ ...form, values: { ...form.values, access: e.target.value } })} options={roles.map((r) => ({ value: r.id, label: r.name }))} />
setForm({ ...form, values: { ...form.values, role: e.target.value } })} /> {form.mode === "add" && ( setForm({ ...form, values: { ...form.values, password: e.target.value } })} /> )}
{err && {err}}
)} {roleEdit && ( setRoleEdit(null)} title={roleEdit.mode === "edit" ? "Edit Role" : "Add Role"} description="Beri nama role, lalu pilih halaman yang boleh diakses." footer={<> }>
setRoleEdit({ ...roleEdit, name: e.target.value })} />
{roleEdit.pages.length} dari {GRANTABLE_PAGES.length} halaman {GRANTABLE_PAGES.map((p) => { const on = roleEdit.pages.includes(p.id); return ( ); })} {ADMIN_ONLY_PAGES.join(", ")} tidak tersedia — hak Direktur tidak bisa diberikan ke role lain.
{roleErr && {roleErr}}
)} {posEdit && ( setPosEdit(null)} title={posEdit.mode === "edit" ? "Edit Jabatan" : "Add Jabatan"} description="Jabatan dipakai sebagai pilihan pada form user." footer={<> }> setPosEdit({ ...posEdit, value: e.target.value })} /> )} {reset && ( setReset(null)} title="Reset Password" description={`Password ${reset.name} akan diganti password sementara.`} footer={<> }> User diminta membuat password baru saat login berikutnya. Sesi aktifnya diakhiri. )} {confirm && ( setConfirm(null)} title="Hapus Akun" description={`Akun ${confirm.name} (${confirm.email}) akan dihapus.`} footer={<> }> Task dan data absen miliknya ikut dihapus. Tindakan ini tercatat di Log Activity. )}
); } Object.assign(window, { UserManagement }); })();