@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/index.js CHANGED
@@ -1,381 +1,14 @@
1
- import * as React12 from 'react';
2
- import React12__default, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
3
- import { Loader2, MessageSquare, AlertTriangle, Pause, Play, Send, RefreshCw, Plus, Clock, CheckCircle2, Check, ShieldCheck, ShieldAlert, QrCode, Zap, ExternalLink, SquarePen, Globe, Mail, Instagram, Phone, Search, User, Inbox, CheckCheck, ChevronLeft, Archive, PanelRight, X, MapPin, Tag, StickyNote, Link2, MoreVertical, Snowflake, Sun, Flame, Building2, UserRound, Copy, FileText, Download } from 'lucide-react';
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", section = "main", position = 1 }) {
117
+ function InboxWatcher({ label, route = "/conversations" }) {
487
118
  const tenantId = useActiveTenantId();
488
- const [naoLidas, setNaoLidas] = React12.useState(0);
489
- const podeTocar = React12.useRef(false);
490
- React12.useEffect(() => {
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 = React12.useCallback(async () => {
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
- React12.useEffect(() => {
146
+ React10.useEffect(() => {
515
147
  void contar();
516
148
  }, [contar]);
517
- React12.useEffect(() => {
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
- useNavContribution("conversations:unread", naoLidas > 0 ? [{ id: "conversations", label, route, badge: naoLidas, badgeTone: "destructive", section, position }] : []);
536
- return null;
537
- }
538
- var STATUS_UI = {
539
- approved: { icon: CheckCircle2, cls: "text-success", label: "aprovado" },
540
- submitted: { icon: Clock, cls: "text-warning", label: "em an\xE1lise" },
541
- pending: { icon: Clock, cls: "text-warning", label: "em an\xE1lise" },
542
- rejected: { icon: AlertTriangle, cls: "text-destructive", label: "recusado" },
543
- paused: { icon: AlertTriangle, cls: "text-warning", label: "pausado" },
544
- disabled: { icon: AlertTriangle, cls: "text-destructive", label: "desativado" },
545
- draft: { icon: Clock, cls: "text-muted-foreground", label: "rascunho" }
546
- };
547
- function slots(body) {
548
- const found = new Set(body.match(/\{\{(\d+)\}\}/g) ?? []);
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 = React12__default.createContext(null);
761
- var ConfigContext = React12__default.createContext(DEFAULT_CONVERSATIONS_CONFIG);
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 = React12__default.useContext(StoreContext);
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 React12__default.useContext(ConfigContext);
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] = React12__default.useState(
219
+ const [matches, setMatches] = React10__default.useState(
793
220
  () => typeof window !== "undefined" && window.matchMedia(query).matches
794
221
  );
795
- React12__default.useEffect(() => {
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 = React12__default.useMemo(() => {
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] = React12__default.useState(null);
1020
- const [providers, setProviders] = React12__default.useState(null);
1021
- React12__default.useEffect(() => {
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
- React12__default.useEffect(() => {
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 = React12__default.useMemo(() => {
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] = React12__default.useState(false);
1129
- const [detail, setDetail] = React12__default.useState(null);
1130
- const [loading, setLoading] = React12__default.useState(false);
1131
- const [err, setErr] = React12__default.useState(null);
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] = React12.useState(null);
1208
- const [erro, setErro] = React12.useState(false);
1209
- React12.useEffect(() => {
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 = React12.useRef(null);
1245
- const [visivel, setVisivel] = React12.useState(false);
739
+ const ref = React10.useRef(null);
740
+ const [visivel, setVisivel] = React10.useState(false);
1246
741
  const { url, erro } = useUrlAssinada(m, visivel);
1247
- React12.useEffect(() => {
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] = React12__default.useState(null);
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] = React12__default.useState("");
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 = React12__default.useRef(null);
1400
- React12__default.useEffect(() => {
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
- React12__default.useEffect(() => {
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 = React12__default.useMemo(() => buildRows(messages), [messages]);
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] = React12__default.useState(void 0);
1545
- React12__default.useEffect(() => {
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] = React12__default.useState("whatsapp");
1713
- const [contact, setContact] = React12__default.useState(null);
1714
- const [typedHandle, setTypedHandle] = React12__default.useState("");
1715
- const [creatingContact, setCreatingContact] = React12__default.useState(false);
1716
- const [firstMessage, setFirstMessage] = React12__default.useState("");
1717
- const [submitting, setSubmitting] = React12__default.useState(false);
1718
- const [pickerKey, setPickerKey] = React12__default.useState(0);
1719
- React12__default.useEffect(() => {
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 = React12.useRef(selectedId);
1329
+ const abertoRef = React10.useRef(selectedId);
1835
1330
  abertoRef.current = selectedId;
1836
- React12.useEffect(() => {
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] = React12__default.useState(false);
1853
- const [newOpen, setNewOpen] = React12__default.useState(false);
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
- React12__default.useEffect(() => {
1350
+ React10__default.useEffect(() => {
1856
1351
  setPanelOpen(isWidePanel && !railOpen);
1857
1352
  }, [isWidePanel, railOpen]);
1858
- const newActions = React12__default.useMemo(() => [{
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
- React12__default.useEffect(() => {
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] = React12__default.useState(null);
3017
- const [draft, setDraft] = React12__default.useState({});
3018
- const [busy, setBusy] = React12__default.useState(null);
3019
- const [numbers, setNumbers] = React12__default.useState({});
3020
- const [qr, setQr] = React12__default.useState(null);
3021
- const load = React12__default.useCallback(async () => {
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
- React12__default.useEffect(() => {
2523
+ React10__default.useEffect(() => {
3029
2524
  void load();
3030
2525
  }, [load]);
3031
- const byId = React12__default.useMemo(
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] = React12.useState(null);
3267
- const [failed, setFailed] = React12.useState(false);
3268
- React12.useEffect(() => {
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 client3() {
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 client3().functions.invoke(TYXTER_NUMBER_CLAIM_FUNCTION, {
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 = () => React12__default.createElement(ConversationsPage, { store: store2, config });
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
- navigation: [
5860
- {
5861
- section: options?.navSection ?? "main",
5862
- position: options?.navPosition ?? 1,
5863
- label: options?.navLabel ?? "Conversations",
5864
- route: "/conversations",
5865
- icon: "MessageCircle",
5866
- permission: { feature: "conversations", action: "read" }
5867
- },
5868
- {
5869
- section: options?.navSection ?? "main",
5870
- position: (options?.navPosition ?? 1) + 1,
5871
- label: "Campanhas",
5872
- route: "/conversations/campaigns",
5873
- icon: "Send",
5874
- permission: { feature: "conversations", action: "read" }
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-watcher",
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 = () => React12__default.createElement(InboxWatcher, {
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 = "InboxWatcher";
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 { CampaignsView, PAYMENT_PREFLIGHT_FUNCTION, TYXTER_CONNECTOR_ID, TYXTER_LATENCY_BUDGET_MS, TemplatesView, WhatsAppProviders, campaignsApi, canOfferPayment, createConversationsPlugin, createMockConversationsProvider, createSupabaseConversationsProvider, isWaitingOnUs, listMessagingChannels, openPaymentSetupSession, providersApi, readPaymentReadiness, tyxterConnectorDef };
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