/** * instanceStore.ts — Gerencia instâncias WhatsApp e QR Codes. * Separado do chatStore para responsabilidade única. */ import { create } from 'zustand' import { immer } from 'zustand/middleware/immer' import { instanceApi } from '../services/chatApiService' import { socketService } from '../services/socketService' import { notify } from './notificationStore' export type InstanceStatus = 'DISCONNECTED' | 'CONNECTING' | 'QR_PENDING' | 'CONNECTED' | 'BANNED' export interface Instance { id: string name: string status: InstanceStatus phone: string | null avatar: string | null } interface InstanceState { instances: Instance[] activeInstanceId: string | null qrByInstance: Record loading: boolean // Sync actions setInstances: (instances: Instance[]) => void setActiveInstance: (id: string | null) => void patchInstance: (id: string, patch: Partial) => void setQr: (instanceId: string, qrBase64: string) => void clearQr: (instanceId: string) => void removeInstance: (id: string) => void // Async actions loadInstances: () => Promise connectInstance: (id: string) => Promise disconnectInstance: (id: string) => Promise createInstance: (name: string) => Promise deleteInstance: (id: string) => Promise // Socket initSocketListeners: () => void } export const useInstanceStore = create()( immer((set, get) => ({ instances: [], // Persiste a seleção ativa no localStorage para sobreviver a reloads activeInstanceId: typeof window !== 'undefined' ? localStorage.getItem('activeInstanceId') : null, qrByInstance: {}, loading: false, // ── Sync ───────────────────────────────────────────────────────────────── setInstances: (instances) => set((s) => { s.instances = instances }), setActiveInstance: (id) => { if (typeof window !== 'undefined') { if (id) localStorage.setItem('activeInstanceId', id) else localStorage.removeItem('activeInstanceId') } set((s) => { s.activeInstanceId = id }) }, patchInstance: (id, patch) => set((s) => { const idx = s.instances.findIndex((i) => i.id === id) if (idx !== -1) Object.assign(s.instances[idx], patch) else s.instances.push({ id, name: '', phone: null, status: 'CONNECTING', ...patch } as Instance) }), setQr: (instanceId, qrBase64) => set((s) => { s.qrByInstance[instanceId] = qrBase64 }), clearQr: (instanceId) => set((s) => { delete s.qrByInstance[instanceId] }), removeInstance: (id) => set((s) => { s.instances = s.instances.filter((i) => i.id !== id) }), // ── Async ───────────────────────────────────────────────────────────────── loadInstances: async () => { set((s) => { s.loading = true }) try { const data = await instanceApi.list() const list: Instance[] = (Array.isArray(data) ? data : (data.instances ?? [])).map((i: any) => ({ ...i, avatar: i.avatar ?? null, })) set((s) => { s.loading = false // Mescla em vez de substituir: preserva status CONNECTED que veio via socket // (socket events são mais recentes que leituras REST — o banco pode estar // desatualizado se a resposta da API chegou antes do DB ser gravado pelo backend) const memMap = new Map(s.instances.map((i) => [i.id, i])) s.instances = list.map((fromDb) => { const inMem = memMap.get(fromDb.id) if (inMem?.status === 'CONNECTED' && fromDb.status !== 'CONNECTED') { // Socket já confirmou CONNECTED — mantém, não deixa REST stale sobrescrever return { ...fromDb, status: 'CONNECTED' as InstanceStatus } } return fromDb }) }) } catch { set((s) => { s.loading = false }) } }, connectInstance: async (id) => { get().patchInstance(id, { status: 'CONNECTING' }) socketService.joinInstance(id) await instanceApi.connect(id) }, disconnectInstance: async (id) => { await instanceApi.disconnect(id) get().patchInstance(id, { status: 'DISCONNECTED' }) get().clearQr(id) }, createInstance: async (name) => { const created: Instance = await instanceApi.create(name) set((s) => { s.instances.push(created) }) return created }, deleteInstance: async (id) => { await instanceApi.delete(id) get().removeInstance(id) if (get().activeInstanceId === id) { set((s) => { s.activeInstanceId = null }) } }, // ── Socket ──────────────────────────────────────────────────────────────── initSocketListeners: () => { socketService.on('instance:status', ({ instanceId, status, qrBase64, }: { instanceId: string status: InstanceStatus qrBase64?: string }) => { get().patchInstance(instanceId, { status }) if (qrBase64) get().setQr(instanceId, qrBase64) if (status === 'CONNECTED') get().clearQr(instanceId) }) socketService.on('qr', ({ instanceId, qrBase64 }: { instanceId: string; qrBase64: string }) => { get().setQr(instanceId, qrBase64) get().patchInstance(instanceId, { status: 'QR_PENDING' }) }) socketService.on('instance:avatar', ({ instanceId, avatar }: { instanceId: string; avatar: string }) => { get().patchInstance(instanceId, { avatar }) }) socketService.on('instance:syncing', ({ instanceId, gapMinutes }: { instanceId: string; gapMinutes: number }) => { const inst = get().instances.find((i) => i.id === instanceId) const label = inst?.name ?? instanceId.slice(0, 8) if (gapMinutes >= 60) { notify( `📥 ${label}: sincronizando mensagens dos últimos ${Math.round(gapMinutes / 60)}h`, 'warning', 8000, 'syncing', ) } else if (gapMinutes > 0) { notify( `📥 ${label}: buscando mensagens perdidas (${gapMinutes} min offline)`, 'info', 6000, 'syncing', ) } }) }, })) )