Files
clube67_newwhats.local/clube67/newwhats.local/frontend/hooks/usePhonebookLogic.ts
T
VPS 4 Deploy Agent 2f8c04a0a7
continuous-integration/webhook Falha no deploy de clube67_newwhats.local (VPS 4)
chore(ops): restore all source files in newwhats.clube67.com
2026-05-18 03:28:29 +02:00

189 lines
7.5 KiB
TypeScript

import { useState, useCallback, useEffect, useRef } from 'react'
import { useInstanceStore } from '../store/instanceStore'
import { contactApi, type BackendContact } from '../services/chatApiService'
const PAGE_SIZE = 50
export function usePhonebookLogic() {
const instanceStore = useInstanceStore()
// ── Data ──────────────────────────────────────────────────────────────────
const [contacts, setContacts] = useState<BackendContact[]>([])
const [total, setTotal] = useState(0)
const [stats, setStats] = useState({ total: 0, blocked: 0, flagged: 0, active: 0 })
const [loading, setLoading] = useState(false)
// ── Filters ───────────────────────────────────────────────────────────────
const [search, setSearch] = useState('')
const [blockedFilter, setBlockedFilter] = useState<'all' | 'blocked' | 'active'>('all')
const [offset, setOffset] = useState(0)
// ── Edit modal ────────────────────────────────────────────────────────────
const [editingContact, setEditingContact] = useState<BackendContact | null>(null)
const [editName, setEditName] = useState('')
const [editSaving, setEditSaving] = useState(false)
// ── Toast ─────────────────────────────────────────────────────────────────
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), 3000)
}, [])
// ── Derived ───────────────────────────────────────────────────────────────
const activeInstanceId = instanceStore.activeInstanceId
const searchDebounceRef = useRef<ReturnType<typeof setTimeout>>()
// ── Load contacts ─────────────────────────────────────────────────────────
const load = useCallback(async (newOffset = 0) => {
setLoading(true)
try {
const params: Parameters<typeof contactApi.list>[0] = {
instanceId: activeInstanceId ?? undefined,
limit: PAGE_SIZE,
offset: newOffset,
}
if (search.trim()) params.search = search.trim()
if (blockedFilter === 'blocked') params.blocked = true
if (blockedFilter === 'active') params.blocked = false
const [data, statsData] = await Promise.all([
contactApi.list(params),
contactApi.stats(activeInstanceId ?? undefined),
])
setContacts(data.contacts)
setTotal(data.total)
setStats(statsData)
setOffset(newOffset)
} catch {
showToast('Erro ao carregar contatos.', 'error')
} finally {
setLoading(false)
}
}, [activeInstanceId, search, blockedFilter, showToast])
// Initial load + instance change
useEffect(() => {
instanceStore.loadInstances()
}, [])
useEffect(() => {
load(0)
}, [activeInstanceId, blockedFilter])
// Debounce search
useEffect(() => {
clearTimeout(searchDebounceRef.current)
searchDebounceRef.current = setTimeout(() => load(0), 350)
return () => clearTimeout(searchDebounceRef.current)
}, [search])
// ── Pagination ────────────────────────────────────────────────────────────
const goToPage = useCallback((page: number) => {
load(page * PAGE_SIZE)
}, [load])
const currentPage = Math.floor(offset / PAGE_SIZE)
const totalPages = Math.ceil(total / PAGE_SIZE)
// ── Block/unblock ─────────────────────────────────────────────────────────
const handleToggleBlock = useCallback(async (contact: BackendContact) => {
try {
const updated = await contactApi.update(contact.id, { isBlocked: !contact.isBlocked })
setContacts((prev) => prev.map((c) => c.id === updated.id ? updated : c))
showToast(updated.isBlocked ? 'Contato bloqueado.' : 'Contato desbloqueado.', 'success')
} catch {
showToast('Erro ao alterar bloqueio.', 'error')
}
}, [showToast])
// ── Delete ────────────────────────────────────────────────────────────────
const handleDelete = useCallback(async (id: string) => {
try {
await contactApi.delete(id)
setContacts((prev) => prev.filter((c) => c.id !== id))
setTotal((t) => t - 1)
showToast('Contato removido.', 'success')
} catch {
showToast('Erro ao remover contato.', 'error')
}
}, [showToast])
// ── Edit ──────────────────────────────────────────────────────────────────
const openEdit = useCallback((contact: BackendContact) => {
setEditingContact(contact)
setEditName(contact.name ?? '')
}, [])
const closeEdit = useCallback(() => {
setEditingContact(null)
setEditName('')
}, [])
const saveEdit = useCallback(async () => {
if (!editingContact) return
setEditSaving(true)
try {
const updated = await contactApi.update(editingContact.id, { name: editName.trim() || undefined })
setContacts((prev) => prev.map((c) => c.id === updated.id ? updated : c))
showToast('Contato atualizado.', 'success')
closeEdit()
} catch {
showToast('Erro ao salvar contato.', 'error')
} finally {
setEditSaving(false)
}
}, [editingContact, editName, showToast, closeEdit])
// ── Display name helper ───────────────────────────────────────────────────
function displayName(c: BackendContact & { displayName?: string }): string {
if (c.displayName || c.name || c.verifiedName || c.notify) {
return c.displayName || c.name || c.verifiedName || c.notify || ''
}
if (c.phone) return `+${c.phone}`
// @lid — identificador interno do WhatsApp: exibe como +número
if (c.jid.endsWith('@lid')) {
const id = c.jid.split('@')[0]
return `+${id}`
}
return c.jid.split('@')[0]
}
return {
// instances
instances: instanceStore.instances,
activeInstanceId,
selectInstance: instanceStore.setActiveInstance.bind(instanceStore),
// data
contacts,
total,
stats,
loading,
// filters
search, setSearch,
blockedFilter, setBlockedFilter,
// pagination
currentPage, totalPages, goToPage,
// actions
handleToggleBlock,
handleDelete,
refresh: () => load(0),
// edit modal
editingContact,
editName, setEditName,
editSaving,
openEdit, closeEdit, saveEdit,
// feedback
toast,
displayName,
}
}