"use client"; import { useEffect, useState } from "react"; import { AdminUserRow, UpdateAdminUserInput } from "@/actions/admin/users"; import { formatDate } from "@/lib/format-date"; import CreateUserModal from "./CreateUserModal"; type UsersPageProps = { initialUsers: AdminUserRow[]; brands: { id: string; name: string }[]; currentUser: { id: string; role: string; can_manage_users?: boolean; }; onUserCreated?: (user: AdminUserRow) => void; }; type PasswordModal = { email: string; tempPassword: string | null; message: string | null; loading: boolean; error: string | null; }; const FLAG_LABELS: Record = { can_manage_products: "Products", can_manage_stops: "Stops", can_manage_orders: "Orders", can_manage_pickup: "Pickup", can_manage_messages: "Messages", can_manage_refunds: "Refunds", can_manage_users: "Users", can_manage_water_log: "Water Log", can_manage_reports: "Reports", }; const ALL_FLAGS = Object.keys(FLAG_LABELS); function RoleBadge({ role }: { role: string }) { const colors: Record = { platform_admin: "bg-purple-100 text-purple-700", brand_admin: "bg-blue-100 text-blue-700", store_employee: "bg-emerald-100 text-emerald-700", staff: "bg-stone-100 text-stone-600", }; return ( {role.replace("_", " ")} ); } type EditingUser = { id?: string; email?: string; password?: string; display_name?: string; phone_number?: string; role: "platform_admin" | "brand_admin" | "store_employee"; brand_id: string | null; flags: Record; active?: boolean; isNew: boolean; }; const defaultFlags: Record = { can_manage_products: false, can_manage_stops: false, can_manage_orders: false, can_manage_pickup: false, can_manage_messages: false, can_manage_refunds: false, can_manage_users: false, can_manage_water_log: false, can_manage_reports: false, }; function emptyEditing(isNew: boolean): EditingUser { return { isNew, role: "store_employee", brand_id: null, display_name: "", phone_number: "", password: "", flags: { ...defaultFlags }, }; } function editingFromRow(row: AdminUserRow): EditingUser { return { id: row.id, email: row.email, display_name: row.display_name ?? "", phone_number: row.phone_number ?? "", role: row.role as "platform_admin" | "brand_admin" | "store_employee", brand_id: row.brand_id, flags: { can_manage_products: row.can_manage_products, can_manage_stops: row.can_manage_stops, can_manage_orders: row.can_manage_orders, can_manage_pickup: row.can_manage_pickup, can_manage_messages: row.can_manage_messages, can_manage_refunds: row.can_manage_refunds, can_manage_users: row.can_manage_users, can_manage_water_log: row.can_manage_water_log, can_manage_reports: row.can_manage_reports, }, active: row.active, isNew: false, }; } export default function UsersPage({ initialUsers, brands, currentUser, onUserCreated }: UsersPageProps) { const [users, setUsers] = useState(initialUsers); const [panelOpen, setPanelOpen] = useState(false); const [showCreateModal, setShowCreateModal] = useState(false); const [editing, setEditing] = useState(() => emptyEditing(true)); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const [deleteConfirmId, setDeleteConfirmId] = useState(null); const [actionMenuId, setActionMenuId] = useState(null); const [passwordModal, setPasswordModal] = useState(null); const [resetLinkLoading, setResetLinkLoading] = useState(null); const [success, setSuccess] = useState(null); // Auto-dismiss the success banner after 6 seconds. useEffect(() => { if (!success) return; const t = setTimeout(() => setSuccess(null), 6000); return () => clearTimeout(t); }, [success]); const canManageUsers = currentUser.role === "platform_admin" || (currentUser.role === "brand_admin" && currentUser.can_manage_users); function openCreate() { setShowCreateModal(true); } function handleUserCreated(user: AdminUserRow) { setUsers((prev) => [user, ...prev]); onUserCreated?.(user); } function openEdit(row: AdminUserRow) { setEditing(editingFromRow(row)); setError(null); setPanelOpen(true); } function closePanel() { setPanelOpen(false); setEditing(emptyEditing(true)); setError(null); } function setRole(role: EditingUser["role"]) { setEditing((prev) => ({ ...prev, role })); } function setBrand(brand_id: string | null) { setEditing((prev) => ({ ...prev, brand_id })); } function toggleFlag(flag: string) { setEditing((prev) => ({ ...prev, flags: { ...prev.flags, [flag]: !prev.flags[flag] }, })); } function setActive(active: boolean) { setEditing((prev) => ({ ...prev, active })); } async function handleSave() { setSaving(true); setError(null); try { if (editing.isNew) { const res = await import("@/actions/admin/users").then((m) => m.createAdminUser({ email: editing.email!, password: editing.password!, display_name: editing.display_name || undefined, phone_number: editing.phone_number || undefined, role: editing.role, brand_id: editing.brand_id, flags: editing.flags, mustChangePassword: true, }) ); if (res.error) { if (res.error === "Not authenticated" || res.error.includes("session") || res.error.includes("authenticated")) { setError("Your session may have expired. Please log in again."); } else if (res.error.includes("permission") || res.error.includes("Insufficient permissions")) { setError("You do not have permission to edit this user."); } else { setError(res.error); } return; } if (res.user) { setUsers((prev) => [res.user!, ...prev]); // Surface the temp password — the slide-in panel has no // success view. The modal is the primary create flow, but // we don't want to silently lose the password here. if (res.tempPassword) { window.alert( `User created. Temporary password (share with the user — it will not be shown again):\n\n${res.tempPassword}`, ); } } } else { const res = await import("@/actions/admin/users").then((m) => m.updateAdminUser({ id: editing.id!, role: editing.role, brand_id: editing.brand_id, flags: editing.flags, active: editing.active, display_name: editing.display_name === "" ? null : editing.display_name || undefined, phone_number: editing.phone_number === "" ? null : editing.phone_number || undefined, }) ); if (res.error) { if (res.error === "Not authenticated" || res.error.includes("session") || res.error.includes("authenticated")) { setError("Your session may have expired. Please log in again."); } else if (res.error.includes("permission") || res.error.includes("Insufficient permissions")) { setError("You do not have permission to edit this user."); } else { setError(res.error); } return; } if (res.user) setUsers((prev) => prev.map((u) => (u.id === res.user!.id ? res.user! : u))); } closePanel(); } finally { setSaving(false); } } async function handleDelete(id: string) { const res = await import("@/actions/admin/users").then((m) => m.deleteAdminUser(id)); if (res.error) { setError(res.error); return; } setUsers((prev) => prev.filter((u) => u.id !== id)); setDeleteConfirmId(null); } async function handleResetPassword(email: string) { setPasswordModal({ email, tempPassword: null, message: null, loading: true, error: null }); try { const { resetAdminPassword } = await import("@/actions/admin/reset-admin"); const result = await resetAdminPassword(email); if (result.success) { if (result.method === "reset_link_sent") { setPasswordModal({ email, tempPassword: null, message: result.message, loading: false, error: null, }); } else { setPasswordModal({ email, tempPassword: result.tempPassword, message: null, loading: false, error: null, }); } } else { setPasswordModal({ email, tempPassword: null, message: null, loading: false, error: result.error ?? "Failed to reset password", }); } } catch (e: unknown) { setPasswordModal({ email, tempPassword: null, message: null, loading: false, error: e instanceof Error ? e.message : "Error", }); } } async function handleForcePasswordChange(id: string) { try { const { setMustChangePassword } = await import("@/actions/admin/users"); const res = await setMustChangePassword(id); if (res.error) throw new Error(res.error); setUsers((prev) => prev.map((u) => u.id === id ? { ...u, must_change_password: true } : u)); } catch (e: unknown) { setError(e instanceof Error ? e.message : "Failed to require password change"); } } async function handleSendResetLink(email: string) { setResetLinkLoading(email); setError(null); setSuccess(null); try { const { sendPasswordResetEmail } = await import("@/actions/admin/users"); const result = await sendPasswordResetEmail(email); setResetLinkLoading(null); if (result.error) throw new Error(result.error); setSuccess(`Password-reset email sent to ${email}.`); } catch (e: unknown) { setResetLinkLoading(null); setError(e instanceof Error ? e.message : "Failed to send reset email"); } } function copyTempPassword() { if (passwordModal?.tempPassword) { navigator.clipboard.writeText(passwordModal.tempPassword).catch(() => {}); } } const showBrandSelect = editing.role === "brand_admin" || editing.role === "store_employee"; const availableRoles: EditingUser["role"][] = currentUser.role === "platform_admin" ? ["platform_admin", "brand_admin", "store_employee"] : ["brand_admin", "store_employee"]; return ( <> {/* User list */}

{users.length} user{users.length !== 1 ? "s" : ""}

{canManageUsers && ( )}
{/* Success banner */} {success && (
✓ {success}
)} {/* Error banner */} {error && (
{error}
)}
{canManageUsers && {users.map((user) => ( {canManageUsers && ( )} ))} {users.length === 0 && ( )}
Name Phone Role Brand Status Created}
{user.display_name || user.email}
{user.display_name && user.display_name !== "No Name" && (
{user.email}
)}
{user.phone_number ?? "—"} {user.brand_name ?? "—"} {user.active ? "Active" : "Inactive"} {formatDate(new Date(user.created_at))}
{actionMenuId === user.id && ( <>
setActionMenuId(null)} />
Actions
{user.id !== currentUser.id && ( )}
)}
No users found.
{/* Delete confirm modal */} {deleteConfirmId && (

Delete user?

This will remove their admin access. Their auth account will remain active.

)} {/* Slide-in panel */} {panelOpen && (
e.stopPropagation()} >

{editing.isNew ? "Create User" : "Edit User"}

{/* Saving overlay */} {saving && (

Saving…

)}
{/* Email (create only) */} {editing.isNew && (
setEditing((p) => ({ ...p, email: e.target.value }))} className="mt-1 w-full rounded-lg border border-stone-200 bg-white px-3 py-2.5 text-sm text-stone-900 placeholder:text-stone-400 focus:border-emerald-500 focus:outline-none focus:ring-1 focus:ring-emerald-500" placeholder="user@example.com" />
)} {/* Password (create only) */} {editing.isNew && (

Minimum 6 characters.

setEditing((p) => ({ ...p, password: e.target.value }))} className="w-full rounded-lg border border-stone-200 bg-white px-3 py-2.5 text-sm text-stone-900 placeholder:text-stone-400 focus:border-emerald-500 focus:outline-none focus:ring-1 focus:ring-emerald-500" placeholder="Choose a password" minLength={6} />
)} {/* Display Name */}

Shown in the admin user list.

setEditing((p) => ({ ...p, display_name: e.target.value }))} className="w-full rounded-lg border border-stone-200 bg-white px-3 py-2.5 text-sm text-stone-900 placeholder:text-stone-400 focus:border-emerald-500 focus:outline-none focus:ring-1 focus:ring-emerald-500" placeholder="Kyle Martinez" />
{/* Phone Number */}

Optional.

setEditing((p) => ({ ...p, phone_number: e.target.value }))} className="w-full rounded-lg border border-stone-200 bg-white px-3 py-2.5 text-sm text-stone-900 placeholder:text-stone-400 focus:border-emerald-500 focus:outline-none focus:ring-1 focus:ring-emerald-500" placeholder="+1 (555) 000-0000" />
{/* Role */}
{availableRoles.map((r) => ( ))}
{/* Brand */} {showBrandSelect && (
)} {/* Active toggle (edit only) */} {!editing.isNew && (

Active

Deactivated users cannot log in

)} {/* Permissions */}
{ALL_FLAGS.map((flag) => ( ))}
)} {/* Password reset modal */} {passwordModal && (

Reset Password

{passwordModal.loading ? (
Resetting password for {passwordModal.email}...
) : passwordModal.error ? (

{passwordModal.error}

) : passwordModal.tempPassword ? (

Temporary password for {passwordModal.email}:

{passwordModal.tempPassword}

Share this password securely with the user. They will be required to change it on next login.

) : passwordModal.message ? (

{passwordModal.message}

) : null}
)} {/* Create User Modal */} setShowCreateModal(false)} onSuccess={handleUserCreated} brands={brands} currentUser={{ role: currentUser.role, }} /> ); }