@fayz-ai/plugin-conversations 0.22.14 → 0.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/ConversationsRailPanel.d.ts +15 -0
- package/dist/components/ConversationsRailPanel.d.ts.map +1 -0
- package/dist/components/InboxWatcher.d.ts +10 -1
- package/dist/components/InboxWatcher.d.ts.map +1 -1
- package/dist/data/channel-connections.d.ts +8 -0
- package/dist/data/channel-connections.d.ts.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +834 -483
- package/dist/index.js.map +1 -1
- package/dist/integrations/tyxter/connectorDef.d.ts.map +1 -1
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/pt-BR.d.ts.map +1 -1
- package/dist/settings/ConversationsSettings.d.ts.map +1 -1
- package/dist/settings/WhatsAppConnectorList.d.ts +8 -0
- package/dist/settings/WhatsAppConnectorList.d.ts.map +1 -0
- package/dist/settings/WhatsAppProviders.d.ts +42 -1
- package/dist/settings/WhatsAppProviders.d.ts.map +1 -1
- package/dist/settings/v1-settings.d.ts +2 -0
- package/dist/settings/v1-settings.d.ts.map +1 -0
- package/dist/store.d.ts +5 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/views/AtendeAIPage.d.ts +8 -0
- package/dist/views/AtendeAIPage.d.ts.map +1 -0
- package/dist/views/ConversationList.d.ts.map +1 -1
- package/dist/views/InboxView.d.ts +13 -1
- package/dist/views/InboxView.d.ts.map +1 -1
- package/dist/views/MessageInspector.d.ts.map +1 -1
- package/dist/views/MessageThread.d.ts +4 -1
- package/dist/views/MessageThread.d.ts.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,54 +1,33 @@
|
|
|
1
|
-
import * as
|
|
2
|
-
import
|
|
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,
|
|
6
|
-
import {
|
|
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 {
|
|
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
|
-
|
|
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
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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] =
|
|
62
|
+
const [matches, setMatches] = React14__default.useState(
|
|
222
63
|
() => typeof window !== "undefined" && window.matchMedia(query).matches
|
|
223
64
|
);
|
|
224
|
-
|
|
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
|
|
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 =
|
|
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) =>
|
|
335
|
-
"
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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] =
|
|
517
|
-
const [providers, setProviders] =
|
|
518
|
-
|
|
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
|
-
|
|
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 =
|
|
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] =
|
|
626
|
-
const [detail, setDetail] =
|
|
627
|
-
const [loading, setLoading] =
|
|
628
|
-
const [err, setErr] =
|
|
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
|
-
|
|
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("
|
|
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] =
|
|
705
|
-
const [erro, setErro] =
|
|
706
|
-
|
|
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 =
|
|
742
|
-
const [visivel, setVisivel] =
|
|
623
|
+
const ref = React14.useRef(null);
|
|
624
|
+
const [visivel, setVisivel] = React14.useState(false);
|
|
743
625
|
const { url, erro } = useUrlAssinada(m, visivel);
|
|
744
|
-
|
|
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] =
|
|
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
|
-
|
|
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] =
|
|
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 =
|
|
897
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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]
|
|
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] =
|
|
1042
|
-
|
|
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] =
|
|
1210
|
-
const [contact, setContact] =
|
|
1211
|
-
const [typedHandle, setTypedHandle] =
|
|
1212
|
-
const [creatingContact, setCreatingContact] =
|
|
1213
|
-
const [firstMessage, setFirstMessage] =
|
|
1214
|
-
const [submitting, setSubmitting] =
|
|
1215
|
-
const [pickerKey, setPickerKey] =
|
|
1216
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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]
|
|
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 =
|
|
1250
|
+
const abertoRef = React14.useRef(selectedId);
|
|
1332
1251
|
abertoRef.current = selectedId;
|
|
1333
|
-
|
|
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
|
|
1267
|
+
const isWideViewport = useMediaQuery("(min-width: 1280px)");
|
|
1268
|
+
const isWidePanel = isWideViewport && !dock;
|
|
1348
1269
|
const railOpen = useRightRailStore((s) => s.open);
|
|
1349
|
-
const [panelOpen, setPanelOpen] =
|
|
1350
|
-
const [newOpen, setNewOpen] =
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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: "
|
|
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
|
-
|
|
1315
|
+
Button,
|
|
1387
1316
|
{
|
|
1388
|
-
|
|
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
|
-
|
|
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] =
|
|
2514
|
-
const [draft, setDraft] =
|
|
2515
|
-
const [busy, setBusy] =
|
|
2516
|
-
const [numbers, setNumbers] =
|
|
2517
|
-
const [qr, setQr] =
|
|
2518
|
-
const load =
|
|
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
|
-
|
|
2754
|
+
onChanged?.();
|
|
2755
|
+
}, [onChanged]);
|
|
2756
|
+
React14__default.useEffect(() => {
|
|
2526
2757
|
void load();
|
|
2527
2758
|
}, [load]);
|
|
2528
|
-
const byId =
|
|
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
|
-
|
|
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] =
|
|
2764
|
-
const [failed, setFailed] =
|
|
2765
|
-
|
|
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__ */
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
children:
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = () =>
|
|
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,19 @@ 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
|
-
|
|
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
|
+
// AtendeAI é recurso de restaurante (ChefControl): só aparece no vertical
|
|
5836
|
+
// 'food'. Uma loja de outro ramo não vê a linha.
|
|
5837
|
+
{ section: "main", position: 45, label: "AtendeAI", route: "/atendeai", icon: "Bot", before: ["/analytics", "/reports"], verticalId: "food", permission: { feature: "conversations", action: "read" } }
|
|
5838
|
+
],
|
|
5497
5839
|
routes: [
|
|
5840
|
+
{
|
|
5841
|
+
path: "/atendeai",
|
|
5842
|
+
component: AtendeAIPage,
|
|
5843
|
+
permission: { feature: "conversations", action: "read" }
|
|
5844
|
+
},
|
|
5498
5845
|
{
|
|
5499
5846
|
path: "/conversations",
|
|
5500
5847
|
component: PageComponent,
|
|
@@ -5502,25 +5849,25 @@ function createConversationsPlugin(options) {
|
|
|
5502
5849
|
permission: { feature: "conversations", action: "read" }
|
|
5503
5850
|
}
|
|
5504
5851
|
],
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
],
|
|
5852
|
+
// In the shell's header, next to the bell (notifications sit at 100): both
|
|
5853
|
+
// say "something is waiting on you", so they sit side by side. Header
|
|
5854
|
+
// controls render outside the route switch, so the watcher's sound and
|
|
5855
|
+
// count survive navigation.
|
|
5856
|
+
headerControls: [{
|
|
5857
|
+
id: "conversations-inbox",
|
|
5858
|
+
component: (() => {
|
|
5859
|
+
const W = () => React14__default.createElement(InboxWatcher, {
|
|
5860
|
+
label: options?.navLabel ?? "Conversations",
|
|
5861
|
+
store: store2,
|
|
5862
|
+
config,
|
|
5863
|
+
inHeader: true
|
|
5864
|
+
});
|
|
5865
|
+
W.displayName = "InboxButton";
|
|
5866
|
+
return W;
|
|
5867
|
+
})(),
|
|
5868
|
+
// After the cash drawer (90), right before the bell.
|
|
5869
|
+
order: 95
|
|
5870
|
+
}],
|
|
5524
5871
|
dashboardWidgets,
|
|
5525
5872
|
events: [
|
|
5526
5873
|
{ name: "conversations.message.received", description: "An inbound message arrived on any channel" },
|
|
@@ -5568,8 +5915,8 @@ function createConversationsPlugin(options) {
|
|
|
5568
5915
|
}
|
|
5569
5916
|
},
|
|
5570
5917
|
suggestions: [
|
|
5571
|
-
{ label: "
|
|
5572
|
-
{ label: "
|
|
5918
|
+
{ label: "Mostre as conversas n\xE3o lidas" },
|
|
5919
|
+
{ label: "Chegou mensagem nova no WhatsApp?" }
|
|
5573
5920
|
],
|
|
5574
5921
|
permission: { feature: "conversations", action: "read" }
|
|
5575
5922
|
},
|
|
@@ -5661,7 +6008,11 @@ function createConversationsPlugin(options) {
|
|
|
5661
6008
|
// The settings screen exists so the connector below can be REACHED: the
|
|
5662
6009
|
// Integrations tab is drawn by PluginSettingsPanel, so a plugin with a
|
|
5663
6010
|
// connector and no settings route ships a connector nobody can open.
|
|
6011
|
+
facets: [
|
|
6012
|
+
{ id: "atendeai", label: "AtendeAI", description: "A intelig\xEAncia artificial que responde no WhatsApp." }
|
|
6013
|
+
],
|
|
5664
6014
|
settings: [
|
|
6015
|
+
...conversationsV1SettingsTabs,
|
|
5665
6016
|
{
|
|
5666
6017
|
id: "conversations",
|
|
5667
6018
|
label: "Conversas",
|