3042ddca38
- /wa-inbox, /wa-sessions e /wa-secretaria rodam sem a sidebar do scoreodonto (isStandaloneView); cada tela tem seu "Voltar" (Secretária ganhou botão na ThinNav). - SessionsView: banner "Você já possui acesso ao WhatsApp" quando há sessão ativa. - avatares: usa a URL do CDN (contactAvatarUrl/instance.avatar) direto; Avatar exibe sem depender de version; self-heal no onError re-busca a foto atual. - deps: framer-motion, immer. Dev override com HMR (docker-compose.dev.yml). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
507 lines
25 KiB
TypeScript
507 lines
25 KiB
TypeScript
'use client';
|
|
|
|
/**
|
|
* ScheduleMessageModal — modal completo de agendamentos de mensagem.
|
|
*
|
|
* Modos:
|
|
* ONCE — data/hora específica (datetime-local)
|
|
* RECURRING — expressão cron com presets visuais
|
|
* EVENT — BIRTHDAY / ANNIVERSARY / SIGNUP_DAYS
|
|
* vinculado a dados de recipients (birthday, anniversary, signupDate)
|
|
*
|
|
* Suporte a variáveis de personalização: {{name}}, {{birthday}}, etc.
|
|
* Destinatários: lista manual de JIDs com nome e dados de evento.
|
|
* Variáveis substituídas em runtime pelo SchedulerService.
|
|
*/
|
|
|
|
import React, { useState, useEffect } from 'react'
|
|
import { createPortal } from 'react-dom'
|
|
import { motion, AnimatePresence } from 'framer-motion'
|
|
import {
|
|
X, Calendar, RotateCw, Gift, Plus, Trash2, Clock,
|
|
CheckCircle2, AlertTriangle, ChevronRight, Users,
|
|
} from 'lucide-react'
|
|
import {
|
|
scheduleApi, instanceApi,
|
|
type ScheduleType, type EventType, type ScheduleRecipient, type TemplateType,
|
|
} from '../services/chatApiService'
|
|
|
|
// ─── CRON presets ─────────────────────────────────────────────────────────────
|
|
|
|
const CRON_PRESETS = [
|
|
{ label: 'Diariamente às 8h', expr: '0 8 * * *' },
|
|
{ label: 'Seg a Sex às 9h', expr: '0 9 * * 1-5' },
|
|
{ label: 'Toda segunda às 10h', expr: '0 10 * * 1' },
|
|
{ label: 'Quinzenal (dia 1/15)', expr: '0 9 1,15 * *' },
|
|
{ label: 'Mensal (dia 1)', expr: '0 9 1 * *' },
|
|
{ label: 'Semanal domingo 11h', expr: '0 11 * * 0' },
|
|
]
|
|
|
|
// ─── Tipos locais ─────────────────────────────────────────────────────────────
|
|
|
|
interface RecipientRow extends ScheduleRecipient {
|
|
_key: string
|
|
}
|
|
|
|
interface Props {
|
|
isOpen: boolean
|
|
onClose: () => void
|
|
payload: Record<string, unknown>
|
|
messageType: TemplateType
|
|
onSaved?: () => void
|
|
}
|
|
|
|
function uid() { return `k_${Date.now()}_${Math.random().toString(36).slice(2, 6)}` }
|
|
|
|
// ─── Component ────────────────────────────────────────────────────────────────
|
|
|
|
export default function ScheduleMessageModal({ isOpen, onClose, payload, messageType, onSaved }: Props) {
|
|
const [step, setStep] = useState<1 | 2 | 3>(1) // 1=tipo, 2=destinatários, 3=confirmação
|
|
|
|
// Step 1 — configuração de schedule
|
|
const [scheduleType, setScheduleType] = useState<ScheduleType>('ONCE')
|
|
const [scheduledAt, setScheduledAt] = useState('')
|
|
const [cronExpr, setCronExpr] = useState('0 9 * * 1-5')
|
|
const [eventType, setEventType] = useState<EventType>('BIRTHDAY')
|
|
const [daysAfter, setDaysAfter] = useState('30')
|
|
const [name, setName] = useState('')
|
|
const [timezone] = useState('America/Sao_Paulo')
|
|
|
|
// Step 2 — destinatários
|
|
const [recipients, setRecipients] = useState<RecipientRow[]>([
|
|
{ _key: uid(), jid: '', name: '', birthday: '', anniversary: '', signupDate: '' },
|
|
])
|
|
|
|
// Step 3 — resultado
|
|
const [saving, setSaving] = useState(false)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [saved, setSaved] = useState(false)
|
|
const [instanceId, setInstanceId] = useState('')
|
|
const [instances, setInstances] = useState<Array<{ id: string; name: string; status: string }>>([])
|
|
|
|
useEffect(() => {
|
|
if (isOpen) {
|
|
instanceApi.list().then((list: any[]) => setInstances(list))
|
|
}
|
|
}, [isOpen])
|
|
|
|
const addRecipient = () => {
|
|
setRecipients(prev => [...prev, { _key: uid(), jid: '', name: '', birthday: '', anniversary: '', signupDate: '' }])
|
|
}
|
|
const removeRecipient = (key: string) => {
|
|
setRecipients(prev => prev.filter(r => r._key !== key))
|
|
}
|
|
const updateRecipient = (key: string, field: keyof ScheduleRecipient, val: string) => {
|
|
setRecipients(prev => prev.map(r => r._key === key ? { ...r, [field]: val } : r))
|
|
}
|
|
|
|
const isStep1Valid = () => {
|
|
if (!name.trim() || !instanceId) return false
|
|
if (scheduleType === 'ONCE' && !scheduledAt) return false
|
|
if (scheduleType === 'RECURRING' && !cronExpr.trim()) return false
|
|
return true
|
|
}
|
|
|
|
const validRecipients = recipients.filter(r => r.jid.trim())
|
|
|
|
const buildScheduleData = () => ({
|
|
instanceId,
|
|
name: name.trim(),
|
|
payload,
|
|
recipients: validRecipients.map(({ _key, ...r }) => ({
|
|
...r,
|
|
jid: r.jid.includes('@') ? r.jid : `${r.jid.replace(/\D/g, '')}@s.whatsapp.net`,
|
|
})),
|
|
scheduleType,
|
|
scheduledAt: scheduleType === 'ONCE' ? scheduledAt : undefined,
|
|
cronExpr: scheduleType === 'RECURRING' ? cronExpr
|
|
: scheduleType === 'EVENT' && eventType === 'SIGNUP_DAYS' ? daysAfter
|
|
: undefined,
|
|
eventType: scheduleType === 'EVENT' ? eventType : undefined,
|
|
timezone,
|
|
})
|
|
|
|
const handleSave = async () => {
|
|
setSaving(true)
|
|
setError(null)
|
|
try {
|
|
await scheduleApi.create(buildScheduleData())
|
|
setSaved(true)
|
|
onSaved?.()
|
|
setTimeout(() => { setSaved(false); onClose(); setStep(1) }, 2000)
|
|
} catch (e: any) {
|
|
setError(e?.response?.data?.error ?? e?.message ?? 'Erro ao salvar')
|
|
} finally {
|
|
setSaving(false)
|
|
}
|
|
}
|
|
|
|
// Rótulo do tipo de mensagem
|
|
const typeLabels: Record<TemplateType, string> = {
|
|
TEXT: 'Texto', BUTTONS: 'Botões', INTERACTIVE: 'CTA', LIST: 'Lista', POLL: 'Enquete', CAROUSEL: 'Carrossel',
|
|
}
|
|
|
|
const minDateTime = new Date(Date.now() + 60_000).toISOString().slice(0, 16)
|
|
|
|
const [mounted, setMounted] = useState(false)
|
|
useEffect(() => { setMounted(true) }, [])
|
|
if (!mounted) return null
|
|
|
|
return createPortal(
|
|
<AnimatePresence>
|
|
{isOpen && (
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-[60] flex items-center justify-center p-4"
|
|
onClick={(e) => { if (e.target === e.currentTarget) onClose() }}
|
|
>
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 30, scale: 0.97 }}
|
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
exit={{ opacity: 0, y: 30, scale: 0.97 }}
|
|
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
|
|
className="bg-white rounded-2xl shadow-2xl w-full max-w-lg max-h-[90vh] flex flex-col overflow-hidden"
|
|
>
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between px-5 py-4 border-b border-gray-100 shrink-0">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-9 h-9 rounded-xl bg-[#fef3c7] flex items-center justify-center">
|
|
<Calendar className="w-5 h-5 text-[#f59e0b]" />
|
|
</div>
|
|
<div>
|
|
<h2 className="text-base font-semibold text-[#111b21]">Agendar Mensagem</h2>
|
|
<p className="text-xs text-[#667781]">Tipo: {typeLabels[messageType]}</p>
|
|
</div>
|
|
</div>
|
|
<button onClick={onClose} className="p-2 rounded-full hover:bg-gray-100 text-gray-400 transition-colors">
|
|
<X className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Step indicator */}
|
|
<div className="px-5 py-3 flex items-center gap-2 shrink-0 border-b border-gray-100">
|
|
{[1, 2, 3].map(s => (
|
|
<React.Fragment key={s}>
|
|
<button
|
|
onClick={() => s < step ? setStep(s as any) : undefined}
|
|
className={`w-7 h-7 rounded-full text-xs font-bold flex items-center justify-center transition-all ${
|
|
step === s ? 'bg-[#f59e0b] text-white shadow-md scale-105' :
|
|
step > s ? 'bg-[#10b981] text-white' : 'bg-gray-100 text-gray-400'
|
|
}`}
|
|
>
|
|
{step > s ? <CheckCircle2 className="w-4 h-4" /> : s}
|
|
</button>
|
|
{s < 3 && <ChevronRight className="w-4 h-4 text-gray-300" />}
|
|
</React.Fragment>
|
|
))}
|
|
<span className="ml-2 text-xs text-[#667781]">
|
|
{step === 1 ? 'Configurar disparo' : step === 2 ? 'Destinatários' : 'Confirmar'}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="flex-1 overflow-y-auto px-5 py-4">
|
|
|
|
{/* ── STEP 1: schedule config ─────────────────────────────── */}
|
|
{step === 1 && (
|
|
<div className="space-y-4">
|
|
<div className="space-y-1">
|
|
<label className="block text-xs font-semibold text-[#667781] uppercase tracking-wider">Nome do agendamento</label>
|
|
<input
|
|
className="w-full rounded-xl border border-gray-200 bg-[#f8f9fa] px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-[#f59e0b]/30"
|
|
placeholder="Ex: Aniversários de Abril"
|
|
value={name}
|
|
onChange={e => setName(e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<label className="block text-xs font-semibold text-[#667781] uppercase tracking-wider">Instância WhatsApp</label>
|
|
<select
|
|
className="w-full rounded-xl border border-gray-200 bg-[#f8f9fa] px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-[#f59e0b]/30"
|
|
value={instanceId}
|
|
onChange={e => setInstanceId(e.target.value)}
|
|
>
|
|
<option value="">Selecionar instância...</option>
|
|
{instances.map((inst: any) => (
|
|
<option key={inst.id} value={inst.id} disabled={inst.status !== 'CONNECTED'}>
|
|
{inst.name} {inst.status !== 'CONNECTED' ? '(desconectada)' : ''}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{/* Schedule type selector */}
|
|
<div className="space-y-2">
|
|
<label className="block text-xs font-semibold text-[#667781] uppercase tracking-wider">Tipo de agendamento</label>
|
|
<div className="grid grid-cols-3 gap-2">
|
|
{[
|
|
{ type: 'ONCE' as ScheduleType, label: 'Uma vez', icon: <Clock className="w-4 h-4" />, color: '#0ea5e9' },
|
|
{ type: 'RECURRING' as ScheduleType, label: 'Recorrente', icon: <RotateCw className="w-4 h-4" />, color: '#8b5cf6' },
|
|
{ type: 'EVENT' as ScheduleType, label: 'Evento', icon: <Gift className="w-4 h-4" />, color: '#ef4444' },
|
|
].map(opt => (
|
|
<button
|
|
key={opt.type}
|
|
onClick={() => setScheduleType(opt.type)}
|
|
className={`flex flex-col items-center gap-1.5 p-3 rounded-xl border-2 transition-all text-sm font-semibold ${
|
|
scheduleType === opt.type
|
|
? 'border-current shadow-sm'
|
|
: 'border-gray-200 text-gray-400 hover:border-gray-300'
|
|
}`}
|
|
style={scheduleType === opt.type ? { borderColor: opt.color, color: opt.color, backgroundColor: opt.color + '10' } : {}}
|
|
>
|
|
{opt.icon}
|
|
{opt.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* ONCE — datetime */}
|
|
{scheduleType === 'ONCE' && (
|
|
<div className="space-y-1">
|
|
<label className="block text-xs font-semibold text-[#667781] uppercase tracking-wider">Data e hora</label>
|
|
<input
|
|
type="datetime-local"
|
|
min={minDateTime}
|
|
className="w-full rounded-xl border border-gray-200 bg-[#f8f9fa] px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-[#0ea5e9]/30"
|
|
value={scheduledAt}
|
|
onChange={e => setScheduledAt(e.target.value)}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* RECURRING — cron */}
|
|
{scheduleType === 'RECURRING' && (
|
|
<div className="space-y-3">
|
|
<div className="space-y-1">
|
|
<label className="block text-xs font-semibold text-[#667781] uppercase tracking-wider">Expressão Cron</label>
|
|
<input
|
|
className="w-full rounded-xl border border-gray-200 bg-[#f8f9fa] px-3 py-2.5 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-[#8b5cf6]/30"
|
|
placeholder="0 9 * * 1-5"
|
|
value={cronExpr}
|
|
onChange={e => setCronExpr(e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<p className="text-xs text-[#667781] font-medium">Presets:</p>
|
|
<div className="flex flex-wrap gap-1.5">
|
|
{CRON_PRESETS.map(p => (
|
|
<button
|
|
key={p.expr}
|
|
onClick={() => setCronExpr(p.expr)}
|
|
className={`px-2.5 py-1 rounded-lg text-xs font-medium transition-all ${
|
|
cronExpr === p.expr
|
|
? 'bg-[#8b5cf6] text-white'
|
|
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
|
}`}
|
|
>
|
|
{p.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* EVENT — tipo */}
|
|
{scheduleType === 'EVENT' && (
|
|
<div className="space-y-3">
|
|
<div className="space-y-1">
|
|
<label className="block text-xs font-semibold text-[#667781] uppercase tracking-wider">Tipo de evento</label>
|
|
<div className="grid grid-cols-3 gap-2">
|
|
{[
|
|
{ type: 'BIRTHDAY' as EventType, label: '🎂 Aniversário' },
|
|
{ type: 'ANNIVERSARY' as EventType, label: '💍 Aniversariante' },
|
|
{ type: 'SIGNUP_DAYS' as EventType, label: '📅 X dias após cadastro' },
|
|
].map(opt => (
|
|
<button
|
|
key={opt.type}
|
|
onClick={() => setEventType(opt.type)}
|
|
className={`py-2.5 px-2 rounded-xl border-2 text-xs font-semibold transition-all ${
|
|
eventType === opt.type
|
|
? 'border-[#ef4444] bg-[#fee2e2] text-[#ef4444]'
|
|
: 'border-gray-200 text-gray-500 hover:border-gray-300'
|
|
}`}
|
|
>
|
|
{opt.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
{eventType === 'SIGNUP_DAYS' && (
|
|
<div className="flex items-center gap-3">
|
|
<label className="text-xs font-semibold text-[#667781] uppercase tracking-wider whitespace-nowrap">Dias após cadastro</label>
|
|
<input
|
|
type="number" min="1" max="365"
|
|
className="w-24 rounded-xl border border-gray-200 bg-[#f8f9fa] px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[#ef4444]/30"
|
|
value={daysAfter}
|
|
onChange={e => setDaysAfter(e.target.value)}
|
|
/>
|
|
</div>
|
|
)}
|
|
<div className="rounded-xl bg-amber-50 border border-amber-200 px-3 py-2.5 text-xs text-amber-700">
|
|
<strong>Variáveis disponíveis:</strong> {'{{name}}, {{birthday}}, {{anniversary}}'}<br />
|
|
São substituídas automaticamente por contato no envio.
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* ── STEP 2: recipients ─────────────────────────────────── */}
|
|
{step === 2 && (
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<Users className="w-4 h-4 text-[#667781]" />
|
|
<span className="text-sm font-semibold text-[#111b21]">Destinatários</span>
|
|
<span className="text-xs text-[#667781]">({validRecipients.length} válido{validRecipients.length !== 1 ? 's' : ''})</span>
|
|
</div>
|
|
<button onClick={addRecipient} className="flex items-center gap-1 text-xs text-[#00a884] font-medium hover:underline">
|
|
<Plus className="w-3.5 h-3.5" /> Adicionar
|
|
</button>
|
|
</div>
|
|
|
|
<div className="space-y-3 max-h-[380px] overflow-y-auto pr-1">
|
|
{recipients.map((r) => (
|
|
<div key={r._key} className="rounded-xl border border-gray-200 bg-[#f8f9fa] p-3 space-y-2">
|
|
<div className="flex gap-2 items-start">
|
|
<input
|
|
className="flex-[2] rounded-lg border border-gray-200 bg-white px-2 py-1.5 text-xs focus:outline-none focus:ring-1 focus:ring-[#00a884]"
|
|
placeholder="Número ou JID ex: 5511999999999"
|
|
value={r.jid}
|
|
onChange={e => updateRecipient(r._key, 'jid', e.target.value)}
|
|
/>
|
|
<input
|
|
className="flex-1 rounded-lg border border-gray-200 bg-white px-2 py-1.5 text-xs focus:outline-none focus:ring-1 focus:ring-[#00a884]"
|
|
placeholder="Nome ({{name}})"
|
|
value={r.name ?? ''}
|
|
onChange={e => updateRecipient(r._key, 'name', e.target.value)}
|
|
/>
|
|
<button onClick={() => removeRecipient(r._key)} disabled={recipients.length <= 1}
|
|
className="p-1.5 rounded-full hover:bg-red-50 text-gray-400 hover:text-red-500 disabled:opacity-30 transition-colors">
|
|
<Trash2 className="w-3.5 h-3.5" />
|
|
</button>
|
|
</div>
|
|
|
|
{scheduleType === 'EVENT' && (
|
|
<div className="grid grid-cols-3 gap-1.5">
|
|
{(eventType === 'BIRTHDAY' || eventType === 'ANNIVERSARY') && (
|
|
<>
|
|
<input
|
|
type="date"
|
|
className="rounded-lg border border-gray-200 bg-white px-2 py-1 text-[11px] focus:outline-none"
|
|
placeholder="Aniversário"
|
|
value={eventType === 'BIRTHDAY' ? (r.birthday ?? '') : (r.anniversary ?? '')}
|
|
onChange={e => updateRecipient(r._key, eventType === 'BIRTHDAY' ? 'birthday' : 'anniversary', e.target.value)}
|
|
/>
|
|
</>
|
|
)}
|
|
{eventType === 'SIGNUP_DAYS' && (
|
|
<input
|
|
type="date"
|
|
className="col-span-2 rounded-lg border border-gray-200 bg-white px-2 py-1 text-[11px] focus:outline-none"
|
|
placeholder="Data de cadastro"
|
|
value={r.signupDate ?? ''}
|
|
onChange={e => updateRecipient(r._key, 'signupDate', e.target.value)}
|
|
/>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{validRecipients.length === 0 && (
|
|
<div className="flex items-center gap-2 text-xs text-amber-600 bg-amber-50 border border-amber-200 rounded-xl px-3 py-2">
|
|
<AlertTriangle className="w-4 h-4 shrink-0" />
|
|
Adicione ao menos um destinatário com número válido.
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* ── STEP 3: confirm ─────────────────────────────────────── */}
|
|
{step === 3 && (
|
|
<div className="space-y-4">
|
|
{saved ? (
|
|
<div className="flex flex-col items-center justify-center py-10 gap-3">
|
|
<CheckCircle2 className="w-14 h-14 text-[#10b981]" />
|
|
<p className="text-base font-semibold text-[#111b21]">Agendamento criado!</p>
|
|
<p className="text-xs text-[#667781] text-center">O disparo será executado conforme configurado.</p>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div className="rounded-xl bg-[#f8f9fa] border border-gray-200 p-4 space-y-3 text-sm">
|
|
<div className="flex justify-between">
|
|
<span className="text-[#667781]">Nome</span>
|
|
<span className="font-medium text-[#111b21]">{name}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-[#667781]">Tipo de mensagem</span>
|
|
<span className="font-medium text-[#111b21]">{typeLabels[messageType]}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-[#667781]">Disparo</span>
|
|
<span className="font-medium text-[#111b21]">
|
|
{scheduleType === 'ONCE' && `Uma vez — ${scheduledAt.replace('T', ' ')}`}
|
|
{scheduleType === 'RECURRING' && `Recorrente — ${cronExpr}`}
|
|
{scheduleType === 'EVENT' && `Evento: ${eventType}`}
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-[#667781]">Destinatários</span>
|
|
<span className="font-medium text-[#111b21]">{validRecipients.length} contato{validRecipients.length !== 1 ? 's' : ''}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="flex items-center gap-2 text-xs text-red-600 bg-red-50 border border-red-200 rounded-xl px-3 py-2">
|
|
<AlertTriangle className="w-4 h-4 shrink-0" />
|
|
{error}
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
{!saved && (
|
|
<div className="px-5 py-3 border-t border-gray-100 flex items-center justify-between shrink-0">
|
|
<button
|
|
onClick={() => step > 1 ? setStep(prev => (prev - 1) as any) : onClose()}
|
|
className="px-4 py-2 rounded-xl text-sm font-medium text-[#667781] hover:bg-gray-100 transition-colors"
|
|
>
|
|
{step > 1 ? 'Voltar' : 'Cancelar'}
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
if (step === 1) setStep(2)
|
|
else if (step === 2) setStep(3)
|
|
else handleSave()
|
|
}}
|
|
disabled={
|
|
(step === 1 && !isStep1Valid()) ||
|
|
(step === 2 && validRecipients.length === 0) ||
|
|
saving
|
|
}
|
|
className="flex items-center gap-2 px-5 py-2 rounded-xl text-sm font-semibold text-white bg-[#f59e0b] hover:bg-[#d97706] disabled:opacity-50 transition-all active:scale-95"
|
|
>
|
|
{saving && <span className="w-4 h-4 border-2 border-white/40 border-t-white rounded-full animate-spin" />}
|
|
{step < 3 ? 'Continuar' : 'Confirmar e Salvar'}
|
|
</button>
|
|
</div>
|
|
)}
|
|
</motion.div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>,
|
|
document.body
|
|
)
|
|
}
|