import { useState, useCallback, useEffect, useMemo } from 'react' import { useInstanceStore } from '../store/instanceStore' import { broadcastApi } from '../services/chatApiService' // ─── Types ──────────────────────────────────────────────────────────────────── export interface BroadcastJob { id: string status: 'RUNNING' | 'DONE' | 'FAILED' message: string totalCount: number sentCount: number failedCount: number createdAt: string finishedAt: string | null _count: { recipients: number } } // ─── Hook ───────────────────────────────────────────────────────────────────── export function useSendingLogic() { // Selectors granulares: cada um re-renderiza apenas quando o slice muda, // sem capturar o objeto inteiro do store (evita re-renders por mudanças em // outros slices como qrByInstance, loading, etc.) const instances = useInstanceStore((s) => s.instances) const activeInstanceId = useInstanceStore((s) => s.activeInstanceId) const setActiveInstance = useInstanceStore((s) => s.setActiveInstance) const loadInstances = useInstanceStore((s) => s.loadInstances) // ── Form state ──────────────────────────────────────────────────────────── const [numbersRaw, setNumbersRaw] = useState('') const [message, setMessage] = useState('') const [delayMs, setDelayMs] = useState(2000) // ── Jobs list ───────────────────────────────────────────────────────────── const [jobs, setJobs] = useState([]) const [jobsLoading, setJobsLoading] = useState(false) // ── Submission state ────────────────────────────────────────────────────── const [submitting, setSubmitting] = useState(false) const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' } | null>(null) const showToast = useCallback((message: string, type: 'success' | 'error') => { setToast({ message, type }) setTimeout(() => setToast(null), 3500) }, []) const connectedInstances = useMemo( () => instances.filter((i) => i.status === 'CONNECTED'), [instances], ) const activeInstance = useMemo( () => instances.find((i) => i.id === activeInstanceId) ?? null, [instances, activeInstanceId], ) const uniqueNumbers = useMemo(() => { const parsed = numbersRaw .split(/[\n,;]+/) .map((n) => n.trim().replace(/\D/g, '')) .filter((n) => n.length >= 5) return Array.from(new Set(parsed)) }, [numbersRaw]) // ── Load jobs ───────────────────────────────────────────────────────────── const loadJobs = useCallback(async () => { if (!activeInstanceId) return setJobsLoading(true) try { const data = await broadcastApi.list(activeInstanceId) setJobs(data as BroadcastJob[]) } catch { // silently ignore } finally { setJobsLoading(false) } }, [activeInstanceId]) useEffect(() => { loadInstances() }, []) useEffect(() => { if (activeInstanceId) loadJobs() }, [activeInstanceId, loadJobs]) // ── Poll running jobs ───────────────────────────────────────────────────── useEffect(() => { const hasRunning = jobs.some((j) => j.status === 'RUNNING') if (!hasRunning) return const id = setInterval(() => loadJobs(), 5000) return () => clearInterval(id) }, [jobs, loadJobs]) // ── Contact picker ──────────────────────────────────────────────────────── const [isPickerOpen, setIsPickerOpen] = useState(false) const addContactsToNumbers = useCallback((phones: string[]) => { if (phones.length === 0) return const cleaned = phones.map(p => p.replace(/\D/g, '')).filter(p => p.length >= 5) setNumbersRaw(prev => { const existing = prev.trim() const merged = existing ? `${existing}\n${cleaned.join('\n')}` : cleaned.join('\n') // Deduplica linhas const lines = Array.from(new Set(merged.split('\n').map(l => l.trim()).filter(Boolean))) return lines.join('\n') }) }, []) // ── Submit ──────────────────────────────────────────────────────────────── const handleSubmit = useCallback(async () => { if (!activeInstanceId) { showToast('Selecione uma instância conectada.', 'error') return } if (uniqueNumbers.length === 0) { showToast('Informe ao menos um número.', 'error') return } if (!message.trim()) { showToast('Informe a mensagem.', 'error') return } if (uniqueNumbers.length > 500) { showToast('Máximo de 500 números por disparo.', 'error') return } setSubmitting(true) try { const result = await broadcastApi.create(activeInstanceId, { numbers: uniqueNumbers, message: message.trim(), delayMs, }) showToast(`Disparo iniciado! Job #${result.jobId.slice(0, 8)} — ${result.total} números.`, 'success') setNumbersRaw('') setMessage('') await loadJobs() } catch (err: any) { const msg = err?.response?.data?.error ?? 'Erro ao iniciar disparo.' showToast(msg, 'error') } finally { setSubmitting(false) } }, [activeInstanceId, uniqueNumbers, message, delayMs, loadJobs, showToast]) const selectInstance = useCallback((id: string) => { setActiveInstance(id) }, [setActiveInstance]) return { // instances connectedInstances, activeInstance, activeInstanceId, selectInstance, // form numbersRaw, setNumbersRaw, message, setMessage, delayMs, setDelayMs, parsedNumbers: uniqueNumbers, // jobs jobs, jobsLoading, loadJobs, // submit submitting, handleSubmit, // feedback toast, // contact picker isPickerOpen, setIsPickerOpen, addContactsToNumbers, } }