189 lines
7.5 KiB
TypeScript
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,
|
|
}
|
|
}
|