mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 06:43:06 +00:00
Lista+detalhe única de edição para coordinator/candidate: nome, e-mail e celular com auto-save debounced (edit-where-you-see-it); carteira de municípios por chips com agregação de território de identidade, adição/remoção com UI otimista; criação inline no topo da tabela; ação de WhatsApp por assessor; reset de senha via e-mail (bloqueado para placeholders @planilha.invalid). Alinha access de update/phone/atribuição de município a isCampaignUnrestricted (coordinator + candidate). Sem migration. Testes int (campaignAdvisorManagement) + unit (schema e portfólio de municípios) cobrindo access, conflito de e-mail único, cap de assessores por município e agregação por território/ZE.
82 lines
2.5 KiB
TypeScript
82 lines
2.5 KiB
TypeScript
'use client'
|
|
|
|
import { KeyRoundIcon } from 'lucide-react'
|
|
import { useActionState, useEffect } from 'react'
|
|
import { toast } from 'sonner'
|
|
|
|
import { Button } from '@/components/ui/button'
|
|
import { Spinner } from '@/components/ui/Spinner'
|
|
import { PLACEHOLDER_RESET_MESSAGE } from '@/lib/schemas/advisor'
|
|
import type { CampaignFormActionState } from '@/utilities/campaignFormActionError'
|
|
|
|
type AdvisorPasswordResetButtonProps = {
|
|
advisorId: number
|
|
disabled: boolean
|
|
/** `icon` is the compact row action; `block` is the labelled button plus hint. */
|
|
layout?: 'block' | 'icon'
|
|
/** Names the advisor for screen readers in the `icon` layout. */
|
|
accessibleName?: string
|
|
formAction: (
|
|
state: CampaignFormActionState,
|
|
formData: FormData,
|
|
) => Promise<CampaignFormActionState>
|
|
}
|
|
|
|
export const AdvisorPasswordResetButton = ({
|
|
advisorId,
|
|
disabled,
|
|
layout = 'block',
|
|
accessibleName = 'Enviar link de senha',
|
|
formAction,
|
|
}: AdvisorPasswordResetButtonProps) => {
|
|
const [state, submitAction, isPending] = useActionState(formAction, {})
|
|
|
|
useEffect(() => {
|
|
if (state.status === 'success') {
|
|
toast.success(state.message ?? 'Link enviado.')
|
|
return
|
|
}
|
|
if (state.message && state.status !== 'success') {
|
|
toast.error(state.message)
|
|
}
|
|
}, [state.message, state.status])
|
|
|
|
if (layout === 'icon') {
|
|
return (
|
|
<form action={submitAction}>
|
|
<input type="hidden" name="advisorId" value={advisorId} />
|
|
<Button
|
|
type="submit"
|
|
variant="ghost"
|
|
size="icon"
|
|
className="size-10"
|
|
disabled={disabled || isPending}
|
|
aria-label={disabled ? `${accessibleName} — ${PLACEHOLDER_RESET_MESSAGE}` : accessibleName}
|
|
>
|
|
{isPending ? (
|
|
<Spinner className="size-4" aria-hidden="true" />
|
|
) : (
|
|
<KeyRoundIcon className="size-4" aria-hidden="true" />
|
|
)}
|
|
</Button>
|
|
</form>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<form action={submitAction} className="flex flex-col gap-2">
|
|
<input type="hidden" name="advisorId" value={advisorId} />
|
|
<Button type="submit" className="min-h-11 w-fit" disabled={disabled || isPending}>
|
|
{isPending ? <Spinner data-icon="inline-start" /> : null}
|
|
Enviar link de senha
|
|
</Button>
|
|
{disabled ? (
|
|
<p className="text-sm text-muted-foreground">{PLACEHOLDER_RESET_MESSAGE}</p>
|
|
) : (
|
|
<p className="text-sm text-muted-foreground">
|
|
Envia um e-mail com link para o assessor definir a senha de acesso.
|
|
</p>
|
|
)}
|
|
</form>
|
|
)
|
|
}
|