'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 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('ONCE') const [scheduledAt, setScheduledAt] = useState('') const [cronExpr, setCronExpr] = useState('0 9 * * 1-5') const [eventType, setEventType] = useState('BIRTHDAY') const [daysAfter, setDaysAfter] = useState('30') const [name, setName] = useState('') const [timezone] = useState('America/Sao_Paulo') // Step 2 — destinatários const [recipients, setRecipients] = useState([ { _key: uid(), jid: '', name: '', birthday: '', anniversary: '', signupDate: '' }, ]) // Step 3 — resultado const [saving, setSaving] = useState(false) const [error, setError] = useState(null) const [saved, setSaved] = useState(false) const [instanceId, setInstanceId] = useState('') const [instances, setInstances] = useState>([]) 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 = { 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( {isOpen && ( { if (e.target === e.currentTarget) onClose() }} > {/* Header */}

Agendar Mensagem

Tipo: {typeLabels[messageType]}

{/* Step indicator */}
{[1, 2, 3].map(s => ( {s < 3 && } ))} {step === 1 ? 'Configurar disparo' : step === 2 ? 'Destinatários' : 'Confirmar'}
{/* Content */}
{/* ── STEP 1: schedule config ─────────────────────────────── */} {step === 1 && (
setName(e.target.value)} />
{/* Schedule type selector */}
{[ { type: 'ONCE' as ScheduleType, label: 'Uma vez', icon: , color: '#0ea5e9' }, { type: 'RECURRING' as ScheduleType, label: 'Recorrente', icon: , color: '#8b5cf6' }, { type: 'EVENT' as ScheduleType, label: 'Evento', icon: , color: '#ef4444' }, ].map(opt => ( ))}
{/* ONCE — datetime */} {scheduleType === 'ONCE' && (
setScheduledAt(e.target.value)} />
)} {/* RECURRING — cron */} {scheduleType === 'RECURRING' && (
setCronExpr(e.target.value)} />

Presets:

{CRON_PRESETS.map(p => ( ))}
)} {/* EVENT — tipo */} {scheduleType === 'EVENT' && (
{[ { 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 => ( ))}
{eventType === 'SIGNUP_DAYS' && (
setDaysAfter(e.target.value)} />
)}
Variáveis disponíveis: {'{{name}}, {{birthday}}, {{anniversary}}'}
São substituídas automaticamente por contato no envio.
)}
)} {/* ── STEP 2: recipients ─────────────────────────────────── */} {step === 2 && (
Destinatários ({validRecipients.length} válido{validRecipients.length !== 1 ? 's' : ''})
{recipients.map((r) => (
updateRecipient(r._key, 'jid', e.target.value)} /> updateRecipient(r._key, 'name', e.target.value)} />
{scheduleType === 'EVENT' && (
{(eventType === 'BIRTHDAY' || eventType === 'ANNIVERSARY') && ( <> updateRecipient(r._key, eventType === 'BIRTHDAY' ? 'birthday' : 'anniversary', e.target.value)} /> )} {eventType === 'SIGNUP_DAYS' && ( updateRecipient(r._key, 'signupDate', e.target.value)} /> )}
)}
))}
{validRecipients.length === 0 && (
Adicione ao menos um destinatário com número válido.
)}
)} {/* ── STEP 3: confirm ─────────────────────────────────────── */} {step === 3 && (
{saved ? (

Agendamento criado!

O disparo será executado conforme configurado.

) : ( <>
Nome {name}
Tipo de mensagem {typeLabels[messageType]}
Disparo {scheduleType === 'ONCE' && `Uma vez — ${scheduledAt.replace('T', ' ')}`} {scheduleType === 'RECURRING' && `Recorrente — ${cronExpr}`} {scheduleType === 'EVENT' && `Evento: ${eventType}`}
Destinatários {validRecipients.length} contato{validRecipients.length !== 1 ? 's' : ''}
{error && (
{error}
)} )}
)}
{/* Footer */} {!saved && (
)}
)}
, document.body ) }