Files
motor-de-encuestas/app/admin/usuarios/invite-form.tsx
markosbenitez 7efab390da fix(admin): layout 3 columnas en invite form, banner de confirmación visible
Email/Organización/botón en una fila en desktop, apilado en mobile.
El mensaje de éxito/error ahora es un banner (no texto inline) visible
al menos 4s o hasta que el usuario vuelva a tocar el formulario.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 16:56:27 -03:00

90 lines
3.2 KiB
TypeScript

'use client'
import { useActionState, useEffect, useState } from 'react'
import { inviteUser, type InviteUserResult } from '@/app/actions/invite-user'
interface OrgOption {
id: string
name: string
}
const INITIAL_STATE: InviteUserResult = { success: false, error: '' }
const BANNER_VISIBLE_MS = 4000
const ERROR_MESSAGES: Record<string, string> = {
no_autenticado: 'Tu sesión expiró — volvé a iniciar sesión.',
forbidden: 'No tenés permisos para invitar usuarios.',
email_invalido: 'El email no es válido.',
organizacion_requerida: 'Elegí una organización.',
organizacion_no_encontrada: 'La organización seleccionada no existe.',
}
function describeError(code: string): string {
if (code.startsWith('invite_failed'))
return `No se pudo enviar la invitación: ${code.slice('invite_failed: '.length)}`
if (code.startsWith('role_insert_failed'))
return `Error interno al asignar el rol: ${code.slice('role_insert_failed: '.length)}`
return ERROR_MESSAGES[code] ?? 'Ocurrió un error inesperado.'
}
export function InviteForm({ organizations }: { organizations: OrgOption[] }) {
const [state, formAction, isPending] = useActionState(
async (_prev: InviteUserResult, formData: FormData) => inviteUser(formData),
INITIAL_STATE
)
const [bannerVisible, setBannerVisible] = useState(false)
// Banner visible al menos BANNER_VISIBLE_MS tras cada resultado nuevo, o
// hasta que el usuario vuelva a tocar el formulario (lo que pase primero).
useEffect(() => {
const hasMessage = state.success || (!state.success && state.error !== '')
setBannerVisible(hasMessage)
if (!hasMessage) return
const timer = setTimeout(() => setBannerVisible(false), BANNER_VISIBLE_MS)
return () => clearTimeout(timer)
}, [state])
return (
<form
action={formAction}
className="invite-form"
onChange={() => setBannerVisible(false)}
>
<div className="invite-form-row">
<div className="login-field">
<label htmlFor="email" className="login-label">Email</label>
<input
id="email"
name="email"
type="email"
required
disabled={isPending}
className="text-short-input"
/>
</div>
<div className="login-field">
<label htmlFor="orgId" className="login-label">Organización</label>
<select id="orgId" name="orgId" required disabled={isPending} className="admin-survey-select">
<option value="">Seleccioná una organización</option>
{organizations.map((org) => (
<option key={org.id} value={org.id}>{org.name}</option>
))}
</select>
</div>
<button type="submit" disabled={isPending} className="login-submit">
{isPending ? 'Enviando…' : 'Enviar invitación'}
</button>
</div>
{bannerVisible && !state.success && state.error && (
<p role="alert" className="invite-banner invite-banner-error">
{describeError(state.error)}
</p>
)}
{bannerVisible && state.success && (
<p role="status" className="invite-banner invite-banner-success">
Invitación enviada correctamente.
</p>
)}
</form>
)
}