@fayz-ai/plugin-conversations 0.9.3 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/context.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
2
  import { type StoreApi } from 'zustand';
3
3
  import type { EntityLookup } from '@fayz-ai/admin';
4
+ import type { EntityDef } from '@fayz-ai/core';
4
5
  import type { ConversationsUIState } from './store';
5
6
  /**
6
7
  * App-tunable knobs the inbox UI reads. Kept tiny on purpose: everything here
@@ -8,12 +9,12 @@ import type { ConversationsUIState } from './store';
8
9
  * never product policy.
9
10
  */
10
11
  export interface ResolvedConversationsConfig {
11
- /** `people.kind` used when the compose modal creates a contact. */
12
+ /** `people.kind` the picker searches. */
12
13
  contactKind: string;
13
- /** Per-vertical extension table linked by `person_id` (skipped when absent). */
14
- contactExtensionTable?: string;
15
14
  /** Search source for the contact picker. Defaults to the person archetype. */
16
15
  contactLookup?: EntityLookup;
16
+ /** The app's own contact entity — "Create" opens ITS form. Agenda's twin is `clientEntityDef`. */
17
+ contactEntityDef?: EntityDef;
17
18
  }
18
19
  export declare const DEFAULT_CONVERSATIONS_CONFIG: ResolvedConversationsConfig;
19
20
  export declare function ConversationsContextProvider({ store, config, children, }: {
@@ -1 +1 @@
1
- {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,EAAY,KAAK,QAAQ,EAAE,MAAM,SAAS,CAAA;AACjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAClD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAA;AAEnD;;;;GAIG;AACH,MAAM,WAAW,2BAA2B;IAC1C,mEAAmE;IACnE,WAAW,EAAE,MAAM,CAAA;IACnB,gFAAgF;IAChF,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,8EAA8E;IAC9E,aAAa,CAAC,EAAE,YAAY,CAAA;CAC7B;AAED,eAAO,MAAM,4BAA4B,EAAE,2BAE1C,CAAA;AAKD,wBAAgB,4BAA4B,CAAC,EAC3C,KAAK,EACL,MAAqC,EACrC,QAAQ,GACT,EAAE;IACD,KAAK,EAAE,QAAQ,CAAC,oBAAoB,CAAC,CAAA;IACrC,MAAM,CAAC,EAAE,2BAA2B,CAAA;IACpC,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC3B,qBAMA;AAED,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,CAAC,GAAG,CAAC,CAIxF;AAED,wBAAgB,sBAAsB,IAAI,2BAA2B,CAEpE"}
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,EAAY,KAAK,QAAQ,EAAE,MAAM,SAAS,CAAA;AACjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAClD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AAC9C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAA;AAEnD;;;;GAIG;AACH,MAAM,WAAW,2BAA2B;IAC1C,yCAAyC;IACzC,WAAW,EAAE,MAAM,CAAA;IACnB,8EAA8E;IAC9E,aAAa,CAAC,EAAE,YAAY,CAAA;IAC5B,kGAAkG;IAClG,gBAAgB,CAAC,EAAE,SAAS,CAAA;CAC7B;AAED,eAAO,MAAM,4BAA4B,EAAE,2BAE1C,CAAA;AAKD,wBAAgB,4BAA4B,CAAC,EAC3C,KAAK,EACL,MAAqC,EACrC,QAAQ,GACT,EAAE;IACD,KAAK,EAAE,QAAQ,CAAC,oBAAoB,CAAC,CAAA;IACrC,MAAM,CAAC,EAAE,2BAA2B,CAAA;IACpC,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC3B,qBAMA;AAED,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,CAAC,GAAG,CAAC,CAIxF;AAED,wBAAgB,sBAAsB,IAAI,2BAA2B,CAEpE"}
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { PluginManifest, PluginScope, VerticalId } from '@fayz-ai/core';
1
+ import type { EntityDef, PluginManifest, PluginScope, VerticalId } from '@fayz-ai/core';
2
2
  import type { EntityLookup } from '@fayz-ai/admin';
3
3
  import type { ConversationsProvider } from './data/types';
4
4
  export interface ConversationsPluginOptions {
@@ -14,13 +14,13 @@ export interface ConversationsPluginOptions {
14
14
  * hence a knob rather than a hardcoded value. Default: 'contact'.
15
15
  */
16
16
  contactKind?: string;
17
- /**
18
- * Per-vertical extension table linked by `person_id` (e.g. 'clients'). Pools
19
- * without it live off `public.people` alone — absent is not an error.
20
- */
21
- contactExtensionTable?: string;
22
17
  /** Custom search source for the picker. Defaults to the person archetype lookup. */
23
18
  contactLookup?: EntityLookup;
19
+ /**
20
+ * The app's contact entity, same definition its registry page uses. "Create"
21
+ * in the picker opens THAT form. Without it the picker searches only.
22
+ */
23
+ contactEntityDef?: EntityDef;
24
24
  }
25
25
  export declare function createConversationsPlugin(options?: ConversationsPluginOptions): PluginManifest;
26
26
  export type { ConversationsProvider } from './data/types';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAE5E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAGlD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AAkBzD,MAAM,WAAW,0BAA0B;IACzC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,UAAU,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,UAAU,CAAA;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,WAAW,CAAA;IACnB,UAAU,CAAC,EAAE,UAAU,CAAA;IACvB,YAAY,CAAC,EAAE,qBAAqB,CAAA;IAEpC;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;OAGG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,oFAAoF;IACpF,aAAa,CAAC,EAAE,YAAY,CAAA;CAC7B;AA2BD,wBAAgB,yBAAyB,CAAC,OAAO,CAAC,EAAE,0BAA0B,GAAG,cAAc,CA6G9F;AAED,YAAY,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AACzD,YAAY,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAA;AACtF,OAAO,EAAE,+BAA+B,EAAE,KAAK,uBAAuB,EAAE,MAAM,aAAa,CAAA;AAC3F,OAAO,EACL,mCAAmC,EACnC,KAAK,2BAA2B,GACjC,MAAM,iBAAiB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAEvF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAGlD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AAkBzD,MAAM,WAAW,0BAA0B;IACzC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,UAAU,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,UAAU,CAAA;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,WAAW,CAAA;IACnB,UAAU,CAAC,EAAE,UAAU,CAAA;IACvB,YAAY,CAAC,EAAE,qBAAqB,CAAA;IAEpC;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,oFAAoF;IACpF,aAAa,CAAC,EAAE,YAAY,CAAA;IAC5B;;;OAGG;IACH,gBAAgB,CAAC,EAAE,SAAS,CAAA;CAC7B;AA2BD,wBAAgB,yBAAyB,CAAC,OAAO,CAAC,EAAE,0BAA0B,GAAG,cAAc,CA8G9F;AAED,YAAY,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AACzD,YAAY,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAA;AACtF,OAAO,EAAE,+BAA+B,EAAE,KAAK,uBAAuB,EAAE,MAAM,aAAa,CAAA;AAC3F,OAAO,EACL,mCAAmC,EACnC,KAAK,2BAA2B,GACjC,MAAM,iBAAiB,CAAA"}
package/dist/index.js CHANGED
@@ -1,18 +1,18 @@
1
- import React3 from 'react';
1
+ import React4 from 'react';
2
2
  import { registerTranslations, getSupabaseClientOptional, getActiveTenantId, useTranslation } from '@fayz-ai/core';
3
3
  import { useStore } from 'zustand';
4
4
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
- import { MessageSquare, Plus, Search, Inbox, ChevronLeft, Clock, Archive, PanelRight, Send, X, User, MapPin, Tag, StickyNote, Link2, Globe, Mail, Instagram, Phone } from 'lucide-react';
6
- import { cn, Button, Input, Skeleton, Modal, ModalContent, toast } from '@fayz-ai/ui';
7
- import { PermissionGate, useLimitGuard, ContactPicker, invalidateLimit } from '@fayz-ai/admin';
5
+ import { SquarePen, MessageSquare, Globe, Mail, Instagram, Phone, Search, Inbox, ChevronLeft, Clock, Archive, PanelRight, Send, X, User, MapPin, Tag, StickyNote, Link2 } from 'lucide-react';
6
+ import { PageHeaderActions, Button, cn, Input, Skeleton, toast } from '@fayz-ai/ui';
7
+ import { PermissionGate, useLimitGuard, RightRailPage, ContactPicker, invalidateLimit } from '@fayz-ai/admin';
8
8
  import { createStore } from 'zustand/vanilla';
9
9
 
10
10
  // src/index.ts
11
11
  var DEFAULT_CONVERSATIONS_CONFIG = {
12
12
  contactKind: "contact"
13
13
  };
14
- var StoreContext = React3.createContext(null);
15
- var ConfigContext = React3.createContext(DEFAULT_CONVERSATIONS_CONFIG);
14
+ var StoreContext = React4.createContext(null);
15
+ var ConfigContext = React4.createContext(DEFAULT_CONVERSATIONS_CONFIG);
16
16
  function ConversationsContextProvider({
17
17
  store,
18
18
  config = DEFAULT_CONVERSATIONS_CONFIG,
@@ -21,12 +21,12 @@ function ConversationsContextProvider({
21
21
  return /* @__PURE__ */ jsx(StoreContext.Provider, { value: store, children: /* @__PURE__ */ jsx(ConfigContext.Provider, { value: config, children }) });
22
22
  }
23
23
  function useConversationsStore(selector) {
24
- const store = React3.useContext(StoreContext);
24
+ const store = React4.useContext(StoreContext);
25
25
  if (!store) throw new Error("useConversationsStore must be used within ConversationsPage");
26
26
  return useStore(store, selector);
27
27
  }
28
28
  function useConversationsConfig() {
29
- return React3.useContext(ConfigContext);
29
+ return React4.useContext(ConfigContext);
30
30
  }
31
31
 
32
32
  // src/types.ts
@@ -54,10 +54,10 @@ var CHANNEL_ACCENT = {
54
54
  webchat: { color: "#f59e0b", badge: "bg-[#f59e0b]/12 text-[#b45309] dark:text-[#fcd34d]" }
55
55
  };
56
56
  function useMediaQuery(query) {
57
- const [matches, setMatches] = React3.useState(
57
+ const [matches, setMatches] = React4.useState(
58
58
  () => typeof window !== "undefined" && window.matchMedia(query).matches
59
59
  );
60
- React3.useEffect(() => {
60
+ React4.useEffect(() => {
61
61
  if (typeof window === "undefined") return;
62
62
  const mql = window.matchMedia(query);
63
63
  const onChange = () => setMatches(mql.matches);
@@ -130,144 +130,6 @@ function dayLabel(iso) {
130
130
  if (dayDiff < 7) return d.toLocaleDateString([], { weekday: "long" });
131
131
  return d.toLocaleDateString([], { month: "short", day: "numeric", year: now.getFullYear() === d.getFullYear() ? void 0 : "numeric" });
132
132
  }
133
- var CHANNELS = ["whatsapp", "sms", "instagram", "email", "webchat"];
134
- function personHandleFor(channel, contact) {
135
- if (!contact) return "";
136
- if (channel === "email") return contact.email ?? "";
137
- if (channel === "sms" || channel === "whatsapp") return contact.phone ?? "";
138
- return "";
139
- }
140
- var HANDLE_LABEL_KEY = {
141
- whatsapp: "conversations.new.handleLabel.phone",
142
- sms: "conversations.new.handleLabel.phone",
143
- email: "conversations.new.handleLabel.email",
144
- instagram: "conversations.new.handleLabel.instagram",
145
- webchat: "conversations.new.handleLabel.webchat"
146
- };
147
- function NewConversationModal({
148
- open,
149
- onOpenChange
150
- }) {
151
- const t = useTranslation();
152
- const create = useConversationsStore((s) => s.create);
153
- const config = useConversationsConfig();
154
- const guardConversations = useLimitGuard("conversations_month");
155
- const [channel, setChannel] = React3.useState("whatsapp");
156
- const [contact, setContact] = React3.useState(null);
157
- const [typedHandle, setTypedHandle] = React3.useState("");
158
- const [creatingContact, setCreatingContact] = React3.useState(false);
159
- const [firstMessage, setFirstMessage] = React3.useState("");
160
- const [submitting, setSubmitting] = React3.useState(false);
161
- const [pickerKey, setPickerKey] = React3.useState(0);
162
- React3.useEffect(() => {
163
- if (open) {
164
- setChannel("whatsapp");
165
- setContact(null);
166
- setTypedHandle("");
167
- setCreatingContact(false);
168
- setFirstMessage("");
169
- setSubmitting(false);
170
- setPickerKey((k) => k + 1);
171
- }
172
- }, [open]);
173
- const derivedHandle = personHandleFor(channel, contact);
174
- const effectiveHandle = derivedHandle || typedHandle;
175
- const handleLabel = t(HANDLE_LABEL_KEY[channel]);
176
- const canSubmit = (contact?.name.trim().length ?? 0) > 0 && !submitting;
177
- async function handleSubmit(e) {
178
- e.preventDefault();
179
- if (!canSubmit) return;
180
- setSubmitting(true);
181
- try {
182
- if (await guardConversations() === "blocked") return;
183
- await create({
184
- channel,
185
- contactName: contact.name.trim(),
186
- contactPersonId: contact?.id,
187
- contactHandle: effectiveHandle.trim() || void 0,
188
- firstMessage: firstMessage.trim() || void 0
189
- });
190
- invalidateLimit("conversations_month");
191
- onOpenChange(false);
192
- } catch (err) {
193
- const message = err instanceof Error ? err.message : String(err);
194
- toast.error(t("conversations.new.createFailed"), { description: message });
195
- } finally {
196
- setSubmitting(false);
197
- }
198
- }
199
- return /* @__PURE__ */ jsx(Modal, { open, onOpenChange, children: /* @__PURE__ */ jsx(ModalContent, { size: "md", children: /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-4", children: [
200
- /* @__PURE__ */ jsx("h2", { className: "text-base font-semibold text-foreground", children: t("conversations.new.title") }),
201
- /* @__PURE__ */ jsxs("div", { children: [
202
- /* @__PURE__ */ jsx("label", { className: "mb-1.5 block text-xs font-medium text-muted-foreground", children: t("conversations.new.channel") }),
203
- /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5", children: CHANNELS.map((ch) => {
204
- const Icon = CHANNEL_ICON[ch];
205
- const active = channel === ch;
206
- return /* @__PURE__ */ jsxs(
207
- "button",
208
- {
209
- type: "button",
210
- onClick: () => setChannel(ch),
211
- "aria-pressed": active,
212
- className: cn(
213
- "inline-flex items-center gap-1.5 rounded-full px-3 py-1.5 text-xs font-medium transition-colors",
214
- active ? "text-white" : "bg-muted text-muted-foreground hover:bg-muted/70"
215
- ),
216
- style: active ? { backgroundColor: CHANNEL_ACCENT[ch].color } : void 0,
217
- children: [
218
- /* @__PURE__ */ jsx(Icon, { className: "h-3 w-3" }),
219
- CHANNEL_LABELS[ch]
220
- ]
221
- },
222
- ch
223
- );
224
- }) })
225
- ] }),
226
- /* @__PURE__ */ jsx(
227
- ContactPicker,
228
- {
229
- value: contact,
230
- onChange: setContact,
231
- kind: config.contactKind,
232
- extensionTable: config.contactExtensionTable,
233
- lookup: config.contactLookup,
234
- allowFreeText: true,
235
- onCreatingChange: setCreatingContact,
236
- autoFocus: true,
237
- label: t("conversations.new.contactName"),
238
- placeholder: t("conversations.new.contactNamePlaceholder"),
239
- secondaryText: derivedHandle || void 0,
240
- handleField: {
241
- label: handleLabel,
242
- derived: derivedHandle || void 0,
243
- value: typedHandle,
244
- onChange: setTypedHandle,
245
- fieldLabel: t("conversations.new.handle"),
246
- placeholder: t("conversations.new.handlePlaceholder")
247
- }
248
- },
249
- pickerKey
250
- ),
251
- /* @__PURE__ */ jsxs("div", { children: [
252
- /* @__PURE__ */ jsx("label", { htmlFor: "conv-first-message", className: "mb-1.5 block text-xs font-medium text-muted-foreground", children: t("conversations.new.firstMessage") }),
253
- /* @__PURE__ */ jsx(
254
- "textarea",
255
- {
256
- id: "conv-first-message",
257
- value: firstMessage,
258
- onChange: (e) => setFirstMessage(e.target.value),
259
- rows: 3,
260
- placeholder: t("conversations.new.firstMessagePlaceholder"),
261
- 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"
262
- }
263
- )
264
- ] }),
265
- /* @__PURE__ */ jsxs("div", { className: "mt-1 flex justify-end gap-2", children: [
266
- /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", onClick: () => onOpenChange(false), children: t("conversations.new.cancel") }),
267
- /* @__PURE__ */ jsx(Button, { type: "submit", disabled: !canSubmit, children: submitting ? t("conversations.new.creating") : t("conversations.new.create") })
268
- ] })
269
- ] }) }) });
270
- }
271
133
  var FILTERS = ["all", "whatsapp", "sms", "instagram", "email", "webchat"];
272
134
  function ConversationList({ className }) {
273
135
  const t = useTranslation();
@@ -281,25 +143,8 @@ function ConversationList({ className }) {
281
143
  setChannelFilter,
282
144
  setSearch
283
145
  } = useConversationsStore((s) => s);
284
- const [newOpen, setNewOpen] = React3.useState(false);
285
146
  return /* @__PURE__ */ jsxs("aside", { className: cn("w-full shrink-0 flex-col border-r border-border bg-card lg:w-[320px]", className), children: [
286
147
  /* @__PURE__ */ jsxs("div", { className: "border-b border-border px-3 py-3", children: [
287
- /* @__PURE__ */ jsxs("div", { className: "mb-2 flex items-center justify-between gap-2", children: [
288
- /* @__PURE__ */ jsx("span", { className: "text-sm font-semibold text-foreground", children: t("conversations.title") }),
289
- /* @__PURE__ */ jsx(PermissionGate, { feature: "conversations", action: "create", children: /* @__PURE__ */ jsxs(
290
- Button,
291
- {
292
- size: "sm",
293
- onClick: () => setNewOpen(true),
294
- "aria-label": t("conversations.list.new"),
295
- "data-testid": "conversations-new",
296
- children: [
297
- /* @__PURE__ */ jsx(Plus, { className: "h-3.5 w-3.5 sm:mr-1" }),
298
- /* @__PURE__ */ jsx("span", { className: "hidden sm:inline", children: t("conversations.list.new") })
299
- ]
300
- }
301
- ) })
302
- ] }),
303
148
  /* @__PURE__ */ jsxs("div", { className: "relative", children: [
304
149
  /* @__PURE__ */ jsx(Search, { className: "pointer-events-none absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" }),
305
150
  /* @__PURE__ */ jsx(
@@ -373,8 +218,7 @@ function ConversationList({ className }) {
373
218
  c.id
374
219
  );
375
220
  })
376
- ] }),
377
- /* @__PURE__ */ jsx(NewConversationModal, { open: newOpen, onOpenChange: setNewOpen })
221
+ ] })
378
222
  ] });
379
223
  }
380
224
  function buildRows(messages) {
@@ -397,9 +241,9 @@ function buildRows(messages) {
397
241
  function MessageThread({ selected, onTogglePanel, panelOpen, onBack, className }) {
398
242
  const t = useTranslation();
399
243
  const { messages, sending, send, setStatus } = useConversationsStore((s) => s);
400
- const [draft, setDraft] = React3.useState("");
401
- const threadRef = React3.useRef(null);
402
- React3.useEffect(() => {
244
+ const [draft, setDraft] = React4.useState("");
245
+ const threadRef = React4.useRef(null);
246
+ React4.useEffect(() => {
403
247
  threadRef.current?.scrollTo({ top: threadRef.current.scrollHeight, behavior: "smooth" });
404
248
  }, [messages.length, selected.id]);
405
249
  async function handleSend() {
@@ -409,7 +253,7 @@ function MessageThread({ selected, onTogglePanel, panelOpen, onBack, className }
409
253
  await send(body);
410
254
  }
411
255
  const accent = CHANNEL_ACCENT[selected.channel];
412
- const rows = React3.useMemo(() => buildRows(messages), [messages]);
256
+ const rows = React4.useMemo(() => buildRows(messages), [messages]);
413
257
  return /* @__PURE__ */ jsxs("section", { className: cn("flex min-w-0 flex-1 flex-col bg-muted/20", className), children: [
414
258
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 border-b border-border bg-card px-3 py-2.5 md:px-5", children: [
415
259
  /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-2 md:gap-3", children: [
@@ -556,52 +400,212 @@ function ContactPanel({ contact, onClose, className }) {
556
400
  /* @__PURE__ */ jsx(Section, { icon: Link2, title: t("conversations.contact.linkedRecords"), children: /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("conversations.contact.noLinkedRecords") }) })
557
401
  ] });
558
402
  }
403
+ var CHANNELS = ["whatsapp", "sms", "instagram", "email", "webchat"];
404
+ function personHandleFor(channel, contact) {
405
+ if (!contact) return "";
406
+ if (channel === "email") return contact.email ?? "";
407
+ if (channel === "sms" || channel === "whatsapp") return contact.phone ?? "";
408
+ return "";
409
+ }
410
+ var HANDLE_LABEL_KEY = {
411
+ whatsapp: "conversations.new.handleLabel.phone",
412
+ sms: "conversations.new.handleLabel.phone",
413
+ email: "conversations.new.handleLabel.email",
414
+ instagram: "conversations.new.handleLabel.instagram",
415
+ webchat: "conversations.new.handleLabel.webchat"
416
+ };
417
+ function NewConversationPanel({
418
+ open,
419
+ onOpenChange
420
+ }) {
421
+ const t = useTranslation();
422
+ const create = useConversationsStore((s) => s.create);
423
+ const config = useConversationsConfig();
424
+ const guardConversations = useLimitGuard("conversations_month");
425
+ const [channel, setChannel] = React4.useState("whatsapp");
426
+ const [contact, setContact] = React4.useState(null);
427
+ const [typedHandle, setTypedHandle] = React4.useState("");
428
+ const [creatingContact, setCreatingContact] = React4.useState(false);
429
+ const [firstMessage, setFirstMessage] = React4.useState("");
430
+ const [submitting, setSubmitting] = React4.useState(false);
431
+ const [pickerKey, setPickerKey] = React4.useState(0);
432
+ React4.useEffect(() => {
433
+ if (open) {
434
+ setChannel("whatsapp");
435
+ setContact(null);
436
+ setTypedHandle("");
437
+ setCreatingContact(false);
438
+ setFirstMessage("");
439
+ setSubmitting(false);
440
+ setPickerKey((k) => k + 1);
441
+ }
442
+ }, [open]);
443
+ const derivedHandle = personHandleFor(channel, contact);
444
+ const effectiveHandle = derivedHandle || typedHandle;
445
+ const handleLabel = t(HANDLE_LABEL_KEY[channel]);
446
+ const canSubmit = (contact?.name.trim().length ?? 0) > 0 && !submitting;
447
+ async function handleSubmit(e) {
448
+ e.preventDefault();
449
+ if (!canSubmit) return;
450
+ setSubmitting(true);
451
+ try {
452
+ if (await guardConversations() === "blocked") return;
453
+ await create({
454
+ channel,
455
+ contactName: contact.name.trim(),
456
+ contactPersonId: contact?.id,
457
+ contactHandle: effectiveHandle.trim() || void 0,
458
+ firstMessage: firstMessage.trim() || void 0
459
+ });
460
+ invalidateLimit("conversations_month");
461
+ onOpenChange(false);
462
+ } catch (err) {
463
+ const message = err instanceof Error ? err.message : String(err);
464
+ toast.error(t("conversations.new.createFailed"), { description: message });
465
+ } finally {
466
+ setSubmitting(false);
467
+ }
468
+ }
469
+ return /* @__PURE__ */ jsx(
470
+ RightRailPage,
471
+ {
472
+ title: t("conversations.new.title"),
473
+ open,
474
+ onClose: () => onOpenChange(false),
475
+ width: "max-w-md",
476
+ children: /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, className: "flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto px-5 py-4", children: [
477
+ /* @__PURE__ */ jsxs("div", { children: [
478
+ /* @__PURE__ */ jsx("label", { className: "mb-1.5 block text-xs font-medium text-muted-foreground", children: t("conversations.new.channel") }),
479
+ /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5", children: CHANNELS.map((ch) => {
480
+ const Icon = CHANNEL_ICON[ch];
481
+ const active = channel === ch;
482
+ return /* @__PURE__ */ jsxs(
483
+ "button",
484
+ {
485
+ type: "button",
486
+ onClick: () => setChannel(ch),
487
+ "aria-pressed": active,
488
+ className: cn(
489
+ "inline-flex items-center gap-1.5 rounded-full px-3 py-1.5 text-xs font-medium transition-colors",
490
+ active ? "text-white" : "bg-muted text-muted-foreground hover:bg-muted/70"
491
+ ),
492
+ style: active ? { backgroundColor: CHANNEL_ACCENT[ch].color } : void 0,
493
+ children: [
494
+ /* @__PURE__ */ jsx(Icon, { className: "h-3 w-3" }),
495
+ CHANNEL_LABELS[ch]
496
+ ]
497
+ },
498
+ ch
499
+ );
500
+ }) })
501
+ ] }),
502
+ /* @__PURE__ */ jsx(
503
+ ContactPicker,
504
+ {
505
+ value: contact,
506
+ onChange: setContact,
507
+ kind: config.contactKind,
508
+ lookup: config.contactLookup,
509
+ createEntityDef: config.contactEntityDef,
510
+ allowFreeText: true,
511
+ onCreatingChange: setCreatingContact,
512
+ autoFocus: true,
513
+ label: t("conversations.new.contactName"),
514
+ placeholder: t("conversations.new.contactNamePlaceholder"),
515
+ secondaryText: derivedHandle || void 0,
516
+ handleField: {
517
+ label: handleLabel,
518
+ derived: derivedHandle || void 0,
519
+ value: typedHandle,
520
+ onChange: setTypedHandle,
521
+ fieldLabel: t("conversations.new.handle"),
522
+ placeholder: t("conversations.new.handlePlaceholder")
523
+ }
524
+ },
525
+ pickerKey
526
+ ),
527
+ /* @__PURE__ */ jsxs("div", { children: [
528
+ /* @__PURE__ */ jsx("label", { htmlFor: "conv-first-message", className: "mb-1.5 block text-xs font-medium text-muted-foreground", children: t("conversations.new.firstMessage") }),
529
+ /* @__PURE__ */ jsx(
530
+ "textarea",
531
+ {
532
+ id: "conv-first-message",
533
+ value: firstMessage,
534
+ onChange: (e) => setFirstMessage(e.target.value),
535
+ rows: 3,
536
+ placeholder: t("conversations.new.firstMessagePlaceholder"),
537
+ 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"
538
+ }
539
+ )
540
+ ] }),
541
+ /* @__PURE__ */ jsx("div", { className: "mt-auto pt-2", children: /* @__PURE__ */ jsx(Button, { type: "submit", disabled: !canSubmit, className: "w-full", children: submitting ? t("conversations.new.creating") : t("conversations.new.create") }) })
542
+ ] })
543
+ }
544
+ );
545
+ }
559
546
  function InboxView() {
560
547
  const t = useTranslation();
561
548
  const { conversations, selectedId, deselect } = useConversationsStore((s) => s);
562
549
  const isWidePanel = useMediaQuery("(min-width: 1280px)");
563
- const [panelOpen, setPanelOpen] = React3.useState(false);
550
+ const [panelOpen, setPanelOpen] = React4.useState(false);
551
+ const [newOpen, setNewOpen] = React4.useState(false);
564
552
  const selected = conversations.find((c) => c.id === selectedId) ?? null;
565
- React3.useEffect(() => {
553
+ React4.useEffect(() => {
566
554
  setPanelOpen(isWidePanel);
567
555
  }, [isWidePanel]);
568
- return /* @__PURE__ */ jsxs("div", { className: "relative flex h-full min-h-0 overflow-hidden bg-background", children: [
569
- /* @__PURE__ */ jsx(ConversationList, { className: cn(selected ? "hidden lg:flex" : "flex") }),
570
- selected ? /* @__PURE__ */ jsx(
571
- MessageThread,
556
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
557
+ /* @__PURE__ */ jsx(PermissionGate, { feature: "conversations", action: "create", children: /* @__PURE__ */ jsx(PageHeaderActions, { children: /* @__PURE__ */ jsxs(
558
+ Button,
572
559
  {
573
- selected,
574
- panelOpen,
575
- onTogglePanel: () => setPanelOpen((v) => !v),
576
- onBack: deselect,
577
- className: "flex"
560
+ size: "sm",
561
+ onClick: () => setNewOpen(true),
562
+ "aria-label": t("conversations.list.new"),
563
+ "data-testid": "conversations-new",
564
+ children: [
565
+ /* @__PURE__ */ jsx(SquarePen, { className: "h-3.5 w-3.5 sm:mr-1.5" }),
566
+ /* @__PURE__ */ jsx("span", { className: "hidden sm:inline", children: t("conversations.list.new") })
567
+ ]
578
568
  }
579
- ) : /* @__PURE__ */ jsxs("section", { className: "hidden min-w-0 flex-1 flex-col items-center justify-center bg-muted/20 text-muted-foreground lg:flex", children: [
580
- /* @__PURE__ */ jsx(MessageSquare, { className: "h-9 w-9" }),
581
- /* @__PURE__ */ jsx("p", { className: "mt-2 text-sm", children: t("conversations.empty.select") })
582
- ] }),
583
- selected && panelOpen && /* @__PURE__ */ jsxs(Fragment, { children: [
584
- /* @__PURE__ */ jsx(
585
- "button",
586
- {
587
- className: "absolute inset-0 z-10 bg-black/40 xl:hidden",
588
- "aria-label": "Close details",
589
- onClick: () => setPanelOpen(false)
590
- }
591
- ),
592
- /* @__PURE__ */ jsx(
593
- ContactPanel,
569
+ ) }) }),
570
+ /* @__PURE__ */ jsx(NewConversationPanel, { open: newOpen, onOpenChange: setNewOpen }),
571
+ /* @__PURE__ */ jsxs("div", { className: "relative flex h-full min-h-0 overflow-hidden bg-background", children: [
572
+ /* @__PURE__ */ jsx(ConversationList, { className: cn(selected ? "hidden lg:flex" : "flex") }),
573
+ selected ? /* @__PURE__ */ jsx(
574
+ MessageThread,
594
575
  {
595
- contact: selected,
596
- onClose: () => setPanelOpen(false),
597
- 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"
576
+ selected,
577
+ panelOpen,
578
+ onTogglePanel: () => setPanelOpen((v) => !v),
579
+ onBack: deselect,
580
+ className: "flex"
598
581
  }
599
- )
582
+ ) : /* @__PURE__ */ jsxs("section", { className: "hidden min-w-0 flex-1 flex-col items-center justify-center bg-muted/20 text-muted-foreground lg:flex", children: [
583
+ /* @__PURE__ */ jsx(MessageSquare, { className: "h-9 w-9" }),
584
+ /* @__PURE__ */ jsx("p", { className: "mt-2 text-sm", children: t("conversations.empty.select") })
585
+ ] }),
586
+ selected && panelOpen && /* @__PURE__ */ jsxs(Fragment, { children: [
587
+ /* @__PURE__ */ jsx(
588
+ "button",
589
+ {
590
+ className: "absolute inset-0 z-10 bg-black/40 xl:hidden",
591
+ "aria-label": "Close details",
592
+ onClick: () => setPanelOpen(false)
593
+ }
594
+ ),
595
+ /* @__PURE__ */ jsx(
596
+ ContactPanel,
597
+ {
598
+ contact: selected,
599
+ onClose: () => setPanelOpen(false),
600
+ 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"
601
+ }
602
+ )
603
+ ] })
600
604
  ] })
601
605
  ] });
602
606
  }
603
607
  function ConversationsPage({ store, config }) {
604
- React3.useEffect(() => {
608
+ React4.useEffect(() => {
605
609
  void store.getState().load();
606
610
  }, []);
607
611
  return /* @__PURE__ */ jsx(ConversationsContextProvider, { store, config, children: /* @__PURE__ */ jsx(InboxView, {}) });
@@ -1384,13 +1388,14 @@ function createConversationsPlugin(options) {
1384
1388
  const store = createConversationsStore(provider);
1385
1389
  const config = {
1386
1390
  contactKind: options?.contactKind ?? "contact",
1387
- contactExtensionTable: options?.contactExtensionTable,
1388
- contactLookup: options?.contactLookup
1391
+ contactLookup: options?.contactLookup,
1392
+ contactEntityDef: options?.contactEntityDef
1389
1393
  };
1390
- const PageComponent = () => React3.createElement(ConversationsPage, { store, config });
1394
+ const PageComponent = () => React4.createElement(ConversationsPage, { store, config });
1391
1395
  PageComponent.displayName = "ConversationsPage";
1392
1396
  return {
1393
1397
  id: "conversations",
1398
+ defaultAgentRole: "frontdesk",
1394
1399
  name: options?.navLabel ?? "Conversations",
1395
1400
  icon: "MessageCircle",
1396
1401
  version: "1.0.0",