import { useState, useEffect } from "react"; import { useParams, useNavigate } from "react-router-dom"; import api from "../api/client"; import { useAuth } from "../auth/AuthContext"; // ─── Default permission sets ─────────────────────────────────────────────── const EDITOR_PERMS = { melodies: { view: true, add: true, delete: true, safe_edit: true, full_edit: true, archetype_access: true, settings_access: true, compose_access: true }, devices: { view: true, add: true, delete: true, safe_edit: true, edit_bells: true, edit_clock: true, edit_warranty: true, full_edit: true, control: true }, app_users: { view: true, add: true, delete: true, safe_edit: true, full_edit: true }, issues_notes: { view: true, add: true, delete: true, edit: true }, mail: { view: true, compose: true, reply: true }, crm: { activity_log: true }, crm_customers: { full_access: true, overview: true, orders_view: true, orders_edit: true, quotations_view: true, quotations_edit: true, comms_view: true, comms_log: true, comms_edit: true, comms_compose: true, add: true, delete: true, files_view: true, files_edit: true, devices_view: true, devices_edit: true }, crm_products: { view: true, add: true, edit: true }, mfg: { view_inventory: true, edit: true, provision: true, firmware_view: true, firmware_edit: true }, api_reference: { access: true }, mqtt: { access: true }, }; const USER_PERMS = { melodies: { view: true, add: false, delete: false, safe_edit: false, full_edit: false, archetype_access: false, settings_access: false, compose_access: false }, devices: { view: true, add: false, delete: false, safe_edit: false, edit_bells: false, edit_clock: false, edit_warranty: false, full_edit: false, control: false }, app_users: { view: true, add: false, delete: false, safe_edit: false, full_edit: false }, issues_notes: { view: true, add: false, delete: false, edit: false }, mail: { view: true, compose: false, reply: false }, crm: { activity_log: false }, crm_customers: { full_access: false, overview: true, orders_view: true, orders_edit: false, quotations_view: true, quotations_edit: false, comms_view: true, comms_log: false, comms_edit: false, comms_compose: false, add: false, delete: false, files_view: true, files_edit: false, devices_view: true, devices_edit: false }, crm_products: { view: true, add: false, edit: false }, mfg: { view_inventory: true, edit: false, provision: false, firmware_view: true, firmware_edit: false }, api_reference: { access: false }, mqtt: { access: false }, }; function deepClone(obj) { return JSON.parse(JSON.stringify(obj)); } // ─── Dependency rules ────────────────────────────────────────────────────── function applyDependencies(section, key, value, prev) { const s = { ...prev[section] }; s[key] = value; if (value) { const VIEW_FORCING = ["add", "delete", "safe_edit", "full_edit", "edit_bells", "edit_clock", "edit_warranty", "control", "edit"]; if (VIEW_FORCING.includes(key) && "view" in s) s.view = true; if (section === "melodies" && key === "full_edit") s.safe_edit = true; if (section === "devices" && key === "full_edit") { s.safe_edit = true; s.edit_bells = true; s.edit_clock = true; s.edit_warranty = true; s.view = true; } if (section === "app_users" && key === "full_edit") s.safe_edit = true; if (section === "crm_customers") { if (key === "full_access") Object.keys(s).forEach((k) => { s[k] = true; }); if (key === "orders_edit") s.orders_view = true; if (key === "quotations_edit") s.quotations_view = true; if (key === "files_edit") s.files_view = true; if (key === "devices_edit") s.devices_view = true; if (["comms_log", "comms_edit", "comms_compose"].includes(key)) s.comms_view = true; } if (section === "mfg" && key === "firmware_edit") s.firmware_view = true; if (section === "mfg" && key === "edit") s.view_inventory = true; if (section === "mfg" && key === "provision") s.view_inventory = true; } return { ...prev, [section]: s }; } // ─── Pill button colors ──────────────────────────────────────────────────── // Disabled / No Access → danger red // View (middle) → badge blue // Enabled / Edit → accent green const PILL_STYLES = { off: { active: { bg: "var(--danger-bg)", border: "var(--danger)", color: "var(--danger-text)", fontWeight: 600 }, inactive: { bg: "var(--bg-input)", border: "var(--border-primary)", color: "var(--text-muted)", fontWeight: 400 }, }, view: { active: { bg: "var(--badge-blue-bg)", border: "var(--badge-blue-text)", color: "var(--badge-blue-text)", fontWeight: 600 }, inactive: { bg: "var(--bg-input)", border: "var(--border-primary)", color: "var(--text-muted)", fontWeight: 400 }, }, on: { active: { bg: "var(--success-bg)", border: "var(--accent)", color: "var(--success-text)", fontWeight: 600 }, inactive: { bg: "var(--bg-input)", border: "var(--border-primary)", color: "var(--text-muted)", fontWeight: 400 }, }, }; function pillStyle(tone, isActive) { const s = PILL_STYLES[tone][isActive ? "active" : "inactive"]; return { backgroundColor: s.bg, borderColor: s.border, color: s.color, fontWeight: s.fontWeight }; } // ─── Sub-components ──────────────────────────────────────────────────────── /** * A permission row: label/description on the left, dual pill buttons on the right. * Disabled / Enabled (red / green) */ function PermRow({ label, description, value, onChange, disabled }) { return (
{label} {description && (

{description}

)}
); } /** * 3-state row: No Access (red) | View (blue) | Edit (green) * value: "none" | "view" | "edit" */ function SegmentedRow({ label, description, value, onChange, disabled }) { return (
{label} {description && (

{description}

)}
); } /** Section card wrapper */ function PermSection({ title, children }) { return (

{title}

{children}
); } // ─── Main Component ──────────────────────────────────────────────────────── export default function StaffForm() { const { id } = useParams(); const navigate = useNavigate(); const { user } = useAuth(); const isEdit = !!id; const [form, setForm] = useState({ name: "", email: "", password: "", role: "user", is_active: true }); const [permissions, setPermissions] = useState(deepClone(USER_PERMS)); const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); const [error, setError] = useState(""); useEffect(() => { if (!isEdit) return; setLoading(true); api.get(`/staff/${id}`) .then((data) => { setForm({ name: data.name, email: data.email, password: "", role: data.role, is_active: data.is_active }); if (data.permissions) { const base = data.role === "editor" ? deepClone(EDITOR_PERMS) : deepClone(USER_PERMS); const merged = {}; Object.keys(base).forEach((sec) => { merged[sec] = { ...base[sec], ...(data.permissions[sec] || {}) }; }); setPermissions(merged); } else if (data.role === "editor") { setPermissions(deepClone(EDITOR_PERMS)); } else { setPermissions(deepClone(USER_PERMS)); } }) .catch((err) => setError(err.message)) .finally(() => setLoading(false)); }, [id]); const handleRoleChange = (newRole) => { setForm((f) => ({ ...f, role: newRole })); if (newRole === "editor") setPermissions(deepClone(EDITOR_PERMS)); else if (newRole === "user") setPermissions(deepClone(USER_PERMS)); }; const setPerm = (section, key, value) => setPermissions((prev) => applyDependencies(section, key, value, prev)); const setSegmented = (section, viewKey, editKey, val) => { setPermissions((prev) => { const next = { ...prev, [section]: { ...prev[section] } }; next[section][viewKey] = val !== "none"; next[section][editKey] = val === "edit"; return next; }); }; const segVal = (section, viewKey, editKey) => { const s = permissions[section] || {}; if (s[editKey]) return "edit"; if (s[viewKey]) return "view"; return "none"; }; const handleSubmit = async (e) => { e.preventDefault(); setError(""); setSaving(true); try { const body = { name: form.name, email: form.email, role: form.role }; if (isEdit) { body.is_active = form.is_active; body.permissions = (form.role === "editor" || form.role === "user") ? permissions : null; await api.put(`/staff/${id}`, body); navigate(`/settings/staff/${id}`); } else { body.password = form.password; if (form.role === "editor" || form.role === "user") body.permissions = permissions; const result = await api.post("/staff", body); navigate(`/settings/staff/${result.id}`); } } catch (err) { setError(err.message); } finally { setSaving(false); } }; if (loading) return
Loading...
; const roleOptions = user?.role === "sysadmin" ? ["sysadmin", "admin", "editor", "user"] : ["editor", "user"]; const inputClass = "w-full px-3 py-2 rounded-md text-sm border"; const inputStyle = { backgroundColor: "var(--bg-input)", color: "var(--text-primary)", borderColor: "var(--border-primary)" }; const showPerms = form.role === "editor" || form.role === "user"; const mel = permissions.melodies || {}; const dev = permissions.devices || {}; const usr = permissions.app_users || {}; const iss = permissions.issues_notes || {}; const mail = permissions.mail || {}; const crm = permissions.crm || {}; const cc = permissions.crm_customers || {}; const cprod = permissions.crm_products || {}; const mfg = permissions.mfg || {}; const apir = permissions.api_reference || {}; const mqtt = permissions.mqtt || {}; const ccLocked = !!cc.full_access; return (

{isEdit ? "Edit Staff Member" : "Add Staff Member"}

{error && (
{error}
)}
{/* ── Account Information ── */}

Account Information

setForm((f) => ({ ...f, name: e.target.value }))} required className={inputClass} style={inputStyle} />
setForm((f) => ({ ...f, email: e.target.value }))} required className={inputClass} style={inputStyle} />
{!isEdit && (
setForm((f) => ({ ...f, password: e.target.value }))} required minLength={6} className={inputClass} style={inputStyle} placeholder="Min 6 characters" />
)}
{isEdit && (
)}
{/* ── Admin / SysAdmin notice ── */} {(form.role === "sysadmin" || form.role === "admin") && (

Permissions

{form.role === "sysadmin" ? "SysAdmin has full god-mode access to all features and settings. No permission customization needed." : "Admin has full access to all features, except managing SysAdmin accounts and system-level settings. No permission customization needed."}

)} {/* ── Permission Sections ── */} {showPerms && ( <>
{/* Melodies */} setPerm("melodies", "view", v)} /> setPerm("melodies", "add", v)} /> setPerm("melodies", "delete", v)} /> setPerm("melodies", "safe_edit", v)} description="Name, Description, Tone, Type, Steps, Colour, Tags" disabled={mel.full_edit} /> setPerm("melodies", "full_edit", v)} description="Edit everything — enables Safe Edit automatically" /> setPerm("melodies", "archetype_access", v)} description="Access the Archetype Editor" /> setPerm("melodies", "settings_access", v)} description="View & change global melody settings" /> setPerm("melodies", "compose_access", v)} description="Use the Composer to create melodies" /> {/* Devices */} setPerm("devices", "view", v)} /> setPerm("devices", "add", v)} /> setPerm("devices", "delete", v)} /> setPerm("devices", "safe_edit", v)} description="Edit General Info tab only" disabled={dev.full_edit} /> setPerm("devices", "edit_bells", v)} description="Bell Mechanisms tab" disabled={dev.full_edit} /> setPerm("devices", "edit_clock", v)} description="Clock & Alerts tab" disabled={dev.full_edit} /> setPerm("devices", "edit_warranty", v)} description="Warranty and Subscription tab" disabled={dev.full_edit} /> setPerm("devices", "full_edit", v)} description="Enables all edit options above" /> setPerm("devices", "control", v)} description="Send commands via the Control tab" /> {/* App Users */} setPerm("app_users", "view", v)} /> setPerm("app_users", "add", v)} /> setPerm("app_users", "delete", v)} /> setPerm("app_users", "safe_edit", v)} description="Photo, Name, Email, Phone, Title only" disabled={usr.full_edit} /> setPerm("app_users", "full_edit", v)} description="Edit everything — enables Safe Edit automatically" /> {/* Issues & Notes */}

These permissions also apply to Notes linked from Device and User pages.

setPerm("issues_notes", "view", v)} /> setPerm("issues_notes", "add", v)} /> setPerm("issues_notes", "delete", v)} /> setPerm("issues_notes", "edit", v)} />
{/* Mail */} setPerm("mail", "view", v)} /> setPerm("mail", "compose", v)} description="Send new emails" /> setPerm("mail", "reply", v)} description="Reply to existing emails" /> {/* CRM */} setPerm("crm", "activity_log", v)} /> {/* CRM Products */} setPerm("crm_products", "view", v)} /> setPerm("crm_products", "add", v)} /> setPerm("crm_products", "edit", v)} /> {/* Manufacturing */} setPerm("mfg", "view_inventory", v)} /> setPerm("mfg", "edit", v)} description="Change device status, delete, download NVS Binary" /> setPerm("mfg", "provision", v)} description="Flash devices via the Provisioning page" /> setSegmented("mfg", "firmware_view", "firmware_edit", val)} />
{/* ── CRM Customers — full width ── */}

CRM Customers

{/* Full Access row */}
Full Access

Enables all CRM Customer permissions. When active, individual settings are locked.

{/* Two-column grid */}
{/* Left column */}
setPerm("crm_customers", "overview", v)} disabled={ccLocked} /> setSegmented("crm_customers", "orders_view", "orders_edit", val)} disabled={ccLocked} /> setSegmented("crm_customers", "quotations_view", "quotations_edit", val)} disabled={ccLocked} /> setSegmented("crm_customers", "files_view", "files_edit", val)} disabled={ccLocked} /> setSegmented("crm_customers", "devices_view", "devices_edit", val)} disabled={ccLocked} />
{/* Right column */}
{/* Add + Delete on one row */}
Customer
{/* Add */}
Add:
{/* Delete */}
Delete:
setPerm("crm_customers", "comms_view", v)} disabled={ccLocked} /> setPerm("crm_customers", "comms_log", v)} disabled={ccLocked} /> setPerm("crm_customers", "comms_edit", v)} disabled={ccLocked} /> setPerm("crm_customers", "comms_compose", v)} disabled={ccLocked} />
{/* ── API Reference + MQTT ── */}
setPerm("api_reference", "access", v)} /> setPerm("mqtt", "access", v)} description="Dashboard, Commands, Logs, WebSocket" />
)} {/* ── Submit ── */}
); }