557 lines
21 KiB
TypeScript
557 lines
21 KiB
TypeScript
/**
|
|
* 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<string, unknown>) =>
|
|
api.post(`/api/chatbot/bots/${instanceId}`, data).then((r) => r.data),
|
|
|
|
updateBot: (instanceId: string, botId: string, data: Record<string, unknown>) =>
|
|
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<ApiKeyRecord[]>,
|
|
|
|
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<Sector[]>,
|
|
|
|
create: (name: string) =>
|
|
api.post('/api/sectors', { name }).then((r) => r.data) as Promise<Sector>,
|
|
|
|
update: (id: string, data: { name?: string; isActive?: boolean }) =>
|
|
api.patch(`/api/sectors/${id}`, data).then((r) => r.data) as Promise<Sector>,
|
|
|
|
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<Team[]>,
|
|
|
|
createTeam: (sectorId: string, name: string) =>
|
|
api.post(`/api/sectors/${sectorId}/teams`, { name }).then((r) => r.data) as Promise<Team>,
|
|
|
|
updateTeam: (sectorId: string, teamId: string, data: { name?: string; isActive?: boolean }) =>
|
|
api.patch(`/api/sectors/${sectorId}/teams/${teamId}`, data).then((r) => r.data) as Promise<Team>,
|
|
|
|
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<BackendContact>,
|
|
|
|
update: (id: string, data: { name?: string; isBlocked?: boolean }) =>
|
|
api.patch(`/api/contacts/${id}`, data).then((r) => r.data) as Promise<BackendContact>,
|
|
|
|
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<Array<{
|
|
id: string; status: string; message: string
|
|
totalCount: number; sentCount: number; failedCount: number
|
|
createdAt: string; finishedAt: string | null
|
|
_count: { recipients: number }
|
|
}>>,
|
|
|
|
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<string, unknown>
|
|
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<MessageTemplate[]>,
|
|
|
|
get: (id: string) =>
|
|
api.get(`/api/templates/${id}`).then((r) => r.data) as Promise<MessageTemplate>,
|
|
|
|
create: (data: { name: string; description?: string; type: TemplateType; payload: Record<string, unknown>; tags?: string[] }) =>
|
|
api.post('/api/templates', data).then((r) => r.data) as Promise<MessageTemplate>,
|
|
|
|
update: (id: string, data: Partial<{ name: string; description: string; type: TemplateType; payload: Record<string, unknown>; tags: string[] }>) =>
|
|
api.put(`/api/templates/${id}`, data).then((r) => r.data) as Promise<MessageTemplate>,
|
|
|
|
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<string, unknown>
|
|
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<MessageTemplate, 'id' | 'name' | 'type'> | 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<ScheduledMessage[]>,
|
|
|
|
get: (id: string) =>
|
|
api.get(`/api/scheduled/${id}`).then((r) => r.data) as Promise<ScheduledMessage>,
|
|
|
|
create: (data: {
|
|
instanceId: string
|
|
name: string
|
|
templateId?: string
|
|
payload: Record<string, unknown>
|
|
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<ScheduledMessage>,
|
|
|
|
update: (id: string, data: Partial<Parameters<typeof scheduleApi.create>[0]>) =>
|
|
api.put(`/api/scheduled/${id}`, data).then((r) => r.data) as Promise<ScheduledMessage>,
|
|
|
|
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<ScheduleLog[]>,
|
|
}
|
|
|
|
// ─── 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 }>,
|
|
}
|