Files
clube67_newwhats.local/clube67/newwhats.local/frontend/hooks/useSendingLogic.ts
T

187 lines
6.7 KiB
TypeScript

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<BroadcastJob[]>([])
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,
}
}