@elevasis/ui 2.25.0 → 2.25.1
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/app/index.css +11 -0
- package/dist/app/index.d.ts +457 -357
- package/dist/app/index.js +8 -7
- package/dist/auth/index.js +4 -4
- package/dist/charts/index.js +7 -7
- package/dist/{chunk-DDZOHLHB.js → chunk-26HQMR7S.js} +1 -1
- package/dist/{chunk-ZMXZ476Y.js → chunk-3AHEHVJ6.js} +3 -3
- package/dist/{chunk-LJWV4TWV.js → chunk-4ZFBVND2.js} +2 -2
- package/dist/{chunk-7D2HSSIW.js → chunk-7LJUTTXU.js} +5 -5
- package/dist/{chunk-ABV5LDDC.js → chunk-AZQY7AJR.js} +13 -24
- package/dist/{chunk-BIWHHWCJ.js → chunk-DWK2QIAK.js} +2 -1
- package/dist/{chunk-ZDKQNQ4X.js → chunk-GESXCQWY.js} +1 -1
- package/dist/{chunk-Z6FAH4XV.js → chunk-HKBEURCV.js} +1 -1
- package/dist/{chunk-HC2KV6BU.js → chunk-HOIT677G.js} +1 -1
- package/dist/chunk-IS53MXE4.js +230 -0
- package/dist/{chunk-WSC5LU3U.js → chunk-IX7LWINC.js} +6 -5
- package/dist/{chunk-AXXTN44Z.js → chunk-IYIZYMIE.js} +2 -2
- package/dist/{chunk-WWVSPOJY.js → chunk-JDQSCEEF.js} +1237 -642
- package/dist/{chunk-AZXSFDG2.js → chunk-JMI7L7Y7.js} +113 -63
- package/dist/{chunk-TSSKOQBX.js → chunk-KMAXFJPH.js} +2 -2
- package/dist/{chunk-HQ7M6PBW.js → chunk-KU7ZDWQ7.js} +1 -1
- package/dist/{chunk-ZGZZIR6K.js → chunk-L4RT57WU.js} +7 -7
- package/dist/{chunk-LK4MPIMK.js → chunk-MU4VPAMR.js} +2 -2
- package/dist/{chunk-3JCMO7SD.js → chunk-N55DVMAG.js} +1 -1
- package/dist/{chunk-EIOJNUPL.js → chunk-PNLJIPV5.js} +1 -1
- package/dist/{chunk-6Z3G4U2R.js → chunk-Q5BEODAT.js} +3 -2
- package/dist/{chunk-HVC2BTFO.js → chunk-QB2CC4VH.js} +1979 -257
- package/dist/{chunk-QJLRDTYS.js → chunk-RSG2O3HF.js} +932 -707
- package/dist/{chunk-M25JL54Z.js → chunk-RYTEQBAO.js} +1 -1
- package/dist/{chunk-XUYBOO32.js → chunk-U36X6NZM.js} +15 -7
- package/dist/{chunk-QSTH6T77.js → chunk-VKMNWHTL.js} +1 -1
- package/dist/{chunk-V3UOW2HG.js → chunk-VOVZLL23.js} +4 -4
- package/dist/{chunk-SLH2QLKV.js → chunk-WFTNY755.js} +1 -1
- package/dist/{chunk-DK2HVHCY.js → chunk-WKJ47GIW.js} +1 -1
- package/dist/chunk-X4WBGKJQ.js +138 -0
- package/dist/{chunk-QHSW4WHM.js → chunk-XTVZFT7U.js} +1 -1
- package/dist/components/index.css +11 -0
- package/dist/components/index.d.ts +478 -386
- package/dist/components/index.js +138 -336
- package/dist/components/navigation/index.css +11 -0
- package/dist/components/navigation/index.js +8 -8
- package/dist/features/auth/index.css +11 -0
- package/dist/features/auth/index.d.ts +369 -363
- package/dist/features/auth/index.js +7 -7
- package/dist/features/crm/index.css +11 -0
- package/dist/features/crm/index.d.ts +551 -360
- package/dist/features/crm/index.js +19 -19
- package/dist/features/dashboard/index.css +11 -0
- package/dist/features/dashboard/index.js +20 -20
- package/dist/features/delivery/index.css +11 -0
- package/dist/features/delivery/index.d.ts +363 -357
- package/dist/features/delivery/index.js +19 -19
- package/dist/features/lead-gen/index.css +11 -0
- package/dist/features/lead-gen/index.d.ts +160 -2
- package/dist/features/lead-gen/index.js +20 -19
- package/dist/features/monitoring/index.css +11 -0
- package/dist/features/monitoring/index.js +21 -21
- package/dist/features/monitoring/requests/index.css +11 -0
- package/dist/features/monitoring/requests/index.js +17 -17
- package/dist/features/operations/index.css +11 -0
- package/dist/features/operations/index.js +24 -23
- package/dist/features/seo/index.js +2 -2
- package/dist/features/settings/index.css +11 -0
- package/dist/features/settings/index.d.ts +369 -363
- package/dist/features/settings/index.js +19 -19
- package/dist/hooks/delivery/index.css +11 -0
- package/dist/hooks/delivery/index.d.ts +363 -357
- package/dist/hooks/delivery/index.js +2 -2
- package/dist/hooks/index.css +11 -0
- package/dist/hooks/index.d.ts +746 -399
- package/dist/hooks/index.js +17 -17
- package/dist/hooks/published.css +11 -0
- package/dist/hooks/published.d.ts +746 -399
- package/dist/hooks/published.js +17 -17
- package/dist/index.css +11 -0
- package/dist/index.d.ts +862 -381
- package/dist/index.js +17 -17
- package/dist/initialization/index.d.ts +369 -363
- package/dist/initialization/index.js +4 -4
- package/dist/layout/index.d.ts +6 -0
- package/dist/layout/index.js +5 -5
- package/dist/organization/index.css +11 -0
- package/dist/organization/index.js +4 -4
- package/dist/profile/index.d.ts +369 -363
- package/dist/profile/index.js +2 -2
- package/dist/provider/ElevasisServiceContext.d.ts +1 -0
- package/dist/provider/ElevasisServiceContext.js +1 -1
- package/dist/provider/index.css +11 -0
- package/dist/provider/index.d.ts +503 -362
- package/dist/provider/index.js +14 -14
- package/dist/provider/published.css +11 -0
- package/dist/provider/published.d.ts +494 -359
- package/dist/provider/published.js +12 -12
- package/dist/supabase/index.d.ts +369 -357
- package/dist/test-utils/index.js +1 -1
- package/dist/typeform/index.js +49 -20
- package/dist/types/index.d.ts +382 -363
- package/package.json +4 -4
- package/dist/chunk-CEWTOKE7.js +0 -109
- /package/dist/{chunk-IRW7JMQ4.js → chunk-5WWZXCS5.js} +0 -0
- /package/dist/{chunk-QJ2KCHKX.js → chunk-E565XMTQ.js} +0 -0
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
import { PageContainer } from './chunk-BZZCNLT6.js';
|
|
2
2
|
import { TableSelectionToolbar, SortableHeader } from './chunk-TUMSNGTX.js';
|
|
3
|
-
import { SubshellNavItem } from './chunk-
|
|
3
|
+
import { SubshellNavItem } from './chunk-X4WBGKJQ.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 {
|
|
8
|
-
import {
|
|
7
|
+
import { useDealTasksDue, useDealsLookup, useCreateDealTask, useDealsSummary, useDeleteDeal, usePaginationState, useDeals, useTableSort, sortData, useTableSelection, useDealDetail, useCompany, dealKeys, useExecuteAction } from './chunk-RSG2O3HF.js';
|
|
8
|
+
import { showApiErrorNotification, showSuccessNotification } from './chunk-HKBEURCV.js';
|
|
9
|
+
import { useCrmActions, DEFAULT_CRM_PRIORITY_RULE_CONFIG, deriveActions, CRM_PRIORITY_BUCKETS } from './chunk-JMI7L7Y7.js';
|
|
9
10
|
import { SubshellContentContainer } from './chunk-TKAYX2SP.js';
|
|
10
|
-
import { CenteredErrorState, CardHeader, PageTitleCaption, EmptyState, ActivityTimeline } from './chunk-
|
|
11
|
+
import { CenteredErrorState, CardHeader, StatCard, PageTitleCaption, EmptyState, ActivityTimeline } from './chunk-U36X6NZM.js';
|
|
11
12
|
import { useRouterContext } from './chunk-Q7DJKLEN.js';
|
|
12
13
|
import { PAGE_SIZE_DEFAULT, formatTimeAgo } from './chunk-SGXXJE52.js';
|
|
13
|
-
import { useElevasisServices } from './chunk-
|
|
14
|
-
import {
|
|
15
|
-
import { IconLayoutGrid, IconColumns, IconFileInvoice, IconAddressBook,
|
|
14
|
+
import { useElevasisServices } from './chunk-5WWZXCS5.js';
|
|
15
|
+
import { Box, Stack, Group, Text, Badge, Center, Loader, UnstyledButton, Button, Modal, Title, Select, TextInput, Textarea, Paper, Alert, Table, SimpleGrid, Tabs, Checkbox, Tooltip, Pagination, Code, Card, Switch, NumberInput, ColorSwatch, Popover, ActionIcon, CopyButton, Divider } from '@mantine/core';
|
|
16
|
+
import { IconLayoutGrid, IconColumns, IconFileInvoice, IconAddressBook, IconTrophy, IconClockExclamation, IconUser, IconPlus, IconChecklist, IconHistory, IconAlertCircle, IconCurrencyDollar, IconChartBar, IconBriefcase, IconSearch, IconTargetArrow, IconAlertTriangle, IconMessages, IconArrowLeft, IconFileText, IconInbox, IconBolt, IconBuilding, IconSettings, IconRestore, IconInfoCircle, IconMailForward, IconCheckbox, IconCalendar, IconMail, IconPhone, IconArrowRight, IconNote, IconCheck, IconCopy } from '@tabler/icons-react';
|
|
16
17
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
17
18
|
import { useState, useMemo, useEffect } from 'react';
|
|
18
|
-
import {
|
|
19
|
-
import { useQuery } from '@tanstack/react-query';
|
|
19
|
+
import { useQuery, useQueryClient, useMutation } from '@tanstack/react-query';
|
|
20
20
|
import { useNavigate } from '@tanstack/react-router';
|
|
21
|
+
import { useForm } from '@mantine/form';
|
|
21
22
|
|
|
22
23
|
var CrmSidebarTop = () => {
|
|
23
24
|
return /* @__PURE__ */ jsx(SubshellSidebarSection, { icon: IconAddressBook, label: "CRM" });
|
|
@@ -45,329 +46,138 @@ var SAVED_VIEW_PRESETS = [
|
|
|
45
46
|
urlFilters: { stage: "closed_won" }
|
|
46
47
|
}
|
|
47
48
|
];
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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 };
|
|
49
|
+
|
|
50
|
+
// src/features/crm/pages/shared.ts
|
|
51
|
+
var DEAL_REFERRER_STORAGE_KEY = "crm:dealReferrer";
|
|
52
|
+
function setDealReferrer(referrer) {
|
|
53
|
+
if (typeof window === "undefined") return;
|
|
54
|
+
try {
|
|
55
|
+
window.sessionStorage.setItem(DEAL_REFERRER_STORAGE_KEY, referrer);
|
|
56
|
+
} catch {
|
|
83
57
|
}
|
|
84
|
-
return { schema, required: true, nullable: false, defaultValue: void 0 };
|
|
85
58
|
}
|
|
86
|
-
function
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (!shape || typeof shape !== "object" || Array.isArray(shape)) {
|
|
94
|
-
return null;
|
|
59
|
+
function getDealReferrer() {
|
|
60
|
+
if (typeof window === "undefined") return "deals";
|
|
61
|
+
try {
|
|
62
|
+
const value = window.sessionStorage.getItem(DEAL_REFERRER_STORAGE_KEY);
|
|
63
|
+
return value === "pipeline" ? "pipeline" : "deals";
|
|
64
|
+
} catch {
|
|
65
|
+
return "deals";
|
|
95
66
|
}
|
|
96
|
-
return shape;
|
|
97
67
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
68
|
+
var DEAL_STAGE_COLORS = {
|
|
69
|
+
interested: "blue",
|
|
70
|
+
proposal: "yellow",
|
|
71
|
+
closing: "orange",
|
|
72
|
+
closed_won: "green",
|
|
73
|
+
closed_lost: "red",
|
|
74
|
+
nurturing: "grape"
|
|
75
|
+
};
|
|
76
|
+
var DEAL_STAGE_OPTIONS = [
|
|
77
|
+
{ value: "interested", label: "Interested" },
|
|
78
|
+
{ value: "proposal", label: "Proposal" },
|
|
79
|
+
{ value: "closing", label: "Closing" },
|
|
80
|
+
{ value: "closed_won", label: "Closed Won" },
|
|
81
|
+
{ value: "closed_lost", label: "Closed Lost" },
|
|
82
|
+
{ value: "nurturing", label: "Nurturing" }
|
|
83
|
+
];
|
|
84
|
+
var DEAL_PRIORITY_OPTIONS = [
|
|
85
|
+
{ value: "needs_response", label: "Needs response" },
|
|
86
|
+
{ value: "follow_up_due", label: "Follow-up due" },
|
|
87
|
+
{ value: "waiting", label: "Waiting" },
|
|
88
|
+
{ value: "stale", label: "Stale" },
|
|
89
|
+
{ value: "closed_low", label: "Closed low" }
|
|
90
|
+
];
|
|
91
|
+
function compareDealsByPriority(a, b, direction = "asc") {
|
|
92
|
+
const rankComparison = comparePriorityRank(a.priority, b.priority, direction);
|
|
93
|
+
if (rankComparison !== 0) return rankComparison;
|
|
94
|
+
const aActivity = getPriorityActivityTimestamp(a);
|
|
95
|
+
const bActivity = getPriorityActivityTimestamp(b);
|
|
96
|
+
return bActivity - aActivity;
|
|
105
97
|
}
|
|
106
|
-
function
|
|
107
|
-
|
|
98
|
+
function compareDealsByPriorityThenUpdated(a, b, direction = "asc") {
|
|
99
|
+
const rankComparison = comparePriorityRank(a.priority, b.priority, direction);
|
|
100
|
+
if (rankComparison !== 0) return rankComparison;
|
|
101
|
+
return getUpdatedTimestamp(b) - getUpdatedTimestamp(a);
|
|
108
102
|
}
|
|
109
|
-
function
|
|
110
|
-
return
|
|
103
|
+
function formatDealStageLabel(stage) {
|
|
104
|
+
return formatDealWorkflowLabel(stage);
|
|
111
105
|
}
|
|
112
|
-
function
|
|
113
|
-
|
|
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
|
-
}
|
|
106
|
+
function formatDealStateLabel(state) {
|
|
107
|
+
return formatDealWorkflowLabel(state);
|
|
131
108
|
}
|
|
132
|
-
function
|
|
133
|
-
|
|
134
|
-
|
|
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 };
|
|
109
|
+
function formatDealWorkflowLabel(value) {
|
|
110
|
+
const words = value?.trim().split("_").map((word) => word.trim().toLowerCase()).filter(Boolean);
|
|
111
|
+
if (!words?.length) return "Unknown";
|
|
112
|
+
return words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
189
113
|
}
|
|
190
|
-
function
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
114
|
+
function comparePriorityRank(a, b, direction) {
|
|
115
|
+
const comparison = a.rank - b.rank;
|
|
116
|
+
return direction === "asc" ? comparison : -comparison;
|
|
117
|
+
}
|
|
118
|
+
function getPriorityActivityTimestamp(deal) {
|
|
119
|
+
const value = deal.priority.latestActivityAt ?? deal.updated_at;
|
|
120
|
+
const timestamp = new Date(value).getTime();
|
|
121
|
+
return Number.isNaN(timestamp) ? 0 : timestamp;
|
|
122
|
+
}
|
|
123
|
+
function getUpdatedTimestamp(deal) {
|
|
124
|
+
const timestamp = new Date(deal.updated_at).getTime();
|
|
125
|
+
return Number.isNaN(timestamp) ? 0 : timestamp;
|
|
126
|
+
}
|
|
127
|
+
var BOOKING_PAGE_URL = "https://elevasis.io/inbound/book";
|
|
128
|
+
function buildBookingUrl(opts = {}) {
|
|
129
|
+
const params = new URLSearchParams();
|
|
130
|
+
if (opts.dealId) params.set("dealId", opts.dealId);
|
|
131
|
+
if (opts.contactId) params.set("contactId", opts.contactId);
|
|
132
|
+
return params.size > 0 ? `${BOOKING_PAGE_URL}?${params.toString()}` : BOOKING_PAGE_URL;
|
|
133
|
+
}
|
|
134
|
+
var KIND_ICONS = {
|
|
135
|
+
call: IconPhone,
|
|
136
|
+
email: IconMail,
|
|
137
|
+
meeting: IconCalendar,
|
|
138
|
+
other: IconCheckbox
|
|
139
|
+
};
|
|
140
|
+
function formatDueLabel(dueAt) {
|
|
141
|
+
if (!dueAt) return "";
|
|
142
|
+
const date = new Date(dueAt);
|
|
143
|
+
const now = /* @__PURE__ */ new Date();
|
|
144
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
145
|
+
const dueDay = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
146
|
+
const diffDays = Math.round((dueDay.getTime() - today.getTime()) / 864e5);
|
|
147
|
+
if (diffDays < 0) return `${Math.abs(diffDays)}d overdue`;
|
|
148
|
+
if (diffDays === 0) {
|
|
149
|
+
return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
200
150
|
}
|
|
201
|
-
return
|
|
151
|
+
return `in ${diffDays}d`;
|
|
202
152
|
}
|
|
203
|
-
function
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
153
|
+
function TaskRow({ task, onClick }) {
|
|
154
|
+
const KindIcon = KIND_ICONS[task.kind];
|
|
155
|
+
const dueLabel = formatDueLabel(task.dueAt);
|
|
156
|
+
const isOverdue = task.dueAt !== null && new Date(task.dueAt) < /* @__PURE__ */ new Date();
|
|
157
|
+
return /* @__PURE__ */ jsx(
|
|
158
|
+
UnstyledButton,
|
|
159
|
+
{
|
|
160
|
+
onClick,
|
|
161
|
+
style: {
|
|
162
|
+
display: "block",
|
|
163
|
+
width: "100%",
|
|
164
|
+
padding: "4px 6px",
|
|
165
|
+
borderRadius: "var(--mantine-radius-sm)",
|
|
166
|
+
transition: `background-color var(--duration-fast) var(--easing)`
|
|
167
|
+
},
|
|
168
|
+
onMouseEnter: (e) => {
|
|
169
|
+
e.currentTarget.style.backgroundColor = "var(--color-surface-hover)";
|
|
170
|
+
},
|
|
171
|
+
onMouseLeave: (e) => {
|
|
172
|
+
e.currentTarget.style.backgroundColor = "transparent";
|
|
173
|
+
},
|
|
174
|
+
children: /* @__PURE__ */ jsxs(Group, { gap: "xs", wrap: "nowrap", children: [
|
|
175
|
+
/* @__PURE__ */ jsx(KindIcon, { size: 14, style: { color: "var(--color-text-dimmed)", flexShrink: 0 } }),
|
|
176
|
+
/* @__PURE__ */ jsx(Text, { size: "xs", truncate: true, style: { flex: 1, minWidth: 0 }, children: task.title }),
|
|
177
|
+
dueLabel && /* @__PURE__ */ jsx(Badge, { size: "xs", variant: "light", color: isOverdue ? "red" : "gray", style: { flexShrink: 0 }, children: dueLabel })
|
|
178
|
+
] })
|
|
208
179
|
}
|
|
209
|
-
|
|
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
|
-
}
|
|
324
|
-
var KIND_ICONS = {
|
|
325
|
-
call: IconPhone,
|
|
326
|
-
email: IconMail,
|
|
327
|
-
meeting: IconCalendar,
|
|
328
|
-
other: IconCheckbox
|
|
329
|
-
};
|
|
330
|
-
function formatDueLabel(dueAt) {
|
|
331
|
-
if (!dueAt) return "";
|
|
332
|
-
const date = new Date(dueAt);
|
|
333
|
-
const now = /* @__PURE__ */ new Date();
|
|
334
|
-
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
335
|
-
const dueDay = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
336
|
-
const diffDays = Math.round((dueDay.getTime() - today.getTime()) / 864e5);
|
|
337
|
-
if (diffDays < 0) return `${Math.abs(diffDays)}d overdue`;
|
|
338
|
-
if (diffDays === 0) {
|
|
339
|
-
return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
340
|
-
}
|
|
341
|
-
return `in ${diffDays}d`;
|
|
342
|
-
}
|
|
343
|
-
function TaskRow({ task, onClick }) {
|
|
344
|
-
const KindIcon = KIND_ICONS[task.kind];
|
|
345
|
-
const dueLabel = formatDueLabel(task.dueAt);
|
|
346
|
-
const isOverdue = task.dueAt !== null && new Date(task.dueAt) < /* @__PURE__ */ new Date();
|
|
347
|
-
return /* @__PURE__ */ jsx(
|
|
348
|
-
UnstyledButton,
|
|
349
|
-
{
|
|
350
|
-
onClick,
|
|
351
|
-
style: {
|
|
352
|
-
display: "block",
|
|
353
|
-
width: "100%",
|
|
354
|
-
padding: "4px 6px",
|
|
355
|
-
borderRadius: "var(--mantine-radius-sm)",
|
|
356
|
-
transition: `background-color var(--duration-fast) var(--easing)`
|
|
357
|
-
},
|
|
358
|
-
onMouseEnter: (e) => {
|
|
359
|
-
e.currentTarget.style.backgroundColor = "var(--color-surface-hover)";
|
|
360
|
-
},
|
|
361
|
-
onMouseLeave: (e) => {
|
|
362
|
-
e.currentTarget.style.backgroundColor = "transparent";
|
|
363
|
-
},
|
|
364
|
-
children: /* @__PURE__ */ jsxs(Group, { gap: "xs", wrap: "nowrap", children: [
|
|
365
|
-
/* @__PURE__ */ jsx(KindIcon, { size: 14, style: { color: "var(--color-text-dimmed)", flexShrink: 0 } }),
|
|
366
|
-
/* @__PURE__ */ jsx(Text, { size: "xs", truncate: true, style: { flex: 1, minWidth: 0 }, children: task.title }),
|
|
367
|
-
dueLabel && /* @__PURE__ */ jsx(Badge, { size: "xs", variant: "light", color: isOverdue ? "red" : "gray", style: { flexShrink: 0 }, children: dueLabel })
|
|
368
|
-
] })
|
|
369
|
-
}
|
|
370
|
-
);
|
|
180
|
+
);
|
|
371
181
|
}
|
|
372
182
|
function MyTasksPanel({
|
|
373
183
|
onTaskClick,
|
|
@@ -571,6 +381,7 @@ var CrmSidebarMiddle = ({ items = CRM_ITEMS } = {}) => {
|
|
|
571
381
|
icon: item.icon,
|
|
572
382
|
label: item.label,
|
|
573
383
|
isActive,
|
|
384
|
+
href: item.to,
|
|
574
385
|
onClick: () => navigate(item.to)
|
|
575
386
|
},
|
|
576
387
|
item.to
|
|
@@ -659,35 +470,6 @@ function useRecentCrmActivity(opts) {
|
|
|
659
470
|
error: query.error
|
|
660
471
|
};
|
|
661
472
|
}
|
|
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
|
-
}
|
|
691
473
|
var STAGE_LABELS = {
|
|
692
474
|
interested: "Interested",
|
|
693
475
|
proposal: "Proposal",
|
|
@@ -793,10 +575,10 @@ function ActivityFeedWidget({ onDealClick, limit }) {
|
|
|
793
575
|
}
|
|
794
576
|
return /* @__PURE__ */ jsx(Paper, { withBorder: true, p: "md", children: /* @__PURE__ */ jsxs(Stack, { gap: "sm", children: [
|
|
795
577
|
/* @__PURE__ */ jsx(CardHeader, { icon: /* @__PURE__ */ jsx(IconHistory, { size: 16 }), title: "Recent Activity" }),
|
|
796
|
-
/* @__PURE__ */ jsxs(Table, { highlightOnHover: true, children: [
|
|
578
|
+
/* @__PURE__ */ jsxs(Table, { highlightOnHover: true, style: { tableLayout: "fixed", width: "100%" }, children: [
|
|
797
579
|
/* @__PURE__ */ jsx(Table.Thead, { children: /* @__PURE__ */ jsxs(Table.Tr, { children: [
|
|
798
580
|
/* @__PURE__ */ jsx(Table.Th, { w: 40 }),
|
|
799
|
-
/* @__PURE__ */ jsx(Table.Th, { children: "Name" }),
|
|
581
|
+
/* @__PURE__ */ jsx(Table.Th, { w: 220, children: "Name" }),
|
|
800
582
|
/* @__PURE__ */ jsx(Table.Th, { children: "Activity" }),
|
|
801
583
|
/* @__PURE__ */ jsx(Table.Th, { w: 120, children: "When" })
|
|
802
584
|
] }) }),
|
|
@@ -804,8 +586,8 @@ function ActivityFeedWidget({ onDealClick, limit }) {
|
|
|
804
586
|
const name = entry.contactName ?? entry.companyName ?? "Unknown";
|
|
805
587
|
return /* @__PURE__ */ jsxs(Table.Tr, { style: { cursor: "pointer" }, onClick: () => onDealClick(entry.dealId), children: [
|
|
806
588
|
/* @__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 }) }),
|
|
589
|
+
/* @__PURE__ */ jsx(Table.Td, { style: { maxWidth: 220 }, children: /* @__PURE__ */ jsx(Text, { size: "sm", fw: 500, truncate: true, title: name, style: { minWidth: 0 }, children: name }) }),
|
|
590
|
+
/* @__PURE__ */ jsx(Table.Td, { style: { minWidth: 0 }, children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", truncate: true, title: entry.description, style: { minWidth: 0 }, children: entry.description }) }),
|
|
809
591
|
/* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", style: { whiteSpace: "nowrap" }, children: formatRelativeTime(entry.occurredAt) }) })
|
|
810
592
|
] }, entry.id);
|
|
811
593
|
}) })
|
|
@@ -820,20 +602,50 @@ var currencyFormatter = new Intl.NumberFormat("en-US", {
|
|
|
820
602
|
function formatPercent(value) {
|
|
821
603
|
return `${Math.round(value * 100)}%`;
|
|
822
604
|
}
|
|
823
|
-
function StatTile({ label, value }) {
|
|
824
|
-
return /* @__PURE__ */ jsx(Card, { padding: "sm", children: /* @__PURE__ */ jsxs(Stack, { gap: 4, children: [
|
|
825
|
-
/* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: label }),
|
|
826
|
-
/* @__PURE__ */ jsx(Text, { fw: 700, size: "xl", children: value })
|
|
827
|
-
] }) });
|
|
828
|
-
}
|
|
829
605
|
function MetricsStrip() {
|
|
830
|
-
const { data } = useCrmQuickMetrics();
|
|
831
|
-
return /* @__PURE__ */
|
|
832
|
-
/* @__PURE__ */ jsx(
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
606
|
+
const { data, isLoading } = useCrmQuickMetrics();
|
|
607
|
+
return /* @__PURE__ */ jsxs(SimpleGrid, { cols: { base: 2, sm: 4 }, children: [
|
|
608
|
+
/* @__PURE__ */ jsx(
|
|
609
|
+
StatCard,
|
|
610
|
+
{
|
|
611
|
+
variant: "hero",
|
|
612
|
+
icon: IconCurrencyDollar,
|
|
613
|
+
value: currencyFormatter.format(data.totalPipelineValue),
|
|
614
|
+
label: "Total Pipeline Value",
|
|
615
|
+
isLoading
|
|
616
|
+
}
|
|
617
|
+
),
|
|
618
|
+
/* @__PURE__ */ jsx(
|
|
619
|
+
StatCard,
|
|
620
|
+
{
|
|
621
|
+
variant: "hero",
|
|
622
|
+
icon: IconChartBar,
|
|
623
|
+
value: formatPercent(data.winRate),
|
|
624
|
+
label: "Win Rate",
|
|
625
|
+
isLoading
|
|
626
|
+
}
|
|
627
|
+
),
|
|
628
|
+
/* @__PURE__ */ jsx(
|
|
629
|
+
StatCard,
|
|
630
|
+
{
|
|
631
|
+
variant: "hero",
|
|
632
|
+
icon: IconBriefcase,
|
|
633
|
+
value: String(data.openDeals),
|
|
634
|
+
label: "Open Deals",
|
|
635
|
+
isLoading
|
|
636
|
+
}
|
|
637
|
+
),
|
|
638
|
+
/* @__PURE__ */ jsx(
|
|
639
|
+
StatCard,
|
|
640
|
+
{
|
|
641
|
+
variant: "hero",
|
|
642
|
+
icon: IconTrophy,
|
|
643
|
+
value: String(data.wonDeals),
|
|
644
|
+
label: "Won This Period",
|
|
645
|
+
isLoading
|
|
646
|
+
}
|
|
647
|
+
)
|
|
648
|
+
] });
|
|
837
649
|
}
|
|
838
650
|
function CrmOverview({
|
|
839
651
|
onStageClick,
|
|
@@ -869,15 +681,30 @@ var sortAccessors = {
|
|
|
869
681
|
contact: (deal) => [deal.contact?.first_name, deal.contact?.last_name].filter(Boolean).join(" ") || "",
|
|
870
682
|
email: (deal) => deal.contact_email || "",
|
|
871
683
|
stage: (deal) => deal.stage_key || "",
|
|
684
|
+
state: (deal) => deal.state_key || "",
|
|
685
|
+
ownership: (deal) => deal.ownership || "",
|
|
872
686
|
updated: (deal) => deal.updated_at || ""
|
|
873
687
|
};
|
|
688
|
+
var DEAL_OWNERSHIP_BADGE_COPY = {
|
|
689
|
+
us: {
|
|
690
|
+
color: "red",
|
|
691
|
+
label: "Our move",
|
|
692
|
+
tooltip: "We owe the next move"
|
|
693
|
+
},
|
|
694
|
+
them: {
|
|
695
|
+
color: "gray",
|
|
696
|
+
label: "Their move",
|
|
697
|
+
tooltip: "Lead owes the next move"
|
|
698
|
+
}
|
|
699
|
+
};
|
|
874
700
|
function DealsListPage() {
|
|
875
701
|
const navigate = useNavigate();
|
|
876
702
|
const deleteDeal = useDeleteDeal();
|
|
877
703
|
const [stageFilter, setStageFilter] = useState(null);
|
|
704
|
+
const [priorityFilter, setPriorityFilter] = useState("all");
|
|
878
705
|
const [searchQuery, setSearchQuery] = useState("");
|
|
879
706
|
const [showBatchDelete, setShowBatchDelete] = useState(false);
|
|
880
|
-
const pagination = usePaginationState(PAGE_SIZE_DEFAULT, [stageFilter, searchQuery]);
|
|
707
|
+
const pagination = usePaginationState(PAGE_SIZE_DEFAULT, [stageFilter, priorityFilter, searchQuery]);
|
|
881
708
|
const {
|
|
882
709
|
data: deals,
|
|
883
710
|
total,
|
|
@@ -889,8 +716,14 @@ function DealsListPage() {
|
|
|
889
716
|
limit: PAGE_SIZE_DEFAULT,
|
|
890
717
|
offset: pagination.offset
|
|
891
718
|
});
|
|
892
|
-
const { sort, toggleSort } = useTableSort("
|
|
893
|
-
const sortedDeals = useMemo(() =>
|
|
719
|
+
const { sort, toggleSort } = useTableSort("priority", "asc");
|
|
720
|
+
const sortedDeals = useMemo(() => {
|
|
721
|
+
const filteredDeals = priorityFilter === "all" ? deals ?? [] : (deals ?? []).filter((deal) => deal.priority.bucketKey === priorityFilter);
|
|
722
|
+
if (sort.column === "priority") {
|
|
723
|
+
return [...filteredDeals].sort((a, b) => compareDealsByPriorityThenUpdated(a, b, sort.direction));
|
|
724
|
+
}
|
|
725
|
+
return sortData(filteredDeals, sort, sortAccessors);
|
|
726
|
+
}, [deals, priorityFilter, sort]);
|
|
894
727
|
const selection = useTableSelection(sortedDeals, sortedDeals);
|
|
895
728
|
const handleDeleteSelected = async () => {
|
|
896
729
|
await Promise.all([...selection.selectedIds].map((dealId) => deleteDeal.mutateAsync(dealId)));
|
|
@@ -939,7 +772,18 @@ function DealsListPage() {
|
|
|
939
772
|
]
|
|
940
773
|
}
|
|
941
774
|
),
|
|
942
|
-
|
|
775
|
+
/* @__PURE__ */ jsx(
|
|
776
|
+
Tabs,
|
|
777
|
+
{
|
|
778
|
+
value: priorityFilter,
|
|
779
|
+
onChange: (value) => setPriorityFilter(value ?? "all"),
|
|
780
|
+
children: /* @__PURE__ */ jsxs(Tabs.List, { children: [
|
|
781
|
+
/* @__PURE__ */ jsx(Tabs.Tab, { value: "all", children: "All priorities" }),
|
|
782
|
+
DEAL_PRIORITY_OPTIONS.map((option) => /* @__PURE__ */ jsx(Tabs.Tab, { value: option.value, children: option.label }, option.value))
|
|
783
|
+
] })
|
|
784
|
+
}
|
|
785
|
+
),
|
|
786
|
+
isLoading ? /* @__PURE__ */ jsx(Center, { p: "xl", children: /* @__PURE__ */ jsx(Loader, {}) }) : error ? /* @__PURE__ */ jsx(CenteredErrorState, { error, title: "Failed to load deals" }) : !sortedDeals.length ? /* @__PURE__ */ jsx(EmptyState, { icon: IconTargetArrow, title: "No deals found" }) : /* @__PURE__ */ jsxs(Table, { style: { tableLayout: "fixed" }, children: [
|
|
943
787
|
/* @__PURE__ */ jsx(Table.Thead, { children: /* @__PURE__ */ jsxs(Table.Tr, { children: [
|
|
944
788
|
/* @__PURE__ */ jsx(Table.Th, { w: 40, children: /* @__PURE__ */ jsx(
|
|
945
789
|
Checkbox,
|
|
@@ -952,333 +796,776 @@ function DealsListPage() {
|
|
|
952
796
|
/* @__PURE__ */ jsx(SortableHeader, { column: "company", sort, onToggle: toggleSort, children: "Company" }),
|
|
953
797
|
/* @__PURE__ */ jsx(SortableHeader, { column: "contact", sort, onToggle: toggleSort, children: "Contact" }),
|
|
954
798
|
/* @__PURE__ */ jsx(SortableHeader, { column: "email", sort, onToggle: toggleSort, children: "Email" }),
|
|
955
|
-
/* @__PURE__ */ jsx(SortableHeader, { column: "stage", sort, onToggle: toggleSort, children: "Stage" }),
|
|
956
|
-
/* @__PURE__ */ jsx(SortableHeader, { column: "
|
|
799
|
+
/* @__PURE__ */ jsx(SortableHeader, { column: "stage", sort, onToggle: toggleSort, w: 120, children: "Stage" }),
|
|
800
|
+
/* @__PURE__ */ jsx(SortableHeader, { column: "state", sort, onToggle: toggleSort, w: 160, children: "State" }),
|
|
801
|
+
/* @__PURE__ */ jsx(SortableHeader, { column: "ownership", sort, onToggle: toggleSort, w: 100, children: "Move" }),
|
|
802
|
+
/* @__PURE__ */ jsx(SortableHeader, { column: "priority", sort, onToggle: toggleSort, w: 140, children: "Priority" }),
|
|
803
|
+
/* @__PURE__ */ jsx(SortableHeader, { column: "updated", sort, onToggle: toggleSort, w: 100, children: "Updated" })
|
|
957
804
|
] }) }),
|
|
958
805
|
/* @__PURE__ */ jsx(Table.Tbody, { children: sortedDeals.map((deal) => {
|
|
959
806
|
const discoveryData = deal.discovery_data;
|
|
960
807
|
const companyName = deal.contact?.company?.name || discoveryData?.company || deal.contact_email?.split("@")[1] || "-";
|
|
961
808
|
const contactName = [deal.contact?.first_name, deal.contact?.last_name].filter(Boolean).join(" ");
|
|
809
|
+
const ownershipBadge = deal.ownership ? DEAL_OWNERSHIP_BADGE_COPY[deal.ownership] : null;
|
|
962
810
|
return /* @__PURE__ */ jsxs(
|
|
963
811
|
Table.Tr,
|
|
964
812
|
{
|
|
965
|
-
style: { cursor: "pointer" },
|
|
966
|
-
onClick: () =>
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
813
|
+
style: { cursor: "pointer" },
|
|
814
|
+
onClick: () => {
|
|
815
|
+
setDealReferrer("deals");
|
|
816
|
+
navigate({
|
|
817
|
+
to: "/crm/deals/$dealId",
|
|
818
|
+
params: { dealId: deal.id }
|
|
819
|
+
});
|
|
820
|
+
},
|
|
821
|
+
children: [
|
|
822
|
+
/* @__PURE__ */ jsx(Table.Td, { onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsx(
|
|
823
|
+
Checkbox,
|
|
824
|
+
{
|
|
825
|
+
checked: selection.isSelected(deal.id),
|
|
826
|
+
onChange: () => selection.toggle(deal.id)
|
|
827
|
+
}
|
|
828
|
+
) }),
|
|
829
|
+
/* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { fw: 500, lineClamp: 1, children: companyName }) }),
|
|
830
|
+
/* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", lineClamp: 1, children: contactName || "-" }) }),
|
|
831
|
+
/* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", lineClamp: 1, children: deal.contact_email || "-" }) }),
|
|
832
|
+
/* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Badge, { variant: "light", color: DEAL_STAGE_COLORS[deal.stage_key || ""] || "gray", size: "sm", children: formatDealStageLabel(deal.stage_key) }) }),
|
|
833
|
+
/* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(
|
|
834
|
+
Badge,
|
|
835
|
+
{
|
|
836
|
+
variant: "light",
|
|
837
|
+
color: deal.state_key ? "gray" : "dark",
|
|
838
|
+
size: "sm",
|
|
839
|
+
style: { maxWidth: "100%", textTransform: "none" },
|
|
840
|
+
children: /* @__PURE__ */ jsx(Text, { span: true, size: "xs", truncate: "end", style: { maxWidth: "100%" }, children: formatDealStateLabel(deal.state_key) })
|
|
841
|
+
}
|
|
842
|
+
) }),
|
|
843
|
+
/* @__PURE__ */ jsx(Table.Td, { children: ownershipBadge ? /* @__PURE__ */ jsx(Tooltip, { label: ownershipBadge.tooltip, withArrow: true, children: /* @__PURE__ */ jsx(
|
|
844
|
+
Badge,
|
|
845
|
+
{
|
|
846
|
+
variant: "light",
|
|
847
|
+
color: ownershipBadge.color,
|
|
848
|
+
size: "sm",
|
|
849
|
+
style: { maxWidth: "100%", textTransform: "none" },
|
|
850
|
+
children: /* @__PURE__ */ jsx(Text, { span: true, size: "xs", truncate: "end", style: { maxWidth: "100%" }, children: ownershipBadge.label })
|
|
851
|
+
}
|
|
852
|
+
) }) : /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: "-" }) }),
|
|
853
|
+
/* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Tooltip, { label: deal.priority.reason, withArrow: true, children: /* @__PURE__ */ jsx(
|
|
854
|
+
Badge,
|
|
855
|
+
{
|
|
856
|
+
variant: "light",
|
|
857
|
+
color: deal.priority.color,
|
|
858
|
+
size: "sm",
|
|
859
|
+
style: { maxWidth: "100%", textTransform: "none" },
|
|
860
|
+
children: /* @__PURE__ */ jsx(Text, { span: true, size: "xs", truncate: "end", style: { maxWidth: "100%" }, children: deal.priority.label })
|
|
861
|
+
}
|
|
862
|
+
) }) }),
|
|
863
|
+
/* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", lineClamp: 1, children: formatTimeAgo(deal.updated_at) }) })
|
|
864
|
+
]
|
|
865
|
+
},
|
|
866
|
+
deal.id
|
|
867
|
+
);
|
|
868
|
+
}) })
|
|
869
|
+
] }),
|
|
870
|
+
total > PAGE_SIZE_DEFAULT && /* @__PURE__ */ jsx(Group, { justify: "flex-start", children: /* @__PURE__ */ jsx(
|
|
871
|
+
Pagination,
|
|
872
|
+
{
|
|
873
|
+
value: pagination.page,
|
|
874
|
+
onChange: pagination.setPage,
|
|
875
|
+
total: pagination.totalPages(total),
|
|
876
|
+
size: "sm"
|
|
877
|
+
}
|
|
878
|
+
) })
|
|
879
|
+
] }) })
|
|
880
|
+
] }),
|
|
881
|
+
/* @__PURE__ */ jsx(
|
|
882
|
+
CustomModal,
|
|
883
|
+
{
|
|
884
|
+
opened: showBatchDelete,
|
|
885
|
+
onClose: () => !deleteDeal.isPending && setShowBatchDelete(false),
|
|
886
|
+
size: "sm",
|
|
887
|
+
loading: deleteDeal.isPending,
|
|
888
|
+
children: /* @__PURE__ */ jsxs(Stack, { gap: "md", children: [
|
|
889
|
+
/* @__PURE__ */ jsxs(Group, { gap: "sm", children: [
|
|
890
|
+
/* @__PURE__ */ jsx(IconAlertTriangle, { size: 24, color: "var(--color-error)" }),
|
|
891
|
+
/* @__PURE__ */ jsxs(Title, { order: 4, children: [
|
|
892
|
+
"Delete ",
|
|
893
|
+
selection.selectedCount,
|
|
894
|
+
" Deals"
|
|
895
|
+
] })
|
|
896
|
+
] }),
|
|
897
|
+
/* @__PURE__ */ jsxs(Text, { size: "sm", children: [
|
|
898
|
+
"Are you sure you want to delete",
|
|
899
|
+
" ",
|
|
900
|
+
/* @__PURE__ */ jsx(Text, { span: true, fw: 600, children: selection.selectedCount }),
|
|
901
|
+
" ",
|
|
902
|
+
"selected deals?"
|
|
903
|
+
] }),
|
|
904
|
+
/* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: "This action cannot be undone." }),
|
|
905
|
+
/* @__PURE__ */ jsxs(Group, { justify: "flex-end", mt: "md", children: [
|
|
906
|
+
/* @__PURE__ */ jsx(Button, { variant: "light", onClick: () => setShowBatchDelete(false), disabled: deleteDeal.isPending, children: "Cancel" }),
|
|
907
|
+
/* @__PURE__ */ jsx(Button, { color: "red", loading: deleteDeal.isPending, onClick: () => void handleDeleteSelected(), children: "Delete" })
|
|
908
|
+
] })
|
|
909
|
+
] })
|
|
910
|
+
}
|
|
911
|
+
)
|
|
912
|
+
] });
|
|
913
|
+
}
|
|
914
|
+
function ActionButton({ action, dealId }) {
|
|
915
|
+
const executeAction = useExecuteAction({ dealId });
|
|
916
|
+
return /* @__PURE__ */ jsx(
|
|
917
|
+
Button,
|
|
918
|
+
{
|
|
919
|
+
variant: "light",
|
|
920
|
+
size: "sm",
|
|
921
|
+
loading: executeAction.isPending,
|
|
922
|
+
onClick: () => executeAction.mutate({ key: action.key }),
|
|
923
|
+
children: action.label
|
|
924
|
+
}
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
function getType(schema) {
|
|
928
|
+
const zodSchema = schema;
|
|
929
|
+
return zodSchema._def?.type ?? zodSchema._def?.typeName ?? "";
|
|
930
|
+
}
|
|
931
|
+
function unwrapField(schema) {
|
|
932
|
+
const type = getType(schema);
|
|
933
|
+
if (type === "optional" || type === "ZodOptional") {
|
|
934
|
+
const inner = schema._def?.innerType;
|
|
935
|
+
if (!inner) return { schema, required: false, nullable: false, defaultValue: void 0 };
|
|
936
|
+
return { ...unwrapField(inner), required: false };
|
|
937
|
+
}
|
|
938
|
+
if (type === "nullable" || type === "ZodNullable") {
|
|
939
|
+
const inner = schema._def?.innerType;
|
|
940
|
+
if (!inner) return { schema, required: true, nullable: true, defaultValue: null };
|
|
941
|
+
return { ...unwrapField(inner), nullable: true };
|
|
942
|
+
}
|
|
943
|
+
if (type === "default" || type === "ZodDefault") {
|
|
944
|
+
const def = schema._def;
|
|
945
|
+
const inner = def?.innerType;
|
|
946
|
+
const defaultValue = typeof def?.defaultValue === "function" ? def.defaultValue() : def?.defaultValue;
|
|
947
|
+
if (!inner) return { schema, required: false, nullable: false, defaultValue };
|
|
948
|
+
return { ...unwrapField(inner), required: false, defaultValue };
|
|
949
|
+
}
|
|
950
|
+
return { schema, required: true, nullable: false, defaultValue: void 0 };
|
|
951
|
+
}
|
|
952
|
+
function getObjectShape(schema) {
|
|
953
|
+
const zodSchema = schema;
|
|
954
|
+
const type = getType(schema);
|
|
955
|
+
if (type !== "object" && type !== "ZodObject") {
|
|
956
|
+
return null;
|
|
957
|
+
}
|
|
958
|
+
const shape = zodSchema.shape;
|
|
959
|
+
if (!shape || typeof shape !== "object" || Array.isArray(shape)) {
|
|
960
|
+
return null;
|
|
961
|
+
}
|
|
962
|
+
return shape;
|
|
963
|
+
}
|
|
964
|
+
function getEnumValues(schema) {
|
|
965
|
+
const zodSchema = schema;
|
|
966
|
+
const values = zodSchema.options ?? zodSchema._def?.values ?? Object.values(zodSchema._def?.entries ?? {});
|
|
967
|
+
if (!Array.isArray(values) || values.some((value) => typeof value !== "string")) {
|
|
968
|
+
return null;
|
|
969
|
+
}
|
|
970
|
+
return values;
|
|
971
|
+
}
|
|
972
|
+
function labelForField(name) {
|
|
973
|
+
return name.replace(/[_-]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/^./, (s) => s.toUpperCase());
|
|
974
|
+
}
|
|
975
|
+
function isLongTextField(name) {
|
|
976
|
+
return ["body", "message", "reply", "replyBody", "emailBody"].includes(name);
|
|
977
|
+
}
|
|
978
|
+
function initialValueFor(field, defaultValue) {
|
|
979
|
+
if (defaultValue !== void 0) {
|
|
980
|
+
if (field.kind === "date" && defaultValue instanceof Date) {
|
|
981
|
+
return defaultValue.toISOString().slice(0, 10);
|
|
982
|
+
}
|
|
983
|
+
return defaultValue;
|
|
984
|
+
}
|
|
985
|
+
switch (field.kind) {
|
|
986
|
+
case "boolean":
|
|
987
|
+
return false;
|
|
988
|
+
case "number":
|
|
989
|
+
return field.required ? 0 : "";
|
|
990
|
+
case "enum":
|
|
991
|
+
return field.required ? field.enumValues?.[0] ?? "" : "";
|
|
992
|
+
case "date":
|
|
993
|
+
case "dateString":
|
|
994
|
+
case "string":
|
|
995
|
+
return "";
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
function describeSchema(schema) {
|
|
999
|
+
if (!schema) {
|
|
1000
|
+
return { supported: false, reason: "This action does not define a payload schema." };
|
|
1001
|
+
}
|
|
1002
|
+
const shape = getObjectShape(schema);
|
|
1003
|
+
if (!shape) {
|
|
1004
|
+
return { supported: false, reason: "This action requires a payload schema that is not a flat object." };
|
|
1005
|
+
}
|
|
1006
|
+
const fields = [];
|
|
1007
|
+
const initialValues = {};
|
|
1008
|
+
for (const [name, rawFieldSchema] of Object.entries(shape)) {
|
|
1009
|
+
const { schema: fieldSchema, required, nullable, defaultValue } = unwrapField(rawFieldSchema);
|
|
1010
|
+
const type = getType(fieldSchema);
|
|
1011
|
+
const zodField = fieldSchema;
|
|
1012
|
+
const stringFormat = zodField._def?.format;
|
|
1013
|
+
let descriptor = null;
|
|
1014
|
+
if (type === "string" || type === "ZodString") {
|
|
1015
|
+
descriptor = {
|
|
1016
|
+
name,
|
|
1017
|
+
label: labelForField(name),
|
|
1018
|
+
kind: stringFormat === "date" ? "dateString" : "string",
|
|
1019
|
+
schema: rawFieldSchema,
|
|
1020
|
+
required,
|
|
1021
|
+
nullable
|
|
1022
|
+
};
|
|
1023
|
+
} else if (type === "number" || type === "ZodNumber") {
|
|
1024
|
+
descriptor = { name, label: labelForField(name), kind: "number", schema: rawFieldSchema, required, nullable };
|
|
1025
|
+
} else if (type === "boolean" || type === "ZodBoolean") {
|
|
1026
|
+
descriptor = { name, label: labelForField(name), kind: "boolean", schema: rawFieldSchema, required, nullable };
|
|
1027
|
+
} else if (type === "enum" || type === "ZodEnum") {
|
|
1028
|
+
const enumValues = getEnumValues(fieldSchema);
|
|
1029
|
+
if (!enumValues) {
|
|
1030
|
+
return { supported: false, reason: `Field "${name}" uses an enum shape that is not supported by this form.` };
|
|
1031
|
+
}
|
|
1032
|
+
descriptor = {
|
|
1033
|
+
name,
|
|
1034
|
+
label: labelForField(name),
|
|
1035
|
+
kind: "enum",
|
|
1036
|
+
schema: rawFieldSchema,
|
|
1037
|
+
required,
|
|
1038
|
+
nullable,
|
|
1039
|
+
enumValues
|
|
1040
|
+
};
|
|
1041
|
+
} else if (type === "date" || type === "ZodDate") {
|
|
1042
|
+
descriptor = { name, label: labelForField(name), kind: "date", schema: rawFieldSchema, required, nullable };
|
|
1043
|
+
}
|
|
1044
|
+
if (!descriptor) {
|
|
1045
|
+
return {
|
|
1046
|
+
supported: false,
|
|
1047
|
+
reason: `Field "${name}" is not supported. CRM action forms currently support only flat string, number, boolean, enum, and date fields.`
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
const field = { ...descriptor, defaultValue: initialValueFor(descriptor, defaultValue) };
|
|
1051
|
+
fields.push(field);
|
|
1052
|
+
initialValues[name] = field.defaultValue;
|
|
1053
|
+
}
|
|
1054
|
+
return { supported: true, fields, initialValues };
|
|
1055
|
+
}
|
|
1056
|
+
function normalizeValue(field, value) {
|
|
1057
|
+
const isEmpty = value === "" || value === void 0 || value === null;
|
|
1058
|
+
if (isEmpty) {
|
|
1059
|
+
if (!field.required) return void 0;
|
|
1060
|
+
if (field.nullable) return null;
|
|
1061
|
+
return value;
|
|
1062
|
+
}
|
|
1063
|
+
if (field.kind === "date") {
|
|
1064
|
+
const acceptsString = (field.schema._def?.coerce ?? false) === true;
|
|
1065
|
+
return acceptsString ? value : new Date(String(value));
|
|
1066
|
+
}
|
|
1067
|
+
return value;
|
|
1068
|
+
}
|
|
1069
|
+
function buildPayload(fields, values) {
|
|
1070
|
+
return fields.reduce((payload, field) => {
|
|
1071
|
+
const value = normalizeValue(field, values[field.name]);
|
|
1072
|
+
if (value !== void 0 || field.required) {
|
|
1073
|
+
payload[field.name] = value;
|
|
1074
|
+
}
|
|
1075
|
+
return payload;
|
|
1076
|
+
}, {});
|
|
1077
|
+
}
|
|
1078
|
+
function ActionFormButton({ action, isActive, onToggle }) {
|
|
1079
|
+
return /* @__PURE__ */ jsx(Button, { variant: isActive ? "filled" : "light", size: "sm", onClick: onToggle, children: action.label });
|
|
1080
|
+
}
|
|
1081
|
+
function ActionForm({ action, dealId, onClose }) {
|
|
1082
|
+
const executeAction = useExecuteAction({ dealId });
|
|
1083
|
+
const schemaDescription = describeSchema(action.payloadSchema);
|
|
1084
|
+
const form = useForm({
|
|
1085
|
+
initialValues: schemaDescription.supported ? schemaDescription.initialValues : {}
|
|
1086
|
+
});
|
|
1087
|
+
const handleSubmit = form.onSubmit(async (values) => {
|
|
1088
|
+
if (!schemaDescription.supported || !action.payloadSchema) return;
|
|
1089
|
+
form.clearErrors();
|
|
1090
|
+
const payload = buildPayload(schemaDescription.fields, values);
|
|
1091
|
+
const parsed = action.payloadSchema.safeParse(payload);
|
|
1092
|
+
if (!parsed.success) {
|
|
1093
|
+
const formErrors = {};
|
|
1094
|
+
for (const issue of parsed.error.issues) {
|
|
1095
|
+
const fieldName = issue.path[0];
|
|
1096
|
+
if (typeof fieldName === "string" && schemaDescription.fields.some((field) => field.name === fieldName)) {
|
|
1097
|
+
formErrors[fieldName] = issue.message;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
form.setErrors(formErrors);
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
await executeAction.mutateAsync({ key: action.key, payload: parsed.data });
|
|
1104
|
+
onClose();
|
|
1105
|
+
});
|
|
1106
|
+
return /* @__PURE__ */ jsxs(Stack, { gap: "sm", children: [
|
|
1107
|
+
/* @__PURE__ */ jsx(Divider, {}),
|
|
1108
|
+
/* @__PURE__ */ jsx(Title, { order: 3, children: action.label }),
|
|
1109
|
+
schemaDescription.supported ? /* @__PURE__ */ jsx("form", { onSubmit: handleSubmit, children: /* @__PURE__ */ jsxs(Stack, { gap: "md", children: [
|
|
1110
|
+
schemaDescription.fields.map((field) => {
|
|
1111
|
+
if (field.kind === "number") {
|
|
1112
|
+
return /* @__PURE__ */ jsx(
|
|
1113
|
+
NumberInput,
|
|
1114
|
+
{
|
|
1115
|
+
label: field.label,
|
|
1116
|
+
required: field.required,
|
|
1117
|
+
disabled: executeAction.isPending,
|
|
1118
|
+
error: form.errors[field.name],
|
|
1119
|
+
...form.getInputProps(field.name)
|
|
1120
|
+
},
|
|
1121
|
+
field.name
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
if (field.kind === "boolean") {
|
|
1125
|
+
return /* @__PURE__ */ jsx(
|
|
1126
|
+
Switch,
|
|
1127
|
+
{
|
|
1128
|
+
label: field.label,
|
|
1129
|
+
disabled: executeAction.isPending,
|
|
1130
|
+
error: form.errors[field.name],
|
|
1131
|
+
...form.getInputProps(field.name, { type: "checkbox" })
|
|
1132
|
+
},
|
|
1133
|
+
field.name
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
if (field.kind === "enum") {
|
|
1137
|
+
return /* @__PURE__ */ jsx(
|
|
1138
|
+
Select,
|
|
1139
|
+
{
|
|
1140
|
+
label: field.label,
|
|
1141
|
+
required: field.required,
|
|
1142
|
+
disabled: executeAction.isPending,
|
|
1143
|
+
data: field.enumValues ?? [],
|
|
1144
|
+
error: form.errors[field.name],
|
|
1145
|
+
...form.getInputProps(field.name)
|
|
1146
|
+
},
|
|
1147
|
+
field.name
|
|
1148
|
+
);
|
|
1149
|
+
}
|
|
1150
|
+
if (field.kind === "string" && isLongTextField(field.name)) {
|
|
1151
|
+
return /* @__PURE__ */ jsx(
|
|
1152
|
+
Textarea,
|
|
1153
|
+
{
|
|
1154
|
+
label: field.label,
|
|
1155
|
+
required: field.required,
|
|
1156
|
+
disabled: executeAction.isPending,
|
|
1157
|
+
error: form.errors[field.name],
|
|
1158
|
+
autosize: true,
|
|
1159
|
+
minRows: 6,
|
|
1160
|
+
...form.getInputProps(field.name)
|
|
1161
|
+
},
|
|
1162
|
+
field.name
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
return /* @__PURE__ */ jsx(
|
|
1166
|
+
TextInput,
|
|
1167
|
+
{
|
|
1168
|
+
label: field.label,
|
|
1169
|
+
type: field.kind === "date" || field.kind === "dateString" ? "date" : "text",
|
|
1170
|
+
required: field.required,
|
|
1171
|
+
disabled: executeAction.isPending,
|
|
1172
|
+
error: form.errors[field.name],
|
|
1173
|
+
...form.getInputProps(field.name)
|
|
1174
|
+
},
|
|
1175
|
+
field.name
|
|
1176
|
+
);
|
|
1177
|
+
}),
|
|
1178
|
+
/* @__PURE__ */ jsxs(Group, { justify: "flex-end", gap: "sm", children: [
|
|
1179
|
+
/* @__PURE__ */ jsx(Button, { variant: "default", onClick: onClose, disabled: executeAction.isPending, children: "Cancel" }),
|
|
1180
|
+
/* @__PURE__ */ jsx(Button, { type: "submit", loading: executeAction.isPending, children: action.label })
|
|
1181
|
+
] })
|
|
1182
|
+
] }) }) : /* @__PURE__ */ jsxs(Stack, { gap: "md", children: [
|
|
1183
|
+
/* @__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 }) }),
|
|
1184
|
+
/* @__PURE__ */ jsx(Group, { justify: "flex-end", children: /* @__PURE__ */ jsx(Button, { variant: "default", onClick: onClose, disabled: executeAction.isPending, children: "Close" }) })
|
|
1185
|
+
] })
|
|
1186
|
+
] });
|
|
1187
|
+
}
|
|
1188
|
+
var THREAD_INDENT_STEP = 6;
|
|
1189
|
+
var THREAD_MAX_INDENT = 36;
|
|
1190
|
+
function formatSentAt(sentAt) {
|
|
1191
|
+
if (!sentAt) return "Unknown time";
|
|
1192
|
+
const date = new Date(sentAt);
|
|
1193
|
+
if (Number.isNaN(date.getTime())) return sentAt;
|
|
1194
|
+
return date.toLocaleString();
|
|
1195
|
+
}
|
|
1196
|
+
function getQuoteDepth(line) {
|
|
1197
|
+
const match = line.match(/^\s*((?:>\s*)+)(.*)$/);
|
|
1198
|
+
if (!match) return { depth: 0, text: line };
|
|
1199
|
+
return {
|
|
1200
|
+
depth: (match[1].match(/>/g) ?? []).length,
|
|
1201
|
+
text: match[2]
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
function MessageBody({ body }) {
|
|
1205
|
+
const resolvedBody = body || "Full message body unavailable from Instantly.";
|
|
1206
|
+
return /* @__PURE__ */ jsx(Stack, { gap: 0, children: resolvedBody.split(/\r?\n/).map((line, index) => {
|
|
1207
|
+
const { depth, text } = getQuoteDepth(line);
|
|
1208
|
+
return /* @__PURE__ */ jsxs(
|
|
1209
|
+
Box,
|
|
1210
|
+
{
|
|
1211
|
+
style: {
|
|
1212
|
+
display: "grid",
|
|
1213
|
+
gridTemplateColumns: `${depth * 12}px minmax(0, 1fr)`,
|
|
1214
|
+
minHeight: "1.55em"
|
|
1215
|
+
},
|
|
1216
|
+
children: [
|
|
1217
|
+
/* @__PURE__ */ jsx(
|
|
1218
|
+
Box,
|
|
1219
|
+
{
|
|
1220
|
+
"aria-hidden": "true",
|
|
1221
|
+
style: {
|
|
1222
|
+
display: "grid",
|
|
1223
|
+
gridTemplateColumns: `repeat(${depth}, 12px)`
|
|
1224
|
+
},
|
|
1225
|
+
children: Array.from({ length: depth }).map((_, depthIndex) => /* @__PURE__ */ jsx(
|
|
1226
|
+
Box,
|
|
1227
|
+
{
|
|
1228
|
+
style: {
|
|
1229
|
+
borderLeft: "1px solid var(--color-border)",
|
|
1230
|
+
marginLeft: 4
|
|
1231
|
+
}
|
|
1232
|
+
},
|
|
1233
|
+
depthIndex
|
|
1234
|
+
))
|
|
1235
|
+
}
|
|
1236
|
+
),
|
|
1237
|
+
/* @__PURE__ */ jsx(Text, { size: "sm", lh: 1.55, style: { whiteSpace: "pre-wrap", overflowWrap: "anywhere" }, children: text || " " })
|
|
1238
|
+
]
|
|
1239
|
+
},
|
|
1240
|
+
`${index}-${line}`
|
|
1241
|
+
);
|
|
1242
|
+
}) });
|
|
1243
|
+
}
|
|
1244
|
+
function ConversationThread({ messages }) {
|
|
1245
|
+
if (messages.length === 0) {
|
|
1246
|
+
return /* @__PURE__ */ jsxs(Stack, { gap: "xs", children: [
|
|
1247
|
+
/* @__PURE__ */ jsx(CardHeader, { icon: /* @__PURE__ */ jsx(IconMessages, { size: 18 }), title: "Conversation", mb: "xs" }),
|
|
1248
|
+
/* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: "No synced email thread is available. Check Activity for local CRM events." })
|
|
1249
|
+
] });
|
|
1250
|
+
}
|
|
1251
|
+
return /* @__PURE__ */ jsxs(Stack, { gap: "md", children: [
|
|
1252
|
+
/* @__PURE__ */ jsx(CardHeader, { icon: /* @__PURE__ */ jsx(IconMessages, { size: 18 }), title: "Conversation", mb: 0 }),
|
|
1253
|
+
[...messages].reverse().map((message, index) => {
|
|
1254
|
+
const isOutbound = message.direction === "outbound";
|
|
1255
|
+
const participant = isOutbound ? message.toEmail : message.fromEmail;
|
|
1256
|
+
const olderMessageIndent = Math.min(index * THREAD_INDENT_STEP, THREAD_MAX_INDENT);
|
|
1257
|
+
const olderMessageLevels = olderMessageIndent / THREAD_INDENT_STEP;
|
|
1258
|
+
const directionLabel = isOutbound ? "Sent to" : "Received from";
|
|
1259
|
+
const messageIcon = isOutbound ? /* @__PURE__ */ jsx(IconMailForward, { size: 16 }) : /* @__PURE__ */ jsx(IconInbox, { size: 16 });
|
|
1260
|
+
return /* @__PURE__ */ jsxs(
|
|
1261
|
+
Box,
|
|
1262
|
+
{
|
|
1263
|
+
style: {
|
|
1264
|
+
display: "grid",
|
|
1265
|
+
gridTemplateColumns: `${olderMessageIndent}px minmax(0, 1fr)`
|
|
1266
|
+
},
|
|
1267
|
+
children: [
|
|
1268
|
+
/* @__PURE__ */ jsx(
|
|
1269
|
+
Box,
|
|
1270
|
+
{
|
|
1271
|
+
"aria-hidden": "true",
|
|
1272
|
+
style: {
|
|
1273
|
+
display: "grid",
|
|
1274
|
+
gridTemplateColumns: `repeat(${olderMessageLevels}, ${THREAD_INDENT_STEP}px)`
|
|
1275
|
+
},
|
|
1276
|
+
children: Array.from({ length: olderMessageLevels }).map((_, levelIndex) => /* @__PURE__ */ jsx(
|
|
1277
|
+
Box,
|
|
1278
|
+
{
|
|
1279
|
+
style: {
|
|
1280
|
+
borderLeft: "1px solid var(--color-border)"
|
|
1281
|
+
}
|
|
1282
|
+
},
|
|
1283
|
+
levelIndex
|
|
1284
|
+
))
|
|
1285
|
+
}
|
|
1286
|
+
),
|
|
1287
|
+
/* @__PURE__ */ jsx(
|
|
1288
|
+
Box,
|
|
1289
|
+
{
|
|
1290
|
+
pl: "md",
|
|
1291
|
+
py: "xs",
|
|
1292
|
+
style: {
|
|
1293
|
+
borderLeft: `2px solid ${isOutbound ? "var(--mantine-color-blue-5)" : "var(--mantine-color-teal-5)"}`
|
|
1294
|
+
},
|
|
1295
|
+
children: /* @__PURE__ */ jsxs(Stack, { gap: 6, children: [
|
|
1296
|
+
/* @__PURE__ */ jsx(
|
|
1297
|
+
CardHeader,
|
|
973
1298
|
{
|
|
974
|
-
|
|
975
|
-
|
|
1299
|
+
icon: messageIcon,
|
|
1300
|
+
iconColor: isOutbound ? "blue" : "teal",
|
|
1301
|
+
title: `${directionLabel} ${participant || "unknown recipient"}`,
|
|
1302
|
+
subtitle: message.subject ?? void 0,
|
|
1303
|
+
titleOrder: 5,
|
|
1304
|
+
mb: 0,
|
|
1305
|
+
rightSection: /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", style: { flexShrink: 0, textAlign: "right" }, children: formatSentAt(message.sentAt) })
|
|
976
1306
|
}
|
|
977
|
-
)
|
|
978
|
-
/* @__PURE__ */ jsx(
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
}) })
|
|
988
|
-
] }),
|
|
989
|
-
total > PAGE_SIZE_DEFAULT && /* @__PURE__ */ jsx(Group, { justify: "center", children: /* @__PURE__ */ jsx(
|
|
990
|
-
Pagination,
|
|
991
|
-
{
|
|
992
|
-
value: pagination.page,
|
|
993
|
-
onChange: pagination.setPage,
|
|
994
|
-
total: pagination.totalPages(total),
|
|
995
|
-
size: "sm"
|
|
996
|
-
}
|
|
997
|
-
) })
|
|
998
|
-
] }) })
|
|
999
|
-
] }),
|
|
1000
|
-
/* @__PURE__ */ jsx(
|
|
1001
|
-
CustomModal,
|
|
1002
|
-
{
|
|
1003
|
-
opened: showBatchDelete,
|
|
1004
|
-
onClose: () => !deleteDeal.isPending && setShowBatchDelete(false),
|
|
1005
|
-
size: "sm",
|
|
1006
|
-
loading: deleteDeal.isPending,
|
|
1007
|
-
children: /* @__PURE__ */ jsxs(Stack, { gap: "md", children: [
|
|
1008
|
-
/* @__PURE__ */ jsxs(Group, { gap: "sm", children: [
|
|
1009
|
-
/* @__PURE__ */ jsx(IconAlertTriangle, { size: 24, color: "var(--color-error)" }),
|
|
1010
|
-
/* @__PURE__ */ jsxs(Title, { order: 4, children: [
|
|
1011
|
-
"Delete ",
|
|
1012
|
-
selection.selectedCount,
|
|
1013
|
-
" Deals"
|
|
1014
|
-
] })
|
|
1015
|
-
] }),
|
|
1016
|
-
/* @__PURE__ */ jsxs(Text, { size: "sm", children: [
|
|
1017
|
-
"Are you sure you want to delete",
|
|
1018
|
-
" ",
|
|
1019
|
-
/* @__PURE__ */ jsx(Text, { span: true, fw: 600, children: selection.selectedCount }),
|
|
1020
|
-
" ",
|
|
1021
|
-
"selected deals?"
|
|
1022
|
-
] }),
|
|
1023
|
-
/* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: "This action cannot be undone." }),
|
|
1024
|
-
/* @__PURE__ */ jsxs(Group, { justify: "flex-end", mt: "md", children: [
|
|
1025
|
-
/* @__PURE__ */ jsx(Button, { variant: "light", onClick: () => setShowBatchDelete(false), disabled: deleteDeal.isPending, children: "Cancel" }),
|
|
1026
|
-
/* @__PURE__ */ jsx(Button, { color: "red", loading: deleteDeal.isPending, onClick: () => void handleDeleteSelected(), children: "Delete" })
|
|
1027
|
-
] })
|
|
1028
|
-
] })
|
|
1029
|
-
}
|
|
1030
|
-
)
|
|
1307
|
+
),
|
|
1308
|
+
/* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(MessageBody, { body: message.body }) })
|
|
1309
|
+
] })
|
|
1310
|
+
}
|
|
1311
|
+
)
|
|
1312
|
+
]
|
|
1313
|
+
},
|
|
1314
|
+
message.id
|
|
1315
|
+
);
|
|
1316
|
+
})
|
|
1031
1317
|
] });
|
|
1032
1318
|
}
|
|
1033
|
-
|
|
1319
|
+
var CANONICAL_ACTION_KEYS = /* @__PURE__ */ new Set(["send_reply"]);
|
|
1320
|
+
var DEAL_OWNERSHIP_BADGE_COPY2 = {
|
|
1321
|
+
us: {
|
|
1322
|
+
color: "red",
|
|
1323
|
+
label: "Our move"
|
|
1324
|
+
},
|
|
1325
|
+
them: {
|
|
1326
|
+
color: "gray",
|
|
1327
|
+
label: "Their move"
|
|
1328
|
+
}
|
|
1329
|
+
};
|
|
1330
|
+
function getContactName(deal) {
|
|
1331
|
+
return [deal.contact?.first_name, deal.contact?.last_name].filter(Boolean).join(" ");
|
|
1332
|
+
}
|
|
1333
|
+
function getCompanyName(deal) {
|
|
1334
|
+
return deal.contact?.company?.name || deal.discovery_data?.company || deal.contact_email?.split("@")[1] || "Unknown";
|
|
1335
|
+
}
|
|
1336
|
+
function toAction(actionDef) {
|
|
1337
|
+
const { key, label, payloadSchema } = actionDef;
|
|
1338
|
+
return { key, label, payloadSchema };
|
|
1339
|
+
}
|
|
1340
|
+
function getVisibleCanonicalDealActions(deal, crmActions) {
|
|
1341
|
+
if (deal.nextAction && CANONICAL_ACTION_KEYS.has(deal.nextAction)) {
|
|
1342
|
+
if (deal.nextAction === "send_reply" && deal.ownership === "them") return [];
|
|
1343
|
+
const nextAction = crmActions.find((action) => action.key === deal.nextAction);
|
|
1344
|
+
return nextAction ? [toAction(nextAction)] : [];
|
|
1345
|
+
}
|
|
1346
|
+
return deriveActions(deal, crmActions).filter((action) => {
|
|
1347
|
+
if (!CANONICAL_ACTION_KEYS.has(action.key)) return false;
|
|
1348
|
+
if (action.key === "send_reply" && deal.ownership === "them") return false;
|
|
1349
|
+
return true;
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
function CopyBookingLinkButton({ dealId, contactId }) {
|
|
1034
1353
|
const url = buildBookingUrl({ dealId, contactId });
|
|
1035
|
-
return /* @__PURE__ */ jsx(
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1354
|
+
return /* @__PURE__ */ jsx(CopyButton, { value: url, timeout: 1500, children: ({ copied, copy }) => /* @__PURE__ */ jsx(
|
|
1355
|
+
Button,
|
|
1356
|
+
{
|
|
1357
|
+
variant: "light",
|
|
1358
|
+
size: "xs",
|
|
1359
|
+
color: copied ? "teal" : void 0,
|
|
1360
|
+
leftSection: copied ? /* @__PURE__ */ jsx(IconCheck, { size: 14 }) : /* @__PURE__ */ jsx(IconCopy, { size: 14 }),
|
|
1361
|
+
onClick: copy,
|
|
1362
|
+
children: copied ? "Copied" : "Copy Booking Link"
|
|
1363
|
+
}
|
|
1364
|
+
) });
|
|
1365
|
+
}
|
|
1366
|
+
function DetailRow({ label, value }) {
|
|
1367
|
+
return /* @__PURE__ */ jsxs(Group, { gap: "xs", align: "flex-start", wrap: "nowrap", children: [
|
|
1368
|
+
/* @__PURE__ */ jsx(Text, { size: "sm", fw: 500, style: { width: 120, flexShrink: 0 }, children: label }),
|
|
1369
|
+
/* @__PURE__ */ jsx(Text, { size: "sm", style: { minWidth: 0, wordBreak: "break-word", color: "var(--color-text-subtle)" }, children: value || "N/A" })
|
|
1370
|
+
] });
|
|
1050
1371
|
}
|
|
1051
1372
|
function DealDetailPage({ dealId, renderActions, onDealLoaded }) {
|
|
1052
1373
|
const navigate = useNavigate();
|
|
1053
|
-
const
|
|
1374
|
+
const [referrer] = useState(() => getDealReferrer());
|
|
1375
|
+
const backTarget = referrer === "pipeline" ? { to: "/crm/pipeline", label: "Pipeline" } : { to: "/crm/deals", label: "Deals" };
|
|
1054
1376
|
const { data: deal, isLoading, error } = useDealDetail(dealId);
|
|
1055
|
-
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
|
1056
1377
|
const crmActions = useCrmActions();
|
|
1057
|
-
const actions = useMemo(
|
|
1378
|
+
const actions = useMemo(
|
|
1379
|
+
() => deal ? getVisibleCanonicalDealActions(deal, crmActions) : [],
|
|
1380
|
+
[deal, crmActions]
|
|
1381
|
+
);
|
|
1382
|
+
const [activeActionKey, setActiveActionKey] = useState(null);
|
|
1383
|
+
const activeAction = activeActionKey ? actions.find((action) => action.key === activeActionKey) : null;
|
|
1384
|
+
useEffect(() => {
|
|
1385
|
+
if (activeActionKey && !actions.some((action) => action.key === activeActionKey)) {
|
|
1386
|
+
setActiveActionKey(null);
|
|
1387
|
+
}
|
|
1388
|
+
}, [actions, activeActionKey]);
|
|
1058
1389
|
useEffect(() => {
|
|
1059
1390
|
if (deal) onDealLoaded?.(deal);
|
|
1060
1391
|
}, [deal, onDealLoaded]);
|
|
1061
|
-
const
|
|
1062
|
-
const
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
);
|
|
1066
|
-
const
|
|
1067
|
-
|
|
1068
|
-
return deal.contact?.company?.name || deal.discovery_data?.company || deal.contact_email?.split("@")[1] || "Unknown";
|
|
1069
|
-
}, [deal]);
|
|
1070
|
-
const headerActions = deal ? /* @__PURE__ */ jsxs(Group, { children: [
|
|
1392
|
+
const titleName = deal ? getContactName(deal) || getCompanyName(deal) : null;
|
|
1393
|
+
const title = titleName ? `${titleName} Deal` : "Deal Detail";
|
|
1394
|
+
const companyName = deal ? getCompanyName(deal) : "Unknown";
|
|
1395
|
+
const ownershipBadge = deal?.ownership ? DEAL_OWNERSHIP_BADGE_COPY2[deal.ownership] : null;
|
|
1396
|
+
const activityLog = (deal?.activity_log || []).filter(Boolean);
|
|
1397
|
+
const hasDiscoveryData = deal?.discovery_data != null && (typeof deal.discovery_data !== "object" || Array.isArray(deal.discovery_data) || Object.keys(deal.discovery_data).length > 0);
|
|
1398
|
+
const headerActions = /* @__PURE__ */ jsxs(Group, { gap: "xs", children: [
|
|
1071
1399
|
/* @__PURE__ */ jsx(
|
|
1072
1400
|
Button,
|
|
1073
1401
|
{
|
|
1074
1402
|
variant: "light",
|
|
1075
1403
|
size: "sm",
|
|
1076
1404
|
leftSection: /* @__PURE__ */ jsx(IconArrowLeft, { size: 16 }),
|
|
1077
|
-
onClick: () => navigate({ to:
|
|
1078
|
-
children:
|
|
1405
|
+
onClick: () => navigate({ to: backTarget.to }),
|
|
1406
|
+
children: backTarget.label
|
|
1079
1407
|
}
|
|
1080
1408
|
),
|
|
1081
|
-
deal
|
|
1409
|
+
deal?.proposal_pdf_url && /* @__PURE__ */ jsx(
|
|
1082
1410
|
Button,
|
|
1083
1411
|
{
|
|
1084
1412
|
variant: "light",
|
|
1413
|
+
size: "sm",
|
|
1085
1414
|
leftSection: /* @__PURE__ */ jsx(IconFileText, { size: 16 }),
|
|
1086
1415
|
onClick: () => window.open(deal.proposal_pdf_url, "_blank"),
|
|
1087
1416
|
children: "View Proposal"
|
|
1088
1417
|
}
|
|
1089
1418
|
),
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
),
|
|
1093
|
-
renderActions?.(deal),
|
|
1094
|
-
/* @__PURE__ */ jsx(ActionIcon, { variant: "subtle", color: "red", onClick: () => setDeleteModalOpen(true), children: /* @__PURE__ */ jsx(IconTrash, { size: 16 }) })
|
|
1095
|
-
] }) : /* @__PURE__ */ jsx(
|
|
1096
|
-
Button,
|
|
1097
|
-
{
|
|
1098
|
-
variant: "light",
|
|
1099
|
-
size: "sm",
|
|
1100
|
-
leftSection: /* @__PURE__ */ jsx(IconArrowLeft, { size: 16 }),
|
|
1101
|
-
onClick: () => navigate({ to: "/crm/deals" }),
|
|
1102
|
-
children: "Deals"
|
|
1103
|
-
}
|
|
1104
|
-
);
|
|
1419
|
+
deal && renderActions?.(deal)
|
|
1420
|
+
] });
|
|
1105
1421
|
if (isLoading) {
|
|
1106
|
-
return /* @__PURE__ */ jsx(SubshellContentContainer, { children: /* @__PURE__ */
|
|
1107
|
-
/* @__PURE__ */ jsx(
|
|
1422
|
+
return /* @__PURE__ */ jsx(SubshellContentContainer, { children: /* @__PURE__ */ jsx(PageContainer, { children: /* @__PURE__ */ jsxs(Stack, { children: [
|
|
1423
|
+
/* @__PURE__ */ jsx(PageTitleCaption, { title, caption: "Loading deal details...", rightSection: headerActions }),
|
|
1108
1424
|
/* @__PURE__ */ jsx(Paper, { withBorder: true, children: /* @__PURE__ */ jsx(Center, { p: "xl", children: /* @__PURE__ */ jsx(Loader, {}) }) })
|
|
1109
|
-
] }) });
|
|
1425
|
+
] }) }) });
|
|
1110
1426
|
}
|
|
1111
1427
|
if (error) {
|
|
1112
|
-
return /* @__PURE__ */ jsx(SubshellContentContainer, { children: /* @__PURE__ */
|
|
1113
|
-
/* @__PURE__ */ jsx(
|
|
1428
|
+
return /* @__PURE__ */ jsx(SubshellContentContainer, { children: /* @__PURE__ */ jsx(PageContainer, { children: /* @__PURE__ */ jsxs(Stack, { children: [
|
|
1429
|
+
/* @__PURE__ */ jsx(PageTitleCaption, { title, caption: "Unable to load deal details", rightSection: headerActions }),
|
|
1114
1430
|
/* @__PURE__ */ jsx(Paper, { withBorder: true, children: /* @__PURE__ */ jsx(CenteredErrorState, { error, title: "Failed to load deal" }) })
|
|
1115
|
-
] }) });
|
|
1431
|
+
] }) }) });
|
|
1116
1432
|
}
|
|
1117
1433
|
if (!deal) {
|
|
1118
|
-
return /* @__PURE__ */ jsx(SubshellContentContainer, { children: /* @__PURE__ */
|
|
1119
|
-
/* @__PURE__ */ jsx(
|
|
1120
|
-
/* @__PURE__ */ jsx(Paper, { withBorder: true, children: /* @__PURE__ */ jsx(EmptyState, { icon:
|
|
1121
|
-
] }) });
|
|
1434
|
+
return /* @__PURE__ */ jsx(SubshellContentContainer, { children: /* @__PURE__ */ jsx(PageContainer, { children: /* @__PURE__ */ jsxs(Stack, { children: [
|
|
1435
|
+
/* @__PURE__ */ jsx(PageTitleCaption, { title, caption: "Deal not found", rightSection: headerActions }),
|
|
1436
|
+
/* @__PURE__ */ jsx(Paper, { withBorder: true, children: /* @__PURE__ */ jsx(EmptyState, { icon: IconInbox, title: "Deal not found", description: "The selected deal no longer exists." }) })
|
|
1437
|
+
] }) }) });
|
|
1122
1438
|
}
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
/* @__PURE__ */ jsx(Stack, { children: /* @__PURE__ */ jsx(
|
|
1439
|
+
return /* @__PURE__ */ jsx(SubshellContentContainer, { children: /* @__PURE__ */ jsx(PageContainer, { children: /* @__PURE__ */ jsxs(Stack, { gap: "md", children: [
|
|
1440
|
+
/* @__PURE__ */ jsx(
|
|
1126
1441
|
PageTitleCaption,
|
|
1127
1442
|
{
|
|
1128
1443
|
title,
|
|
1129
|
-
caption: `${companyName
|
|
1444
|
+
caption: `${companyName} - ${formatDealStageLabel(deal.stage_key)}`,
|
|
1130
1445
|
rightSection: headerActions
|
|
1131
1446
|
}
|
|
1132
|
-
)
|
|
1133
|
-
/* @__PURE__ */ jsx(Paper, { withBorder: true, children: /* @__PURE__ */ jsxs(
|
|
1134
|
-
/* @__PURE__ */ jsxs(
|
|
1135
|
-
/* @__PURE__ */ jsx(
|
|
1136
|
-
|
|
1137
|
-
|
|
1447
|
+
),
|
|
1448
|
+
/* @__PURE__ */ jsx(Paper, { withBorder: true, children: /* @__PURE__ */ jsxs(Stack, { gap: "lg", children: [
|
|
1449
|
+
/* @__PURE__ */ jsxs(Stack, { gap: "md", children: [
|
|
1450
|
+
/* @__PURE__ */ jsx(
|
|
1451
|
+
CardHeader,
|
|
1452
|
+
{
|
|
1453
|
+
icon: /* @__PURE__ */ jsx(IconBriefcase, { size: 18 }),
|
|
1454
|
+
title: "Deal",
|
|
1455
|
+
mb: 0,
|
|
1456
|
+
rightSection: /* @__PURE__ */ jsx(CopyBookingLinkButton, { dealId: deal.id, contactId: deal.contact?.id })
|
|
1457
|
+
}
|
|
1458
|
+
),
|
|
1459
|
+
/* @__PURE__ */ jsxs(SimpleGrid, { cols: { base: 1, sm: 2 }, spacing: "sm", children: [
|
|
1460
|
+
/* @__PURE__ */ jsx(
|
|
1461
|
+
DetailRow,
|
|
1462
|
+
{
|
|
1463
|
+
label: "Stage",
|
|
1464
|
+
value: /* @__PURE__ */ jsx(Badge, { color: DEAL_STAGE_COLORS[deal.stage_key || ""] || "gray", children: formatDealStageLabel(deal.stage_key) })
|
|
1465
|
+
}
|
|
1466
|
+
),
|
|
1467
|
+
/* @__PURE__ */ jsx(
|
|
1468
|
+
DetailRow,
|
|
1469
|
+
{
|
|
1470
|
+
label: "Priority",
|
|
1471
|
+
value: /* @__PURE__ */ jsx(Tooltip, { label: deal.priority.reason, withArrow: true, children: /* @__PURE__ */ jsx(
|
|
1472
|
+
Badge,
|
|
1473
|
+
{
|
|
1474
|
+
variant: "light",
|
|
1475
|
+
color: deal.priority.color,
|
|
1476
|
+
size: "sm",
|
|
1477
|
+
style: { maxWidth: "100%", textTransform: "none" },
|
|
1478
|
+
children: /* @__PURE__ */ jsx(Text, { span: true, size: "xs", truncate: "end", style: { maxWidth: "100%" }, children: deal.priority.label })
|
|
1479
|
+
}
|
|
1480
|
+
) })
|
|
1481
|
+
}
|
|
1482
|
+
),
|
|
1483
|
+
/* @__PURE__ */ jsx(
|
|
1484
|
+
DetailRow,
|
|
1485
|
+
{
|
|
1486
|
+
label: "Move",
|
|
1487
|
+
value: ownershipBadge ? /* @__PURE__ */ jsx(
|
|
1488
|
+
Badge,
|
|
1489
|
+
{
|
|
1490
|
+
variant: "light",
|
|
1491
|
+
color: ownershipBadge.color,
|
|
1492
|
+
size: "sm",
|
|
1493
|
+
style: { maxWidth: "100%", textTransform: "none" },
|
|
1494
|
+
children: /* @__PURE__ */ jsx(Text, { span: true, size: "xs", truncate: "end", style: { maxWidth: "100%" }, children: ownershipBadge.label })
|
|
1495
|
+
}
|
|
1496
|
+
) : "N/A"
|
|
1497
|
+
}
|
|
1498
|
+
),
|
|
1499
|
+
/* @__PURE__ */ jsx(
|
|
1500
|
+
DetailRow,
|
|
1501
|
+
{
|
|
1502
|
+
label: "Sent",
|
|
1503
|
+
value: deal.proposal_sent_at ? new Date(deal.proposal_sent_at).toLocaleString() : "N/A"
|
|
1504
|
+
}
|
|
1505
|
+
),
|
|
1506
|
+
/* @__PURE__ */ jsx(DetailRow, { label: "Envelope ID", value: deal.signature_envelope_id || "N/A" }),
|
|
1507
|
+
/* @__PURE__ */ jsx(DetailRow, { label: "Contact Email", value: deal.contact?.email || deal.contact_email || "N/A" })
|
|
1508
|
+
] })
|
|
1138
1509
|
] }),
|
|
1139
|
-
/* @__PURE__ */
|
|
1140
|
-
/* @__PURE__ */ jsx(
|
|
1141
|
-
|
|
1142
|
-
/* @__PURE__ */
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
/* @__PURE__ */
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
] }),
|
|
1170
|
-
/* @__PURE__ */ jsxs(Group, { children: [
|
|
1171
|
-
/* @__PURE__ */ jsx(Text, { fw: 500, children: "Pipeline Status:" }),
|
|
1172
|
-
/* @__PURE__ */ jsx(Text, { size: "sm", children: deal.contact?.pipeline_status ? Object.entries(deal.contact.pipeline_status).map(([key, val]) => {
|
|
1173
|
-
const status = val?.status;
|
|
1174
|
-
return status ? `${key}: ${status}` : null;
|
|
1175
|
-
}).filter(Boolean).join(", ") || "N/A" : "N/A" })
|
|
1176
|
-
] }),
|
|
1177
|
-
deal.contact?.headline && /* @__PURE__ */ jsxs(Group, { children: [
|
|
1178
|
-
/* @__PURE__ */ jsx(Text, { fw: 500, children: "Headline:" }),
|
|
1179
|
-
/* @__PURE__ */ jsx(Text, { children: deal.contact.headline })
|
|
1180
|
-
] }),
|
|
1181
|
-
deal.contact?.linkedin_url && /* @__PURE__ */ jsxs(Group, { children: [
|
|
1182
|
-
/* @__PURE__ */ jsx(Text, { fw: 500, children: "LinkedIn:" }),
|
|
1183
|
-
/* @__PURE__ */ jsx(Text, { component: "a", href: deal.contact.linkedin_url, target: "_blank", c: "blue", children: "View Profile" })
|
|
1184
|
-
] })
|
|
1185
|
-
] })
|
|
1186
|
-
] }) }),
|
|
1187
|
-
deal.contact_id && /* @__PURE__ */ jsx(BookingLinkPanel, { dealId: deal.id, contactId: deal.contact_id }),
|
|
1188
|
-
/* @__PURE__ */ jsx(Card, { withBorder: true, children: /* @__PURE__ */ jsxs(Stack, { gap: "sm", children: [
|
|
1189
|
-
/* @__PURE__ */ jsx(Title, { order: 4, children: "Company" }),
|
|
1190
|
-
/* @__PURE__ */ jsxs(SimpleGrid, { cols: { base: 1, sm: 2 }, spacing: "sm", children: [
|
|
1191
|
-
/* @__PURE__ */ jsxs(Group, { children: [
|
|
1192
|
-
/* @__PURE__ */ jsx(Text, { fw: 500, children: "Name:" }),
|
|
1193
|
-
/* @__PURE__ */ jsx(Text, { children: deal.contact?.company?.name || "N/A" })
|
|
1194
|
-
] }),
|
|
1195
|
-
/* @__PURE__ */ jsxs(Group, { children: [
|
|
1196
|
-
/* @__PURE__ */ jsx(Text, { fw: 500, children: "Domain:" }),
|
|
1197
|
-
/* @__PURE__ */ jsx(Text, { children: deal.contact?.company?.domain || "N/A" })
|
|
1198
|
-
] }),
|
|
1199
|
-
/* @__PURE__ */ jsxs(Group, { children: [
|
|
1200
|
-
/* @__PURE__ */ jsx(Text, { fw: 500, children: "Segment:" }),
|
|
1201
|
-
/* @__PURE__ */ jsx(Text, { children: deal.contact?.company?.segment || "N/A" })
|
|
1202
|
-
] }),
|
|
1203
|
-
/* @__PURE__ */ jsxs(Group, { children: [
|
|
1204
|
-
/* @__PURE__ */ jsx(Text, { fw: 500, children: "Category:" }),
|
|
1205
|
-
/* @__PURE__ */ jsx(Text, { children: deal.contact?.company?.category || "N/A" })
|
|
1206
|
-
] }),
|
|
1207
|
-
/* @__PURE__ */ jsxs(Group, { children: [
|
|
1208
|
-
/* @__PURE__ */ jsx(Text, { fw: 500, children: "Employees:" }),
|
|
1209
|
-
/* @__PURE__ */ jsx(Text, { children: deal.contact?.company?.num_employees || "N/A" })
|
|
1210
|
-
] }),
|
|
1211
|
-
deal.contact?.company?.website && /* @__PURE__ */ jsxs(Group, { children: [
|
|
1212
|
-
/* @__PURE__ */ jsx(Text, { fw: 500, children: "Website:" }),
|
|
1213
|
-
/* @__PURE__ */ jsx(Text, { component: "a", href: deal.contact.company.website, target: "_blank", c: "blue", children: deal.contact.company.website })
|
|
1214
|
-
] })
|
|
1215
|
-
] })
|
|
1216
|
-
] }) }),
|
|
1217
|
-
["closing", "closed_won"].includes(deal.stage_key || "") && /* @__PURE__ */ jsx(Card, { withBorder: true, children: /* @__PURE__ */ jsxs(Stack, { gap: "sm", children: [
|
|
1218
|
-
/* @__PURE__ */ jsx(Title, { order: 4, children: "Payment" }),
|
|
1219
|
-
/* @__PURE__ */ jsxs(SimpleGrid, { cols: { base: 1, sm: 2 }, spacing: "sm", children: [
|
|
1220
|
-
/* @__PURE__ */ jsxs(Group, { children: [
|
|
1221
|
-
/* @__PURE__ */ jsx(Text, { fw: 500, children: "Initial Fee:" }),
|
|
1222
|
-
/* @__PURE__ */ jsxs(Text, { children: [
|
|
1223
|
-
"$",
|
|
1224
|
-
deal.initial_fee?.toLocaleString() || "Not set"
|
|
1225
|
-
] })
|
|
1226
|
-
] }),
|
|
1227
|
-
/* @__PURE__ */ jsxs(Group, { children: [
|
|
1228
|
-
/* @__PURE__ */ jsx(Text, { fw: 500, children: "Monthly Fee:" }),
|
|
1229
|
-
/* @__PURE__ */ jsxs(Text, { children: [
|
|
1230
|
-
"$",
|
|
1231
|
-
deal.monthly_fee?.toLocaleString() || "Not set",
|
|
1232
|
-
"/mo"
|
|
1233
|
-
] })
|
|
1234
|
-
] })
|
|
1235
|
-
] }),
|
|
1236
|
-
deal.stripe_payment_link && /* @__PURE__ */ jsxs(Group, { children: [
|
|
1237
|
-
/* @__PURE__ */ jsx(Text, { fw: 500, children: "Payment Link:" }),
|
|
1238
|
-
/* @__PURE__ */ jsx(Text, { component: "a", href: deal.stripe_payment_link, target: "_blank", c: "blue", children: deal.stripe_payment_link })
|
|
1239
|
-
] })
|
|
1510
|
+
/* @__PURE__ */ jsxs(Stack, { gap: "md", children: [
|
|
1511
|
+
/* @__PURE__ */ jsx(CardHeader, { icon: /* @__PURE__ */ jsx(IconUser, { size: 18 }), title: "Contact", mb: 0 }),
|
|
1512
|
+
/* @__PURE__ */ jsxs(SimpleGrid, { cols: { base: 1, sm: 2 }, spacing: "sm", children: [
|
|
1513
|
+
/* @__PURE__ */ jsx(DetailRow, { label: "Name", value: getContactName(deal) || "N/A" }),
|
|
1514
|
+
/* @__PURE__ */ jsx(DetailRow, { label: "Title", value: deal.contact?.title || "N/A" }),
|
|
1515
|
+
/* @__PURE__ */ jsx(DetailRow, { label: "Company", value: deal.contact?.company?.name || "N/A" }),
|
|
1516
|
+
/* @__PURE__ */ jsx(DetailRow, { label: "Domain", value: deal.contact?.company?.domain || "N/A" }),
|
|
1517
|
+
/* @__PURE__ */ jsx(DetailRow, { label: "Segment", value: deal.contact?.company?.segment || "N/A" }),
|
|
1518
|
+
/* @__PURE__ */ jsx(DetailRow, { label: "Category", value: deal.contact?.company?.category || "N/A" })
|
|
1519
|
+
] })
|
|
1520
|
+
] }),
|
|
1521
|
+
/* @__PURE__ */ jsxs(Tabs, { defaultValue: "conversation", keepMounted: false, children: [
|
|
1522
|
+
/* @__PURE__ */ jsxs(Tabs.List, { children: [
|
|
1523
|
+
/* @__PURE__ */ jsx(Tabs.Tab, { value: "conversation", leftSection: /* @__PURE__ */ jsx(IconMessages, { size: 16 }), children: "Conversation" }),
|
|
1524
|
+
/* @__PURE__ */ jsx(Tabs.Tab, { value: "activity", leftSection: /* @__PURE__ */ jsx(IconHistory, { size: 16 }), children: "Activity" }),
|
|
1525
|
+
/* @__PURE__ */ jsx(Tabs.Tab, { value: "discovery", leftSection: /* @__PURE__ */ jsx(IconSearch, { size: 16 }), children: "Discovery" })
|
|
1526
|
+
] }),
|
|
1527
|
+
/* @__PURE__ */ jsx(Tabs.Panel, { value: "conversation", pt: "md", children: /* @__PURE__ */ jsx(ConversationThread, { messages: deal.conversation.messages }) }),
|
|
1528
|
+
/* @__PURE__ */ jsx(Tabs.Panel, { value: "activity", pt: "md", children: /* @__PURE__ */ jsx(ActivityTimeline, { activities: activityLog }) }),
|
|
1529
|
+
/* @__PURE__ */ jsx(Tabs.Panel, { value: "discovery", pt: "md", children: /* @__PURE__ */ jsxs(Stack, { gap: "md", children: [
|
|
1530
|
+
/* @__PURE__ */ jsx(
|
|
1531
|
+
CardHeader,
|
|
1532
|
+
{
|
|
1533
|
+
icon: /* @__PURE__ */ jsx(IconSearch, { size: 18 }),
|
|
1534
|
+
title: "Discovery",
|
|
1535
|
+
subtitle: "Structured intake answers and qualification context captured before proposal work.",
|
|
1536
|
+
mb: 0
|
|
1537
|
+
}
|
|
1538
|
+
),
|
|
1539
|
+
hasDiscoveryData ? /* @__PURE__ */ jsx(Code, { block: true, children: JSON.stringify(deal.discovery_data, null, 2) }) : /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: "No discovery responses have been captured for this deal." })
|
|
1240
1540
|
] }) })
|
|
1241
|
-
] })
|
|
1242
|
-
/* @__PURE__ */ jsx(Tabs.Panel, { value: "discovery", pt: "md", children: /* @__PURE__ */ jsx(Card, { withBorder: true, children: /* @__PURE__ */ jsx(Code, { block: true, children: JSON.stringify(deal.discovery_data, null, 2) }) }) }),
|
|
1243
|
-
/* @__PURE__ */ jsx(Tabs.Panel, { value: "activity", pt: "md", children: /* @__PURE__ */ jsx(ActivityTimeline, { activities: activityLog }) })
|
|
1541
|
+
] })
|
|
1244
1542
|
] }) }),
|
|
1245
|
-
/* @__PURE__ */ jsx(
|
|
1246
|
-
/* @__PURE__ */
|
|
1247
|
-
|
|
1248
|
-
/* @__PURE__ */ jsx(
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
] }),
|
|
1262
|
-
/* @__PURE__ */ jsx(Divider, {}),
|
|
1263
|
-
/* @__PURE__ */ jsxs(Group, { justify: "flex-end", children: [
|
|
1264
|
-
/* @__PURE__ */ jsx(Button, { variant: "default", onClick: () => setDeleteModalOpen(false), disabled: deleteDeal.isPending, children: "Cancel" }),
|
|
1265
|
-
/* @__PURE__ */ jsx(
|
|
1266
|
-
Button,
|
|
1543
|
+
/* @__PURE__ */ jsx(Paper, { withBorder: true, children: /* @__PURE__ */ jsxs(Stack, { gap: "md", children: [
|
|
1544
|
+
/* @__PURE__ */ jsx(CardHeader, { icon: /* @__PURE__ */ jsx(IconBolt, { size: 18 }), title: "Actions", titleOrder: 2, mb: 0 }),
|
|
1545
|
+
actions.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1546
|
+
/* @__PURE__ */ jsx(Group, { gap: "sm", children: actions.map(
|
|
1547
|
+
(action) => action.payloadSchema ? /* @__PURE__ */ jsx(
|
|
1548
|
+
ActionFormButton,
|
|
1549
|
+
{
|
|
1550
|
+
action,
|
|
1551
|
+
isActive: activeActionKey === action.key,
|
|
1552
|
+
onToggle: () => setActiveActionKey((current) => current === action.key ? null : action.key)
|
|
1553
|
+
},
|
|
1554
|
+
action.key
|
|
1555
|
+
) : /* @__PURE__ */ jsx(ActionButton, { action, dealId: deal.id }, action.key)
|
|
1556
|
+
) }),
|
|
1557
|
+
activeAction?.payloadSchema && /* @__PURE__ */ jsx(
|
|
1558
|
+
ActionForm,
|
|
1267
1559
|
{
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
void navigate({ to: "/crm/deals" });
|
|
1274
|
-
}
|
|
1275
|
-
}),
|
|
1276
|
-
children: "Delete deal"
|
|
1277
|
-
}
|
|
1560
|
+
action: activeAction,
|
|
1561
|
+
dealId: deal.id,
|
|
1562
|
+
onClose: () => setActiveActionKey(null)
|
|
1563
|
+
},
|
|
1564
|
+
activeAction.key
|
|
1278
1565
|
)
|
|
1279
|
-
] })
|
|
1566
|
+
] }) : /* @__PURE__ */ jsx(Alert, { color: "gray", variant: "light", children: "No reply actions are currently available for this deal." })
|
|
1280
1567
|
] }) })
|
|
1281
|
-
] }) });
|
|
1568
|
+
] }) }) });
|
|
1282
1569
|
}
|
|
1283
1570
|
function CompanyDetailPage({ companyId }) {
|
|
1284
1571
|
const navigate = useNavigate();
|
|
@@ -1398,5 +1685,313 @@ function CompanyDetailPage({ companyId }) {
|
|
|
1398
1685
|
] }) })
|
|
1399
1686
|
] }) });
|
|
1400
1687
|
}
|
|
1688
|
+
var crmPrioritySettingsKeys = {
|
|
1689
|
+
all: ["crm-priority-settings"],
|
|
1690
|
+
detail: (organizationId) => [...crmPrioritySettingsKeys.all, organizationId]
|
|
1691
|
+
};
|
|
1692
|
+
function useCrmPrioritySettings() {
|
|
1693
|
+
const { apiRequest, isReady, organizationId } = useElevasisServices();
|
|
1694
|
+
return useQuery({
|
|
1695
|
+
queryKey: crmPrioritySettingsKeys.detail(organizationId),
|
|
1696
|
+
queryFn: () => apiRequest("/crm/settings/priority"),
|
|
1697
|
+
enabled: isReady
|
|
1698
|
+
});
|
|
1699
|
+
}
|
|
1700
|
+
function useUpdateCrmPrioritySettings() {
|
|
1701
|
+
const { apiRequest, organizationId } = useElevasisServices();
|
|
1702
|
+
const queryClient = useQueryClient();
|
|
1703
|
+
return useMutation({
|
|
1704
|
+
mutationFn: (override) => apiRequest("/crm/settings/priority", {
|
|
1705
|
+
method: "PATCH",
|
|
1706
|
+
body: JSON.stringify(override)
|
|
1707
|
+
}),
|
|
1708
|
+
onSuccess: () => {
|
|
1709
|
+
queryClient.invalidateQueries({ queryKey: crmPrioritySettingsKeys.detail(organizationId) });
|
|
1710
|
+
queryClient.invalidateQueries({ queryKey: dealKeys.lists() });
|
|
1711
|
+
queryClient.invalidateQueries({ queryKey: dealKeys.details() });
|
|
1712
|
+
showSuccessNotification("CRM priority settings saved.");
|
|
1713
|
+
},
|
|
1714
|
+
onError: (error) => {
|
|
1715
|
+
showApiErrorNotification(error);
|
|
1716
|
+
}
|
|
1717
|
+
});
|
|
1718
|
+
}
|
|
1719
|
+
function useResetCrmPrioritySettings() {
|
|
1720
|
+
const { apiRequest, organizationId } = useElevasisServices();
|
|
1721
|
+
const queryClient = useQueryClient();
|
|
1722
|
+
return useMutation({
|
|
1723
|
+
mutationFn: () => apiRequest("/crm/settings/priority", {
|
|
1724
|
+
method: "DELETE"
|
|
1725
|
+
}),
|
|
1726
|
+
onSuccess: () => {
|
|
1727
|
+
queryClient.invalidateQueries({ queryKey: crmPrioritySettingsKeys.detail(organizationId) });
|
|
1728
|
+
queryClient.invalidateQueries({ queryKey: dealKeys.lists() });
|
|
1729
|
+
queryClient.invalidateQueries({ queryKey: dealKeys.details() });
|
|
1730
|
+
showSuccessNotification("CRM priority settings reset to Org-OS defaults.");
|
|
1731
|
+
},
|
|
1732
|
+
onError: (error) => {
|
|
1733
|
+
showApiErrorNotification(error);
|
|
1734
|
+
}
|
|
1735
|
+
});
|
|
1736
|
+
}
|
|
1737
|
+
var BUCKET_KEYS = [
|
|
1738
|
+
"needs_response",
|
|
1739
|
+
"follow_up_due",
|
|
1740
|
+
"waiting",
|
|
1741
|
+
"stale",
|
|
1742
|
+
"closed_low"
|
|
1743
|
+
];
|
|
1744
|
+
var COLOR_OPTIONS = ["red", "orange", "yellow", "blue", "gray", "dark", "green", "teal", "violet", "grape"].map(
|
|
1745
|
+
(color) => ({ value: color, label: color })
|
|
1746
|
+
);
|
|
1747
|
+
var ORDER_OPTIONS = [
|
|
1748
|
+
{ value: "10", label: "First - most urgent" },
|
|
1749
|
+
{ value: "20", label: "Second - follow-up queue" },
|
|
1750
|
+
{ value: "30", label: "Middle - waiting" },
|
|
1751
|
+
{ value: "40", label: "Later - stale review" },
|
|
1752
|
+
{ value: "50", label: "Last - closed or low priority" }
|
|
1753
|
+
];
|
|
1754
|
+
var BUCKET_RULE_COPY = {
|
|
1755
|
+
needs_response: {
|
|
1756
|
+
applies: "Current state or latest activity indicates the lead replied or cancelled and needs a human response.",
|
|
1757
|
+
example: "A lead replies to the discovery email; the deal moves to Needs Response immediately."
|
|
1758
|
+
},
|
|
1759
|
+
follow_up_due: {
|
|
1760
|
+
applies: "The configured follow-up window for the current state has elapsed.",
|
|
1761
|
+
example: "A discovery link was sent 5 days ago and the follow-up window is 3 days."
|
|
1762
|
+
},
|
|
1763
|
+
waiting: {
|
|
1764
|
+
applies: "No response is needed yet, or the next action time is still in the future.",
|
|
1765
|
+
example: "A discovery link was sent yesterday and the follow-up window has not elapsed."
|
|
1766
|
+
},
|
|
1767
|
+
stale: {
|
|
1768
|
+
applies: "No meaningful activity has happened for the stale threshold and no stronger bucket applies.",
|
|
1769
|
+
example: "A deal has had no activity for 14 days and is not waiting on a scheduled follow-up."
|
|
1770
|
+
},
|
|
1771
|
+
closed_low: {
|
|
1772
|
+
applies: "The deal is in a closed stage, so it should stay out of the active follow-up queue.",
|
|
1773
|
+
example: "A deal is marked Closed Won or Closed Lost."
|
|
1774
|
+
}
|
|
1775
|
+
};
|
|
1776
|
+
function getOrderLabel(rank) {
|
|
1777
|
+
if (rank <= 10) return "Shown first";
|
|
1778
|
+
if (rank <= 20) return "After urgent replies";
|
|
1779
|
+
if (rank <= 30) return "Middle";
|
|
1780
|
+
if (rank <= 40) return "Later review";
|
|
1781
|
+
return "Shown last";
|
|
1782
|
+
}
|
|
1783
|
+
function toResolvedConfig(config = DEFAULT_CRM_PRIORITY_RULE_CONFIG) {
|
|
1784
|
+
return {
|
|
1785
|
+
...config,
|
|
1786
|
+
enabled: "enabled" in config ? config.enabled : true,
|
|
1787
|
+
buckets: [...config.buckets].sort((a, b) => a.rank - b.rank),
|
|
1788
|
+
closedStageKeys: [...config.closedStageKeys],
|
|
1789
|
+
followUpAfterDaysByStateKey: { ...config.followUpAfterDaysByStateKey }
|
|
1790
|
+
};
|
|
1791
|
+
}
|
|
1792
|
+
function draftFromConfig(config) {
|
|
1793
|
+
const defaultBuckets = new Map(CRM_PRIORITY_BUCKETS.map((bucket) => [bucket.bucketKey, bucket]));
|
|
1794
|
+
const resolvedBuckets = new Map(config.buckets.map((bucket) => [bucket.bucketKey, bucket]));
|
|
1795
|
+
return {
|
|
1796
|
+
enabled: config.enabled,
|
|
1797
|
+
staleAfterDays: config.staleAfterDays,
|
|
1798
|
+
buckets: Object.fromEntries(
|
|
1799
|
+
BUCKET_KEYS.map((bucketKey) => {
|
|
1800
|
+
const bucket = resolvedBuckets.get(bucketKey) ?? defaultBuckets.get(bucketKey);
|
|
1801
|
+
return [bucketKey, { label: bucket.label, color: bucket.color, rank: bucket.rank }];
|
|
1802
|
+
})
|
|
1803
|
+
)
|
|
1804
|
+
};
|
|
1805
|
+
}
|
|
1806
|
+
function toOverride(draft) {
|
|
1807
|
+
return {
|
|
1808
|
+
enabled: draft.enabled,
|
|
1809
|
+
staleAfterDays: draft.staleAfterDays,
|
|
1810
|
+
buckets: Object.fromEntries(
|
|
1811
|
+
BUCKET_KEYS.map((bucketKey) => [
|
|
1812
|
+
bucketKey,
|
|
1813
|
+
{
|
|
1814
|
+
label: draft.buckets[bucketKey].label.trim(),
|
|
1815
|
+
color: draft.buckets[bucketKey].color.trim(),
|
|
1816
|
+
rank: draft.buckets[bucketKey].rank
|
|
1817
|
+
}
|
|
1818
|
+
])
|
|
1819
|
+
)
|
|
1820
|
+
};
|
|
1821
|
+
}
|
|
1822
|
+
function CrmSettingsPage() {
|
|
1823
|
+
const settingsQuery = useCrmPrioritySettings();
|
|
1824
|
+
const updateSettings = useUpdateCrmPrioritySettings();
|
|
1825
|
+
const resetSettings = useResetCrmPrioritySettings();
|
|
1826
|
+
const resolvedConfig = useMemo(() => toResolvedConfig(settingsQuery.data?.resolved), [settingsQuery.data?.resolved]);
|
|
1827
|
+
const defaultConfig = useMemo(() => toResolvedConfig(settingsQuery.data?.defaults), [settingsQuery.data?.defaults]);
|
|
1828
|
+
const [draft, setDraft] = useState(() => draftFromConfig(resolvedConfig));
|
|
1829
|
+
useEffect(() => {
|
|
1830
|
+
if (settingsQuery.data) {
|
|
1831
|
+
setDraft(draftFromConfig(toResolvedConfig(settingsQuery.data.resolved)));
|
|
1832
|
+
}
|
|
1833
|
+
}, [settingsQuery.data]);
|
|
1834
|
+
const hasOverride = Boolean(settingsQuery.data?.override);
|
|
1835
|
+
const isSaving = updateSettings.isPending || resetSettings.isPending;
|
|
1836
|
+
const updateBucket = (bucketKey, field, value) => {
|
|
1837
|
+
setDraft((current) => ({
|
|
1838
|
+
...current,
|
|
1839
|
+
buckets: {
|
|
1840
|
+
...current.buckets,
|
|
1841
|
+
[bucketKey]: {
|
|
1842
|
+
...current.buckets[bucketKey],
|
|
1843
|
+
[field]: value
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
}));
|
|
1847
|
+
};
|
|
1848
|
+
const handleReset = async () => {
|
|
1849
|
+
const response = await resetSettings.mutateAsync();
|
|
1850
|
+
setDraft(draftFromConfig(toResolvedConfig(response.resolved)));
|
|
1851
|
+
};
|
|
1852
|
+
return /* @__PURE__ */ jsx(SubshellContentContainer, { children: /* @__PURE__ */ jsx(PageContainer, { children: /* @__PURE__ */ jsxs(Stack, { children: [
|
|
1853
|
+
/* @__PURE__ */ jsx(
|
|
1854
|
+
PageTitleCaption,
|
|
1855
|
+
{
|
|
1856
|
+
title: "CRM Settings",
|
|
1857
|
+
caption: "Scoped CRM controls for priority presentation and thresholds",
|
|
1858
|
+
rightSection: !settingsQuery.isLoading && !settingsQuery.error ? /* @__PURE__ */ jsx(
|
|
1859
|
+
Switch,
|
|
1860
|
+
{
|
|
1861
|
+
label: "Priority Enabled",
|
|
1862
|
+
checked: draft.enabled,
|
|
1863
|
+
onChange: (event) => setDraft((current) => ({ ...current, enabled: event.currentTarget.checked }))
|
|
1864
|
+
}
|
|
1865
|
+
) : null
|
|
1866
|
+
}
|
|
1867
|
+
),
|
|
1868
|
+
settingsQuery.isLoading ? /* @__PURE__ */ jsx(Center, { p: "xl", children: /* @__PURE__ */ jsx(Loader, {}) }) : settingsQuery.error ? /* @__PURE__ */ jsx(CenteredErrorState, { error: settingsQuery.error, title: "Failed to load CRM settings" }) : /* @__PURE__ */ jsx(Paper, { withBorder: true, children: /* @__PURE__ */ jsxs(Stack, { children: [
|
|
1869
|
+
/* @__PURE__ */ jsxs(Group, { justify: "space-between", align: "flex-start", children: [
|
|
1870
|
+
/* @__PURE__ */ jsxs(Stack, { gap: 4, children: [
|
|
1871
|
+
/* @__PURE__ */ jsxs(Group, { gap: "xs", children: [
|
|
1872
|
+
/* @__PURE__ */ jsx(IconSettings, { size: 18 }),
|
|
1873
|
+
/* @__PURE__ */ jsx(Title, { order: 3, children: "Priority Rules" }),
|
|
1874
|
+
/* @__PURE__ */ jsx(Badge, { variant: "light", color: hasOverride ? "blue" : "gray", children: hasOverride ? "Custom" : "Org-OS defaults" })
|
|
1875
|
+
] }),
|
|
1876
|
+
/* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: "These settings are saved for CRM priority evaluation. Deals continue to receive priority from the backend response." })
|
|
1877
|
+
] }),
|
|
1878
|
+
/* @__PURE__ */ jsxs(Group, { gap: "xs", children: [
|
|
1879
|
+
/* @__PURE__ */ jsx(
|
|
1880
|
+
Button,
|
|
1881
|
+
{
|
|
1882
|
+
variant: "light",
|
|
1883
|
+
leftSection: /* @__PURE__ */ jsx(IconRestore, { size: 16 }),
|
|
1884
|
+
loading: resetSettings.isPending,
|
|
1885
|
+
disabled: isSaving,
|
|
1886
|
+
onClick: () => void handleReset(),
|
|
1887
|
+
children: "Reset defaults"
|
|
1888
|
+
}
|
|
1889
|
+
),
|
|
1890
|
+
/* @__PURE__ */ jsx(
|
|
1891
|
+
Button,
|
|
1892
|
+
{
|
|
1893
|
+
loading: updateSettings.isPending,
|
|
1894
|
+
disabled: isSaving,
|
|
1895
|
+
onClick: () => void updateSettings.mutateAsync(toOverride(draft)),
|
|
1896
|
+
children: "Save changes"
|
|
1897
|
+
}
|
|
1898
|
+
)
|
|
1899
|
+
] })
|
|
1900
|
+
] }),
|
|
1901
|
+
/* @__PURE__ */ jsx(
|
|
1902
|
+
NumberInput,
|
|
1903
|
+
{
|
|
1904
|
+
label: "Stale threshold",
|
|
1905
|
+
description: "Deals with no meaningful activity after this many days can move to Stale.",
|
|
1906
|
+
min: 1,
|
|
1907
|
+
max: 365,
|
|
1908
|
+
value: draft.staleAfterDays,
|
|
1909
|
+
onChange: (value) => setDraft((current) => ({ ...current, staleAfterDays: Number(value) || current.staleAfterDays })),
|
|
1910
|
+
suffix: " days",
|
|
1911
|
+
maw: 320
|
|
1912
|
+
}
|
|
1913
|
+
),
|
|
1914
|
+
/* @__PURE__ */ jsxs(Table, { style: { tableLayout: "fixed" }, children: [
|
|
1915
|
+
/* @__PURE__ */ jsx(Table.Thead, { children: /* @__PURE__ */ jsxs(Table.Tr, { children: [
|
|
1916
|
+
/* @__PURE__ */ jsx(Table.Th, { w: 150, children: "Bucket" }),
|
|
1917
|
+
/* @__PURE__ */ jsx(Table.Th, { w: 180, children: "Label" }),
|
|
1918
|
+
/* @__PURE__ */ jsx(Table.Th, { w: 170, children: "Color" }),
|
|
1919
|
+
/* @__PURE__ */ jsx(Table.Th, { w: 210, children: "Display order" }),
|
|
1920
|
+
/* @__PURE__ */ jsx(Table.Th, { w: 80, children: "Logic" })
|
|
1921
|
+
] }) }),
|
|
1922
|
+
/* @__PURE__ */ jsx(Table.Tbody, { children: BUCKET_KEYS.map((bucketKey) => {
|
|
1923
|
+
const bucket = draft.buckets[bucketKey];
|
|
1924
|
+
const defaultBucket = defaultConfig.buckets.find((candidate) => candidate.bucketKey === bucketKey);
|
|
1925
|
+
return /* @__PURE__ */ jsxs(Table.Tr, { children: [
|
|
1926
|
+
/* @__PURE__ */ jsxs(Table.Td, { children: [
|
|
1927
|
+
/* @__PURE__ */ jsx(Text, { size: "sm", fw: 600, children: bucketKey.replaceAll("_", " ") }),
|
|
1928
|
+
/* @__PURE__ */ jsxs(
|
|
1929
|
+
Badge,
|
|
1930
|
+
{
|
|
1931
|
+
mt: 6,
|
|
1932
|
+
variant: "light",
|
|
1933
|
+
color: defaultBucket?.color ?? "gray",
|
|
1934
|
+
style: { textTransform: "none" },
|
|
1935
|
+
children: [
|
|
1936
|
+
"Default: ",
|
|
1937
|
+
defaultBucket ? getOrderLabel(defaultBucket.rank) : "Not set"
|
|
1938
|
+
]
|
|
1939
|
+
}
|
|
1940
|
+
)
|
|
1941
|
+
] }),
|
|
1942
|
+
/* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(
|
|
1943
|
+
TextInput,
|
|
1944
|
+
{
|
|
1945
|
+
size: "sm",
|
|
1946
|
+
value: bucket.label,
|
|
1947
|
+
onChange: (event) => updateBucket(bucketKey, "label", event.currentTarget.value)
|
|
1948
|
+
}
|
|
1949
|
+
) }),
|
|
1950
|
+
/* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsxs(Group, { gap: "xs", wrap: "nowrap", children: [
|
|
1951
|
+
/* @__PURE__ */ jsx(ColorSwatch, { color: `var(--mantine-color-${bucket.color}-6)`, size: 18 }),
|
|
1952
|
+
/* @__PURE__ */ jsx(
|
|
1953
|
+
Select,
|
|
1954
|
+
{
|
|
1955
|
+
size: "sm",
|
|
1956
|
+
data: COLOR_OPTIONS,
|
|
1957
|
+
value: bucket.color,
|
|
1958
|
+
searchable: true,
|
|
1959
|
+
allowDeselect: false,
|
|
1960
|
+
onChange: (value) => updateBucket(bucketKey, "color", value ?? bucket.color)
|
|
1961
|
+
}
|
|
1962
|
+
)
|
|
1963
|
+
] }) }),
|
|
1964
|
+
/* @__PURE__ */ jsxs(Table.Td, { children: [
|
|
1965
|
+
/* @__PURE__ */ jsx(
|
|
1966
|
+
Select,
|
|
1967
|
+
{
|
|
1968
|
+
size: "sm",
|
|
1969
|
+
data: ORDER_OPTIONS,
|
|
1970
|
+
value: String(bucket.rank),
|
|
1971
|
+
allowDeselect: false,
|
|
1972
|
+
onChange: (value) => updateBucket(bucketKey, "rank", Number(value) || bucket.rank)
|
|
1973
|
+
}
|
|
1974
|
+
),
|
|
1975
|
+
/* @__PURE__ */ jsx(Text, { mt: 4, size: "xs", c: "dimmed", children: "Lower order appears earlier in Pipeline and Deals." })
|
|
1976
|
+
] }),
|
|
1977
|
+
/* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsxs(Popover, { width: 360, position: "left", withArrow: true, shadow: "md", children: [
|
|
1978
|
+
/* @__PURE__ */ jsx(Popover.Target, { children: /* @__PURE__ */ jsx(ActionIcon, { variant: "subtle", color: "gray", "aria-label": `Show ${bucket.label} logic`, children: /* @__PURE__ */ jsx(IconInfoCircle, { size: 18 }) }) }),
|
|
1979
|
+
/* @__PURE__ */ jsx(Popover.Dropdown, { children: /* @__PURE__ */ jsxs(Stack, { gap: "sm", children: [
|
|
1980
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
1981
|
+
/* @__PURE__ */ jsx(Text, { size: "xs", fw: 700, tt: "uppercase", c: "dimmed", children: "When it applies" }),
|
|
1982
|
+
/* @__PURE__ */ jsx(Text, { size: "sm", children: BUCKET_RULE_COPY[bucketKey].applies })
|
|
1983
|
+
] }),
|
|
1984
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
1985
|
+
/* @__PURE__ */ jsx(Text, { size: "xs", fw: 700, tt: "uppercase", c: "dimmed", children: "Example" }),
|
|
1986
|
+
/* @__PURE__ */ jsx(Text, { size: "sm", children: BUCKET_RULE_COPY[bucketKey].example })
|
|
1987
|
+
] })
|
|
1988
|
+
] }) })
|
|
1989
|
+
] }) })
|
|
1990
|
+
] }, bucketKey);
|
|
1991
|
+
}) })
|
|
1992
|
+
] })
|
|
1993
|
+
] }) })
|
|
1994
|
+
] }) }) });
|
|
1995
|
+
}
|
|
1401
1996
|
|
|
1402
|
-
export {
|
|
1997
|
+
export { ActivityFeedWidget, CRM_ITEMS, CompanyDetailPage, ConversationThread, CrmOverview, CrmSettingsPage, CrmSidebar, CrmSidebarMiddle, CrmSidebarTop, DEAL_STAGE_COLORS, DEAL_STAGE_OPTIONS, DealDetailPage, DealsListPage, MetricsStrip, MyTasksPanel, PIPELINE_FUNNEL_ORDER, PipelineFunnelWidget, QuickCreateActions, SAVED_VIEW_PRESETS, SavedViewsPanel, compareDealsByPriority, crmManifest, crmPrioritySettingsKeys, formatDealStageLabel, setDealReferrer, useCrmPipelineSummary, useCrmPrioritySettings, useCrmQuickMetrics, useRecentCrmActivity, useResetCrmPrioritySettings, useUpdateCrmPrioritySettings };
|