'use client'; import { formatErrorBanner } from '@/lib/api/toast-error'; import { useState, useEffect } from 'react'; import Link from 'next/link'; 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 { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; import { PhoneInput, type PhoneInputValue } from '@/components/shared/phone-input'; import { useUIStore } from '@/stores/ui-store'; import { apiFetch } from '@/lib/api/client'; import { formatRole } 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({ open, onOpenChange, user, onSuccess }: UserFormProps) { const [roles, setRoles] = useState([]); const [firstName, setFirstName] = useState(''); const [lastName, setLastName] = useState(''); const [email, setEmail] = useState(''); const [originalEmail, setOriginalEmail] = useState(''); const [emailConfirmOpen, setEmailConfirmOpen] = useState(false); const [password, setPassword] = useState(''); const [displayName, setDisplayName] = useState(''); const [phoneValue, setPhoneValue] = useState(null); const [roleId, setRoleId] = useState(''); const [isActive, setIsActive] = useState(true); const [residentialAccess, setResidentialAccess] = useState(false); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const portSlug = useUIStore((s) => s.currentPortSlug); const isEdit = !!user; const fullName = `${firstName} ${lastName}`.trim(); useEffect(() => { if (open) { void apiFetch<{ data: Role[] }>('/api/v1/admin/roles').then((res) => setRoles(res.data)); } }, [open]); useEffect(() => { if (open) { if (user) { // Prefer canonical first/last from the API; fall back to a best- // effort split of displayName for older records that pre-date the // first_name/last_name columns. const first = user.firstName ?? ''; const last = user.lastName ?? ''; if (first || last) { setFirstName(first); setLastName(last); } else if (user.fullName) { const parts = user.fullName.split(/\s+/); setFirstName(parts[0] ?? ''); setLastName(parts.slice(1).join(' ')); } else { const parts = user.displayName.split(/\s+/); setFirstName(parts[0] ?? ''); setLastName(parts.slice(1).join(' ')); } setEmail(user.email); setOriginalEmail(user.email); setDisplayName(user.displayName); setPhoneValue(user.phone ? { e164: user.phone, country: 'US' } : null); setRoleId(user.role.id); setIsActive(user.isActive); setResidentialAccess(user.residentialAccess ?? false); setPassword(''); } else { setFirstName(''); setLastName(''); setEmail(''); setOriginalEmail(''); setDisplayName(''); setPhoneValue(null); setRoleId(''); setIsActive(true); setResidentialAccess(false); setPassword(''); } setError(null); } }, [open, user]); 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, password, 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'}
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 && (
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.

)} {isEdit && portSlug && (

Fine-tuned permissions

The selected role grants a baseline. To add or remove a specific permission for this user only, open the role & permissions page.

Manage permissions →
)} {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
); }