@fayz-ai/plugin-conversations 0.14.1 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/data/tables.d.ts +3 -0
- package/dist/data/tables.d.ts.map +1 -1
- package/dist/index.js +194 -77
- package/dist/index.js.map +1 -1
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/pt-BR.d.ts.map +1 -1
- package/dist/migrations/index.d.ts +1 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/store.d.ts +3 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/views/ContactPanel.d.ts.map +1 -1
- package/dist/views/ConversationList.d.ts.map +1 -1
- package/package.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"supabase.d.ts","sourceRoot":"","sources":["../../src/data/supabase.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAA;AAwBpD,MAAM,WAAW,2BAA2B;IAC1C,+EAA+E;IAC/E,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,SAAS,CAAC,CAAA;IAC9C,+DAA+D;IAC/D,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;
|
|
1
|
+
{"version":3,"file":"supabase.d.ts","sourceRoot":"","sources":["../../src/data/supabase.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAA;AAwBpD,MAAM,WAAW,2BAA2B;IAC1C,+EAA+E;IAC/E,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,SAAS,CAAC,CAAA;IAC9C,+DAA+D;IAC/D,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AA6CD,wBAAgB,mCAAmC,CACjD,MAAM,CAAC,EAAE,2BAA2B,GACnC,qBAAqB,CAmRvB"}
|
package/dist/data/tables.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export declare const T: {
|
|
2
2
|
readonly conversations: "plg_conversations";
|
|
3
|
+
/** A LEITURA da conversa: a mesma linha, com o dono do lead junto. A escrita
|
|
4
|
+
* continua na tabela — view com `security_invoker` não aceita INSERT aqui. */
|
|
5
|
+
readonly conversationsView: "v_conversations";
|
|
3
6
|
readonly messages: "plg_conversation_messages";
|
|
4
7
|
readonly channels: "plg_conversations_channels";
|
|
5
8
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tables.d.ts","sourceRoot":"","sources":["../../src/data/tables.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,CAAC;;;;
|
|
1
|
+
{"version":3,"file":"tables.d.ts","sourceRoot":"","sources":["../../src/data/tables.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,CAAC;;IAEZ;mFAC+E;;;;CAIvE,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import * as
|
|
2
|
-
import
|
|
3
|
-
import { Loader2, MessageSquare, AlertTriangle, Pause, Play, Send, RefreshCw, Plus, Clock, CheckCircle2, Check, ShieldCheck, ShieldAlert, QrCode, Zap, ExternalLink, SquarePen, Globe, Mail, Instagram, Phone, Search, Inbox, ChevronLeft, Archive, PanelRight, X,
|
|
4
|
-
import { toast, Button, Input, defineKpiWidget, defineTableWidget, PageHeaderActions, cn, Badge, Skeleton, KpiCard, TableWidget } from '@fayz-ai/ui';
|
|
1
|
+
import * as React11 from 'react';
|
|
2
|
+
import React11__default, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
|
|
3
|
+
import { Loader2, MessageSquare, AlertTriangle, Pause, Play, Send, RefreshCw, Plus, Clock, CheckCircle2, Check, ShieldCheck, ShieldAlert, QrCode, Zap, ExternalLink, SquarePen, Globe, Mail, Instagram, Phone, Search, User, Inbox, ChevronLeft, Archive, PanelRight, X, MapPin, Tag, StickyNote, Link2, MoreVertical, CheckCheck, Snowflake, Sun, Flame, Building2 } from 'lucide-react';
|
|
4
|
+
import { toast, Button, Input, defineKpiWidget, defineTableWidget, PageHeaderActions, cn, Badge, FilterChip, Skeleton, KpiCard, TableWidget } from '@fayz-ai/ui';
|
|
5
5
|
import { createConnectionStore, connectionRuns, connectionStatus, getActiveTenantId, getSupabaseClientOptional, useActiveTenantId, registerTranslations, useTranslation, countByTenant, CONNECTOR_RUNTIME_TOKEN_HEADER, connectorRuntimeToken, errorMessage } from '@fayz-ai/core';
|
|
6
6
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
7
7
|
import { SettingsGroup, PluginSettingsPanel, useNavContribution, PermissionGate, useLimitGuard, RightRailPage, ContactPicker, invalidateLimit } from '@fayz-ai/admin';
|
|
@@ -211,12 +211,12 @@ function Bar({ c }) {
|
|
|
211
211
|
] });
|
|
212
212
|
}
|
|
213
213
|
function CampaignsView() {
|
|
214
|
-
const [rows, setRows] =
|
|
215
|
-
const [templates, setTemplates] =
|
|
216
|
-
const [providers, setProviders] =
|
|
217
|
-
const [running, setRunning] =
|
|
218
|
-
const stop =
|
|
219
|
-
const load =
|
|
214
|
+
const [rows, setRows] = React11__default.useState(null);
|
|
215
|
+
const [templates, setTemplates] = React11__default.useState([]);
|
|
216
|
+
const [providers, setProviders] = React11__default.useState([]);
|
|
217
|
+
const [running, setRunning] = React11__default.useState(null);
|
|
218
|
+
const stop = React11__default.useRef(false);
|
|
219
|
+
const load = React11__default.useCallback(async () => {
|
|
220
220
|
try {
|
|
221
221
|
const [c, t, p] = await Promise.all([
|
|
222
222
|
campaignsApi.list(),
|
|
@@ -231,7 +231,7 @@ function CampaignsView() {
|
|
|
231
231
|
setRows([]);
|
|
232
232
|
}
|
|
233
233
|
}, []);
|
|
234
|
-
|
|
234
|
+
React11__default.useEffect(() => {
|
|
235
235
|
void load();
|
|
236
236
|
}, [load]);
|
|
237
237
|
const active = providers.find((p) => p.isActive);
|
|
@@ -380,6 +380,9 @@ function CampaignsView() {
|
|
|
380
380
|
// src/data/tables.ts
|
|
381
381
|
var T = {
|
|
382
382
|
conversations: "plg_conversations",
|
|
383
|
+
/** A LEITURA da conversa: a mesma linha, com o dono do lead junto. A escrita
|
|
384
|
+
* continua na tabela — view com `security_invoker` não aceita INSERT aqui. */
|
|
385
|
+
conversationsView: "v_conversations",
|
|
383
386
|
messages: "plg_conversation_messages",
|
|
384
387
|
channels: "plg_conversations_channels"
|
|
385
388
|
};
|
|
@@ -424,9 +427,9 @@ function tocar() {
|
|
|
424
427
|
}
|
|
425
428
|
}
|
|
426
429
|
function InboxWatcher({ label, route = "/conversations", section = "main", position = 1 }) {
|
|
427
|
-
const [naoLidas, setNaoLidas] =
|
|
428
|
-
const podeTocar =
|
|
429
|
-
|
|
430
|
+
const [naoLidas, setNaoLidas] = React11.useState(0);
|
|
431
|
+
const podeTocar = React11.useRef(false);
|
|
432
|
+
React11.useEffect(() => {
|
|
430
433
|
const liberar = () => {
|
|
431
434
|
podeTocar.current = true;
|
|
432
435
|
};
|
|
@@ -437,7 +440,7 @@ function InboxWatcher({ label, route = "/conversations", section = "main", posit
|
|
|
437
440
|
for (const ev of ["pointerdown", "keydown", "touchstart"]) window.removeEventListener(ev, liberar);
|
|
438
441
|
};
|
|
439
442
|
}, []);
|
|
440
|
-
const contar =
|
|
443
|
+
const contar = React11.useCallback(async () => {
|
|
441
444
|
const supabase = getSupabaseClientOptional();
|
|
442
445
|
if (!supabase) return;
|
|
443
446
|
const { data } = await supabase.from(T.conversations).select("id, status, last_message_direction, unread_count").eq("status", "open");
|
|
@@ -449,10 +452,10 @@ function InboxWatcher({ label, route = "/conversations", section = "main", posit
|
|
|
449
452
|
})).length;
|
|
450
453
|
setNaoLidas(n);
|
|
451
454
|
}, []);
|
|
452
|
-
|
|
455
|
+
React11.useEffect(() => {
|
|
453
456
|
void contar();
|
|
454
457
|
}, [contar]);
|
|
455
|
-
|
|
458
|
+
React11.useEffect(() => {
|
|
456
459
|
const supabase = getSupabaseClientOptional();
|
|
457
460
|
if (!supabase?.channel) return;
|
|
458
461
|
const canal = supabase.channel("conversas:entrada").on(
|
|
@@ -485,14 +488,14 @@ function slots(body) {
|
|
|
485
488
|
return found.size;
|
|
486
489
|
}
|
|
487
490
|
function TemplatesView() {
|
|
488
|
-
const [mine, setMine] =
|
|
489
|
-
const [theirs, setTheirs] =
|
|
490
|
-
const [providers, setProviders] =
|
|
491
|
-
const [busy, setBusy] =
|
|
492
|
-
const [draft, setDraft] =
|
|
491
|
+
const [mine, setMine] = React11__default.useState(null);
|
|
492
|
+
const [theirs, setTheirs] = React11__default.useState([]);
|
|
493
|
+
const [providers, setProviders] = React11__default.useState([]);
|
|
494
|
+
const [busy, setBusy] = React11__default.useState(null);
|
|
495
|
+
const [draft, setDraft] = React11__default.useState({ name: "", body: "", variables: "" });
|
|
493
496
|
const active = providers.find((p) => p.isActive);
|
|
494
497
|
const supportsTemplates = active?.capabilities?.templates !== false;
|
|
495
|
-
const load =
|
|
498
|
+
const load = React11__default.useCallback(async () => {
|
|
496
499
|
try {
|
|
497
500
|
const [m, p] = await Promise.all([campaignsApi.templates(), providersApi.list()]);
|
|
498
501
|
setMine(m);
|
|
@@ -502,7 +505,7 @@ function TemplatesView() {
|
|
|
502
505
|
setMine([]);
|
|
503
506
|
}
|
|
504
507
|
}, []);
|
|
505
|
-
|
|
508
|
+
React11__default.useEffect(() => {
|
|
506
509
|
void load();
|
|
507
510
|
}, [load]);
|
|
508
511
|
const sync = async () => {
|
|
@@ -693,8 +696,8 @@ function TemplatesView() {
|
|
|
693
696
|
var DEFAULT_CONVERSATIONS_CONFIG = {
|
|
694
697
|
contactKind: "contact"
|
|
695
698
|
};
|
|
696
|
-
var StoreContext =
|
|
697
|
-
var ConfigContext =
|
|
699
|
+
var StoreContext = React11__default.createContext(null);
|
|
700
|
+
var ConfigContext = React11__default.createContext(DEFAULT_CONVERSATIONS_CONFIG);
|
|
698
701
|
function ConversationsContextProvider({
|
|
699
702
|
store: store2,
|
|
700
703
|
config = DEFAULT_CONVERSATIONS_CONFIG,
|
|
@@ -703,12 +706,12 @@ function ConversationsContextProvider({
|
|
|
703
706
|
return /* @__PURE__ */ jsx(StoreContext.Provider, { value: store2, children: /* @__PURE__ */ jsx(ConfigContext.Provider, { value: config, children }) });
|
|
704
707
|
}
|
|
705
708
|
function useConversationsStore(selector) {
|
|
706
|
-
const store2 =
|
|
709
|
+
const store2 = React11__default.useContext(StoreContext);
|
|
707
710
|
if (!store2) throw new Error("useConversationsStore must be used within ConversationsPage");
|
|
708
711
|
return useStore(store2, selector);
|
|
709
712
|
}
|
|
710
713
|
function useConversationsConfig() {
|
|
711
|
-
return
|
|
714
|
+
return React11__default.useContext(ConfigContext);
|
|
712
715
|
}
|
|
713
716
|
var CHANNEL_ICON = {
|
|
714
717
|
sms: Phone,
|
|
@@ -725,10 +728,10 @@ var CHANNEL_ACCENT = {
|
|
|
725
728
|
webchat: { color: "#f59e0b", badge: "bg-[#f59e0b]/12 text-[#b45309] dark:text-[#fcd34d]" }
|
|
726
729
|
};
|
|
727
730
|
function useMediaQuery(query) {
|
|
728
|
-
const [matches, setMatches] =
|
|
731
|
+
const [matches, setMatches] = React11__default.useState(
|
|
729
732
|
() => typeof window !== "undefined" && window.matchMedia(query).matches
|
|
730
733
|
);
|
|
731
|
-
|
|
734
|
+
React11__default.useEffect(() => {
|
|
732
735
|
if (typeof window === "undefined") return;
|
|
733
736
|
const mql = window.matchMedia(query);
|
|
734
737
|
const onChange = () => setMatches(mql.matches);
|
|
@@ -808,12 +811,21 @@ function ConversationList({ className }) {
|
|
|
808
811
|
conversations,
|
|
809
812
|
selectedId,
|
|
810
813
|
channelFilter,
|
|
814
|
+
leadOwnerFilter,
|
|
811
815
|
search,
|
|
812
816
|
loading,
|
|
813
817
|
select,
|
|
814
818
|
setChannelFilter,
|
|
819
|
+
setLeadOwnerFilter,
|
|
815
820
|
setSearch
|
|
816
821
|
} = useConversationsStore((s) => s);
|
|
822
|
+
const donos = React11__default.useMemo(() => {
|
|
823
|
+
const vistos = /* @__PURE__ */ new Map();
|
|
824
|
+
for (const c of conversations) {
|
|
825
|
+
if (c.leadOwnerId) vistos.set(c.leadOwnerId, c.leadOwnerName ?? c.leadOwnerId);
|
|
826
|
+
}
|
|
827
|
+
return [...vistos].map(([value, label]) => ({ value, label }));
|
|
828
|
+
}, [conversations]);
|
|
817
829
|
return /* @__PURE__ */ jsxs("aside", { className: cn("w-full shrink-0 flex-col border-r border-border bg-card lg:w-[320px]", className), children: [
|
|
818
830
|
/* @__PURE__ */ jsxs("div", { className: "border-b border-border px-3 py-3", children: [
|
|
819
831
|
/* @__PURE__ */ jsxs("div", { className: "relative", children: [
|
|
@@ -839,7 +851,20 @@ function ConversationList({ className }) {
|
|
|
839
851
|
children: t(`conversations.filter.${id}`)
|
|
840
852
|
},
|
|
841
853
|
id
|
|
842
|
-
)) })
|
|
854
|
+
)) }),
|
|
855
|
+
donos.length > 0 && /* @__PURE__ */ jsx("div", { className: "mt-2", children: /* @__PURE__ */ jsx(
|
|
856
|
+
FilterChip,
|
|
857
|
+
{
|
|
858
|
+
label: t("conversations.contact.leadOwner"),
|
|
859
|
+
icon: User,
|
|
860
|
+
allLabel: t("conversations.filter.anyOwner"),
|
|
861
|
+
options: [{ value: "none", label: t("conversations.filter.noOwner") }, ...donos],
|
|
862
|
+
value: leadOwnerFilter,
|
|
863
|
+
onChange: (v) => {
|
|
864
|
+
void setLeadOwnerFilter(v);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
) })
|
|
843
868
|
] }),
|
|
844
869
|
/* @__PURE__ */ jsxs("div", { className: "min-h-0 flex-1 overflow-y-auto", children: [
|
|
845
870
|
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: [
|
|
@@ -888,7 +913,11 @@ function ConversationList({ className }) {
|
|
|
888
913
|
] }),
|
|
889
914
|
/* @__PURE__ */ jsxs("div", { className: "mt-1 flex items-center gap-2", children: [
|
|
890
915
|
/* @__PURE__ */ jsx(ChannelBadge, { channel: c.channel }),
|
|
891
|
-
c.status !== "open" && /* @__PURE__ */ jsx("span", { className: "text-[10px] uppercase tracking-wide text-muted-foreground/70", children: t(`conversations.status.${c.status}`) })
|
|
916
|
+
c.status !== "open" && /* @__PURE__ */ jsx("span", { className: "text-[10px] uppercase tracking-wide text-muted-foreground/70", children: t(`conversations.status.${c.status}`) }),
|
|
917
|
+
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: [
|
|
918
|
+
/* @__PURE__ */ jsx(User, { className: "h-2.5 w-2.5" }),
|
|
919
|
+
c.leadOwnerName.split(" ")[0]
|
|
920
|
+
] })
|
|
892
921
|
] }),
|
|
893
922
|
/* @__PURE__ */ jsxs("div", { className: "mt-1 flex items-center justify-between gap-2", children: [
|
|
894
923
|
/* @__PURE__ */ jsx("span", { className: cn("truncate text-xs", unread ? "text-foreground" : "text-muted-foreground"), children: c.lastMessagePreview }),
|
|
@@ -904,9 +933,9 @@ function ConversationList({ className }) {
|
|
|
904
933
|
] });
|
|
905
934
|
}
|
|
906
935
|
function useSendWindow(conversationId) {
|
|
907
|
-
const [row, setRow] =
|
|
908
|
-
const [providers, setProviders] =
|
|
909
|
-
|
|
936
|
+
const [row, setRow] = React11__default.useState(null);
|
|
937
|
+
const [providers, setProviders] = React11__default.useState(null);
|
|
938
|
+
React11__default.useEffect(() => {
|
|
910
939
|
let alive = true;
|
|
911
940
|
providersApi.list().then((p) => {
|
|
912
941
|
if (alive) setProviders(p);
|
|
@@ -917,7 +946,7 @@ function useSendWindow(conversationId) {
|
|
|
917
946
|
alive = false;
|
|
918
947
|
};
|
|
919
948
|
}, []);
|
|
920
|
-
|
|
949
|
+
React11__default.useEffect(() => {
|
|
921
950
|
let alive = true;
|
|
922
951
|
if (!conversationId) {
|
|
923
952
|
setRow(null);
|
|
@@ -938,7 +967,7 @@ function useSendWindow(conversationId) {
|
|
|
938
967
|
};
|
|
939
968
|
}, [conversationId]);
|
|
940
969
|
const provider = (providers ?? []).find((p) => p.isActive);
|
|
941
|
-
const mode =
|
|
970
|
+
const mode = React11__default.useMemo(() => {
|
|
942
971
|
if (!provider) return { kind: "blocked", reason: "no_provider" };
|
|
943
972
|
if (!provider.capabilities?.requires_template) {
|
|
944
973
|
return { kind: "free", until: null, minutesLeft: null };
|
|
@@ -1013,10 +1042,10 @@ function DeliveryMark({ m }) {
|
|
|
1013
1042
|
] });
|
|
1014
1043
|
}
|
|
1015
1044
|
function MessageMenu({ m }) {
|
|
1016
|
-
const [open, setOpen] =
|
|
1017
|
-
const [detail, setDetail] =
|
|
1018
|
-
const [loading, setLoading] =
|
|
1019
|
-
const [err, setErr] =
|
|
1045
|
+
const [open, setOpen] = React11__default.useState(false);
|
|
1046
|
+
const [detail, setDetail] = React11__default.useState(null);
|
|
1047
|
+
const [loading, setLoading] = React11__default.useState(false);
|
|
1048
|
+
const [err, setErr] = React11__default.useState(null);
|
|
1020
1049
|
async function load() {
|
|
1021
1050
|
setOpen(true);
|
|
1022
1051
|
if (!m.providerMessageId) return;
|
|
@@ -1107,14 +1136,14 @@ function buildRows(messages) {
|
|
|
1107
1136
|
function MessageThread({ selected, onTogglePanel, panelOpen, onBack, className }) {
|
|
1108
1137
|
const t = useTranslation();
|
|
1109
1138
|
const { messages, sending, send, setStatus, refreshMessages } = useConversationsStore((s) => s);
|
|
1110
|
-
const [draft, setDraft] =
|
|
1139
|
+
const [draft, setDraft] = React11__default.useState("");
|
|
1111
1140
|
const win = useSendWindow(selected.id);
|
|
1112
1141
|
const canType = win.mode.kind === "free";
|
|
1113
|
-
const threadRef =
|
|
1114
|
-
|
|
1142
|
+
const threadRef = React11__default.useRef(null);
|
|
1143
|
+
React11__default.useEffect(() => {
|
|
1115
1144
|
threadRef.current?.scrollTo({ top: threadRef.current.scrollHeight, behavior: "smooth" });
|
|
1116
1145
|
}, [messages.length, selected.id]);
|
|
1117
|
-
|
|
1146
|
+
React11__default.useEffect(() => {
|
|
1118
1147
|
if (!selected.id) return;
|
|
1119
1148
|
let timer = null;
|
|
1120
1149
|
const start = () => {
|
|
@@ -1143,7 +1172,7 @@ function MessageThread({ selected, onTogglePanel, panelOpen, onBack, className }
|
|
|
1143
1172
|
await send(body);
|
|
1144
1173
|
}
|
|
1145
1174
|
const accent = CHANNEL_ACCENT[selected.channel];
|
|
1146
|
-
const rows =
|
|
1175
|
+
const rows = React11__default.useMemo(() => buildRows(messages), [messages]);
|
|
1147
1176
|
return /* @__PURE__ */ jsxs("section", { className: cn("flex min-w-0 flex-1 flex-col bg-muted/20", className), children: [
|
|
1148
1177
|
/* @__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: [
|
|
1149
1178
|
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-2 md:gap-3", children: [
|
|
@@ -1255,8 +1284,8 @@ var TEMP = {
|
|
|
1255
1284
|
cold: { icon: Snowflake, cls: "bg-info-soft text-info-soft-foreground", label: "Frio" }
|
|
1256
1285
|
};
|
|
1257
1286
|
function LeadCard({ personId, handle }) {
|
|
1258
|
-
const [lead, setLead] =
|
|
1259
|
-
|
|
1287
|
+
const [lead, setLead] = React11__default.useState(void 0);
|
|
1288
|
+
React11__default.useEffect(() => {
|
|
1260
1289
|
let alive = true;
|
|
1261
1290
|
const sb = getSupabaseClientOptional();
|
|
1262
1291
|
const tenantId = getActiveTenantId();
|
|
@@ -1383,6 +1412,10 @@ function ContactPanel({ contact, onClose, className }) {
|
|
|
1383
1412
|
/* @__PURE__ */ jsx("dt", { className: "text-muted-foreground", children: t("conversations.contact.assignedTo") }),
|
|
1384
1413
|
/* @__PURE__ */ jsx("dd", { className: "text-foreground", children: contact.assignedTo })
|
|
1385
1414
|
] }),
|
|
1415
|
+
contact.leadOwnerName && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
|
|
1416
|
+
/* @__PURE__ */ jsx("dt", { className: "text-muted-foreground", children: t("conversations.contact.leadOwner") }),
|
|
1417
|
+
/* @__PURE__ */ jsx("dd", { className: "font-medium text-foreground", children: contact.leadOwnerName })
|
|
1418
|
+
] }),
|
|
1386
1419
|
contact.location && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
|
|
1387
1420
|
/* @__PURE__ */ jsxs("dt", { className: "flex items-center gap-1 text-muted-foreground", children: [
|
|
1388
1421
|
/* @__PURE__ */ jsx(MapPin, { className: "h-3 w-3" }),
|
|
@@ -1419,14 +1452,14 @@ function NewConversationPanel({
|
|
|
1419
1452
|
const create = useConversationsStore((s) => s.create);
|
|
1420
1453
|
const config = useConversationsConfig();
|
|
1421
1454
|
const guardConversations = useLimitGuard("conversations_month");
|
|
1422
|
-
const [channel, setChannel] =
|
|
1423
|
-
const [contact, setContact] =
|
|
1424
|
-
const [typedHandle, setTypedHandle] =
|
|
1425
|
-
const [creatingContact, setCreatingContact] =
|
|
1426
|
-
const [firstMessage, setFirstMessage] =
|
|
1427
|
-
const [submitting, setSubmitting] =
|
|
1428
|
-
const [pickerKey, setPickerKey] =
|
|
1429
|
-
|
|
1455
|
+
const [channel, setChannel] = React11__default.useState("whatsapp");
|
|
1456
|
+
const [contact, setContact] = React11__default.useState(null);
|
|
1457
|
+
const [typedHandle, setTypedHandle] = React11__default.useState("");
|
|
1458
|
+
const [creatingContact, setCreatingContact] = React11__default.useState(false);
|
|
1459
|
+
const [firstMessage, setFirstMessage] = React11__default.useState("");
|
|
1460
|
+
const [submitting, setSubmitting] = React11__default.useState(false);
|
|
1461
|
+
const [pickerKey, setPickerKey] = React11__default.useState(0);
|
|
1462
|
+
React11__default.useEffect(() => {
|
|
1430
1463
|
if (open) {
|
|
1431
1464
|
setChannel("whatsapp");
|
|
1432
1465
|
setContact(null);
|
|
@@ -1544,10 +1577,10 @@ function InboxView() {
|
|
|
1544
1577
|
const t = useTranslation();
|
|
1545
1578
|
const { conversations, selectedId, deselect } = useConversationsStore((s) => s);
|
|
1546
1579
|
const isWidePanel = useMediaQuery("(min-width: 1280px)");
|
|
1547
|
-
const [panelOpen, setPanelOpen] =
|
|
1548
|
-
const [newOpen, setNewOpen] =
|
|
1580
|
+
const [panelOpen, setPanelOpen] = React11__default.useState(false);
|
|
1581
|
+
const [newOpen, setNewOpen] = React11__default.useState(false);
|
|
1549
1582
|
const selected = conversations.find((c) => c.id === selectedId) ?? null;
|
|
1550
|
-
|
|
1583
|
+
React11__default.useEffect(() => {
|
|
1551
1584
|
setPanelOpen(isWidePanel);
|
|
1552
1585
|
}, [isWidePanel]);
|
|
1553
1586
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
@@ -1620,7 +1653,7 @@ function requestedContact() {
|
|
|
1620
1653
|
}
|
|
1621
1654
|
}
|
|
1622
1655
|
function ConversationsPage({ store: store2, config }) {
|
|
1623
|
-
|
|
1656
|
+
React11__default.useEffect(() => {
|
|
1624
1657
|
const wanted = requestedContact();
|
|
1625
1658
|
if (!wanted) {
|
|
1626
1659
|
void store2.getState().load();
|
|
@@ -1923,6 +1956,10 @@ function mapConversation(r) {
|
|
|
1923
1956
|
unreadCount: Number(r.unread_count ?? 0),
|
|
1924
1957
|
status: r.status ?? "open",
|
|
1925
1958
|
assignedTo: r.assigned_to ?? void 0,
|
|
1959
|
+
// Presentes só quando a leitura veio da view. Na escrita (que volta da
|
|
1960
|
+
// tabela) ficam nulos, e a lista recarrega logo a seguir.
|
|
1961
|
+
leadOwnerId: r.lead_owner_id ?? void 0,
|
|
1962
|
+
leadOwnerName: r.lead_owner_name ?? void 0,
|
|
1926
1963
|
accent: r.accent ?? "#6366f1",
|
|
1927
1964
|
tags: r.tags ?? [],
|
|
1928
1965
|
location: r.location ?? void 0,
|
|
@@ -1965,7 +2002,7 @@ function createSupabaseConversationsProvider(config) {
|
|
|
1965
2002
|
}
|
|
1966
2003
|
return {
|
|
1967
2004
|
async listConversations(query) {
|
|
1968
|
-
let q = client4().from(T.
|
|
2005
|
+
let q = client4().from(T.conversationsView).select("*");
|
|
1969
2006
|
const tenantId = resolveTenantId();
|
|
1970
2007
|
if (tenantId) q = q.eq("tenant_id", tenantId);
|
|
1971
2008
|
if (query?.channel && query.channel !== "all") {
|
|
@@ -1974,6 +2011,11 @@ function createSupabaseConversationsProvider(config) {
|
|
|
1974
2011
|
if (query?.status && query.status !== "all") {
|
|
1975
2012
|
q = q.eq("status", query.status);
|
|
1976
2013
|
}
|
|
2014
|
+
if (query?.leadOwnerId === "none") {
|
|
2015
|
+
q = q.is("lead_owner_id", null);
|
|
2016
|
+
} else if (query?.leadOwnerId) {
|
|
2017
|
+
q = q.eq("lead_owner_id", query.leadOwnerId);
|
|
2018
|
+
}
|
|
1977
2019
|
if (query?.search) {
|
|
1978
2020
|
const term = `%${query.search}%`;
|
|
1979
2021
|
q = q.or(
|
|
@@ -2142,6 +2184,7 @@ function createConversationsStore(provider) {
|
|
|
2142
2184
|
messages: [],
|
|
2143
2185
|
selectedId: null,
|
|
2144
2186
|
channelFilter: "all",
|
|
2187
|
+
leadOwnerFilter: void 0,
|
|
2145
2188
|
search: "",
|
|
2146
2189
|
loading: false,
|
|
2147
2190
|
sending: false,
|
|
@@ -2149,6 +2192,7 @@ function createConversationsStore(provider) {
|
|
|
2149
2192
|
set({ loading: true });
|
|
2150
2193
|
const conversations = await provider.listConversations({
|
|
2151
2194
|
channel: get().channelFilter,
|
|
2195
|
+
leadOwnerId: get().leadOwnerFilter,
|
|
2152
2196
|
search: get().search || void 0
|
|
2153
2197
|
});
|
|
2154
2198
|
const selectedId = get().selectedId ?? conversations[0]?.id ?? null;
|
|
@@ -2187,6 +2231,10 @@ function createConversationsStore(provider) {
|
|
|
2187
2231
|
deselect() {
|
|
2188
2232
|
set({ selectedId: null, messages: [] });
|
|
2189
2233
|
},
|
|
2234
|
+
async setLeadOwnerFilter(ownerId) {
|
|
2235
|
+
set({ leadOwnerFilter: ownerId });
|
|
2236
|
+
await get().load();
|
|
2237
|
+
},
|
|
2190
2238
|
async setChannelFilter(channel) {
|
|
2191
2239
|
set({ channelFilter: channel });
|
|
2192
2240
|
await get().load();
|
|
@@ -2309,6 +2357,9 @@ var en = {
|
|
|
2309
2357
|
"conversations.contact.closeDetails": "Close details",
|
|
2310
2358
|
"conversations.contact.channel": "Channel",
|
|
2311
2359
|
"conversations.contact.status": "Status",
|
|
2360
|
+
"conversations.filter.anyOwner": "Any owner",
|
|
2361
|
+
"conversations.filter.noOwner": "No owner",
|
|
2362
|
+
"conversations.contact.leadOwner": "Lead owner",
|
|
2312
2363
|
"conversations.contact.assignedTo": "Assigned to",
|
|
2313
2364
|
"conversations.contact.location": "Location",
|
|
2314
2365
|
"conversations.contact.tags": "Tags",
|
|
@@ -2401,6 +2452,9 @@ var ptBR = {
|
|
|
2401
2452
|
"conversations.contact.closeDetails": "Fechar detalhes",
|
|
2402
2453
|
"conversations.contact.channel": "Canal",
|
|
2403
2454
|
"conversations.contact.status": "Status",
|
|
2455
|
+
"conversations.filter.anyOwner": "Qualquer dono",
|
|
2456
|
+
"conversations.filter.noOwner": "Sem dono",
|
|
2457
|
+
"conversations.contact.leadOwner": "Dono do lead",
|
|
2404
2458
|
"conversations.contact.assignedTo": "Respons\xE1vel",
|
|
2405
2459
|
"conversations.contact.location": "Localiza\xE7\xE3o",
|
|
2406
2460
|
"conversations.contact.tags": "Etiquetas",
|
|
@@ -2666,22 +2720,22 @@ function NumberRow({ n }) {
|
|
|
2666
2720
|
] });
|
|
2667
2721
|
}
|
|
2668
2722
|
function WhatsAppProviders() {
|
|
2669
|
-
const [rows, setRows] =
|
|
2670
|
-
const [draft, setDraft] =
|
|
2671
|
-
const [busy, setBusy] =
|
|
2672
|
-
const [numbers, setNumbers] =
|
|
2673
|
-
const [qr, setQr] =
|
|
2674
|
-
const load =
|
|
2723
|
+
const [rows, setRows] = React11__default.useState(null);
|
|
2724
|
+
const [draft, setDraft] = React11__default.useState({});
|
|
2725
|
+
const [busy, setBusy] = React11__default.useState(null);
|
|
2726
|
+
const [numbers, setNumbers] = React11__default.useState({});
|
|
2727
|
+
const [qr, setQr] = React11__default.useState(null);
|
|
2728
|
+
const load = React11__default.useCallback(async () => {
|
|
2675
2729
|
try {
|
|
2676
2730
|
setRows(await providersApi.list());
|
|
2677
2731
|
} catch {
|
|
2678
2732
|
setRows([]);
|
|
2679
2733
|
}
|
|
2680
2734
|
}, []);
|
|
2681
|
-
|
|
2735
|
+
React11__default.useEffect(() => {
|
|
2682
2736
|
void load();
|
|
2683
2737
|
}, [load]);
|
|
2684
|
-
const byId =
|
|
2738
|
+
const byId = React11__default.useMemo(
|
|
2685
2739
|
() => Object.fromEntries((rows ?? []).map((r) => [r.provider, r])),
|
|
2686
2740
|
[rows]
|
|
2687
2741
|
);
|
|
@@ -2916,9 +2970,9 @@ function ChannelRow({ account }) {
|
|
|
2916
2970
|
}
|
|
2917
2971
|
function ConversationsGeneralSettings() {
|
|
2918
2972
|
const t = useTranslation();
|
|
2919
|
-
const [channels, setChannels] =
|
|
2920
|
-
const [failed, setFailed] =
|
|
2921
|
-
|
|
2973
|
+
const [channels, setChannels] = React11.useState(null);
|
|
2974
|
+
const [failed, setFailed] = React11.useState(false);
|
|
2975
|
+
React11.useEffect(() => {
|
|
2922
2976
|
let cancelled = false;
|
|
2923
2977
|
listMessagingChannels().then((rows) => {
|
|
2924
2978
|
if (!cancelled) setChannels(rows);
|
|
@@ -4999,6 +5053,68 @@ UPDATE public.plg_conversations
|
|
|
4999
5053
|
AND status = 'open'
|
|
5000
5054
|
AND coalesce(unread_count, 0) = 0;
|
|
5001
5055
|
`;
|
|
5056
|
+
var MIGRATION_007_A_CONVERSA_DIZ_DE_QUEM_E_O_LEAD = `-- ---------------------------------------------------------------------------
|
|
5057
|
+
-- 007_a_conversa_diz_de_quem_e_o_lead.sql
|
|
5058
|
+
--
|
|
5059
|
+
-- \u2500\u2500 duas coisas chamadas "respons\xE1vel" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
5060
|
+
--
|
|
5061
|
+
-- A caixa j\xE1 mostra um "Respons\xE1vel", e ele \xE9 \`plg_conversations.assigned_to\`:
|
|
5062
|
+
-- quem est\xE1 ATENDENDO aquela conversa. N\xE3o \xE9 o dono do lead.
|
|
5063
|
+
--
|
|
5064
|
+
-- Os dois convivem e precisam conviver: a Carolina \xE9 dona do lead, e quem
|
|
5065
|
+
-- responde a mensagem que chegou \xE0s 2h da manh\xE3 pode ser outra pessoa. Colapsar
|
|
5066
|
+
-- num s\xF3 campo faria uma das duas perguntas ficar sem resposta \u2014 e a que
|
|
5067
|
+
-- desapareceria \xE9 a do funil, que \xE9 a que decide quem faz o follow-up.
|
|
5068
|
+
--
|
|
5069
|
+
-- Esta view acrescenta a SEGUNDA, com nome pr\xF3prio (\`lead_owner_*\`), sem tocar
|
|
5070
|
+
-- na primeira.
|
|
5071
|
+
--
|
|
5072
|
+
-- \u2500\u2500 por que view, e n\xE3o uma coluna na conversa \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
5073
|
+
--
|
|
5074
|
+
-- O dono do lead muda \u2014 algu\xE9m clica "Assumir" no funil e a carteira troca de
|
|
5075
|
+
-- m\xE3os. Uma coluna copiada aqui seria uma segunda verdade que envelhece em
|
|
5076
|
+
-- sil\xEAncio: a conversa diria Carolina depois de o lead j\xE1 ser do Julio, e
|
|
5077
|
+
-- ningu\xE9m saberia dizer qual das duas telas est\xE1 certa.
|
|
5078
|
+
--
|
|
5079
|
+
-- \xC9 exatamente o problema que a 047 acabou de resolver do outro lado, onde o
|
|
5080
|
+
-- dono estava escrito em dois lugares que n\xE3o se falavam. Derivar \xE9 o que
|
|
5081
|
+
-- impede a repeti\xE7\xE3o.
|
|
5082
|
+
--
|
|
5083
|
+
-- \u2500\u2500 e por que \`v_leads\` e n\xE3o \`plg_crm_deals\` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
5084
|
+
--
|
|
5085
|
+
-- \`v_leads.assigned_to_id\` j\xE1 resolve a preced\xEAncia entre o que a PESSOA
|
|
5086
|
+
-- declara (\`metadata.assignedToId\`) e o que o NEG\xD3CIO diz (047). Ir direto ao
|
|
5087
|
+
-- neg\xF3cio aqui reimplementaria essa regra, e as duas divergiriam no dia em que
|
|
5088
|
+
-- uma mudasse.
|
|
5089
|
+
--
|
|
5090
|
+
-- \u2500\u2500 o CRM \xE9 opcional \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
5091
|
+
--
|
|
5092
|
+
-- \`plugin-conversations\` roda em casa sem funil nenhum. \`v_leads\` \xE9 da espinha
|
|
5093
|
+
-- (n\xE3o do CRM), ent\xE3o a depend\xEAncia \xE9 leg\xEDtima \u2014 mas a jun\xE7\xE3o \xE9 LEFT: uma
|
|
5094
|
+
-- conversa de quem n\xE3o \xE9 lead continua aparecendo, com o dono nulo.
|
|
5095
|
+
-- ---------------------------------------------------------------------------
|
|
5096
|
+
|
|
5097
|
+
CREATE OR REPLACE VIEW public.v_conversations WITH (security_invoker = true) AS
|
|
5098
|
+
SELECT c.*,
|
|
5099
|
+
l.assigned_to_id AS lead_owner_id,
|
|
5100
|
+
p.name AS lead_owner_name
|
|
5101
|
+
FROM public.plg_conversations c
|
|
5102
|
+
-- Pelo contato: \xE9 \`contact_person_id\` que liga a conversa a uma pessoa da
|
|
5103
|
+
-- base. Nulo enquanto ningu\xE9m casou o telefone com algu\xE9m, e a\xED n\xE3o h\xE1
|
|
5104
|
+
-- lead a quem perguntar o dono.
|
|
5105
|
+
LEFT JOIN public.v_leads l
|
|
5106
|
+
ON l.id = c.contact_person_id
|
|
5107
|
+
-- O NOME do dono, que \xE9 o que a tela mostra. O id sozinho obrigaria cada
|
|
5108
|
+
-- tela a resolver o mesmo de-para \u2014 foi o que fez o filtro de leads exibir
|
|
5109
|
+
-- UUID em produ\xE7\xE3o por um dia.
|
|
5110
|
+
LEFT JOIN public.v_team_members m
|
|
5111
|
+
ON m.user_id::text = l.assigned_to_id AND m.tenant_id = c.tenant_id
|
|
5112
|
+
LEFT JOIN public.people p
|
|
5113
|
+
ON p.id = m.person_id;
|
|
5114
|
+
|
|
5115
|
+
COMMENT ON VIEW public.v_conversations IS
|
|
5116
|
+
'A conversa com o DONO DO LEAD junto \u2014 diferente de \`assigned_to\`, que \xE9 quem atende a conversa. Derivado de \`v_leads\` para n\xE3o virar uma segunda verdade que envelhece (007).';
|
|
5117
|
+
`;
|
|
5002
5118
|
var MIGRATIONS = [
|
|
5003
5119
|
{ id: "000_baseline", sql: MIGRATION_000_BASELINE },
|
|
5004
5120
|
{ id: "001_a_caixa_sabe_quem_falou_por_ultimo", sql: MIGRATION_001_A_CAIXA_SABE_QUEM_FALOU_POR_ULTIMO },
|
|
@@ -5006,7 +5122,8 @@ var MIGRATIONS = [
|
|
|
5006
5122
|
{ id: "003_a_campanha_sai_no_ritmo_que_o_numero_aguenta", sql: MIGRATION_003_A_CAMPANHA_SAI_NO_RITMO_QUE_O_NUMERO_AGUENTA },
|
|
5007
5123
|
{ id: "004_a_tela_sabe_se_pode_falar_antes_de_deixar_escrever", sql: MIGRATION_004_A_TELA_SABE_SE_PODE_FALAR_ANTES_DE_DEIXAR_ESCREVER },
|
|
5008
5124
|
{ id: "005_a_conversa_conta_ao_funil_que_houve_contato", sql: MIGRATION_005_A_CONVERSA_CONTA_AO_FUNIL_QUE_HOUVE_CONTATO },
|
|
5009
|
-
{ id: "006_a_caixa_avisa_que_chegou", sql: MIGRATION_006_A_CAIXA_AVISA_QUE_CHEGOU }
|
|
5125
|
+
{ id: "006_a_caixa_avisa_que_chegou", sql: MIGRATION_006_A_CAIXA_AVISA_QUE_CHEGOU },
|
|
5126
|
+
{ id: "007_a_conversa_diz_de_quem_e_o_lead", sql: MIGRATION_007_A_CONVERSA_DIZ_DE_QUEM_E_O_LEAD }
|
|
5010
5127
|
];
|
|
5011
5128
|
|
|
5012
5129
|
// src/index.ts
|
|
@@ -5040,11 +5157,11 @@ function createConversationsPlugin(options) {
|
|
|
5040
5157
|
contactEntityDef: options?.contactEntityDef
|
|
5041
5158
|
};
|
|
5042
5159
|
const dashboardWidgets = createConversationsDashboardWidgets({ store: store2, config });
|
|
5043
|
-
const PageComponent = () =>
|
|
5160
|
+
const PageComponent = () => React11__default.createElement(ConversationsPage, { store: store2, config });
|
|
5044
5161
|
PageComponent.displayName = "ConversationsPage";
|
|
5045
|
-
const CampaignsPageComponent = () =>
|
|
5162
|
+
const CampaignsPageComponent = () => React11__default.createElement(CampaignsView);
|
|
5046
5163
|
CampaignsPageComponent.displayName = "ConversationsCampaignsPage";
|
|
5047
|
-
const TemplatesPageComponent = () =>
|
|
5164
|
+
const TemplatesPageComponent = () => React11__default.createElement(TemplatesView);
|
|
5048
5165
|
TemplatesPageComponent.displayName = "ConversationsTemplatesPage";
|
|
5049
5166
|
return {
|
|
5050
5167
|
id: "conversations",
|
|
@@ -5120,7 +5237,7 @@ function createConversationsPlugin(options) {
|
|
|
5120
5237
|
// quando ele serve para alguma coisa.
|
|
5121
5238
|
zone: "shell.topbar.end",
|
|
5122
5239
|
component: (() => {
|
|
5123
|
-
const W = () =>
|
|
5240
|
+
const W = () => React11__default.createElement(InboxWatcher, {
|
|
5124
5241
|
label: options?.navLabel ?? "Conversations",
|
|
5125
5242
|
section: options?.navSection ?? "main",
|
|
5126
5243
|
position: options?.navPosition ?? 1
|