@fayz-ai/plugin-conversations 0.8.3 → 0.9.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +13 -11
- package/dist/index.cjs +0 -1498
- package/dist/index.cjs.map +0 -1
package/dist/index.cjs
DELETED
|
@@ -1,1498 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
var React3 = require('react');
|
|
4
|
-
var core = require('@fayz-ai/core');
|
|
5
|
-
var zustand = require('zustand');
|
|
6
|
-
var jsxRuntime = require('react/jsx-runtime');
|
|
7
|
-
var lucideReact = require('lucide-react');
|
|
8
|
-
var ui = require('@fayz-ai/ui');
|
|
9
|
-
var saas = require('@fayz-ai/saas');
|
|
10
|
-
var vanilla = require('zustand/vanilla');
|
|
11
|
-
|
|
12
|
-
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
13
|
-
|
|
14
|
-
var React3__default = /*#__PURE__*/_interopDefault(React3);
|
|
15
|
-
|
|
16
|
-
// src/index.ts
|
|
17
|
-
var DEFAULT_CONVERSATIONS_CONFIG = {
|
|
18
|
-
contactKind: "contact"
|
|
19
|
-
};
|
|
20
|
-
var StoreContext = React3__default.default.createContext(null);
|
|
21
|
-
var ConfigContext = React3__default.default.createContext(DEFAULT_CONVERSATIONS_CONFIG);
|
|
22
|
-
function ConversationsContextProvider({
|
|
23
|
-
store,
|
|
24
|
-
config = DEFAULT_CONVERSATIONS_CONFIG,
|
|
25
|
-
children
|
|
26
|
-
}) {
|
|
27
|
-
return /* @__PURE__ */ jsxRuntime.jsx(StoreContext.Provider, { value: store, children: /* @__PURE__ */ jsxRuntime.jsx(ConfigContext.Provider, { value: config, children }) });
|
|
28
|
-
}
|
|
29
|
-
function useConversationsStore(selector) {
|
|
30
|
-
const store = React3__default.default.useContext(StoreContext);
|
|
31
|
-
if (!store) throw new Error("useConversationsStore must be used within ConversationsPage");
|
|
32
|
-
return zustand.useStore(store, selector);
|
|
33
|
-
}
|
|
34
|
-
function useConversationsConfig() {
|
|
35
|
-
return React3__default.default.useContext(ConfigContext);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// src/types.ts
|
|
39
|
-
var CHANNEL_LABELS = {
|
|
40
|
-
sms: "SMS",
|
|
41
|
-
whatsapp: "WhatsApp",
|
|
42
|
-
instagram: "Instagram",
|
|
43
|
-
email: "Email",
|
|
44
|
-
webchat: "Web Chat"
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
// src/channel.ts
|
|
48
|
-
var CHANNEL_ICON = {
|
|
49
|
-
sms: lucideReact.Phone,
|
|
50
|
-
whatsapp: lucideReact.MessageSquare,
|
|
51
|
-
instagram: lucideReact.Instagram,
|
|
52
|
-
email: lucideReact.Mail,
|
|
53
|
-
webchat: lucideReact.Globe
|
|
54
|
-
};
|
|
55
|
-
var CHANNEL_ACCENT = {
|
|
56
|
-
whatsapp: { color: "#22c55e", badge: "bg-[#22c55e]/12 text-[#15803d] dark:text-[#4ade80]" },
|
|
57
|
-
sms: { color: "#6366f1", badge: "bg-[#6366f1]/12 text-[#4338ca] dark:text-[#a5b4fc]" },
|
|
58
|
-
instagram: { color: "#ec4899", badge: "bg-[#ec4899]/12 text-[#be185d] dark:text-[#f9a8d4]" },
|
|
59
|
-
email: { color: "#0ea5e9", badge: "bg-[#0ea5e9]/12 text-[#0369a1] dark:text-[#7dd3fc]" },
|
|
60
|
-
webchat: { color: "#f59e0b", badge: "bg-[#f59e0b]/12 text-[#b45309] dark:text-[#fcd34d]" }
|
|
61
|
-
};
|
|
62
|
-
function useMediaQuery(query) {
|
|
63
|
-
const [matches, setMatches] = React3__default.default.useState(
|
|
64
|
-
() => typeof window !== "undefined" && window.matchMedia(query).matches
|
|
65
|
-
);
|
|
66
|
-
React3__default.default.useEffect(() => {
|
|
67
|
-
if (typeof window === "undefined") return;
|
|
68
|
-
const mql = window.matchMedia(query);
|
|
69
|
-
const onChange = () => setMatches(mql.matches);
|
|
70
|
-
onChange();
|
|
71
|
-
mql.addEventListener("change", onChange);
|
|
72
|
-
return () => mql.removeEventListener("change", onChange);
|
|
73
|
-
}, [query]);
|
|
74
|
-
return matches;
|
|
75
|
-
}
|
|
76
|
-
function initialsOf(name) {
|
|
77
|
-
return name.replace("@", "").split(/\s+/).filter(Boolean).map((w) => w[0]).slice(0, 2).join("").toUpperCase();
|
|
78
|
-
}
|
|
79
|
-
function Avatar({ name, accent, size = "md", channel }) {
|
|
80
|
-
const dims = size === "lg" ? "h-14 w-14 text-lg" : size === "sm" ? "h-9 w-9 text-xs" : "h-10 w-10 text-sm";
|
|
81
|
-
const Icon = channel ? CHANNEL_ICON[channel] : null;
|
|
82
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative shrink-0", children: [
|
|
83
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
84
|
-
"div",
|
|
85
|
-
{
|
|
86
|
-
className: ui.cn("flex items-center justify-center rounded-full font-semibold text-white", dims),
|
|
87
|
-
style: { backgroundColor: accent },
|
|
88
|
-
children: initialsOf(name)
|
|
89
|
-
}
|
|
90
|
-
),
|
|
91
|
-
Icon && /* @__PURE__ */ jsxRuntime.jsx(
|
|
92
|
-
"span",
|
|
93
|
-
{
|
|
94
|
-
className: "absolute -bottom-0.5 -right-0.5 flex h-4 w-4 items-center justify-center rounded-full ring-2 ring-card",
|
|
95
|
-
style: { backgroundColor: CHANNEL_ACCENT[channel].color },
|
|
96
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { className: "h-2.5 w-2.5 text-white" })
|
|
97
|
-
}
|
|
98
|
-
)
|
|
99
|
-
] });
|
|
100
|
-
}
|
|
101
|
-
function ChannelBadge({ channel, className }) {
|
|
102
|
-
const Icon = CHANNEL_ICON[channel];
|
|
103
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
104
|
-
"span",
|
|
105
|
-
{
|
|
106
|
-
className: ui.cn(
|
|
107
|
-
"inline-flex items-center gap-1 rounded-full px-1.5 py-0.5 text-[10px] font-medium",
|
|
108
|
-
CHANNEL_ACCENT[channel].badge,
|
|
109
|
-
className
|
|
110
|
-
),
|
|
111
|
-
children: [
|
|
112
|
-
/* @__PURE__ */ jsxRuntime.jsx(Icon, { className: "h-2.5 w-2.5" }),
|
|
113
|
-
CHANNEL_LABELS[channel]
|
|
114
|
-
]
|
|
115
|
-
}
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
function relativeTime(iso) {
|
|
119
|
-
const diff = Date.now() - new Date(iso).getTime();
|
|
120
|
-
const mins = Math.round(diff / 6e4);
|
|
121
|
-
if (mins < 1) return "now";
|
|
122
|
-
if (mins < 60) return `${mins}m`;
|
|
123
|
-
const hours = Math.round(mins / 60);
|
|
124
|
-
if (hours < 24) return `${hours}h`;
|
|
125
|
-
const days = Math.round(hours / 24);
|
|
126
|
-
if (days < 7) return `${days}d`;
|
|
127
|
-
return `${Math.round(days / 7)}w`;
|
|
128
|
-
}
|
|
129
|
-
function dayLabel(iso) {
|
|
130
|
-
const d = new Date(iso);
|
|
131
|
-
const now = /* @__PURE__ */ new Date();
|
|
132
|
-
const startOf = (x) => new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime();
|
|
133
|
-
const dayDiff = Math.round((startOf(now) - startOf(d)) / 864e5);
|
|
134
|
-
if (dayDiff <= 0) return "Today";
|
|
135
|
-
if (dayDiff === 1) return "Yesterday";
|
|
136
|
-
if (dayDiff < 7) return d.toLocaleDateString([], { weekday: "long" });
|
|
137
|
-
return d.toLocaleDateString([], { month: "short", day: "numeric", year: now.getFullYear() === d.getFullYear() ? void 0 : "numeric" });
|
|
138
|
-
}
|
|
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"];
|
|
278
|
-
function ConversationList({ className }) {
|
|
279
|
-
const t = core.useTranslation();
|
|
280
|
-
const {
|
|
281
|
-
conversations,
|
|
282
|
-
selectedId,
|
|
283
|
-
channelFilter,
|
|
284
|
-
search,
|
|
285
|
-
loading,
|
|
286
|
-
select,
|
|
287
|
-
setChannelFilter,
|
|
288
|
-
setSearch
|
|
289
|
-
} = useConversationsStore((s) => s);
|
|
290
|
-
const [newOpen, setNewOpen] = React3__default.default.useState(false);
|
|
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: [
|
|
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
|
-
] }),
|
|
309
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
|
|
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" }),
|
|
311
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
312
|
-
ui.Input,
|
|
313
|
-
{
|
|
314
|
-
value: search,
|
|
315
|
-
onChange: (e) => setSearch(e.target.value),
|
|
316
|
-
placeholder: t("conversations.list.search"),
|
|
317
|
-
className: "pl-8"
|
|
318
|
-
}
|
|
319
|
-
)
|
|
320
|
-
] }),
|
|
321
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-2 flex flex-wrap gap-1", children: FILTERS.map((id) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
322
|
-
"button",
|
|
323
|
-
{
|
|
324
|
-
onClick: () => setChannelFilter(id),
|
|
325
|
-
className: ui.cn(
|
|
326
|
-
"rounded-full px-2.5 py-1 text-xs font-medium transition-colors",
|
|
327
|
-
channelFilter === id ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground hover:bg-muted/70"
|
|
328
|
-
),
|
|
329
|
-
children: t(`conversations.filter.${id}`)
|
|
330
|
-
},
|
|
331
|
-
id
|
|
332
|
-
)) })
|
|
333
|
-
] }),
|
|
334
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-h-0 flex-1 overflow-y-auto", children: [
|
|
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)),
|
|
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: [
|
|
347
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Inbox, { className: "h-6 w-6" }),
|
|
348
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm", children: t("conversations.list.empty") })
|
|
349
|
-
] }),
|
|
350
|
-
conversations.map((c) => {
|
|
351
|
-
const active = c.id === selectedId;
|
|
352
|
-
const unread = c.unreadCount > 0;
|
|
353
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
354
|
-
"button",
|
|
355
|
-
{
|
|
356
|
-
onClick: () => select(c.id),
|
|
357
|
-
className: ui.cn(
|
|
358
|
-
"flex w-full items-start gap-3 border-b border-border/50 px-3 py-3 text-left transition-colors",
|
|
359
|
-
active ? "bg-accent" : "hover:bg-muted/50"
|
|
360
|
-
),
|
|
361
|
-
children: [
|
|
362
|
-
/* @__PURE__ */ jsxRuntime.jsx(Avatar, { name: c.contactName, accent: c.accent, channel: c.channel }),
|
|
363
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
364
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-2", children: [
|
|
365
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: ui.cn("truncate text-sm text-foreground", unread ? "font-semibold" : "font-medium"), children: c.contactName }),
|
|
366
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: ui.cn("shrink-0 text-[11px]", unread ? "font-semibold text-primary" : "text-muted-foreground"), children: relativeTime(c.lastMessageAt) })
|
|
367
|
-
] }),
|
|
368
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-1 flex items-center gap-2", children: [
|
|
369
|
-
/* @__PURE__ */ jsxRuntime.jsx(ChannelBadge, { channel: c.channel }),
|
|
370
|
-
c.status !== "open" && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[10px] uppercase tracking-wide text-muted-foreground/70", children: t(`conversations.status.${c.status}`) })
|
|
371
|
-
] }),
|
|
372
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-1 flex items-center justify-between gap-2", children: [
|
|
373
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: ui.cn("truncate text-xs", unread ? "text-foreground" : "text-muted-foreground"), children: c.lastMessagePreview }),
|
|
374
|
-
unread && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex h-[18px] min-w-[18px] shrink-0 items-center justify-center rounded-full bg-primary px-1.5 text-[10px] font-semibold text-primary-foreground", children: c.unreadCount })
|
|
375
|
-
] })
|
|
376
|
-
] })
|
|
377
|
-
]
|
|
378
|
-
},
|
|
379
|
-
c.id
|
|
380
|
-
);
|
|
381
|
-
})
|
|
382
|
-
] }),
|
|
383
|
-
/* @__PURE__ */ jsxRuntime.jsx(NewConversationModal, { open: newOpen, onOpenChange: setNewOpen })
|
|
384
|
-
] });
|
|
385
|
-
}
|
|
386
|
-
function buildRows(messages) {
|
|
387
|
-
const rows = [];
|
|
388
|
-
let lastDay = "";
|
|
389
|
-
messages.forEach((m, i) => {
|
|
390
|
-
const day = new Date(m.at).toDateString();
|
|
391
|
-
if (day !== lastDay) {
|
|
392
|
-
rows.push({ kind: "day", id: `day-${day}`, label: dayLabel(m.at) });
|
|
393
|
-
lastDay = day;
|
|
394
|
-
}
|
|
395
|
-
const prev = messages[i - 1];
|
|
396
|
-
const next = messages[i + 1];
|
|
397
|
-
const samePrev = prev && prev.direction === m.direction && new Date(prev.at).toDateString() === day;
|
|
398
|
-
const sameNext = next && next.direction === m.direction && new Date(next.at).toDateString() === day;
|
|
399
|
-
rows.push({ kind: "msg", id: m.id, message: m, startsRun: !samePrev, endsRun: !sameNext });
|
|
400
|
-
});
|
|
401
|
-
return rows;
|
|
402
|
-
}
|
|
403
|
-
function MessageThread({ selected, onTogglePanel, panelOpen, onBack, className }) {
|
|
404
|
-
const t = core.useTranslation();
|
|
405
|
-
const { messages, sending, send, setStatus } = useConversationsStore((s) => s);
|
|
406
|
-
const [draft, setDraft] = React3__default.default.useState("");
|
|
407
|
-
const threadRef = React3__default.default.useRef(null);
|
|
408
|
-
React3__default.default.useEffect(() => {
|
|
409
|
-
threadRef.current?.scrollTo({ top: threadRef.current.scrollHeight, behavior: "smooth" });
|
|
410
|
-
}, [messages.length, selected.id]);
|
|
411
|
-
async function handleSend() {
|
|
412
|
-
if (!draft.trim()) return;
|
|
413
|
-
const body = draft;
|
|
414
|
-
setDraft("");
|
|
415
|
-
await send(body);
|
|
416
|
-
}
|
|
417
|
-
const accent = CHANNEL_ACCENT[selected.channel];
|
|
418
|
-
const rows = React3__default.default.useMemo(() => buildRows(messages), [messages]);
|
|
419
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("section", { className: ui.cn("flex min-w-0 flex-1 flex-col bg-muted/20", className), children: [
|
|
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: [
|
|
421
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-w-0 items-center gap-2 md:gap-3", children: [
|
|
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" }) }),
|
|
423
|
-
/* @__PURE__ */ jsxRuntime.jsx(Avatar, { name: selected.contactName, accent: selected.accent, size: "sm", channel: selected.channel }),
|
|
424
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0", children: [
|
|
425
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "truncate text-sm font-semibold text-foreground", children: selected.contactName }),
|
|
426
|
-
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [
|
|
427
|
-
/* @__PURE__ */ jsxRuntime.jsx(ChannelBadge, { channel: selected.channel }),
|
|
428
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: selected.contactHandle })
|
|
429
|
-
] })
|
|
430
|
-
] })
|
|
431
|
-
] }),
|
|
432
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex shrink-0 items-center gap-1.5", children: [
|
|
433
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { variant: "outline", size: "sm", onClick: () => setStatus("snoozed"), "aria-label": t("conversations.thread.snooze"), children: [
|
|
434
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Clock, { className: "h-3.5 w-3.5 sm:mr-1" }),
|
|
435
|
-
" ",
|
|
436
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden sm:inline", children: t("conversations.thread.snooze") })
|
|
437
|
-
] }),
|
|
438
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { variant: "outline", size: "sm", onClick: () => setStatus("closed"), "aria-label": t("conversations.thread.close"), children: [
|
|
439
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Archive, { className: "h-3.5 w-3.5 sm:mr-1" }),
|
|
440
|
-
" ",
|
|
441
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden sm:inline", children: t("conversations.thread.close") })
|
|
442
|
-
] }),
|
|
443
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
444
|
-
ui.Button,
|
|
445
|
-
{
|
|
446
|
-
variant: panelOpen ? "secondary" : "ghost",
|
|
447
|
-
size: "icon",
|
|
448
|
-
onClick: onTogglePanel,
|
|
449
|
-
"aria-label": t("conversations.thread.details"),
|
|
450
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.PanelRight, { className: "h-4 w-4" })
|
|
451
|
-
}
|
|
452
|
-
)
|
|
453
|
-
] })
|
|
454
|
-
] }),
|
|
455
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { ref: threadRef, className: "min-h-0 flex-1 overflow-y-auto px-5 py-4", children: [
|
|
456
|
-
rows.map((row) => {
|
|
457
|
-
if (row.kind === "day") {
|
|
458
|
-
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "my-3 flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "rounded-full bg-muted px-3 py-0.5 text-[11px] font-medium text-muted-foreground", children: row.label }) }, row.id);
|
|
459
|
-
}
|
|
460
|
-
const m = row.message;
|
|
461
|
-
const outbound = m.direction === "outbound";
|
|
462
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
463
|
-
"div",
|
|
464
|
-
{
|
|
465
|
-
className: ui.cn("flex", outbound ? "justify-end" : "justify-start", row.startsRun ? "mt-2.5" : "mt-0.5"),
|
|
466
|
-
children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
467
|
-
"div",
|
|
468
|
-
{
|
|
469
|
-
className: ui.cn(
|
|
470
|
-
"max-w-[68%] px-3.5 py-2 text-sm shadow-sm",
|
|
471
|
-
outbound ? "rounded-2xl text-white" : "rounded-2xl bg-card text-foreground",
|
|
472
|
-
// Tail only on the last bubble of a run, on the sender's side.
|
|
473
|
-
outbound && row.endsRun && "rounded-br-sm",
|
|
474
|
-
!outbound && row.endsRun && "rounded-bl-sm"
|
|
475
|
-
),
|
|
476
|
-
style: outbound ? { backgroundColor: accent.color } : void 0,
|
|
477
|
-
children: [
|
|
478
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "whitespace-pre-wrap break-words", children: m.body }),
|
|
479
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: ui.cn("mt-0.5 text-right text-[10px]", outbound ? "text-white/70" : "text-muted-foreground"), children: new Date(m.at).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) })
|
|
480
|
-
]
|
|
481
|
-
}
|
|
482
|
-
)
|
|
483
|
-
},
|
|
484
|
-
row.id
|
|
485
|
-
);
|
|
486
|
-
}),
|
|
487
|
-
rows.length === 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex h-full flex-col items-center justify-center text-muted-foreground", children: [
|
|
488
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.MessageSquare, { className: "h-7 w-7" }),
|
|
489
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 text-sm", children: t("conversations.thread.empty") })
|
|
490
|
-
] })
|
|
491
|
-
] }),
|
|
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: [
|
|
493
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
494
|
-
"textarea",
|
|
495
|
-
{
|
|
496
|
-
value: draft,
|
|
497
|
-
onChange: (e) => setDraft(e.target.value),
|
|
498
|
-
onKeyDown: (e) => {
|
|
499
|
-
if (e.key === "Enter" && !e.shiftKey) {
|
|
500
|
-
e.preventDefault();
|
|
501
|
-
void handleSend();
|
|
502
|
-
}
|
|
503
|
-
},
|
|
504
|
-
rows: 1,
|
|
505
|
-
placeholder: t("conversations.thread.reply", { channel: CHANNEL_LABELS[selected.channel] }),
|
|
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"
|
|
507
|
-
}
|
|
508
|
-
),
|
|
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" }) })
|
|
510
|
-
] }) })
|
|
511
|
-
] });
|
|
512
|
-
}
|
|
513
|
-
function Section({ icon: Icon, title, children }) {
|
|
514
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border-t border-border px-4 py-3", children: [
|
|
515
|
-
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "mb-1.5 flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground", children: [
|
|
516
|
-
/* @__PURE__ */ jsxRuntime.jsx(Icon, { className: "h-3 w-3" }),
|
|
517
|
-
" ",
|
|
518
|
-
title
|
|
519
|
-
] }),
|
|
520
|
-
children
|
|
521
|
-
] });
|
|
522
|
-
}
|
|
523
|
-
function ContactPanel({ contact, onClose, className }) {
|
|
524
|
-
const t = core.useTranslation();
|
|
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: [
|
|
526
|
-
onClose && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between border-b border-border px-3 py-2 xl:hidden", children: [
|
|
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" }) })
|
|
529
|
-
] }),
|
|
530
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-2 px-4 py-5 text-center", children: [
|
|
531
|
-
/* @__PURE__ */ jsxRuntime.jsx(Avatar, { name: contact.contactName, accent: contact.accent, size: "lg", channel: contact.channel }),
|
|
532
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
533
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-semibold text-foreground", children: contact.contactName }),
|
|
534
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: contact.contactHandle })
|
|
535
|
-
] }),
|
|
536
|
-
/* @__PURE__ */ jsxRuntime.jsx(ChannelBadge, { channel: contact.channel })
|
|
537
|
-
] }),
|
|
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: [
|
|
539
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-2", children: [
|
|
540
|
-
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "text-muted-foreground", children: t("conversations.contact.channel") }),
|
|
541
|
-
/* @__PURE__ */ jsxRuntime.jsx("dd", { className: "text-foreground", children: CHANNEL_LABELS[contact.channel] })
|
|
542
|
-
] }),
|
|
543
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-2", children: [
|
|
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}`) })
|
|
546
|
-
] }),
|
|
547
|
-
contact.assignedTo && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-2", children: [
|
|
548
|
-
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "text-muted-foreground", children: t("conversations.contact.assignedTo") }),
|
|
549
|
-
/* @__PURE__ */ jsxRuntime.jsx("dd", { className: "text-foreground", children: contact.assignedTo })
|
|
550
|
-
] }),
|
|
551
|
-
contact.location && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-2", children: [
|
|
552
|
-
/* @__PURE__ */ jsxRuntime.jsxs("dt", { className: "flex items-center gap-1 text-muted-foreground", children: [
|
|
553
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.MapPin, { className: "h-3 w-3" }),
|
|
554
|
-
" ",
|
|
555
|
-
t("conversations.contact.location")
|
|
556
|
-
] }),
|
|
557
|
-
/* @__PURE__ */ jsxRuntime.jsx("dd", { className: "text-foreground", children: contact.location })
|
|
558
|
-
] })
|
|
559
|
-
] }) }),
|
|
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") }) })
|
|
563
|
-
] });
|
|
564
|
-
}
|
|
565
|
-
function InboxView() {
|
|
566
|
-
const t = core.useTranslation();
|
|
567
|
-
const { conversations, selectedId, deselect } = useConversationsStore((s) => s);
|
|
568
|
-
const isWidePanel = useMediaQuery("(min-width: 1280px)");
|
|
569
|
-
const [panelOpen, setPanelOpen] = React3__default.default.useState(false);
|
|
570
|
-
const selected = conversations.find((c) => c.id === selectedId) ?? null;
|
|
571
|
-
React3__default.default.useEffect(() => {
|
|
572
|
-
setPanelOpen(isWidePanel);
|
|
573
|
-
}, [isWidePanel]);
|
|
574
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex h-full min-h-0 overflow-hidden bg-background", children: [
|
|
575
|
-
/* @__PURE__ */ jsxRuntime.jsx(ConversationList, { className: ui.cn(selected ? "hidden lg:flex" : "flex") }),
|
|
576
|
-
selected ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
577
|
-
MessageThread,
|
|
578
|
-
{
|
|
579
|
-
selected,
|
|
580
|
-
panelOpen,
|
|
581
|
-
onTogglePanel: () => setPanelOpen((v) => !v),
|
|
582
|
-
onBack: deselect,
|
|
583
|
-
className: "flex"
|
|
584
|
-
}
|
|
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: [
|
|
586
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.MessageSquare, { className: "h-9 w-9" }),
|
|
587
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 text-sm", children: t("conversations.empty.select") })
|
|
588
|
-
] }),
|
|
589
|
-
selected && panelOpen && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
590
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
591
|
-
"button",
|
|
592
|
-
{
|
|
593
|
-
className: "absolute inset-0 z-10 bg-black/40 xl:hidden",
|
|
594
|
-
"aria-label": "Close details",
|
|
595
|
-
onClick: () => setPanelOpen(false)
|
|
596
|
-
}
|
|
597
|
-
),
|
|
598
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
599
|
-
ContactPanel,
|
|
600
|
-
{
|
|
601
|
-
contact: selected,
|
|
602
|
-
onClose: () => setPanelOpen(false),
|
|
603
|
-
className: "absolute inset-y-0 right-0 z-20 w-[340px] max-w-[88%] shadow-2xl xl:static xl:z-auto xl:w-[300px] xl:max-w-none xl:shadow-none"
|
|
604
|
-
}
|
|
605
|
-
)
|
|
606
|
-
] })
|
|
607
|
-
] });
|
|
608
|
-
}
|
|
609
|
-
function ConversationsPage({ store, config }) {
|
|
610
|
-
React3__default.default.useEffect(() => {
|
|
611
|
-
void store.getState().load();
|
|
612
|
-
}, []);
|
|
613
|
-
return /* @__PURE__ */ jsxRuntime.jsx(ConversationsContextProvider, { store, config, children: /* @__PURE__ */ jsxRuntime.jsx(InboxView, {}) });
|
|
614
|
-
}
|
|
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
|
-
|
|
625
|
-
// src/data/mock.ts
|
|
626
|
-
function minutesAgo(base, mins) {
|
|
627
|
-
return new Date(base - mins * 6e4).toISOString();
|
|
628
|
-
}
|
|
629
|
-
function seed() {
|
|
630
|
-
const base = (/* @__PURE__ */ new Date("2026-06-16T14:00:00Z")).getTime();
|
|
631
|
-
const conversations = [
|
|
632
|
-
{
|
|
633
|
-
id: "c1",
|
|
634
|
-
contactName: "Marina Alves",
|
|
635
|
-
contactHandle: "+55 11 99876-1020",
|
|
636
|
-
channel: "whatsapp",
|
|
637
|
-
lastMessagePreview: "Perfect, can we book for Friday at 3pm?",
|
|
638
|
-
lastMessageAt: minutesAgo(base, 4),
|
|
639
|
-
unreadCount: 2,
|
|
640
|
-
status: "open",
|
|
641
|
-
assignedTo: "You",
|
|
642
|
-
accent: "#22c55e",
|
|
643
|
-
tags: ["Hot lead"],
|
|
644
|
-
location: "S\xE3o Paulo, BR",
|
|
645
|
-
note: "Referred by Instagram ad \u2014 interested in full color + cut."
|
|
646
|
-
},
|
|
647
|
-
{
|
|
648
|
-
id: "c2",
|
|
649
|
-
contactName: "Jordan Pierce",
|
|
650
|
-
contactHandle: "+1 (415) 555-0142",
|
|
651
|
-
channel: "sms",
|
|
652
|
-
lastMessagePreview: "Got it \u2014 sending the deposit now.",
|
|
653
|
-
lastMessageAt: minutesAgo(base, 22),
|
|
654
|
-
unreadCount: 0,
|
|
655
|
-
status: "open",
|
|
656
|
-
assignedTo: "You",
|
|
657
|
-
accent: "#6366f1",
|
|
658
|
-
tags: ["Customer"],
|
|
659
|
-
location: "San Francisco, US"
|
|
660
|
-
},
|
|
661
|
-
{
|
|
662
|
-
id: "c3",
|
|
663
|
-
contactName: "@thehairloft",
|
|
664
|
-
contactHandle: "thehairloft",
|
|
665
|
-
channel: "instagram",
|
|
666
|
-
lastMessagePreview: "Do you offer balayage on weekends?",
|
|
667
|
-
lastMessageAt: minutesAgo(base, 51),
|
|
668
|
-
unreadCount: 1,
|
|
669
|
-
status: "open",
|
|
670
|
-
accent: "#ec4899",
|
|
671
|
-
tags: ["New"]
|
|
672
|
-
},
|
|
673
|
-
{
|
|
674
|
-
id: "c4",
|
|
675
|
-
contactName: "David Whitman",
|
|
676
|
-
contactHandle: "david@whitman.co",
|
|
677
|
-
channel: "email",
|
|
678
|
-
lastMessagePreview: "Re: Proposal \u2014 looks great, one question on pricing\u2026",
|
|
679
|
-
lastMessageAt: minutesAgo(base, 95),
|
|
680
|
-
unreadCount: 0,
|
|
681
|
-
status: "open",
|
|
682
|
-
assignedTo: "Sofia",
|
|
683
|
-
accent: "#0ea5e9",
|
|
684
|
-
tags: ["Proposal"],
|
|
685
|
-
location: "Austin, US",
|
|
686
|
-
note: "Evaluating the retainer tier. Decision expected this week."
|
|
687
|
-
},
|
|
688
|
-
{
|
|
689
|
-
id: "c5",
|
|
690
|
-
contactName: "Website visitor",
|
|
691
|
-
contactHandle: "live chat \xB7 acme.com",
|
|
692
|
-
channel: "webchat",
|
|
693
|
-
lastMessagePreview: "Is anyone available to chat?",
|
|
694
|
-
lastMessageAt: minutesAgo(base, 140),
|
|
695
|
-
unreadCount: 0,
|
|
696
|
-
status: "snoozed",
|
|
697
|
-
accent: "#f59e0b",
|
|
698
|
-
tags: []
|
|
699
|
-
},
|
|
700
|
-
{
|
|
701
|
-
id: "c6",
|
|
702
|
-
contactName: "Priya Nair",
|
|
703
|
-
contactHandle: "+44 7700 900123",
|
|
704
|
-
channel: "whatsapp",
|
|
705
|
-
lastMessagePreview: "Thank you! See you next week \u{1F64C}",
|
|
706
|
-
lastMessageAt: minutesAgo(base, 1440),
|
|
707
|
-
unreadCount: 0,
|
|
708
|
-
status: "closed",
|
|
709
|
-
assignedTo: "You",
|
|
710
|
-
accent: "#14b8a6",
|
|
711
|
-
tags: ["Customer"],
|
|
712
|
-
location: "London, UK"
|
|
713
|
-
}
|
|
714
|
-
];
|
|
715
|
-
const messages = [
|
|
716
|
-
msg("m1", "c1", "whatsapp", "inbound", "Hi! I saw your ad \u2014 do you have availability this week?", "Marina Alves", minutesAgo(base, 18)),
|
|
717
|
-
msg("m2", "c1", "whatsapp", "outbound", "Hi Marina! Yes, we do. What service are you interested in?", "You", minutesAgo(base, 15)),
|
|
718
|
-
msg("m3", "c1", "whatsapp", "inbound", "A full color + cut.", "Marina Alves", minutesAgo(base, 9)),
|
|
719
|
-
msg("m4", "c1", "whatsapp", "inbound", "Perfect, can we book for Friday at 3pm?", "Marina Alves", minutesAgo(base, 4)),
|
|
720
|
-
msg("m5", "c2", "sms", "outbound", "Your appointment is confirmed for tomorrow at 10am.", "You", minutesAgo(base, 40)),
|
|
721
|
-
msg("m6", "c2", "sms", "inbound", "Got it \u2014 sending the deposit now.", "Jordan Pierce", minutesAgo(base, 22)),
|
|
722
|
-
msg("m7", "c3", "instagram", "inbound", "Do you offer balayage on weekends?", "@thehairloft", minutesAgo(base, 51)),
|
|
723
|
-
msg("m8", "c4", "email", "inbound", "Re: Proposal \u2014 looks great, one question on pricing for the retainer tier.", "David Whitman", minutesAgo(base, 95)),
|
|
724
|
-
msg("m9", "c4", "email", "outbound", "Happy to walk you through it \u2014 are you free for a quick call tomorrow?", "Sofia", minutesAgo(base, 80)),
|
|
725
|
-
msg("m10", "c5", "webchat", "inbound", "Is anyone available to chat?", "Website visitor", minutesAgo(base, 140)),
|
|
726
|
-
msg("m11", "c6", "whatsapp", "outbound", "You are all set for next Tuesday. Anything else?", "You", minutesAgo(base, 1500)),
|
|
727
|
-
msg("m12", "c6", "whatsapp", "inbound", "Thank you! See you next week \u{1F64C}", "Priya Nair", minutesAgo(base, 1440))
|
|
728
|
-
];
|
|
729
|
-
return { conversations, messages };
|
|
730
|
-
}
|
|
731
|
-
function msg(id, conversationId, channel, direction, body, author, at) {
|
|
732
|
-
return { id, conversationId, channel, direction, body, author, at };
|
|
733
|
-
}
|
|
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();
|
|
784
|
-
return {
|
|
785
|
-
async listConversations(query) {
|
|
786
|
-
let list = [...state.conversations];
|
|
787
|
-
if (query?.channel && query.channel !== "all") list = list.filter((c) => c.channel === query.channel);
|
|
788
|
-
if (query?.status && query.status !== "all") list = list.filter((c) => c.status === query.status);
|
|
789
|
-
if (query?.search) {
|
|
790
|
-
const q = query.search.toLowerCase();
|
|
791
|
-
list = list.filter(
|
|
792
|
-
(c) => c.contactName.toLowerCase().includes(q) || c.lastMessagePreview.toLowerCase().includes(q)
|
|
793
|
-
);
|
|
794
|
-
}
|
|
795
|
-
return list.sort((a, b) => b.lastMessageAt.localeCompare(a.lastMessageAt));
|
|
796
|
-
},
|
|
797
|
-
async getMessages(conversationId) {
|
|
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;
|
|
833
|
-
},
|
|
834
|
-
async sendMessage(input) {
|
|
835
|
-
const conv = state.conversations.find((c) => c.id === input.conversationId);
|
|
836
|
-
const created = {
|
|
837
|
-
id: `m${++state.counter}`,
|
|
838
|
-
conversationId: input.conversationId,
|
|
839
|
-
channel: conv?.channel ?? "sms",
|
|
840
|
-
direction: "outbound",
|
|
841
|
-
body: input.body,
|
|
842
|
-
author: selfAuthor,
|
|
843
|
-
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
844
|
-
};
|
|
845
|
-
state.messages.push(created);
|
|
846
|
-
if (conv) {
|
|
847
|
-
conv.lastMessagePreview = input.body;
|
|
848
|
-
conv.lastMessageAt = created.at;
|
|
849
|
-
conv.unreadCount = 0;
|
|
850
|
-
if (conv.status === "closed") conv.status = "open";
|
|
851
|
-
}
|
|
852
|
-
persist();
|
|
853
|
-
return created;
|
|
854
|
-
},
|
|
855
|
-
async markRead(conversationId) {
|
|
856
|
-
const conv = state.conversations.find((c) => c.id === conversationId);
|
|
857
|
-
if (conv && conv.unreadCount !== 0) {
|
|
858
|
-
conv.unreadCount = 0;
|
|
859
|
-
persist();
|
|
860
|
-
}
|
|
861
|
-
},
|
|
862
|
-
async setStatus(conversationId, status) {
|
|
863
|
-
const conv = state.conversations.find((c) => c.id === conversationId);
|
|
864
|
-
if (!conv) throw new Error("Conversation not found");
|
|
865
|
-
conv.status = status;
|
|
866
|
-
persist();
|
|
867
|
-
return conv;
|
|
868
|
-
}
|
|
869
|
-
};
|
|
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
|
|
879
|
-
function mapConversation(r) {
|
|
880
|
-
return {
|
|
881
|
-
id: String(r.id),
|
|
882
|
-
contactName: r.contact_name ?? "",
|
|
883
|
-
contactPersonId: r.contact_person_id ?? void 0,
|
|
884
|
-
contactHandle: r.contact_handle ?? "",
|
|
885
|
-
channel: r.channel ?? "sms",
|
|
886
|
-
lastMessagePreview: r.last_message_preview ?? "",
|
|
887
|
-
lastMessageAt: r.last_message_at ?? "",
|
|
888
|
-
unreadCount: Number(r.unread_count ?? 0),
|
|
889
|
-
status: r.status ?? "open",
|
|
890
|
-
assignedTo: r.assigned_to ?? void 0,
|
|
891
|
-
accent: r.accent ?? "#6366f1",
|
|
892
|
-
tags: r.tags ?? [],
|
|
893
|
-
location: r.location ?? void 0,
|
|
894
|
-
note: r.note ?? void 0
|
|
895
|
-
};
|
|
896
|
-
}
|
|
897
|
-
function mapMessage(r) {
|
|
898
|
-
return {
|
|
899
|
-
id: String(r.id),
|
|
900
|
-
conversationId: String(r.conversation_id),
|
|
901
|
-
channel: r.channel ?? "sms",
|
|
902
|
-
direction: r.direction ?? "inbound",
|
|
903
|
-
body: r.body ?? "",
|
|
904
|
-
at: r.at ?? "",
|
|
905
|
-
author: r.author ?? ""
|
|
906
|
-
};
|
|
907
|
-
}
|
|
908
|
-
function createSupabaseConversationsProvider(config) {
|
|
909
|
-
const selfAuthor = config?.selfAuthor ?? "You";
|
|
910
|
-
function resolveTenantId() {
|
|
911
|
-
if (!config?.tenantId) return void 0;
|
|
912
|
-
return typeof config.tenantId === "function" ? config.tenantId() : config.tenantId;
|
|
913
|
-
}
|
|
914
|
-
function client() {
|
|
915
|
-
const supabase = config?.supabaseClient ?? core.getSupabaseClientOptional();
|
|
916
|
-
if (!supabase) {
|
|
917
|
-
throw new Error(
|
|
918
|
-
"[plugin-conversations] Supabase client not available. Pass supabaseClient or register the global client via createFayzApp."
|
|
919
|
-
);
|
|
920
|
-
}
|
|
921
|
-
return supabase;
|
|
922
|
-
}
|
|
923
|
-
return {
|
|
924
|
-
async listConversations(query) {
|
|
925
|
-
let q = client().from(T.conversations).select("*");
|
|
926
|
-
const tenantId = resolveTenantId();
|
|
927
|
-
if (tenantId) q = q.eq("tenant_id", tenantId);
|
|
928
|
-
if (query?.channel && query.channel !== "all") {
|
|
929
|
-
q = q.eq("channel", query.channel);
|
|
930
|
-
}
|
|
931
|
-
if (query?.status && query.status !== "all") {
|
|
932
|
-
q = q.eq("status", query.status);
|
|
933
|
-
}
|
|
934
|
-
if (query?.search) {
|
|
935
|
-
const term = `%${query.search}%`;
|
|
936
|
-
q = q.or(
|
|
937
|
-
`contact_name.ilike.${term},last_message_preview.ilike.${term}`
|
|
938
|
-
);
|
|
939
|
-
}
|
|
940
|
-
q = q.order("last_message_at", {
|
|
941
|
-
ascending: false
|
|
942
|
-
});
|
|
943
|
-
const { data, error } = await q;
|
|
944
|
-
if (error) throw error;
|
|
945
|
-
return (data ?? []).map(mapConversation);
|
|
946
|
-
},
|
|
947
|
-
async getMessages(conversationId) {
|
|
948
|
-
const selected = client().from(T.messages).select("*");
|
|
949
|
-
const filtered = selected.eq(
|
|
950
|
-
"conversation_id",
|
|
951
|
-
conversationId
|
|
952
|
-
);
|
|
953
|
-
const ordered = filtered.order("at", {
|
|
954
|
-
ascending: true
|
|
955
|
-
});
|
|
956
|
-
const { data, error } = await ordered;
|
|
957
|
-
if (error) throw error;
|
|
958
|
-
return (data ?? []).map(mapMessage);
|
|
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
|
-
},
|
|
1002
|
-
async sendMessage(input) {
|
|
1003
|
-
const tenantId = resolveTenantId();
|
|
1004
|
-
const convSelected = client().from(T.conversations).select(
|
|
1005
|
-
"channel"
|
|
1006
|
-
);
|
|
1007
|
-
const convFiltered = convSelected.eq(
|
|
1008
|
-
"id",
|
|
1009
|
-
input.conversationId
|
|
1010
|
-
);
|
|
1011
|
-
const { data: conv } = await convFiltered.maybeSingle();
|
|
1012
|
-
const channel = conv?.channel ?? "sms";
|
|
1013
|
-
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
1014
|
-
const row = {
|
|
1015
|
-
conversation_id: input.conversationId,
|
|
1016
|
-
channel,
|
|
1017
|
-
direction: "outbound",
|
|
1018
|
-
body: input.body,
|
|
1019
|
-
author: selfAuthor,
|
|
1020
|
-
at
|
|
1021
|
-
};
|
|
1022
|
-
if (tenantId) row.tenant_id = tenantId;
|
|
1023
|
-
const { data: created, error } = await client().from(T.messages).insert(row).select().single();
|
|
1024
|
-
if (error) throw error;
|
|
1025
|
-
await client().from(T.conversations).update({
|
|
1026
|
-
last_message_preview: input.body,
|
|
1027
|
-
last_message_at: at,
|
|
1028
|
-
unread_count: 0,
|
|
1029
|
-
status: "open"
|
|
1030
|
-
}).eq("id", input.conversationId);
|
|
1031
|
-
return mapMessage(created ?? row);
|
|
1032
|
-
},
|
|
1033
|
-
async markRead(conversationId) {
|
|
1034
|
-
const { error } = await client().from(T.conversations).update({
|
|
1035
|
-
unread_count: 0
|
|
1036
|
-
}).eq("id", conversationId);
|
|
1037
|
-
if (error) throw error;
|
|
1038
|
-
},
|
|
1039
|
-
async setStatus(conversationId, status) {
|
|
1040
|
-
const updated = client().from(T.conversations).update({
|
|
1041
|
-
status
|
|
1042
|
-
});
|
|
1043
|
-
const filtered = updated.eq("id", conversationId);
|
|
1044
|
-
const selected = filtered.select();
|
|
1045
|
-
const { data, error } = await selected.single();
|
|
1046
|
-
if (error) throw error;
|
|
1047
|
-
if (!data) throw new Error("Conversation not found");
|
|
1048
|
-
return mapConversation(data);
|
|
1049
|
-
}
|
|
1050
|
-
};
|
|
1051
|
-
}
|
|
1052
|
-
function createConversationsStore(provider) {
|
|
1053
|
-
return vanilla.createStore((set, get) => ({
|
|
1054
|
-
conversations: [],
|
|
1055
|
-
messages: [],
|
|
1056
|
-
selectedId: null,
|
|
1057
|
-
channelFilter: "all",
|
|
1058
|
-
search: "",
|
|
1059
|
-
loading: false,
|
|
1060
|
-
sending: false,
|
|
1061
|
-
async load() {
|
|
1062
|
-
set({ loading: true });
|
|
1063
|
-
const conversations = await provider.listConversations({
|
|
1064
|
-
channel: get().channelFilter,
|
|
1065
|
-
search: get().search || void 0
|
|
1066
|
-
});
|
|
1067
|
-
const selectedId = get().selectedId ?? conversations[0]?.id ?? null;
|
|
1068
|
-
set({ conversations, loading: false, selectedId });
|
|
1069
|
-
if (selectedId) await get().select(selectedId);
|
|
1070
|
-
},
|
|
1071
|
-
async select(id) {
|
|
1072
|
-
set({ selectedId: id });
|
|
1073
|
-
const messages = await provider.getMessages(id);
|
|
1074
|
-
set({ messages });
|
|
1075
|
-
await provider.markRead(id);
|
|
1076
|
-
set((s) => ({
|
|
1077
|
-
conversations: s.conversations.map((c) => c.id === id ? { ...c, unreadCount: 0 } : c)
|
|
1078
|
-
}));
|
|
1079
|
-
},
|
|
1080
|
-
deselect() {
|
|
1081
|
-
set({ selectedId: null, messages: [] });
|
|
1082
|
-
},
|
|
1083
|
-
async setChannelFilter(channel) {
|
|
1084
|
-
set({ channelFilter: channel });
|
|
1085
|
-
await get().load();
|
|
1086
|
-
},
|
|
1087
|
-
async setSearch(search) {
|
|
1088
|
-
set({ search });
|
|
1089
|
-
await get().load();
|
|
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
|
-
},
|
|
1110
|
-
async send(body) {
|
|
1111
|
-
const id = get().selectedId;
|
|
1112
|
-
if (!id || !body.trim()) return;
|
|
1113
|
-
set({ sending: true });
|
|
1114
|
-
const created = await provider.sendMessage({ conversationId: id, body: body.trim() });
|
|
1115
|
-
set((s) => ({
|
|
1116
|
-
sending: false,
|
|
1117
|
-
messages: [...s.messages, created],
|
|
1118
|
-
conversations: s.conversations.map(
|
|
1119
|
-
(c) => c.id === id ? { ...c, lastMessagePreview: created.body, lastMessageAt: created.at } : c
|
|
1120
|
-
)
|
|
1121
|
-
}));
|
|
1122
|
-
},
|
|
1123
|
-
async setStatus(status) {
|
|
1124
|
-
const id = get().selectedId;
|
|
1125
|
-
if (!id) return;
|
|
1126
|
-
const updated = await provider.setStatus(id, status);
|
|
1127
|
-
set((s) => ({
|
|
1128
|
-
conversations: s.conversations.map((c) => c.id === id ? updated : c)
|
|
1129
|
-
}));
|
|
1130
|
-
}
|
|
1131
|
-
}));
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
// src/locales/en.ts
|
|
1135
|
-
var en = {
|
|
1136
|
-
"conversations.title": "Conversations",
|
|
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"
|
|
1258
|
-
};
|
|
1259
|
-
|
|
1260
|
-
// src/locales/index.ts
|
|
1261
|
-
var conversationsLocales = {
|
|
1262
|
-
en,
|
|
1263
|
-
"pt-BR": ptBR
|
|
1264
|
-
};
|
|
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
|
-
|
|
1366
|
-
// src/index.ts
|
|
1367
|
-
function createSafeProvider() {
|
|
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
|
-
};
|
|
1386
|
-
}
|
|
1387
|
-
function createConversationsPlugin(options) {
|
|
1388
|
-
core.registerTranslations(conversationsLocales);
|
|
1389
|
-
const provider = options?.dataProvider ?? createSafeProvider();
|
|
1390
|
-
const store = createConversationsStore(provider);
|
|
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 });
|
|
1397
|
-
PageComponent.displayName = "ConversationsPage";
|
|
1398
|
-
return {
|
|
1399
|
-
id: "conversations",
|
|
1400
|
-
name: options?.navLabel ?? "Conversations",
|
|
1401
|
-
icon: "MessageCircle",
|
|
1402
|
-
version: "1.0.0",
|
|
1403
|
-
scope: options?.scope ?? "universal",
|
|
1404
|
-
verticalId: options?.verticalId,
|
|
1405
|
-
defaultEnabled: true,
|
|
1406
|
-
dependencies: [],
|
|
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
|
-
],
|
|
1412
|
-
navigation: [
|
|
1413
|
-
{
|
|
1414
|
-
section: options?.navSection ?? "main",
|
|
1415
|
-
position: options?.navPosition ?? 1,
|
|
1416
|
-
label: options?.navLabel ?? "Conversations",
|
|
1417
|
-
route: "/conversations",
|
|
1418
|
-
icon: "MessageCircle",
|
|
1419
|
-
permission: { feature: "conversations", action: "read" }
|
|
1420
|
-
}
|
|
1421
|
-
],
|
|
1422
|
-
routes: [
|
|
1423
|
-
{
|
|
1424
|
-
path: "/conversations",
|
|
1425
|
-
component: PageComponent,
|
|
1426
|
-
fullBleed: true,
|
|
1427
|
-
permission: { feature: "conversations", action: "read" }
|
|
1428
|
-
}
|
|
1429
|
-
],
|
|
1430
|
-
widgets: [],
|
|
1431
|
-
events: [
|
|
1432
|
-
{ name: "conversations.message.received", description: "An inbound message arrived on any channel" },
|
|
1433
|
-
{ name: "conversations.message.sent", description: "An outbound message was sent" }
|
|
1434
|
-
],
|
|
1435
|
-
aiTools: [
|
|
1436
|
-
{
|
|
1437
|
-
id: "conversations.list-threads",
|
|
1438
|
-
name: "listConversations",
|
|
1439
|
-
description: "Lists open conversations across all channels, optionally filtered by channel.",
|
|
1440
|
-
icon: "MessageCircle",
|
|
1441
|
-
mode: "read",
|
|
1442
|
-
category: "Conversations",
|
|
1443
|
-
parameters: {
|
|
1444
|
-
type: "object",
|
|
1445
|
-
properties: {
|
|
1446
|
-
channel: {
|
|
1447
|
-
type: "string",
|
|
1448
|
-
enum: ["all", "sms", "whatsapp", "instagram", "email", "webchat"]
|
|
1449
|
-
}
|
|
1450
|
-
}
|
|
1451
|
-
},
|
|
1452
|
-
suggestions: [
|
|
1453
|
-
{ label: "Show unread conversations" },
|
|
1454
|
-
{ label: "Any new WhatsApp messages?" }
|
|
1455
|
-
],
|
|
1456
|
-
permission: { feature: "conversations", action: "read" }
|
|
1457
|
-
},
|
|
1458
|
-
{
|
|
1459
|
-
id: "conversations.send-message",
|
|
1460
|
-
name: "sendMessage",
|
|
1461
|
-
description: "Sends a reply in a conversation thread.",
|
|
1462
|
-
icon: "Send",
|
|
1463
|
-
mode: "persist",
|
|
1464
|
-
category: "Conversations",
|
|
1465
|
-
parameters: {
|
|
1466
|
-
type: "object",
|
|
1467
|
-
properties: {
|
|
1468
|
-
conversationId: { type: "string", description: "Conversation id" },
|
|
1469
|
-
body: { type: "string", description: "Message body" }
|
|
1470
|
-
},
|
|
1471
|
-
required: ["conversationId", "body"]
|
|
1472
|
-
},
|
|
1473
|
-
permission: { feature: "conversations", action: "create" }
|
|
1474
|
-
}
|
|
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
|
-
],
|
|
1490
|
-
locales: conversationsLocales
|
|
1491
|
-
};
|
|
1492
|
-
}
|
|
1493
|
-
|
|
1494
|
-
exports.createConversationsPlugin = createConversationsPlugin;
|
|
1495
|
-
exports.createMockConversationsProvider = createMockConversationsProvider;
|
|
1496
|
-
exports.createSupabaseConversationsProvider = createSupabaseConversationsProvider;
|
|
1497
|
-
//# sourceMappingURL=index.cjs.map
|
|
1498
|
-
//# sourceMappingURL=index.cjs.map
|