@fayz-ai/plugin-conversations 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/InboxWatcher.d.ts +6 -8
- package/dist/components/InboxWatcher.d.ts.map +1 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +294 -714
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,381 +1,14 @@
|
|
|
1
|
-
import * as
|
|
2
|
-
import
|
|
3
|
-
import { Loader2,
|
|
4
|
-
import { toast, Button, Input, defineKpiWidget, defineTableWidget, PageHeaderActions, cn, Badge, Modal, ModalContent, ModalHeader, ModalTitle, ModalBody, ModalFooter, FilterChip, Skeleton, KpiCard, TableWidget } from '@fayz-ai/ui';
|
|
1
|
+
import * as React10 from 'react';
|
|
2
|
+
import React10__default, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
|
|
3
|
+
import { Loader2, Check, ShieldCheck, ShieldAlert, RefreshCw, QrCode, Zap, ExternalLink, MessageCircle, SquarePen, MessageSquare, Globe, Mail, Instagram, Phone, Search, User, Inbox, CheckCheck, ChevronLeft, Archive, PanelRight, Send, X, MapPin, Tag, StickyNote, Link2, MoreVertical, AlertTriangle, Clock, Snowflake, Sun, Flame, Building2, UserRound, Copy, FileText, Download } from 'lucide-react';
|
|
5
4
|
import { createConnectionStore, connectionRuns, connectionStatus, getActiveTenantId, getSupabaseClientOptional, useActiveTenantId, registerTranslations, useTranslation, countByTenant, CONNECTOR_RUNTIME_TOKEN_HEADER, connectorRuntimeToken, errorMessage } from '@fayz-ai/core';
|
|
5
|
+
import { SettingsGroup, PluginSettingsPanel, useAdminPath, adminNavigateTo, useRightRailStore, PageActions, useLimitGuard, ContactPicker, invalidateLimit } from '@fayz-ai/admin';
|
|
6
6
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
7
|
-
import { SettingsGroup, PluginSettingsPanel, useNavContribution, useRightRailStore, PageActions, useLimitGuard, ContactPicker, invalidateLimit } from '@fayz-ai/admin';
|
|
8
7
|
import { useStore } from 'zustand';
|
|
8
|
+
import { Input, Button, toast, defineKpiWidget, defineTableWidget, PageHeaderActions, cn, Badge, Modal, ModalContent, ModalHeader, ModalTitle, ModalBody, ModalFooter, FilterChip, Skeleton, KpiCard, TableWidget } from '@fayz-ai/ui';
|
|
9
9
|
import { createStore } from 'zustand/vanilla';
|
|
10
10
|
|
|
11
11
|
// src/index.ts
|
|
12
|
-
function client() {
|
|
13
|
-
const c = getSupabaseClientOptional();
|
|
14
|
-
if (!c) throw new Error("Supabase n\xE3o inicializado");
|
|
15
|
-
return c;
|
|
16
|
-
}
|
|
17
|
-
function tenant() {
|
|
18
|
-
const t = getActiveTenantId();
|
|
19
|
-
if (!t) throw new Error("Nenhuma casa ativa");
|
|
20
|
-
return t;
|
|
21
|
-
}
|
|
22
|
-
var campaignsApi = {
|
|
23
|
-
async templates() {
|
|
24
|
-
const { data, error } = await client().from("plg_conversations_templates").select("id, name, language, category, body, variables, provider, status, status_detail").eq("tenant_id", tenant()).order("name");
|
|
25
|
-
if (error) throw new Error(error.message);
|
|
26
|
-
return (data ?? []).map((r) => ({
|
|
27
|
-
id: r.id,
|
|
28
|
-
name: r.name,
|
|
29
|
-
language: r.language,
|
|
30
|
-
category: r.category,
|
|
31
|
-
body: r.body,
|
|
32
|
-
variables: r.variables ?? [],
|
|
33
|
-
provider: r.provider,
|
|
34
|
-
status: r.status,
|
|
35
|
-
statusDetail: r.status_detail ?? null
|
|
36
|
-
}));
|
|
37
|
-
},
|
|
38
|
-
async saveTemplate(t) {
|
|
39
|
-
const row = {
|
|
40
|
-
tenant_id: tenant(),
|
|
41
|
-
name: t.name,
|
|
42
|
-
body: t.body,
|
|
43
|
-
variables: t.variables ?? [],
|
|
44
|
-
language: t.language ?? "pt_BR",
|
|
45
|
-
category: t.category ?? "MARKETING",
|
|
46
|
-
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
47
|
-
};
|
|
48
|
-
const q = t.id ? client().from("plg_conversations_templates").update(row).eq("id", t.id) : client().from("plg_conversations_templates").insert(row);
|
|
49
|
-
const { error } = await q;
|
|
50
|
-
if (error) throw new Error(error.message);
|
|
51
|
-
},
|
|
52
|
-
async list() {
|
|
53
|
-
const { data, error } = await client().from("v_conversations_campaign_progress").select("*").eq("tenant_id", tenant());
|
|
54
|
-
if (error) throw new Error(error.message);
|
|
55
|
-
return (data ?? []).map((r) => ({
|
|
56
|
-
campaignId: r.campaign_id,
|
|
57
|
-
name: r.name,
|
|
58
|
-
status: r.status,
|
|
59
|
-
pausedReason: r.paused_reason ?? null,
|
|
60
|
-
provider: r.provider,
|
|
61
|
-
total: Number(r.total ?? 0),
|
|
62
|
-
queued: Number(r.queued ?? 0),
|
|
63
|
-
sending: Number(r.sending ?? 0),
|
|
64
|
-
sent: Number(r.sent ?? 0),
|
|
65
|
-
delivered: Number(r.delivered ?? 0),
|
|
66
|
-
read: Number(r.read ?? 0),
|
|
67
|
-
replied: Number(r.replied ?? 0),
|
|
68
|
-
failed: Number(r.failed ?? 0),
|
|
69
|
-
skipped: Number(r.skipped ?? 0),
|
|
70
|
-
replyRate: r.reply_rate == null ? null : Number(r.reply_rate),
|
|
71
|
-
firstSentAt: r.first_sent_at ?? null,
|
|
72
|
-
lastSentAt: r.last_sent_at ?? null
|
|
73
|
-
}));
|
|
74
|
-
},
|
|
75
|
-
/** Cria a campanha e enfileira os destinatários.
|
|
76
|
-
*
|
|
77
|
-
* `ON CONFLICT DO NOTHING` via upsert: o índice único (campanha, telefone,
|
|
78
|
-
* rodada) é quem impede o disparo duplo, e não uma checagem aqui. Dois
|
|
79
|
-
* cliques no botão enfileiram uma vez. */
|
|
80
|
-
async create(input) {
|
|
81
|
-
const tenantId = tenant();
|
|
82
|
-
const { data: camp, error } = await client().from("plg_conversations_campaigns").insert({
|
|
83
|
-
tenant_id: tenantId,
|
|
84
|
-
name: input.name,
|
|
85
|
-
template_id: input.templateId ?? null,
|
|
86
|
-
provider: input.provider ?? null,
|
|
87
|
-
channel_id: input.channelId ?? null,
|
|
88
|
-
status: "draft"
|
|
89
|
-
}).select("id").single();
|
|
90
|
-
if (error) throw new Error(error.message);
|
|
91
|
-
const rows = input.targets.map((t) => ({
|
|
92
|
-
tenant_id: tenantId,
|
|
93
|
-
campaign_id: camp.id,
|
|
94
|
-
phone_e164: String(t.phone).replace(/\D/g, ""),
|
|
95
|
-
person_id: t.personId ?? null,
|
|
96
|
-
variables: t.variables ?? {}
|
|
97
|
-
})).filter((r) => r.phone_e164.length >= 10);
|
|
98
|
-
for (let i = 0; i < rows.length; i += 200) {
|
|
99
|
-
const { error: e } = await client().from("plg_conversations_campaign_targets").upsert(rows.slice(i, i + 200), { onConflict: "campaign_id,phone_e164,rotation", ignoreDuplicates: true });
|
|
100
|
-
if (e) throw new Error(e.message);
|
|
101
|
-
}
|
|
102
|
-
return camp.id;
|
|
103
|
-
},
|
|
104
|
-
async setStatus(campaignId, status) {
|
|
105
|
-
const patch = { status, updated_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
106
|
-
if (status === "running") {
|
|
107
|
-
patch.started_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
108
|
-
patch.paused_reason = null;
|
|
109
|
-
}
|
|
110
|
-
const { error } = await client().from("plg_conversations_campaigns").update(patch).eq("id", campaignId);
|
|
111
|
-
if (error) throw new Error(error.message);
|
|
112
|
-
},
|
|
113
|
-
/** Um lote. Quem chama repete até `remaining` chegar a zero — o arrendamento
|
|
114
|
-
* de cinco minutos é o que impede duas chamadas de mandarem o mesmo. */
|
|
115
|
-
async dispatch(campaignId, limit) {
|
|
116
|
-
const { data, error } = await client().functions.invoke("conversations-provider", {
|
|
117
|
-
body: { tenantId: tenant(), action: "dispatch", campaignId, limit }
|
|
118
|
-
});
|
|
119
|
-
if (error) {
|
|
120
|
-
const detail = await error.context?.json?.().catch(() => null);
|
|
121
|
-
throw new Error(detail?.message ?? error.message);
|
|
122
|
-
}
|
|
123
|
-
if (data?.error) throw new Error(data.message ?? data.error);
|
|
124
|
-
return data;
|
|
125
|
-
}
|
|
126
|
-
};
|
|
127
|
-
var FN = "conversations-provider";
|
|
128
|
-
function client2() {
|
|
129
|
-
const c = getSupabaseClientOptional();
|
|
130
|
-
if (!c) throw new Error("Supabase n\xE3o inicializado");
|
|
131
|
-
return c;
|
|
132
|
-
}
|
|
133
|
-
async function callFn(action, body = {}) {
|
|
134
|
-
const tenantId = getActiveTenantId();
|
|
135
|
-
if (!tenantId) throw new Error("Nenhuma casa ativa");
|
|
136
|
-
const { data, error } = await client2().functions.invoke(FN, {
|
|
137
|
-
body: { tenantId, action, ...body }
|
|
138
|
-
});
|
|
139
|
-
if (error) {
|
|
140
|
-
const detail = await error.context?.json?.().catch(() => null);
|
|
141
|
-
throw new Error(detail?.message ?? error.message);
|
|
142
|
-
}
|
|
143
|
-
if (data?.error) throw new Error(data.message ?? data.error);
|
|
144
|
-
return data;
|
|
145
|
-
}
|
|
146
|
-
var providersApi = {
|
|
147
|
-
/** O que está configurado nesta casa. Sem segredo. */
|
|
148
|
-
async list() {
|
|
149
|
-
const tenantId = getActiveTenantId();
|
|
150
|
-
if (!tenantId) return [];
|
|
151
|
-
const { data, error } = await client2().from("v_conversations_providers").select("provider, config, capabilities, is_active, status, status_detail, checked_at, has_credential").eq("tenant_id", tenantId);
|
|
152
|
-
if (error) throw new Error(error.message);
|
|
153
|
-
return (data ?? []).map((r) => ({
|
|
154
|
-
provider: r.provider,
|
|
155
|
-
config: r.config ?? {},
|
|
156
|
-
capabilities: r.capabilities ?? {},
|
|
157
|
-
isActive: Boolean(r.is_active),
|
|
158
|
-
status: r.status,
|
|
159
|
-
statusDetail: r.status_detail ?? null,
|
|
160
|
-
checkedAt: r.checked_at ?? null,
|
|
161
|
-
hasCredential: Boolean(r.has_credential)
|
|
162
|
-
}));
|
|
163
|
-
},
|
|
164
|
-
/** Guarda config e, quando vier, a chave. Chave ausente é "não mexe": salvar
|
|
165
|
-
* a URL do Evolution sem redigitar a chave é o gesto comum. */
|
|
166
|
-
async save(provider, config, apiKey) {
|
|
167
|
-
const tenantId = getActiveTenantId();
|
|
168
|
-
if (!tenantId) throw new Error("Nenhuma casa ativa");
|
|
169
|
-
const { error } = await client2().rpc("conversations_set_provider", {
|
|
170
|
-
p_tenant: tenantId,
|
|
171
|
-
p_provider: provider,
|
|
172
|
-
p_config: config,
|
|
173
|
-
p_api_key: apiKey && apiKey.trim() ? apiKey.trim() : null
|
|
174
|
-
});
|
|
175
|
-
if (error) throw new Error(error.message);
|
|
176
|
-
},
|
|
177
|
-
/** A troca. Um ativo por casa, garantido por índice único no banco. */
|
|
178
|
-
async activate(provider) {
|
|
179
|
-
const tenantId = getActiveTenantId();
|
|
180
|
-
if (!tenantId) throw new Error("Nenhuma casa ativa");
|
|
181
|
-
const { error } = await client2().rpc("conversations_activate_provider", {
|
|
182
|
-
p_tenant: tenantId,
|
|
183
|
-
p_provider: provider
|
|
184
|
-
});
|
|
185
|
-
if (error) throw new Error(error.message);
|
|
186
|
-
},
|
|
187
|
-
health: (provider) => callFn("health", { provider }),
|
|
188
|
-
numbers: (provider) => callFn("numbers", { provider }),
|
|
189
|
-
/** O QR do Evolution. O Tyxter recusa com `unsupported`. */
|
|
190
|
-
pair: (provider) => callFn("pair", { provider }),
|
|
191
|
-
templates: (provider) => callFn("templates", { provider }),
|
|
192
|
-
createTemplate: (template, provider) => callFn("create_template", { template, provider }),
|
|
193
|
-
send: (input) => callFn("send", input)
|
|
194
|
-
};
|
|
195
|
-
var STATE_COPY = {
|
|
196
|
-
draft: { label: "rascunho", cls: "bg-muted text-muted-foreground" },
|
|
197
|
-
scheduled: { label: "agendada", cls: "bg-info-soft text-info-soft-foreground" },
|
|
198
|
-
running: { label: "disparando", cls: "bg-success-soft text-success-soft-foreground" },
|
|
199
|
-
paused: { label: "pausada", cls: "bg-warning-soft text-warning-soft-foreground" },
|
|
200
|
-
done: { label: "conclu\xEDda", cls: "bg-muted text-muted-foreground" },
|
|
201
|
-
cancelled: { label: "cancelada", cls: "bg-muted text-muted-foreground" }
|
|
202
|
-
};
|
|
203
|
-
function Bar({ c }) {
|
|
204
|
-
const total = Math.max(1, c.total);
|
|
205
|
-
const seg = (n, cls, title) => n > 0 ? /* @__PURE__ */ jsx("div", { className: cls, style: { width: `${n / total * 100}%` }, title: `${title}: ${n}` }) : null;
|
|
206
|
-
return /* @__PURE__ */ jsxs("div", { className: "flex h-1.5 w-full overflow-hidden rounded-full bg-muted", children: [
|
|
207
|
-
seg(c.replied, "bg-success", "responderam"),
|
|
208
|
-
seg(c.sent - c.replied, "bg-primary", "enviadas"),
|
|
209
|
-
seg(c.failed, "bg-destructive", "falharam"),
|
|
210
|
-
seg(c.skipped, "bg-muted-foreground/40", "puladas")
|
|
211
|
-
] });
|
|
212
|
-
}
|
|
213
|
-
function CampaignsView() {
|
|
214
|
-
const [rows, setRows] = React12__default.useState(null);
|
|
215
|
-
const [templates, setTemplates] = React12__default.useState([]);
|
|
216
|
-
const [providers, setProviders] = React12__default.useState([]);
|
|
217
|
-
const [running, setRunning] = React12__default.useState(null);
|
|
218
|
-
const stop = React12__default.useRef(false);
|
|
219
|
-
const load = React12__default.useCallback(async () => {
|
|
220
|
-
try {
|
|
221
|
-
const [c, t, p] = await Promise.all([
|
|
222
|
-
campaignsApi.list(),
|
|
223
|
-
campaignsApi.templates(),
|
|
224
|
-
providersApi.list()
|
|
225
|
-
]);
|
|
226
|
-
setRows(c);
|
|
227
|
-
setTemplates(t);
|
|
228
|
-
setProviders(p);
|
|
229
|
-
} catch (e) {
|
|
230
|
-
toast.error(e?.message ?? "N\xE3o consegui carregar");
|
|
231
|
-
setRows([]);
|
|
232
|
-
}
|
|
233
|
-
}, []);
|
|
234
|
-
React12__default.useEffect(() => {
|
|
235
|
-
void load();
|
|
236
|
-
}, [load]);
|
|
237
|
-
const active = providers.find((p) => p.isActive);
|
|
238
|
-
async function run(c) {
|
|
239
|
-
stop.current = false;
|
|
240
|
-
setRunning(c.campaignId);
|
|
241
|
-
try {
|
|
242
|
-
if (c.status !== "running") await campaignsApi.setStatus(c.campaignId, "running");
|
|
243
|
-
for (; ; ) {
|
|
244
|
-
if (stop.current) {
|
|
245
|
-
await campaignsApi.setStatus(c.campaignId, "paused");
|
|
246
|
-
break;
|
|
247
|
-
}
|
|
248
|
-
const r = await campaignsApi.dispatch(c.campaignId);
|
|
249
|
-
await load();
|
|
250
|
-
if (r.paused) {
|
|
251
|
-
toast.error(`Pausado: ${r.paused}`);
|
|
252
|
-
break;
|
|
253
|
-
}
|
|
254
|
-
if (r.skipped) {
|
|
255
|
-
toast.error(r.reason ?? "Campanha n\xE3o est\xE1 disparando");
|
|
256
|
-
break;
|
|
257
|
-
}
|
|
258
|
-
if (r.failures?.length) {
|
|
259
|
-
for (const f of r.failures.slice(0, 3)) toast.error(`${f.to}: ${f.message}`);
|
|
260
|
-
}
|
|
261
|
-
if (!r.remaining) {
|
|
262
|
-
toast.success("Campanha conclu\xEDda");
|
|
263
|
-
break;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
} catch (e) {
|
|
267
|
-
toast.error(e?.message ?? "Falhou");
|
|
268
|
-
} finally {
|
|
269
|
-
setRunning(null);
|
|
270
|
-
void load();
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
274
|
-
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center justify-between gap-2", children: /* @__PURE__ */ jsxs("div", { children: [
|
|
275
|
-
/* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold", children: "Campanhas" }),
|
|
276
|
-
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: active ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
277
|
-
"saindo por ",
|
|
278
|
-
/* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: active.provider }),
|
|
279
|
-
active.capabilities?.requires_template && " \xB7 exige template aprovado"
|
|
280
|
-
] }) : "nenhum provedor de WhatsApp ativo \u2014 configure em Ajustes" })
|
|
281
|
-
] }) }),
|
|
282
|
-
rows === null && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 py-8 text-sm text-muted-foreground", children: [
|
|
283
|
-
/* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin" }),
|
|
284
|
-
" carregando\u2026"
|
|
285
|
-
] }),
|
|
286
|
-
rows?.length === 0 && /* @__PURE__ */ jsxs("div", { className: "rounded-card border-2 border-dashed py-10 text-center", children: [
|
|
287
|
-
/* @__PURE__ */ jsx(MessageSquare, { className: "mx-auto h-8 w-8 text-muted-foreground/30" }),
|
|
288
|
-
/* @__PURE__ */ jsx("p", { className: "mt-2 text-sm text-muted-foreground", children: "Nenhuma campanha ainda." }),
|
|
289
|
-
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: 'Crie uma a partir da lista de leads: filtre, selecione e escolha "Enviar WhatsApp".' })
|
|
290
|
-
] }),
|
|
291
|
-
rows?.map((c) => {
|
|
292
|
-
const st = STATE_COPY[c.status] ?? STATE_COPY.draft;
|
|
293
|
-
const busy = running === c.campaignId;
|
|
294
|
-
return /* @__PURE__ */ jsxs("div", { className: "rounded-card border bg-card p-4", "data-testid": `campaign-${c.campaignId}`, children: [
|
|
295
|
-
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
|
|
296
|
-
/* @__PURE__ */ jsx("span", { className: "text-sm font-semibold", children: c.name }),
|
|
297
|
-
/* @__PURE__ */ jsx("span", { className: `rounded-full px-2 py-0.5 text-[10px] font-medium ${st.cls}`, children: st.label }),
|
|
298
|
-
c.provider && /* @__PURE__ */ jsx("span", { className: "text-[10px] text-muted-foreground", children: c.provider }),
|
|
299
|
-
c.pausedReason && /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1 text-[10px] text-warning", children: [
|
|
300
|
-
/* @__PURE__ */ jsx(AlertTriangle, { className: "h-3 w-3" }),
|
|
301
|
-
" ",
|
|
302
|
-
c.pausedReason
|
|
303
|
-
] })
|
|
304
|
-
] }),
|
|
305
|
-
/* @__PURE__ */ jsx("div", { className: "mt-2", children: /* @__PURE__ */ jsx(Bar, { c }) }),
|
|
306
|
-
/* @__PURE__ */ jsxs("div", { className: "mt-2 flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground", children: [
|
|
307
|
-
/* @__PURE__ */ jsxs("span", { children: [
|
|
308
|
-
c.total,
|
|
309
|
-
" destinat\xE1rios"
|
|
310
|
-
] }),
|
|
311
|
-
/* @__PURE__ */ jsxs("span", { children: [
|
|
312
|
-
"enviadas: ",
|
|
313
|
-
/* @__PURE__ */ jsx("span", { className: "text-foreground tabular-nums", children: c.sent })
|
|
314
|
-
] }),
|
|
315
|
-
/* @__PURE__ */ jsxs("span", { children: [
|
|
316
|
-
"responderam: ",
|
|
317
|
-
/* @__PURE__ */ jsx("span", { className: "text-foreground tabular-nums", children: c.replied })
|
|
318
|
-
] }),
|
|
319
|
-
c.failed > 0 && /* @__PURE__ */ jsxs("span", { className: "text-destructive", children: [
|
|
320
|
-
"falharam: ",
|
|
321
|
-
c.failed
|
|
322
|
-
] }),
|
|
323
|
-
c.skipped > 0 && /* @__PURE__ */ jsxs("span", { children: [
|
|
324
|
-
"puladas: ",
|
|
325
|
-
c.skipped
|
|
326
|
-
] }),
|
|
327
|
-
c.queued > 0 && /* @__PURE__ */ jsxs("span", { children: [
|
|
328
|
-
"na fila: ",
|
|
329
|
-
/* @__PURE__ */ jsx("span", { className: "text-foreground tabular-nums", children: c.queued })
|
|
330
|
-
] }),
|
|
331
|
-
c.replyRate != null && /* @__PURE__ */ jsxs("span", { className: c.replyRate < 30 ? "text-warning" : "text-success", children: [
|
|
332
|
-
"resposta: ",
|
|
333
|
-
c.replyRate,
|
|
334
|
-
"%"
|
|
335
|
-
] })
|
|
336
|
-
] }),
|
|
337
|
-
/* @__PURE__ */ jsxs("div", { className: "mt-3 flex gap-2", children: [
|
|
338
|
-
busy ? /* @__PURE__ */ jsxs(Button, { size: "sm", variant: "outline", onClick: () => {
|
|
339
|
-
stop.current = true;
|
|
340
|
-
}, children: [
|
|
341
|
-
/* @__PURE__ */ jsx(Pause, { className: "mr-1 h-3 w-3" }),
|
|
342
|
-
" Pausar"
|
|
343
|
-
] }) : /* @__PURE__ */ jsx(
|
|
344
|
-
Button,
|
|
345
|
-
{
|
|
346
|
-
size: "sm",
|
|
347
|
-
disabled: !active || c.queued === 0 || c.status === "done" || c.status === "cancelled",
|
|
348
|
-
onClick: () => void run(c),
|
|
349
|
-
children: c.status === "paused" ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
350
|
-
/* @__PURE__ */ jsx(Play, { className: "mr-1 h-3 w-3" }),
|
|
351
|
-
" Continuar"
|
|
352
|
-
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
353
|
-
/* @__PURE__ */ jsx(Send, { className: "mr-1 h-3 w-3" }),
|
|
354
|
-
" Disparar"
|
|
355
|
-
] })
|
|
356
|
-
}
|
|
357
|
-
),
|
|
358
|
-
busy && /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1 text-xs text-muted-foreground", children: [
|
|
359
|
-
/* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 animate-spin" }),
|
|
360
|
-
" disparando \u2014 mantenha esta aba aberta"
|
|
361
|
-
] })
|
|
362
|
-
] })
|
|
363
|
-
] }, c.campaignId);
|
|
364
|
-
}),
|
|
365
|
-
templates.length > 0 && /* @__PURE__ */ jsxs("div", { className: "rounded-card border bg-card p-4", children: [
|
|
366
|
-
/* @__PURE__ */ jsxs("p", { className: "text-xs font-medium text-muted-foreground mb-2", children: [
|
|
367
|
-
"Templates (",
|
|
368
|
-
templates.length,
|
|
369
|
-
")"
|
|
370
|
-
] }),
|
|
371
|
-
templates.map((t) => /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-baseline gap-2 py-1 text-xs", children: [
|
|
372
|
-
/* @__PURE__ */ jsx("span", { className: "font-medium", children: t.name }),
|
|
373
|
-
/* @__PURE__ */ jsx("span", { className: t.status === "approved" ? "text-success" : "text-muted-foreground", children: t.status }),
|
|
374
|
-
/* @__PURE__ */ jsx("span", { className: "truncate text-muted-foreground", children: t.body.slice(0, 70) })
|
|
375
|
-
] }, t.id))
|
|
376
|
-
] })
|
|
377
|
-
] });
|
|
378
|
-
}
|
|
379
12
|
|
|
380
13
|
// src/data/tables.ts
|
|
381
14
|
var T = {
|
|
@@ -432,8 +65,6 @@ function isWaitingOnUs(conversation) {
|
|
|
432
65
|
if (conversation.lastMessageDirection) return conversation.lastMessageDirection === "inbound";
|
|
433
66
|
return conversation.unreadCount > 0;
|
|
434
67
|
}
|
|
435
|
-
|
|
436
|
-
// src/components/InboxWatcher.tsx
|
|
437
68
|
function tocar() {
|
|
438
69
|
try {
|
|
439
70
|
const Ctx = window.AudioContext ?? window.webkitAudioContext;
|
|
@@ -483,11 +114,12 @@ function avisarNoSistema(titulo, corpo, tag) {
|
|
|
483
114
|
} catch {
|
|
484
115
|
}
|
|
485
116
|
}
|
|
486
|
-
function InboxWatcher({ label, route = "/conversations"
|
|
117
|
+
function InboxWatcher({ label, route = "/conversations" }) {
|
|
487
118
|
const tenantId = useActiveTenantId();
|
|
488
|
-
const
|
|
489
|
-
const
|
|
490
|
-
|
|
119
|
+
const path = useAdminPath();
|
|
120
|
+
const [naoLidas, setNaoLidas] = React10.useState(0);
|
|
121
|
+
const podeTocar = React10.useRef(false);
|
|
122
|
+
React10.useEffect(() => {
|
|
491
123
|
const liberar = () => {
|
|
492
124
|
podeTocar.current = true;
|
|
493
125
|
void pedirPermissao();
|
|
@@ -499,7 +131,7 @@ function InboxWatcher({ label, route = "/conversations", section = "main", posit
|
|
|
499
131
|
for (const ev of ["pointerdown", "keydown", "touchstart"]) window.removeEventListener(ev, liberar);
|
|
500
132
|
};
|
|
501
133
|
}, []);
|
|
502
|
-
const contar =
|
|
134
|
+
const contar = React10.useCallback(async () => {
|
|
503
135
|
const supabase = getSupabaseClientOptional();
|
|
504
136
|
if (!supabase) return;
|
|
505
137
|
const { data } = await supabase.from(T.conversations).select("id, status, last_message_direction, unread_count").eq("status", "open");
|
|
@@ -511,10 +143,10 @@ function InboxWatcher({ label, route = "/conversations", section = "main", posit
|
|
|
511
143
|
})).length;
|
|
512
144
|
setNaoLidas(n);
|
|
513
145
|
}, []);
|
|
514
|
-
|
|
146
|
+
React10.useEffect(() => {
|
|
515
147
|
void contar();
|
|
516
148
|
}, [contar]);
|
|
517
|
-
|
|
149
|
+
React10.useEffect(() => {
|
|
518
150
|
if (!tenantId) return;
|
|
519
151
|
return assinarChegada(tenantId, ({ conversationId }) => {
|
|
520
152
|
void contar();
|
|
@@ -532,233 +164,28 @@ function InboxWatcher({ label, route = "/conversations", section = "main", posit
|
|
|
532
164
|
})();
|
|
533
165
|
});
|
|
534
166
|
}, [contar, tenantId, label]);
|
|
535
|
-
|
|
536
|
-
return
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
return found.size;
|
|
550
|
-
}
|
|
551
|
-
function TemplatesView() {
|
|
552
|
-
const [mine, setMine] = React12__default.useState(null);
|
|
553
|
-
const [theirs, setTheirs] = React12__default.useState([]);
|
|
554
|
-
const [providers, setProviders] = React12__default.useState([]);
|
|
555
|
-
const [busy, setBusy] = React12__default.useState(null);
|
|
556
|
-
const [draft, setDraft] = React12__default.useState({ name: "", body: "", variables: "" });
|
|
557
|
-
const active = providers.find((p) => p.isActive);
|
|
558
|
-
const supportsTemplates = active?.capabilities?.templates !== false;
|
|
559
|
-
const load = React12__default.useCallback(async () => {
|
|
560
|
-
try {
|
|
561
|
-
const [m, p] = await Promise.all([campaignsApi.templates(), providersApi.list()]);
|
|
562
|
-
setMine(m);
|
|
563
|
-
setProviders(p);
|
|
564
|
-
} catch (e) {
|
|
565
|
-
toast.error(e?.message ?? "N\xE3o consegui carregar");
|
|
566
|
-
setMine([]);
|
|
567
|
-
}
|
|
568
|
-
}, []);
|
|
569
|
-
React12__default.useEffect(() => {
|
|
570
|
-
void load();
|
|
571
|
-
}, [load]);
|
|
572
|
-
const sync = async () => {
|
|
573
|
-
setBusy("sync");
|
|
574
|
-
try {
|
|
575
|
-
const d = await providersApi.templates();
|
|
576
|
-
setTheirs(d.templates);
|
|
577
|
-
toast.success(`${d.templates.length} template(s) no provedor`);
|
|
578
|
-
} catch (e) {
|
|
579
|
-
toast.error(e?.message ?? "Falhou");
|
|
580
|
-
} finally {
|
|
581
|
-
setBusy(null);
|
|
582
|
-
}
|
|
583
|
-
};
|
|
584
|
-
const create = async () => {
|
|
585
|
-
const name = draft.name.trim().toLowerCase().replace(/[^a-z0-9_]/g, "_");
|
|
586
|
-
if (!name || !draft.body.trim()) {
|
|
587
|
-
toast.error("Nome e corpo s\xE3o obrigat\xF3rios");
|
|
588
|
-
return;
|
|
589
|
-
}
|
|
590
|
-
if (draft.body.length > 1024) {
|
|
591
|
-
toast.error(`Corpo com ${draft.body.length} caracteres \u2014 a Meta aceita at\xE9 1024`);
|
|
592
|
-
return;
|
|
593
|
-
}
|
|
594
|
-
setBusy("create");
|
|
595
|
-
try {
|
|
596
|
-
const vars = draft.variables.split(",").map((v) => v.trim()).filter(Boolean);
|
|
597
|
-
await campaignsApi.saveTemplate({ name, body: draft.body, variables: vars });
|
|
598
|
-
if (supportsTemplates) {
|
|
599
|
-
await providersApi.createTemplate({
|
|
600
|
-
name,
|
|
601
|
-
body: draft.body,
|
|
602
|
-
example: vars.length ? vars.map((v) => `exemplo ${v}`) : void 0
|
|
603
|
-
});
|
|
604
|
-
toast.success("Submetido \u2014 a Meta costuma responder em ~24h");
|
|
605
|
-
} else {
|
|
606
|
-
toast.success("Guardado. Este provedor n\xE3o usa templates: o texto sai direto.");
|
|
607
|
-
}
|
|
608
|
-
setDraft({ name: "", body: "", variables: "" });
|
|
609
|
-
await load();
|
|
610
|
-
await sync();
|
|
611
|
-
} catch (e) {
|
|
612
|
-
toast.error(e?.message ?? "Falhou");
|
|
613
|
-
} finally {
|
|
614
|
-
setBusy(null);
|
|
167
|
+
const aberta = path === route || path.startsWith(`${route}/`);
|
|
168
|
+
return /* @__PURE__ */ jsxs(
|
|
169
|
+
"button",
|
|
170
|
+
{
|
|
171
|
+
type: "button",
|
|
172
|
+
onClick: () => adminNavigateTo(route),
|
|
173
|
+
"aria-label": naoLidas > 0 ? `${label} (${naoLidas})` : label,
|
|
174
|
+
title: label,
|
|
175
|
+
"aria-current": aberta ? "page" : void 0,
|
|
176
|
+
className: `fz-tap relative inline-flex shrink-0 items-center justify-center rounded-button p-2 transition-colors hover:bg-accent ${aberta ? "text-foreground" : "text-muted-foreground hover:text-foreground"}`,
|
|
177
|
+
children: [
|
|
178
|
+
/* @__PURE__ */ jsx(MessageCircle, { className: "h-4 w-4" }),
|
|
179
|
+
naoLidas > 0 && /* @__PURE__ */ jsx("span", { className: "absolute -right-0.5 -top-0.5 inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-destructive px-1 text-[10px] font-medium leading-none text-destructive-foreground tabular-nums", children: naoLidas > 9 ? "9+" : naoLidas })
|
|
180
|
+
]
|
|
615
181
|
}
|
|
616
|
-
|
|
617
|
-
const byName = new Map(theirs.map((t) => [t.name, t]));
|
|
618
|
-
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
619
|
-
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [
|
|
620
|
-
/* @__PURE__ */ jsxs("div", { children: [
|
|
621
|
-
/* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold", children: "Templates" }),
|
|
622
|
-
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: supportsTemplates ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
623
|
-
"No ",
|
|
624
|
-
/* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: active?.provider ?? "provedor" }),
|
|
625
|
-
", um template aprovado \xE9 a \xFAnica porta para falar com quem nunca respondeu."
|
|
626
|
-
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
627
|
-
"O ",
|
|
628
|
-
/* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: active?.provider }),
|
|
629
|
-
" n\xE3o usa templates \u2014 o texto sai direto. O que voc\xEA escrever aqui vira o corpo da campanha."
|
|
630
|
-
] }) })
|
|
631
|
-
] }),
|
|
632
|
-
/* @__PURE__ */ jsx(Button, { size: "sm", variant: "outline", disabled: busy === "sync" || !active, onClick: () => void sync(), children: busy === "sync" ? /* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 animate-spin" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
633
|
-
/* @__PURE__ */ jsx(RefreshCw, { className: "mr-1 h-3 w-3" }),
|
|
634
|
-
" Buscar no provedor"
|
|
635
|
-
] }) })
|
|
636
|
-
] }),
|
|
637
|
-
/* @__PURE__ */ jsxs("div", { className: "rounded-card border bg-card p-4", children: [
|
|
638
|
-
/* @__PURE__ */ jsxs("p", { className: "mb-2 flex items-center gap-1.5 text-sm font-medium", children: [
|
|
639
|
-
/* @__PURE__ */ jsx(Plus, { className: "h-3.5 w-3.5" }),
|
|
640
|
-
" Novo template"
|
|
641
|
-
] }),
|
|
642
|
-
/* @__PURE__ */ jsxs("div", { className: "grid gap-2 sm:grid-cols-[1fr_1fr]", children: [
|
|
643
|
-
/* @__PURE__ */ jsxs("label", { className: "block", children: [
|
|
644
|
-
/* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground", children: "Nome (sem espa\xE7os)" }),
|
|
645
|
-
/* @__PURE__ */ jsx(
|
|
646
|
-
Input,
|
|
647
|
-
{
|
|
648
|
-
className: "mt-0.5 h-8 text-sm",
|
|
649
|
-
placeholder: "primeiro_contato_feira",
|
|
650
|
-
value: draft.name,
|
|
651
|
-
onChange: (e) => setDraft({ ...draft, name: e.target.value })
|
|
652
|
-
}
|
|
653
|
-
)
|
|
654
|
-
] }),
|
|
655
|
-
/* @__PURE__ */ jsxs("label", { className: "block", children: [
|
|
656
|
-
/* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground", children: "Vari\xE1veis, em ordem, separadas por v\xEDrgula" }),
|
|
657
|
-
/* @__PURE__ */ jsx(
|
|
658
|
-
Input,
|
|
659
|
-
{
|
|
660
|
-
className: "mt-0.5 h-8 text-sm",
|
|
661
|
-
placeholder: "nome, vendedor, dor",
|
|
662
|
-
value: draft.variables,
|
|
663
|
-
onChange: (e) => setDraft({ ...draft, variables: e.target.value })
|
|
664
|
-
}
|
|
665
|
-
)
|
|
666
|
-
] })
|
|
667
|
-
] }),
|
|
668
|
-
/* @__PURE__ */ jsxs("label", { className: "mt-2 block", children: [
|
|
669
|
-
/* @__PURE__ */ jsxs("span", { className: "text-[11px] text-muted-foreground", children: [
|
|
670
|
-
"Corpo \u2014 use ",
|
|
671
|
-
"{{1}}",
|
|
672
|
-
", ",
|
|
673
|
-
"{{2}}",
|
|
674
|
-
" na ordem das vari\xE1veis"
|
|
675
|
-
] }),
|
|
676
|
-
/* @__PURE__ */ jsx(
|
|
677
|
-
"textarea",
|
|
678
|
-
{
|
|
679
|
-
rows: 5,
|
|
680
|
-
className: "mt-0.5 w-full resize-y rounded-md border border-border bg-background px-3 py-2 text-sm outline-none focus:border-primary",
|
|
681
|
-
placeholder: "Oi {{1}}! Aqui \xE9 {{2}}, do ChefControl \u{1F44B}\n\nA gente se falou no Sal\xE3o Abrasel...",
|
|
682
|
-
value: draft.body,
|
|
683
|
-
onChange: (e) => setDraft({ ...draft, body: e.target.value })
|
|
684
|
-
}
|
|
685
|
-
)
|
|
686
|
-
] }),
|
|
687
|
-
/* @__PURE__ */ jsxs("div", { className: "mt-1 flex flex-wrap items-center gap-3 text-[11px]", children: [
|
|
688
|
-
/* @__PURE__ */ jsxs("span", { className: draft.body.length > 1024 ? "font-medium text-destructive" : "text-muted-foreground", children: [
|
|
689
|
-
draft.body.length,
|
|
690
|
-
"/1024 caracteres"
|
|
691
|
-
] }),
|
|
692
|
-
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
|
|
693
|
-
slots(draft.body),
|
|
694
|
-
" vari\xE1vel(is) no corpo"
|
|
695
|
-
] }),
|
|
696
|
-
slots(draft.body) !== draft.variables.split(",").filter((v) => v.trim()).length && draft.body && /* @__PURE__ */ jsxs("span", { className: "text-warning", children: [
|
|
697
|
-
"\u26A0 o corpo tem ",
|
|
698
|
-
slots(draft.body),
|
|
699
|
-
" e voc\xEA nomeou ",
|
|
700
|
-
draft.variables.split(",").filter((v) => v.trim()).length
|
|
701
|
-
] })
|
|
702
|
-
] }),
|
|
703
|
-
/* @__PURE__ */ jsx(Button, { size: "sm", className: "mt-2", disabled: busy === "create", onClick: () => void create(), children: busy === "create" ? /* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 animate-spin" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
704
|
-
/* @__PURE__ */ jsx(Send, { className: "mr-1 h-3 w-3" }),
|
|
705
|
-
" ",
|
|
706
|
-
supportsTemplates ? "Guardar e submeter" : "Guardar"
|
|
707
|
-
] }) })
|
|
708
|
-
] }),
|
|
709
|
-
/* @__PURE__ */ jsxs("div", { className: "rounded-card border bg-card", children: [
|
|
710
|
-
/* @__PURE__ */ jsxs("p", { className: "border-b px-4 py-2 text-xs font-medium text-muted-foreground", children: [
|
|
711
|
-
"Desta casa ",
|
|
712
|
-
mine ? `(${mine.length})` : ""
|
|
713
|
-
] }),
|
|
714
|
-
mine === null && /* @__PURE__ */ jsx("p", { className: "px-4 py-3 text-xs text-muted-foreground", children: "carregando\u2026" }),
|
|
715
|
-
mine?.length === 0 && /* @__PURE__ */ jsx("p", { className: "px-4 py-3 text-xs text-muted-foreground", children: "Nenhum template ainda." }),
|
|
716
|
-
mine?.map((t) => {
|
|
717
|
-
const remote = byName.get(t.name);
|
|
718
|
-
const st = STATUS_UI[remote?.status ?? t.status] ?? STATUS_UI.draft;
|
|
719
|
-
const Icon = st.icon;
|
|
720
|
-
return /* @__PURE__ */ jsxs("div", { className: "border-b px-4 py-2.5 last:border-b-0", children: [
|
|
721
|
-
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
|
|
722
|
-
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: t.name }),
|
|
723
|
-
/* @__PURE__ */ jsxs("span", { className: `inline-flex items-center gap-1 text-[10px] ${st.cls}`, children: [
|
|
724
|
-
/* @__PURE__ */ jsx(Icon, { className: "h-3 w-3" }),
|
|
725
|
-
" ",
|
|
726
|
-
st.label
|
|
727
|
-
] }),
|
|
728
|
-
t.variables.length > 0 && /* @__PURE__ */ jsx("span", { className: "text-[10px] text-muted-foreground", children: t.variables.join(" \xB7 ") }),
|
|
729
|
-
supportsTemplates && !remote && /* @__PURE__ */ jsx("span", { className: "text-[10px] text-warning", children: "n\xE3o est\xE1 no provedor" })
|
|
730
|
-
] }),
|
|
731
|
-
/* @__PURE__ */ jsx("p", { className: "mt-0.5 whitespace-pre-wrap text-xs text-muted-foreground", children: t.body }),
|
|
732
|
-
remote?.rejectionReason && /* @__PURE__ */ jsxs("p", { className: "mt-1 text-[11px] text-destructive", children: [
|
|
733
|
-
"Recusado: ",
|
|
734
|
-
remote.rejectionReason
|
|
735
|
-
] })
|
|
736
|
-
] }, t.id);
|
|
737
|
-
})
|
|
738
|
-
] }),
|
|
739
|
-
theirs.filter((t) => !(mine ?? []).some((m) => m.name === t.name)).length > 0 && /* @__PURE__ */ jsxs("div", { className: "rounded-card border bg-card", children: [
|
|
740
|
-
/* @__PURE__ */ jsx("p", { className: "border-b px-4 py-2 text-xs font-medium text-muted-foreground", children: "No provedor, sem par nesta casa \u2014 criados por fora" }),
|
|
741
|
-
theirs.filter((t) => !(mine ?? []).some((m) => m.name === t.name)).map((t) => {
|
|
742
|
-
const st = STATUS_UI[t.status] ?? STATUS_UI.draft;
|
|
743
|
-
const Icon = st.icon;
|
|
744
|
-
return /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-baseline gap-2 border-b px-4 py-2 text-xs last:border-b-0", children: [
|
|
745
|
-
/* @__PURE__ */ jsx("span", { className: "font-medium", children: t.name }),
|
|
746
|
-
/* @__PURE__ */ jsxs("span", { className: `inline-flex items-center gap-1 text-[10px] ${st.cls}`, children: [
|
|
747
|
-
/* @__PURE__ */ jsx(Icon, { className: "h-3 w-3" }),
|
|
748
|
-
" ",
|
|
749
|
-
st.label
|
|
750
|
-
] }),
|
|
751
|
-
/* @__PURE__ */ jsx("span", { className: "truncate text-muted-foreground", children: t.body.slice(0, 80) })
|
|
752
|
-
] }, t.id);
|
|
753
|
-
})
|
|
754
|
-
] })
|
|
755
|
-
] });
|
|
182
|
+
);
|
|
756
183
|
}
|
|
757
184
|
var DEFAULT_CONVERSATIONS_CONFIG = {
|
|
758
185
|
contactKind: "contact"
|
|
759
186
|
};
|
|
760
|
-
var StoreContext =
|
|
761
|
-
var ConfigContext =
|
|
187
|
+
var StoreContext = React10__default.createContext(null);
|
|
188
|
+
var ConfigContext = React10__default.createContext(DEFAULT_CONVERSATIONS_CONFIG);
|
|
762
189
|
function ConversationsContextProvider({
|
|
763
190
|
store: store2,
|
|
764
191
|
config = DEFAULT_CONVERSATIONS_CONFIG,
|
|
@@ -767,12 +194,12 @@ function ConversationsContextProvider({
|
|
|
767
194
|
return /* @__PURE__ */ jsx(StoreContext.Provider, { value: store2, children: /* @__PURE__ */ jsx(ConfigContext.Provider, { value: config, children }) });
|
|
768
195
|
}
|
|
769
196
|
function useConversationsStore(selector) {
|
|
770
|
-
const store2 =
|
|
197
|
+
const store2 = React10__default.useContext(StoreContext);
|
|
771
198
|
if (!store2) throw new Error("useConversationsStore must be used within ConversationsPage");
|
|
772
199
|
return useStore(store2, selector);
|
|
773
200
|
}
|
|
774
201
|
function useConversationsConfig() {
|
|
775
|
-
return
|
|
202
|
+
return React10__default.useContext(ConfigContext);
|
|
776
203
|
}
|
|
777
204
|
var CHANNEL_ICON = {
|
|
778
205
|
sms: Phone,
|
|
@@ -789,10 +216,10 @@ var CHANNEL_ACCENT = {
|
|
|
789
216
|
webchat: { color: "#f59e0b", badge: "bg-[#f59e0b]/12 text-[#b45309] dark:text-[#fcd34d]" }
|
|
790
217
|
};
|
|
791
218
|
function useMediaQuery(query) {
|
|
792
|
-
const [matches, setMatches] =
|
|
219
|
+
const [matches, setMatches] = React10__default.useState(
|
|
793
220
|
() => typeof window !== "undefined" && window.matchMedia(query).matches
|
|
794
221
|
);
|
|
795
|
-
|
|
222
|
+
React10__default.useEffect(() => {
|
|
796
223
|
if (typeof window === "undefined") return;
|
|
797
224
|
const mql = window.matchMedia(query);
|
|
798
225
|
const onChange = () => setMatches(mql.matches);
|
|
@@ -881,7 +308,7 @@ function ConversationList({ className }) {
|
|
|
881
308
|
setLeadOwnerFilter,
|
|
882
309
|
setSearch
|
|
883
310
|
} = useConversationsStore((s) => s);
|
|
884
|
-
const donos =
|
|
311
|
+
const donos = React10__default.useMemo(() => {
|
|
885
312
|
const vistos = /* @__PURE__ */ new Map();
|
|
886
313
|
for (const c of conversations) {
|
|
887
314
|
if (c.leadOwnerId) vistos.set(c.leadOwnerId, c.leadOwnerName ?? c.leadOwnerId);
|
|
@@ -1015,10 +442,78 @@ function ConversationList({ className }) {
|
|
|
1015
442
|
] })
|
|
1016
443
|
] });
|
|
1017
444
|
}
|
|
445
|
+
var FN = "conversations-provider";
|
|
446
|
+
function client() {
|
|
447
|
+
const c = getSupabaseClientOptional();
|
|
448
|
+
if (!c) throw new Error("Supabase n\xE3o inicializado");
|
|
449
|
+
return c;
|
|
450
|
+
}
|
|
451
|
+
async function callFn(action, body = {}) {
|
|
452
|
+
const tenantId = getActiveTenantId();
|
|
453
|
+
if (!tenantId) throw new Error("Nenhuma casa ativa");
|
|
454
|
+
const { data, error } = await client().functions.invoke(FN, {
|
|
455
|
+
body: { tenantId, action, ...body }
|
|
456
|
+
});
|
|
457
|
+
if (error) {
|
|
458
|
+
const detail = await error.context?.json?.().catch(() => null);
|
|
459
|
+
throw new Error(detail?.message ?? error.message);
|
|
460
|
+
}
|
|
461
|
+
if (data?.error) throw new Error(data.message ?? data.error);
|
|
462
|
+
return data;
|
|
463
|
+
}
|
|
464
|
+
var providersApi = {
|
|
465
|
+
/** O que está configurado nesta casa. Sem segredo. */
|
|
466
|
+
async list() {
|
|
467
|
+
const tenantId = getActiveTenantId();
|
|
468
|
+
if (!tenantId) return [];
|
|
469
|
+
const { data, error } = await client().from("v_conversations_providers").select("provider, config, capabilities, is_active, status, status_detail, checked_at, has_credential").eq("tenant_id", tenantId);
|
|
470
|
+
if (error) throw new Error(error.message);
|
|
471
|
+
return (data ?? []).map((r) => ({
|
|
472
|
+
provider: r.provider,
|
|
473
|
+
config: r.config ?? {},
|
|
474
|
+
capabilities: r.capabilities ?? {},
|
|
475
|
+
isActive: Boolean(r.is_active),
|
|
476
|
+
status: r.status,
|
|
477
|
+
statusDetail: r.status_detail ?? null,
|
|
478
|
+
checkedAt: r.checked_at ?? null,
|
|
479
|
+
hasCredential: Boolean(r.has_credential)
|
|
480
|
+
}));
|
|
481
|
+
},
|
|
482
|
+
/** Guarda config e, quando vier, a chave. Chave ausente é "não mexe": salvar
|
|
483
|
+
* a URL do Evolution sem redigitar a chave é o gesto comum. */
|
|
484
|
+
async save(provider, config, apiKey) {
|
|
485
|
+
const tenantId = getActiveTenantId();
|
|
486
|
+
if (!tenantId) throw new Error("Nenhuma casa ativa");
|
|
487
|
+
const { error } = await client().rpc("conversations_set_provider", {
|
|
488
|
+
p_tenant: tenantId,
|
|
489
|
+
p_provider: provider,
|
|
490
|
+
p_config: config,
|
|
491
|
+
p_api_key: apiKey && apiKey.trim() ? apiKey.trim() : null
|
|
492
|
+
});
|
|
493
|
+
if (error) throw new Error(error.message);
|
|
494
|
+
},
|
|
495
|
+
/** A troca. Um ativo por casa, garantido por índice único no banco. */
|
|
496
|
+
async activate(provider) {
|
|
497
|
+
const tenantId = getActiveTenantId();
|
|
498
|
+
if (!tenantId) throw new Error("Nenhuma casa ativa");
|
|
499
|
+
const { error } = await client().rpc("conversations_activate_provider", {
|
|
500
|
+
p_tenant: tenantId,
|
|
501
|
+
p_provider: provider
|
|
502
|
+
});
|
|
503
|
+
if (error) throw new Error(error.message);
|
|
504
|
+
},
|
|
505
|
+
health: (provider) => callFn("health", { provider }),
|
|
506
|
+
numbers: (provider) => callFn("numbers", { provider }),
|
|
507
|
+
/** O QR do Evolution. O Tyxter recusa com `unsupported`. */
|
|
508
|
+
pair: (provider) => callFn("pair", { provider }),
|
|
509
|
+
templates: (provider) => callFn("templates", { provider }),
|
|
510
|
+
createTemplate: (template, provider) => callFn("create_template", { template, provider }),
|
|
511
|
+
send: (input) => callFn("send", input)
|
|
512
|
+
};
|
|
1018
513
|
function useSendWindow(conversationId) {
|
|
1019
|
-
const [row, setRow] =
|
|
1020
|
-
const [providers, setProviders] =
|
|
1021
|
-
|
|
514
|
+
const [row, setRow] = React10__default.useState(null);
|
|
515
|
+
const [providers, setProviders] = React10__default.useState(null);
|
|
516
|
+
React10__default.useEffect(() => {
|
|
1022
517
|
let alive = true;
|
|
1023
518
|
providersApi.list().then((p) => {
|
|
1024
519
|
if (alive) setProviders(p);
|
|
@@ -1029,7 +524,7 @@ function useSendWindow(conversationId) {
|
|
|
1029
524
|
alive = false;
|
|
1030
525
|
};
|
|
1031
526
|
}, []);
|
|
1032
|
-
|
|
527
|
+
React10__default.useEffect(() => {
|
|
1033
528
|
let alive = true;
|
|
1034
529
|
if (!conversationId) {
|
|
1035
530
|
setRow(null);
|
|
@@ -1050,7 +545,7 @@ function useSendWindow(conversationId) {
|
|
|
1050
545
|
};
|
|
1051
546
|
}, [conversationId]);
|
|
1052
547
|
const provider = (providers ?? []).find((p) => p.isActive);
|
|
1053
|
-
const mode =
|
|
548
|
+
const mode = React10__default.useMemo(() => {
|
|
1054
549
|
if (!provider) return { kind: "blocked", reason: "no_provider" };
|
|
1055
550
|
if (!provider.capabilities?.requires_template) {
|
|
1056
551
|
return { kind: "free", until: null, minutesLeft: null };
|
|
@@ -1125,10 +620,10 @@ function DeliveryMark({ m }) {
|
|
|
1125
620
|
] });
|
|
1126
621
|
}
|
|
1127
622
|
function MessageMenu({ m }) {
|
|
1128
|
-
const [open, setOpen] =
|
|
1129
|
-
const [detail, setDetail] =
|
|
1130
|
-
const [loading, setLoading] =
|
|
1131
|
-
const [err, setErr] =
|
|
623
|
+
const [open, setOpen] = React10__default.useState(false);
|
|
624
|
+
const [detail, setDetail] = React10__default.useState(null);
|
|
625
|
+
const [loading, setLoading] = React10__default.useState(false);
|
|
626
|
+
const [err, setErr] = React10__default.useState(null);
|
|
1132
627
|
async function load() {
|
|
1133
628
|
setOpen(true);
|
|
1134
629
|
if (!m.providerMessageId) return;
|
|
@@ -1204,9 +699,9 @@ function temArquivo(m) {
|
|
|
1204
699
|
return !!m.mediaPath && COM_ARQUIVO.has(String(m.messageType));
|
|
1205
700
|
}
|
|
1206
701
|
function useUrlAssinada(m, visivel) {
|
|
1207
|
-
const [url, setUrl] =
|
|
1208
|
-
const [erro, setErro] =
|
|
1209
|
-
|
|
702
|
+
const [url, setUrl] = React10.useState(null);
|
|
703
|
+
const [erro, setErro] = React10.useState(false);
|
|
704
|
+
React10.useEffect(() => {
|
|
1210
705
|
if (!visivel || url || erro || !temArquivo(m)) return;
|
|
1211
706
|
let vivo = true;
|
|
1212
707
|
(async () => {
|
|
@@ -1241,10 +736,10 @@ function duracao(seg) {
|
|
|
1241
736
|
return `${m}:${String(s).padStart(2, "0")}`;
|
|
1242
737
|
}
|
|
1243
738
|
function MediaBubble({ m, outbound }) {
|
|
1244
|
-
const ref =
|
|
1245
|
-
const [visivel, setVisivel] =
|
|
739
|
+
const ref = React10.useRef(null);
|
|
740
|
+
const [visivel, setVisivel] = React10.useState(false);
|
|
1246
741
|
const { url, erro } = useUrlAssinada(m, visivel);
|
|
1247
|
-
|
|
742
|
+
React10.useEffect(() => {
|
|
1248
743
|
const el = ref.current;
|
|
1249
744
|
if (!el || typeof IntersectionObserver === "undefined") {
|
|
1250
745
|
setVisivel(true);
|
|
@@ -1328,7 +823,7 @@ function MediaBubble({ m, outbound }) {
|
|
|
1328
823
|
] });
|
|
1329
824
|
}
|
|
1330
825
|
function ContactCard({ contacts, outbound }) {
|
|
1331
|
-
const [copied, setCopied] =
|
|
826
|
+
const [copied, setCopied] = React10__default.useState(null);
|
|
1332
827
|
const copy = (value) => {
|
|
1333
828
|
void navigator.clipboard?.writeText(value);
|
|
1334
829
|
setCopied(value);
|
|
@@ -1393,14 +888,14 @@ function buildRows(messages) {
|
|
|
1393
888
|
function MessageThread({ selected, onTogglePanel, panelOpen, onBack, className }) {
|
|
1394
889
|
const t = useTranslation();
|
|
1395
890
|
const { messages, sending, send, setStatus, refreshMessages } = useConversationsStore((s) => s);
|
|
1396
|
-
const [draft, setDraft] =
|
|
891
|
+
const [draft, setDraft] = React10__default.useState("");
|
|
1397
892
|
const win = useSendWindow(selected.id);
|
|
1398
893
|
const canType = win.mode.kind === "free";
|
|
1399
|
-
const threadRef =
|
|
1400
|
-
|
|
894
|
+
const threadRef = React10__default.useRef(null);
|
|
895
|
+
React10__default.useEffect(() => {
|
|
1401
896
|
threadRef.current?.scrollTo({ top: threadRef.current.scrollHeight, behavior: "smooth" });
|
|
1402
897
|
}, [messages.length, selected.id]);
|
|
1403
|
-
|
|
898
|
+
React10__default.useEffect(() => {
|
|
1404
899
|
if (!selected.id) return;
|
|
1405
900
|
let timer = null;
|
|
1406
901
|
const start = () => {
|
|
@@ -1429,7 +924,7 @@ function MessageThread({ selected, onTogglePanel, panelOpen, onBack, className }
|
|
|
1429
924
|
await send(body);
|
|
1430
925
|
}
|
|
1431
926
|
const accent = CHANNEL_ACCENT[selected.channel];
|
|
1432
|
-
const rows =
|
|
927
|
+
const rows = React10__default.useMemo(() => buildRows(messages), [messages]);
|
|
1433
928
|
return /* @__PURE__ */ jsxs("section", { className: cn("flex min-w-0 flex-1 flex-col bg-muted/20", className), children: [
|
|
1434
929
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 border-b border-border bg-card px-3 py-2.5 md:px-5", children: [
|
|
1435
930
|
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-2 md:gap-3", children: [
|
|
@@ -1541,8 +1036,8 @@ var TEMP = {
|
|
|
1541
1036
|
cold: { icon: Snowflake, cls: "bg-info-soft text-info-soft-foreground", label: "Frio" }
|
|
1542
1037
|
};
|
|
1543
1038
|
function LeadCard({ personId, handle }) {
|
|
1544
|
-
const [lead, setLead] =
|
|
1545
|
-
|
|
1039
|
+
const [lead, setLead] = React10__default.useState(void 0);
|
|
1040
|
+
React10__default.useEffect(() => {
|
|
1546
1041
|
let alive = true;
|
|
1547
1042
|
const sb = getSupabaseClientOptional();
|
|
1548
1043
|
const tenantId = getActiveTenantId();
|
|
@@ -1709,14 +1204,14 @@ function NewConversationPanel({
|
|
|
1709
1204
|
const create = useConversationsStore((s) => s.create);
|
|
1710
1205
|
const config = useConversationsConfig();
|
|
1711
1206
|
const guardConversations = useLimitGuard("conversations_month");
|
|
1712
|
-
const [channel, setChannel] =
|
|
1713
|
-
const [contact, setContact] =
|
|
1714
|
-
const [typedHandle, setTypedHandle] =
|
|
1715
|
-
const [creatingContact, setCreatingContact] =
|
|
1716
|
-
const [firstMessage, setFirstMessage] =
|
|
1717
|
-
const [submitting, setSubmitting] =
|
|
1718
|
-
const [pickerKey, setPickerKey] =
|
|
1719
|
-
|
|
1207
|
+
const [channel, setChannel] = React10__default.useState("whatsapp");
|
|
1208
|
+
const [contact, setContact] = React10__default.useState(null);
|
|
1209
|
+
const [typedHandle, setTypedHandle] = React10__default.useState("");
|
|
1210
|
+
const [creatingContact, setCreatingContact] = React10__default.useState(false);
|
|
1211
|
+
const [firstMessage, setFirstMessage] = React10__default.useState("");
|
|
1212
|
+
const [submitting, setSubmitting] = React10__default.useState(false);
|
|
1213
|
+
const [pickerKey, setPickerKey] = React10__default.useState(0);
|
|
1214
|
+
React10__default.useEffect(() => {
|
|
1720
1215
|
if (open) {
|
|
1721
1216
|
setChannel("whatsapp");
|
|
1722
1217
|
setContact(null);
|
|
@@ -1831,9 +1326,9 @@ function useCaixaAoVivo() {
|
|
|
1831
1326
|
const load = useConversationsStore((s) => s.load);
|
|
1832
1327
|
const refreshMessages = useConversationsStore((s) => s.refreshMessages);
|
|
1833
1328
|
const selectedId = useConversationsStore((s) => s.selectedId);
|
|
1834
|
-
const abertoRef =
|
|
1329
|
+
const abertoRef = React10.useRef(selectedId);
|
|
1835
1330
|
abertoRef.current = selectedId;
|
|
1836
|
-
|
|
1331
|
+
React10.useEffect(() => {
|
|
1837
1332
|
if (!tenantId) return;
|
|
1838
1333
|
return assinarChegada(tenantId, ({ conversationId }) => {
|
|
1839
1334
|
void load();
|
|
@@ -1849,13 +1344,13 @@ function InboxView() {
|
|
|
1849
1344
|
useCaixaAoVivo();
|
|
1850
1345
|
const isWidePanel = useMediaQuery("(min-width: 1280px)");
|
|
1851
1346
|
const railOpen = useRightRailStore((s) => s.open);
|
|
1852
|
-
const [panelOpen, setPanelOpen] =
|
|
1853
|
-
const [newOpen, setNewOpen] =
|
|
1347
|
+
const [panelOpen, setPanelOpen] = React10__default.useState(false);
|
|
1348
|
+
const [newOpen, setNewOpen] = React10__default.useState(false);
|
|
1854
1349
|
const selected = conversations.find((c) => c.id === selectedId) ?? null;
|
|
1855
|
-
|
|
1350
|
+
React10__default.useEffect(() => {
|
|
1856
1351
|
setPanelOpen(isWidePanel && !railOpen);
|
|
1857
1352
|
}, [isWidePanel, railOpen]);
|
|
1858
|
-
const newActions =
|
|
1353
|
+
const newActions = React10__default.useMemo(() => [{
|
|
1859
1354
|
id: "new-conversation",
|
|
1860
1355
|
label: t("conversations.list.new"),
|
|
1861
1356
|
icon: SquarePen,
|
|
@@ -1924,7 +1419,7 @@ function requestedContact() {
|
|
|
1924
1419
|
}
|
|
1925
1420
|
}
|
|
1926
1421
|
function ConversationsPage({ store: store2, config }) {
|
|
1927
|
-
|
|
1422
|
+
React10__default.useEffect(() => {
|
|
1928
1423
|
const wanted = requestedContact();
|
|
1929
1424
|
if (!wanted) {
|
|
1930
1425
|
void store2.getState().load();
|
|
@@ -3013,22 +2508,22 @@ function NumberRow({ n }) {
|
|
|
3013
2508
|
] });
|
|
3014
2509
|
}
|
|
3015
2510
|
function WhatsAppProviders() {
|
|
3016
|
-
const [rows, setRows] =
|
|
3017
|
-
const [draft, setDraft] =
|
|
3018
|
-
const [busy, setBusy] =
|
|
3019
|
-
const [numbers, setNumbers] =
|
|
3020
|
-
const [qr, setQr] =
|
|
3021
|
-
const load =
|
|
2511
|
+
const [rows, setRows] = React10__default.useState(null);
|
|
2512
|
+
const [draft, setDraft] = React10__default.useState({});
|
|
2513
|
+
const [busy, setBusy] = React10__default.useState(null);
|
|
2514
|
+
const [numbers, setNumbers] = React10__default.useState({});
|
|
2515
|
+
const [qr, setQr] = React10__default.useState(null);
|
|
2516
|
+
const load = React10__default.useCallback(async () => {
|
|
3022
2517
|
try {
|
|
3023
2518
|
setRows(await providersApi.list());
|
|
3024
2519
|
} catch {
|
|
3025
2520
|
setRows([]);
|
|
3026
2521
|
}
|
|
3027
2522
|
}, []);
|
|
3028
|
-
|
|
2523
|
+
React10__default.useEffect(() => {
|
|
3029
2524
|
void load();
|
|
3030
2525
|
}, [load]);
|
|
3031
|
-
const byId =
|
|
2526
|
+
const byId = React10__default.useMemo(
|
|
3032
2527
|
() => Object.fromEntries((rows ?? []).map((r) => [r.provider, r])),
|
|
3033
2528
|
[rows]
|
|
3034
2529
|
);
|
|
@@ -3263,9 +2758,9 @@ function ChannelRow({ account }) {
|
|
|
3263
2758
|
}
|
|
3264
2759
|
function ConversationsGeneralSettings() {
|
|
3265
2760
|
const t = useTranslation();
|
|
3266
|
-
const [channels, setChannels] =
|
|
3267
|
-
const [failed, setFailed] =
|
|
3268
|
-
|
|
2761
|
+
const [channels, setChannels] = React10.useState(null);
|
|
2762
|
+
const [failed, setFailed] = React10.useState(false);
|
|
2763
|
+
React10.useEffect(() => {
|
|
3269
2764
|
let cancelled = false;
|
|
3270
2765
|
listMessagingChannels().then((rows) => {
|
|
3271
2766
|
if (!cancelled) setChannels(rows);
|
|
@@ -3364,7 +2859,7 @@ function buildConversationsOnboarding() {
|
|
|
3364
2859
|
};
|
|
3365
2860
|
}
|
|
3366
2861
|
var TYXTER_NUMBER_CLAIM_FUNCTION = "tyxter-number-claim";
|
|
3367
|
-
function
|
|
2862
|
+
function client2() {
|
|
3368
2863
|
const supabase = getSupabaseClientOptional();
|
|
3369
2864
|
if (!supabase) throw new Error("Sem conex\xE3o com o banco para falar com a Tyxter.");
|
|
3370
2865
|
return supabase;
|
|
@@ -3376,7 +2871,7 @@ async function invoke(body) {
|
|
|
3376
2871
|
} catch {
|
|
3377
2872
|
headers = void 0;
|
|
3378
2873
|
}
|
|
3379
|
-
const { data, error } = await
|
|
2874
|
+
const { data, error } = await client2().functions.invoke(TYXTER_NUMBER_CLAIM_FUNCTION, {
|
|
3380
2875
|
body,
|
|
3381
2876
|
...headers ? { headers } : {}
|
|
3382
2877
|
});
|
|
@@ -5803,6 +5298,121 @@ var MIGRATIONS = [
|
|
|
5803
5298
|
{ id: "012_a_rich_message_keeps_its_shape", sql: MIGRATION_012_A_RICH_MESSAGE_KEEPS_ITS_SHAPE },
|
|
5804
5299
|
{ id: "013_a_midia_fica_conosco", sql: MIGRATION_013_A_MIDIA_FICA_CONOSCO }
|
|
5805
5300
|
];
|
|
5301
|
+
function client3() {
|
|
5302
|
+
const c = getSupabaseClientOptional();
|
|
5303
|
+
if (!c) throw new Error("Supabase n\xE3o inicializado");
|
|
5304
|
+
return c;
|
|
5305
|
+
}
|
|
5306
|
+
function tenant() {
|
|
5307
|
+
const t = getActiveTenantId();
|
|
5308
|
+
if (!t) throw new Error("Nenhuma casa ativa");
|
|
5309
|
+
return t;
|
|
5310
|
+
}
|
|
5311
|
+
var campaignsApi = {
|
|
5312
|
+
async templates() {
|
|
5313
|
+
const { data, error } = await client3().from("plg_conversations_templates").select("id, name, language, category, body, variables, provider, status, status_detail").eq("tenant_id", tenant()).order("name");
|
|
5314
|
+
if (error) throw new Error(error.message);
|
|
5315
|
+
return (data ?? []).map((r) => ({
|
|
5316
|
+
id: r.id,
|
|
5317
|
+
name: r.name,
|
|
5318
|
+
language: r.language,
|
|
5319
|
+
category: r.category,
|
|
5320
|
+
body: r.body,
|
|
5321
|
+
variables: r.variables ?? [],
|
|
5322
|
+
provider: r.provider,
|
|
5323
|
+
status: r.status,
|
|
5324
|
+
statusDetail: r.status_detail ?? null
|
|
5325
|
+
}));
|
|
5326
|
+
},
|
|
5327
|
+
async saveTemplate(t) {
|
|
5328
|
+
const row = {
|
|
5329
|
+
tenant_id: tenant(),
|
|
5330
|
+
name: t.name,
|
|
5331
|
+
body: t.body,
|
|
5332
|
+
variables: t.variables ?? [],
|
|
5333
|
+
language: t.language ?? "pt_BR",
|
|
5334
|
+
category: t.category ?? "MARKETING",
|
|
5335
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
5336
|
+
};
|
|
5337
|
+
const q = t.id ? client3().from("plg_conversations_templates").update(row).eq("id", t.id) : client3().from("plg_conversations_templates").insert(row);
|
|
5338
|
+
const { error } = await q;
|
|
5339
|
+
if (error) throw new Error(error.message);
|
|
5340
|
+
},
|
|
5341
|
+
async list() {
|
|
5342
|
+
const { data, error } = await client3().from("v_conversations_campaign_progress").select("*").eq("tenant_id", tenant());
|
|
5343
|
+
if (error) throw new Error(error.message);
|
|
5344
|
+
return (data ?? []).map((r) => ({
|
|
5345
|
+
campaignId: r.campaign_id,
|
|
5346
|
+
name: r.name,
|
|
5347
|
+
status: r.status,
|
|
5348
|
+
pausedReason: r.paused_reason ?? null,
|
|
5349
|
+
provider: r.provider,
|
|
5350
|
+
total: Number(r.total ?? 0),
|
|
5351
|
+
queued: Number(r.queued ?? 0),
|
|
5352
|
+
sending: Number(r.sending ?? 0),
|
|
5353
|
+
sent: Number(r.sent ?? 0),
|
|
5354
|
+
delivered: Number(r.delivered ?? 0),
|
|
5355
|
+
read: Number(r.read ?? 0),
|
|
5356
|
+
replied: Number(r.replied ?? 0),
|
|
5357
|
+
failed: Number(r.failed ?? 0),
|
|
5358
|
+
skipped: Number(r.skipped ?? 0),
|
|
5359
|
+
replyRate: r.reply_rate == null ? null : Number(r.reply_rate),
|
|
5360
|
+
firstSentAt: r.first_sent_at ?? null,
|
|
5361
|
+
lastSentAt: r.last_sent_at ?? null
|
|
5362
|
+
}));
|
|
5363
|
+
},
|
|
5364
|
+
/** Cria a campanha e enfileira os destinatários.
|
|
5365
|
+
*
|
|
5366
|
+
* `ON CONFLICT DO NOTHING` via upsert: o índice único (campanha, telefone,
|
|
5367
|
+
* rodada) é quem impede o disparo duplo, e não uma checagem aqui. Dois
|
|
5368
|
+
* cliques no botão enfileiram uma vez. */
|
|
5369
|
+
async create(input) {
|
|
5370
|
+
const tenantId = tenant();
|
|
5371
|
+
const { data: camp, error } = await client3().from("plg_conversations_campaigns").insert({
|
|
5372
|
+
tenant_id: tenantId,
|
|
5373
|
+
name: input.name,
|
|
5374
|
+
template_id: input.templateId ?? null,
|
|
5375
|
+
provider: input.provider ?? null,
|
|
5376
|
+
channel_id: input.channelId ?? null,
|
|
5377
|
+
status: "draft"
|
|
5378
|
+
}).select("id").single();
|
|
5379
|
+
if (error) throw new Error(error.message);
|
|
5380
|
+
const rows = input.targets.map((t) => ({
|
|
5381
|
+
tenant_id: tenantId,
|
|
5382
|
+
campaign_id: camp.id,
|
|
5383
|
+
phone_e164: String(t.phone).replace(/\D/g, ""),
|
|
5384
|
+
person_id: t.personId ?? null,
|
|
5385
|
+
variables: t.variables ?? {}
|
|
5386
|
+
})).filter((r) => r.phone_e164.length >= 10);
|
|
5387
|
+
for (let i = 0; i < rows.length; i += 200) {
|
|
5388
|
+
const { error: e } = await client3().from("plg_conversations_campaign_targets").upsert(rows.slice(i, i + 200), { onConflict: "campaign_id,phone_e164,rotation", ignoreDuplicates: true });
|
|
5389
|
+
if (e) throw new Error(e.message);
|
|
5390
|
+
}
|
|
5391
|
+
return camp.id;
|
|
5392
|
+
},
|
|
5393
|
+
async setStatus(campaignId, status) {
|
|
5394
|
+
const patch = { status, updated_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
5395
|
+
if (status === "running") {
|
|
5396
|
+
patch.started_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
5397
|
+
patch.paused_reason = null;
|
|
5398
|
+
}
|
|
5399
|
+
const { error } = await client3().from("plg_conversations_campaigns").update(patch).eq("id", campaignId);
|
|
5400
|
+
if (error) throw new Error(error.message);
|
|
5401
|
+
},
|
|
5402
|
+
/** Um lote. Quem chama repete até `remaining` chegar a zero — o arrendamento
|
|
5403
|
+
* de cinco minutos é o que impede duas chamadas de mandarem o mesmo. */
|
|
5404
|
+
async dispatch(campaignId, limit) {
|
|
5405
|
+
const { data, error } = await client3().functions.invoke("conversations-provider", {
|
|
5406
|
+
body: { tenantId: tenant(), action: "dispatch", campaignId, limit }
|
|
5407
|
+
});
|
|
5408
|
+
if (error) {
|
|
5409
|
+
const detail = await error.context?.json?.().catch(() => null);
|
|
5410
|
+
throw new Error(detail?.message ?? error.message);
|
|
5411
|
+
}
|
|
5412
|
+
if (data?.error) throw new Error(data.message ?? data.error);
|
|
5413
|
+
return data;
|
|
5414
|
+
}
|
|
5415
|
+
};
|
|
5806
5416
|
|
|
5807
5417
|
// src/index.ts
|
|
5808
5418
|
function createSafeProvider() {
|
|
@@ -5835,12 +5445,8 @@ function createConversationsPlugin(options) {
|
|
|
5835
5445
|
contactEntityDef: options?.contactEntityDef
|
|
5836
5446
|
};
|
|
5837
5447
|
const dashboardWidgets = createConversationsDashboardWidgets({ store: store2, config });
|
|
5838
|
-
const PageComponent = () =>
|
|
5448
|
+
const PageComponent = () => React10__default.createElement(ConversationsPage, { store: store2, config });
|
|
5839
5449
|
PageComponent.displayName = "ConversationsPage";
|
|
5840
|
-
const CampaignsPageComponent = () => React12__default.createElement(CampaignsView);
|
|
5841
|
-
CampaignsPageComponent.displayName = "ConversationsCampaignsPage";
|
|
5842
|
-
const TemplatesPageComponent = () => React12__default.createElement(TemplatesView);
|
|
5843
|
-
TemplatesPageComponent.displayName = "ConversationsTemplatesPage";
|
|
5844
5450
|
return {
|
|
5845
5451
|
id: "conversations",
|
|
5846
5452
|
defaultAgentRole: "frontdesk",
|
|
@@ -5856,49 +5462,24 @@ function createConversationsPlugin(options) {
|
|
|
5856
5462
|
declaredLimits: [
|
|
5857
5463
|
{ key: "conversations_month", label: "Conversations this month", table: "plg_conversations", period: "month" }
|
|
5858
5464
|
],
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
{
|
|
5877
|
-
section: options?.navSection ?? "main",
|
|
5878
|
-
position: (options?.navPosition ?? 1) + 2,
|
|
5879
|
-
label: "Templates",
|
|
5880
|
-
route: "/conversations/templates",
|
|
5881
|
-
icon: "FileText",
|
|
5882
|
-
permission: { feature: "conversations", action: "read" }
|
|
5883
|
-
}
|
|
5884
|
-
],
|
|
5465
|
+
// SEM linha de rail para `/conversations`. A caixa de entrada subiu para a
|
|
5466
|
+
// faixa do aplicativo, como ícone com a conta do que espera resposta — ver
|
|
5467
|
+
// o widget `conversations-inbox` abaixo. Ela não é um destino como o
|
|
5468
|
+
// Financeiro: é um estado que se lê de canto de olho, e no rail a conta
|
|
5469
|
+
// ficava escondida atrás de um menu que recolhe.
|
|
5470
|
+
//
|
|
5471
|
+
// E SEM CAMPANHAS NEM TEMPLATES. As duas telas foram removidas — não
|
|
5472
|
+
// escondidas: elas tomavam duas linhas de rail num plugin que declara, na
|
|
5473
|
+
// nota acima, que nem a própria caixa de entrada merece uma. Ficavam
|
|
5474
|
+
// acima de Encontros, CRM e Financeiro em toda conta com conversas
|
|
5475
|
+
// ligadas, e a segunda cópia que apareceu embaixo de Estoque foi o sinal
|
|
5476
|
+
// de que ninguém sabia mais de onde elas vinham.
|
|
5477
|
+
//
|
|
5478
|
+
// O MAQUINÁRIO FICA: `campaignsApi`, `plg_conversations_campaigns` e o
|
|
5479
|
+
// ritmo de disparo continuam, e é por eles que Marketing e Automações
|
|
5480
|
+
// mandam mensagem. O que saiu foi a tela solta e o link para ela.
|
|
5481
|
+
navigation: [],
|
|
5885
5482
|
routes: [
|
|
5886
|
-
{
|
|
5887
|
-
path: "/conversations/templates",
|
|
5888
|
-
component: TemplatesPageComponent,
|
|
5889
|
-
title: "Templates",
|
|
5890
|
-
parentRoute: "/conversations",
|
|
5891
|
-
permission: { feature: "conversations", action: "read" }
|
|
5892
|
-
},
|
|
5893
|
-
{
|
|
5894
|
-
// Antes da rota-pai: a shell ordena por especificidade, mas declarar
|
|
5895
|
-
// a filha primeiro é o que deixa isso legível para quem ler depois.
|
|
5896
|
-
path: "/conversations/campaigns",
|
|
5897
|
-
component: CampaignsPageComponent,
|
|
5898
|
-
title: "Campanhas",
|
|
5899
|
-
parentRoute: "/conversations",
|
|
5900
|
-
permission: { feature: "conversations", action: "read" }
|
|
5901
|
-
},
|
|
5902
5483
|
{
|
|
5903
5484
|
path: "/conversations",
|
|
5904
5485
|
component: PageComponent,
|
|
@@ -5908,19 +5489,18 @@ function createConversationsPlugin(options) {
|
|
|
5908
5489
|
],
|
|
5909
5490
|
widgets: [
|
|
5910
5491
|
{
|
|
5911
|
-
id: "conversations-inbox
|
|
5492
|
+
id: "conversations-inbox",
|
|
5912
5493
|
// `shell.topbar.end` é a única zona global que o AdminShell renderiza
|
|
5913
5494
|
// FORA do switch de rota: navegar não a desmonta. Um vigia montado
|
|
5914
5495
|
// dentro da tela de Conversas morreria ao sair dela — que é justamente
|
|
5915
|
-
// quando ele serve para alguma coisa.
|
|
5496
|
+
// quando ele serve para alguma coisa. O botão da caixa é o mesmo
|
|
5497
|
+
// componente: quem conta as não lidas é quem as mostra.
|
|
5916
5498
|
zone: "shell.topbar.end",
|
|
5917
5499
|
component: (() => {
|
|
5918
|
-
const W = () =>
|
|
5919
|
-
label: options?.navLabel ?? "Conversations"
|
|
5920
|
-
section: options?.navSection ?? "main",
|
|
5921
|
-
position: options?.navPosition ?? 1
|
|
5500
|
+
const W = () => React10__default.createElement(InboxWatcher, {
|
|
5501
|
+
label: options?.navLabel ?? "Conversations"
|
|
5922
5502
|
});
|
|
5923
|
-
W.displayName = "
|
|
5503
|
+
W.displayName = "InboxButton";
|
|
5924
5504
|
return W;
|
|
5925
5505
|
})(),
|
|
5926
5506
|
order: -20
|
|
@@ -6084,6 +5664,6 @@ function createConversationsPlugin(options) {
|
|
|
6084
5664
|
};
|
|
6085
5665
|
}
|
|
6086
5666
|
|
|
6087
|
-
export {
|
|
5667
|
+
export { PAYMENT_PREFLIGHT_FUNCTION, TYXTER_CONNECTOR_ID, TYXTER_LATENCY_BUDGET_MS, WhatsAppProviders, campaignsApi, canOfferPayment, createConversationsPlugin, createMockConversationsProvider, createSupabaseConversationsProvider, isWaitingOnUs, listMessagingChannels, openPaymentSetupSession, providersApi, readPaymentReadiness, tyxterConnectorDef };
|
|
6088
5668
|
//# sourceMappingURL=index.js.map
|
|
6089
5669
|
//# sourceMappingURL=index.js.map
|