@fayz-ai/plugin-conversations 0.8.0 → 0.8.3

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.
Files changed (60) hide show
  1. package/dist/ConversationsContext.d.ts +18 -1
  2. package/dist/ConversationsContext.d.ts.map +1 -1
  3. package/dist/ConversationsPage.d.ts +3 -1
  4. package/dist/ConversationsPage.d.ts.map +1 -1
  5. package/dist/data/accents.d.ts +3 -0
  6. package/dist/data/accents.d.ts.map +1 -0
  7. package/dist/data/mock.d.ts +7 -1
  8. package/dist/data/mock.d.ts.map +1 -1
  9. package/dist/data/mock.test.d.ts +2 -0
  10. package/dist/data/mock.test.d.ts.map +1 -0
  11. package/dist/data/supabase.d.ts.map +1 -1
  12. package/dist/data/tables.d.ts +5 -0
  13. package/dist/data/tables.d.ts.map +1 -0
  14. package/dist/data/types.d.ts +2 -1
  15. package/dist/data/types.d.ts.map +1 -1
  16. package/dist/index.cjs +664 -70
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.ts +16 -2
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +667 -73
  21. package/dist/index.js.map +1 -1
  22. package/dist/locales/en.d.ts.map +1 -1
  23. package/dist/locales/index.d.ts.map +1 -1
  24. package/dist/locales/pt-BR.d.ts +2 -0
  25. package/dist/locales/pt-BR.d.ts.map +1 -0
  26. package/dist/migrations/index.d.ts +7 -0
  27. package/dist/migrations/index.d.ts.map +1 -0
  28. package/dist/store.d.ts +2 -1
  29. package/dist/store.d.ts.map +1 -1
  30. package/dist/types.d.ts +18 -0
  31. package/dist/types.d.ts.map +1 -1
  32. package/dist/views/ContactPanel.d.ts.map +1 -1
  33. package/dist/views/ConversationList.d.ts.map +1 -1
  34. package/dist/views/InboxView.d.ts.map +1 -1
  35. package/dist/views/MessageThread.d.ts.map +1 -1
  36. package/dist/views/NewConversationModal.d.ts +6 -0
  37. package/dist/views/NewConversationModal.d.ts.map +1 -0
  38. package/package.json +7 -5
  39. package/src/ConversationsContext.tsx +31 -1
  40. package/src/ConversationsPage.tsx +6 -3
  41. package/src/data/accents.ts +12 -0
  42. package/src/data/mock.test.ts +90 -0
  43. package/src/data/mock.ts +131 -12
  44. package/src/data/supabase.ts +69 -7
  45. package/src/data/tables.ts +7 -0
  46. package/src/data/types.ts +2 -0
  47. package/src/index.ts +69 -11
  48. package/src/locales/en.ts +64 -0
  49. package/src/locales/index.ts +2 -0
  50. package/src/locales/pt-BR.ts +68 -0
  51. package/src/migrations/001_conversations.sql +74 -0
  52. package/src/migrations/002_contact_person.sql +22 -0
  53. package/src/migrations/index.ts +108 -0
  54. package/src/store.ts +44 -1
  55. package/src/types.ts +19 -0
  56. package/src/views/ContactPanel.tsx +14 -12
  57. package/src/views/ConversationList.tsx +48 -21
  58. package/src/views/InboxView.tsx +3 -1
  59. package/src/views/MessageThread.tsx +14 -16
  60. package/src/views/NewConversationModal.tsx +204 -0
package/dist/index.cjs CHANGED
@@ -6,6 +6,7 @@ var zustand = require('zustand');
6
6
  var jsxRuntime = require('react/jsx-runtime');
7
7
  var lucideReact = require('lucide-react');
8
8
  var ui = require('@fayz-ai/ui');
9
+ var saas = require('@fayz-ai/saas');
9
10
  var vanilla = require('zustand/vanilla');
10
11
 
11
12
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
@@ -13,18 +14,26 @@ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
13
14
  var React3__default = /*#__PURE__*/_interopDefault(React3);
14
15
 
15
16
  // src/index.ts
17
+ var DEFAULT_CONVERSATIONS_CONFIG = {
18
+ contactKind: "contact"
19
+ };
16
20
  var StoreContext = React3__default.default.createContext(null);
21
+ var ConfigContext = React3__default.default.createContext(DEFAULT_CONVERSATIONS_CONFIG);
17
22
  function ConversationsContextProvider({
18
23
  store,
24
+ config = DEFAULT_CONVERSATIONS_CONFIG,
19
25
  children
20
26
  }) {
21
- return /* @__PURE__ */ jsxRuntime.jsx(StoreContext.Provider, { value: store, children });
27
+ return /* @__PURE__ */ jsxRuntime.jsx(StoreContext.Provider, { value: store, children: /* @__PURE__ */ jsxRuntime.jsx(ConfigContext.Provider, { value: config, children }) });
22
28
  }
23
29
  function useConversationsStore(selector) {
24
30
  const store = React3__default.default.useContext(StoreContext);
25
31
  if (!store) throw new Error("useConversationsStore must be used within ConversationsPage");
26
32
  return zustand.useStore(store, selector);
27
33
  }
34
+ function useConversationsConfig() {
35
+ return React3__default.default.useContext(ConfigContext);
36
+ }
28
37
 
29
38
  // src/types.ts
30
39
  var CHANNEL_LABELS = {
@@ -127,15 +136,147 @@ function dayLabel(iso) {
127
136
  if (dayDiff < 7) return d.toLocaleDateString([], { weekday: "long" });
128
137
  return d.toLocaleDateString([], { month: "short", day: "numeric", year: now.getFullYear() === d.getFullYear() ? void 0 : "numeric" });
129
138
  }
130
- var FILTERS = [
131
- { id: "all", label: "All" },
132
- { id: "whatsapp", label: "WhatsApp" },
133
- { id: "sms", label: "SMS" },
134
- { id: "instagram", label: "Instagram" },
135
- { id: "email", label: "Email" },
136
- { id: "webchat", label: "Web" }
137
- ];
139
+ var CHANNELS = ["whatsapp", "sms", "instagram", "email", "webchat"];
140
+ function personHandleFor(channel, contact) {
141
+ if (!contact) return "";
142
+ if (channel === "email") return contact.email ?? "";
143
+ if (channel === "sms" || channel === "whatsapp") return contact.phone ?? "";
144
+ return "";
145
+ }
146
+ var HANDLE_LABEL_KEY = {
147
+ whatsapp: "conversations.new.handleLabel.phone",
148
+ sms: "conversations.new.handleLabel.phone",
149
+ email: "conversations.new.handleLabel.email",
150
+ instagram: "conversations.new.handleLabel.instagram",
151
+ webchat: "conversations.new.handleLabel.webchat"
152
+ };
153
+ function NewConversationModal({
154
+ open,
155
+ onOpenChange
156
+ }) {
157
+ const t = core.useTranslation();
158
+ const create = useConversationsStore((s) => s.create);
159
+ const config = useConversationsConfig();
160
+ const guardConversations = saas.useLimitGuard("conversations_month");
161
+ const [channel, setChannel] = React3__default.default.useState("whatsapp");
162
+ const [contact, setContact] = React3__default.default.useState(null);
163
+ const [typedHandle, setTypedHandle] = React3__default.default.useState("");
164
+ const [creatingContact, setCreatingContact] = React3__default.default.useState(false);
165
+ const [firstMessage, setFirstMessage] = React3__default.default.useState("");
166
+ const [submitting, setSubmitting] = React3__default.default.useState(false);
167
+ const [pickerKey, setPickerKey] = React3__default.default.useState(0);
168
+ React3__default.default.useEffect(() => {
169
+ if (open) {
170
+ setChannel("whatsapp");
171
+ setContact(null);
172
+ setTypedHandle("");
173
+ setCreatingContact(false);
174
+ setFirstMessage("");
175
+ setSubmitting(false);
176
+ setPickerKey((k) => k + 1);
177
+ }
178
+ }, [open]);
179
+ const derivedHandle = personHandleFor(channel, contact);
180
+ const effectiveHandle = derivedHandle || typedHandle;
181
+ const handleLabel = t(HANDLE_LABEL_KEY[channel]);
182
+ const canSubmit = (contact?.name.trim().length ?? 0) > 0 && !submitting;
183
+ async function handleSubmit(e) {
184
+ e.preventDefault();
185
+ if (!canSubmit) return;
186
+ setSubmitting(true);
187
+ try {
188
+ if (await guardConversations() === "blocked") return;
189
+ await create({
190
+ channel,
191
+ contactName: contact.name.trim(),
192
+ contactPersonId: contact?.id,
193
+ contactHandle: effectiveHandle.trim() || void 0,
194
+ firstMessage: firstMessage.trim() || void 0
195
+ });
196
+ saas.invalidateLimit("conversations_month");
197
+ onOpenChange(false);
198
+ } catch (err) {
199
+ const message = err instanceof Error ? err.message : String(err);
200
+ ui.toast.error(t("conversations.new.createFailed"), { description: message });
201
+ } finally {
202
+ setSubmitting(false);
203
+ }
204
+ }
205
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Modal, { open, onOpenChange, children: /* @__PURE__ */ jsxRuntime.jsx(ui.ModalContent, { size: "md", children: /* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-4", children: [
206
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-base font-semibold text-foreground", children: t("conversations.new.title") }),
207
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
208
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "mb-1.5 block text-xs font-medium text-muted-foreground", children: t("conversations.new.channel") }),
209
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-1.5", children: CHANNELS.map((ch) => {
210
+ const Icon = CHANNEL_ICON[ch];
211
+ const active = channel === ch;
212
+ return /* @__PURE__ */ jsxRuntime.jsxs(
213
+ "button",
214
+ {
215
+ type: "button",
216
+ onClick: () => setChannel(ch),
217
+ "aria-pressed": active,
218
+ className: ui.cn(
219
+ "inline-flex items-center gap-1.5 rounded-full px-3 py-1.5 text-xs font-medium transition-colors",
220
+ active ? "text-white" : "bg-muted text-muted-foreground hover:bg-muted/70"
221
+ ),
222
+ style: active ? { backgroundColor: CHANNEL_ACCENT[ch].color } : void 0,
223
+ children: [
224
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { className: "h-3 w-3" }),
225
+ CHANNEL_LABELS[ch]
226
+ ]
227
+ },
228
+ ch
229
+ );
230
+ }) })
231
+ ] }),
232
+ /* @__PURE__ */ jsxRuntime.jsx(
233
+ saas.ContactPicker,
234
+ {
235
+ value: contact,
236
+ onChange: setContact,
237
+ kind: config.contactKind,
238
+ extensionTable: config.contactExtensionTable,
239
+ lookup: config.contactLookup,
240
+ allowFreeText: true,
241
+ onCreatingChange: setCreatingContact,
242
+ autoFocus: true,
243
+ label: t("conversations.new.contactName"),
244
+ placeholder: t("conversations.new.contactNamePlaceholder"),
245
+ secondaryText: derivedHandle || void 0,
246
+ handleField: {
247
+ label: handleLabel,
248
+ derived: derivedHandle || void 0,
249
+ value: typedHandle,
250
+ onChange: setTypedHandle,
251
+ fieldLabel: t("conversations.new.handle"),
252
+ placeholder: t("conversations.new.handlePlaceholder")
253
+ }
254
+ },
255
+ pickerKey
256
+ ),
257
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
258
+ /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "conv-first-message", className: "mb-1.5 block text-xs font-medium text-muted-foreground", children: t("conversations.new.firstMessage") }),
259
+ /* @__PURE__ */ jsxRuntime.jsx(
260
+ "textarea",
261
+ {
262
+ id: "conv-first-message",
263
+ value: firstMessage,
264
+ onChange: (e) => setFirstMessage(e.target.value),
265
+ rows: 3,
266
+ placeholder: t("conversations.new.firstMessagePlaceholder"),
267
+ className: "max-h-40 min-h-[64px] w-full resize-none rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground outline-none focus:border-primary"
268
+ }
269
+ )
270
+ ] }),
271
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-1 flex justify-end gap-2", children: [
272
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { type: "button", variant: "outline", onClick: () => onOpenChange(false), children: t("conversations.new.cancel") }),
273
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { type: "submit", disabled: !canSubmit, children: submitting ? t("conversations.new.creating") : t("conversations.new.create") })
274
+ ] })
275
+ ] }) }) });
276
+ }
277
+ var FILTERS = ["all", "whatsapp", "sms", "instagram", "email", "webchat"];
138
278
  function ConversationList({ className }) {
279
+ const t = core.useTranslation();
139
280
  const {
140
281
  conversations,
141
282
  selectedId,
@@ -146,8 +287,25 @@ function ConversationList({ className }) {
146
287
  setChannelFilter,
147
288
  setSearch
148
289
  } = useConversationsStore((s) => s);
290
+ const [newOpen, setNewOpen] = React3__default.default.useState(false);
149
291
  return /* @__PURE__ */ jsxRuntime.jsxs("aside", { className: ui.cn("w-full shrink-0 flex-col border-r border-border bg-card lg:w-[320px]", className), children: [
150
292
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border-b border-border px-3 py-3", children: [
293
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-2 flex items-center justify-between gap-2", children: [
294
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-semibold text-foreground", children: t("conversations.title") }),
295
+ /* @__PURE__ */ jsxRuntime.jsx(saas.PermissionGate, { feature: "conversations", action: "create", children: /* @__PURE__ */ jsxRuntime.jsxs(
296
+ ui.Button,
297
+ {
298
+ size: "sm",
299
+ onClick: () => setNewOpen(true),
300
+ "aria-label": t("conversations.list.new"),
301
+ "data-testid": "conversations-new",
302
+ children: [
303
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { className: "h-3.5 w-3.5 sm:mr-1" }),
304
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden sm:inline", children: t("conversations.list.new") })
305
+ ]
306
+ }
307
+ ) })
308
+ ] }),
151
309
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
152
310
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Search, { className: "pointer-events-none absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" }),
153
311
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -155,29 +313,39 @@ function ConversationList({ className }) {
155
313
  {
156
314
  value: search,
157
315
  onChange: (e) => setSearch(e.target.value),
158
- placeholder: "Search conversations",
316
+ placeholder: t("conversations.list.search"),
159
317
  className: "pl-8"
160
318
  }
161
319
  )
162
320
  ] }),
163
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-2 flex flex-wrap gap-1", children: FILTERS.map((f) => /* @__PURE__ */ jsxRuntime.jsx(
321
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-2 flex flex-wrap gap-1", children: FILTERS.map((id) => /* @__PURE__ */ jsxRuntime.jsx(
164
322
  "button",
165
323
  {
166
- onClick: () => setChannelFilter(f.id),
324
+ onClick: () => setChannelFilter(id),
167
325
  className: ui.cn(
168
326
  "rounded-full px-2.5 py-1 text-xs font-medium transition-colors",
169
- channelFilter === f.id ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground hover:bg-muted/70"
327
+ channelFilter === id ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground hover:bg-muted/70"
170
328
  ),
171
- children: f.label
329
+ children: t(`conversations.filter.${id}`)
172
330
  },
173
- f.id
331
+ id
174
332
  )) })
175
333
  ] }),
176
334
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-h-0 flex-1 overflow-y-auto", children: [
177
- loading && conversations.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "p-4 text-sm text-muted-foreground", children: "Loading\u2026" }),
335
+ loading && conversations.length === 0 && Array.from({ length: 6 }, (_, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex w-full items-start gap-3 border-b border-border/50 px-3 py-3", children: [
336
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-10 w-10 shrink-0 rounded-full" }),
337
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
338
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-2", children: [
339
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-4 w-28" }),
340
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-3 w-8" })
341
+ ] }),
342
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "mt-1.5 h-4 w-16 rounded-full" }),
343
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "mt-1.5 h-3 w-3/4" })
344
+ ] })
345
+ ] }, i)),
178
346
  !loading && conversations.length === 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-2 p-8 text-center text-muted-foreground", children: [
179
347
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Inbox, { className: "h-6 w-6" }),
180
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm", children: "No conversations" })
348
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm", children: t("conversations.list.empty") })
181
349
  ] }),
182
350
  conversations.map((c) => {
183
351
  const active = c.id === selectedId;
@@ -199,7 +367,7 @@ function ConversationList({ className }) {
199
367
  ] }),
200
368
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-1 flex items-center gap-2", children: [
201
369
  /* @__PURE__ */ jsxRuntime.jsx(ChannelBadge, { channel: c.channel }),
202
- c.status !== "open" && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[10px] uppercase tracking-wide text-muted-foreground/70", children: c.status })
370
+ c.status !== "open" && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[10px] uppercase tracking-wide text-muted-foreground/70", children: t(`conversations.status.${c.status}`) })
203
371
  ] }),
204
372
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-1 flex items-center justify-between gap-2", children: [
205
373
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: ui.cn("truncate text-xs", unread ? "text-foreground" : "text-muted-foreground"), children: c.lastMessagePreview }),
@@ -211,7 +379,8 @@ function ConversationList({ className }) {
211
379
  c.id
212
380
  );
213
381
  })
214
- ] })
382
+ ] }),
383
+ /* @__PURE__ */ jsxRuntime.jsx(NewConversationModal, { open: newOpen, onOpenChange: setNewOpen })
215
384
  ] });
216
385
  }
217
386
  function buildRows(messages) {
@@ -232,6 +401,7 @@ function buildRows(messages) {
232
401
  return rows;
233
402
  }
234
403
  function MessageThread({ selected, onTogglePanel, panelOpen, onBack, className }) {
404
+ const t = core.useTranslation();
235
405
  const { messages, sending, send, setStatus } = useConversationsStore((s) => s);
236
406
  const [draft, setDraft] = React3__default.default.useState("");
237
407
  const threadRef = React3__default.default.useRef(null);
@@ -249,7 +419,7 @@ function MessageThread({ selected, onTogglePanel, panelOpen, onBack, className }
249
419
  return /* @__PURE__ */ jsxRuntime.jsxs("section", { className: ui.cn("flex min-w-0 flex-1 flex-col bg-muted/20", className), children: [
250
420
  /* @__PURE__ */ jsxRuntime.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: [
251
421
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-w-0 items-center gap-2 md:gap-3", children: [
252
- onBack && /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "ghost", size: "icon", className: "-ml-1 shrink-0 lg:hidden", onClick: onBack, "aria-label": "Back to conversations", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronLeft, { className: "h-5 w-5" }) }),
422
+ onBack && /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "ghost", size: "icon", className: "-ml-1 shrink-0 lg:hidden", onClick: onBack, "aria-label": t("conversations.thread.back"), children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronLeft, { className: "h-5 w-5" }) }),
253
423
  /* @__PURE__ */ jsxRuntime.jsx(Avatar, { name: selected.contactName, accent: selected.accent, size: "sm", channel: selected.channel }),
254
424
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0", children: [
255
425
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "truncate text-sm font-semibold text-foreground", children: selected.contactName }),
@@ -260,15 +430,15 @@ function MessageThread({ selected, onTogglePanel, panelOpen, onBack, className }
260
430
  ] })
261
431
  ] }),
262
432
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex shrink-0 items-center gap-1.5", children: [
263
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { variant: "outline", size: "sm", onClick: () => setStatus("snoozed"), "aria-label": "Snooze", children: [
433
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { variant: "outline", size: "sm", onClick: () => setStatus("snoozed"), "aria-label": t("conversations.thread.snooze"), children: [
264
434
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Clock, { className: "h-3.5 w-3.5 sm:mr-1" }),
265
435
  " ",
266
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden sm:inline", children: "Snooze" })
436
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden sm:inline", children: t("conversations.thread.snooze") })
267
437
  ] }),
268
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { variant: "outline", size: "sm", onClick: () => setStatus("closed"), "aria-label": "Close conversation", children: [
438
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { variant: "outline", size: "sm", onClick: () => setStatus("closed"), "aria-label": t("conversations.thread.close"), children: [
269
439
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Archive, { className: "h-3.5 w-3.5 sm:mr-1" }),
270
440
  " ",
271
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden sm:inline", children: "Close" })
441
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden sm:inline", children: t("conversations.thread.close") })
272
442
  ] }),
273
443
  /* @__PURE__ */ jsxRuntime.jsx(
274
444
  ui.Button,
@@ -276,7 +446,7 @@ function MessageThread({ selected, onTogglePanel, panelOpen, onBack, className }
276
446
  variant: panelOpen ? "secondary" : "ghost",
277
447
  size: "icon",
278
448
  onClick: onTogglePanel,
279
- "aria-label": "Toggle contact details",
449
+ "aria-label": t("conversations.thread.details"),
280
450
  children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.PanelRight, { className: "h-4 w-4" })
281
451
  }
282
452
  )
@@ -316,12 +486,10 @@ function MessageThread({ selected, onTogglePanel, panelOpen, onBack, className }
316
486
  }),
317
487
  rows.length === 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex h-full flex-col items-center justify-center text-muted-foreground", children: [
318
488
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MessageSquare, { className: "h-7 w-7" }),
319
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 text-sm", children: "No messages yet" })
489
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 text-sm", children: t("conversations.thread.empty") })
320
490
  ] })
321
491
  ] }),
322
492
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-t border-border bg-card px-4 py-3", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-end gap-2", children: [
323
- /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "ghost", size: "icon", className: "mb-0.5 text-muted-foreground", "aria-label": "Add emoji", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Smile, { className: "h-4 w-4" }) }),
324
- /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "ghost", size: "icon", className: "mb-0.5 text-muted-foreground", "aria-label": "Attach file", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Paperclip, { className: "h-4 w-4" }) }),
325
493
  /* @__PURE__ */ jsxRuntime.jsx(
326
494
  "textarea",
327
495
  {
@@ -334,11 +502,11 @@ function MessageThread({ selected, onTogglePanel, panelOpen, onBack, className }
334
502
  }
335
503
  },
336
504
  rows: 1,
337
- placeholder: `Reply via ${CHANNEL_LABELS[selected.channel]}\u2026`,
505
+ placeholder: t("conversations.thread.reply", { channel: CHANNEL_LABELS[selected.channel] }),
338
506
  className: "max-h-32 min-h-[40px] flex-1 resize-none rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground outline-none focus:border-primary"
339
507
  }
340
508
  ),
341
- /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { onClick: () => void handleSend(), disabled: sending || !draft.trim(), "aria-label": "Send", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Send, { className: "h-4 w-4" }) })
509
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { onClick: () => void handleSend(), disabled: sending || !draft.trim(), "aria-label": t("conversations.thread.send"), children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Send, { className: "h-4 w-4" }) })
342
510
  ] }) })
343
511
  ] });
344
512
  }
@@ -353,10 +521,11 @@ function Section({ icon: Icon, title, children }) {
353
521
  ] });
354
522
  }
355
523
  function ContactPanel({ contact, onClose, className }) {
524
+ const t = core.useTranslation();
356
525
  return /* @__PURE__ */ jsxRuntime.jsxs("aside", { className: ui.cn("flex shrink-0 flex-col overflow-y-auto border-l border-border bg-card", className), children: [
357
526
  onClose && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between border-b border-border px-3 py-2 xl:hidden", children: [
358
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-semibold text-foreground", children: "Details" }),
359
- /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "ghost", size: "icon", onClick: onClose, "aria-label": "Close details", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { className: "h-4 w-4" }) })
527
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-semibold text-foreground", children: t("conversations.contact.details") }),
528
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "ghost", size: "icon", onClick: onClose, "aria-label": t("conversations.contact.closeDetails"), children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { className: "h-4 w-4" }) })
360
529
  ] }),
361
530
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-2 px-4 py-5 text-center", children: [
362
531
  /* @__PURE__ */ jsxRuntime.jsx(Avatar, { name: contact.contactName, accent: contact.accent, size: "lg", channel: contact.channel }),
@@ -366,33 +535,35 @@ function ContactPanel({ contact, onClose, className }) {
366
535
  ] }),
367
536
  /* @__PURE__ */ jsxRuntime.jsx(ChannelBadge, { channel: contact.channel })
368
537
  ] }),
369
- /* @__PURE__ */ jsxRuntime.jsx(Section, { icon: lucideReact.User, title: "Details", children: /* @__PURE__ */ jsxRuntime.jsxs("dl", { className: "space-y-1.5 text-sm", children: [
538
+ /* @__PURE__ */ jsxRuntime.jsx(Section, { icon: lucideReact.User, title: t("conversations.contact.details"), children: /* @__PURE__ */ jsxRuntime.jsxs("dl", { className: "space-y-1.5 text-sm", children: [
370
539
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-2", children: [
371
- /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "text-muted-foreground", children: "Channel" }),
540
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "text-muted-foreground", children: t("conversations.contact.channel") }),
372
541
  /* @__PURE__ */ jsxRuntime.jsx("dd", { className: "text-foreground", children: CHANNEL_LABELS[contact.channel] })
373
542
  ] }),
374
543
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-2", children: [
375
- /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "text-muted-foreground", children: "Status" }),
376
- /* @__PURE__ */ jsxRuntime.jsx("dd", { className: "capitalize text-foreground", children: contact.status })
544
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "text-muted-foreground", children: t("conversations.contact.status") }),
545
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { className: "text-foreground", children: t(`conversations.status.${contact.status}`) })
377
546
  ] }),
378
547
  contact.assignedTo && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-2", children: [
379
- /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "text-muted-foreground", children: "Assigned to" }),
548
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "text-muted-foreground", children: t("conversations.contact.assignedTo") }),
380
549
  /* @__PURE__ */ jsxRuntime.jsx("dd", { className: "text-foreground", children: contact.assignedTo })
381
550
  ] }),
382
551
  contact.location && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-2", children: [
383
552
  /* @__PURE__ */ jsxRuntime.jsxs("dt", { className: "flex items-center gap-1 text-muted-foreground", children: [
384
553
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MapPin, { className: "h-3 w-3" }),
385
- " Location"
554
+ " ",
555
+ t("conversations.contact.location")
386
556
  ] }),
387
557
  /* @__PURE__ */ jsxRuntime.jsx("dd", { className: "text-foreground", children: contact.location })
388
558
  ] })
389
559
  ] }) }),
390
- contact.tags.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(Section, { icon: lucideReact.Tag, title: "Tags", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-1.5", children: contact.tags.map((tag) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "rounded-full bg-muted px-2 py-0.5 text-xs font-medium text-foreground", children: tag }, tag)) }) }),
391
- contact.note && /* @__PURE__ */ jsxRuntime.jsx(Section, { icon: lucideReact.StickyNote, title: "Note", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-foreground", children: contact.note }) }),
392
- /* @__PURE__ */ jsxRuntime.jsx(Section, { icon: lucideReact.Link2, title: "Linked records", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: "No linked records yet." }) })
560
+ contact.tags.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(Section, { icon: lucideReact.Tag, title: t("conversations.contact.tags"), children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-1.5", children: contact.tags.map((tag) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "rounded-full bg-muted px-2 py-0.5 text-xs font-medium text-foreground", children: tag }, tag)) }) }),
561
+ contact.note && /* @__PURE__ */ jsxRuntime.jsx(Section, { icon: lucideReact.StickyNote, title: t("conversations.contact.note"), children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-foreground", children: contact.note }) }),
562
+ /* @__PURE__ */ jsxRuntime.jsx(Section, { icon: lucideReact.Link2, title: t("conversations.contact.linkedRecords"), children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: t("conversations.contact.noLinkedRecords") }) })
393
563
  ] });
394
564
  }
395
565
  function InboxView() {
566
+ const t = core.useTranslation();
396
567
  const { conversations, selectedId, deselect } = useConversationsStore((s) => s);
397
568
  const isWidePanel = useMediaQuery("(min-width: 1280px)");
398
569
  const [panelOpen, setPanelOpen] = React3__default.default.useState(false);
@@ -413,7 +584,7 @@ function InboxView() {
413
584
  }
414
585
  ) : /* @__PURE__ */ jsxRuntime.jsxs("section", { className: "hidden min-w-0 flex-1 flex-col items-center justify-center bg-muted/20 text-muted-foreground lg:flex", children: [
415
586
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MessageSquare, { className: "h-9 w-9" }),
416
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 text-sm", children: "Select a conversation to start chatting" })
587
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 text-sm", children: t("conversations.empty.select") })
417
588
  ] }),
418
589
  selected && panelOpen && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
419
590
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -435,13 +606,22 @@ function InboxView() {
435
606
  ] })
436
607
  ] });
437
608
  }
438
- function ConversationsPage({ store }) {
609
+ function ConversationsPage({ store, config }) {
439
610
  React3__default.default.useEffect(() => {
440
611
  void store.getState().load();
441
612
  }, []);
442
- return /* @__PURE__ */ jsxRuntime.jsx(ConversationsContextProvider, { store, children: /* @__PURE__ */ jsxRuntime.jsx(InboxView, {}) });
613
+ return /* @__PURE__ */ jsxRuntime.jsx(ConversationsContextProvider, { store, config, children: /* @__PURE__ */ jsxRuntime.jsx(InboxView, {}) });
443
614
  }
444
615
 
616
+ // src/data/accents.ts
617
+ var CHANNEL_ACCENT_HEX = {
618
+ whatsapp: "#22c55e",
619
+ sms: "#6366f1",
620
+ instagram: "#ec4899",
621
+ email: "#0ea5e9",
622
+ webchat: "#f59e0b"
623
+ };
624
+
445
625
  // src/data/mock.ts
446
626
  function minutesAgo(base, mins) {
447
627
  return new Date(base - mins * 6e4).toISOString();
@@ -551,12 +731,59 @@ function seed() {
551
731
  function msg(id, conversationId, channel, direction, body, author, at) {
552
732
  return { id, conversationId, channel, direction, body, author, at };
553
733
  }
554
- function createMockConversationsProvider() {
555
- const { conversations, messages } = seed();
556
- let counter = 100;
734
+ function createMockConversationsProvider(config) {
735
+ const selfAuthor = config?.selfAuthor ?? "You";
736
+ function resolveTenant() {
737
+ const raw = typeof config?.tenantId === "function" ? config.tenantId() : config?.tenantId;
738
+ return raw || "default";
739
+ }
740
+ const storageKey = () => `saas:mock:conversations:${resolveTenant()}`;
741
+ function hasStorage() {
742
+ try {
743
+ return typeof window !== "undefined" && !!window.localStorage;
744
+ } catch {
745
+ return false;
746
+ }
747
+ }
748
+ function load() {
749
+ if (hasStorage()) {
750
+ try {
751
+ const raw = window.localStorage.getItem(storageKey());
752
+ if (raw) {
753
+ const parsed = JSON.parse(raw);
754
+ if (Array.isArray(parsed.conversations) && Array.isArray(parsed.messages)) {
755
+ return {
756
+ conversations: parsed.conversations,
757
+ messages: parsed.messages,
758
+ counter: parsed.counter ?? 100
759
+ };
760
+ }
761
+ }
762
+ } catch {
763
+ }
764
+ }
765
+ const seeded = seed();
766
+ return { conversations: seeded.conversations, messages: seeded.messages, counter: 100 };
767
+ }
768
+ const state = load();
769
+ function persist() {
770
+ if (!hasStorage()) return;
771
+ try {
772
+ window.localStorage.setItem(
773
+ storageKey(),
774
+ JSON.stringify({
775
+ conversations: state.conversations,
776
+ messages: state.messages,
777
+ counter: state.counter
778
+ })
779
+ );
780
+ } catch {
781
+ }
782
+ }
783
+ persist();
557
784
  return {
558
785
  async listConversations(query) {
559
- let list = [...conversations];
786
+ let list = [...state.conversations];
560
787
  if (query?.channel && query.channel !== "all") list = list.filter((c) => c.channel === query.channel);
561
788
  if (query?.status && query.status !== "all") list = list.filter((c) => c.status === query.status);
562
789
  if (query?.search) {
@@ -568,44 +795,92 @@ function createMockConversationsProvider() {
568
795
  return list.sort((a, b) => b.lastMessageAt.localeCompare(a.lastMessageAt));
569
796
  },
570
797
  async getMessages(conversationId) {
571
- return messages.filter((m) => m.conversationId === conversationId).sort((a, b) => a.at.localeCompare(b.at));
798
+ return state.messages.filter((m) => m.conversationId === conversationId).sort((a, b) => a.at.localeCompare(b.at));
799
+ },
800
+ async createConversation(input) {
801
+ const now = (/* @__PURE__ */ new Date()).toISOString();
802
+ const firstMessage = input.firstMessage?.trim();
803
+ const id = `c${++state.counter}`;
804
+ const conversation = {
805
+ id,
806
+ contactName: input.contactName.trim(),
807
+ contactPersonId: input.contactPersonId,
808
+ contactHandle: input.contactHandle?.trim() ?? "",
809
+ channel: input.channel,
810
+ lastMessagePreview: firstMessage ?? "",
811
+ lastMessageAt: now,
812
+ unreadCount: 0,
813
+ status: "open",
814
+ assignedTo: selfAuthor,
815
+ accent: CHANNEL_ACCENT_HEX[input.channel],
816
+ tags: [],
817
+ note: input.note?.trim() || void 0
818
+ };
819
+ state.conversations.unshift(conversation);
820
+ if (firstMessage) {
821
+ state.messages.push({
822
+ id: `m${++state.counter}`,
823
+ conversationId: id,
824
+ channel: input.channel,
825
+ direction: "outbound",
826
+ body: firstMessage,
827
+ author: selfAuthor,
828
+ at: now
829
+ });
830
+ }
831
+ persist();
832
+ return conversation;
572
833
  },
573
834
  async sendMessage(input) {
574
- const conv = conversations.find((c) => c.id === input.conversationId);
835
+ const conv = state.conversations.find((c) => c.id === input.conversationId);
575
836
  const created = {
576
- id: `m${++counter}`,
837
+ id: `m${++state.counter}`,
577
838
  conversationId: input.conversationId,
578
839
  channel: conv?.channel ?? "sms",
579
840
  direction: "outbound",
580
841
  body: input.body,
581
- author: "You",
842
+ author: selfAuthor,
582
843
  at: (/* @__PURE__ */ new Date()).toISOString()
583
844
  };
584
- messages.push(created);
845
+ state.messages.push(created);
585
846
  if (conv) {
586
847
  conv.lastMessagePreview = input.body;
587
848
  conv.lastMessageAt = created.at;
588
849
  conv.unreadCount = 0;
589
850
  if (conv.status === "closed") conv.status = "open";
590
851
  }
852
+ persist();
591
853
  return created;
592
854
  },
593
855
  async markRead(conversationId) {
594
- const conv = conversations.find((c) => c.id === conversationId);
595
- if (conv) conv.unreadCount = 0;
856
+ const conv = state.conversations.find((c) => c.id === conversationId);
857
+ if (conv && conv.unreadCount !== 0) {
858
+ conv.unreadCount = 0;
859
+ persist();
860
+ }
596
861
  },
597
862
  async setStatus(conversationId, status) {
598
- const conv = conversations.find((c) => c.id === conversationId);
863
+ const conv = state.conversations.find((c) => c.id === conversationId);
599
864
  if (!conv) throw new Error("Conversation not found");
600
865
  conv.status = status;
866
+ persist();
601
867
  return conv;
602
868
  }
603
869
  };
604
870
  }
871
+
872
+ // src/data/tables.ts
873
+ var T = {
874
+ conversations: "plg_conversations",
875
+ messages: "plg_conversation_messages"
876
+ };
877
+
878
+ // src/data/supabase.ts
605
879
  function mapConversation(r) {
606
880
  return {
607
881
  id: String(r.id),
608
882
  contactName: r.contact_name ?? "",
883
+ contactPersonId: r.contact_person_id ?? void 0,
609
884
  contactHandle: r.contact_handle ?? "",
610
885
  channel: r.channel ?? "sms",
611
886
  lastMessagePreview: r.last_message_preview ?? "",
@@ -647,7 +922,7 @@ function createSupabaseConversationsProvider(config) {
647
922
  }
648
923
  return {
649
924
  async listConversations(query) {
650
- let q = client().from("conversations").select("*");
925
+ let q = client().from(T.conversations).select("*");
651
926
  const tenantId = resolveTenantId();
652
927
  if (tenantId) q = q.eq("tenant_id", tenantId);
653
928
  if (query?.channel && query.channel !== "all") {
@@ -670,7 +945,7 @@ function createSupabaseConversationsProvider(config) {
670
945
  return (data ?? []).map(mapConversation);
671
946
  },
672
947
  async getMessages(conversationId) {
673
- const selected = client().from("conversation_messages").select("*");
948
+ const selected = client().from(T.messages).select("*");
674
949
  const filtered = selected.eq(
675
950
  "conversation_id",
676
951
  conversationId
@@ -682,9 +957,51 @@ function createSupabaseConversationsProvider(config) {
682
957
  if (error) throw error;
683
958
  return (data ?? []).map(mapMessage);
684
959
  },
960
+ async createConversation(input) {
961
+ const tenantId = resolveTenantId();
962
+ if (!tenantId) {
963
+ throw new Error("[plugin-conversations] Active tenant not resolved \u2014 cannot create conversation. Try again in a moment.");
964
+ }
965
+ const now = (/* @__PURE__ */ new Date()).toISOString();
966
+ const firstMessage = input.firstMessage?.trim();
967
+ const convRow = {
968
+ contact_name: input.contactName.trim(),
969
+ // Only written when the picker resolved a real person — an app that
970
+ // hasn't run migration 002 yet would reject an unknown column, and
971
+ // omitting it keeps the free-text path working there.
972
+ ...input.contactPersonId ? { contact_person_id: input.contactPersonId } : {},
973
+ contact_handle: input.contactHandle?.trim() || null,
974
+ channel: input.channel,
975
+ last_message_preview: firstMessage || null,
976
+ last_message_at: now,
977
+ unread_count: 0,
978
+ status: "open",
979
+ assigned_to: selfAuthor,
980
+ accent: CHANNEL_ACCENT_HEX[input.channel],
981
+ tags: [],
982
+ note: input.note?.trim() || null
983
+ };
984
+ if (tenantId) convRow.tenant_id = tenantId;
985
+ const { data: created, error } = await client().from(T.conversations).insert(convRow).select().single();
986
+ if (error) throw error;
987
+ if (!created) throw new Error("Conversation not created");
988
+ if (firstMessage) {
989
+ const msgRow = {
990
+ conversation_id: String(created.id),
991
+ channel: input.channel,
992
+ direction: "outbound",
993
+ body: firstMessage,
994
+ author: selfAuthor,
995
+ at: now
996
+ };
997
+ if (tenantId) msgRow.tenant_id = tenantId;
998
+ await client().from(T.messages).insert(msgRow);
999
+ }
1000
+ return mapConversation(created);
1001
+ },
685
1002
  async sendMessage(input) {
686
1003
  const tenantId = resolveTenantId();
687
- const convSelected = client().from("conversations").select(
1004
+ const convSelected = client().from(T.conversations).select(
688
1005
  "channel"
689
1006
  );
690
1007
  const convFiltered = convSelected.eq(
@@ -703,9 +1020,9 @@ function createSupabaseConversationsProvider(config) {
703
1020
  at
704
1021
  };
705
1022
  if (tenantId) row.tenant_id = tenantId;
706
- const { data: created, error } = await client().from("conversation_messages").insert(row).select().single();
1023
+ const { data: created, error } = await client().from(T.messages).insert(row).select().single();
707
1024
  if (error) throw error;
708
- await client().from("conversations").update({
1025
+ await client().from(T.conversations).update({
709
1026
  last_message_preview: input.body,
710
1027
  last_message_at: at,
711
1028
  unread_count: 0,
@@ -714,13 +1031,13 @@ function createSupabaseConversationsProvider(config) {
714
1031
  return mapMessage(created ?? row);
715
1032
  },
716
1033
  async markRead(conversationId) {
717
- const { error } = await client().from("conversations").update({
1034
+ const { error } = await client().from(T.conversations).update({
718
1035
  unread_count: 0
719
1036
  }).eq("id", conversationId);
720
1037
  if (error) throw error;
721
1038
  },
722
1039
  async setStatus(conversationId, status) {
723
- const updated = client().from("conversations").update({
1040
+ const updated = client().from(T.conversations).update({
724
1041
  status
725
1042
  });
726
1043
  const filtered = updated.eq("id", conversationId);
@@ -771,6 +1088,25 @@ function createConversationsStore(provider) {
771
1088
  set({ search });
772
1089
  await get().load();
773
1090
  },
1091
+ async create(input) {
1092
+ const created = await provider.createConversation(input);
1093
+ set((s) => ({
1094
+ channelFilter: "all",
1095
+ search: "",
1096
+ conversations: [created, ...s.conversations.filter((c) => c.id !== created.id)],
1097
+ selectedId: created.id
1098
+ }));
1099
+ void (async () => {
1100
+ try {
1101
+ const conversations = await provider.listConversations({});
1102
+ const merged = conversations.some((c) => c.id === created.id) ? conversations : [created, ...conversations];
1103
+ set({ conversations: merged });
1104
+ await get().select(created.id);
1105
+ } catch {
1106
+ }
1107
+ })();
1108
+ return created;
1109
+ },
774
1110
  async send(body) {
775
1111
  const id = get().selectedId;
776
1112
  if (!id || !body.trim()) return;
@@ -798,26 +1134,266 @@ function createConversationsStore(provider) {
798
1134
  // src/locales/en.ts
799
1135
  var en = {
800
1136
  "conversations.title": "Conversations",
801
- "conversations.subtitle": "Unified inbox across every channel"
1137
+ "conversations.subtitle": "Unified inbox across every channel",
1138
+ // Conversation list
1139
+ "conversations.list.search": "Search conversations",
1140
+ "conversations.list.loading": "Loading\u2026",
1141
+ "conversations.list.empty": "No conversations",
1142
+ "conversations.list.new": "New conversation",
1143
+ // Channel filters
1144
+ "conversations.filter.all": "All",
1145
+ "conversations.filter.whatsapp": "WhatsApp",
1146
+ "conversations.filter.sms": "SMS",
1147
+ "conversations.filter.instagram": "Instagram",
1148
+ "conversations.filter.email": "Email",
1149
+ "conversations.filter.webchat": "Web",
1150
+ // Status labels
1151
+ "conversations.status.open": "Open",
1152
+ "conversations.status.snoozed": "Snoozed",
1153
+ "conversations.status.closed": "Closed",
1154
+ // Empty state
1155
+ "conversations.empty.select": "Select a conversation to start chatting",
1156
+ // Thread
1157
+ "conversations.thread.back": "Back to conversations",
1158
+ "conversations.thread.snooze": "Snooze",
1159
+ "conversations.thread.close": "Close",
1160
+ "conversations.thread.details": "Toggle contact details",
1161
+ "conversations.thread.empty": "No messages yet",
1162
+ "conversations.thread.reply": "Reply via {{channel}}\u2026",
1163
+ "conversations.thread.send": "Send",
1164
+ // Contact panel
1165
+ "conversations.contact.details": "Details",
1166
+ "conversations.contact.closeDetails": "Close details",
1167
+ "conversations.contact.channel": "Channel",
1168
+ "conversations.contact.status": "Status",
1169
+ "conversations.contact.assignedTo": "Assigned to",
1170
+ "conversations.contact.location": "Location",
1171
+ "conversations.contact.tags": "Tags",
1172
+ "conversations.contact.note": "Note",
1173
+ "conversations.contact.linkedRecords": "Linked records",
1174
+ "conversations.contact.noLinkedRecords": "No linked records yet.",
1175
+ // New-conversation modal
1176
+ "conversations.new.title": "New conversation",
1177
+ "conversations.new.channel": "Channel",
1178
+ "conversations.new.contactName": "Contact name",
1179
+ "conversations.new.contactNamePlaceholder": "e.g. Jane Doe",
1180
+ "conversations.new.handle": "Phone / handle / email",
1181
+ "conversations.new.handlePlaceholder": "+1 555 000 0000",
1182
+ // The handle field only surfaces when the picked contact has nothing usable
1183
+ // for the active channel — otherwise it is derived and shown on the chip.
1184
+ "conversations.new.addHandle": "Add {label}",
1185
+ "conversations.new.handleLabel.phone": "phone",
1186
+ "conversations.new.handleLabel.email": "email",
1187
+ "conversations.new.handleLabel.instagram": "Instagram handle",
1188
+ "conversations.new.handleLabel.webchat": "web chat id",
1189
+ "conversations.new.firstMessage": "First message",
1190
+ "conversations.new.firstMessagePlaceholder": "Write the first message (optional)\u2026",
1191
+ "conversations.new.cancel": "Cancel",
1192
+ "conversations.new.create": "Start conversation",
1193
+ "conversations.new.creating": "Starting\u2026",
1194
+ "conversations.new.createFailed": "Could not create the conversation"
1195
+ };
1196
+
1197
+ // src/locales/pt-BR.ts
1198
+ var ptBR = {
1199
+ "conversations.title": "Conversas",
1200
+ "conversations.subtitle": "Caixa de entrada unificada de todos os canais",
1201
+ // Lista de conversas
1202
+ "conversations.list.search": "Buscar conversas",
1203
+ "conversations.list.loading": "Carregando\u2026",
1204
+ "conversations.list.empty": "Nenhuma conversa",
1205
+ "conversations.list.new": "Nova conversa",
1206
+ // Filtros de canal
1207
+ "conversations.filter.all": "Todas",
1208
+ "conversations.filter.whatsapp": "WhatsApp",
1209
+ "conversations.filter.sms": "SMS",
1210
+ "conversations.filter.instagram": "Instagram",
1211
+ "conversations.filter.email": "E-mail",
1212
+ "conversations.filter.webchat": "Web",
1213
+ // Rótulos de status
1214
+ "conversations.status.open": "Aberta",
1215
+ "conversations.status.snoozed": "Adiada",
1216
+ "conversations.status.closed": "Encerrada",
1217
+ // Estado vazio
1218
+ "conversations.empty.select": "Selecione uma conversa para come\xE7ar a conversar",
1219
+ // Thread
1220
+ "conversations.thread.back": "Voltar \xE0s conversas",
1221
+ "conversations.thread.snooze": "Adiar",
1222
+ "conversations.thread.close": "Encerrar",
1223
+ "conversations.thread.details": "Alternar detalhes do contato",
1224
+ "conversations.thread.empty": "Nenhuma mensagem ainda",
1225
+ "conversations.thread.reply": "Responder via {{channel}}\u2026",
1226
+ "conversations.thread.send": "Enviar",
1227
+ // Painel do contato
1228
+ "conversations.contact.details": "Detalhes",
1229
+ "conversations.contact.closeDetails": "Fechar detalhes",
1230
+ "conversations.contact.channel": "Canal",
1231
+ "conversations.contact.status": "Status",
1232
+ "conversations.contact.assignedTo": "Respons\xE1vel",
1233
+ "conversations.contact.location": "Localiza\xE7\xE3o",
1234
+ "conversations.contact.tags": "Etiquetas",
1235
+ "conversations.contact.note": "Nota",
1236
+ "conversations.contact.linkedRecords": "Registros vinculados",
1237
+ "conversations.contact.noLinkedRecords": "Nenhum registro vinculado ainda.",
1238
+ // Modal de nova conversa
1239
+ "conversations.new.title": "Nova conversa",
1240
+ "conversations.new.channel": "Canal",
1241
+ "conversations.new.contactName": "Nome do contato",
1242
+ "conversations.new.contactNamePlaceholder": "ex.: Maria Silva",
1243
+ "conversations.new.handle": "Telefone / usu\xE1rio / e-mail",
1244
+ "conversations.new.handlePlaceholder": "+55 11 99999-0000",
1245
+ // O campo de handle só aparece quando o contato escolhido não tem o dado do
1246
+ // canal ativo — caso contrário ele é derivado e mostrado no chip.
1247
+ "conversations.new.addHandle": "Adicionar {label}",
1248
+ "conversations.new.handleLabel.phone": "telefone",
1249
+ "conversations.new.handleLabel.email": "e-mail",
1250
+ "conversations.new.handleLabel.instagram": "@ do Instagram",
1251
+ "conversations.new.handleLabel.webchat": "id do chat",
1252
+ "conversations.new.firstMessage": "Primeira mensagem",
1253
+ "conversations.new.firstMessagePlaceholder": "Escreva a primeira mensagem (opcional)\u2026",
1254
+ "conversations.new.cancel": "Cancelar",
1255
+ "conversations.new.create": "Iniciar conversa",
1256
+ "conversations.new.creating": "Iniciando\u2026",
1257
+ "conversations.new.createFailed": "N\xE3o foi poss\xEDvel criar a conversa"
802
1258
  };
803
1259
 
804
1260
  // src/locales/index.ts
805
1261
  var conversationsLocales = {
806
- en
1262
+ en,
1263
+ "pt-BR": ptBR
807
1264
  };
808
1265
 
1266
+ // src/migrations/index.ts
1267
+ var MIGRATION_001_CONVERSATIONS = `-- ============================================================================
1268
+ -- plugin-conversations 001: omni-channel inbox model (SMS / WhatsApp /
1269
+ -- Instagram / Email / Web chat). Prefix: plg_conversations / plg_conversation_messages.
1270
+ -- \xA71 plg_conversations \u2014 one thread per contact+channel
1271
+ -- \xA72 plg_conversation_messages \u2014 inbound/outbound messages within a thread
1272
+ -- \xA73 RLS: authenticated tenant-scoped CRUD on both tables + GRANTs
1273
+ --
1274
+ -- Column names mirror exactly what supabase.ts's mapConversation / mapMessage
1275
+ -- read. Real channel connectors (Twilio, WhatsApp Cloud, Meta, IMAP) deliver
1276
+ -- inbound rows here out-of-band; the provider is the read/compose surface.
1277
+ -- Idempotent + safe to re-run.
1278
+ -- ============================================================================
1279
+
1280
+ -- \xA71 \u2014 conversations (threads)
1281
+ CREATE TABLE IF NOT EXISTS public.plg_conversations (
1282
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
1283
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
1284
+ contact_name text NOT NULL,
1285
+ contact_handle text,
1286
+ channel text NOT NULL
1287
+ CHECK (channel IN ('sms', 'whatsapp', 'instagram', 'email', 'webchat')),
1288
+ last_message_preview text,
1289
+ last_message_at timestamptz DEFAULT now(),
1290
+ unread_count int DEFAULT 0,
1291
+ status text DEFAULT 'open'
1292
+ CHECK (status IN ('open', 'snoozed', 'closed')),
1293
+ assigned_to text,
1294
+ accent text,
1295
+ tags text[],
1296
+ location text,
1297
+ note text,
1298
+ created_at timestamptz NOT NULL DEFAULT now(),
1299
+ updated_at timestamptz NOT NULL DEFAULT now()
1300
+ );
1301
+ ALTER TABLE public.plg_conversations ENABLE ROW LEVEL SECURITY;
1302
+ CREATE INDEX IF NOT EXISTS idx_plg_conversations_tenant ON public.plg_conversations(tenant_id);
1303
+ CREATE INDEX IF NOT EXISTS idx_plg_conversations_tenant_recent ON public.plg_conversations(tenant_id, last_message_at DESC);
1304
+
1305
+ -- \xA72 \u2014 messages
1306
+ CREATE TABLE IF NOT EXISTS public.plg_conversation_messages (
1307
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
1308
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
1309
+ conversation_id uuid NOT NULL REFERENCES public.plg_conversations(id) ON DELETE CASCADE,
1310
+ channel text
1311
+ CHECK (channel IS NULL OR channel IN ('sms', 'whatsapp', 'instagram', 'email', 'webchat')),
1312
+ direction text
1313
+ CHECK (direction IN ('inbound', 'outbound')),
1314
+ body text NOT NULL,
1315
+ author text,
1316
+ at timestamptz DEFAULT now()
1317
+ );
1318
+ ALTER TABLE public.plg_conversation_messages ENABLE ROW LEVEL SECURITY;
1319
+ CREATE INDEX IF NOT EXISTS idx_plg_conversation_messages_thread ON public.plg_conversation_messages(conversation_id, at);
1320
+
1321
+ -- \xA73 \u2014 RLS: authenticated tenant CRUD (the inbox reads/writes here)
1322
+ DROP POLICY IF EXISTS plg_conversations_select ON public.plg_conversations;
1323
+ DROP POLICY IF EXISTS plg_conversations_insert ON public.plg_conversations;
1324
+ DROP POLICY IF EXISTS plg_conversations_update ON public.plg_conversations;
1325
+ DROP POLICY IF EXISTS plg_conversations_delete ON public.plg_conversations;
1326
+ CREATE POLICY plg_conversations_select ON public.plg_conversations FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
1327
+ CREATE POLICY plg_conversations_insert ON public.plg_conversations FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
1328
+ CREATE POLICY plg_conversations_update ON public.plg_conversations FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
1329
+ CREATE POLICY plg_conversations_delete ON public.plg_conversations FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
1330
+ GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_conversations TO authenticated;
1331
+
1332
+ DROP POLICY IF EXISTS plg_conversation_messages_select ON public.plg_conversation_messages;
1333
+ DROP POLICY IF EXISTS plg_conversation_messages_insert ON public.plg_conversation_messages;
1334
+ DROP POLICY IF EXISTS plg_conversation_messages_update ON public.plg_conversation_messages;
1335
+ DROP POLICY IF EXISTS plg_conversation_messages_delete ON public.plg_conversation_messages;
1336
+ CREATE POLICY plg_conversation_messages_select ON public.plg_conversation_messages FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
1337
+ CREATE POLICY plg_conversation_messages_insert ON public.plg_conversation_messages FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
1338
+ CREATE POLICY plg_conversation_messages_update ON public.plg_conversation_messages FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
1339
+ CREATE POLICY plg_conversation_messages_delete ON public.plg_conversation_messages FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
1340
+ GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_conversation_messages TO authenticated;
1341
+ `;
1342
+ var MIGRATION_002_CONTACT_PERSON = `-- ============================================================================
1343
+ -- plugin-conversations 002: link a thread to a REAL person record.
1344
+ --
1345
+ -- The compose modal used to take a free-text name + handle, so a conversation
1346
+ -- with "Maria" had nothing to do with the Maria in the agenda, the CRM or the
1347
+ -- financial module. The shared ContactPicker (find-or-create over
1348
+ -- public.people) now resolves a person, and this column stores that link.
1349
+ --
1350
+ -- Nullable on purpose, in both directions of time:
1351
+ -- \u2022 rows created before this migration keep working (name/handle only);
1352
+ -- \u2022 an inbound message from an unknown number still opens a thread with no
1353
+ -- person attached \u2014 the contact panel can offer "create contact" later.
1354
+ -- ON DELETE SET NULL: deleting a person must never take their history with it.
1355
+ -- Idempotent + safe to re-run.
1356
+ -- ============================================================================
1357
+
1358
+ ALTER TABLE public.plg_conversations
1359
+ ADD COLUMN IF NOT EXISTS contact_person_id uuid REFERENCES public.people(id) ON DELETE SET NULL;
1360
+
1361
+ CREATE INDEX IF NOT EXISTS idx_plg_conversations_person
1362
+ ON public.plg_conversations(tenant_id, contact_person_id)
1363
+ WHERE contact_person_id IS NOT NULL;
1364
+ `;
1365
+
809
1366
  // src/index.ts
810
1367
  function createSafeProvider() {
811
- if (core.getSupabaseClientOptional()) {
812
- return createSupabaseConversationsProvider({ tenantId: () => core.getActiveTenantId() });
813
- }
814
- return createMockConversationsProvider();
1368
+ let real = null;
1369
+ let mock = null;
1370
+ const resolve = () => {
1371
+ if (core.getSupabaseClientOptional()) {
1372
+ real ?? (real = createSupabaseConversationsProvider({ tenantId: () => core.getActiveTenantId() }));
1373
+ return real;
1374
+ }
1375
+ mock ?? (mock = createMockConversationsProvider({ tenantId: () => core.getActiveTenantId() }));
1376
+ return mock;
1377
+ };
1378
+ return {
1379
+ listConversations: (...a) => resolve().listConversations(...a),
1380
+ getMessages: (...a) => resolve().getMessages(...a),
1381
+ sendMessage: (...a) => resolve().sendMessage(...a),
1382
+ markRead: (...a) => resolve().markRead(...a),
1383
+ setStatus: (...a) => resolve().setStatus(...a),
1384
+ createConversation: (...a) => resolve().createConversation(...a)
1385
+ };
815
1386
  }
816
1387
  function createConversationsPlugin(options) {
817
1388
  core.registerTranslations(conversationsLocales);
818
1389
  const provider = options?.dataProvider ?? createSafeProvider();
819
1390
  const store = createConversationsStore(provider);
820
- const PageComponent = () => React3__default.default.createElement(ConversationsPage, { store });
1391
+ const config = {
1392
+ contactKind: options?.contactKind ?? "contact",
1393
+ contactExtensionTable: options?.contactExtensionTable,
1394
+ contactLookup: options?.contactLookup
1395
+ };
1396
+ const PageComponent = () => React3__default.default.createElement(ConversationsPage, { store, config });
821
1397
  PageComponent.displayName = "ConversationsPage";
822
1398
  return {
823
1399
  id: "conversations",
@@ -829,6 +1405,10 @@ function createConversationsPlugin(options) {
829
1405
  defaultEnabled: true,
830
1406
  dependencies: [],
831
1407
  declaredFeatures: [{ id: "conversations", label: "Conversations", group: "Engage" }],
1408
+ // Recurring monthly quota — counts conversation threads created this month.
1409
+ declaredLimits: [
1410
+ { key: "conversations_month", label: "Conversations this month", table: "plg_conversations", period: "month" }
1411
+ ],
832
1412
  navigation: [
833
1413
  {
834
1414
  section: options?.navSection ?? "main",
@@ -893,6 +1473,20 @@ function createConversationsPlugin(options) {
893
1473
  permission: { feature: "conversations", action: "create" }
894
1474
  }
895
1475
  ],
1476
+ migrations: [
1477
+ {
1478
+ id: "conversations-001-base-tables",
1479
+ version: "1.0.0",
1480
+ sql: MIGRATION_001_CONVERSATIONS,
1481
+ description: "Create plg_conversations and plg_conversation_messages (tenant-scoped RLS)"
1482
+ },
1483
+ {
1484
+ id: "conversations-002-contact-person",
1485
+ version: "1.1.0",
1486
+ sql: MIGRATION_002_CONTACT_PERSON,
1487
+ description: "Link threads to public.people via contact_person_id (nullable, ON DELETE SET NULL)"
1488
+ }
1489
+ ],
896
1490
  locales: conversationsLocales
897
1491
  };
898
1492
  }