/** * chatApiService.ts — Client HTTP para o novo backend NewWhats. * Consumido pelo chatStore e pelos componentes React. */ import axios, { type AxiosInstance } from 'axios' const API = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:3003' function createClient(): AxiosInstance { const client = axios.create({ baseURL: API }) client.interceptors.request.use((config) => { const token = typeof window !== 'undefined' ? localStorage.getItem('token') : null if (token) config.headers.Authorization = `Bearer ${token}` return config }) // Renova token automaticamente em 401 client.interceptors.response.use( (r) => r, async (error) => { const original = error.config if (error.response?.status === 401 && !original._retry) { original._retry = true try { const refreshToken = localStorage.getItem('refreshToken') if (!refreshToken) throw new Error('Sem refresh token') const { data } = await axios.post(`${API}/api/auth/refresh`, { refreshToken }) localStorage.setItem('token', data.accessToken) if (typeof window !== 'undefined') { document.cookie = `accessToken=${data.accessToken}; path=/; SameSite=Lax; Secure` } localStorage.setItem('refreshToken', data.refreshToken) original.headers.Authorization = `Bearer ${data.accessToken}` return client(original) } catch { localStorage.removeItem('token') localStorage.removeItem('refreshToken') if (typeof window !== 'undefined') { document.cookie = 'accessToken=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/' } window.location.href = '/login' } } return Promise.reject(error) } ) return client } export const api = createClient() // ─── Auth ───────────────────────────────────────────────────────────────────── export const authApi = { register: (data: { name: string; email: string; password: string }) => api.post('/api/auth/register', data).then((r) => r.data), login: (data: { email: string; password: string }) => api.post('/api/auth/login', data).then((r) => r.data), logout: () => api.post('/api/auth/logout').then((r) => r.data), me: () => api.get('/api/auth/me').then((r) => r.data), changePassword: (data: { currentPassword: string; newPassword: string }) => api.patch('/api/auth/password', data).then((r) => r.data), } // ─── Instâncias ─────────────────────────────────────────────────────────────── export const instanceApi = { list: () => api.get('/api/instances').then((r) => r.data), create: (name: string) => api.post('/api/instances', { name }).then((r) => r.data), connect: (id: string) => api.post(`/api/instances/${id}/connect`).then((r) => r.data), disconnect: (id: string) => api.post(`/api/instances/${id}/disconnect`).then((r) => r.data), getQr: (id: string) => api.get(`/api/instances/${id}/qr`).then((r) => r.data), delete: (id: string) => api.delete(`/api/instances/${id}`).then((r) => r.data), } // ─── Chats ──────────────────────────────────────────────────────────────────── export const chatApi = { list: (instanceId: string, opts: { search?: string; archived?: boolean; limit?: number } = {}) => api.get('/api/chats', { params: { instanceId, ...opts } }).then((r) => r.data), get: (chatId: string) => api.get(`/api/chats/${chatId}`).then((r) => r.data), markRead: (chatId: string) => api.post(`/api/chats/${chatId}/read`).then((r) => r.data), archive: (chatId: string, archived: boolean) => api.patch(`/api/chats/${chatId}/archive`, { archived }).then((r) => r.data), pin: (chatId: string, pinned: boolean) => api.patch(`/api/chats/${chatId}/pin`, { pinned }).then((r) => r.data), delete: (chatId: string) => api.delete(`/api/chats/${chatId}`).then((r) => r.data), stats: (instanceId: string) => api.get('/api/chats/stats/overview', { params: { instanceId } }).then((r) => r.data), } // ─── Mensagens ──────────────────────────────────────────────────────────────── export const messageApi = { list: (chatId: string, opts: { before?: string; after?: string; limit?: number } = {}) => api.get(`/api/chats/${chatId}/messages`, { params: opts }).then((r) => r.data) as Promise<{ messages: any[]; hasMore: boolean }>, search: (instanceId: string, q: string, limit?: number) => api.get('/api/chats/search/messages', { params: { instanceId, q, limit } }).then((r) => r.data), send: (instanceId: string, jid: string, text: string, replyToMessageId?: string, mentions?: string[]) => api.post(`/api/instances/${instanceId}/send`, { jid, text, replyToMessageId, ...(mentions?.length ? { mentions } : {}) }).then((r) => r.data), } // ─── Grupos ────────────────────────────────────────────────────────────────── export interface GroupParticipant { jid: string admin: string | null name: string | null phone: string } export const groupApi = { getParticipants: (instanceId: string, groupJid: string) => api.get(`/api/instances/${instanceId}/groups/${encodeURIComponent(groupJid)}/participants`).then((r) => r.data) as Promise<{ groupJid: string; subject: string; participants: GroupParticipant[] }>, } // ─── Reconciliação ─────────────────────────────────────────────────────────── export const reconcileApi = { reconcileNames: (instanceId: string) => api.post(`/api/instances/${instanceId}/reconcile-names`).then((r) => r.data) as Promise<{ updated: number } | void>, } // ─── Protocolos ─────────────────────────────────────────────────────────────── export const protocolApi = { list: (chatId: string) => api.get(`/api/chats/${chatId}/protocol`).then((r) => r.data), open: (chatId: string, data: { sectorId?: string; teamId?: string }) => api.post(`/api/chats/${chatId}/protocol`, data).then((r) => r.data), update: (chatId: string, protocolId: string, data: { status?: string summary?: string }) => api.patch(`/api/chats/${chatId}/protocol/${protocolId}`, data).then((r) => r.data), } // ─── Mídia ──────────────────────────────────────────────────────────────────── export const mediaApi = { sendMedia: ( instanceId: string, jid: string, file: File, caption?: string, replyToMessageId?: string ) => { const fd = new FormData() fd.append('file', file) fd.append('jid', jid) if (caption) fd.append('caption', caption) if (replyToMessageId) fd.append('replyToMessageId', replyToMessageId) return api .post(`/api/instances/${instanceId}/send-media`, fd, { headers: { 'Content-Type': 'multipart/form-data' }, }) .then((r) => r.data) }, sendAudio: (instanceId: string, jid: string, blob: Blob) => { const fd = new FormData() fd.append('audio', blob, 'audio.ogg') fd.append('jid', jid) return api .post(`/api/instances/${instanceId}/send-audio`, fd, { headers: { 'Content-Type': 'multipart/form-data' }, }) .then((r) => r.data) }, sendSticker: (instanceId: string, jid: string, blob: Blob) => { const fd = new FormData() fd.append('file', blob, 'sticker.webp') fd.append('jid', jid) return api .post(`/api/instances/${instanceId}/send-media`, fd, { headers: { 'Content-Type': 'multipart/form-data' }, }) .then((r) => r.data) }, redownload: (instanceId: string, messageDbId: string): Promise<{ mediaUrl: string }> => api.post(`/api/instances/${instanceId}/redownload-media/${messageDbId}`).then((r) => r.data), } // ─── Chatbot / IA ───────────────────────────────────────────────────────────── export const chatbotApi = { getCredentials: (instanceId: string) => api.get(`/api/chatbot/credentials/${instanceId}`).then((r) => r.data), createCredential: (instanceId: string, data: { name: string; apiKey: string; provider?: 'GEMINI' | 'OPENAI' }) => api.post(`/api/chatbot/credentials/${instanceId}`, data).then((r) => r.data), deleteCredential: (instanceId: string, credId: string) => api.delete(`/api/chatbot/credentials/${instanceId}/${credId}`).then((r) => r.data), getBots: (instanceId: string) => api.get(`/api/chatbot/bots/${instanceId}`).then((r) => r.data), createBot: (instanceId: string, data: Record) => api.post(`/api/chatbot/bots/${instanceId}`, data).then((r) => r.data), updateBot: (instanceId: string, botId: string, data: Record) => api.put(`/api/chatbot/bots/${instanceId}/${botId}`, data).then((r) => r.data), deleteBot: (instanceId: string, botId: string) => api.delete(`/api/chatbot/bots/${instanceId}/${botId}`).then((r) => r.data), pauseChat: (_instanceId: string, chatId: string) => api.post(`/api/chatbot/chats/${chatId}/pause`).then((r) => r.data), resumeChat: (_instanceId: string, chatId: string) => api.post(`/api/chatbot/chats/${chatId}/resume`).then((r) => r.data), } // ─── API Keys ───────────────────────────────────────────────────────────────── export interface ApiKeyRecord { id: string name: string keyPreview: string // '••••••••XXXXXXXX' isActive: boolean createdAt: string expiresAt: string | null } export const apiKeyApi = { list: () => api.get('/api/api-keys').then((r) => r.data) as Promise, create: (name: string, expiresAt?: string) => api.post('/api/api-keys', { name, expiresAt }).then((r) => r.data) as Promise<{ id: string; name: string; key: string; isActive: boolean; createdAt: string; expiresAt: string | null }>, toggle: (id: string, isActive: boolean) => api.patch(`/api/api-keys/${id}`, { isActive }).then((r) => r.data), delete: (id: string) => api.delete(`/api/api-keys/${id}`).then((r) => r.data), } // ─── Setores e Equipes ──────────────────────────────────────────────────────── export interface Sector { id: string tenantId: string name: string isActive: boolean createdAt: string updatedAt: string _count?: { teams: number; protocols: number } } export interface Team { id: string tenantId: string sectorId: string name: string isActive: boolean createdAt: string updatedAt: string _count?: { protocols: number } } export const sectorApi = { list: () => api.get('/api/sectors').then((r) => r.data) as Promise, create: (name: string) => api.post('/api/sectors', { name }).then((r) => r.data) as Promise, update: (id: string, data: { name?: string; isActive?: boolean }) => api.patch(`/api/sectors/${id}`, data).then((r) => r.data) as Promise, delete: (id: string) => api.delete(`/api/sectors/${id}`).then((r) => r.data), listTeams: (sectorId: string) => api.get(`/api/sectors/${sectorId}/teams`).then((r) => r.data) as Promise, createTeam: (sectorId: string, name: string) => api.post(`/api/sectors/${sectorId}/teams`, { name }).then((r) => r.data) as Promise, updateTeam: (sectorId: string, teamId: string, data: { name?: string; isActive?: boolean }) => api.patch(`/api/sectors/${sectorId}/teams/${teamId}`, data).then((r) => r.data) as Promise, deleteTeam: (sectorId: string, teamId: string) => api.delete(`/api/sectors/${sectorId}/teams/${teamId}`).then((r) => r.data), } // ─── Contatos / Phonebook ───────────────────────────────────────────────────── export interface BackendContact { id: string instanceId: string jid: string name: string | null verifiedName: string | null notify: string | null phone: string | null avatarUrl: string | null scoreReputacao: number flagRestricao: boolean isBlocked: boolean createdAt: string updatedAt: string } export const contactApi = { list: (opts: { instanceId?: string; search?: string; blocked?: boolean; limit?: number; offset?: number } = {}) => api.get('/api/contacts', { params: opts }).then((r) => r.data) as Promise<{ total: number; contacts: BackendContact[] }>, get: (id: string) => api.get(`/api/contacts/${id}`).then((r) => r.data) as Promise, update: (id: string, data: { name?: string; isBlocked?: boolean }) => api.patch(`/api/contacts/${id}`, data).then((r) => r.data) as Promise, delete: (id: string) => api.delete(`/api/contacts/${id}`).then((r) => r.data), stats: (instanceId?: string) => api.get('/api/contacts/stats/overview', { params: instanceId ? { instanceId } : undefined }).then((r) => r.data) as Promise<{ total: number; blocked: number; flagged: number; active: number }>, } // ─── Broadcast ─────────────────────────────────────────────────────────────── export const broadcastApi = { create: (instanceId: string, data: { numbers: string[]; message: string; delayMs?: number }) => api.post(`/api/instances/${instanceId}/broadcast`, data).then((r) => r.data) as Promise<{ jobId: string; total: number }>, list: (instanceId: string, limit = 20) => api.get(`/api/instances/${instanceId}/broadcast`, { params: { limit } }).then((r) => r.data) as Promise>, get: (instanceId: string, broadcastId: string) => api.get(`/api/instances/${instanceId}/broadcast/${broadcastId}`).then((r) => r.data), } // ─── Plano ──────────────────────────────────────────────────────────────────── export const planApi = { status: () => api.get('/api/plan/status').then((r) => r.data) as Promise<{ daysRemaining: number | null; planName: string }>, } // ─── Mensagens Ricas (tipos avançados de WhatsApp) ──────────────────────────── export interface RichPayload { text?: string footer?: string nativeButtons?: Array<{ type: string; id?: string; text: string; url?: string; copyText?: string; phoneNumber?: string }> nativeList?: { buttonText: string; sections: Array<{ title: string; rows: Array<{ id: string; title: string; description?: string }> }> } nativeCarousel?: { cards: Array<{ title?: string; body?: string; footer?: string; image?: { url: string }; buttons: Array<{ type?: string; id?: string; text: string }> }> } poll?: { name: string; values: string[]; selectableCount?: number } } export const richMessageApi = { send: (instanceId: string, jid: string, payload: RichPayload) => api.post(`/api/instances/${instanceId}/send`, { jid, ...payload }).then((r) => r.data), } // ─── Templates de Mensagem ──────────────────────────────────────────────────── export type TemplateType = 'TEXT' | 'BUTTONS' | 'INTERACTIVE' | 'LIST' | 'POLL' | 'CAROUSEL' export interface MessageTemplate { id: string tenantId: string name: string description: string | null type: TemplateType payload: Record tags: string[] usageCount: number createdAt: string updatedAt: string } export const templateApi = { list: (opts: { type?: TemplateType; search?: string } = {}) => api.get('/api/templates', { params: opts }).then((r) => r.data) as Promise, get: (id: string) => api.get(`/api/templates/${id}`).then((r) => r.data) as Promise, create: (data: { name: string; description?: string; type: TemplateType; payload: Record; tags?: string[] }) => api.post('/api/templates', data).then((r) => r.data) as Promise, update: (id: string, data: Partial<{ name: string; description: string; type: TemplateType; payload: Record; tags: string[] }>) => api.put(`/api/templates/${id}`, data).then((r) => r.data) as Promise, delete: (id: string) => api.delete(`/api/templates/${id}`).then((r) => r.data), markUsed: (id: string) => api.post(`/api/templates/${id}/use`).then((r) => r.data), } // ─── Agendamentos de Mensagem ───────────────────────────────────────────────── export type ScheduleType = 'ONCE' | 'RECURRING' | 'EVENT' export type ScheduleStatus = 'ACTIVE' | 'PAUSED' | 'DONE' | 'CANCELLED' export type EventType = 'BIRTHDAY' | 'ANNIVERSARY' | 'SIGNUP_DAYS' export type RecipientMode = 'MANUAL' | 'ALL_CONTACTS' | 'TAG_FILTER' export interface ScheduleRecipient { jid: string name?: string birthday?: string anniversary?: string signupDate?: string } export interface ScheduledMessage { id: string tenantId: string instanceId: string name: string templateId: string | null payload: Record recipientMode: RecipientMode recipients: ScheduleRecipient[] scheduleType: ScheduleType cronExpr: string | null scheduledAt: string | null eventType: EventType | null timezone: string status: ScheduleStatus lastRunAt: string | null nextRunAt: string | null runCount: number createdAt: string updatedAt: string template?: Pick | null _count?: { logs: number } } export interface ScheduleLog { id: string scheduleId: string status: string sentCount: number failedCount: number error: string | null runAt: string } export const scheduleApi = { list: (opts: { status?: ScheduleStatus; type?: ScheduleType } = {}) => api.get('/api/scheduled', { params: opts }).then((r) => r.data) as Promise, get: (id: string) => api.get(`/api/scheduled/${id}`).then((r) => r.data) as Promise, create: (data: { instanceId: string name: string templateId?: string payload: Record recipientMode?: RecipientMode recipients?: ScheduleRecipient[] scheduleType: ScheduleType cronExpr?: string scheduledAt?: string eventType?: EventType timezone?: string }) => api.post('/api/scheduled', data).then((r) => r.data) as Promise, update: (id: string, data: Partial[0]>) => api.put(`/api/scheduled/${id}`, data).then((r) => r.data) as Promise, delete: (id: string) => api.delete(`/api/scheduled/${id}`).then((r) => r.data), pause: (id: string) => api.post(`/api/scheduled/${id}/pause`).then((r) => r.data), resume: (id: string) => api.post(`/api/scheduled/${id}/resume`).then((r) => r.data), runNow: (id: string) => api.post(`/api/scheduled/${id}/run-now`).then((r) => r.data), logs: (id: string) => api.get(`/api/scheduled/${id}/logs`).then((r) => r.data) as Promise, } // ─── Stickers ───────────────────────────────────────────────────────────────── export interface StickerRecord { id: string sha256: string wasabiPath: string isAnimated: boolean isFavorite: boolean useCount?: number lastSeenAt?: string } export const stickerApi = { list: () => api.get('/api/stickers').then((r) => r.data) as Promise<{ listVersion: string; stickers: StickerRecord[] }>, recent: () => api.get('/api/stickers/recent').then((r) => r.data) as Promise<{ stickers: StickerRecord[] }>, toggleFavorite: (id: string) => api.patch(`/api/stickers/${id}/favorite`).then((r) => r.data) as Promise<{ id: string; isFavorite: boolean }>, }