@fayz-ai/plugin-conversations 0.22.13 → 0.23.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,54 +1,33 @@
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';
4
- import { createConnectionStore, connectionRuns, connectionStatus, getActiveTenantId, getSupabaseClientOptional, useActiveTenantId, registerTranslations, describeError, useTranslation, countByTenant, CONNECTOR_RUNTIME_TOKEN_HEADER, connectorRuntimeToken, errorMessage } from '@fayz-ai/core';
5
- import { SettingsGroup, PluginSettingsPanel, useAdminPath, useWidgetSurface, adminNavigateTo, useRightRailStore, PageActions, useLimitGuard, ContactPicker, invalidateLimit } from '@fayz-ai/admin';
6
- import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
1
+ import * as React14 from 'react';
2
+ import React14__default, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
3
+ import { Loader2, Check, ShieldCheck, ShieldAlert, RefreshCw, QrCode, Zap, ExternalLink, Bot, Sparkles, Settings2, MessageCircle, SquarePen, MessageSquare, Globe, Mail, Instagram, Phone, Search, User, Inbox, CheckCheck, ChevronLeft, Archive, PanelRight, Send, X, MapPin, Tag, StickyNote, Link2, CheckCircle2, MoreVertical, AlertTriangle, Clock, Snowflake, Sun, Flame, Building2, UserRound, Copy, FileText, Download } from 'lucide-react';
4
+ import { createConnectionStore, connectionRuns, connectionStatus, tl, getActiveTenantId, getSupabaseClientOptional, useActiveTenantId, registerTranslations, describeError, useTranslation, usePluginRuntimeOptional, countByTenant, CONNECTOR_RUNTIME_TOKEN_HEADER, connectorRuntimeToken, formatCurrency, errorMessage } from '@fayz-ai/core';
5
+ import { defineSettingsTab, SettingsGroup, PluginSettingsPanel, PRICE_BOOK, useRightRailStore, useAdminPath, useRightRailPanel, useWidgetSurface, adminNavigateTo, useConnectors, useConnectorsPanelAvailable, ConnectorMark, StateChip, openConnectorsPanel, ConnectorDetail, PageActions, useLimitGuard, ContactPicker, useRightRailMenu, invalidateLimit } from '@fayz-ai/admin';
6
+ import { Input, Button, toast, defineKpiWidget, defineTableWidget, Card, Badge, ComingSoon, Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetBody, PageHeaderActions, cn, Modal, ModalContent, ModalHeader, ModalTitle, ModalBody, Textarea, ModalFooter, FilterChip, Skeleton, KpiCard, TableWidget } from '@fayz-ai/ui';
7
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';
8
+ import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
9
9
  import { createStore } from 'zustand/vanilla';
10
10
 
11
11
  // src/index.ts
12
-
13
- // src/data/tables.ts
14
- var T = {
15
- conversations: "plg_conversations",
16
- /** A LEITURA da conversa: a mesma linha, com o dono do lead junto. A escrita
17
- * continua na tabela — view com `security_invoker` não aceita INSERT aqui. */
18
- conversationsView: "v_conversations",
19
- messages: "plg_conversation_messages",
20
- channels: "plg_conversations_channels"
12
+ var DEFAULT_CONVERSATIONS_CONFIG = {
13
+ contactKind: "contact"
21
14
  };
22
- var porConta = /* @__PURE__ */ new Map();
23
- function assinarChegada(tenantId, ouvinte) {
24
- const supabase = getSupabaseClientOptional();
25
- if (!supabase?.channel || !tenantId) return () => {
26
- };
27
- let assinatura = porConta.get(tenantId);
28
- if (!assinatura) {
29
- const ouvintes = /* @__PURE__ */ new Set();
30
- const canal = supabase.channel(`conversas:${tenantId}`).on("broadcast", { event: "inbound" }, (msg2) => {
31
- const carga = { conversationId: msg2?.payload?.conversationId };
32
- for (const f of [...ouvintes]) {
33
- try {
34
- f(carga);
35
- } catch {
36
- }
37
- }
38
- }).subscribe();
39
- assinatura = { canal, ouvintes };
40
- porConta.set(tenantId, assinatura);
41
- }
42
- assinatura.ouvintes.add(ouvinte);
43
- return () => {
44
- const a = porConta.get(tenantId);
45
- if (!a) return;
46
- a.ouvintes.delete(ouvinte);
47
- if (a.ouvintes.size === 0) {
48
- porConta.delete(tenantId);
49
- void getSupabaseClientOptional()?.removeChannel?.(a.canal);
50
- }
51
- };
15
+ var StoreContext = React14__default.createContext(null);
16
+ var ConfigContext = React14__default.createContext(DEFAULT_CONVERSATIONS_CONFIG);
17
+ function ConversationsContextProvider({
18
+ store: store2,
19
+ config = DEFAULT_CONVERSATIONS_CONFIG,
20
+ children
21
+ }) {
22
+ return /* @__PURE__ */ jsx(StoreContext.Provider, { value: store2, children: /* @__PURE__ */ jsx(ConfigContext.Provider, { value: config, children }) });
23
+ }
24
+ function useConversationsStore(selector) {
25
+ const store2 = React14__default.useContext(StoreContext);
26
+ if (!store2) throw new Error("useConversationsStore must be used within ConversationsPage");
27
+ return useStore(store2, selector);
28
+ }
29
+ function useConversationsConfig() {
30
+ return React14__default.useContext(ConfigContext);
52
31
  }
53
32
 
54
33
  // src/types.ts
@@ -65,144 +44,6 @@ function isWaitingOnUs(conversation) {
65
44
  if (conversation.lastMessageDirection) return conversation.lastMessageDirection === "inbound";
66
45
  return conversation.unreadCount > 0;
67
46
  }
68
- function tocar() {
69
- try {
70
- const Ctx = window.AudioContext ?? window.webkitAudioContext;
71
- if (!Ctx) return;
72
- const ctx = new Ctx();
73
- const agora = ctx.currentTime;
74
- for (const [freq, atraso] of [[880, 0], [1320, 0.09]]) {
75
- const osc = ctx.createOscillator();
76
- const vol = ctx.createGain();
77
- osc.type = "sine";
78
- osc.frequency.value = freq;
79
- vol.gain.setValueAtTime(1e-4, agora + atraso);
80
- vol.gain.exponentialRampToValueAtTime(0.18, agora + atraso + 0.02);
81
- vol.gain.exponentialRampToValueAtTime(1e-4, agora + atraso + 0.28);
82
- osc.connect(vol);
83
- vol.connect(ctx.destination);
84
- osc.start(agora + atraso);
85
- osc.stop(agora + atraso + 0.3);
86
- }
87
- setTimeout(() => void ctx.close().catch(() => {
88
- }), 900);
89
- } catch {
90
- }
91
- }
92
- async function pedirPermissao() {
93
- if (typeof Notification === "undefined") return false;
94
- if (Notification.permission === "granted") return true;
95
- if (Notification.permission === "denied") return false;
96
- try {
97
- return await Notification.requestPermission() === "granted";
98
- } catch {
99
- return false;
100
- }
101
- }
102
- function avisarNoSistema(titulo, corpo, tag) {
103
- try {
104
- if (typeof Notification === "undefined" || Notification.permission !== "granted") return;
105
- if (typeof document !== "undefined" && document.visibilityState === "visible") return;
106
- const n = new Notification(titulo, { body: corpo, tag, renotify: true });
107
- n.onclick = () => {
108
- try {
109
- window.focus();
110
- n.close();
111
- } catch {
112
- }
113
- };
114
- } catch {
115
- }
116
- }
117
- function InboxWatcher({ label, route = "/conversations" }) {
118
- const tenantId = useActiveTenantId();
119
- const path = useAdminPath();
120
- const [naoLidas, setNaoLidas] = React10.useState(0);
121
- const podeTocar = React10.useRef(false);
122
- React10.useEffect(() => {
123
- const liberar = () => {
124
- podeTocar.current = true;
125
- void pedirPermissao();
126
- };
127
- for (const ev of ["pointerdown", "keydown", "touchstart"]) {
128
- window.addEventListener(ev, liberar, { once: true, passive: true });
129
- }
130
- return () => {
131
- for (const ev of ["pointerdown", "keydown", "touchstart"]) window.removeEventListener(ev, liberar);
132
- };
133
- }, []);
134
- const contar = React10.useCallback(async () => {
135
- const supabase = getSupabaseClientOptional();
136
- if (!supabase) return;
137
- const { data } = await supabase.from(T.conversations).select("id, status, last_message_direction, unread_count").eq("status", "open");
138
- if (!data) return;
139
- const n = data.filter((r) => isWaitingOnUs({
140
- status: r.status,
141
- lastMessageDirection: r.last_message_direction ?? void 0,
142
- unreadCount: r.unread_count ?? 0
143
- })).length;
144
- setNaoLidas(n);
145
- }, []);
146
- React10.useEffect(() => {
147
- void contar();
148
- }, [contar]);
149
- React10.useEffect(() => {
150
- if (!tenantId) return;
151
- return assinarChegada(tenantId, ({ conversationId }) => {
152
- void contar();
153
- if (podeTocar.current) tocar();
154
- void (async () => {
155
- const supabase = getSupabaseClientOptional();
156
- let titulo = label;
157
- let corpo = "";
158
- if (conversationId && supabase) {
159
- const { data } = await supabase.from(T.conversations).select("contact_name, last_message_preview").eq("id", conversationId).maybeSingle();
160
- titulo = data?.contact_name || label;
161
- corpo = String(data?.last_message_preview || "").replace(/\s+/g, " ").slice(0, 140);
162
- }
163
- avisarNoSistema(titulo, corpo, String(conversationId ?? "inbox"));
164
- })();
165
- });
166
- }, [contar, tenantId, label]);
167
- const aberta = path === route || path.startsWith(`${route}/`);
168
- const named = useWidgetSurface() === "rail";
169
- return /* @__PURE__ */ jsxs(
170
- "button",
171
- {
172
- type: "button",
173
- onClick: () => adminNavigateTo(route),
174
- "aria-label": naoLidas > 0 ? `${label} (${naoLidas})` : label,
175
- title: label,
176
- "aria-current": aberta ? "page" : void 0,
177
- className: `fz-tap relative inline-flex shrink-0 items-center rounded-button transition-colors hover:bg-accent ${named ? "w-full gap-3 px-2.5 py-2.5" : "justify-center p-2"} ${aberta ? "text-foreground" : "text-muted-foreground hover:text-foreground"}`,
178
- children: [
179
- /* @__PURE__ */ jsx(MessageCircle, { className: "h-4 w-4 shrink-0" }),
180
- named && /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-left", children: label }),
181
- 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 })
182
- ]
183
- }
184
- );
185
- }
186
- var DEFAULT_CONVERSATIONS_CONFIG = {
187
- contactKind: "contact"
188
- };
189
- var StoreContext = React10__default.createContext(null);
190
- var ConfigContext = React10__default.createContext(DEFAULT_CONVERSATIONS_CONFIG);
191
- function ConversationsContextProvider({
192
- store: store2,
193
- config = DEFAULT_CONVERSATIONS_CONFIG,
194
- children
195
- }) {
196
- return /* @__PURE__ */ jsx(StoreContext.Provider, { value: store2, children: /* @__PURE__ */ jsx(ConfigContext.Provider, { value: config, children }) });
197
- }
198
- function useConversationsStore(selector) {
199
- const store2 = React10__default.useContext(StoreContext);
200
- if (!store2) throw new Error("useConversationsStore must be used within ConversationsPage");
201
- return useStore(store2, selector);
202
- }
203
- function useConversationsConfig() {
204
- return React10__default.useContext(ConfigContext);
205
- }
206
47
  var CHANNEL_ICON = {
207
48
  sms: Phone,
208
49
  whatsapp: MessageSquare,
@@ -218,10 +59,10 @@ var CHANNEL_ACCENT = {
218
59
  webchat: { color: "#f59e0b", badge: "bg-[#f59e0b]/12 text-[#b45309] dark:text-[#fcd34d]" }
219
60
  };
220
61
  function useMediaQuery(query) {
221
- const [matches, setMatches] = React10__default.useState(
62
+ const [matches, setMatches] = React14__default.useState(
222
63
  () => typeof window !== "undefined" && window.matchMedia(query).matches
223
64
  );
224
- React10__default.useEffect(() => {
65
+ React14__default.useEffect(() => {
225
66
  if (typeof window === "undefined") return;
226
67
  const mql = window.matchMedia(query);
227
68
  const onChange = () => setMatches(mql.matches);
@@ -294,9 +135,106 @@ function dayLabel(iso) {
294
135
  if (dayDiff < 7) return d.toLocaleDateString([], { weekday: "long" });
295
136
  return d.toLocaleDateString([], { month: "short", day: "numeric", year: now.getFullYear() === d.getFullYear() ? void 0 : "numeric" });
296
137
  }
297
- var FILTERS = ["all", "whatsapp", "sms", "instagram", "email", "webchat"];
138
+ var FN = "conversations-provider";
139
+ function client() {
140
+ const c = getSupabaseClientOptional();
141
+ if (!c) throw new Error("Supabase n\xE3o inicializado");
142
+ return c;
143
+ }
144
+ async function callFn(action, body = {}) {
145
+ const tenantId = getActiveTenantId();
146
+ if (!tenantId) throw new Error("Nenhuma casa ativa");
147
+ const { data, error } = await client().functions.invoke(FN, {
148
+ body: { tenantId, action, ...body }
149
+ });
150
+ if (error) {
151
+ const detail = await error.context?.json?.().catch(() => null);
152
+ throw new Error(detail?.message ?? error.message);
153
+ }
154
+ if (data?.error) throw new Error(data.message ?? data.error);
155
+ return data;
156
+ }
157
+ var providersApi = {
158
+ /** O que está configurado nesta casa. Sem segredo. */
159
+ async list() {
160
+ const tenantId = getActiveTenantId();
161
+ if (!tenantId) return [];
162
+ 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);
163
+ if (error) throw new Error(error.message);
164
+ return (data ?? []).map((r) => ({
165
+ provider: r.provider,
166
+ config: r.config ?? {},
167
+ capabilities: r.capabilities ?? {},
168
+ isActive: Boolean(r.is_active),
169
+ status: r.status,
170
+ statusDetail: r.status_detail ?? null,
171
+ checkedAt: r.checked_at ?? null,
172
+ hasCredential: Boolean(r.has_credential)
173
+ }));
174
+ },
175
+ /** Guarda config e, quando vier, a chave. Chave ausente é "não mexe": salvar
176
+ * a URL do Evolution sem redigitar a chave é o gesto comum. */
177
+ async save(provider, config, apiKey) {
178
+ const tenantId = getActiveTenantId();
179
+ if (!tenantId) throw new Error("Nenhuma casa ativa");
180
+ const { error } = await client().rpc("conversations_set_provider", {
181
+ p_tenant: tenantId,
182
+ p_provider: provider,
183
+ p_config: config,
184
+ p_api_key: apiKey && apiKey.trim() ? apiKey.trim() : null
185
+ });
186
+ if (error) throw new Error(error.message);
187
+ },
188
+ /** A troca. Um ativo por casa, garantido por índice único no banco. */
189
+ async activate(provider) {
190
+ const tenantId = getActiveTenantId();
191
+ if (!tenantId) throw new Error("Nenhuma casa ativa");
192
+ const { error } = await client().rpc("conversations_activate_provider", {
193
+ p_tenant: tenantId,
194
+ p_provider: provider
195
+ });
196
+ if (error) throw new Error(error.message);
197
+ },
198
+ health: (provider) => callFn("health", { provider }),
199
+ numbers: (provider) => callFn("numbers", { provider }),
200
+ /** O QR do Evolution. O Tyxter recusa com `unsupported`. */
201
+ pair: (provider) => callFn("pair", { provider }),
202
+ templates: (provider) => callFn("templates", { provider }),
203
+ createTemplate: (template, provider) => callFn("create_template", { template, provider }),
204
+ send: (input) => callFn("send", input)
205
+ };
206
+
207
+ // src/data/channel-connections.ts
208
+ var whatsappCache = null;
209
+ var whatsappConnected = () => {
210
+ whatsappCache ?? (whatsappCache = providersApi.list().then((rows) => rows.some((r) => r.isActive || r.hasCredential)).catch(() => {
211
+ whatsappCache = null;
212
+ return false;
213
+ }));
214
+ return whatsappCache;
215
+ };
216
+ function useChannelConnections() {
217
+ const [whatsapp, setWhatsapp] = React14.useState(void 0);
218
+ const { connectors } = useConnectors({ hostPluginId: "conversations", includeGrants: false });
219
+ React14.useEffect(() => {
220
+ let cancelled = false;
221
+ void whatsappConnected().then((v) => {
222
+ if (!cancelled) setWhatsapp(v);
223
+ });
224
+ return () => {
225
+ cancelled = true;
226
+ };
227
+ }, []);
228
+ return React14.useMemo(() => {
229
+ if (whatsapp === void 0) return void 0;
230
+ const live = (needle) => (connectors ?? []).some((c) => c.state === "connected" && c.slug.toLowerCase().includes(needle));
231
+ return { whatsapp: whatsapp || live("whatsapp"), email: live("mail"), instagram: live("instagram") };
232
+ }, [whatsapp, connectors]);
233
+ }
234
+ var FILTERS = ["all", "whatsapp", "instagram", "email"];
298
235
  function ConversationList({ className }) {
299
236
  const t = useTranslation();
237
+ const connected = useChannelConnections();
300
238
  const {
301
239
  conversations,
302
240
  selectedId,
@@ -310,7 +248,7 @@ function ConversationList({ className }) {
310
248
  setLeadOwnerFilter,
311
249
  setSearch
312
250
  } = useConversationsStore((s) => s);
313
- const donos = React10__default.useMemo(() => {
251
+ const donos = React14__default.useMemo(() => {
314
252
  const vistos = /* @__PURE__ */ new Map();
315
253
  for (const c of conversations) {
316
254
  if (c.leadOwnerId) vistos.set(c.leadOwnerId, c.leadOwnerName ?? c.leadOwnerId);
@@ -331,18 +269,24 @@ function ConversationList({ className }) {
331
269
  }
332
270
  )
333
271
  ] }),
334
- /* @__PURE__ */ jsx("div", { className: "mt-2 flex flex-wrap gap-1", children: FILTERS.map((id) => /* @__PURE__ */ jsx(
335
- "button",
336
- {
337
- onClick: () => setChannelFilter(id),
338
- className: cn(
339
- "rounded-full px-2.5 py-1 text-xs font-medium transition-colors",
340
- channelFilter === id ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground hover:bg-muted/70"
341
- ),
342
- children: t(`conversations.filter.${id}`)
343
- },
344
- id
345
- )) }),
272
+ /* @__PURE__ */ jsx("div", { className: "mt-2 flex flex-wrap gap-1", children: FILTERS.map((id) => {
273
+ const off = id !== "all" && connected !== void 0 && !connected[id];
274
+ return /* @__PURE__ */ jsx(
275
+ Button,
276
+ {
277
+ variant: "bare",
278
+ size: "bare",
279
+ onClick: () => setChannelFilter(id),
280
+ title: off ? t("conversations.filter.notConnected") : void 0,
281
+ className: cn(
282
+ "rounded-full border px-2.5 py-1 text-xs font-medium transition-colors",
283
+ channelFilter === id ? "border-transparent bg-primary text-primary-foreground" : off ? "border-dashed border-muted-foreground/40 bg-transparent text-muted-foreground hover:bg-muted/50" : "border-transparent bg-muted text-muted-foreground hover:bg-muted/70"
284
+ ),
285
+ children: t(`conversations.filter.${id}`)
286
+ },
287
+ id
288
+ );
289
+ }) }),
346
290
  donos.length > 0 && /* @__PURE__ */ jsx("div", { className: "mt-2", children: /* @__PURE__ */ jsx(
347
291
  FilterChip,
348
292
  {
@@ -383,11 +327,13 @@ function ConversationList({ className }) {
383
327
  // ninguém entende por quê.
384
328
  /* @__PURE__ */ jsxs("div", { className: "group relative", children: [
385
329
  /* @__PURE__ */ jsxs(
386
- "button",
330
+ Button,
387
331
  {
332
+ variant: "bare",
333
+ size: "bare",
388
334
  onClick: () => select(c.id),
389
335
  className: cn(
390
- "flex w-full items-start gap-3 border-b border-border/50 px-3 py-3 text-left transition-colors",
336
+ "flex w-full items-start justify-start gap-3 rounded-none border-b border-border/50 px-3 py-3 text-left font-normal transition-colors",
391
337
  // Um TOM da cor de marca, e não `bg-accent`.
392
338
  //
393
339
  // `bg-accent` resolve para quase preto neste tema, e a linha da
@@ -425,8 +371,10 @@ function ConversationList({ className }) {
425
371
  }
426
372
  ),
427
373
  unread && /* @__PURE__ */ jsx(
428
- "button",
374
+ Button,
429
375
  {
376
+ variant: "bare",
377
+ size: "bare",
430
378
  type: "button",
431
379
  title: t("conversations.markRead"),
432
380
  "aria-label": t("conversations.markRead"),
@@ -444,78 +392,10 @@ function ConversationList({ className }) {
444
392
  ] })
445
393
  ] });
446
394
  }
447
- var FN = "conversations-provider";
448
- function client() {
449
- const c = getSupabaseClientOptional();
450
- if (!c) throw new Error("Supabase n\xE3o inicializado");
451
- return c;
452
- }
453
- async function callFn(action, body = {}) {
454
- const tenantId = getActiveTenantId();
455
- if (!tenantId) throw new Error("Nenhuma casa ativa");
456
- const { data, error } = await client().functions.invoke(FN, {
457
- body: { tenantId, action, ...body }
458
- });
459
- if (error) {
460
- const detail = await error.context?.json?.().catch(() => null);
461
- throw new Error(detail?.message ?? error.message);
462
- }
463
- if (data?.error) throw new Error(data.message ?? data.error);
464
- return data;
465
- }
466
- var providersApi = {
467
- /** O que está configurado nesta casa. Sem segredo. */
468
- async list() {
469
- const tenantId = getActiveTenantId();
470
- if (!tenantId) return [];
471
- 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);
472
- if (error) throw new Error(error.message);
473
- return (data ?? []).map((r) => ({
474
- provider: r.provider,
475
- config: r.config ?? {},
476
- capabilities: r.capabilities ?? {},
477
- isActive: Boolean(r.is_active),
478
- status: r.status,
479
- statusDetail: r.status_detail ?? null,
480
- checkedAt: r.checked_at ?? null,
481
- hasCredential: Boolean(r.has_credential)
482
- }));
483
- },
484
- /** Guarda config e, quando vier, a chave. Chave ausente é "não mexe": salvar
485
- * a URL do Evolution sem redigitar a chave é o gesto comum. */
486
- async save(provider, config, apiKey) {
487
- const tenantId = getActiveTenantId();
488
- if (!tenantId) throw new Error("Nenhuma casa ativa");
489
- const { error } = await client().rpc("conversations_set_provider", {
490
- p_tenant: tenantId,
491
- p_provider: provider,
492
- p_config: config,
493
- p_api_key: apiKey && apiKey.trim() ? apiKey.trim() : null
494
- });
495
- if (error) throw new Error(error.message);
496
- },
497
- /** A troca. Um ativo por casa, garantido por índice único no banco. */
498
- async activate(provider) {
499
- const tenantId = getActiveTenantId();
500
- if (!tenantId) throw new Error("Nenhuma casa ativa");
501
- const { error } = await client().rpc("conversations_activate_provider", {
502
- p_tenant: tenantId,
503
- p_provider: provider
504
- });
505
- if (error) throw new Error(error.message);
506
- },
507
- health: (provider) => callFn("health", { provider }),
508
- numbers: (provider) => callFn("numbers", { provider }),
509
- /** O QR do Evolution. O Tyxter recusa com `unsupported`. */
510
- pair: (provider) => callFn("pair", { provider }),
511
- templates: (provider) => callFn("templates", { provider }),
512
- createTemplate: (template, provider) => callFn("create_template", { template, provider }),
513
- send: (input) => callFn("send", input)
514
- };
515
395
  function useSendWindow(conversationId) {
516
- const [row, setRow] = React10__default.useState(null);
517
- const [providers, setProviders] = React10__default.useState(null);
518
- React10__default.useEffect(() => {
396
+ const [row, setRow] = React14__default.useState(null);
397
+ const [providers, setProviders] = React14__default.useState(null);
398
+ React14__default.useEffect(() => {
519
399
  let alive = true;
520
400
  providersApi.list().then((p) => {
521
401
  if (alive) setProviders(p);
@@ -526,7 +406,7 @@ function useSendWindow(conversationId) {
526
406
  alive = false;
527
407
  };
528
408
  }, []);
529
- React10__default.useEffect(() => {
409
+ React14__default.useEffect(() => {
530
410
  let alive = true;
531
411
  if (!conversationId) {
532
412
  setRow(null);
@@ -547,7 +427,7 @@ function useSendWindow(conversationId) {
547
427
  };
548
428
  }, [conversationId]);
549
429
  const provider = (providers ?? []).find((p) => p.isActive);
550
- const mode = React10__default.useMemo(() => {
430
+ const mode = React14__default.useMemo(() => {
551
431
  if (!provider) return { kind: "blocked", reason: "no_provider" };
552
432
  if (!provider.capabilities?.requires_template) {
553
433
  return { kind: "free", until: null, minutesLeft: null };
@@ -622,10 +502,10 @@ function DeliveryMark({ m }) {
622
502
  ] });
623
503
  }
624
504
  function MessageMenu({ m }) {
625
- const [open, setOpen] = React10__default.useState(false);
626
- const [detail, setDetail] = React10__default.useState(null);
627
- const [loading, setLoading] = React10__default.useState(false);
628
- const [err, setErr] = React10__default.useState(null);
505
+ const [open, setOpen] = React14__default.useState(false);
506
+ const [detail, setDetail] = React14__default.useState(null);
507
+ const [loading, setLoading] = React14__default.useState(false);
508
+ const [err, setErr] = React14__default.useState(null);
629
509
  async function load() {
630
510
  setOpen(true);
631
511
  if (!m.providerMessageId) return;
@@ -650,8 +530,10 @@ function MessageMenu({ m }) {
650
530
  }
651
531
  return /* @__PURE__ */ jsxs(Fragment, { children: [
652
532
  /* @__PURE__ */ jsx(
653
- "button",
533
+ Button,
654
534
  {
535
+ variant: "bare",
536
+ size: "bare",
655
537
  onClick: () => void load(),
656
538
  "aria-label": "Detalhes da mensagem",
657
539
  className: "opacity-0 transition-opacity group-hover:opacity-60 hover:!opacity-100",
@@ -669,7 +551,7 @@ function MessageMenu({ m }) {
669
551
  m.deliveryStatus ? ` \xB7 ${m.deliveryStatus}` : ""
670
552
  ] })
671
553
  ] }),
672
- /* @__PURE__ */ jsx("button", { onClick: () => setOpen(false), "aria-label": "Fechar", children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" }) })
554
+ /* @__PURE__ */ jsx(Button, { variant: "bare", size: "bare", onClick: () => setOpen(false), "aria-label": "Fechar", children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" }) })
673
555
  ] }),
674
556
  /* @__PURE__ */ jsxs("div", { className: "max-h-[60vh] overflow-auto p-4 text-xs", children: [
675
557
  /* @__PURE__ */ jsx("p", { className: "mb-1 font-medium text-muted-foreground", children: "Nesta base" }),
@@ -701,9 +583,9 @@ function temArquivo(m) {
701
583
  return !!m.mediaPath && COM_ARQUIVO.has(String(m.messageType));
702
584
  }
703
585
  function useUrlAssinada(m, visivel) {
704
- const [url, setUrl] = React10.useState(null);
705
- const [erro, setErro] = React10.useState(false);
706
- React10.useEffect(() => {
586
+ const [url, setUrl] = React14.useState(null);
587
+ const [erro, setErro] = React14.useState(false);
588
+ React14.useEffect(() => {
707
589
  if (!visivel || url || erro || !temArquivo(m)) return;
708
590
  let vivo = true;
709
591
  (async () => {
@@ -738,10 +620,10 @@ function duracao(seg) {
738
620
  return `${m}:${String(s).padStart(2, "0")}`;
739
621
  }
740
622
  function MediaBubble({ m, outbound }) {
741
- const ref = React10.useRef(null);
742
- const [visivel, setVisivel] = React10.useState(false);
623
+ const ref = React14.useRef(null);
624
+ const [visivel, setVisivel] = React14.useState(false);
743
625
  const { url, erro } = useUrlAssinada(m, visivel);
744
- React10.useEffect(() => {
626
+ React14.useEffect(() => {
745
627
  const el = ref.current;
746
628
  if (!el || typeof IntersectionObserver === "undefined") {
747
629
  setVisivel(true);
@@ -825,7 +707,7 @@ function MediaBubble({ m, outbound }) {
825
707
  ] });
826
708
  }
827
709
  function ContactCard({ contacts, outbound }) {
828
- const [copied, setCopied] = React10__default.useState(null);
710
+ const [copied, setCopied] = React14__default.useState(null);
829
711
  const copy = (value) => {
830
712
  void navigator.clipboard?.writeText(value);
831
713
  setCopied(value);
@@ -843,12 +725,14 @@ function ContactCard({ contacts, outbound }) {
843
725
  /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
844
726
  /* @__PURE__ */ jsx("p", { className: "truncate font-medium", children: c.name }),
845
727
  c.phones.map((p) => /* @__PURE__ */ jsxs(
846
- "button",
728
+ Button,
847
729
  {
730
+ variant: "bare",
731
+ size: "bare",
848
732
  type: "button",
849
733
  onClick: () => copy(p.display),
850
734
  className: cn(
851
- "group/phone mt-0.5 flex items-center gap-1.5 text-left text-[13px] tabular-nums",
735
+ "group/phone mt-0.5 flex items-center justify-start gap-1.5 text-left text-[13px] font-normal tabular-nums",
852
736
  outbound ? "text-white/90 hover:text-white" : "text-muted-foreground hover:text-foreground"
853
737
  ),
854
738
  children: [
@@ -887,17 +771,17 @@ function buildRows(messages) {
887
771
  });
888
772
  return rows;
889
773
  }
890
- function MessageThread({ selected, onTogglePanel, panelOpen, onBack, className }) {
774
+ function MessageThread({ selected, onTogglePanel, panelOpen, onBack, backAlways, className }) {
891
775
  const t = useTranslation();
892
776
  const { messages, sending, send, setStatus, refreshMessages } = useConversationsStore((s) => s);
893
- const [draft, setDraft] = React10__default.useState("");
777
+ const [draft, setDraft] = React14__default.useState("");
894
778
  const win = useSendWindow(selected.id);
895
779
  const canType = win.mode.kind === "free";
896
- const threadRef = React10__default.useRef(null);
897
- React10__default.useEffect(() => {
780
+ const threadRef = React14__default.useRef(null);
781
+ React14__default.useEffect(() => {
898
782
  threadRef.current?.scrollTo({ top: threadRef.current.scrollHeight, behavior: "smooth" });
899
783
  }, [messages.length, selected.id]);
900
- React10__default.useEffect(() => {
784
+ React14__default.useEffect(() => {
901
785
  if (!selected.id) return;
902
786
  let timer = null;
903
787
  const start = () => {
@@ -926,11 +810,11 @@ function MessageThread({ selected, onTogglePanel, panelOpen, onBack, className }
926
810
  await send(body);
927
811
  }
928
812
  const accent = CHANNEL_ACCENT[selected.channel];
929
- const rows = React10__default.useMemo(() => buildRows(messages), [messages]);
813
+ const rows = React14__default.useMemo(() => buildRows(messages), [messages]);
930
814
  return /* @__PURE__ */ jsxs("section", { className: cn("flex min-w-0 flex-1 flex-col bg-muted/20", className), children: [
931
815
  /* @__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: [
932
816
  /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-2 md:gap-3", children: [
933
- onBack && /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon", className: "-ml-1 shrink-0 lg:hidden", onClick: onBack, "aria-label": t("conversations.thread.back"), children: /* @__PURE__ */ jsx(ChevronLeft, { className: "h-5 w-5" }) }),
817
+ onBack && /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon", className: cn("-ml-1 shrink-0", !backAlways && "lg:hidden"), onClick: onBack, "aria-label": t("conversations.thread.back"), children: /* @__PURE__ */ jsx(ChevronLeft, { className: "h-5 w-5" }) }),
934
818
  /* @__PURE__ */ jsx(Avatar, { name: selected.contactName, accent: selected.accent, size: "sm", channel: selected.channel }),
935
819
  /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
936
820
  /* @__PURE__ */ jsx("p", { className: "truncate text-sm font-semibold text-foreground", children: selected.contactName }),
@@ -1010,8 +894,8 @@ function MessageThread({ selected, onTogglePanel, panelOpen, onBack, className }
1010
894
  /* @__PURE__ */ jsxs("div", { className: "border-t border-border bg-card", children: [
1011
895
  /* @__PURE__ */ jsx(SendWindowBanner, { state: win }),
1012
896
  /* @__PURE__ */ jsxs("div", { className: "flex items-end gap-2 px-4 py-3", children: [
1013
- /* @__PURE__ */ jsx(
1014
- "textarea",
897
+ /* @__PURE__ */ jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsx(
898
+ Textarea,
1015
899
  {
1016
900
  value: draft,
1017
901
  onChange: (e) => setDraft(e.target.value),
@@ -1024,9 +908,9 @@ function MessageThread({ selected, onTogglePanel, panelOpen, onBack, className }
1024
908
  rows: 1,
1025
909
  disabled: !canType,
1026
910
  placeholder: canType ? t("conversations.thread.reply", { channel: CHANNEL_LABELS[selected.channel] }) : win.mode.kind === "blocked" ? "Envio bloqueado" : "Fora da janela \u2014 envie um template",
1027
- className: "max-h-32 min-h-[40px] flex-1 resize-none rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground outline-none focus:border-primary disabled:cursor-not-allowed disabled:bg-muted/40 disabled:text-muted-foreground"
911
+ className: "max-h-32 min-h-[40px] resize-none rounded-md border-border bg-background shadow-none focus:border-primary focus-visible:ring-0 focus-visible:ring-offset-0 disabled:bg-muted/40 disabled:text-muted-foreground disabled:opacity-100"
1028
912
  }
1029
- ),
913
+ ) }),
1030
914
  /* @__PURE__ */ jsx(Button, { onClick: () => void handleSend(), disabled: !canType || sending || !draft.trim(), "aria-label": t("conversations.thread.send"), children: /* @__PURE__ */ jsx(Send, { className: "h-4 w-4" }) })
1031
915
  ] })
1032
916
  ] })
@@ -1038,8 +922,8 @@ var TEMP = {
1038
922
  cold: { icon: Snowflake, cls: "bg-info-soft text-info-soft-foreground", label: "Frio" }
1039
923
  };
1040
924
  function LeadCard({ personId, handle }) {
1041
- const [lead, setLead] = React10__default.useState(void 0);
1042
- React10__default.useEffect(() => {
925
+ const [lead, setLead] = React14__default.useState(void 0);
926
+ React14__default.useEffect(() => {
1043
927
  let alive = true;
1044
928
  const sb = getSupabaseClientOptional();
1045
929
  const tenantId = getActiveTenantId();
@@ -1206,14 +1090,14 @@ function NewConversationPanel({
1206
1090
  const create = useConversationsStore((s) => s.create);
1207
1091
  const config = useConversationsConfig();
1208
1092
  const guardConversations = useLimitGuard("conversations_month");
1209
- const [channel, setChannel] = React10__default.useState("whatsapp");
1210
- const [contact, setContact] = React10__default.useState(null);
1211
- const [typedHandle, setTypedHandle] = React10__default.useState("");
1212
- const [creatingContact, setCreatingContact] = React10__default.useState(false);
1213
- const [firstMessage, setFirstMessage] = React10__default.useState("");
1214
- const [submitting, setSubmitting] = React10__default.useState(false);
1215
- const [pickerKey, setPickerKey] = React10__default.useState(0);
1216
- React10__default.useEffect(() => {
1093
+ const [channel, setChannel] = React14__default.useState("whatsapp");
1094
+ const [contact, setContact] = React14__default.useState(null);
1095
+ const [typedHandle, setTypedHandle] = React14__default.useState("");
1096
+ const [creatingContact, setCreatingContact] = React14__default.useState(false);
1097
+ const [firstMessage, setFirstMessage] = React14__default.useState("");
1098
+ const [submitting, setSubmitting] = React14__default.useState(false);
1099
+ const [pickerKey, setPickerKey] = React14__default.useState(0);
1100
+ React14__default.useEffect(() => {
1217
1101
  if (open) {
1218
1102
  setChannel("whatsapp");
1219
1103
  setContact(null);
@@ -1260,8 +1144,10 @@ function NewConversationPanel({
1260
1144
  const Icon = CHANNEL_ICON[ch];
1261
1145
  const active = channel === ch;
1262
1146
  return /* @__PURE__ */ jsxs(
1263
- "button",
1147
+ Button,
1264
1148
  {
1149
+ variant: "bare",
1150
+ size: "bare",
1265
1151
  type: "button",
1266
1152
  onClick: () => setChannel(ch),
1267
1153
  "aria-pressed": active,
@@ -1307,14 +1193,14 @@ function NewConversationPanel({
1307
1193
  /* @__PURE__ */ jsxs("div", { children: [
1308
1194
  /* @__PURE__ */ jsx("label", { htmlFor: "conv-first-message", className: "mb-1.5 block text-xs font-medium text-muted-foreground", children: t("conversations.new.firstMessage") }),
1309
1195
  /* @__PURE__ */ jsx(
1310
- "textarea",
1196
+ Textarea,
1311
1197
  {
1312
1198
  id: "conv-first-message",
1313
1199
  value: firstMessage,
1314
1200
  onChange: (e) => setFirstMessage(e.target.value),
1315
1201
  rows: 3,
1316
1202
  placeholder: t("conversations.new.firstMessagePlaceholder"),
1317
- className: "max-h-40 min-h-[64px] w-full resize-none rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground outline-none focus:border-primary"
1203
+ className: "max-h-40 min-h-[64px] resize-none rounded-md border-border bg-background shadow-none focus:border-primary focus-visible:ring-0 focus-visible:ring-offset-0"
1318
1204
  }
1319
1205
  )
1320
1206
  ] })
@@ -1323,14 +1209,47 @@ function NewConversationPanel({
1323
1209
  ] })
1324
1210
  ] }) });
1325
1211
  }
1212
+ var porConta = /* @__PURE__ */ new Map();
1213
+ function assinarChegada(tenantId, ouvinte) {
1214
+ const supabase = getSupabaseClientOptional();
1215
+ if (!supabase?.channel || !tenantId) return () => {
1216
+ };
1217
+ let assinatura = porConta.get(tenantId);
1218
+ if (!assinatura) {
1219
+ const ouvintes = /* @__PURE__ */ new Set();
1220
+ const canal = supabase.channel(`conversas:${tenantId}`).on("broadcast", { event: "inbound" }, (msg2) => {
1221
+ const carga = { conversationId: msg2?.payload?.conversationId };
1222
+ for (const f of [...ouvintes]) {
1223
+ try {
1224
+ f(carga);
1225
+ } catch {
1226
+ }
1227
+ }
1228
+ }).subscribe();
1229
+ assinatura = { canal, ouvintes };
1230
+ porConta.set(tenantId, assinatura);
1231
+ }
1232
+ assinatura.ouvintes.add(ouvinte);
1233
+ return () => {
1234
+ const a = porConta.get(tenantId);
1235
+ if (!a) return;
1236
+ a.ouvintes.delete(ouvinte);
1237
+ if (a.ouvintes.size === 0) {
1238
+ porConta.delete(tenantId);
1239
+ void getSupabaseClientOptional()?.removeChannel?.(a.canal);
1240
+ }
1241
+ };
1242
+ }
1243
+
1244
+ // src/views/useCaixaAoVivo.ts
1326
1245
  function useCaixaAoVivo() {
1327
1246
  const tenantId = useActiveTenantId();
1328
1247
  const load = useConversationsStore((s) => s.load);
1329
1248
  const refreshMessages = useConversationsStore((s) => s.refreshMessages);
1330
1249
  const selectedId = useConversationsStore((s) => s.selectedId);
1331
- const abertoRef = React10.useRef(selectedId);
1250
+ const abertoRef = React14.useRef(selectedId);
1332
1251
  abertoRef.current = selectedId;
1333
- React10.useEffect(() => {
1252
+ React14.useEffect(() => {
1334
1253
  if (!tenantId) return;
1335
1254
  return assinarChegada(tenantId, ({ conversationId }) => {
1336
1255
  void load();
@@ -1340,19 +1259,24 @@ function useCaixaAoVivo() {
1340
1259
  });
1341
1260
  }, [tenantId, load, refreshMessages]);
1342
1261
  }
1343
- function InboxView() {
1262
+ function InboxView({ layout = "page", newSignal = 0 } = {}) {
1344
1263
  const t = useTranslation();
1264
+ const dock = layout !== "page";
1345
1265
  const { conversations, selectedId, deselect } = useConversationsStore((s) => s);
1346
1266
  useCaixaAoVivo();
1347
- const isWidePanel = useMediaQuery("(min-width: 1280px)");
1267
+ const isWideViewport = useMediaQuery("(min-width: 1280px)");
1268
+ const isWidePanel = isWideViewport && !dock;
1348
1269
  const railOpen = useRightRailStore((s) => s.open);
1349
- const [panelOpen, setPanelOpen] = React10__default.useState(false);
1350
- const [newOpen, setNewOpen] = React10__default.useState(false);
1270
+ const [panelOpen, setPanelOpen] = React14__default.useState(false);
1271
+ const [newOpen, setNewOpen] = React14__default.useState(false);
1351
1272
  const selected = conversations.find((c) => c.id === selectedId) ?? null;
1352
- React10__default.useEffect(() => {
1273
+ React14__default.useEffect(() => {
1274
+ if (newSignal > 0) setNewOpen(true);
1275
+ }, [newSignal]);
1276
+ React14__default.useEffect(() => {
1353
1277
  setPanelOpen(isWidePanel && !railOpen);
1354
1278
  }, [isWidePanel, railOpen]);
1355
- const newActions = React10__default.useMemo(() => [{
1279
+ const newActions = React14__default.useMemo(() => [{
1356
1280
  id: "new-conversation",
1357
1281
  label: t("conversations.list.new"),
1358
1282
  icon: SquarePen,
@@ -1364,10 +1288,14 @@ function InboxView() {
1364
1288
  onSelect: () => setNewOpen(true)
1365
1289
  }], [t]);
1366
1290
  return /* @__PURE__ */ jsxs(Fragment, { children: [
1367
- /* @__PURE__ */ jsx(PageHeaderActions, { children: /* @__PURE__ */ jsx(PageActions, { actions: newActions }) }),
1291
+ !dock && /* @__PURE__ */ jsx(PageHeaderActions, { children: /* @__PURE__ */ jsx(PageActions, { actions: newActions }) }),
1368
1292
  /* @__PURE__ */ jsx(NewConversationPanel, { open: newOpen, onOpenChange: setNewOpen }),
1369
1293
  /* @__PURE__ */ jsxs("div", { className: "relative flex h-full min-h-0 overflow-hidden bg-background", children: [
1370
- /* @__PURE__ */ jsx(ConversationList, { className: cn(selected ? "hidden lg:flex" : "flex") }),
1294
+ /* @__PURE__ */ jsx(ConversationList, { className: cn(
1295
+ layout === "page" && (selected ? "hidden lg:flex" : "flex"),
1296
+ layout === "dock" && (selected ? "hidden" : "flex lg:w-full border-r-0"),
1297
+ layout === "dock-wide" && "flex lg:w-[300px]"
1298
+ ) }),
1371
1299
  selected ? /* @__PURE__ */ jsx(
1372
1300
  MessageThread,
1373
1301
  {
@@ -1375,17 +1303,20 @@ function InboxView() {
1375
1303
  panelOpen,
1376
1304
  onTogglePanel: () => setPanelOpen((v) => !v),
1377
1305
  onBack: deselect,
1306
+ backAlways: layout === "dock",
1378
1307
  className: "flex"
1379
1308
  }
1380
- ) : /* @__PURE__ */ jsxs("section", { className: "hidden min-w-0 flex-1 flex-col items-center justify-center bg-muted/20 text-muted-foreground lg:flex", children: [
1309
+ ) : layout === "dock" ? null : /* @__PURE__ */ jsxs("section", { className: cn("min-w-0 flex-1 flex-col items-center justify-center bg-muted/20 text-muted-foreground", dock ? "flex" : "hidden lg:flex"), children: [
1381
1310
  /* @__PURE__ */ jsx(MessageSquare, { className: "h-9 w-9" }),
1382
1311
  /* @__PURE__ */ jsx("p", { className: "mt-2 text-sm", children: t("conversations.empty.select") })
1383
1312
  ] }),
1384
1313
  selected && panelOpen && /* @__PURE__ */ jsxs(Fragment, { children: [
1385
1314
  /* @__PURE__ */ jsx(
1386
- "button",
1315
+ Button,
1387
1316
  {
1388
- className: "absolute inset-0 z-10 bg-black/40 xl:hidden",
1317
+ variant: "bare",
1318
+ size: "bare",
1319
+ className: cn("absolute inset-0 z-10 rounded-none bg-black/40", !dock && "xl:hidden"),
1389
1320
  "aria-label": "Close details",
1390
1321
  onClick: () => setPanelOpen(false)
1391
1322
  }
@@ -1395,13 +1326,229 @@ function InboxView() {
1395
1326
  {
1396
1327
  contact: selected,
1397
1328
  onClose: () => setPanelOpen(false),
1398
- className: "absolute inset-y-0 right-0 z-20 w-[340px] max-w-[88%] shadow-2xl xl:static xl:z-auto xl:w-[300px] xl:max-w-none xl:shadow-none"
1329
+ className: cn("absolute inset-y-0 right-0 z-20 w-[340px] max-w-[88%] shadow-2xl", !dock && "xl:static xl:z-auto xl:w-[300px] xl:max-w-none xl:shadow-none")
1399
1330
  }
1400
1331
  )
1401
1332
  ] })
1402
1333
  ] })
1403
1334
  ] });
1404
1335
  }
1336
+ var CONNECTORS_ROUTE = "/settings/conversations/connectors";
1337
+ var SPLIT_AT = 720;
1338
+ function ConversationsRailPanel({ store: store2, config }) {
1339
+ const t = useTranslation();
1340
+ const ref = React14.useRef(null);
1341
+ const [width, setWidth] = React14.useState(0);
1342
+ const [newSignal, setNewSignal] = React14.useState(0);
1343
+ const connected = useChannelConnections();
1344
+ const closeRail = useRightRailStore((s) => s.close);
1345
+ const wide = width >= SPLIT_AT;
1346
+ React14.useLayoutEffect(() => {
1347
+ const el = ref.current;
1348
+ if (!el) return;
1349
+ const ro = new ResizeObserver(([e]) => setWidth(e.contentRect.width));
1350
+ ro.observe(el);
1351
+ return () => ro.disconnect();
1352
+ }, []);
1353
+ React14.useEffect(() => {
1354
+ const narrow = (ref.current?.getBoundingClientRect().width ?? 0) < SPLIT_AT;
1355
+ if (narrow) store2.getState().deselect();
1356
+ void store2.getState().load({ autoSelect: !narrow });
1357
+ }, [store2]);
1358
+ const go = (route) => {
1359
+ closeRail();
1360
+ adminNavigateTo(route);
1361
+ };
1362
+ useRightRailMenu([
1363
+ { id: "new", label: t("conversations.list.new"), icon: SquarePen, onSelect: () => setNewSignal((n) => n + 1) },
1364
+ { id: "full", label: tl("Open full screen", "Abrir em tela cheia"), icon: ExternalLink, onSelect: () => go("/conversations") }
1365
+ ]);
1366
+ return /* @__PURE__ */ jsxs("div", { ref, className: "flex h-full min-h-0 flex-col", children: [
1367
+ /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1", children: /* @__PURE__ */ jsx(ConversationsContextProvider, { store: store2, config, children: /* @__PURE__ */ jsx(InboxView, { layout: wide ? "dock-wide" : "dock", newSignal }) }) }),
1368
+ /* @__PURE__ */ jsx("div", { className: "shrink-0 border-t border-border p-3", children: connected === void 0 ? /* @__PURE__ */ jsx("div", { className: "h-10", "aria-hidden": true }) : connected.whatsapp ? /* @__PURE__ */ jsxs(
1369
+ Button,
1370
+ {
1371
+ variant: "bare",
1372
+ size: "bare",
1373
+ type: "button",
1374
+ onClick: () => go(CONNECTORS_ROUTE),
1375
+ className: "font-normal whitespace-normal justify-start fz-tap flex h-10 w-full items-center gap-2 rounded-button px-2 text-sm text-muted-foreground transition-colors hover:bg-accent hover:text-foreground",
1376
+ children: [
1377
+ /* @__PURE__ */ jsx(CheckCircle2, { className: "h-4 w-4 text-[#25D366]", "aria-hidden": true }),
1378
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-left", children: tl("WhatsApp connected", "WhatsApp conectado") }),
1379
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-primary", children: tl("Manage", "Gerenciar") })
1380
+ ]
1381
+ }
1382
+ ) : /* @__PURE__ */ jsxs(
1383
+ Button,
1384
+ {
1385
+ variant: "bare",
1386
+ size: "bare",
1387
+ type: "button",
1388
+ onClick: () => go(CONNECTORS_ROUTE),
1389
+ "data-testid": "inbox-connect-whatsapp",
1390
+ className: "font-normal whitespace-normal fz-tap flex h-11 w-full items-center justify-center gap-2 rounded-button bg-[#25D366] text-sm font-semibold text-white shadow-sm transition-colors hover:bg-[#1EBE5A] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#25D366] focus-visible:ring-offset-2",
1391
+ children: [
1392
+ /* @__PURE__ */ jsx(WhatsAppMark, { className: "h-[18px] w-[18px]" }),
1393
+ tl("Connect WhatsApp", "Conectar WhatsApp")
1394
+ ]
1395
+ }
1396
+ ) })
1397
+ ] });
1398
+ }
1399
+ function WhatsAppMark({ className }) {
1400
+ return /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "currentColor", className, "aria-hidden": true, children: /* @__PURE__ */ jsx("path", { d: "M17.47 14.38c-.3-.15-1.76-.87-2.03-.97-.27-.1-.47-.15-.67.15-.2.3-.77.96-.94 1.16-.17.2-.35.22-.65.07-.3-.15-1.26-.46-2.4-1.48-.89-.79-1.49-1.77-1.66-2.07-.17-.3-.02-.46.13-.61.13-.13.3-.35.45-.52.15-.17.2-.3.3-.5.1-.2.05-.37-.02-.52-.08-.15-.67-1.61-.92-2.2-.24-.58-.49-.5-.67-.51h-.57c-.2 0-.52.07-.79.37-.27.3-1.04 1.02-1.04 2.48s1.07 2.88 1.21 3.08c.15.2 2.1 3.2 5.08 4.49.71.31 1.26.49 1.69.63.71.22 1.36.19 1.87.12.57-.09 1.76-.72 2.01-1.41.25-.7.25-1.29.17-1.41-.07-.13-.27-.2-.57-.35zM12.05 21.5h-.01a9.4 9.4 0 0 1-4.8-1.32l-.34-.2-3.56.93.95-3.47-.22-.36a9.4 9.4 0 0 1-1.44-5.02c0-5.2 4.23-9.43 9.43-9.43 2.52 0 4.89.98 6.67 2.77a9.37 9.37 0 0 1 2.76 6.67c0 5.2-4.23 9.43-9.44 9.43zm8.03-17.46A11.3 11.3 0 0 0 12.05.72C5.79.72.7 5.81.7 12.07c0 2 .52 3.95 1.52 5.67L.6 23.6l6-1.57a11.32 11.32 0 0 0 5.44 1.39h.01c6.26 0 11.35-5.1 11.35-11.35 0-3.03-1.18-5.88-3.32-8.03z" }) });
1401
+ }
1402
+
1403
+ // src/data/tables.ts
1404
+ var T = {
1405
+ conversations: "plg_conversations",
1406
+ /** A LEITURA da conversa: a mesma linha, com o dono do lead junto. A escrita
1407
+ * continua na tabela — view com `security_invoker` não aceita INSERT aqui. */
1408
+ conversationsView: "v_conversations",
1409
+ messages: "plg_conversation_messages",
1410
+ channels: "plg_conversations_channels"
1411
+ };
1412
+ function tocar() {
1413
+ try {
1414
+ const Ctx = window.AudioContext ?? window.webkitAudioContext;
1415
+ if (!Ctx) return;
1416
+ const ctx = new Ctx();
1417
+ const agora = ctx.currentTime;
1418
+ for (const [freq, atraso] of [[880, 0], [1320, 0.09]]) {
1419
+ const osc = ctx.createOscillator();
1420
+ const vol = ctx.createGain();
1421
+ osc.type = "sine";
1422
+ osc.frequency.value = freq;
1423
+ vol.gain.setValueAtTime(1e-4, agora + atraso);
1424
+ vol.gain.exponentialRampToValueAtTime(0.18, agora + atraso + 0.02);
1425
+ vol.gain.exponentialRampToValueAtTime(1e-4, agora + atraso + 0.28);
1426
+ osc.connect(vol);
1427
+ vol.connect(ctx.destination);
1428
+ osc.start(agora + atraso);
1429
+ osc.stop(agora + atraso + 0.3);
1430
+ }
1431
+ setTimeout(() => void ctx.close().catch(() => {
1432
+ }), 900);
1433
+ } catch {
1434
+ }
1435
+ }
1436
+ async function pedirPermissao() {
1437
+ if (typeof Notification === "undefined") return false;
1438
+ if (Notification.permission === "granted") return true;
1439
+ if (Notification.permission === "denied") return false;
1440
+ try {
1441
+ return await Notification.requestPermission() === "granted";
1442
+ } catch {
1443
+ return false;
1444
+ }
1445
+ }
1446
+ function avisarNoSistema(titulo, corpo, tag) {
1447
+ try {
1448
+ if (typeof Notification === "undefined" || Notification.permission !== "granted") return;
1449
+ if (typeof document !== "undefined" && document.visibilityState === "visible") return;
1450
+ const n = new Notification(titulo, { body: corpo, tag, renotify: true });
1451
+ n.onclick = () => {
1452
+ try {
1453
+ window.focus();
1454
+ n.close();
1455
+ } catch {
1456
+ }
1457
+ };
1458
+ } catch {
1459
+ }
1460
+ }
1461
+ var RAIL_ID = "conversations";
1462
+ function InboxWatcher({ label, route = "/conversations", store: store2, config, inHeader }) {
1463
+ const Panel = React14.useMemo(() => {
1464
+ if (!store2) return null;
1465
+ const C = () => React14.createElement(ConversationsRailPanel, { store: store2, config });
1466
+ C.displayName = "ConversationsRailPanel";
1467
+ return C;
1468
+ }, [store2, config]);
1469
+ const railOpen = useRightRailStore((s) => s.open && s.active === RAIL_ID);
1470
+ const togglePanel = useRightRailStore((s) => s.togglePanel);
1471
+ const tenantId = useActiveTenantId();
1472
+ const path = useAdminPath();
1473
+ const [naoLidas, setNaoLidas] = React14.useState(0);
1474
+ const podeTocar = React14.useRef(false);
1475
+ React14.useEffect(() => {
1476
+ const liberar = () => {
1477
+ podeTocar.current = true;
1478
+ void pedirPermissao();
1479
+ };
1480
+ for (const ev of ["pointerdown", "keydown", "touchstart"]) {
1481
+ window.addEventListener(ev, liberar, { once: true, passive: true });
1482
+ }
1483
+ return () => {
1484
+ for (const ev of ["pointerdown", "keydown", "touchstart"]) window.removeEventListener(ev, liberar);
1485
+ };
1486
+ }, []);
1487
+ const contar = React14.useCallback(async () => {
1488
+ const supabase = getSupabaseClientOptional();
1489
+ if (!supabase) return;
1490
+ const { data } = await supabase.from(T.conversations).select("id, status, last_message_direction, unread_count").eq("status", "open");
1491
+ if (!data) return;
1492
+ const n = data.filter((r) => isWaitingOnUs({
1493
+ status: r.status,
1494
+ lastMessageDirection: r.last_message_direction ?? void 0,
1495
+ unreadCount: r.unread_count ?? 0
1496
+ })).length;
1497
+ setNaoLidas(n);
1498
+ }, []);
1499
+ React14.useEffect(() => {
1500
+ void contar();
1501
+ }, [contar]);
1502
+ React14.useEffect(() => {
1503
+ if (!tenantId) return;
1504
+ return assinarChegada(tenantId, ({ conversationId }) => {
1505
+ void contar();
1506
+ if (podeTocar.current) tocar();
1507
+ void (async () => {
1508
+ const supabase = getSupabaseClientOptional();
1509
+ let titulo = label;
1510
+ let corpo = "";
1511
+ if (conversationId && supabase) {
1512
+ const { data } = await supabase.from(T.conversations).select("contact_name, last_message_preview").eq("id", conversationId).maybeSingle();
1513
+ titulo = data?.contact_name || label;
1514
+ corpo = String(data?.last_message_preview || "").replace(/\s+/g, " ").slice(0, 140);
1515
+ }
1516
+ avisarNoSistema(titulo, corpo, String(conversationId ?? "inbox"));
1517
+ })();
1518
+ });
1519
+ }, [contar, tenantId, label]);
1520
+ useRightRailPanel(React14.useMemo(() => Panel ? {
1521
+ id: RAIL_ID,
1522
+ label,
1523
+ icon: MessageCircle,
1524
+ order: 12,
1525
+ badge: { count: naoLidas, tone: "destructive" },
1526
+ Component: Panel
1527
+ } : null, [Panel, label, naoLidas]));
1528
+ const naPagina = path === route || path.startsWith(`${route}/`);
1529
+ const aberta = naPagina || railOpen;
1530
+ const surface = useWidgetSurface();
1531
+ const named = !inHeader && surface === "rail";
1532
+ return /* @__PURE__ */ jsxs(
1533
+ Button,
1534
+ {
1535
+ variant: "bare",
1536
+ size: "bare",
1537
+ type: "button",
1538
+ onClick: () => Panel && !naPagina ? togglePanel(RAIL_ID) : adminNavigateTo(route),
1539
+ "aria-expanded": Panel ? railOpen : void 0,
1540
+ "aria-label": naoLidas > 0 ? `${label} (${naoLidas})` : label,
1541
+ title: label,
1542
+ "aria-current": aberta ? "page" : void 0,
1543
+ className: `fz-tap relative inline-flex shrink-0 items-center rounded-button transition-colors hover:bg-accent ${named ? "w-full gap-3 px-2.5 py-2.5" : "justify-center p-2"} ${aberta ? "text-foreground" : "text-muted-foreground hover:text-foreground"}`,
1544
+ children: [
1545
+ /* @__PURE__ */ jsx(MessageCircle, { className: "h-4 w-4 shrink-0" }),
1546
+ named && /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-left", children: label }),
1547
+ 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 })
1548
+ ]
1549
+ }
1550
+ );
1551
+ }
1405
1552
  var CHANNELS2 = ["sms", "whatsapp", "instagram", "email", "webchat"];
1406
1553
  function requestedContact() {
1407
1554
  try {
@@ -1421,7 +1568,7 @@ function requestedContact() {
1421
1568
  }
1422
1569
  }
1423
1570
  function ConversationsPage({ store: store2, config }) {
1424
- React10__default.useEffect(() => {
1571
+ React14__default.useEffect(() => {
1425
1572
  const wanted = requestedContact();
1426
1573
  if (!wanted) {
1427
1574
  void store2.getState().load();
@@ -1439,6 +1586,87 @@ function ConversationsPage({ store: store2, config }) {
1439
1586
  }, []);
1440
1587
  return /* @__PURE__ */ jsx(ConversationsContextProvider, { store: store2, config, children: /* @__PURE__ */ jsx(InboxView, {}) });
1441
1588
  }
1589
+ var money = (n) => formatCurrency(n, "BRL");
1590
+ var PROMISES = [
1591
+ ["Answers your clients on WhatsApp, day and night", "Responde seus clientes no WhatsApp, de dia e de noite"],
1592
+ ["Books, confirms and takes orders by itself", "Agenda, confirma e anota pedidos sozinho"],
1593
+ ["Hands over to a person when it should", "Passa para uma pessoa quando precisa"],
1594
+ ["Learns your prices, hours and services", "Aprende seus pre\xE7os, hor\xE1rios e servi\xE7os"]
1595
+ ];
1596
+ function AtendeAIPage() {
1597
+ const runtime = usePluginRuntimeOptional();
1598
+ const plugin = runtime?.plugins.find((p) => p.id === "conversations");
1599
+ const on = plugin?.config?.facets?.atendeai === true;
1600
+ const price = PRICE_BOOK["conversations.atendeai"];
1601
+ const go = (hash) => {
1602
+ window.location.hash = hash;
1603
+ };
1604
+ if (!on) {
1605
+ return /* @__PURE__ */ jsxs("div", { className: "mx-auto max-w-3xl space-y-4", children: [
1606
+ /* @__PURE__ */ jsx(Card, { className: "overflow-hidden border-violet-200 dark:border-violet-900", children: /* @__PURE__ */ jsxs("div", { className: "space-y-5 bg-gradient-to-br from-violet-50 to-transparent p-6 dark:from-violet-950/40", children: [
1607
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
1608
+ /* @__PURE__ */ jsx("span", { className: "flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-violet-100 text-violet-700 dark:bg-violet-900/60 dark:text-violet-200", children: /* @__PURE__ */ jsx(Bot, { className: "h-6 w-6", "aria-hidden": true }) }),
1609
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 space-y-1", children: [
1610
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
1611
+ /* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold", children: "AtendeAI" }),
1612
+ /* @__PURE__ */ jsxs(Badge, { className: "border-transparent bg-violet-100 text-violet-700 dark:bg-violet-900/60 dark:text-violet-200", children: [
1613
+ /* @__PURE__ */ jsx(Sparkles, { className: "mr-1 h-3 w-3", "aria-hidden": true }),
1614
+ tl("AI", "IA")
1615
+ ] })
1616
+ ] }),
1617
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: tl(
1618
+ "An attendant that never sleeps: it talks to your clients on WhatsApp and only calls your team when it matters.",
1619
+ "Um atendente que n\xE3o dorme: conversa com seus clientes no WhatsApp e s\xF3 chama a sua equipe quando importa."
1620
+ ) })
1621
+ ] })
1622
+ ] }),
1623
+ /* @__PURE__ */ jsx("ul", { className: "grid gap-2 sm:grid-cols-2", children: PROMISES.map(([en2, pt]) => /* @__PURE__ */ jsxs("li", { className: "flex items-start gap-2 text-sm", children: [
1624
+ /* @__PURE__ */ jsx(Check, { className: "mt-0.5 h-4 w-4 shrink-0 text-violet-600 dark:text-violet-300", "aria-hidden": true }),
1625
+ /* @__PURE__ */ jsx("span", { children: tl(en2, pt) })
1626
+ ] }, pt)) }),
1627
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between", children: [
1628
+ price && /* @__PURE__ */ jsxs("p", { className: "text-sm", children: [
1629
+ /* @__PURE__ */ jsx("span", { className: "text-2xl font-bold tabular-nums", children: money(price.price) }),
1630
+ /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
1631
+ " ",
1632
+ price.per === "unit" ? tl("per unit / month", "por unidade / m\xEAs") : tl("/ month", "/ m\xEAs")
1633
+ ] })
1634
+ ] }),
1635
+ /* @__PURE__ */ jsxs(Button, { className: "h-11 bg-violet-600 text-white hover:bg-violet-700", onClick: () => go("/settings/modules"), children: [
1636
+ /* @__PURE__ */ jsx(Sparkles, { className: "h-4 w-4", "aria-hidden": true }),
1637
+ tl("Turn on AtendeAI", "Ligar o AtendeAI")
1638
+ ] })
1639
+ ] })
1640
+ ] }) }),
1641
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: tl("Turned on in Settings \u203A Modules; its settings appear right after.", "Liga em Configura\xE7\xF5es \u203A M\xF3dulos; as configura\xE7\xF5es dele aparecem logo em seguida.") })
1642
+ ] });
1643
+ }
1644
+ return /* @__PURE__ */ jsxs("div", { className: "mx-auto max-w-3xl space-y-4", children: [
1645
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap justify-end gap-2", children: [
1646
+ /* @__PURE__ */ jsx(Button, { variant: "ghost", className: "h-11", onClick: () => go("/settings/modules"), children: tl("Manage or turn off", "Gerenciar ou desligar") }),
1647
+ /* @__PURE__ */ jsxs(Button, { variant: "outline", className: "h-11", onClick: () => go("/settings/conversations-atendeai"), children: [
1648
+ /* @__PURE__ */ jsx(Settings2, { className: "h-4 w-4", "aria-hidden": true }),
1649
+ tl("AtendeAI settings", "Configura\xE7\xF5es do AtendeAI")
1650
+ ] })
1651
+ ] }),
1652
+ /* @__PURE__ */ jsx(
1653
+ ComingSoon,
1654
+ {
1655
+ title: tl("AtendeAI operation", "Opera\xE7\xE3o do AtendeAI"),
1656
+ summary: tl(
1657
+ "Follow what the AI is answering, accept or correct its drafts and teach it new answers.",
1658
+ "Acompanhe o que a IA est\xE1 respondendo, aceite ou corrija os rascunhos e ensine respostas novas."
1659
+ ),
1660
+ v1Path: "AtendeAI \u2192 Opera\xE7\xE3o",
1661
+ missing: [
1662
+ tl("The WhatsApp number of each unit connected", "O n\xFAmero de WhatsApp de cada unidade conectado"),
1663
+ tl("The AI answering engine (personality, Q&A, capabilities)", "O motor de respostas da IA (personalidade, perguntas e respostas, capacidades)"),
1664
+ tl("The queue of drafts waiting for acceptance", "A fila de rascunhos esperando aceite")
1665
+ ]
1666
+ }
1667
+ )
1668
+ ] });
1669
+ }
1442
1670
 
1443
1671
  // src/data/accents.ts
1444
1672
  var CHANNEL_ACCENT_HEX = {
@@ -1967,14 +2195,14 @@ function createConversationsStore(provider) {
1967
2195
  search: "",
1968
2196
  loading: false,
1969
2197
  sending: false,
1970
- async load() {
2198
+ async load(opts) {
1971
2199
  set({ loading: true });
1972
2200
  const conversations = await provider.listConversations({
1973
2201
  channel: get().channelFilter,
1974
2202
  leadOwnerId: get().leadOwnerFilter,
1975
2203
  search: get().search || void 0
1976
2204
  });
1977
- const selectedId = get().selectedId ?? conversations[0]?.id ?? null;
2205
+ const selectedId = get().selectedId ?? (opts?.autoSelect === false ? null : conversations[0]?.id ?? null);
1978
2206
  set({ conversations, loading: false, selectedId });
1979
2207
  if (selectedId) await get().select(selectedId);
1980
2208
  },
@@ -2119,6 +2347,7 @@ var en = {
2119
2347
  "conversations.list.search": "Search conversations",
2120
2348
  "conversations.list.loading": "Loading\u2026",
2121
2349
  "conversations.list.empty": "No conversations",
2350
+ "conversations.filter.notConnected": "Account not connected yet",
2122
2351
  "conversations.list.new": "New conversation",
2123
2352
  // Channel filters
2124
2353
  "conversations.filter.all": "All",
@@ -2215,6 +2444,7 @@ var ptBR = {
2215
2444
  "conversations.list.search": "Buscar conversas",
2216
2445
  "conversations.list.loading": "Carregando\u2026",
2217
2446
  "conversations.list.empty": "Nenhuma conversa",
2447
+ "conversations.filter.notConnected": "Conta ainda n\xE3o conectada",
2218
2448
  "conversations.list.new": "Nova conversa",
2219
2449
  // Filtros de canal
2220
2450
  "conversations.filter.all": "Todas",
@@ -2509,23 +2739,24 @@ function NumberRow({ n }) {
2509
2739
  !n.ready && /* @__PURE__ */ jsx("span", { className: "text-warning", children: "n\xE3o pronto para enviar" })
2510
2740
  ] });
2511
2741
  }
2512
- function WhatsAppProviders() {
2513
- const [rows, setRows] = React10__default.useState(null);
2514
- const [draft, setDraft] = React10__default.useState({});
2515
- const [busy, setBusy] = React10__default.useState(null);
2516
- const [numbers, setNumbers] = React10__default.useState({});
2517
- const [qr, setQr] = React10__default.useState(null);
2518
- const load = React10__default.useCallback(async () => {
2742
+ function WhatsAppProviders({ only, onChanged } = {}) {
2743
+ const [rows, setRows] = React14__default.useState(null);
2744
+ const [draft, setDraft] = React14__default.useState({});
2745
+ const [busy, setBusy] = React14__default.useState(null);
2746
+ const [numbers, setNumbers] = React14__default.useState({});
2747
+ const [qr, setQr] = React14__default.useState(null);
2748
+ const load = React14__default.useCallback(async () => {
2519
2749
  try {
2520
2750
  setRows(await providersApi.list());
2521
2751
  } catch {
2522
2752
  setRows([]);
2523
2753
  }
2524
- }, []);
2525
- React10__default.useEffect(() => {
2754
+ onChanged?.();
2755
+ }, [onChanged]);
2756
+ React14__default.useEffect(() => {
2526
2757
  void load();
2527
2758
  }, [load]);
2528
- const byId = React10__default.useMemo(
2759
+ const byId = React14__default.useMemo(
2529
2760
  () => Object.fromEntries((rows ?? []).map((r) => [r.provider, r])),
2530
2761
  [rows]
2531
2762
  );
@@ -2581,99 +2812,178 @@ function WhatsAppProviders() {
2581
2812
  setQr({ provider: p, image: d.qr, code: d.code });
2582
2813
  if (!d.qr && !d.code) toast.error("A inst\xE2ncia n\xE3o devolveu QR \u2014 j\xE1 est\xE1 pareada?");
2583
2814
  });
2584
- return /* @__PURE__ */ jsxs(
2815
+ const body = /* @__PURE__ */ jsxs(Fragment, { children: [
2816
+ rows === null && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 py-6 text-sm text-muted-foreground", children: [
2817
+ /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin" }),
2818
+ " carregando\u2026"
2819
+ ] }),
2820
+ rows !== null && (only ? [only] : ALL).map((p) => {
2821
+ const row = byId[p];
2822
+ const copy = COPY[p];
2823
+ const active = Boolean(row?.isActive);
2824
+ const caps = row?.capabilities ?? {};
2825
+ return /* @__PURE__ */ jsxs(
2826
+ "div",
2827
+ {
2828
+ className: `rounded-card border p-4 my-2 ${active ? "border-primary bg-primary/[0.03]" : "bg-card"}`,
2829
+ "data-testid": `provider-${p}`,
2830
+ children: [
2831
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
2832
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-semibold", children: copy.name }),
2833
+ active && /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-primary px-2 py-0.5 text-[10px] font-medium text-primary-foreground", children: [
2834
+ /* @__PURE__ */ jsx(Check, { className: "h-3 w-3" }),
2835
+ " valendo agora"
2836
+ ] }),
2837
+ /* @__PURE__ */ jsx(StatusChip, { row }),
2838
+ row?.hasCredential && /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1 text-[10px] text-muted-foreground", children: [
2839
+ /* @__PURE__ */ jsx(ShieldCheck, { className: "h-3 w-3" }),
2840
+ " chave guardada"
2841
+ ] }),
2842
+ caps.ban_risk === "high" && /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1 text-[10px] text-warning", children: [
2843
+ /* @__PURE__ */ jsx(ShieldAlert, { className: "h-3 w-3" }),
2844
+ " risco de banimento"
2845
+ ] })
2846
+ ] }),
2847
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: copy.what }),
2848
+ /* @__PURE__ */ jsxs("div", { className: "mt-2 grid gap-1 sm:grid-cols-2", children: [
2849
+ /* @__PURE__ */ jsxs("p", { className: "text-[11px] text-success-soft-foreground", children: [
2850
+ "\u2713 ",
2851
+ copy.strength
2852
+ ] }),
2853
+ /* @__PURE__ */ jsxs("p", { className: "text-[11px] text-warning", children: [
2854
+ "! ",
2855
+ copy.cost
2856
+ ] })
2857
+ ] }),
2858
+ /* @__PURE__ */ jsx("div", { className: "mt-3 grid gap-2 sm:grid-cols-2", children: copy.fields.map((f) => /* @__PURE__ */ jsxs("label", { className: "block", children: [
2859
+ /* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground", children: f.label }),
2860
+ /* @__PURE__ */ jsx(
2861
+ Input,
2862
+ {
2863
+ className: "mt-0.5 h-8 text-sm",
2864
+ type: f.secret ? "password" : "text",
2865
+ autoComplete: "off",
2866
+ placeholder: f.secret && row?.hasCredential ? "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022 (guardada)" : f.placeholder,
2867
+ value: field(p, f.key),
2868
+ onChange: (e) => setField(p, f.key, e.target.value)
2869
+ }
2870
+ )
2871
+ ] }, f.key)) }),
2872
+ /* @__PURE__ */ jsxs("div", { className: "mt-3 flex flex-wrap gap-2", children: [
2873
+ /* @__PURE__ */ jsx(Button, { size: "sm", variant: "outline", disabled: busy === `save:${p}`, onClick: () => save(p), children: busy === `save:${p}` ? /* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 animate-spin" }) : "Guardar" }),
2874
+ /* @__PURE__ */ jsx(Button, { size: "sm", variant: "outline", disabled: !row?.hasCredential || busy === `test:${p}`, onClick: () => test(p), children: busy === `test:${p}` ? /* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 animate-spin" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
2875
+ /* @__PURE__ */ jsx(RefreshCw, { className: "mr-1 h-3 w-3" }),
2876
+ " Testar"
2877
+ ] }) }),
2878
+ /* @__PURE__ */ jsx(Button, { size: "sm", variant: "outline", disabled: !row?.hasCredential || busy === `numbers:${p}`, onClick: () => refreshNumbers(p), children: "N\xFAmeros" }),
2879
+ caps.qr_pairing && /* @__PURE__ */ jsxs(Button, { size: "sm", variant: "outline", disabled: !row?.hasCredential || busy === `qr:${p}`, onClick: () => pair(p), children: [
2880
+ /* @__PURE__ */ jsx(QrCode, { className: "mr-1 h-3 w-3" }),
2881
+ " Parear"
2882
+ ] }),
2883
+ !active && /* @__PURE__ */ jsxs(Button, { size: "sm", disabled: !row?.hasCredential || busy === `on:${p}`, onClick: () => activate(p), children: [
2884
+ /* @__PURE__ */ jsx(Zap, { className: "mr-1 h-3 w-3" }),
2885
+ " Usar este"
2886
+ ] })
2887
+ ] }),
2888
+ numbers[p]?.length ? /* @__PURE__ */ jsx("div", { className: "mt-3 border-t pt-2", children: numbers[p].map((n) => /* @__PURE__ */ jsx(NumberRow, { n }, n.id)) }) : null,
2889
+ qr?.provider === p && /* @__PURE__ */ jsxs("div", { className: "mt-3 border-t pt-3", children: [
2890
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground mb-2", children: "Abra o WhatsApp no celular \u2192 Aparelhos conectados \u2192 Conectar aparelho." }),
2891
+ qr.image && /* @__PURE__ */ jsx("img", { src: qr.image, alt: "QR", className: "h-48 w-48 rounded border bg-white p-2" }),
2892
+ qr.code && /* @__PURE__ */ jsx("p", { className: "mt-2 font-mono text-lg tracking-widest", children: qr.code })
2893
+ ] })
2894
+ ]
2895
+ },
2896
+ p
2897
+ );
2898
+ })
2899
+ ] });
2900
+ return only ? body : /* @__PURE__ */ jsx(
2585
2901
  SettingsGroup,
2586
2902
  {
2587
2903
  title: "WhatsApp \u2014 por onde as mensagens saem",
2588
2904
  description: "Dois caminhos, um valendo de cada vez. Troque quando quiser; a configura\xE7\xE3o do outro fica guardada.",
2589
- children: [
2590
- rows === null && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 py-6 text-sm text-muted-foreground", children: [
2591
- /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin" }),
2592
- " carregando\u2026"
2593
- ] }),
2594
- rows !== null && ALL.map((p) => {
2595
- const row = byId[p];
2596
- const copy = COPY[p];
2597
- const active = Boolean(row?.isActive);
2598
- const caps = row?.capabilities ?? {};
2599
- return /* @__PURE__ */ jsxs(
2600
- "div",
2601
- {
2602
- className: `rounded-card border p-4 my-2 ${active ? "border-primary bg-primary/[0.03]" : "bg-card"}`,
2603
- "data-testid": `provider-${p}`,
2604
- children: [
2605
- /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
2606
- /* @__PURE__ */ jsx("span", { className: "text-sm font-semibold", children: copy.name }),
2607
- active && /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-primary px-2 py-0.5 text-[10px] font-medium text-primary-foreground", children: [
2608
- /* @__PURE__ */ jsx(Check, { className: "h-3 w-3" }),
2609
- " valendo agora"
2610
- ] }),
2611
- /* @__PURE__ */ jsx(StatusChip, { row }),
2612
- row?.hasCredential && /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1 text-[10px] text-muted-foreground", children: [
2613
- /* @__PURE__ */ jsx(ShieldCheck, { className: "h-3 w-3" }),
2614
- " chave guardada"
2615
- ] }),
2616
- caps.ban_risk === "high" && /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1 text-[10px] text-warning", children: [
2617
- /* @__PURE__ */ jsx(ShieldAlert, { className: "h-3 w-3" }),
2618
- " risco de banimento"
2619
- ] })
2620
- ] }),
2621
- /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: copy.what }),
2622
- /* @__PURE__ */ jsxs("div", { className: "mt-2 grid gap-1 sm:grid-cols-2", children: [
2623
- /* @__PURE__ */ jsxs("p", { className: "text-[11px] text-success-soft-foreground", children: [
2624
- "\u2713 ",
2625
- copy.strength
2626
- ] }),
2627
- /* @__PURE__ */ jsxs("p", { className: "text-[11px] text-warning", children: [
2628
- "! ",
2629
- copy.cost
2630
- ] })
2631
- ] }),
2632
- /* @__PURE__ */ jsx("div", { className: "mt-3 grid gap-2 sm:grid-cols-2", children: copy.fields.map((f) => /* @__PURE__ */ jsxs("label", { className: "block", children: [
2633
- /* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground", children: f.label }),
2634
- /* @__PURE__ */ jsx(
2635
- Input,
2636
- {
2637
- className: "mt-0.5 h-8 text-sm",
2638
- type: f.secret ? "password" : "text",
2639
- autoComplete: "off",
2640
- placeholder: f.secret && row?.hasCredential ? "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022 (guardada)" : f.placeholder,
2641
- value: field(p, f.key),
2642
- onChange: (e) => setField(p, f.key, e.target.value)
2643
- }
2644
- )
2645
- ] }, f.key)) }),
2646
- /* @__PURE__ */ jsxs("div", { className: "mt-3 flex flex-wrap gap-2", children: [
2647
- /* @__PURE__ */ jsx(Button, { size: "sm", variant: "outline", disabled: busy === `save:${p}`, onClick: () => save(p), children: busy === `save:${p}` ? /* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 animate-spin" }) : "Guardar" }),
2648
- /* @__PURE__ */ jsx(Button, { size: "sm", variant: "outline", disabled: !row?.hasCredential || busy === `test:${p}`, onClick: () => test(p), children: busy === `test:${p}` ? /* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 animate-spin" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
2649
- /* @__PURE__ */ jsx(RefreshCw, { className: "mr-1 h-3 w-3" }),
2650
- " Testar"
2651
- ] }) }),
2652
- /* @__PURE__ */ jsx(Button, { size: "sm", variant: "outline", disabled: !row?.hasCredential || busy === `numbers:${p}`, onClick: () => refreshNumbers(p), children: "N\xFAmeros" }),
2653
- caps.qr_pairing && /* @__PURE__ */ jsxs(Button, { size: "sm", variant: "outline", disabled: !row?.hasCredential || busy === `qr:${p}`, onClick: () => pair(p), children: [
2654
- /* @__PURE__ */ jsx(QrCode, { className: "mr-1 h-3 w-3" }),
2655
- " Parear"
2656
- ] }),
2657
- !active && /* @__PURE__ */ jsxs(Button, { size: "sm", disabled: !row?.hasCredential || busy === `on:${p}`, onClick: () => activate(p), children: [
2658
- /* @__PURE__ */ jsx(Zap, { className: "mr-1 h-3 w-3" }),
2659
- " Usar este"
2660
- ] })
2661
- ] }),
2662
- numbers[p]?.length ? /* @__PURE__ */ jsx("div", { className: "mt-3 border-t pt-2", children: numbers[p].map((n) => /* @__PURE__ */ jsx(NumberRow, { n }, n.id)) }) : null,
2663
- qr?.provider === p && /* @__PURE__ */ jsxs("div", { className: "mt-3 border-t pt-3", children: [
2664
- /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground mb-2", children: "Abra o WhatsApp no celular \u2192 Aparelhos conectados \u2192 Conectar aparelho." }),
2665
- qr.image && /* @__PURE__ */ jsx("img", { src: qr.image, alt: "QR", className: "h-48 w-48 rounded border bg-white p-2" }),
2666
- qr.code && /* @__PURE__ */ jsx("p", { className: "mt-2 font-mono text-lg tracking-widest", children: qr.code })
2667
- ] })
2668
- ]
2669
- },
2670
- p
2671
- );
2672
- })
2673
- ]
2905
+ children: body
2674
2906
  }
2675
2907
  );
2676
2908
  }
2909
+ var UPCOMING = ["Meta Cloud API", "Z-API", "Twilio"];
2910
+ var PROVIDERS = ["tyxter", "evolution"];
2911
+ function providerState(row) {
2912
+ if (row?.isActive) return { label: tl("In use", "Valendo agora"), tone: "default" };
2913
+ if (row?.hasCredential) return { label: tl("Connected", "Conectado"), tone: "secondary" };
2914
+ return { label: tl("Not connected", "N\xE3o conectado"), tone: "outline" };
2915
+ }
2916
+ function WhatsAppConnectorList() {
2917
+ const [rows, setRows] = React14.useState(null);
2918
+ const [open, setOpen] = React14.useState(null);
2919
+ const [hubSelected, setHubSelected] = React14.useState(null);
2920
+ const { connectors } = useConnectors({ hostPluginId: "conversations", includeGrants: false });
2921
+ const panelAvailable = useConnectorsPanelAvailable();
2922
+ const load = React14.useCallback(() => {
2923
+ providersApi.list().then(setRows).catch(() => setRows([]));
2924
+ }, []);
2925
+ React14.useEffect(load, [load]);
2926
+ const others = (connectors ?? []).filter((c) => !PROVIDERS.includes(c.slug));
2927
+ const byId = Object.fromEntries((rows ?? []).map((r) => [r.provider, r]));
2928
+ const tyxterHub = (connectors ?? []).find((c) => c.slug === "tyxter");
2929
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
2930
+ /* @__PURE__ */ jsx("h3", { className: "px-1 text-sm font-semibold", children: tl("WhatsApp providers", "Provedores de WhatsApp") }),
2931
+ /* @__PURE__ */ jsxs("ul", { className: "divide-y rounded-card border bg-card shadow-sm", children: [
2932
+ PROVIDERS.map((p) => {
2933
+ const row = byId[p];
2934
+ const state = providerState(row);
2935
+ const copy = COPY[p];
2936
+ return /* @__PURE__ */ jsxs("li", { className: "flex items-center gap-3 p-4", children: [
2937
+ p === "tyxter" && tyxterHub ? /* @__PURE__ */ jsx(ConnectorMark, { connector: tyxterHub }) : /* @__PURE__ */ jsx("span", { className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-muted text-xs font-semibold text-muted-foreground", children: copy.name.slice(0, 2) }),
2938
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
2939
+ /* @__PURE__ */ jsxs("span", { className: "flex flex-wrap items-center gap-2", children: [
2940
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: copy.name }),
2941
+ /* @__PURE__ */ jsx(Badge, { variant: state.tone, className: "font-normal", children: state.label })
2942
+ ] }),
2943
+ /* @__PURE__ */ jsx("span", { className: "mt-0.5 block text-xs text-muted-foreground", children: copy.what })
2944
+ ] }),
2945
+ /* @__PURE__ */ jsx(Button, { variant: "outline", className: "h-9 shrink-0", onClick: () => setOpen(p), children: row?.hasCredential ? tl("Configure", "Configurar") : tl("Connect", "Conectar") })
2946
+ ] }, p);
2947
+ }),
2948
+ others.map((c) => /* @__PURE__ */ jsxs("li", { className: "flex items-center gap-3 p-4", children: [
2949
+ /* @__PURE__ */ jsx(ConnectorMark, { connector: c }),
2950
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
2951
+ /* @__PURE__ */ jsxs("span", { className: "flex flex-wrap items-center gap-2", children: [
2952
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: c.name }),
2953
+ /* @__PURE__ */ jsx(StateChip, { state: c.state })
2954
+ ] }),
2955
+ c.description && /* @__PURE__ */ jsx("span", { className: "mt-0.5 block text-xs text-muted-foreground", children: c.description })
2956
+ ] }),
2957
+ /* @__PURE__ */ jsx(
2958
+ Button,
2959
+ {
2960
+ variant: "outline",
2961
+ className: "h-9 shrink-0",
2962
+ onClick: () => panelAvailable ? openConnectorsPanel(c) : setHubSelected(c),
2963
+ children: c.state === "connected" ? tl("Configure", "Configurar") : tl("Connect", "Conectar")
2964
+ }
2965
+ )
2966
+ ] }, c.id)),
2967
+ UPCOMING.map((name) => /* @__PURE__ */ jsxs("li", { className: "flex items-center gap-3 p-4 opacity-70", children: [
2968
+ /* @__PURE__ */ jsx("span", { className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-muted text-xs font-semibold text-muted-foreground", children: name.slice(0, 2) }),
2969
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 font-medium", children: name }),
2970
+ /* @__PURE__ */ jsx(Badge, { variant: "secondary", children: tl("Coming soon", "Em breve") })
2971
+ ] }, name))
2972
+ ] }),
2973
+ /* @__PURE__ */ jsx(Sheet, { open: !!open, onOpenChange: (o) => {
2974
+ if (!o) setOpen(null);
2975
+ }, children: /* @__PURE__ */ jsxs(SheetContent, { width: "max-w-xl", children: [
2976
+ /* @__PURE__ */ jsxs(SheetHeader, { children: [
2977
+ /* @__PURE__ */ jsx(SheetTitle, { children: open ? COPY[open].name : "" }),
2978
+ /* @__PURE__ */ jsx(SheetDescription, { children: tl("One provider sends at a time; the other keeps its settings.", "Um provedor envia por vez; o outro guarda a configura\xE7\xE3o.") })
2979
+ ] }),
2980
+ /* @__PURE__ */ jsx(SheetBody, { children: open && /* @__PURE__ */ jsx(WhatsAppProviders, { only: open, onChanged: load }) })
2981
+ ] }) }),
2982
+ /* @__PURE__ */ jsx(ConnectorDetail, { connector: hubSelected, open: !!hubSelected, onOpenChange: (o) => {
2983
+ if (!o) setHubSelected(null);
2984
+ } })
2985
+ ] });
2986
+ }
2677
2987
  function mapChannel(r) {
2678
2988
  return {
2679
2989
  id: String(r.id),
@@ -2760,9 +3070,9 @@ function ChannelRow({ account }) {
2760
3070
  }
2761
3071
  function ConversationsGeneralSettings() {
2762
3072
  const t = useTranslation();
2763
- const [channels, setChannels] = React10.useState(null);
2764
- const [failed, setFailed] = React10.useState(false);
2765
- React10.useEffect(() => {
3073
+ const [channels, setChannels] = React14.useState(null);
3074
+ const [failed, setFailed] = React14.useState(false);
3075
+ React14.useEffect(() => {
2766
3076
  let cancelled = false;
2767
3077
  listMessagingChannels().then((rows) => {
2768
3078
  if (!cancelled) setChannels(rows);
@@ -2776,22 +3086,19 @@ function ConversationsGeneralSettings() {
2776
3086
  cancelled = true;
2777
3087
  };
2778
3088
  }, []);
2779
- return /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
2780
- /* @__PURE__ */ jsx(WhatsAppProviders, {}),
2781
- /* @__PURE__ */ jsxs(
2782
- SettingsGroup,
2783
- {
2784
- title: t("conversations.settings.channels"),
2785
- description: t("conversations.settings.channelsHelp"),
2786
- children: [
2787
- channels === null && /* @__PURE__ */ jsx("p", { className: "py-2 text-sm text-muted-foreground", children: t("conversations.list.loading") }),
2788
- channels !== null && failed && /* @__PURE__ */ jsx("p", { className: "py-2 text-sm text-muted-foreground", children: t("conversations.settings.channelsUnavailable") }),
2789
- channels !== null && !failed && channels.length === 0 && /* @__PURE__ */ jsx("p", { className: "py-2 text-sm text-muted-foreground", children: t("conversations.settings.channelsEmpty") }),
2790
- channels?.map((account) => /* @__PURE__ */ jsx(ChannelRow, { account }, account.id))
2791
- ]
2792
- }
2793
- )
2794
- ] });
3089
+ return /* @__PURE__ */ jsx("div", { className: "space-y-6", children: /* @__PURE__ */ jsxs(
3090
+ SettingsGroup,
3091
+ {
3092
+ title: t("conversations.settings.channels"),
3093
+ description: t("conversations.settings.channelsHelp"),
3094
+ children: [
3095
+ channels === null && /* @__PURE__ */ jsx("p", { className: "py-2 text-sm text-muted-foreground", children: t("conversations.list.loading") }),
3096
+ channels !== null && failed && /* @__PURE__ */ jsx("p", { className: "py-2 text-sm text-muted-foreground", children: t("conversations.settings.channelsUnavailable") }),
3097
+ channels !== null && !failed && channels.length === 0 && /* @__PURE__ */ jsx("p", { className: "py-2 text-sm text-muted-foreground", children: t("conversations.settings.channelsEmpty") }),
3098
+ channels?.map((account) => /* @__PURE__ */ jsx(ChannelRow, { account }, account.id))
3099
+ ]
3100
+ }
3101
+ ) });
2795
3102
  }
2796
3103
  ConversationsGeneralSettings.displayName = "ConversationsGeneralSettings";
2797
3104
  function ConversationsSettingsTab() {
@@ -2801,7 +3108,7 @@ function ConversationsSettingsTab() {
2801
3108
  {
2802
3109
  title: t("conversations.settings.title"),
2803
3110
  generalSettings: /* @__PURE__ */ jsx(ConversationsGeneralSettings, {}),
2804
- hostPluginId: "conversations",
3111
+ customTabs: [{ id: "connectors", label: tl("Connectors", "Conectores"), icon: "Plug", content: /* @__PURE__ */ jsx(WhatsAppConnectorList, {}) }],
2805
3112
  routeBase: "/settings/conversations"
2806
3113
  }
2807
3114
  );
@@ -3045,18 +3352,9 @@ function TyxterPaymentsPanel() {
3045
3352
  className: "inline-block rounded-md border px-3 py-1.5 text-xs font-medium hover:bg-muted",
3046
3353
  children: "Abrir a configura\xE7\xE3o de pagamento \u2192"
3047
3354
  }
3048
- ) : /* @__PURE__ */ jsx(
3049
- "button",
3050
- {
3051
- type: "button",
3052
- onClick: () => {
3053
- void connect();
3054
- },
3055
- disabled: opening,
3056
- className: "rounded-md border px-3 py-1.5 text-xs font-medium hover:bg-muted disabled:opacity-50",
3057
- children: opening ? "Abrindo\u2026" : "Configurar pagamento"
3058
- }
3059
- )
3355
+ ) : /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => {
3356
+ void connect();
3357
+ }, disabled: opening, children: opening ? "Abrindo\u2026" : "Configurar pagamento" })
3060
3358
  ] }),
3061
3359
  error && /* @__PURE__ */ jsx("p", { className: "text-xs text-destructive", children: error })
3062
3360
  ] });
@@ -5313,6 +5611,44 @@ var MIGRATIONS = [
5313
5611
  { id: "013_a_midia_fica_conosco", sql: MIGRATION_013_A_MIDIA_FICA_CONOSCO },
5314
5612
  { id: "014_claiming_targets_is_server_only", sql: MIGRATION_014_CLAIMING_TARGETS_IS_SERVER_ONLY }
5315
5613
  ];
5614
+ var conversationsV1SettingsTabs = [
5615
+ defineSettingsTab({
5616
+ id: "conversations-atendeai",
5617
+ label: "AtendeAI",
5618
+ icon: "Bot",
5619
+ section: "communication",
5620
+ requiresFacet: "atendeai",
5621
+ pending: true,
5622
+ defaults: {
5623
+ "atendeai.assistant_name": "Assistente",
5624
+ "atendeai.tone": "friendly",
5625
+ "atendeai.ai_disclosure": true,
5626
+ "atendeai.max_messages_per_conversation": 30,
5627
+ "atendeai.escalation_timeout_minutes": 30
5628
+ },
5629
+ sections: [
5630
+ { title: tl("General", "Geral"), fields: [
5631
+ { key: "atendeai.assistant_name", type: "string", label: tl("Assistant name", "Nome do assistente") },
5632
+ { key: "atendeai.ai_disclosure", type: "boolean", label: tl("Say it is an AI", "Avisar que \xE9 uma intelig\xEAncia artificial") }
5633
+ ] },
5634
+ { title: tl("Personality", "Personalidade"), fields: [
5635
+ { key: "atendeai.tone", type: "select", label: tl("Tone", "Tom"), options: [
5636
+ { value: "friendly", label: tl("Friendly", "Amig\xE1vel") },
5637
+ { value: "formal", label: tl("Formal", "Formal") },
5638
+ { value: "casual", label: tl("Casual", "Descontra\xEDdo") }
5639
+ ] },
5640
+ { key: "atendeai.greeting_message", type: "string", label: tl("Greeting", "Mensagem de boas-vindas") },
5641
+ { key: "atendeai.farewell_message", type: "string", label: tl("Farewell", "Mensagem de despedida") },
5642
+ { key: "atendeai.handoff_message", type: "string", label: tl("When handing over to a person", "Ao passar para uma pessoa") },
5643
+ { key: "atendeai.out_of_scope_message", type: "string", label: tl("When the question is out of scope", "Quando a pergunta foge do escopo") }
5644
+ ] },
5645
+ { title: tl("Limits", "Limites"), fields: [
5646
+ { key: "atendeai.max_messages_per_conversation", type: "number", min: 1, max: 200, label: tl("Max AI messages per conversation", "M\xE1ximo de mensagens da IA por conversa") },
5647
+ { key: "atendeai.escalation_timeout_minutes", type: "number", min: 1, max: 1440, label: tl("Minutes before handing over", "Minutos at\xE9 passar para uma pessoa") }
5648
+ ] }
5649
+ ]
5650
+ })
5651
+ ];
5316
5652
  function client3() {
5317
5653
  const c = getSupabaseClientOptional();
5318
5654
  if (!c) throw new Error("Supabase n\xE3o inicializado");
@@ -5460,7 +5796,7 @@ function createConversationsPlugin(options) {
5460
5796
  contactEntityDef: options?.contactEntityDef
5461
5797
  };
5462
5798
  const dashboardWidgets = createConversationsDashboardWidgets({ store: store2, config });
5463
- const PageComponent = () => React10__default.createElement(ConversationsPage, { store: store2, config });
5799
+ const PageComponent = () => React14__default.createElement(ConversationsPage, { store: store2, config });
5464
5800
  PageComponent.displayName = "ConversationsPage";
5465
5801
  return {
5466
5802
  id: "conversations",
@@ -5493,8 +5829,17 @@ function createConversationsPlugin(options) {
5493
5829
  // O MAQUINÁRIO FICA: `campaignsApi`, `plg_conversations_campaigns` e o
5494
5830
  // ritmo de disparo continuam, e é por eles que Marketing e Automações
5495
5831
  // mandam mensagem. O que saiu foi a tela solta e o link para ela.
5496
- navigation: [],
5832
+ // AtendeAI does get a rail line, as in V1: whoever runs the AI follows it
5833
+ // all day. Offered to every account — off, the page is the upsell.
5834
+ navigation: [
5835
+ { section: "main", position: 45, label: "AtendeAI", route: "/atendeai", icon: "Bot", before: ["/analytics", "/reports"], permission: { feature: "conversations", action: "read" } }
5836
+ ],
5497
5837
  routes: [
5838
+ {
5839
+ path: "/atendeai",
5840
+ component: AtendeAIPage,
5841
+ permission: { feature: "conversations", action: "read" }
5842
+ },
5498
5843
  {
5499
5844
  path: "/conversations",
5500
5845
  component: PageComponent,
@@ -5502,25 +5847,25 @@ function createConversationsPlugin(options) {
5502
5847
  permission: { feature: "conversations", action: "read" }
5503
5848
  }
5504
5849
  ],
5505
- widgets: [
5506
- {
5507
- id: "conversations-inbox",
5508
- // `shell.topbar.end` é a única zona global que o AdminShell renderiza
5509
- // FORA do switch de rota: navegar não a desmonta. Um vigia montado
5510
- // dentro da tela de Conversas morreria ao sair dela — que é justamente
5511
- // quando ele serve para alguma coisa. O botão da caixa é o mesmo
5512
- // componente: quem conta as não lidas é quem as mostra.
5513
- zone: "shell.topbar.end",
5514
- component: (() => {
5515
- const W = () => React10__default.createElement(InboxWatcher, {
5516
- label: options?.navLabel ?? "Conversations"
5517
- });
5518
- W.displayName = "InboxButton";
5519
- return W;
5520
- })(),
5521
- order: -20
5522
- }
5523
- ],
5850
+ // In the shell's header, next to the bell (notifications sit at 100): both
5851
+ // say "something is waiting on you", so they sit side by side. Header
5852
+ // controls render outside the route switch, so the watcher's sound and
5853
+ // count survive navigation.
5854
+ headerControls: [{
5855
+ id: "conversations-inbox",
5856
+ component: (() => {
5857
+ const W = () => React14__default.createElement(InboxWatcher, {
5858
+ label: options?.navLabel ?? "Conversations",
5859
+ store: store2,
5860
+ config,
5861
+ inHeader: true
5862
+ });
5863
+ W.displayName = "InboxButton";
5864
+ return W;
5865
+ })(),
5866
+ // After the cash drawer (90), right before the bell.
5867
+ order: 95
5868
+ }],
5524
5869
  dashboardWidgets,
5525
5870
  events: [
5526
5871
  { name: "conversations.message.received", description: "An inbound message arrived on any channel" },
@@ -5661,7 +6006,11 @@ function createConversationsPlugin(options) {
5661
6006
  // The settings screen exists so the connector below can be REACHED: the
5662
6007
  // Integrations tab is drawn by PluginSettingsPanel, so a plugin with a
5663
6008
  // connector and no settings route ships a connector nobody can open.
6009
+ facets: [
6010
+ { id: "atendeai", label: "AtendeAI", description: "A intelig\xEAncia artificial que responde no WhatsApp." }
6011
+ ],
5664
6012
  settings: [
6013
+ ...conversationsV1SettingsTabs,
5665
6014
  {
5666
6015
  id: "conversations",
5667
6016
  label: "Conversas",