@elevasis/ui 2.23.0 → 2.24.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.
Files changed (57) hide show
  1. package/dist/app/index.d.ts +2915 -0
  2. package/dist/app/index.js +5 -4
  3. package/dist/{chunk-3HEUGBOT.js → chunk-2WZ635SS.js} +2 -2
  4. package/dist/{chunk-D3KQAABP.js → chunk-4NWNS7TX.js} +1 -1
  5. package/dist/{chunk-7PGEGSUM.js → chunk-FUEXGRFR.js} +2 -2
  6. package/dist/{chunk-6IA2OMAE.js → chunk-HC2KV6BU.js} +9 -0
  7. package/dist/{chunk-YU6MBDVO.js → chunk-KCJ6VATY.js} +4 -68
  8. package/dist/{chunk-FXWETLEB.js → chunk-KLFIJDTD.js} +1 -1
  9. package/dist/{chunk-PXGSJNBH.js → chunk-M2HWJY6O.js} +704 -375
  10. package/dist/{chunk-N6WLOWOD.js → chunk-MTR6AN2C.js} +3 -12
  11. package/dist/chunk-OWHQ65EQ.js +211 -0
  12. package/dist/{chunk-XOTJNW4Q.js → chunk-QIW6OCEI.js} +18 -1
  13. package/dist/{chunk-GUJUK6EH.js → chunk-QULLZ5PE.js} +172 -2
  14. package/dist/{chunk-QZJM3RYI.js → chunk-SNHGSCKH.js} +1 -1
  15. package/dist/{chunk-LVUCBY7X.js → chunk-UDJE54WN.js} +85 -3
  16. package/dist/{chunk-EPV7NU2E.js → chunk-VGNAV3TH.js} +385 -188
  17. package/dist/{chunk-PTUOINQ2.js → chunk-YBZT7MJR.js} +3 -3
  18. package/dist/{chunk-SQ5JGELM.js → chunk-ZDKQNQ4X.js} +19 -1
  19. package/dist/components/index.d.ts +488 -452
  20. package/dist/components/index.js +58 -22
  21. package/dist/components/navigation/index.js +2 -2
  22. package/dist/features/auth/index.d.ts +463 -377
  23. package/dist/features/crm/index.d.ts +459 -379
  24. package/dist/features/crm/index.js +8 -8
  25. package/dist/features/dashboard/index.js +8 -8
  26. package/dist/features/delivery/index.d.ts +457 -371
  27. package/dist/features/delivery/index.js +8 -8
  28. package/dist/features/lead-gen/index.d.ts +213 -65
  29. package/dist/features/lead-gen/index.js +9 -8
  30. package/dist/features/monitoring/index.js +9 -9
  31. package/dist/features/monitoring/requests/index.js +7 -7
  32. package/dist/features/operations/index.js +11 -10
  33. package/dist/features/settings/index.d.ts +463 -377
  34. package/dist/features/settings/index.js +9 -9
  35. package/dist/hooks/delivery/index.d.ts +457 -371
  36. package/dist/hooks/index.d.ts +936 -718
  37. package/dist/hooks/index.js +7 -7
  38. package/dist/hooks/published.d.ts +936 -718
  39. package/dist/hooks/published.js +7 -7
  40. package/dist/index.d.ts +1313 -1027
  41. package/dist/index.js +8 -8
  42. package/dist/initialization/index.d.ts +463 -377
  43. package/dist/organization/index.d.ts +11 -1
  44. package/dist/organization/index.js +2 -2
  45. package/dist/profile/index.d.ts +463 -377
  46. package/dist/provider/index.d.ts +3132 -169
  47. package/dist/provider/index.js +6 -6
  48. package/dist/provider/published.d.ts +3098 -168
  49. package/dist/provider/published.js +3 -3
  50. package/dist/supabase/index.d.ts +559 -389
  51. package/dist/test-utils/index.d.ts +21 -1
  52. package/dist/test-utils/index.js +13 -4
  53. package/dist/theme/index.js +2 -2
  54. package/dist/types/index.d.ts +463 -377
  55. package/package.json +4 -4
  56. package/src/test-utils/README.md +2 -0
  57. /package/dist/{chunk-ZBCTB5CA.js → chunk-EIOJNUPL.js} +0 -0
@@ -4,16 +4,18 @@ import { SubshellNavItem } from './chunk-CEWTOKE7.js';
4
4
  import { SubshellSidebarSection } from './chunk-IIMU5YAJ.js';
5
5
  import { FilterBar } from './chunk-PDHTXPSF.js';
6
6
  import { CustomModal } from './chunk-KVJ3LFH2.js';
7
- import { useDealTasksDue, useDealsLookup, useCreateDealTask, useDealsSummary, useDeleteDeal, usePaginationState, useDeals, useTableSort, sortData, useTableSelection, useDealDetail, useCompany } from './chunk-GUJUK6EH.js';
7
+ import { useExecuteAction, useDealTasksDue, useDealsLookup, useCreateDealTask, useDealsSummary, useDeleteDeal, usePaginationState, useDeals, useTableSort, sortData, useTableSelection, useDealDetail, useCompany } from './chunk-QULLZ5PE.js';
8
+ import { useCrmActions, deriveActions } from './chunk-UDJE54WN.js';
8
9
  import { SubshellContentContainer } from './chunk-TKAYX2SP.js';
9
10
  import { CenteredErrorState, CardHeader, PageTitleCaption, EmptyState, ActivityTimeline } from './chunk-XUYBOO32.js';
10
11
  import { useRouterContext } from './chunk-Q7DJKLEN.js';
11
12
  import { PAGE_SIZE_DEFAULT, formatTimeAgo } from './chunk-SGXXJE52.js';
12
13
  import { useElevasisServices } from './chunk-IRW7JMQ4.js';
13
- import { Box, Stack, Group, Text, Badge, Center, Loader, UnstyledButton, Button, Modal, Title, Select, TextInput, Textarea, Paper, Alert, SimpleGrid, Table, Checkbox, Pagination, ActionIcon, Tabs, Card, Code, Divider, Anchor } from '@mantine/core';
14
- import { IconLayoutGrid, IconColumns, IconFileInvoice, IconAddressBook, IconTrophy, IconClockExclamation, IconUser, IconPlus, IconChecklist, IconAlertCircle, IconHistory, IconSearch, IconTargetArrow, IconAlertTriangle, IconArrowLeft, IconFileText, IconTrash, IconX, IconBuilding, IconCheckbox, IconCalendar, IconMail, IconPhone, IconArrowRight, IconNote } from '@tabler/icons-react';
14
+ import { Button, Modal, Stack, NumberInput, Switch, Select, Textarea, TextInput, Group, Alert, Text, Box, Badge, Center, Loader, UnstyledButton, Title, Paper, Table, SimpleGrid, Checkbox, Pagination, ActionIcon, Tabs, Card, Code, Divider, CopyButton, Tooltip } from '@mantine/core';
15
+ import { IconLayoutGrid, IconColumns, IconFileInvoice, IconAddressBook, IconAlertCircle, IconTrophy, IconClockExclamation, IconUser, IconPlus, IconChecklist, IconHistory, IconSearch, IconTargetArrow, IconAlertTriangle, IconArrowLeft, IconFileText, IconTrash, IconX, IconBuilding, IconCheckbox, IconCalendar, IconMail, IconPhone, IconArrowRight, IconNote, IconCheck, IconCopy } from '@tabler/icons-react';
15
16
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
16
17
  import { useState, useMemo, useEffect } from 'react';
18
+ import { useForm } from '@mantine/form';
17
19
  import { useQuery } from '@tanstack/react-query';
18
20
  import { useNavigate } from '@tanstack/react-router';
19
21
 
@@ -43,6 +45,282 @@ var SAVED_VIEW_PRESETS = [
43
45
  urlFilters: { stage: "closed_won" }
44
46
  }
45
47
  ];
48
+ function ActionButton({ action, dealId }) {
49
+ const executeAction = useExecuteAction({ dealId });
50
+ return /* @__PURE__ */ jsx(
51
+ Button,
52
+ {
53
+ variant: "light",
54
+ size: "sm",
55
+ loading: executeAction.isPending,
56
+ onClick: () => executeAction.mutate({ key: action.key }),
57
+ children: action.label
58
+ }
59
+ );
60
+ }
61
+ function getType(schema) {
62
+ const zodSchema = schema;
63
+ return zodSchema._def?.type ?? zodSchema._def?.typeName ?? "";
64
+ }
65
+ function unwrapField(schema) {
66
+ const type = getType(schema);
67
+ if (type === "optional" || type === "ZodOptional") {
68
+ const inner = schema._def?.innerType;
69
+ if (!inner) return { schema, required: false, nullable: false, defaultValue: void 0 };
70
+ return { ...unwrapField(inner), required: false };
71
+ }
72
+ if (type === "nullable" || type === "ZodNullable") {
73
+ const inner = schema._def?.innerType;
74
+ if (!inner) return { schema, required: true, nullable: true, defaultValue: null };
75
+ return { ...unwrapField(inner), nullable: true };
76
+ }
77
+ if (type === "default" || type === "ZodDefault") {
78
+ const def = schema._def;
79
+ const inner = def?.innerType;
80
+ const defaultValue = typeof def?.defaultValue === "function" ? def.defaultValue() : def?.defaultValue;
81
+ if (!inner) return { schema, required: false, nullable: false, defaultValue };
82
+ return { ...unwrapField(inner), required: false, defaultValue };
83
+ }
84
+ return { schema, required: true, nullable: false, defaultValue: void 0 };
85
+ }
86
+ function getObjectShape(schema) {
87
+ const zodSchema = schema;
88
+ const type = getType(schema);
89
+ if (type !== "object" && type !== "ZodObject") {
90
+ return null;
91
+ }
92
+ const shape = zodSchema.shape;
93
+ if (!shape || typeof shape !== "object" || Array.isArray(shape)) {
94
+ return null;
95
+ }
96
+ return shape;
97
+ }
98
+ function getEnumValues(schema) {
99
+ const zodSchema = schema;
100
+ const values = zodSchema.options ?? zodSchema._def?.values ?? Object.values(zodSchema._def?.entries ?? {});
101
+ if (!Array.isArray(values) || values.some((value) => typeof value !== "string")) {
102
+ return null;
103
+ }
104
+ return values;
105
+ }
106
+ function labelForField(name) {
107
+ return name.replace(/[_-]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/^./, (s) => s.toUpperCase());
108
+ }
109
+ function isLongTextField(name) {
110
+ return ["body", "message", "reply", "replyBody", "emailBody"].includes(name);
111
+ }
112
+ function initialValueFor(field, defaultValue) {
113
+ if (defaultValue !== void 0) {
114
+ if (field.kind === "date" && defaultValue instanceof Date) {
115
+ return defaultValue.toISOString().slice(0, 10);
116
+ }
117
+ return defaultValue;
118
+ }
119
+ switch (field.kind) {
120
+ case "boolean":
121
+ return false;
122
+ case "number":
123
+ return field.required ? 0 : "";
124
+ case "enum":
125
+ return field.required ? field.enumValues?.[0] ?? "" : "";
126
+ case "date":
127
+ case "dateString":
128
+ case "string":
129
+ return "";
130
+ }
131
+ }
132
+ function describeSchema(schema) {
133
+ if (!schema) {
134
+ return { supported: false, reason: "This action does not define a payload schema." };
135
+ }
136
+ const shape = getObjectShape(schema);
137
+ if (!shape) {
138
+ return { supported: false, reason: "This action requires a payload schema that is not a flat object." };
139
+ }
140
+ const fields = [];
141
+ const initialValues = {};
142
+ for (const [name, rawFieldSchema] of Object.entries(shape)) {
143
+ const { schema: fieldSchema, required, nullable, defaultValue } = unwrapField(rawFieldSchema);
144
+ const type = getType(fieldSchema);
145
+ const zodField = fieldSchema;
146
+ const stringFormat = zodField._def?.format;
147
+ let descriptor = null;
148
+ if (type === "string" || type === "ZodString") {
149
+ descriptor = {
150
+ name,
151
+ label: labelForField(name),
152
+ kind: stringFormat === "date" ? "dateString" : "string",
153
+ schema: rawFieldSchema,
154
+ required,
155
+ nullable
156
+ };
157
+ } else if (type === "number" || type === "ZodNumber") {
158
+ descriptor = { name, label: labelForField(name), kind: "number", schema: rawFieldSchema, required, nullable };
159
+ } else if (type === "boolean" || type === "ZodBoolean") {
160
+ descriptor = { name, label: labelForField(name), kind: "boolean", schema: rawFieldSchema, required, nullable };
161
+ } else if (type === "enum" || type === "ZodEnum") {
162
+ const enumValues = getEnumValues(fieldSchema);
163
+ if (!enumValues) {
164
+ return { supported: false, reason: `Field "${name}" uses an enum shape that is not supported by this form.` };
165
+ }
166
+ descriptor = {
167
+ name,
168
+ label: labelForField(name),
169
+ kind: "enum",
170
+ schema: rawFieldSchema,
171
+ required,
172
+ nullable,
173
+ enumValues
174
+ };
175
+ } else if (type === "date" || type === "ZodDate") {
176
+ descriptor = { name, label: labelForField(name), kind: "date", schema: rawFieldSchema, required, nullable };
177
+ }
178
+ if (!descriptor) {
179
+ return {
180
+ supported: false,
181
+ reason: `Field "${name}" is not supported. CRM action forms currently support only flat string, number, boolean, enum, and date fields.`
182
+ };
183
+ }
184
+ const field = { ...descriptor, defaultValue: initialValueFor(descriptor, defaultValue) };
185
+ fields.push(field);
186
+ initialValues[name] = field.defaultValue;
187
+ }
188
+ return { supported: true, fields, initialValues };
189
+ }
190
+ function normalizeValue(field, value) {
191
+ const isEmpty = value === "" || value === void 0 || value === null;
192
+ if (isEmpty) {
193
+ if (!field.required) return void 0;
194
+ if (field.nullable) return null;
195
+ return value;
196
+ }
197
+ if (field.kind === "date") {
198
+ const acceptsString = (field.schema._def?.coerce ?? false) === true;
199
+ return acceptsString ? value : new Date(String(value));
200
+ }
201
+ return value;
202
+ }
203
+ function buildPayload(fields, values) {
204
+ return fields.reduce((payload, field) => {
205
+ const value = normalizeValue(field, values[field.name]);
206
+ if (value !== void 0 || field.required) {
207
+ payload[field.name] = value;
208
+ }
209
+ return payload;
210
+ }, {});
211
+ }
212
+ function ActionFormButton({ action, dealId }) {
213
+ const [opened, setOpened] = useState(false);
214
+ const executeAction = useExecuteAction({ dealId });
215
+ const schemaDescription = describeSchema(action.payloadSchema);
216
+ const form = useForm({
217
+ initialValues: schemaDescription.supported ? schemaDescription.initialValues : {}
218
+ });
219
+ const handleClose = () => {
220
+ setOpened(false);
221
+ form.reset();
222
+ form.clearErrors();
223
+ };
224
+ const handleSubmit = form.onSubmit(async (values) => {
225
+ if (!schemaDescription.supported || !action.payloadSchema) return;
226
+ form.clearErrors();
227
+ const payload = buildPayload(schemaDescription.fields, values);
228
+ const parsed = action.payloadSchema.safeParse(payload);
229
+ if (!parsed.success) {
230
+ const formErrors = {};
231
+ for (const issue of parsed.error.issues) {
232
+ const fieldName = issue.path[0];
233
+ if (typeof fieldName === "string" && schemaDescription.fields.some((field) => field.name === fieldName)) {
234
+ formErrors[fieldName] = issue.message;
235
+ }
236
+ }
237
+ form.setErrors(formErrors);
238
+ return;
239
+ }
240
+ await executeAction.mutateAsync({ key: action.key, payload: parsed.data });
241
+ handleClose();
242
+ });
243
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
244
+ /* @__PURE__ */ jsx(Button, { variant: "light", size: "sm", onClick: () => setOpened(true), children: action.label }),
245
+ /* @__PURE__ */ jsx(Modal, { opened, onClose: handleClose, title: action.label, size: "xl", children: schemaDescription.supported ? /* @__PURE__ */ jsx("form", { onSubmit: handleSubmit, children: /* @__PURE__ */ jsxs(Stack, { gap: "md", children: [
246
+ schemaDescription.fields.map((field) => {
247
+ if (field.kind === "number") {
248
+ return /* @__PURE__ */ jsx(
249
+ NumberInput,
250
+ {
251
+ label: field.label,
252
+ required: field.required,
253
+ disabled: executeAction.isPending,
254
+ error: form.errors[field.name],
255
+ ...form.getInputProps(field.name)
256
+ },
257
+ field.name
258
+ );
259
+ }
260
+ if (field.kind === "boolean") {
261
+ return /* @__PURE__ */ jsx(
262
+ Switch,
263
+ {
264
+ label: field.label,
265
+ disabled: executeAction.isPending,
266
+ error: form.errors[field.name],
267
+ ...form.getInputProps(field.name, { type: "checkbox" })
268
+ },
269
+ field.name
270
+ );
271
+ }
272
+ if (field.kind === "enum") {
273
+ return /* @__PURE__ */ jsx(
274
+ Select,
275
+ {
276
+ label: field.label,
277
+ required: field.required,
278
+ disabled: executeAction.isPending,
279
+ data: field.enumValues ?? [],
280
+ error: form.errors[field.name],
281
+ ...form.getInputProps(field.name)
282
+ },
283
+ field.name
284
+ );
285
+ }
286
+ if (field.kind === "string" && isLongTextField(field.name)) {
287
+ return /* @__PURE__ */ jsx(
288
+ Textarea,
289
+ {
290
+ label: field.label,
291
+ required: field.required,
292
+ disabled: executeAction.isPending,
293
+ error: form.errors[field.name],
294
+ autosize: true,
295
+ minRows: 6,
296
+ ...form.getInputProps(field.name)
297
+ },
298
+ field.name
299
+ );
300
+ }
301
+ return /* @__PURE__ */ jsx(
302
+ TextInput,
303
+ {
304
+ label: field.label,
305
+ type: field.kind === "date" || field.kind === "dateString" ? "date" : "text",
306
+ required: field.required,
307
+ disabled: executeAction.isPending,
308
+ error: form.errors[field.name],
309
+ ...form.getInputProps(field.name)
310
+ },
311
+ field.name
312
+ );
313
+ }),
314
+ /* @__PURE__ */ jsxs(Group, { justify: "flex-end", gap: "sm", children: [
315
+ /* @__PURE__ */ jsx(Button, { variant: "default", onClick: handleClose, disabled: executeAction.isPending, children: "Cancel" }),
316
+ /* @__PURE__ */ jsx(Button, { type: "submit", loading: executeAction.isPending, children: action.label })
317
+ ] })
318
+ ] }) }) : /* @__PURE__ */ jsxs(Stack, { gap: "md", children: [
319
+ /* @__PURE__ */ jsx(Alert, { color: "yellow", variant: "light", icon: /* @__PURE__ */ jsx(IconAlertCircle, { size: 16 }), title: "Unsupported action form", children: /* @__PURE__ */ jsx(Text, { size: "sm", children: schemaDescription.reason }) }),
320
+ /* @__PURE__ */ jsx(Button, { variant: "default", onClick: handleClose, disabled: executeAction.isPending, children: "Close" })
321
+ ] }) })
322
+ ] });
323
+ }
46
324
  var KIND_ICONS = {
47
325
  call: IconPhone,
48
326
  email: IconMail,
@@ -63,7 +341,7 @@ function formatDueLabel(dueAt) {
63
341
  return `in ${diffDays}d`;
64
342
  }
65
343
  function TaskRow({ task, onClick }) {
66
- const KindIcon2 = KIND_ICONS[task.kind];
344
+ const KindIcon = KIND_ICONS[task.kind];
67
345
  const dueLabel = formatDueLabel(task.dueAt);
68
346
  const isOverdue = task.dueAt !== null && new Date(task.dueAt) < /* @__PURE__ */ new Date();
69
347
  return /* @__PURE__ */ jsx(
@@ -84,7 +362,7 @@ function TaskRow({ task, onClick }) {
84
362
  e.currentTarget.style.backgroundColor = "transparent";
85
363
  },
86
364
  children: /* @__PURE__ */ jsxs(Group, { gap: "xs", wrap: "nowrap", children: [
87
- /* @__PURE__ */ jsx(KindIcon2, { size: 14, style: { color: "var(--color-text-dimmed)", flexShrink: 0 } }),
365
+ /* @__PURE__ */ jsx(KindIcon, { size: 14, style: { color: "var(--color-text-dimmed)", flexShrink: 0 } }),
88
366
  /* @__PURE__ */ jsx(Text, { size: "xs", truncate: true, style: { flex: 1, minWidth: 0 }, children: task.title }),
89
367
  dueLabel && /* @__PURE__ */ jsx(Badge, { size: "xs", variant: "light", color: isOverdue ? "red" : "gray", style: { flexShrink: 0 }, children: dueLabel })
90
368
  ] })
@@ -381,11 +659,35 @@ function useRecentCrmActivity(opts) {
381
659
  error: query.error
382
660
  };
383
661
  }
384
- var currencyFormatter = new Intl.NumberFormat("en-US", {
385
- style: "currency",
386
- currency: "USD",
387
- maximumFractionDigits: 0
388
- });
662
+
663
+ // src/features/crm/pages/shared.ts
664
+ var DEAL_STAGE_COLORS = {
665
+ interested: "blue",
666
+ proposal: "yellow",
667
+ closing: "orange",
668
+ closed_won: "green",
669
+ closed_lost: "red",
670
+ nurturing: "grape"
671
+ };
672
+ var DEAL_STAGE_OPTIONS = [
673
+ { value: "interested", label: "Interested" },
674
+ { value: "proposal", label: "Proposal" },
675
+ { value: "closing", label: "Closing" },
676
+ { value: "closed_won", label: "Closed Won" },
677
+ { value: "closed_lost", label: "Closed Lost" },
678
+ { value: "nurturing", label: "Nurturing" }
679
+ ];
680
+ function formatDealStageLabel(stage) {
681
+ if (!stage) return "Unknown";
682
+ return stage.split("_").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
683
+ }
684
+ var BOOKING_PAGE_URL = "https://elevasis.io/inbound/book";
685
+ function buildBookingUrl(opts = {}) {
686
+ const params = new URLSearchParams();
687
+ if (opts.dealId) params.set("dealId", opts.dealId);
688
+ if (opts.contactId) params.set("contactId", opts.contactId);
689
+ return params.size > 0 ? `${BOOKING_PAGE_URL}?${params.toString()}` : BOOKING_PAGE_URL;
690
+ }
389
691
  var STAGE_LABELS = {
390
692
  interested: "Interested",
391
693
  proposal: "Proposal",
@@ -403,143 +705,54 @@ function PipelineFunnelWidget({ onStageClick, getDealValue }) {
403
705
  return /* @__PURE__ */ jsx(Paper, { withBorder: true, p: "md", children: /* @__PURE__ */ jsx(CenteredErrorState, { error, title: "Failed to load pipeline data" }) });
404
706
  }
405
707
  const totalDeals = data.reduce((sum, s) => sum + s.count, 0);
406
- const maxCount = Math.max(...data.map((s) => s.count), 1);
407
- if (totalDeals === 0) {
408
- return /* @__PURE__ */ jsxs(Paper, { withBorder: true, p: "md", children: [
409
- /* @__PURE__ */ jsx(CardHeader, { icon: /* @__PURE__ */ jsx(IconColumns, { size: 16 }), title: "Pipeline" }),
410
- /* @__PURE__ */ jsx(Center, { h: 200, children: /* @__PURE__ */ jsx(Alert, { icon: /* @__PURE__ */ jsx(IconAlertCircle, {}), color: "gray", variant: "light", children: "No deals in the pipeline yet" }) })
411
- ] });
412
- }
413
708
  return /* @__PURE__ */ jsxs(Paper, { withBorder: true, p: "md", children: [
414
709
  /* @__PURE__ */ jsx(
415
710
  CardHeader,
416
711
  {
417
712
  icon: /* @__PURE__ */ jsx(IconColumns, { size: 16 }),
418
713
  title: "Pipeline",
419
- subtitle: `${totalDeals} deal${totalDeals !== 1 ? "s" : ""} total`
714
+ subtitle: totalDeals === 0 ? "No deals in the pipeline yet" : `${totalDeals} deal${totalDeals !== 1 ? "s" : ""} total`
420
715
  }
421
716
  ),
422
- /* @__PURE__ */ jsx(Box, { children: PIPELINE_FUNNEL_ORDER.map((stage) => {
717
+ /* @__PURE__ */ jsx(Group, { gap: "lg", wrap: "nowrap", mt: "md", children: PIPELINE_FUNNEL_ORDER.map((stage) => {
423
718
  const summary = data.find((s) => s.stage === stage);
424
719
  const isEmpty = summary.count === 0;
425
- const barWidth = isEmpty ? 2 : Math.max(4, summary.count / maxCount * 100);
426
- return /* @__PURE__ */ jsx(
720
+ const stageColor = DEAL_STAGE_COLORS[stage] ?? "gray";
721
+ const accent = `var(--mantine-color-${stageColor}-6)`;
722
+ return /* @__PURE__ */ jsxs(
427
723
  Box,
428
724
  {
429
725
  onClick: () => onStageClick(stage),
430
726
  style: {
727
+ flex: 1,
728
+ minWidth: 0,
431
729
  cursor: "pointer",
432
- borderRadius: "var(--mantine-radius-sm)",
433
- padding: "6px 8px",
434
- transition: `background-color var(--duration-fast) var(--easing)`,
435
- marginBottom: 4
436
- },
437
- onMouseEnter: (e) => {
438
- e.currentTarget.style.backgroundColor = "var(--color-surface-hover)";
730
+ padding: "4px 0"
439
731
  },
440
- onMouseLeave: (e) => {
441
- e.currentTarget.style.backgroundColor = "transparent";
442
- },
443
- children: /* @__PURE__ */ jsxs(Group, { gap: "sm", wrap: "nowrap", children: [
444
- /* @__PURE__ */ jsx(Text, { size: "sm", c: isEmpty ? "dimmed" : void 0, style: { width: 130, flexShrink: 0 }, children: STAGE_LABELS[stage] }),
445
- /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "light", color: isEmpty ? "gray" : void 0, style: { flexShrink: 0 }, children: summary.count }),
446
- /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "outline", color: isEmpty ? "gray" : "teal", style: { flexShrink: 0 }, children: currencyFormatter.format(summary.totalValue) }),
447
- /* @__PURE__ */ jsx(Box, { style: { flex: 1, minWidth: 0 }, children: /* @__PURE__ */ jsx(
448
- Box,
449
- {
450
- style: {
451
- height: 8,
452
- width: `${barWidth}%`,
453
- borderRadius: 4,
454
- backgroundColor: isEmpty ? "var(--color-border)" : "color-mix(in srgb, var(--color-primary) 70%, transparent)",
455
- opacity: isEmpty ? 0.4 : 1,
456
- transition: `width var(--duration-normal) var(--easing)`
732
+ children: [
733
+ /* @__PURE__ */ jsxs(Group, { gap: 8, wrap: "nowrap", align: "center", mb: 6, children: [
734
+ /* @__PURE__ */ jsx(
735
+ Box,
736
+ {
737
+ style: {
738
+ width: 8,
739
+ height: 8,
740
+ borderRadius: "50%",
741
+ background: accent,
742
+ flexShrink: 0
743
+ }
457
744
  }
458
- }
459
- ) })
460
- ] })
745
+ ),
746
+ /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", truncate: true, children: STAGE_LABELS[stage] })
747
+ ] }),
748
+ /* @__PURE__ */ jsx(Text, { size: "xl", fw: 600, c: isEmpty ? "dimmed" : void 0, lh: 1, children: summary.count })
749
+ ]
461
750
  },
462
751
  stage
463
752
  );
464
753
  }) })
465
754
  ] });
466
755
  }
467
- var MAX_VISIBLE = 5;
468
- function KindIcon({ kind }) {
469
- const size = 16;
470
- switch (kind) {
471
- case "call":
472
- return /* @__PURE__ */ jsx(IconPhone, { size });
473
- case "email":
474
- return /* @__PURE__ */ jsx(IconMail, { size });
475
- case "meeting":
476
- return /* @__PURE__ */ jsx(IconCalendar, { size });
477
- default:
478
- return /* @__PURE__ */ jsx(IconCheckbox, { size });
479
- }
480
- }
481
- function formatDueDate(dueAt) {
482
- if (!dueAt) return "No due date";
483
- return new Date(dueAt).toLocaleDateString();
484
- }
485
- function TasksDueWidget({ onTaskClick, onSeeAll }) {
486
- const { data: tasks, isLoading, error } = useDealTasksDue({ window: "today_and_overdue" });
487
- if (isLoading) {
488
- return /* @__PURE__ */ jsx(Paper, { withBorder: true, p: "md", children: /* @__PURE__ */ jsx(Center, { p: "xl", children: /* @__PURE__ */ jsx(Loader, {}) }) });
489
- }
490
- if (error) {
491
- return /* @__PURE__ */ jsx(Paper, { withBorder: true, p: "md", children: /* @__PURE__ */ jsx(CenteredErrorState, { error, title: "Failed to load tasks" }) });
492
- }
493
- const totalCount = tasks?.length ?? 0;
494
- const visibleTasks = (tasks ?? []).slice(0, MAX_VISIBLE);
495
- const hasMore = totalCount > MAX_VISIBLE;
496
- const seeAllLink = onSeeAll && hasMore ? /* @__PURE__ */ jsxs(Anchor, { size: "sm", onClick: onSeeAll, style: { cursor: "pointer" }, children: [
497
- "See all (",
498
- totalCount,
499
- ")"
500
- ] }) : void 0;
501
- if (totalCount === 0) {
502
- return /* @__PURE__ */ jsxs(Paper, { withBorder: true, p: "md", children: [
503
- /* @__PURE__ */ jsx(CardHeader, { icon: /* @__PURE__ */ jsx(IconChecklist, { size: 16 }), title: "Tasks Due" }),
504
- /* @__PURE__ */ jsx(Center, { h: 120, children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: "No tasks due today" }) })
505
- ] });
506
- }
507
- return /* @__PURE__ */ jsxs(Paper, { withBorder: true, p: "md", children: [
508
- /* @__PURE__ */ jsx(
509
- CardHeader,
510
- {
511
- icon: /* @__PURE__ */ jsx(IconChecklist, { size: 16 }),
512
- title: "Tasks Due",
513
- subtitle: `${totalCount} task${totalCount !== 1 ? "s" : ""}`,
514
- rightSection: seeAllLink
515
- }
516
- ),
517
- /* @__PURE__ */ jsx(Stack, { gap: "xs", children: visibleTasks.map((task) => /* @__PURE__ */ jsx(
518
- Box,
519
- {
520
- onClick: () => onTaskClick(task.dealId),
521
- style: {
522
- cursor: "pointer",
523
- borderRadius: "var(--mantine-radius-sm)",
524
- padding: "6px 8px",
525
- transition: `background-color var(--duration-fast) var(--easing)`
526
- },
527
- onMouseEnter: (e) => {
528
- e.currentTarget.style.backgroundColor = "var(--color-surface-hover)";
529
- },
530
- onMouseLeave: (e) => {
531
- e.currentTarget.style.backgroundColor = "transparent";
532
- },
533
- children: /* @__PURE__ */ jsxs(Group, { gap: "sm", wrap: "nowrap", children: [
534
- /* @__PURE__ */ jsx(Text, { c: "dimmed", style: { flexShrink: 0 }, children: /* @__PURE__ */ jsx(KindIcon, { kind: task.kind }) }),
535
- /* @__PURE__ */ jsx(Text, { size: "sm", style: { flex: 1, minWidth: 0 }, truncate: true, children: task.title }),
536
- /* @__PURE__ */ jsx(Badge, { size: "xs", variant: "light", color: "gray", style: { flexShrink: 0 }, children: formatDueDate(task.dueAt) })
537
- ] })
538
- },
539
- task.id
540
- )) })
541
- ] });
542
- }
543
756
  function ActivityKindIcon({ kind }) {
544
757
  const size = 16;
545
758
  switch (kind) {
@@ -578,41 +791,28 @@ function ActivityFeedWidget({ onDealClick, limit }) {
578
791
  /* @__PURE__ */ jsx(Center, { h: 120, children: /* @__PURE__ */ jsx(Alert, { icon: /* @__PURE__ */ jsx(IconAlertCircle, {}), color: "gray", variant: "light", children: "No recent activity" }) })
579
792
  ] });
580
793
  }
581
- return /* @__PURE__ */ jsxs(Paper, { withBorder: true, p: "md", children: [
794
+ return /* @__PURE__ */ jsx(Paper, { withBorder: true, p: "md", children: /* @__PURE__ */ jsxs(Stack, { gap: "sm", children: [
582
795
  /* @__PURE__ */ jsx(CardHeader, { icon: /* @__PURE__ */ jsx(IconHistory, { size: 16 }), title: "Recent Activity" }),
583
- /* @__PURE__ */ jsx(Stack, { gap: 4, children: data.map((entry) => {
584
- const name = entry.contactName ?? entry.companyName ?? "Unknown";
585
- return /* @__PURE__ */ jsx(
586
- Box,
587
- {
588
- onClick: () => onDealClick(entry.dealId),
589
- style: {
590
- cursor: "pointer",
591
- borderRadius: "var(--mantine-radius-sm)",
592
- padding: "6px 8px",
593
- transition: `background-color var(--duration-fast) var(--easing)`
594
- },
595
- onMouseEnter: (e) => {
596
- e.currentTarget.style.backgroundColor = "var(--color-surface-hover)";
597
- },
598
- onMouseLeave: (e) => {
599
- e.currentTarget.style.backgroundColor = "transparent";
600
- },
601
- children: /* @__PURE__ */ jsxs(Group, { gap: "sm", wrap: "nowrap", align: "flex-start", children: [
602
- /* @__PURE__ */ jsx(Text, { c: "dimmed", style: { flexShrink: 0, paddingTop: 2 }, children: /* @__PURE__ */ jsx(ActivityKindIcon, { kind: entry.kind }) }),
603
- /* @__PURE__ */ jsx(Box, { style: { flex: 1, minWidth: 0 }, children: /* @__PURE__ */ jsxs(Group, { gap: "xs", wrap: "nowrap", children: [
604
- /* @__PURE__ */ jsx(Text, { size: "sm", fw: 600, truncate: true, style: { flexShrink: 0, maxWidth: 140 }, children: name }),
605
- /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", truncate: true, style: { flex: 1, minWidth: 0 }, children: entry.description })
606
- ] }) }),
607
- /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", style: { flexShrink: 0, whiteSpace: "nowrap" }, children: formatRelativeTime(entry.occurredAt) })
608
- ] })
609
- },
610
- entry.id
611
- );
612
- }) })
613
- ] });
796
+ /* @__PURE__ */ jsxs(Table, { highlightOnHover: true, children: [
797
+ /* @__PURE__ */ jsx(Table.Thead, { children: /* @__PURE__ */ jsxs(Table.Tr, { children: [
798
+ /* @__PURE__ */ jsx(Table.Th, { w: 40 }),
799
+ /* @__PURE__ */ jsx(Table.Th, { children: "Name" }),
800
+ /* @__PURE__ */ jsx(Table.Th, { children: "Activity" }),
801
+ /* @__PURE__ */ jsx(Table.Th, { w: 120, children: "When" })
802
+ ] }) }),
803
+ /* @__PURE__ */ jsx(Table.Tbody, { children: data.map((entry) => {
804
+ const name = entry.contactName ?? entry.companyName ?? "Unknown";
805
+ return /* @__PURE__ */ jsxs(Table.Tr, { style: { cursor: "pointer" }, onClick: () => onDealClick(entry.dealId), children: [
806
+ /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { c: "dimmed", component: "span", children: /* @__PURE__ */ jsx(ActivityKindIcon, { kind: entry.kind }) }) }),
807
+ /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", fw: 500, truncate: true, children: name }) }),
808
+ /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", truncate: true, children: entry.description }) }),
809
+ /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", style: { whiteSpace: "nowrap" }, children: formatRelativeTime(entry.occurredAt) }) })
810
+ ] }, entry.id);
811
+ }) })
812
+ ] })
813
+ ] }) });
614
814
  }
615
- var currencyFormatter2 = new Intl.NumberFormat("en-US", {
815
+ var currencyFormatter = new Intl.NumberFormat("en-US", {
616
816
  style: "currency",
617
817
  currency: "USD",
618
818
  maximumFractionDigits: 0
@@ -629,7 +829,7 @@ function StatTile({ label, value }) {
629
829
  function MetricsStrip() {
630
830
  const { data } = useCrmQuickMetrics();
631
831
  return /* @__PURE__ */ jsx(Paper, { withBorder: true, p: "md", children: /* @__PURE__ */ jsxs(SimpleGrid, { cols: { base: 2, sm: 4 }, children: [
632
- /* @__PURE__ */ jsx(StatTile, { label: "Total Pipeline Value", value: currencyFormatter2.format(data.totalPipelineValue) }),
832
+ /* @__PURE__ */ jsx(StatTile, { label: "Total Pipeline Value", value: currencyFormatter.format(data.totalPipelineValue) }),
633
833
  /* @__PURE__ */ jsx(StatTile, { label: "Win Rate", value: formatPercent(data.winRate) }),
634
834
  /* @__PURE__ */ jsx(StatTile, { label: "Open Deals", value: String(data.openDeals) }),
635
835
  /* @__PURE__ */ jsx(StatTile, { label: "Won This Period", value: String(data.wonDeals) })
@@ -653,13 +853,8 @@ function CrmOverview({
653
853
  }
654
854
  ),
655
855
  /* @__PURE__ */ jsx(MetricsStrip, {}),
656
- /* @__PURE__ */ jsxs(SimpleGrid, { cols: { base: 1, lg: 2 }, spacing: "md", children: [
657
- /* @__PURE__ */ jsx(PipelineFunnelWidget, { onStageClick, getDealValue }),
658
- /* @__PURE__ */ jsxs(Stack, { gap: "md", children: [
659
- /* @__PURE__ */ jsx(TasksDueWidget, { onTaskClick: onDealClick }),
660
- /* @__PURE__ */ jsx(ActivityFeedWidget, { onDealClick })
661
- ] })
662
- ] })
856
+ /* @__PURE__ */ jsx(PipelineFunnelWidget, { onStageClick, getDealValue }),
857
+ /* @__PURE__ */ jsx(ActivityFeedWidget, { onDealClick })
663
858
  ] });
664
859
  }
665
860
  var crmManifest = {
@@ -669,28 +864,6 @@ var crmManifest = {
669
864
  icon: IconAddressBook,
670
865
  sidebar: CrmSidebar
671
866
  };
672
-
673
- // src/features/crm/pages/shared.ts
674
- var DEAL_STAGE_COLORS = {
675
- interested: "blue",
676
- proposal: "yellow",
677
- closing: "orange",
678
- closed_won: "green",
679
- closed_lost: "red",
680
- nurturing: "grape"
681
- };
682
- var DEAL_STAGE_OPTIONS = [
683
- { value: "interested", label: "Interested" },
684
- { value: "proposal", label: "Proposal" },
685
- { value: "closing", label: "Closing" },
686
- { value: "closed_won", label: "Closed Won" },
687
- { value: "closed_lost", label: "Closed Lost" },
688
- { value: "nurturing", label: "Nurturing" }
689
- ];
690
- function formatDealStageLabel(stage) {
691
- if (!stage) return "Unknown";
692
- return stage.split("_").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
693
- }
694
867
  var sortAccessors = {
695
868
  company: (deal) => deal.contact?.company?.name || deal.discovery_data?.company || deal.contact_email?.split("@")[1] || "",
696
869
  contact: (deal) => [deal.contact?.first_name, deal.contact?.last_name].filter(Boolean).join(" ") || "",
@@ -857,11 +1030,31 @@ function DealsListPage() {
857
1030
  )
858
1031
  ] });
859
1032
  }
1033
+ function BookingLinkPanel({ dealId, contactId }) {
1034
+ const url = buildBookingUrl({ dealId, contactId });
1035
+ return /* @__PURE__ */ jsx(Card, { withBorder: true, children: /* @__PURE__ */ jsxs(Stack, { gap: "sm", children: [
1036
+ /* @__PURE__ */ jsx(Title, { order: 4, children: "Booking Link" }),
1037
+ /* @__PURE__ */ jsxs(Group, { gap: "xs", wrap: "nowrap", children: [
1038
+ /* @__PURE__ */ jsx(
1039
+ Text,
1040
+ {
1041
+ size: "sm",
1042
+ c: "var(--color-primary)",
1043
+ style: { flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
1044
+ children: url
1045
+ }
1046
+ ),
1047
+ /* @__PURE__ */ jsx(CopyButton, { value: url, children: ({ copied, copy }) => /* @__PURE__ */ jsx(Tooltip, { label: copied ? "Copied" : "Copy", withArrow: true, children: /* @__PURE__ */ jsx(ActionIcon, { variant: "light", color: copied ? "teal" : void 0, onClick: copy, children: copied ? /* @__PURE__ */ jsx(IconCheck, { size: 16 }) : /* @__PURE__ */ jsx(IconCopy, { size: 16 }) }) }) })
1048
+ ] })
1049
+ ] }) });
1050
+ }
860
1051
  function DealDetailPage({ dealId, renderActions, onDealLoaded }) {
861
1052
  const navigate = useNavigate();
862
1053
  const deleteDeal = useDeleteDeal();
863
1054
  const { data: deal, isLoading, error } = useDealDetail(dealId);
864
1055
  const [deleteModalOpen, setDeleteModalOpen] = useState(false);
1056
+ const crmActions = useCrmActions();
1057
+ const actions = useMemo(() => deal ? deriveActions(deal, crmActions) : [], [deal, crmActions]);
865
1058
  useEffect(() => {
866
1059
  if (deal) onDealLoaded?.(deal);
867
1060
  }, [deal, onDealLoaded]);
@@ -894,6 +1087,9 @@ function DealDetailPage({ dealId, renderActions, onDealLoaded }) {
894
1087
  children: "View Proposal"
895
1088
  }
896
1089
  ),
1090
+ actions.map(
1091
+ (action) => action.payloadSchema ? /* @__PURE__ */ jsx(ActionFormButton, { action, dealId: deal.id }, action.key) : /* @__PURE__ */ jsx(ActionButton, { action, dealId: deal.id }, action.key)
1092
+ ),
897
1093
  renderActions?.(deal),
898
1094
  /* @__PURE__ */ jsx(ActionIcon, { variant: "subtle", color: "red", onClick: () => setDeleteModalOpen(true), children: /* @__PURE__ */ jsx(IconTrash, { size: 16 }) })
899
1095
  ] }) : /* @__PURE__ */ jsx(
@@ -988,6 +1184,7 @@ function DealDetailPage({ dealId, renderActions, onDealLoaded }) {
988
1184
  ] })
989
1185
  ] })
990
1186
  ] }) }),
1187
+ deal.contact_id && /* @__PURE__ */ jsx(BookingLinkPanel, { dealId: deal.id, contactId: deal.contact_id }),
991
1188
  /* @__PURE__ */ jsx(Card, { withBorder: true, children: /* @__PURE__ */ jsxs(Stack, { gap: "sm", children: [
992
1189
  /* @__PURE__ */ jsx(Title, { order: 4, children: "Company" }),
993
1190
  /* @__PURE__ */ jsxs(SimpleGrid, { cols: { base: 1, sm: 2 }, spacing: "sm", children: [
@@ -1202,4 +1399,4 @@ function CompanyDetailPage({ companyId }) {
1202
1399
  ] }) });
1203
1400
  }
1204
1401
 
1205
- export { ActivityFeedWidget, CRM_ITEMS, CompanyDetailPage, CrmOverview, CrmSidebar, CrmSidebarMiddle, CrmSidebarTop, DEAL_STAGE_COLORS, DEAL_STAGE_OPTIONS, DealDetailPage, DealsListPage, MetricsStrip, MyTasksPanel, PIPELINE_FUNNEL_ORDER, PipelineFunnelWidget, QuickCreateActions, SAVED_VIEW_PRESETS, SavedViewsPanel, TasksDueWidget, crmManifest, formatDealStageLabel, useCrmPipelineSummary, useCrmQuickMetrics, useRecentCrmActivity };
1402
+ export { ActionButton, ActionFormButton, ActivityFeedWidget, CRM_ITEMS, CompanyDetailPage, CrmOverview, CrmSidebar, CrmSidebarMiddle, CrmSidebarTop, DEAL_STAGE_COLORS, DEAL_STAGE_OPTIONS, DealDetailPage, DealsListPage, MetricsStrip, MyTasksPanel, PIPELINE_FUNNEL_ORDER, PipelineFunnelWidget, QuickCreateActions, SAVED_VIEW_PRESETS, SavedViewsPanel, crmManifest, formatDealStageLabel, useCrmPipelineSummary, useCrmQuickMetrics, useRecentCrmActivity };