@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.
@@ -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;AAyCD,wBAAgB,mCAAmC,CACjD,MAAM,CAAC,EAAE,2BAA2B,GACnC,qBAAqB,CA4QvB"}
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"}
@@ -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;;;;CAIJ,CAAA"}
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 React10 from 'react';
2
- import React10__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, Inbox, ChevronLeft, Archive, PanelRight, X, User, MapPin, Tag, StickyNote, Link2, MoreVertical, CheckCheck, Snowflake, Sun, Flame, Building2 } from 'lucide-react';
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] = React10__default.useState(null);
215
- const [templates, setTemplates] = React10__default.useState([]);
216
- const [providers, setProviders] = React10__default.useState([]);
217
- const [running, setRunning] = React10__default.useState(null);
218
- const stop = React10__default.useRef(false);
219
- const load = React10__default.useCallback(async () => {
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
- React10__default.useEffect(() => {
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] = React10.useState(0);
428
- const podeTocar = React10.useRef(false);
429
- React10.useEffect(() => {
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 = React10.useCallback(async () => {
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
- React10.useEffect(() => {
455
+ React11.useEffect(() => {
453
456
  void contar();
454
457
  }, [contar]);
455
- React10.useEffect(() => {
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] = React10__default.useState(null);
489
- const [theirs, setTheirs] = React10__default.useState([]);
490
- const [providers, setProviders] = React10__default.useState([]);
491
- const [busy, setBusy] = React10__default.useState(null);
492
- const [draft, setDraft] = React10__default.useState({ name: "", body: "", variables: "" });
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 = React10__default.useCallback(async () => {
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
- React10__default.useEffect(() => {
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 = React10__default.createContext(null);
697
- var ConfigContext = React10__default.createContext(DEFAULT_CONVERSATIONS_CONFIG);
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 = React10__default.useContext(StoreContext);
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 React10__default.useContext(ConfigContext);
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] = React10__default.useState(
731
+ const [matches, setMatches] = React11__default.useState(
729
732
  () => typeof window !== "undefined" && window.matchMedia(query).matches
730
733
  );
731
- React10__default.useEffect(() => {
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] = React10__default.useState(null);
908
- const [providers, setProviders] = React10__default.useState(null);
909
- React10__default.useEffect(() => {
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
- React10__default.useEffect(() => {
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 = React10__default.useMemo(() => {
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] = React10__default.useState(false);
1017
- const [detail, setDetail] = React10__default.useState(null);
1018
- const [loading, setLoading] = React10__default.useState(false);
1019
- const [err, setErr] = React10__default.useState(null);
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] = React10__default.useState("");
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 = React10__default.useRef(null);
1114
- React10__default.useEffect(() => {
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
- React10__default.useEffect(() => {
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 = React10__default.useMemo(() => buildRows(messages), [messages]);
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] = React10__default.useState(void 0);
1259
- React10__default.useEffect(() => {
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] = React10__default.useState("whatsapp");
1423
- const [contact, setContact] = React10__default.useState(null);
1424
- const [typedHandle, setTypedHandle] = React10__default.useState("");
1425
- const [creatingContact, setCreatingContact] = React10__default.useState(false);
1426
- const [firstMessage, setFirstMessage] = React10__default.useState("");
1427
- const [submitting, setSubmitting] = React10__default.useState(false);
1428
- const [pickerKey, setPickerKey] = React10__default.useState(0);
1429
- React10__default.useEffect(() => {
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] = React10__default.useState(false);
1548
- const [newOpen, setNewOpen] = React10__default.useState(false);
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
- React10__default.useEffect(() => {
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
- React10__default.useEffect(() => {
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.conversations).select("*");
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] = React10__default.useState(null);
2670
- const [draft, setDraft] = React10__default.useState({});
2671
- const [busy, setBusy] = React10__default.useState(null);
2672
- const [numbers, setNumbers] = React10__default.useState({});
2673
- const [qr, setQr] = React10__default.useState(null);
2674
- const load = React10__default.useCallback(async () => {
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
- React10__default.useEffect(() => {
2735
+ React11__default.useEffect(() => {
2682
2736
  void load();
2683
2737
  }, [load]);
2684
- const byId = React10__default.useMemo(
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] = React10.useState(null);
2920
- const [failed, setFailed] = React10.useState(false);
2921
- React10.useEffect(() => {
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 = () => React10__default.createElement(ConversationsPage, { store: store2, config });
5160
+ const PageComponent = () => React11__default.createElement(ConversationsPage, { store: store2, config });
5044
5161
  PageComponent.displayName = "ConversationsPage";
5045
- const CampaignsPageComponent = () => React10__default.createElement(CampaignsView);
5162
+ const CampaignsPageComponent = () => React11__default.createElement(CampaignsView);
5046
5163
  CampaignsPageComponent.displayName = "ConversationsCampaignsPage";
5047
- const TemplatesPageComponent = () => React10__default.createElement(TemplatesView);
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 = () => React10__default.createElement(InboxWatcher, {
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