'use client'; import { formatErrorBanner } from '@/lib/api/toast-error'; import { useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Switch } from '@/components/ui/switch'; import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetFooter } from '@/components/ui/sheet'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { UserPermissionMatrix } from './user-permission-matrix'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; import { PhoneInput, type PhoneInputValue } from '@/components/shared/phone-input'; import { apiFetch } from '@/lib/api/client'; import { formatRole, NON_ASSIGNABLE_ROLE_NAMES } from '@/lib/constants'; interface Role { id: string; name: string; } interface UserFormProps { open: boolean; onOpenChange: (open: boolean) => void; user?: { userId: string; displayName: string; fullName?: string | null; firstName?: string | null; lastName?: string | null; email: string; phone: string | null; isActive: boolean; role: { id: string; name: string }; residentialAccess?: boolean; } | null; onSuccess: () => void; } export function UserForm(props: UserFormProps) { return ( ); } function UserFormBody({ open, onOpenChange, user, onSuccess }: UserFormProps) { // Derive initial first/last names from the user payload. const initialNames = (() => { if (!user) return { first: '', last: '' }; if (user.firstName || user.lastName) { return { first: user.firstName ?? '', last: user.lastName ?? '' }; } const source = user.fullName ?? user.displayName; const parts = source.split(/\s+/); return { first: parts[0] ?? '', last: parts.slice(1).join(' ') }; })(); // useQuery replaces the prior useEffect(fetch+setRoles) pattern. const rolesQuery = useQuery<{ data: Role[] }>({ queryKey: ['admin', 'roles'], queryFn: () => apiFetch('/api/v1/admin/roles'), enabled: open, }); const roles = rolesQuery.data?.data ?? []; // Hide retired/owner-only system roles from the picker, but always keep the // role the user being edited already holds so their record stays editable. const selectableRoles = roles.filter( (r) => !NON_ASSIGNABLE_ROLE_NAMES.has(r.name) || r.id === user?.role.id, ); const [firstName, setFirstName] = useState(initialNames.first); const [lastName, setLastName] = useState(initialNames.last); const [email, setEmail] = useState(user?.email ?? ''); const [originalEmail] = useState(user?.email ?? ''); const [emailConfirmOpen, setEmailConfirmOpen] = useState(false); const [password, setPassword] = useState(''); // New users: email them a set-password link by default rather than typing a // password here. Toggle off to set one manually. const [sendSetupEmail, setSendSetupEmail] = useState(true); const [displayName, setDisplayName] = useState(user?.displayName ?? ''); const [phoneValue, setPhoneValue] = useState( user?.phone ? { e164: user.phone, country: 'US' } : null, ); const [roleId, setRoleId] = useState(user?.role.id ?? ''); const [isActive, setIsActive] = useState(user?.isActive ?? true); const [residentialAccess, setResidentialAccess] = useState(user?.residentialAccess ?? false); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const isEdit = !!user; const fullName = `${firstName} ${lastName}`.trim(); function handleSubmit(e: React.FormEvent) { e.preventDefault(); // Admin email change for an existing user goes through a confirmation // dialog because it locks the original sign-in identity out - the // submit path runs after the admin acknowledges. New-user creation // and same-email saves go straight through. if (isEdit && email.trim().toLowerCase() !== originalEmail.toLowerCase()) { setEmailConfirmOpen(true); return; } void persist(); } async function persist() { setError(null); setLoading(true); const phoneE164 = phoneValue?.e164 ?? null; try { if (isEdit) { const emailChanged = email.trim().toLowerCase() !== originalEmail.toLowerCase(); await apiFetch(`/api/v1/admin/users/${user.userId}`, { method: 'PATCH', body: { firstName: firstName || null, lastName: lastName || null, fullName: fullName || displayName, displayName, email: emailChanged ? email.trim() : undefined, phone: phoneE164, roleId, isActive, residentialAccess, notifyEmailChange: emailChanged ? true : undefined, }, }); } else { await apiFetch('/api/v1/admin/users', { method: 'POST', body: { name: fullName || displayName, firstName: firstName || null, lastName: lastName || null, email, // Email mode omits the password entirely; manual mode sends it. password: sendSetupEmail ? undefined : password, sendSetupEmail, displayName, phone: phoneE164 ?? undefined, roleId, residentialAccess, }, }); } onSuccess(); onOpenChange(false); } catch (err: unknown) { const message = formatErrorBanner(err); setError(message); } finally { setLoading(false); } } return ( {isEdit ? 'Edit User' : 'New User'} Profile & role Permissions {isEdit ? ( ) : (

Save the new user first, then return here to fine-tune their permissions.

)}
setFirstName(e.target.value)} placeholder="Jane" required />
setLastName(e.target.value)} placeholder="Doe" required />
setDisplayName(e.target.value)} placeholder={fullName || 'Jane Doe'} required />

How this user appears across the app - usually their full name, but they can pick a nickname.

setEmail(e.target.value)} placeholder="user@example.com" required /> {isEdit && email.trim().toLowerCase() !== originalEmail.toLowerCase() ? (

You'll be asked to confirm - the original address will receive an automated notice that you, the admin, changed their sign-in email.

) : isEdit ? (

Changing this address is an admin-only override; the user will be notified at the old address.

) : null}
{!isEdit && ( <>

The user gets an email to choose their own password. Turn off to set one here instead.

{!sendSetupEmail && (
setPassword(e.target.value)} placeholder="Min 12 characters" minLength={12} required />
)} )}

Grant this user access to residential clients and interests in addition to their primary role.

{isEdit && (

Disabled users cannot sign in.

)} {error &&

{error}

}
Change this user's sign-in email? You're about to change {originalEmail} to{' '} {email}. From now on, they must sign in with the new address. The original address will receive an automated notification explaining that an administrator made the change. Cancel { e.preventDefault(); setEmailConfirmOpen(false); void persist(); }} disabled={loading} > Confirm change
); }