@fayz-ai/plugin-conversations 0.23.14 → 0.23.15
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/AtendeAIPage-PBBMWIDW.js +92 -0
- package/dist/AtendeAIPage-PBBMWIDW.js.map +1 -0
- package/dist/ConversationsPage-4V4VLM4D.js +47 -0
- package/dist/ConversationsPage-4V4VLM4D.js.map +1 -0
- package/dist/ConversationsSettings-7UQYM26R.js +158 -0
- package/dist/ConversationsSettings-7UQYM26R.js.map +1 -0
- package/dist/chunk-3FZX3ZQ3.js +318 -0
- package/dist/chunk-3FZX3ZQ3.js.map +1 -0
- package/dist/chunk-HMWUU5O7.js +75 -0
- package/dist/chunk-HMWUU5O7.js.map +1 -0
- package/dist/chunk-LJKLHGFX.js +1268 -0
- package/dist/chunk-LJKLHGFX.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +79 -4103
- package/dist/index.js.map +1 -1
- package/dist/migrations/main.browser.js +23 -0
- package/dist/migrations/main.browser.js.map +1 -0
- package/dist/migrations/main.js +2141 -0
- package/dist/migrations/main.js.map +1 -0
- package/dist/views/ConversationsPage.d.ts.map +1 -1
- package/package.json +10 -4
|
@@ -0,0 +1,1268 @@
|
|
|
1
|
+
import { providersApi } from './chunk-HMWUU5O7.js';
|
|
2
|
+
import * as React10 from 'react';
|
|
3
|
+
import React10__default from 'react';
|
|
4
|
+
import { useStore } from 'zustand';
|
|
5
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
6
|
+
import { SquarePen, MessageSquare, Globe, Mail, Instagram, Phone, Search, User, Inbox, CheckCheck, ChevronLeft, Archive, PanelRight, Send, X, MapPin, Tag, StickyNote, Link2, MoreVertical, Loader2, AlertTriangle, Check, Clock, Snowflake, Sun, Flame, Building2, ExternalLink, UserRound, Copy, FileText, Download } from 'lucide-react';
|
|
7
|
+
import { PageHeaderActions, cn, Button, Modal, ModalContent, ModalHeader, ModalTitle, ModalBody, Textarea, ModalFooter, Input, FilterChip, Skeleton, toast } from '@fayz-ai/ui';
|
|
8
|
+
import { useConnectors, useRightRailStore, PageActions, useLimitGuard, ContactPicker, invalidateLimit } from '@fayz-ai/admin';
|
|
9
|
+
import { getSupabaseClientOptional, useTranslation, useActiveTenantId, errorMessage, getActiveTenantId, appHref } from '@fayz-ai/core';
|
|
10
|
+
|
|
11
|
+
var DEFAULT_CONVERSATIONS_CONFIG = {
|
|
12
|
+
contactKind: "contact"
|
|
13
|
+
};
|
|
14
|
+
var StoreContext = React10__default.createContext(null);
|
|
15
|
+
var ConfigContext = React10__default.createContext(DEFAULT_CONVERSATIONS_CONFIG);
|
|
16
|
+
function ConversationsContextProvider({
|
|
17
|
+
store,
|
|
18
|
+
config = DEFAULT_CONVERSATIONS_CONFIG,
|
|
19
|
+
children
|
|
20
|
+
}) {
|
|
21
|
+
return /* @__PURE__ */ jsx(StoreContext.Provider, { value: store, children: /* @__PURE__ */ jsx(ConfigContext.Provider, { value: config, children }) });
|
|
22
|
+
}
|
|
23
|
+
function useConversationsStore(selector) {
|
|
24
|
+
const store = React10__default.useContext(StoreContext);
|
|
25
|
+
if (!store) throw new Error("useConversationsStore must be used within ConversationsPage");
|
|
26
|
+
return useStore(store, selector);
|
|
27
|
+
}
|
|
28
|
+
function useConversationsConfig() {
|
|
29
|
+
return React10__default.useContext(ConfigContext);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// src/types.ts
|
|
33
|
+
var CHANNEL_LABELS = {
|
|
34
|
+
sms: "SMS",
|
|
35
|
+
whatsapp: "WhatsApp",
|
|
36
|
+
instagram: "Instagram",
|
|
37
|
+
email: "Email",
|
|
38
|
+
webchat: "Web Chat"
|
|
39
|
+
};
|
|
40
|
+
function isWaitingOnUs(conversation) {
|
|
41
|
+
if (conversation.status !== "open") return false;
|
|
42
|
+
if (conversation.acknowledgedAt && conversation.lastMessageAt && conversation.acknowledgedAt >= conversation.lastMessageAt) return false;
|
|
43
|
+
if (conversation.lastMessageDirection) return conversation.lastMessageDirection === "inbound";
|
|
44
|
+
return conversation.unreadCount > 0;
|
|
45
|
+
}
|
|
46
|
+
var CHANNEL_ICON = {
|
|
47
|
+
sms: Phone,
|
|
48
|
+
whatsapp: MessageSquare,
|
|
49
|
+
instagram: Instagram,
|
|
50
|
+
email: Mail,
|
|
51
|
+
webchat: Globe
|
|
52
|
+
};
|
|
53
|
+
var CHANNEL_ACCENT = {
|
|
54
|
+
whatsapp: { color: "#22c55e", badge: "bg-[#22c55e]/12 text-[#15803d] dark:text-[#4ade80]" },
|
|
55
|
+
sms: { color: "#6366f1", badge: "bg-[#6366f1]/12 text-[#4338ca] dark:text-[#a5b4fc]" },
|
|
56
|
+
instagram: { color: "#ec4899", badge: "bg-[#ec4899]/12 text-[#be185d] dark:text-[#f9a8d4]" },
|
|
57
|
+
email: { color: "#0ea5e9", badge: "bg-[#0ea5e9]/12 text-[#0369a1] dark:text-[#7dd3fc]" },
|
|
58
|
+
webchat: { color: "#f59e0b", badge: "bg-[#f59e0b]/12 text-[#b45309] dark:text-[#fcd34d]" }
|
|
59
|
+
};
|
|
60
|
+
function useMediaQuery(query) {
|
|
61
|
+
const [matches, setMatches] = React10__default.useState(
|
|
62
|
+
() => typeof window !== "undefined" && window.matchMedia(query).matches
|
|
63
|
+
);
|
|
64
|
+
React10__default.useEffect(() => {
|
|
65
|
+
if (typeof window === "undefined") return;
|
|
66
|
+
const mql = window.matchMedia(query);
|
|
67
|
+
const onChange = () => setMatches(mql.matches);
|
|
68
|
+
onChange();
|
|
69
|
+
mql.addEventListener("change", onChange);
|
|
70
|
+
return () => mql.removeEventListener("change", onChange);
|
|
71
|
+
}, [query]);
|
|
72
|
+
return matches;
|
|
73
|
+
}
|
|
74
|
+
function initialsOf(name) {
|
|
75
|
+
return name.replace("@", "").split(/\s+/).filter(Boolean).map((w) => w[0]).slice(0, 2).join("").toUpperCase();
|
|
76
|
+
}
|
|
77
|
+
function Avatar({ name, accent, size = "md", channel }) {
|
|
78
|
+
const dims = size === "lg" ? "h-14 w-14 text-lg" : size === "sm" ? "h-9 w-9 text-xs" : "h-10 w-10 text-sm";
|
|
79
|
+
const Icon = channel ? CHANNEL_ICON[channel] : null;
|
|
80
|
+
return /* @__PURE__ */ jsxs("div", { className: "relative shrink-0", children: [
|
|
81
|
+
/* @__PURE__ */ jsx(
|
|
82
|
+
"div",
|
|
83
|
+
{
|
|
84
|
+
className: cn("flex items-center justify-center rounded-full font-semibold text-white", dims),
|
|
85
|
+
style: { backgroundColor: accent },
|
|
86
|
+
children: initialsOf(name)
|
|
87
|
+
}
|
|
88
|
+
),
|
|
89
|
+
Icon && /* @__PURE__ */ jsx(
|
|
90
|
+
"span",
|
|
91
|
+
{
|
|
92
|
+
className: "absolute -bottom-0.5 -right-0.5 flex h-4 w-4 items-center justify-center rounded-full ring-2 ring-card",
|
|
93
|
+
style: { backgroundColor: CHANNEL_ACCENT[channel].color },
|
|
94
|
+
children: /* @__PURE__ */ jsx(Icon, { className: "h-2.5 w-2.5 text-white" })
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
] });
|
|
98
|
+
}
|
|
99
|
+
function ChannelBadge({ channel, className }) {
|
|
100
|
+
const Icon = CHANNEL_ICON[channel];
|
|
101
|
+
return /* @__PURE__ */ jsxs(
|
|
102
|
+
"span",
|
|
103
|
+
{
|
|
104
|
+
className: cn(
|
|
105
|
+
"inline-flex items-center gap-1 rounded-full px-1.5 py-0.5 text-[10px] font-medium",
|
|
106
|
+
CHANNEL_ACCENT[channel].badge,
|
|
107
|
+
className
|
|
108
|
+
),
|
|
109
|
+
children: [
|
|
110
|
+
/* @__PURE__ */ jsx(Icon, { className: "h-2.5 w-2.5" }),
|
|
111
|
+
CHANNEL_LABELS[channel]
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
function relativeTime(iso) {
|
|
117
|
+
const diff = Date.now() - new Date(iso).getTime();
|
|
118
|
+
const mins = Math.round(diff / 6e4);
|
|
119
|
+
if (mins < 1) return "now";
|
|
120
|
+
if (mins < 60) return `${mins}m`;
|
|
121
|
+
const hours = Math.round(mins / 60);
|
|
122
|
+
if (hours < 24) return `${hours}h`;
|
|
123
|
+
const days = Math.round(hours / 24);
|
|
124
|
+
if (days < 7) return `${days}d`;
|
|
125
|
+
return `${Math.round(days / 7)}w`;
|
|
126
|
+
}
|
|
127
|
+
function dayLabel(iso) {
|
|
128
|
+
const d = new Date(iso);
|
|
129
|
+
const now = /* @__PURE__ */ new Date();
|
|
130
|
+
const startOf = (x) => new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime();
|
|
131
|
+
const dayDiff = Math.round((startOf(now) - startOf(d)) / 864e5);
|
|
132
|
+
if (dayDiff <= 0) return "Today";
|
|
133
|
+
if (dayDiff === 1) return "Yesterday";
|
|
134
|
+
if (dayDiff < 7) return d.toLocaleDateString([], { weekday: "long" });
|
|
135
|
+
return d.toLocaleDateString([], { month: "short", day: "numeric", year: now.getFullYear() === d.getFullYear() ? void 0 : "numeric" });
|
|
136
|
+
}
|
|
137
|
+
var whatsappCache = null;
|
|
138
|
+
var whatsappConnected = () => {
|
|
139
|
+
whatsappCache ?? (whatsappCache = providersApi.list().then((rows) => rows.some((r) => r.isActive || r.hasCredential)).catch(() => {
|
|
140
|
+
whatsappCache = null;
|
|
141
|
+
return false;
|
|
142
|
+
}));
|
|
143
|
+
return whatsappCache;
|
|
144
|
+
};
|
|
145
|
+
function useChannelConnections() {
|
|
146
|
+
const [whatsapp, setWhatsapp] = React10.useState(void 0);
|
|
147
|
+
const { connectors } = useConnectors({ hostPluginId: "conversations", includeGrants: false });
|
|
148
|
+
React10.useEffect(() => {
|
|
149
|
+
let cancelled = false;
|
|
150
|
+
void whatsappConnected().then((v) => {
|
|
151
|
+
if (!cancelled) setWhatsapp(v);
|
|
152
|
+
});
|
|
153
|
+
return () => {
|
|
154
|
+
cancelled = true;
|
|
155
|
+
};
|
|
156
|
+
}, []);
|
|
157
|
+
return React10.useMemo(() => {
|
|
158
|
+
if (whatsapp === void 0) return void 0;
|
|
159
|
+
const live = (needle) => (connectors ?? []).some((c) => c.state === "connected" && c.slug.toLowerCase().includes(needle));
|
|
160
|
+
return { whatsapp: whatsapp || live("whatsapp"), email: live("mail"), instagram: live("instagram") };
|
|
161
|
+
}, [whatsapp, connectors]);
|
|
162
|
+
}
|
|
163
|
+
var FILTERS = ["all", "whatsapp", "instagram", "email"];
|
|
164
|
+
function ConversationList({ className }) {
|
|
165
|
+
const t = useTranslation();
|
|
166
|
+
const connected = useChannelConnections();
|
|
167
|
+
const {
|
|
168
|
+
conversations,
|
|
169
|
+
selectedId,
|
|
170
|
+
channelFilter,
|
|
171
|
+
leadOwnerFilter,
|
|
172
|
+
search,
|
|
173
|
+
loading,
|
|
174
|
+
select,
|
|
175
|
+
markRead,
|
|
176
|
+
setChannelFilter,
|
|
177
|
+
setLeadOwnerFilter,
|
|
178
|
+
setSearch
|
|
179
|
+
} = useConversationsStore((s) => s);
|
|
180
|
+
const donos = React10__default.useMemo(() => {
|
|
181
|
+
const vistos = /* @__PURE__ */ new Map();
|
|
182
|
+
for (const c of conversations) {
|
|
183
|
+
if (c.leadOwnerId) vistos.set(c.leadOwnerId, c.leadOwnerName ?? c.leadOwnerId);
|
|
184
|
+
}
|
|
185
|
+
return [...vistos].map(([value, label]) => ({ value, label }));
|
|
186
|
+
}, [conversations]);
|
|
187
|
+
return /* @__PURE__ */ jsxs("aside", { className: cn("w-full shrink-0 flex-col border-r border-border bg-card lg:w-[320px]", className), children: [
|
|
188
|
+
/* @__PURE__ */ jsxs("div", { className: "border-b border-border px-3 py-3", children: [
|
|
189
|
+
/* @__PURE__ */ jsxs("div", { className: "relative", children: [
|
|
190
|
+
/* @__PURE__ */ jsx(Search, { className: "pointer-events-none absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" }),
|
|
191
|
+
/* @__PURE__ */ jsx(
|
|
192
|
+
Input,
|
|
193
|
+
{
|
|
194
|
+
value: search,
|
|
195
|
+
onChange: (e) => setSearch(e.target.value),
|
|
196
|
+
placeholder: t("conversations.list.search"),
|
|
197
|
+
className: "pl-8"
|
|
198
|
+
}
|
|
199
|
+
)
|
|
200
|
+
] }),
|
|
201
|
+
/* @__PURE__ */ jsx("div", { className: "mt-2 flex flex-wrap gap-1", children: FILTERS.map((id) => {
|
|
202
|
+
const off = id !== "all" && connected !== void 0 && !connected[id];
|
|
203
|
+
return /* @__PURE__ */ jsx(
|
|
204
|
+
Button,
|
|
205
|
+
{
|
|
206
|
+
variant: "bare",
|
|
207
|
+
size: "bare",
|
|
208
|
+
onClick: () => setChannelFilter(id),
|
|
209
|
+
title: off ? t("conversations.filter.notConnected") : void 0,
|
|
210
|
+
className: cn(
|
|
211
|
+
"rounded-full border px-2.5 py-1 text-xs font-medium transition-colors",
|
|
212
|
+
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"
|
|
213
|
+
),
|
|
214
|
+
children: t(`conversations.filter.${id}`)
|
|
215
|
+
},
|
|
216
|
+
id
|
|
217
|
+
);
|
|
218
|
+
}) }),
|
|
219
|
+
donos.length > 0 && /* @__PURE__ */ jsx("div", { className: "mt-2", children: /* @__PURE__ */ jsx(
|
|
220
|
+
FilterChip,
|
|
221
|
+
{
|
|
222
|
+
label: t("conversations.contact.leadOwner"),
|
|
223
|
+
icon: User,
|
|
224
|
+
allLabel: t("conversations.filter.anyOwner"),
|
|
225
|
+
options: [{ value: "none", label: t("conversations.filter.noOwner") }, ...donos],
|
|
226
|
+
value: leadOwnerFilter,
|
|
227
|
+
onChange: (v) => {
|
|
228
|
+
void setLeadOwnerFilter(v);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
) })
|
|
232
|
+
] }),
|
|
233
|
+
/* @__PURE__ */ jsxs("div", { className: "min-h-0 flex-1 overflow-y-auto", children: [
|
|
234
|
+
loading && conversations.length === 0 && Array.from({ length: 6 }, (_, i) => /* @__PURE__ */ jsxs("div", { className: "flex w-full items-start gap-3 border-b border-border/50 px-3 py-3", children: [
|
|
235
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-10 w-10 shrink-0 rounded-full" }),
|
|
236
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
237
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
|
|
238
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-28" }),
|
|
239
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-3 w-8" })
|
|
240
|
+
] }),
|
|
241
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "mt-1.5 h-4 w-16 rounded-full" }),
|
|
242
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "mt-1.5 h-3 w-3/4" })
|
|
243
|
+
] })
|
|
244
|
+
] }, i)),
|
|
245
|
+
!loading && conversations.length === 0 && /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-2 p-8 text-center text-muted-foreground", children: [
|
|
246
|
+
/* @__PURE__ */ jsx(Inbox, { className: "h-6 w-6" }),
|
|
247
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm", children: t("conversations.list.empty") })
|
|
248
|
+
] }),
|
|
249
|
+
conversations.map((c) => {
|
|
250
|
+
const active = c.id === selectedId;
|
|
251
|
+
const unread = isWaitingOnUs(c);
|
|
252
|
+
return (
|
|
253
|
+
// `div` em volta e não um botão dentro de outro: HTML não permite
|
|
254
|
+
// aninhar, e o navegador resolve isso silenciosamente movendo o
|
|
255
|
+
// interno para fora — a linha inteira deixa de ser clicável e
|
|
256
|
+
// ninguém entende por quê.
|
|
257
|
+
/* @__PURE__ */ jsxs("div", { className: "group relative", children: [
|
|
258
|
+
/* @__PURE__ */ jsxs(
|
|
259
|
+
Button,
|
|
260
|
+
{
|
|
261
|
+
variant: "bare",
|
|
262
|
+
size: "bare",
|
|
263
|
+
onClick: () => select(c.id),
|
|
264
|
+
className: cn(
|
|
265
|
+
"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",
|
|
266
|
+
// Um TOM da cor de marca, e não `bg-accent`.
|
|
267
|
+
//
|
|
268
|
+
// `bg-accent` resolve para quase preto neste tema, e a linha da
|
|
269
|
+
// conversa aberta ficava escura sobre escura. Trocar junto o
|
|
270
|
+
// `text-accent-foreground` deixava legível, mas pintava de
|
|
271
|
+
// preto a única linha que se está a ler — chamava mais atenção
|
|
272
|
+
// que a conversa.
|
|
273
|
+
//
|
|
274
|
+
// `bg-primary/10` é um véu: o texto continua com as cores dele,
|
|
275
|
+
// a seleção se lê como seleção e não como bloco, e funciona nos
|
|
276
|
+
// dois temas porque não presume nada sobre o fundo.
|
|
277
|
+
active ? "bg-primary/10" : "hover:bg-muted/50"
|
|
278
|
+
),
|
|
279
|
+
children: [
|
|
280
|
+
/* @__PURE__ */ jsx(Avatar, { name: c.contactName, accent: c.accent, channel: c.channel }),
|
|
281
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
282
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
|
|
283
|
+
/* @__PURE__ */ jsx("span", { className: cn("truncate text-sm text-foreground", unread ? "font-semibold" : "font-medium"), children: c.contactName }),
|
|
284
|
+
/* @__PURE__ */ jsx("span", { className: cn("shrink-0 text-[11px]", unread ? "font-semibold text-primary" : "text-muted-foreground"), children: relativeTime(c.lastMessageAt) })
|
|
285
|
+
] }),
|
|
286
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-1 flex items-center gap-2", children: [
|
|
287
|
+
/* @__PURE__ */ jsx(ChannelBadge, { channel: c.channel }),
|
|
288
|
+
c.status !== "open" && /* @__PURE__ */ jsx("span", { className: "text-[10px] uppercase tracking-wide text-muted-foreground/70", children: t(`conversations.status.${c.status}`) }),
|
|
289
|
+
c.leadOwnerName && /* @__PURE__ */ jsxs("span", { className: "inline-flex shrink-0 items-center gap-0.5 rounded-full bg-muted px-1.5 text-[10px] text-muted-foreground", children: [
|
|
290
|
+
/* @__PURE__ */ jsx(User, { className: "h-2.5 w-2.5" }),
|
|
291
|
+
c.leadOwnerName.split(" ")[0]
|
|
292
|
+
] })
|
|
293
|
+
] }),
|
|
294
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-1 flex items-center justify-between gap-2", children: [
|
|
295
|
+
/* @__PURE__ */ jsx("span", { className: cn("truncate text-xs", unread ? "text-foreground" : "text-muted-foreground"), children: c.lastMessagePreview }),
|
|
296
|
+
unread && /* @__PURE__ */ jsx("span", { className: "flex h-[18px] min-w-[18px] shrink-0 items-center justify-center rounded-full bg-destructive px-1.5 text-[10px] font-semibold text-destructive-foreground", children: c.unreadCount > 0 ? c.unreadCount : "\xB7" })
|
|
297
|
+
] })
|
|
298
|
+
] })
|
|
299
|
+
]
|
|
300
|
+
}
|
|
301
|
+
),
|
|
302
|
+
unread && /* @__PURE__ */ jsx(
|
|
303
|
+
Button,
|
|
304
|
+
{
|
|
305
|
+
variant: "bare",
|
|
306
|
+
size: "bare",
|
|
307
|
+
type: "button",
|
|
308
|
+
title: t("conversations.markRead"),
|
|
309
|
+
"aria-label": t("conversations.markRead"),
|
|
310
|
+
onClick: (e) => {
|
|
311
|
+
e.stopPropagation();
|
|
312
|
+
void markRead(c.id);
|
|
313
|
+
},
|
|
314
|
+
className: "absolute right-2 top-2 hidden rounded-full p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground group-hover:block",
|
|
315
|
+
children: /* @__PURE__ */ jsx(CheckCheck, { className: "h-3.5 w-3.5" })
|
|
316
|
+
}
|
|
317
|
+
)
|
|
318
|
+
] }, c.id)
|
|
319
|
+
);
|
|
320
|
+
})
|
|
321
|
+
] })
|
|
322
|
+
] });
|
|
323
|
+
}
|
|
324
|
+
function useSendWindow(conversationId) {
|
|
325
|
+
const [row, setRow] = React10__default.useState(null);
|
|
326
|
+
const [providers, setProviders] = React10__default.useState(null);
|
|
327
|
+
React10__default.useEffect(() => {
|
|
328
|
+
let alive = true;
|
|
329
|
+
providersApi.list().then((p) => {
|
|
330
|
+
if (alive) setProviders(p);
|
|
331
|
+
}).catch(() => {
|
|
332
|
+
if (alive) setProviders([]);
|
|
333
|
+
});
|
|
334
|
+
return () => {
|
|
335
|
+
alive = false;
|
|
336
|
+
};
|
|
337
|
+
}, []);
|
|
338
|
+
React10__default.useEffect(() => {
|
|
339
|
+
let alive = true;
|
|
340
|
+
if (!conversationId) {
|
|
341
|
+
setRow(null);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
const sb = getSupabaseClientOptional();
|
|
345
|
+
if (!sb) {
|
|
346
|
+
setRow(null);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
sb.from("v_conversations_window").select("window_open, window_expires_at, minutes_left, last_inbound_at").eq("conversation_id", conversationId).eq("tenant_id", getActiveTenantId()).maybeSingle().then(({ data }) => {
|
|
350
|
+
if (alive) setRow(data ?? null);
|
|
351
|
+
}).catch(() => {
|
|
352
|
+
if (alive) setRow(null);
|
|
353
|
+
});
|
|
354
|
+
return () => {
|
|
355
|
+
alive = false;
|
|
356
|
+
};
|
|
357
|
+
}, [conversationId]);
|
|
358
|
+
const provider = (providers ?? []).find((p) => p.isActive);
|
|
359
|
+
const mode = React10__default.useMemo(() => {
|
|
360
|
+
if (!provider) return { kind: "blocked", reason: "no_provider" };
|
|
361
|
+
if (!provider.capabilities?.requires_template) {
|
|
362
|
+
return { kind: "free", until: null, minutesLeft: null };
|
|
363
|
+
}
|
|
364
|
+
if (row?.window_open) {
|
|
365
|
+
return { kind: "free", until: row.window_expires_at, minutesLeft: row.minutes_left };
|
|
366
|
+
}
|
|
367
|
+
return { kind: "template", reason: row?.last_inbound_at ? "window_closed" : "never_replied" };
|
|
368
|
+
}, [row, provider]);
|
|
369
|
+
return { loading: providers === null, mode, provider };
|
|
370
|
+
}
|
|
371
|
+
function humanLeft(minutes) {
|
|
372
|
+
if (minutes == null) return "";
|
|
373
|
+
if (minutes < 60) return `${minutes} min`;
|
|
374
|
+
const h = Math.floor(minutes / 60);
|
|
375
|
+
return h < 24 ? `${h}h` : `${Math.floor(h / 24)}d`;
|
|
376
|
+
}
|
|
377
|
+
function SendWindowBanner({ state }) {
|
|
378
|
+
const { mode, provider } = state;
|
|
379
|
+
if (mode.kind === "free" && mode.minutesLeft == null) return null;
|
|
380
|
+
if (mode.kind === "free") {
|
|
381
|
+
const urgent = (mode.minutesLeft ?? 0) < 120;
|
|
382
|
+
return /* @__PURE__ */ jsxs("div", { className: `flex items-center gap-2 px-4 py-1.5 text-[11px] ${urgent ? "bg-warning-soft text-warning-soft-foreground" : "text-muted-foreground"}`, children: [
|
|
383
|
+
/* @__PURE__ */ jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-success" }),
|
|
384
|
+
"Janela aberta \u2014 texto livre por mais ",
|
|
385
|
+
humanLeft(mode.minutesLeft)
|
|
386
|
+
] });
|
|
387
|
+
}
|
|
388
|
+
if (mode.kind === "blocked") {
|
|
389
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 bg-destructive-soft px-4 py-2 text-[11px] text-destructive-soft-foreground", children: [
|
|
390
|
+
/* @__PURE__ */ jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-destructive" }),
|
|
391
|
+
"Nenhum provedor de WhatsApp ativo \u2014 configure em Ajustes \u203A Conversas."
|
|
392
|
+
] });
|
|
393
|
+
}
|
|
394
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-x-2 gap-y-1 bg-warning-soft px-4 py-2 text-[11px] text-warning-soft-foreground", children: [
|
|
395
|
+
/* @__PURE__ */ jsx("span", { className: "h-1.5 w-1.5 shrink-0 rounded-full bg-warning" }),
|
|
396
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium", children: mode.reason === "never_replied" ? "Esta pessoa nunca respondeu." : "A janela de 24 horas fechou." }),
|
|
397
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
398
|
+
"No ",
|
|
399
|
+
provider?.provider ?? "provedor",
|
|
400
|
+
", texto livre s\xF3 sai dentro de 24h da resposta do cliente. Para falar agora \xE9 preciso um template aprovado."
|
|
401
|
+
] })
|
|
402
|
+
] });
|
|
403
|
+
}
|
|
404
|
+
var STATUS = {
|
|
405
|
+
queued: { icon: Clock, label: "na fila", cls: "text-white/60" },
|
|
406
|
+
sent: { icon: Check, label: "enviada", cls: "text-white/70" },
|
|
407
|
+
delivered: { icon: CheckCheck, label: "entregue", cls: "text-white/80" },
|
|
408
|
+
read: { icon: CheckCheck, label: "lida", cls: "text-sky-200" },
|
|
409
|
+
failed: { icon: AlertTriangle, label: "N\xC3O ENVIADA", cls: "text-white" },
|
|
410
|
+
expired: { icon: AlertTriangle, label: "expirou", cls: "text-white" },
|
|
411
|
+
delivery_timeout: { icon: AlertTriangle, label: "sem confirma\xE7\xE3o", cls: "text-white" },
|
|
412
|
+
cancelled: { icon: X, label: "cancelada", cls: "text-white" },
|
|
413
|
+
opted_out: { icon: X, label: "recusada", cls: "text-white" }
|
|
414
|
+
};
|
|
415
|
+
var BAD = /* @__PURE__ */ new Set(["failed", "expired", "cancelled", "opted_out", "delivery_timeout"]);
|
|
416
|
+
function isFailed(m) {
|
|
417
|
+
if (m.direction !== "outbound") return false;
|
|
418
|
+
if (m.deliveryStatus) return BAD.has(m.deliveryStatus);
|
|
419
|
+
if (m.providerMessageId) return false;
|
|
420
|
+
const age = Date.now() - new Date(m.at).getTime();
|
|
421
|
+
return Number.isFinite(age) && age > 12e4;
|
|
422
|
+
}
|
|
423
|
+
function DeliveryMark({ m }) {
|
|
424
|
+
if (m.direction !== "outbound") return null;
|
|
425
|
+
const key = isFailed(m) ? "failed" : m.deliveryStatus ?? "sent";
|
|
426
|
+
const cfg = STATUS[key] ?? STATUS.sent;
|
|
427
|
+
const Icon = cfg.icon;
|
|
428
|
+
return /* @__PURE__ */ jsxs("span", { className: `inline-flex items-center gap-0.5 ${cfg.cls}`, title: cfg.label, children: [
|
|
429
|
+
/* @__PURE__ */ jsx(Icon, { className: "h-3 w-3" }),
|
|
430
|
+
BAD.has(key) && /* @__PURE__ */ jsx("span", { className: "text-[9px] font-semibold uppercase", children: cfg.label })
|
|
431
|
+
] });
|
|
432
|
+
}
|
|
433
|
+
function MessageMenu({ m }) {
|
|
434
|
+
const [open, setOpen] = React10__default.useState(false);
|
|
435
|
+
const [detail, setDetail] = React10__default.useState(null);
|
|
436
|
+
const [loading, setLoading] = React10__default.useState(false);
|
|
437
|
+
const [err, setErr] = React10__default.useState(null);
|
|
438
|
+
async function load() {
|
|
439
|
+
setOpen(true);
|
|
440
|
+
if (!m.providerMessageId) return;
|
|
441
|
+
setLoading(true);
|
|
442
|
+
setErr(null);
|
|
443
|
+
try {
|
|
444
|
+
const sb = getSupabaseClientOptional();
|
|
445
|
+
const { data, error } = await sb.functions.invoke("conversations-provider", {
|
|
446
|
+
body: { tenantId: getActiveTenantId(), action: "message_detail", providerMessageId: m.providerMessageId }
|
|
447
|
+
});
|
|
448
|
+
if (error) {
|
|
449
|
+
const d = await error.context?.json?.().catch(() => null);
|
|
450
|
+
throw new Error(d?.message ?? error.message);
|
|
451
|
+
}
|
|
452
|
+
if (data?.error) throw new Error(data.message ?? data.error);
|
|
453
|
+
setDetail(data.detail);
|
|
454
|
+
} catch (e) {
|
|
455
|
+
setErr(e?.message ?? "N\xE3o consegui buscar");
|
|
456
|
+
} finally {
|
|
457
|
+
setLoading(false);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
461
|
+
/* @__PURE__ */ jsx(
|
|
462
|
+
Button,
|
|
463
|
+
{
|
|
464
|
+
variant: "bare",
|
|
465
|
+
size: "bare",
|
|
466
|
+
onClick: () => void load(),
|
|
467
|
+
"aria-label": "Detalhes da mensagem",
|
|
468
|
+
className: "opacity-0 transition-opacity group-hover:opacity-60 hover:!opacity-100",
|
|
469
|
+
children: /* @__PURE__ */ jsx(MoreVertical, { className: "h-3.5 w-3.5" })
|
|
470
|
+
}
|
|
471
|
+
),
|
|
472
|
+
open && /* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-[120] flex items-center justify-center bg-black/50 p-4", onClick: () => setOpen(false), children: /* @__PURE__ */ jsxs("div", { className: "max-h-[80vh] w-full max-w-2xl overflow-hidden rounded-card bg-card shadow-2xl", onClick: (e) => e.stopPropagation(), children: [
|
|
473
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between border-b px-4 py-3", children: [
|
|
474
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
475
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm font-semibold", children: "Detalhes da mensagem" }),
|
|
476
|
+
/* @__PURE__ */ jsxs("p", { className: "text-[11px] text-muted-foreground", children: [
|
|
477
|
+
new Date(m.at).toLocaleString(),
|
|
478
|
+
" \xB7 ",
|
|
479
|
+
m.direction === "outbound" ? "sa\xEDda" : "entrada",
|
|
480
|
+
m.deliveryStatus ? ` \xB7 ${m.deliveryStatus}` : ""
|
|
481
|
+
] })
|
|
482
|
+
] }),
|
|
483
|
+
/* @__PURE__ */ jsx(Button, { variant: "bare", size: "bare", onClick: () => setOpen(false), "aria-label": "Fechar", children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" }) })
|
|
484
|
+
] }),
|
|
485
|
+
/* @__PURE__ */ jsxs("div", { className: "max-h-[60vh] overflow-auto p-4 text-xs", children: [
|
|
486
|
+
/* @__PURE__ */ jsx("p", { className: "mb-1 font-medium text-muted-foreground", children: "Nesta base" }),
|
|
487
|
+
/* @__PURE__ */ jsx("pre", { className: "mb-4 overflow-auto rounded bg-muted/40 p-3 text-[11px] leading-relaxed", children: JSON.stringify({
|
|
488
|
+
id: m.id,
|
|
489
|
+
direction: m.direction,
|
|
490
|
+
body: m.body,
|
|
491
|
+
at: m.at,
|
|
492
|
+
author: m.author,
|
|
493
|
+
deliveryStatus: m.deliveryStatus,
|
|
494
|
+
providerMessageId: m.providerMessageId
|
|
495
|
+
}, null, 2) }),
|
|
496
|
+
/* @__PURE__ */ jsx("p", { className: "mb-1 font-medium text-muted-foreground", children: "No provedor" }),
|
|
497
|
+
!m.providerMessageId && // A ausência é a informação: sem id, ninguém do outro lado
|
|
498
|
+
// chegou a aceitar esta mensagem. Ela não atrasou — ela não saiu.
|
|
499
|
+
/* @__PURE__ */ jsx("p", { className: "rounded bg-destructive-soft p-3 text-[11px] text-destructive-soft-foreground", children: "Esta mensagem n\xE3o tem id no provedor \u2014 ela nunca chegou a ser aceita para envio. N\xE3o h\xE1 o que buscar do outro lado." }),
|
|
500
|
+
loading && /* @__PURE__ */ jsxs("p", { className: "flex items-center gap-2 p-3 text-muted-foreground", children: [
|
|
501
|
+
/* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 animate-spin" }),
|
|
502
|
+
" buscando\u2026"
|
|
503
|
+
] }),
|
|
504
|
+
err && /* @__PURE__ */ jsx("p", { className: "rounded bg-destructive-soft p-3 text-[11px] text-destructive-soft-foreground", children: err }),
|
|
505
|
+
detail != null && /* @__PURE__ */ jsx("pre", { className: "overflow-auto rounded bg-muted/40 p-3 text-[11px] leading-relaxed", children: JSON.stringify(detail, null, 2) })
|
|
506
|
+
] })
|
|
507
|
+
] }) })
|
|
508
|
+
] });
|
|
509
|
+
}
|
|
510
|
+
var COM_ARQUIVO = /* @__PURE__ */ new Set(["image", "video", "audio", "document", "sticker"]);
|
|
511
|
+
function temArquivo(m) {
|
|
512
|
+
return !!m.mediaPath && COM_ARQUIVO.has(String(m.messageType));
|
|
513
|
+
}
|
|
514
|
+
function useUrlAssinada(m, visivel) {
|
|
515
|
+
const [url, setUrl] = React10.useState(null);
|
|
516
|
+
const [erro, setErro] = React10.useState(false);
|
|
517
|
+
React10.useEffect(() => {
|
|
518
|
+
if (!visivel || url || erro || !temArquivo(m)) return;
|
|
519
|
+
let vivo = true;
|
|
520
|
+
(async () => {
|
|
521
|
+
try {
|
|
522
|
+
const sb = getSupabaseClientOptional();
|
|
523
|
+
const { data, error } = await sb.functions.invoke("conversations-provider", {
|
|
524
|
+
body: { tenantId: getActiveTenantId(), action: "media_url", messageId: m.id }
|
|
525
|
+
});
|
|
526
|
+
if (error || data?.error) throw new Error("falhou");
|
|
527
|
+
if (vivo && data?.url) setUrl(data.url);
|
|
528
|
+
else if (vivo) setErro(true);
|
|
529
|
+
} catch {
|
|
530
|
+
if (vivo) setErro(true);
|
|
531
|
+
}
|
|
532
|
+
})();
|
|
533
|
+
return () => {
|
|
534
|
+
vivo = false;
|
|
535
|
+
};
|
|
536
|
+
}, [visivel, m.id, url, erro]);
|
|
537
|
+
return { url, erro };
|
|
538
|
+
}
|
|
539
|
+
function tamanho(bytes) {
|
|
540
|
+
if (!bytes || bytes <= 0) return "";
|
|
541
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
542
|
+
if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
|
|
543
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
544
|
+
}
|
|
545
|
+
function duracao(seg) {
|
|
546
|
+
if (!seg || seg <= 0) return "";
|
|
547
|
+
const m = Math.floor(seg / 60);
|
|
548
|
+
const s = Math.floor(seg % 60);
|
|
549
|
+
return `${m}:${String(s).padStart(2, "0")}`;
|
|
550
|
+
}
|
|
551
|
+
function MediaBubble({ m, outbound }) {
|
|
552
|
+
const ref = React10.useRef(null);
|
|
553
|
+
const [visivel, setVisivel] = React10.useState(false);
|
|
554
|
+
const { url, erro } = useUrlAssinada(m, visivel);
|
|
555
|
+
React10.useEffect(() => {
|
|
556
|
+
const el = ref.current;
|
|
557
|
+
if (!el || typeof IntersectionObserver === "undefined") {
|
|
558
|
+
setVisivel(true);
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
const obs = new IntersectionObserver((entradas) => {
|
|
562
|
+
if (entradas.some((e) => e.isIntersecting)) {
|
|
563
|
+
setVisivel(true);
|
|
564
|
+
obs.disconnect();
|
|
565
|
+
}
|
|
566
|
+
}, { rootMargin: "200px" });
|
|
567
|
+
obs.observe(el);
|
|
568
|
+
return () => obs.disconnect();
|
|
569
|
+
}, []);
|
|
570
|
+
const tipo = String(m.messageType);
|
|
571
|
+
const p = m.payload ?? {};
|
|
572
|
+
const legenda = p.caption ?? null;
|
|
573
|
+
const rotulo = /* @__PURE__ */ jsx("p", { className: "whitespace-pre-wrap break-words", children: m.body });
|
|
574
|
+
if (erro) {
|
|
575
|
+
return /* @__PURE__ */ jsxs("div", { ref, className: "space-y-1", children: [
|
|
576
|
+
rotulo,
|
|
577
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs opacity-60", children: "O arquivo n\xE3o carregou." })
|
|
578
|
+
] });
|
|
579
|
+
}
|
|
580
|
+
if (!url) {
|
|
581
|
+
return /* @__PURE__ */ jsxs("div", { ref, className: "space-y-1", children: [
|
|
582
|
+
rotulo,
|
|
583
|
+
/* @__PURE__ */ jsx("div", { className: "h-1 w-24 animate-pulse rounded bg-current opacity-20" })
|
|
584
|
+
] });
|
|
585
|
+
}
|
|
586
|
+
if (tipo === "image" || tipo === "sticker") {
|
|
587
|
+
return /* @__PURE__ */ jsxs("div", { ref, className: "space-y-1", children: [
|
|
588
|
+
/* @__PURE__ */ jsx("a", { href: url, target: "_blank", rel: "noreferrer", children: /* @__PURE__ */ jsx(
|
|
589
|
+
"img",
|
|
590
|
+
{
|
|
591
|
+
src: url,
|
|
592
|
+
alt: legenda ?? (tipo === "sticker" ? "Figurinha" : "Imagem recebida"),
|
|
593
|
+
className: tipo === "sticker" ? "max-h-32 w-auto" : "max-h-80 w-auto rounded-lg",
|
|
594
|
+
loading: "lazy"
|
|
595
|
+
}
|
|
596
|
+
) }),
|
|
597
|
+
legenda ? /* @__PURE__ */ jsx("p", { className: "whitespace-pre-wrap break-words", children: legenda }) : null
|
|
598
|
+
] });
|
|
599
|
+
}
|
|
600
|
+
if (tipo === "video") {
|
|
601
|
+
return /* @__PURE__ */ jsxs("div", { ref, className: "space-y-1", children: [
|
|
602
|
+
/* @__PURE__ */ jsx("video", { src: url, controls: true, preload: "metadata", className: "max-h-80 w-auto rounded-lg" }),
|
|
603
|
+
legenda ? /* @__PURE__ */ jsx("p", { className: "whitespace-pre-wrap break-words", children: legenda }) : null
|
|
604
|
+
] });
|
|
605
|
+
}
|
|
606
|
+
if (tipo === "audio") {
|
|
607
|
+
return /* @__PURE__ */ jsxs("div", { ref, className: "space-y-1", children: [
|
|
608
|
+
/* @__PURE__ */ jsx("audio", { src: url, controls: true, preload: "metadata", className: "h-9 w-64 max-w-full" }),
|
|
609
|
+
/* @__PURE__ */ jsxs("p", { className: "text-xs opacity-60", children: [
|
|
610
|
+
p.ptt ? "Mensagem de voz" : "\xC1udio",
|
|
611
|
+
duracao(p.seconds) ? ` \xB7 ${duracao(p.seconds)}` : ""
|
|
612
|
+
] })
|
|
613
|
+
] });
|
|
614
|
+
}
|
|
615
|
+
const nome = p.fileName ?? "Documento";
|
|
616
|
+
return /* @__PURE__ */ jsxs("div", { ref, className: "space-y-1", children: [
|
|
617
|
+
/* @__PURE__ */ jsxs(
|
|
618
|
+
"a",
|
|
619
|
+
{
|
|
620
|
+
href: url,
|
|
621
|
+
target: "_blank",
|
|
622
|
+
rel: "noreferrer",
|
|
623
|
+
download: nome,
|
|
624
|
+
className: `flex items-center gap-2 rounded-lg border p-2 ${outbound ? "border-white/30" : "border-black/10"}`,
|
|
625
|
+
children: [
|
|
626
|
+
/* @__PURE__ */ jsx(FileText, { className: "h-5 w-5 shrink-0 opacity-70" }),
|
|
627
|
+
/* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
|
|
628
|
+
/* @__PURE__ */ jsx("span", { className: "block truncate text-sm", children: nome }),
|
|
629
|
+
tamanho(p.fileLength) ? /* @__PURE__ */ jsx("span", { className: "block text-xs opacity-60", children: tamanho(p.fileLength) }) : null
|
|
630
|
+
] }),
|
|
631
|
+
/* @__PURE__ */ jsx(Download, { className: "h-4 w-4 shrink-0 opacity-70" })
|
|
632
|
+
]
|
|
633
|
+
}
|
|
634
|
+
),
|
|
635
|
+
legenda ? /* @__PURE__ */ jsx("p", { className: "whitespace-pre-wrap break-words", children: legenda }) : null
|
|
636
|
+
] });
|
|
637
|
+
}
|
|
638
|
+
function ContactCard({ contacts, outbound }) {
|
|
639
|
+
const [copied, setCopied] = React10__default.useState(null);
|
|
640
|
+
const copy = (value) => {
|
|
641
|
+
void navigator.clipboard?.writeText(value);
|
|
642
|
+
setCopied(value);
|
|
643
|
+
window.setTimeout(() => setCopied((c) => c === value ? null : c), 1500);
|
|
644
|
+
};
|
|
645
|
+
return /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2", children: contacts.map((c, i) => /* @__PURE__ */ jsxs(
|
|
646
|
+
"div",
|
|
647
|
+
{
|
|
648
|
+
className: cn(
|
|
649
|
+
"flex items-start gap-2.5 rounded-xl px-2.5 py-2",
|
|
650
|
+
outbound ? "bg-white/15" : "bg-muted"
|
|
651
|
+
),
|
|
652
|
+
children: [
|
|
653
|
+
/* @__PURE__ */ jsx(UserRound, { className: cn("mt-0.5 h-4 w-4 shrink-0", outbound ? "text-white/80" : "text-muted-foreground") }),
|
|
654
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
655
|
+
/* @__PURE__ */ jsx("p", { className: "truncate font-medium", children: c.name }),
|
|
656
|
+
c.phones.map((p) => /* @__PURE__ */ jsxs(
|
|
657
|
+
Button,
|
|
658
|
+
{
|
|
659
|
+
variant: "bare",
|
|
660
|
+
size: "bare",
|
|
661
|
+
type: "button",
|
|
662
|
+
onClick: () => copy(p.display),
|
|
663
|
+
className: cn(
|
|
664
|
+
"group/phone mt-0.5 flex items-center justify-start gap-1.5 text-left text-[13px] font-normal tabular-nums",
|
|
665
|
+
outbound ? "text-white/90 hover:text-white" : "text-muted-foreground hover:text-foreground"
|
|
666
|
+
),
|
|
667
|
+
children: [
|
|
668
|
+
p.display,
|
|
669
|
+
copied === p.display ? /* @__PURE__ */ jsx(Check, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(Copy, { className: "h-3 w-3 opacity-0 transition-opacity group-hover/phone:opacity-100" })
|
|
670
|
+
]
|
|
671
|
+
},
|
|
672
|
+
p.digits
|
|
673
|
+
))
|
|
674
|
+
] })
|
|
675
|
+
]
|
|
676
|
+
},
|
|
677
|
+
`${c.name}-${i}`
|
|
678
|
+
)) });
|
|
679
|
+
}
|
|
680
|
+
function MessageBody({ m, outbound }) {
|
|
681
|
+
const contacts = m.messageType === "contact" ? m.payload?.contacts ?? [] : [];
|
|
682
|
+
if (contacts.length > 0) return /* @__PURE__ */ jsx(ContactCard, { contacts, outbound });
|
|
683
|
+
if (temArquivo(m)) return /* @__PURE__ */ jsx(MediaBubble, { m, outbound });
|
|
684
|
+
return /* @__PURE__ */ jsx("p", { className: "whitespace-pre-wrap break-words", children: m.body });
|
|
685
|
+
}
|
|
686
|
+
function buildRows(messages) {
|
|
687
|
+
const rows = [];
|
|
688
|
+
let lastDay = "";
|
|
689
|
+
messages.forEach((m, i) => {
|
|
690
|
+
const day = new Date(m.at).toDateString();
|
|
691
|
+
if (day !== lastDay) {
|
|
692
|
+
rows.push({ kind: "day", id: `day-${day}`, label: dayLabel(m.at) });
|
|
693
|
+
lastDay = day;
|
|
694
|
+
}
|
|
695
|
+
const prev = messages[i - 1];
|
|
696
|
+
const next = messages[i + 1];
|
|
697
|
+
const samePrev = prev && prev.direction === m.direction && new Date(prev.at).toDateString() === day;
|
|
698
|
+
const sameNext = next && next.direction === m.direction && new Date(next.at).toDateString() === day;
|
|
699
|
+
rows.push({ kind: "msg", id: m.id, message: m, startsRun: !samePrev, endsRun: !sameNext });
|
|
700
|
+
});
|
|
701
|
+
return rows;
|
|
702
|
+
}
|
|
703
|
+
function MessageThread({ selected, onTogglePanel, panelOpen, onBack, backAlways, className }) {
|
|
704
|
+
const t = useTranslation();
|
|
705
|
+
const { messages, sending, send, setStatus, refreshMessages } = useConversationsStore((s) => s);
|
|
706
|
+
const [draft, setDraft] = React10__default.useState("");
|
|
707
|
+
const win = useSendWindow(selected.id);
|
|
708
|
+
const canType = win.mode.kind === "free";
|
|
709
|
+
const threadRef = React10__default.useRef(null);
|
|
710
|
+
React10__default.useEffect(() => {
|
|
711
|
+
threadRef.current?.scrollTo({ top: threadRef.current.scrollHeight, behavior: "smooth" });
|
|
712
|
+
}, [messages.length, selected.id]);
|
|
713
|
+
React10__default.useEffect(() => {
|
|
714
|
+
if (!selected.id) return;
|
|
715
|
+
let timer = null;
|
|
716
|
+
const start = () => {
|
|
717
|
+
if (!timer) timer = setInterval(() => {
|
|
718
|
+
void refreshMessages();
|
|
719
|
+
}, 1e4);
|
|
720
|
+
};
|
|
721
|
+
const stop = () => {
|
|
722
|
+
if (timer) {
|
|
723
|
+
clearInterval(timer);
|
|
724
|
+
timer = null;
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
const onVis = () => document.visibilityState === "visible" ? start() : stop();
|
|
728
|
+
onVis();
|
|
729
|
+
document.addEventListener("visibilitychange", onVis);
|
|
730
|
+
return () => {
|
|
731
|
+
stop();
|
|
732
|
+
document.removeEventListener("visibilitychange", onVis);
|
|
733
|
+
};
|
|
734
|
+
}, [selected.id, refreshMessages]);
|
|
735
|
+
async function handleSend() {
|
|
736
|
+
if (!draft.trim()) return;
|
|
737
|
+
const body = draft;
|
|
738
|
+
setDraft("");
|
|
739
|
+
await send(body);
|
|
740
|
+
}
|
|
741
|
+
const accent = CHANNEL_ACCENT[selected.channel];
|
|
742
|
+
const rows = React10__default.useMemo(() => buildRows(messages), [messages]);
|
|
743
|
+
return /* @__PURE__ */ jsxs("section", { className: cn("flex min-w-0 flex-1 flex-col bg-muted/20", className), children: [
|
|
744
|
+
/* @__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: [
|
|
745
|
+
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-2 md:gap-3", children: [
|
|
746
|
+
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" }) }),
|
|
747
|
+
/* @__PURE__ */ jsx(Avatar, { name: selected.contactName, accent: selected.accent, size: "sm", channel: selected.channel }),
|
|
748
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
749
|
+
/* @__PURE__ */ jsx("p", { className: "truncate text-sm font-semibold text-foreground", children: selected.contactName }),
|
|
750
|
+
!panelOpen && /* @__PURE__ */ jsxs("p", { className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [
|
|
751
|
+
/* @__PURE__ */ jsx(ChannelBadge, { channel: selected.channel }),
|
|
752
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: selected.contactHandle })
|
|
753
|
+
] })
|
|
754
|
+
] })
|
|
755
|
+
] }),
|
|
756
|
+
/* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-1.5", children: [
|
|
757
|
+
/* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", onClick: () => setStatus("closed"), "aria-label": "Arquivar", children: [
|
|
758
|
+
/* @__PURE__ */ jsx(Archive, { className: "h-3.5 w-3.5 sm:mr-1" }),
|
|
759
|
+
" ",
|
|
760
|
+
/* @__PURE__ */ jsx("span", { className: "hidden sm:inline", children: "Arquivar" })
|
|
761
|
+
] }),
|
|
762
|
+
/* @__PURE__ */ jsx(
|
|
763
|
+
Button,
|
|
764
|
+
{
|
|
765
|
+
variant: panelOpen ? "secondary" : "ghost",
|
|
766
|
+
size: "icon",
|
|
767
|
+
onClick: onTogglePanel,
|
|
768
|
+
"aria-label": t("conversations.thread.details"),
|
|
769
|
+
children: /* @__PURE__ */ jsx(PanelRight, { className: "h-4 w-4" })
|
|
770
|
+
}
|
|
771
|
+
)
|
|
772
|
+
] })
|
|
773
|
+
] }),
|
|
774
|
+
/* @__PURE__ */ jsxs("div", { ref: threadRef, className: "min-h-0 flex-1 overflow-y-auto px-5 py-4", children: [
|
|
775
|
+
rows.map((row) => {
|
|
776
|
+
if (row.kind === "day") {
|
|
777
|
+
return /* @__PURE__ */ jsx("div", { className: "my-3 flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "rounded-full bg-muted px-3 py-0.5 text-[11px] font-medium text-muted-foreground", children: row.label }) }, row.id);
|
|
778
|
+
}
|
|
779
|
+
const m = row.message;
|
|
780
|
+
const outbound = m.direction === "outbound";
|
|
781
|
+
const failed = isFailed(m);
|
|
782
|
+
return /* @__PURE__ */ jsxs(
|
|
783
|
+
"div",
|
|
784
|
+
{
|
|
785
|
+
className: cn("group flex items-center gap-1", outbound ? "justify-end" : "justify-start", row.startsRun ? "mt-2.5" : "mt-0.5"),
|
|
786
|
+
children: [
|
|
787
|
+
outbound && /* @__PURE__ */ jsx(MessageMenu, { m }),
|
|
788
|
+
/* @__PURE__ */ jsxs(
|
|
789
|
+
"div",
|
|
790
|
+
{
|
|
791
|
+
className: cn(
|
|
792
|
+
"max-w-[68%] px-3.5 py-2 text-sm shadow-sm",
|
|
793
|
+
outbound ? "rounded-2xl text-white" : "rounded-2xl bg-card text-foreground",
|
|
794
|
+
// Tail only on the last bubble of a run, on the sender's side.
|
|
795
|
+
outbound && row.endsRun && "rounded-br-sm",
|
|
796
|
+
!outbound && row.endsRun && "rounded-bl-sm",
|
|
797
|
+
// UMA MENSAGEM QUE FALHOU PRECISA PARECER QUE FALHOU. Duas
|
|
798
|
+
// ficaram um dia na caixa com a cara de qualquer outra e nunca
|
|
799
|
+
// chegaram a ninguém — não havia como saber olhando.
|
|
800
|
+
outbound && failed && "ring-2 ring-destructive"
|
|
801
|
+
),
|
|
802
|
+
style: outbound ? { backgroundColor: failed ? "hsl(var(--destructive))" : accent.color } : void 0,
|
|
803
|
+
children: [
|
|
804
|
+
/* @__PURE__ */ jsx(MessageBody, { m, outbound }),
|
|
805
|
+
/* @__PURE__ */ jsxs("div", { className: cn("mt-0.5 flex items-center justify-end gap-1 text-[10px]", outbound ? "text-white/70" : "text-muted-foreground"), children: [
|
|
806
|
+
new Date(m.at).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }),
|
|
807
|
+
/* @__PURE__ */ jsx(DeliveryMark, { m })
|
|
808
|
+
] })
|
|
809
|
+
]
|
|
810
|
+
}
|
|
811
|
+
),
|
|
812
|
+
!outbound && /* @__PURE__ */ jsx(MessageMenu, { m })
|
|
813
|
+
]
|
|
814
|
+
},
|
|
815
|
+
row.id
|
|
816
|
+
);
|
|
817
|
+
}),
|
|
818
|
+
rows.length === 0 && /* @__PURE__ */ jsxs("div", { className: "flex h-full flex-col items-center justify-center text-muted-foreground", children: [
|
|
819
|
+
/* @__PURE__ */ jsx(MessageSquare, { className: "h-7 w-7" }),
|
|
820
|
+
/* @__PURE__ */ jsx("p", { className: "mt-2 text-sm", children: t("conversations.thread.empty") })
|
|
821
|
+
] })
|
|
822
|
+
] }),
|
|
823
|
+
/* @__PURE__ */ jsxs("div", { className: "border-t border-border bg-card", children: [
|
|
824
|
+
/* @__PURE__ */ jsx(SendWindowBanner, { state: win }),
|
|
825
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-end gap-2 px-4 py-3", children: [
|
|
826
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsx(
|
|
827
|
+
Textarea,
|
|
828
|
+
{
|
|
829
|
+
value: draft,
|
|
830
|
+
onChange: (e) => setDraft(e.target.value),
|
|
831
|
+
onKeyDown: (e) => {
|
|
832
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
833
|
+
e.preventDefault();
|
|
834
|
+
void handleSend();
|
|
835
|
+
}
|
|
836
|
+
},
|
|
837
|
+
rows: 1,
|
|
838
|
+
disabled: !canType,
|
|
839
|
+
placeholder: canType ? t("conversations.thread.reply", { channel: CHANNEL_LABELS[selected.channel] }) : win.mode.kind === "blocked" ? "Envio bloqueado" : "Fora da janela \u2014 envie um template",
|
|
840
|
+
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"
|
|
841
|
+
}
|
|
842
|
+
) }),
|
|
843
|
+
/* @__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" }) })
|
|
844
|
+
] })
|
|
845
|
+
] })
|
|
846
|
+
] });
|
|
847
|
+
}
|
|
848
|
+
var TEMP = {
|
|
849
|
+
hot: { icon: Flame, cls: "bg-destructive-soft text-destructive-soft-foreground", label: "Quente" },
|
|
850
|
+
warm: { icon: Sun, cls: "bg-warning-soft text-warning-soft-foreground", label: "Morno" },
|
|
851
|
+
cold: { icon: Snowflake, cls: "bg-info-soft text-info-soft-foreground", label: "Frio" }
|
|
852
|
+
};
|
|
853
|
+
function LeadCard({ personId, handle }) {
|
|
854
|
+
const [lead, setLead] = React10__default.useState(void 0);
|
|
855
|
+
React10__default.useEffect(() => {
|
|
856
|
+
let alive = true;
|
|
857
|
+
const sb = getSupabaseClientOptional();
|
|
858
|
+
const tenantId = getActiveTenantId();
|
|
859
|
+
if (!sb || !tenantId || !personId && !handle) {
|
|
860
|
+
setLead(null);
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
(async () => {
|
|
864
|
+
try {
|
|
865
|
+
let q = sb.from("people").select("id, name, metadata, phone").eq("tenant_id", tenantId).eq("kind", "lead").limit(1);
|
|
866
|
+
q = personId ? q.eq("id", personId) : q.like("phone", `%${String(handle).replace(/\D/g, "").slice(-8)}`);
|
|
867
|
+
const { data } = await q.maybeSingle();
|
|
868
|
+
if (!alive) return;
|
|
869
|
+
if (!data) {
|
|
870
|
+
setLead(null);
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
const meta = data.metadata ?? {};
|
|
874
|
+
let score = null;
|
|
875
|
+
try {
|
|
876
|
+
const { data: s } = await sb.from("v_crm_lead_score").select("temperature, fit_score, interest_score, reasons").eq("lead_id", data.id).maybeSingle();
|
|
877
|
+
score = s;
|
|
878
|
+
} catch {
|
|
879
|
+
}
|
|
880
|
+
setLead({
|
|
881
|
+
id: data.id,
|
|
882
|
+
name: data.name,
|
|
883
|
+
company: meta.company ?? null,
|
|
884
|
+
status: meta.status ?? null,
|
|
885
|
+
sourceName: meta.sourceName ?? null,
|
|
886
|
+
temperature: score?.temperature ?? null,
|
|
887
|
+
fitScore: score?.fit_score ?? null,
|
|
888
|
+
interestScore: score?.interest_score ?? null,
|
|
889
|
+
reasons: Array.isArray(score?.reasons) ? score.reasons : []
|
|
890
|
+
});
|
|
891
|
+
} catch {
|
|
892
|
+
if (alive) setLead(null);
|
|
893
|
+
}
|
|
894
|
+
})();
|
|
895
|
+
return () => {
|
|
896
|
+
alive = false;
|
|
897
|
+
};
|
|
898
|
+
}, [personId, handle]);
|
|
899
|
+
if (lead === void 0) return /* @__PURE__ */ jsx("div", { className: "px-4 py-3 text-xs text-muted-foreground", children: "carregando\u2026" });
|
|
900
|
+
if (!lead) {
|
|
901
|
+
return /* @__PURE__ */ jsx("div", { className: "border-t border-border px-4 py-3", children: /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "Este contato ainda n\xE3o \xE9 um lead cadastrado." }) });
|
|
902
|
+
}
|
|
903
|
+
const t = lead.temperature ? TEMP[lead.temperature] : null;
|
|
904
|
+
const Icon = t?.icon;
|
|
905
|
+
return /* @__PURE__ */ jsxs("div", { className: "border-t border-border px-4 py-3", children: [
|
|
906
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-2", children: [
|
|
907
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
908
|
+
/* @__PURE__ */ jsx("p", { className: "truncate text-sm font-semibold", children: lead.name }),
|
|
909
|
+
lead.company && /* @__PURE__ */ jsxs("p", { className: "flex items-center gap-1 truncate text-xs text-muted-foreground", children: [
|
|
910
|
+
/* @__PURE__ */ jsx(Building2, { className: "h-3 w-3 shrink-0" }),
|
|
911
|
+
" ",
|
|
912
|
+
lead.company
|
|
913
|
+
] })
|
|
914
|
+
] }),
|
|
915
|
+
/* @__PURE__ */ jsxs(
|
|
916
|
+
"a",
|
|
917
|
+
{
|
|
918
|
+
href: appHref(`/sales/leads/${lead.id}`),
|
|
919
|
+
className: "inline-flex shrink-0 items-center gap-1 rounded-button border bg-card px-2 py-1 text-[11px] font-medium hover:bg-muted",
|
|
920
|
+
children: [
|
|
921
|
+
"Abrir lead ",
|
|
922
|
+
/* @__PURE__ */ jsx(ExternalLink, { className: "h-3 w-3" })
|
|
923
|
+
]
|
|
924
|
+
}
|
|
925
|
+
)
|
|
926
|
+
] }),
|
|
927
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-2 flex flex-wrap items-center gap-1.5", children: [
|
|
928
|
+
t && Icon && /* @__PURE__ */ jsxs("span", { className: `inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-medium ${t.cls}`, children: [
|
|
929
|
+
/* @__PURE__ */ jsx(Icon, { className: "h-3 w-3" }),
|
|
930
|
+
" ",
|
|
931
|
+
t.label,
|
|
932
|
+
lead.interestScore != null && /* @__PURE__ */ jsx("span", { className: "tabular-nums opacity-70", children: lead.interestScore })
|
|
933
|
+
] }),
|
|
934
|
+
lead.status && /* @__PURE__ */ jsx("span", { className: "rounded-full bg-muted px-2 py-0.5 text-[10px] capitalize text-muted-foreground", children: lead.status }),
|
|
935
|
+
lead.sourceName && /* @__PURE__ */ jsx("span", { className: "rounded-full bg-muted px-2 py-0.5 text-[10px] text-muted-foreground", children: lead.sourceName })
|
|
936
|
+
] }),
|
|
937
|
+
lead.reasons.length > 0 && /* @__PURE__ */ jsx("ul", { className: "mt-2 space-y-0.5", children: lead.reasons.slice(0, 4).map((r, i) => /* @__PURE__ */ jsxs("li", { className: "flex gap-1.5 text-[11px] text-muted-foreground", children: [
|
|
938
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground/40", children: "\u2022" }),
|
|
939
|
+
/* @__PURE__ */ jsx("span", { children: r })
|
|
940
|
+
] }, i)) })
|
|
941
|
+
] });
|
|
942
|
+
}
|
|
943
|
+
function Section({ icon: Icon, title, children }) {
|
|
944
|
+
return /* @__PURE__ */ jsxs("div", { className: "border-t border-border px-4 py-3", children: [
|
|
945
|
+
/* @__PURE__ */ jsxs("p", { className: "mb-1.5 flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground", children: [
|
|
946
|
+
/* @__PURE__ */ jsx(Icon, { className: "h-3 w-3" }),
|
|
947
|
+
" ",
|
|
948
|
+
title
|
|
949
|
+
] }),
|
|
950
|
+
children
|
|
951
|
+
] });
|
|
952
|
+
}
|
|
953
|
+
function ContactPanel({ contact, onClose, className }) {
|
|
954
|
+
const t = useTranslation();
|
|
955
|
+
return /* @__PURE__ */ jsxs("aside", { className: cn("flex shrink-0 flex-col overflow-y-auto border-l border-border bg-card", className), children: [
|
|
956
|
+
onClose && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between border-b border-border px-3 py-2 xl:hidden", children: [
|
|
957
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-semibold text-foreground", children: t("conversations.contact.details") }),
|
|
958
|
+
/* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon", onClick: onClose, "aria-label": t("conversations.contact.closeDetails"), children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" }) })
|
|
959
|
+
] }),
|
|
960
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-2 px-4 py-5 text-center", children: [
|
|
961
|
+
/* @__PURE__ */ jsx(Avatar, { name: contact.contactName, accent: contact.accent, size: "lg", channel: contact.channel }),
|
|
962
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
963
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm font-semibold text-foreground", children: contact.contactName }),
|
|
964
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: contact.contactHandle })
|
|
965
|
+
] }),
|
|
966
|
+
/* @__PURE__ */ jsx(ChannelBadge, { channel: contact.channel })
|
|
967
|
+
] }),
|
|
968
|
+
/* @__PURE__ */ jsx(LeadCard, { personId: contact.contactPersonId, handle: contact.contactHandle }),
|
|
969
|
+
/* @__PURE__ */ jsx(Section, { icon: User, title: t("conversations.contact.details"), children: /* @__PURE__ */ jsxs("dl", { className: "space-y-1.5 text-sm", children: [
|
|
970
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
|
|
971
|
+
/* @__PURE__ */ jsx("dt", { className: "text-muted-foreground", children: t("conversations.contact.channel") }),
|
|
972
|
+
/* @__PURE__ */ jsx("dd", { className: "text-foreground", children: CHANNEL_LABELS[contact.channel] })
|
|
973
|
+
] }),
|
|
974
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
|
|
975
|
+
/* @__PURE__ */ jsx("dt", { className: "text-muted-foreground", children: t("conversations.contact.status") }),
|
|
976
|
+
/* @__PURE__ */ jsx("dd", { className: "text-foreground", children: t(`conversations.status.${contact.status}`) })
|
|
977
|
+
] }),
|
|
978
|
+
contact.assignedTo && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
|
|
979
|
+
/* @__PURE__ */ jsx("dt", { className: "text-muted-foreground", children: t("conversations.contact.assignedTo") }),
|
|
980
|
+
/* @__PURE__ */ jsx("dd", { className: "text-foreground", children: contact.assignedTo })
|
|
981
|
+
] }),
|
|
982
|
+
contact.leadOwnerName && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
|
|
983
|
+
/* @__PURE__ */ jsx("dt", { className: "text-muted-foreground", children: t("conversations.contact.leadOwner") }),
|
|
984
|
+
/* @__PURE__ */ jsx("dd", { className: "font-medium text-foreground", children: contact.leadOwnerName })
|
|
985
|
+
] }),
|
|
986
|
+
contact.location && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
|
|
987
|
+
/* @__PURE__ */ jsxs("dt", { className: "flex items-center gap-1 text-muted-foreground", children: [
|
|
988
|
+
/* @__PURE__ */ jsx(MapPin, { className: "h-3 w-3" }),
|
|
989
|
+
" ",
|
|
990
|
+
t("conversations.contact.location")
|
|
991
|
+
] }),
|
|
992
|
+
/* @__PURE__ */ jsx("dd", { className: "text-foreground", children: contact.location })
|
|
993
|
+
] })
|
|
994
|
+
] }) }),
|
|
995
|
+
contact.tags.length > 0 && /* @__PURE__ */ jsx(Section, { icon: Tag, title: t("conversations.contact.tags"), children: /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5", children: contact.tags.map((tag) => /* @__PURE__ */ jsx("span", { className: "rounded-full bg-muted px-2 py-0.5 text-xs font-medium text-foreground", children: tag }, tag)) }) }),
|
|
996
|
+
contact.note && /* @__PURE__ */ jsx(Section, { icon: StickyNote, title: t("conversations.contact.note"), children: /* @__PURE__ */ jsx("p", { className: "text-sm text-foreground", children: contact.note }) }),
|
|
997
|
+
/* @__PURE__ */ jsx(Section, { icon: Link2, title: t("conversations.contact.linkedRecords"), children: /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("conversations.contact.noLinkedRecords") }) })
|
|
998
|
+
] });
|
|
999
|
+
}
|
|
1000
|
+
var CHANNELS = ["whatsapp", "sms", "instagram", "email", "webchat"];
|
|
1001
|
+
function personHandleFor(channel, contact) {
|
|
1002
|
+
if (!contact) return "";
|
|
1003
|
+
if (channel === "email") return contact.email ?? "";
|
|
1004
|
+
if (channel === "sms" || channel === "whatsapp") return contact.phone ?? "";
|
|
1005
|
+
return "";
|
|
1006
|
+
}
|
|
1007
|
+
var HANDLE_LABEL_KEY = {
|
|
1008
|
+
whatsapp: "conversations.new.handleLabel.phone",
|
|
1009
|
+
sms: "conversations.new.handleLabel.phone",
|
|
1010
|
+
email: "conversations.new.handleLabel.email",
|
|
1011
|
+
instagram: "conversations.new.handleLabel.instagram",
|
|
1012
|
+
webchat: "conversations.new.handleLabel.webchat"
|
|
1013
|
+
};
|
|
1014
|
+
function NewConversationPanel({
|
|
1015
|
+
open,
|
|
1016
|
+
onOpenChange
|
|
1017
|
+
}) {
|
|
1018
|
+
const t = useTranslation();
|
|
1019
|
+
const create = useConversationsStore((s) => s.create);
|
|
1020
|
+
const config = useConversationsConfig();
|
|
1021
|
+
const guardConversations = useLimitGuard("conversations_month");
|
|
1022
|
+
const [channel, setChannel] = React10__default.useState("whatsapp");
|
|
1023
|
+
const [contact, setContact] = React10__default.useState(null);
|
|
1024
|
+
const [typedHandle, setTypedHandle] = React10__default.useState("");
|
|
1025
|
+
const [creatingContact, setCreatingContact] = React10__default.useState(false);
|
|
1026
|
+
const [firstMessage, setFirstMessage] = React10__default.useState("");
|
|
1027
|
+
const [submitting, setSubmitting] = React10__default.useState(false);
|
|
1028
|
+
const [pickerKey, setPickerKey] = React10__default.useState(0);
|
|
1029
|
+
React10__default.useEffect(() => {
|
|
1030
|
+
if (open) {
|
|
1031
|
+
setChannel("whatsapp");
|
|
1032
|
+
setContact(null);
|
|
1033
|
+
setTypedHandle("");
|
|
1034
|
+
setCreatingContact(false);
|
|
1035
|
+
setFirstMessage("");
|
|
1036
|
+
setSubmitting(false);
|
|
1037
|
+
setPickerKey((k) => k + 1);
|
|
1038
|
+
}
|
|
1039
|
+
}, [open]);
|
|
1040
|
+
const derivedHandle = personHandleFor(channel, contact);
|
|
1041
|
+
const effectiveHandle = derivedHandle || typedHandle;
|
|
1042
|
+
const handleLabel = t(HANDLE_LABEL_KEY[channel]);
|
|
1043
|
+
const canSubmit = (contact?.name.trim().length ?? 0) > 0 && !submitting;
|
|
1044
|
+
async function handleSubmit(e) {
|
|
1045
|
+
e.preventDefault();
|
|
1046
|
+
if (!canSubmit) return;
|
|
1047
|
+
setSubmitting(true);
|
|
1048
|
+
try {
|
|
1049
|
+
if (await guardConversations() === "blocked") return;
|
|
1050
|
+
await create({
|
|
1051
|
+
channel,
|
|
1052
|
+
contactName: contact.name.trim(),
|
|
1053
|
+
contactPersonId: contact?.id,
|
|
1054
|
+
contactHandle: effectiveHandle.trim() || void 0,
|
|
1055
|
+
firstMessage: firstMessage.trim() || void 0
|
|
1056
|
+
});
|
|
1057
|
+
invalidateLimit("conversations_month");
|
|
1058
|
+
onOpenChange(false);
|
|
1059
|
+
} catch (err) {
|
|
1060
|
+
const message = errorMessage(err);
|
|
1061
|
+
toast.error(t("conversations.new.createFailed"), { description: message });
|
|
1062
|
+
} finally {
|
|
1063
|
+
setSubmitting(false);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
return /* @__PURE__ */ jsx(Modal, { open, onOpenChange, children: /* @__PURE__ */ jsxs(ModalContent, { className: "max-w-md", children: [
|
|
1067
|
+
/* @__PURE__ */ jsx(ModalHeader, { children: /* @__PURE__ */ jsx(ModalTitle, { children: t("conversations.new.title") }) }),
|
|
1068
|
+
/* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, className: "flex min-h-0 flex-1 flex-col overflow-hidden", children: [
|
|
1069
|
+
/* @__PURE__ */ jsxs(ModalBody, { className: "space-y-4", children: [
|
|
1070
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
1071
|
+
/* @__PURE__ */ jsx("label", { className: "mb-1.5 block text-xs font-medium text-muted-foreground", children: t("conversations.new.channel") }),
|
|
1072
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5", children: CHANNELS.map((ch) => {
|
|
1073
|
+
const Icon = CHANNEL_ICON[ch];
|
|
1074
|
+
const active = channel === ch;
|
|
1075
|
+
return /* @__PURE__ */ jsxs(
|
|
1076
|
+
Button,
|
|
1077
|
+
{
|
|
1078
|
+
variant: "bare",
|
|
1079
|
+
size: "bare",
|
|
1080
|
+
type: "button",
|
|
1081
|
+
onClick: () => setChannel(ch),
|
|
1082
|
+
"aria-pressed": active,
|
|
1083
|
+
className: cn(
|
|
1084
|
+
"inline-flex items-center gap-1.5 rounded-full px-3 py-1.5 text-xs font-medium transition-colors",
|
|
1085
|
+
active ? "text-white" : "bg-muted text-muted-foreground hover:bg-muted/70"
|
|
1086
|
+
),
|
|
1087
|
+
style: active ? { backgroundColor: CHANNEL_ACCENT[ch].color } : void 0,
|
|
1088
|
+
children: [
|
|
1089
|
+
/* @__PURE__ */ jsx(Icon, { className: "h-3 w-3" }),
|
|
1090
|
+
CHANNEL_LABELS[ch]
|
|
1091
|
+
]
|
|
1092
|
+
},
|
|
1093
|
+
ch
|
|
1094
|
+
);
|
|
1095
|
+
}) })
|
|
1096
|
+
] }),
|
|
1097
|
+
/* @__PURE__ */ jsx(
|
|
1098
|
+
ContactPicker,
|
|
1099
|
+
{
|
|
1100
|
+
value: contact,
|
|
1101
|
+
onChange: setContact,
|
|
1102
|
+
kind: config.contactKind,
|
|
1103
|
+
lookup: config.contactLookup,
|
|
1104
|
+
createEntityDef: config.contactEntityDef,
|
|
1105
|
+
allowFreeText: true,
|
|
1106
|
+
onCreatingChange: setCreatingContact,
|
|
1107
|
+
autoFocus: true,
|
|
1108
|
+
label: t("conversations.new.contactName"),
|
|
1109
|
+
placeholder: t("conversations.new.contactNamePlaceholder"),
|
|
1110
|
+
secondaryText: derivedHandle || void 0,
|
|
1111
|
+
handleField: {
|
|
1112
|
+
label: handleLabel,
|
|
1113
|
+
derived: derivedHandle || void 0,
|
|
1114
|
+
value: typedHandle,
|
|
1115
|
+
onChange: setTypedHandle,
|
|
1116
|
+
fieldLabel: t("conversations.new.handle"),
|
|
1117
|
+
placeholder: t("conversations.new.handlePlaceholder")
|
|
1118
|
+
}
|
|
1119
|
+
},
|
|
1120
|
+
pickerKey
|
|
1121
|
+
),
|
|
1122
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
1123
|
+
/* @__PURE__ */ jsx("label", { htmlFor: "conv-first-message", className: "mb-1.5 block text-xs font-medium text-muted-foreground", children: t("conversations.new.firstMessage") }),
|
|
1124
|
+
/* @__PURE__ */ jsx(
|
|
1125
|
+
Textarea,
|
|
1126
|
+
{
|
|
1127
|
+
id: "conv-first-message",
|
|
1128
|
+
value: firstMessage,
|
|
1129
|
+
onChange: (e) => setFirstMessage(e.target.value),
|
|
1130
|
+
rows: 3,
|
|
1131
|
+
placeholder: t("conversations.new.firstMessagePlaceholder"),
|
|
1132
|
+
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"
|
|
1133
|
+
}
|
|
1134
|
+
)
|
|
1135
|
+
] })
|
|
1136
|
+
] }),
|
|
1137
|
+
/* @__PURE__ */ jsx(ModalFooter, { children: /* @__PURE__ */ jsx(Button, { type: "submit", disabled: !canSubmit, className: "w-full", children: submitting ? t("conversations.new.creating") : t("conversations.new.create") }) })
|
|
1138
|
+
] })
|
|
1139
|
+
] }) });
|
|
1140
|
+
}
|
|
1141
|
+
var porConta = /* @__PURE__ */ new Map();
|
|
1142
|
+
function assinarChegada(tenantId, ouvinte) {
|
|
1143
|
+
const supabase = getSupabaseClientOptional();
|
|
1144
|
+
if (!supabase?.channel || !tenantId) return () => {
|
|
1145
|
+
};
|
|
1146
|
+
let assinatura = porConta.get(tenantId);
|
|
1147
|
+
if (!assinatura) {
|
|
1148
|
+
const ouvintes = /* @__PURE__ */ new Set();
|
|
1149
|
+
const canal = supabase.channel(`conversas:${tenantId}`).on("broadcast", { event: "inbound" }, (msg) => {
|
|
1150
|
+
const carga = { conversationId: msg?.payload?.conversationId };
|
|
1151
|
+
for (const f of [...ouvintes]) {
|
|
1152
|
+
try {
|
|
1153
|
+
f(carga);
|
|
1154
|
+
} catch {
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
}).subscribe();
|
|
1158
|
+
assinatura = { canal, ouvintes };
|
|
1159
|
+
porConta.set(tenantId, assinatura);
|
|
1160
|
+
}
|
|
1161
|
+
assinatura.ouvintes.add(ouvinte);
|
|
1162
|
+
return () => {
|
|
1163
|
+
const a = porConta.get(tenantId);
|
|
1164
|
+
if (!a) return;
|
|
1165
|
+
a.ouvintes.delete(ouvinte);
|
|
1166
|
+
if (a.ouvintes.size === 0) {
|
|
1167
|
+
porConta.delete(tenantId);
|
|
1168
|
+
void getSupabaseClientOptional()?.removeChannel?.(a.canal);
|
|
1169
|
+
}
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// src/views/useCaixaAoVivo.ts
|
|
1174
|
+
function useCaixaAoVivo() {
|
|
1175
|
+
const tenantId = useActiveTenantId();
|
|
1176
|
+
const load = useConversationsStore((s) => s.load);
|
|
1177
|
+
const refreshMessages = useConversationsStore((s) => s.refreshMessages);
|
|
1178
|
+
const selectedId = useConversationsStore((s) => s.selectedId);
|
|
1179
|
+
const abertoRef = React10.useRef(selectedId);
|
|
1180
|
+
abertoRef.current = selectedId;
|
|
1181
|
+
React10.useEffect(() => {
|
|
1182
|
+
if (!tenantId) return;
|
|
1183
|
+
return assinarChegada(tenantId, ({ conversationId }) => {
|
|
1184
|
+
void load();
|
|
1185
|
+
if (conversationId && conversationId === abertoRef.current) {
|
|
1186
|
+
void refreshMessages();
|
|
1187
|
+
}
|
|
1188
|
+
});
|
|
1189
|
+
}, [tenantId, load, refreshMessages]);
|
|
1190
|
+
}
|
|
1191
|
+
function InboxView({ layout = "page", newSignal = 0 } = {}) {
|
|
1192
|
+
const t = useTranslation();
|
|
1193
|
+
const dock = layout !== "page";
|
|
1194
|
+
const { conversations, selectedId, deselect } = useConversationsStore((s) => s);
|
|
1195
|
+
useCaixaAoVivo();
|
|
1196
|
+
const isWideViewport = useMediaQuery("(min-width: 1280px)");
|
|
1197
|
+
const isWidePanel = isWideViewport && !dock;
|
|
1198
|
+
const railOpen = useRightRailStore((s) => s.open);
|
|
1199
|
+
const [panelOpen, setPanelOpen] = React10__default.useState(false);
|
|
1200
|
+
const [newOpen, setNewOpen] = React10__default.useState(false);
|
|
1201
|
+
const selected = conversations.find((c) => c.id === selectedId) ?? null;
|
|
1202
|
+
React10__default.useEffect(() => {
|
|
1203
|
+
if (newSignal > 0) setNewOpen(true);
|
|
1204
|
+
}, [newSignal]);
|
|
1205
|
+
React10__default.useEffect(() => {
|
|
1206
|
+
setPanelOpen(isWidePanel && !railOpen);
|
|
1207
|
+
}, [isWidePanel, railOpen]);
|
|
1208
|
+
const newActions = React10__default.useMemo(() => [{
|
|
1209
|
+
id: "new-conversation",
|
|
1210
|
+
label: t("conversations.list.new"),
|
|
1211
|
+
icon: SquarePen,
|
|
1212
|
+
intent: "primary",
|
|
1213
|
+
// Starting a conversation creates a thread — the FAB is a "+".
|
|
1214
|
+
creates: true,
|
|
1215
|
+
testId: "conversations-new",
|
|
1216
|
+
permission: { feature: "conversations", action: "create" },
|
|
1217
|
+
onSelect: () => setNewOpen(true)
|
|
1218
|
+
}], [t]);
|
|
1219
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1220
|
+
!dock && /* @__PURE__ */ jsx(PageHeaderActions, { children: /* @__PURE__ */ jsx(PageActions, { actions: newActions }) }),
|
|
1221
|
+
/* @__PURE__ */ jsx(NewConversationPanel, { open: newOpen, onOpenChange: setNewOpen }),
|
|
1222
|
+
/* @__PURE__ */ jsxs("div", { className: "relative flex h-full min-h-0 overflow-hidden bg-background", children: [
|
|
1223
|
+
/* @__PURE__ */ jsx(ConversationList, { className: cn(
|
|
1224
|
+
layout === "page" && (selected ? "hidden lg:flex" : "flex"),
|
|
1225
|
+
layout === "dock" && (selected ? "hidden" : "flex lg:w-full border-r-0"),
|
|
1226
|
+
layout === "dock-wide" && "flex lg:w-[300px]"
|
|
1227
|
+
) }),
|
|
1228
|
+
selected ? /* @__PURE__ */ jsx(
|
|
1229
|
+
MessageThread,
|
|
1230
|
+
{
|
|
1231
|
+
selected,
|
|
1232
|
+
panelOpen,
|
|
1233
|
+
onTogglePanel: () => setPanelOpen((v) => !v),
|
|
1234
|
+
onBack: deselect,
|
|
1235
|
+
backAlways: layout === "dock",
|
|
1236
|
+
className: "flex"
|
|
1237
|
+
}
|
|
1238
|
+
) : 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: [
|
|
1239
|
+
/* @__PURE__ */ jsx(MessageSquare, { className: "h-9 w-9" }),
|
|
1240
|
+
/* @__PURE__ */ jsx("p", { className: "mt-2 text-sm", children: t("conversations.empty.select") })
|
|
1241
|
+
] }),
|
|
1242
|
+
selected && panelOpen && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1243
|
+
/* @__PURE__ */ jsx(
|
|
1244
|
+
Button,
|
|
1245
|
+
{
|
|
1246
|
+
variant: "bare",
|
|
1247
|
+
size: "bare",
|
|
1248
|
+
className: cn("absolute inset-0 z-10 rounded-none bg-black/40", !dock && "xl:hidden"),
|
|
1249
|
+
"aria-label": "Close details",
|
|
1250
|
+
onClick: () => setPanelOpen(false)
|
|
1251
|
+
}
|
|
1252
|
+
),
|
|
1253
|
+
/* @__PURE__ */ jsx(
|
|
1254
|
+
ContactPanel,
|
|
1255
|
+
{
|
|
1256
|
+
contact: selected,
|
|
1257
|
+
onClose: () => setPanelOpen(false),
|
|
1258
|
+
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")
|
|
1259
|
+
}
|
|
1260
|
+
)
|
|
1261
|
+
] })
|
|
1262
|
+
] })
|
|
1263
|
+
] });
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
export { ConversationsContextProvider, InboxView, assinarChegada, isWaitingOnUs, useChannelConnections, useConversationsStore };
|
|
1267
|
+
//# sourceMappingURL=chunk-LJKLHGFX.js.map
|
|
1268
|
+
//# sourceMappingURL=chunk-LJKLHGFX.js.map
|