@elevasis/ui 2.34.0 → 2.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/dist/api/index.js +3 -3
  2. package/dist/app/index.d.ts +12 -12
  3. package/dist/app/index.js +25 -23
  4. package/dist/charts/index.js +3 -5
  5. package/dist/chunk-26HFM4MH.js +41449 -0
  6. package/dist/{chunk-DTFKWZ7A.js → chunk-4U3XAWCN.js} +502 -484
  7. package/dist/{chunk-ND5TDV2J.js → chunk-57OZ3AEG.js} +1 -1
  8. package/dist/{chunk-E4WQGJNS.js → chunk-7FPLLSHN.js} +14 -1
  9. package/dist/{chunk-RQA2EVN3.js → chunk-AKW7KISS.js} +39 -3
  10. package/dist/chunk-AUDNF2Q7.js +2050 -0
  11. package/dist/{chunk-TYRUKGGD.js → chunk-GX6XBRRF.js} +1 -2
  12. package/dist/{chunk-V6SZ4ECN.js → chunk-LUYVRATI.js} +257 -6
  13. package/dist/{chunk-X4WBGKJQ.js → chunk-R3VCBZDC.js} +50 -3
  14. package/dist/chunk-SIQ3P4OR.js +1764 -0
  15. package/dist/{chunk-RIAXZ6AH.js → chunk-VDOOGGBA.js} +1 -1
  16. package/dist/{chunk-3FV6HBXS.js → chunk-WF7CONXF.js} +23 -23
  17. package/dist/{chunk-3QXJK5IY.js → chunk-YYX7OPZQ.js} +1 -1
  18. package/dist/components/index.d.ts +69 -69
  19. package/dist/components/index.js +20 -2795
  20. package/dist/components/navigation/index.js +25 -5
  21. package/dist/execution/index.d.ts +9 -9
  22. package/dist/execution/index.js +1 -2
  23. package/dist/features/auth/index.js +23 -2
  24. package/dist/features/clients/index.js +20 -26
  25. package/dist/features/crm/index.js +20 -30
  26. package/dist/features/dashboard/index.d.ts +68 -68
  27. package/dist/features/dashboard/index.js +20 -28
  28. package/dist/features/delivery/index.js +20 -30
  29. package/dist/features/knowledge/index.js +25 -9
  30. package/dist/features/lead-gen/index.d.ts +9 -9
  31. package/dist/features/lead-gen/index.js +20 -31
  32. package/dist/features/monitoring/index.js +20 -30
  33. package/dist/features/monitoring/requests/index.js +20 -25
  34. package/dist/features/operations/index.d.ts +153 -153
  35. package/dist/features/operations/index.js +18 -37
  36. package/dist/features/seo/index.js +3 -4
  37. package/dist/features/settings/index.js +20 -27
  38. package/dist/graph/index.js +1 -1
  39. package/dist/hooks/delivery/index.js +30 -2
  40. package/dist/hooks/index.d.ts +85 -85
  41. package/dist/hooks/index.js +20 -21
  42. package/dist/hooks/operations/command-view/utils/transformCommandViewData.d.ts +35 -35
  43. package/dist/hooks/published.d.ts +85 -85
  44. package/dist/hooks/published.js +20 -20
  45. package/dist/index.css +532 -532
  46. package/dist/index.d.ts +9256 -5803
  47. package/dist/index.js +22 -26
  48. package/dist/knowledge/index.d.ts +21 -21
  49. package/dist/knowledge/index.js +8 -15
  50. package/dist/layout/index.js +4 -10
  51. package/dist/organization/index.js +27 -1
  52. package/dist/provider/index.d.ts +47 -21
  53. package/dist/provider/index.js +20 -15
  54. package/dist/provider/published.d.ts +15 -16
  55. package/dist/provider/published.js +20 -11
  56. package/dist/test-utils/index.js +3 -3
  57. package/dist/theme/index.js +2 -3
  58. package/dist/theme/presets/index.d.ts +28 -3
  59. package/dist/theme/presets/index.js +1 -1
  60. package/dist/typeform/index.js +1 -2049
  61. package/dist/types/index.d.ts +68 -68
  62. package/dist/utils/index.d.ts +46 -46
  63. package/dist/utils/index.js +1 -1
  64. package/dist/zustand/index.d.ts +6 -6
  65. package/dist/zustand/index.js +0 -3
  66. package/package.json +5 -5
  67. package/dist/chunk-3AJVNMY5.js +0 -4769
  68. package/dist/chunk-3MEXPLWT.js +0 -265
  69. package/dist/chunk-3ZMAGTWF.js +0 -18
  70. package/dist/chunk-4O4MII5S.js +0 -4716
  71. package/dist/chunk-5EYJ2GIN.js +0 -122
  72. package/dist/chunk-7M2VOCYN.js +0 -1
  73. package/dist/chunk-BPQVTIUP.js +0 -105
  74. package/dist/chunk-BZZCNLT6.js +0 -12
  75. package/dist/chunk-CLDCYJQT.js +0 -1
  76. package/dist/chunk-E565XMTQ.js +0 -17
  77. package/dist/chunk-HRWLKKWM.js +0 -758
  78. package/dist/chunk-IGDYWFNE.js +0 -5198
  79. package/dist/chunk-IIMU5YAJ.js +0 -53
  80. package/dist/chunk-IVGI4GDL.js +0 -1593
  81. package/dist/chunk-JFL3GRD4.js +0 -39
  82. package/dist/chunk-LAWLB6CT.js +0 -951
  83. package/dist/chunk-LGKLC5MG.js +0 -44
  84. package/dist/chunk-LRWTWOGP.js +0 -1778
  85. package/dist/chunk-MP3GPBPX.js +0 -1874
  86. package/dist/chunk-N55DVMAG.js +0 -14
  87. package/dist/chunk-NLBQTDOW.js +0 -12051
  88. package/dist/chunk-O6JXQ6UQ.js +0 -468
  89. package/dist/chunk-OBBQ2JCM.js +0 -68
  90. package/dist/chunk-PDHTXPSF.js +0 -12
  91. package/dist/chunk-PLP3NYPL.js +0 -356
  92. package/dist/chunk-R2XR4FCV.js +0 -48
  93. package/dist/chunk-R66W5UDG.js +0 -26
  94. package/dist/chunk-RYTEQBAO.js +0 -37
  95. package/dist/chunk-SDXSB3HN.js +0 -425
  96. package/dist/chunk-TKAYX2SP.js +0 -204
  97. package/dist/chunk-TUMSNGTX.js +0 -35
  98. package/dist/chunk-VNAZTCHA.js +0 -65
  99. package/dist/chunk-VNFR57DF.js +0 -87
  100. package/dist/chunk-VTXTZXAU.js +0 -539
  101. package/dist/chunk-W73ZABT6.js +0 -85
  102. package/dist/chunk-WU4FNWCW.js +0 -2281
  103. package/dist/chunk-XZGSCABI.js +0 -383
  104. package/dist/chunk-YNWZIWJL.js +0 -1863
  105. /package/dist/{chunk-2RJMVWFJ.js → chunk-GEFWMU26.js} +0 -0
  106. /package/dist/{chunk-22UVE3RA.js → chunk-HENXLGVD.js} +0 -0
@@ -1,4769 +0,0 @@
1
- import { canonicalOrganizationModel, PROSPECTING_STEPS as PROSPECTING_STEPS$1, SalesSidebar, LeadGenSidebarMiddle, LEAD_GEN_PIPELINE_DEFINITIONS } from './chunk-4O4MII5S.js';
2
- import { sanitizeInput } from './chunk-3MEXPLWT.js';
3
- import { SubshellSidebarSection } from './chunk-IIMU5YAJ.js';
4
- import { PageContainer } from './chunk-BZZCNLT6.js';
5
- import { TableSelectionToolbar, SortableHeader } from './chunk-TUMSNGTX.js';
6
- import { FilterBar } from './chunk-PDHTXPSF.js';
7
- import { CustomModal } from './chunk-R66W5UDG.js';
8
- import { acquisitionListKeys, useListsTelemetry, useLists, useCreateList, useTableSort, sortData, usePaginationState, useTableSelection, useWorkflowExecution, useList, useListProgress, useListExecutions, useDeleteList, useCompanyFacets, useCompanies, useDeleteCompanies, useContacts, useDeleteContacts, useCredentials, useVerifyCredential, useInFlightExecutions, useListRecords, useExecutionSSE, useListMembers, useListMember, useTransitionListMember, useDeriveActions, useArtifacts } from './chunk-IGDYWFNE.js';
9
- import { showApiErrorNotification, showSuccessNotification } from './chunk-XZGSCABI.js';
10
- import { useListActions } from './chunk-PLP3NYPL.js';
11
- import { PageTitleCaption, CenteredErrorState, StatCard, CardHeader, EmptyState, JsonViewer } from './chunk-HRWLKKWM.js';
12
- import { SubshellContentContainer } from './chunk-TKAYX2SP.js';
13
- import { PROSPECTING_STEPS, getLeadGenStageCatalog, findPipeline } from './chunk-DTFKWZ7A.js';
14
- import { isStepStartedContext, isStepCompletedContext, isStepFailedContext } from './chunk-KRWALB24.js';
15
- import { useElevasisServices } from './chunk-KJ3QUBNU.js';
16
- import { Stack, Group, Title, Text, Alert, Button, Collapse, Paper, Anchor, Center, Loader, SimpleGrid, Card, Table, Badge, TextInput, Select, Checkbox, Pagination, Textarea, ActionIcon, Tooltip, Tabs, ThemeIcon, Box, Pill, SegmentedControl, UnstyledButton, Drawer, JsonInput, Switch, NumberInput, MultiSelect, TagsInput, ScrollArea, Progress } from '@mantine/core';
17
- import { IconTarget, IconAlertCircle, IconPlayerPlay, IconArrowRight, IconSparkles, IconListDetails, IconPlus, IconSearch, IconList, IconExternalLink, IconAlertTriangle, IconLayoutDashboard, IconBolt, IconCopy, IconBuilding, IconUsers, IconTrash, IconBuildingFactory2, IconArrowLeft, IconChevronDown, IconChevronRight, IconRefresh, IconAddressBook, IconSettings, IconCircleCheck, IconCircleDot, IconTerminal2, IconProgressCheck, IconMail, IconUser, IconDatabase } from '@tabler/icons-react';
18
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
19
- import { useState, useRef, useMemo, useEffect, Fragment as Fragment$1, useCallback } from 'react';
20
- import { useForm } from '@mantine/form';
21
- import { zod4Resolver } from 'mantine-form-zod-resolver';
22
- import { Link, useNavigate, useSearch } from '@tanstack/react-router';
23
- import { useQueryClient, useMutation } from '@tanstack/react-query';
24
-
25
- var LeadGenSidebarTop = () => {
26
- return /* @__PURE__ */ jsx(SubshellSidebarSection, { icon: IconTarget, label: "Lead Gen" });
27
- };
28
- var LeadGenSidebar = ({ children } = {}) => {
29
- return /* @__PURE__ */ jsxs(Stack, { gap: 0, style: { height: "100%", display: "flex", flexDirection: "column" }, children: [
30
- /* @__PURE__ */ jsx(LeadGenSidebarTop, {}),
31
- children ?? /* @__PURE__ */ jsx(LeadGenSidebarMiddle, {})
32
- ] });
33
- };
34
-
35
- // ../core/src/business/acquisition/build-templates.ts
36
- var BUILD_TEMPLATE_CATALOG = [
37
- {
38
- id: "local-services",
39
- label: "Local Services",
40
- description: "Source, analyze, qualify, and personalize local service businesses for outreach.",
41
- steps: Object.values(PROSPECTING_STEPS.localServices)
42
- },
43
- {
44
- id: "dtc-subscription-apollo-clickup",
45
- label: "DTC Subscription (Apollo + ClickUp)",
46
- description: "Import DTC brand leads from Apollo, crawl their websites, score fit, enrich contacts, and export via ClickUp.",
47
- steps: Object.values(PROSPECTING_STEPS.dtcApolloClickup)
48
- }
49
- ];
50
- var DEFAULT_PROSPECTING_BUILD_TEMPLATE_ID = "local-services";
51
- var PROSPECTING_BUILD_TEMPLATE_OPTIONS = BUILD_TEMPLATE_CATALOG.map(({ id, label, description }) => ({
52
- id,
53
- label,
54
- description
55
- }));
56
- function createBuildPlanSnapshotFromTemplateId(templateId) {
57
- const template = BUILD_TEMPLATE_CATALOG.find((item) => item.id === templateId);
58
- if (!template) return null;
59
- return {
60
- templateId: template.id,
61
- templateLabel: template.label,
62
- steps: template.steps.map((step) => {
63
- const snapshotStep = {
64
- id: step.id,
65
- label: step.label,
66
- primaryEntity: step.primaryEntity,
67
- outputs: [...step.outputs],
68
- stageKey: step.stageKey,
69
- recordsStageKey: step.recordsStageKey ?? step.stageKey,
70
- recordSourceStageKey: step.recordSourceStageKey ?? step.recordsStageKey ?? step.stageKey,
71
- dependencyMode: step.dependencyMode,
72
- actionKey: step.actionKey,
73
- defaultBatchSize: step.defaultBatchSize,
74
- maxBatchSize: step.maxBatchSize
75
- };
76
- if (step.description) snapshotStep.description = step.description;
77
- if (step.recordEntity) snapshotStep.recordEntity = step.recordEntity;
78
- if (step.dependsOn?.length) snapshotStep.dependsOn = [...step.dependsOn];
79
- if (step.credentialRequirements?.length) {
80
- snapshotStep.credentialRequirements = step.credentialRequirements.map((requirement) => ({
81
- ...requirement
82
- }));
83
- }
84
- if (step.recordColumns) {
85
- snapshotStep.recordColumns = {
86
- ...step.recordColumns.company ? {
87
- company: step.recordColumns.company.map((column) => ({ ...column }))
88
- } : {},
89
- ...step.recordColumns.contact ? {
90
- contact: step.recordColumns.contact.map((column) => ({ ...column }))
91
- } : {}
92
- };
93
- }
94
- return snapshotStep;
95
- })
96
- };
97
- }
98
- function TabSection({ icon, title, description, rightSection, children }) {
99
- const wrappedIcon = icon ? /* @__PURE__ */ jsx(ThemeIcon, { size: "md", variant: "light", children: icon }) : null;
100
- return /* @__PURE__ */ jsxs(Stack, { gap: "sm", p: "md", children: [
101
- /* @__PURE__ */ jsxs(Group, { justify: "space-between", gap: "md", align: "flex-start", wrap: "nowrap", children: [
102
- /* @__PURE__ */ jsxs(Stack, { gap: 2, style: { minWidth: 0 }, children: [
103
- /* @__PURE__ */ jsxs(Group, { gap: "xs", wrap: "nowrap", children: [
104
- wrappedIcon,
105
- /* @__PURE__ */ jsx(Title, { order: 3, style: { overflowWrap: "anywhere" }, children: title })
106
- ] }),
107
- description ? /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", style: { overflowWrap: "anywhere" }, children: description }) : null
108
- ] }),
109
- rightSection
110
- ] }),
111
- children
112
- ] });
113
- }
114
- function renderField(field, form, disabled, jsonState, setJsonState) {
115
- const inputProps = form.getInputProps(field.path);
116
- const label = field.label;
117
- const description = field.description;
118
- const placeholder = field.placeholder;
119
- switch (field.component) {
120
- case "segmented": {
121
- const options = field.options ?? [];
122
- return /* @__PURE__ */ jsx(
123
- SegmentedControl,
124
- {
125
- fullWidth: true,
126
- disabled,
127
- data: options,
128
- value: inputProps.value ?? options[0]?.value ?? "",
129
- onChange: (val) => {
130
- form.setFieldValue(field.path, val);
131
- }
132
- },
133
- field.path
134
- );
135
- }
136
- case "tags":
137
- return /* @__PURE__ */ jsx(
138
- TagsInput,
139
- {
140
- label,
141
- description,
142
- placeholder,
143
- disabled,
144
- value: inputProps.value ?? [],
145
- onChange: (val) => form.setFieldValue(field.path, val),
146
- error: inputProps.error,
147
- clearable: true
148
- },
149
- field.path
150
- );
151
- case "multiselect":
152
- return /* @__PURE__ */ jsx(
153
- MultiSelect,
154
- {
155
- label,
156
- description,
157
- placeholder,
158
- disabled,
159
- data: field.options ?? [],
160
- value: inputProps.value ?? [],
161
- onChange: (val) => form.setFieldValue(field.path, val),
162
- error: inputProps.error,
163
- clearable: true
164
- },
165
- field.path
166
- );
167
- case "select":
168
- return /* @__PURE__ */ jsx(
169
- Select,
170
- {
171
- label,
172
- description,
173
- placeholder,
174
- disabled,
175
- data: field.options ?? [],
176
- ...inputProps
177
- },
178
- field.path
179
- );
180
- case "textinput":
181
- return /* @__PURE__ */ jsx(
182
- TextInput,
183
- {
184
- label,
185
- description,
186
- placeholder,
187
- disabled,
188
- ...inputProps
189
- },
190
- field.path
191
- );
192
- case "textarea":
193
- return /* @__PURE__ */ jsx(
194
- Textarea,
195
- {
196
- label,
197
- description,
198
- placeholder,
199
- disabled,
200
- autosize: true,
201
- minRows: 3,
202
- ...inputProps
203
- },
204
- field.path
205
- );
206
- case "numberinput":
207
- return /* @__PURE__ */ jsx(
208
- NumberInput,
209
- {
210
- label,
211
- description,
212
- placeholder,
213
- disabled,
214
- min: field.min,
215
- max: field.max,
216
- step: field.step,
217
- allowDecimal: false,
218
- value: inputProps.value ?? 0,
219
- onChange: (val) => form.setFieldValue(field.path, typeof val === "number" ? val : 0),
220
- error: inputProps.error
221
- },
222
- field.path
223
- );
224
- case "switch":
225
- return /* @__PURE__ */ jsx(
226
- Switch,
227
- {
228
- label,
229
- description,
230
- disabled,
231
- checked: inputProps.value ?? false,
232
- onChange: (e) => form.setFieldValue(field.path, e.currentTarget.checked),
233
- error: inputProps.error
234
- },
235
- field.path
236
- );
237
- case "json": {
238
- const path = field.path;
239
- const state = jsonState[path];
240
- const rawValue = state?.raw ?? (() => {
241
- const current = inputProps.value;
242
- if (current === void 0 || current === null) return "";
243
- if (typeof current === "string") return current;
244
- try {
245
- return JSON.stringify(current, null, 2);
246
- } catch {
247
- return "";
248
- }
249
- })();
250
- return /* @__PURE__ */ jsx(
251
- JsonInput,
252
- {
253
- label,
254
- description,
255
- placeholder,
256
- disabled,
257
- value: rawValue,
258
- onChange: (next) => {
259
- if (!next.trim()) {
260
- setJsonState(path, { raw: next, invalid: false });
261
- form.setFieldValue(path, void 0);
262
- return;
263
- }
264
- try {
265
- const parsed = JSON.parse(next);
266
- setJsonState(path, { raw: next, invalid: false });
267
- form.setFieldValue(path, parsed);
268
- } catch {
269
- setJsonState(path, { raw: next, invalid: true });
270
- form.setFieldError(path, "Invalid JSON");
271
- }
272
- },
273
- error: state?.invalid ? "Invalid JSON" : inputProps.error,
274
- autosize: true,
275
- minRows: 3,
276
- formatOnBlur: true
277
- },
278
- path
279
- );
280
- }
281
- default:
282
- return null;
283
- }
284
- }
285
- function renderSection(section, values, form, disabled, jsonState, setJsonState) {
286
- if (section.when && !section.when(values)) return null;
287
- const visibleFields = section.fields.filter((f) => !f.when || f.when(values));
288
- return /* @__PURE__ */ jsxs(Stack, { gap: "sm", children: [
289
- section.title && /* @__PURE__ */ jsxs(Box, { children: [
290
- /* @__PURE__ */ jsx(Title, { order: 5, children: section.title }),
291
- section.description && /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: section.description })
292
- ] }),
293
- visibleFields.map((field) => renderField(field, form, disabled, jsonState, setJsonState))
294
- ] }, section.id);
295
- }
296
- function StepConfigForm({
297
- schema,
298
- layout,
299
- value,
300
- onChange,
301
- onAction,
302
- actions,
303
- disabled = false,
304
- slot = "all"
305
- }) {
306
- const initialValues = sanitizeInput(schema, value);
307
- const [validationError, setValidationError] = useState(null);
308
- const [advancedOpen, setAdvancedOpen] = useState(false);
309
- const [jsonState, setJsonStateMap] = useState({});
310
- const validationErrorRef = useRef(null);
311
- const setJsonState = (path, next) => {
312
- setJsonStateMap((prev) => ({ ...prev, [path]: next }));
313
- };
314
- const form = useForm({
315
- initialValues,
316
- validate: zod4Resolver(schema),
317
- // onValuesChange fires synchronously after each field update — no setTimeout needed.
318
- onValuesChange: (values2) => {
319
- if (validationErrorRef.current) {
320
- validationErrorRef.current = null;
321
- setValidationError(null);
322
- }
323
- onChange(values2);
324
- }
325
- });
326
- const handleActionClick = (action) => {
327
- const parsed = schema.safeParse(form.values);
328
- if (!parsed.success) {
329
- const firstIssue = parsed.error.issues[0];
330
- const fieldErrors = {};
331
- for (const issue of parsed.error.issues) {
332
- const path = issue.path.join(".");
333
- if (path && !fieldErrors[path]) {
334
- fieldErrors[path] = issue.message;
335
- }
336
- }
337
- form.setErrors(fieldErrors);
338
- const msg = firstIssue?.message ?? "Validation failed";
339
- validationErrorRef.current = msg;
340
- setValidationError(msg);
341
- return;
342
- }
343
- validationErrorRef.current = null;
344
- setValidationError(null);
345
- void onAction?.(action, parsed.data);
346
- };
347
- const values = form.values;
348
- if (slot === "advanced") {
349
- return /* @__PURE__ */ jsx(Stack, { gap: "md", children: layout.advanced ? renderSection(layout.advanced, values, form, disabled, jsonState, setJsonState) : /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: "No advanced settings for this step." }) });
350
- }
351
- return /* @__PURE__ */ jsxs(Stack, { gap: "md", children: [
352
- slot === "all" && validationError && /* @__PURE__ */ jsx(
353
- Alert,
354
- {
355
- color: "red",
356
- onClose: () => {
357
- validationErrorRef.current = null;
358
- setValidationError(null);
359
- },
360
- withCloseButton: true,
361
- children: validationError
362
- }
363
- ),
364
- layout.sections.map((section) => renderSection(section, values, form, disabled, jsonState, setJsonState)),
365
- slot === "all" && layout.advanced && /* @__PURE__ */ jsxs(Stack, { gap: "xs", children: [
366
- /* @__PURE__ */ jsx(
367
- Button,
368
- {
369
- variant: "subtle",
370
- size: "xs",
371
- onClick: () => setAdvancedOpen((o) => !o),
372
- style: { alignSelf: "flex-start" },
373
- children: advancedOpen ? "Hide advanced" : "Show advanced"
374
- }
375
- ),
376
- /* @__PURE__ */ jsx(Collapse, { in: advancedOpen, children: renderSection(layout.advanced, values, form, disabled, jsonState, setJsonState) })
377
- ] }),
378
- slot === "all" && actions && actions.length > 0 && /* @__PURE__ */ jsx(Group, { gap: "sm", children: actions.map((action) => /* @__PURE__ */ jsx(
379
- Button,
380
- {
381
- variant: action.variant ?? "filled",
382
- loading: action.loading,
383
- disabled: disabled || action.disabled,
384
- onClick: () => handleActionClick(action),
385
- children: action.label
386
- },
387
- action.id
388
- )) })
389
- ] });
390
- }
391
-
392
- // src/features/lead-gen/manifest.ts
393
- var leadGenManifest = {
394
- key: "lead-gen",
395
- systemId: "sales.lead-gen",
396
- capabilityIds: ["leadgen.lists.manage"],
397
- icon: IconTarget,
398
- sidebar: SalesSidebar
399
- };
400
- var LEAD_GEN_ROUTE_LINKS = [
401
- { label: "Overview", to: "/lead-gen" },
402
- { label: "Lists", to: "/lead-gen/lists" },
403
- { label: "Companies", to: "/lead-gen/companies" },
404
- { label: "Contacts", to: "/lead-gen/contacts" }
405
- ];
406
- function LeadGenRouteShell({
407
- title,
408
- caption,
409
- body,
410
- links = LEAD_GEN_ROUTE_LINKS
411
- }) {
412
- return /* @__PURE__ */ jsx(SubshellContentContainer, { children: /* @__PURE__ */ jsx(PageContainer, { children: /* @__PURE__ */ jsxs(Stack, { children: [
413
- /* @__PURE__ */ jsx(PageTitleCaption, { title, caption }),
414
- /* @__PURE__ */ jsx(Paper, { withBorder: true, p: "lg", children: /* @__PURE__ */ jsxs(Stack, { gap: "sm", children: [
415
- body,
416
- /* @__PURE__ */ jsx(Text, { children: links.map((link, index) => /* @__PURE__ */ jsxs("span", { children: [
417
- index > 0 ? " | " : null,
418
- /* @__PURE__ */ jsx(Anchor, { component: Link, to: link.to, children: link.label })
419
- ] }, link.to)) })
420
- ] }) })
421
- ] }) }) });
422
- }
423
- function formatDate(dateValue) {
424
- const date = typeof dateValue === "string" ? new Date(dateValue) : dateValue;
425
- return date.toLocaleDateString("en-US", {
426
- month: "short",
427
- day: "numeric",
428
- year: "numeric"
429
- });
430
- }
431
- function getStatusColor(status) {
432
- return status === "active" ? "green" : status === "invalid" ? "red" : "gray";
433
- }
434
- function getStateKeyColor(stateKey) {
435
- switch (stateKey) {
436
- case "draft":
437
- return "gray";
438
- case "enriching":
439
- return "blue";
440
- case "launched":
441
- return "green";
442
- case "closing":
443
- return "yellow";
444
- case "archived":
445
- return "dimmed";
446
- default:
447
- return "gray";
448
- }
449
- }
450
- function getEnrichmentColor(status) {
451
- switch (status) {
452
- case "complete":
453
- return "green";
454
- case "pending":
455
- return "yellow";
456
- case "failed":
457
- return "red";
458
- default:
459
- return "gray";
460
- }
461
- }
462
- function getEnrichmentStatus(enrichmentData) {
463
- if (!enrichmentData || typeof enrichmentData !== "object") return "pending";
464
- const website = enrichmentData.website;
465
- const linkedin = enrichmentData.linkedin;
466
- if (website === "complete" && linkedin === "complete") return "complete";
467
- if (website === "failed" || linkedin === "failed") return "failed";
468
- return "pending";
469
- }
470
- function useDeleteLists() {
471
- const { apiRequest, workOSOrganizationId } = useElevasisServices();
472
- const queryClient = useQueryClient();
473
- return useMutation({
474
- mutationFn: async (listIds) => {
475
- const uniqueIds = [...new Set(listIds)].filter(Boolean);
476
- if (uniqueIds.length === 0) return;
477
- await Promise.all(
478
- uniqueIds.map(
479
- (listId) => apiRequest(`/acquisition/lists/${listId}`, {
480
- method: "DELETE"
481
- })
482
- )
483
- );
484
- },
485
- onSuccess: () => {
486
- queryClient.invalidateQueries({ queryKey: acquisitionListKeys.list(workOSOrganizationId) });
487
- queryClient.invalidateQueries({ queryKey: acquisitionListKeys.telemetry(workOSOrganizationId) });
488
- showSuccessNotification("Lists deleted");
489
- },
490
- onError: (error) => {
491
- showApiErrorNotification(error);
492
- }
493
- });
494
- }
495
- function computeBacklog(current, completed) {
496
- return Math.max(current - completed, 0);
497
- }
498
- function getOverviewStatus(list) {
499
- if ((list.activeWorkflows?.length ?? 0) > 0) {
500
- return { label: "Active Work", color: "green" };
501
- }
502
- if (list.stageCounts.personalized > 0 && list.stageCounts.personalized === list.stageCounts.uploaded) {
503
- return { label: "Complete", color: "blue" };
504
- }
505
- return { label: "Idle", color: "gray" };
506
- }
507
- function getNextFocus(list) {
508
- if ((list.activeWorkflows?.length ?? 0) > 0) {
509
- return { label: "Workflow running", count: list.activeWorkflows?.length ?? 0 };
510
- }
511
- if (list.totalCompanies === 0 && list.totalContacts === 0) {
512
- return { label: "Populate list", count: null };
513
- }
514
- const extractGap = computeBacklog(list.stageCounts.populated, list.stageCounts.extracted);
515
- if (extractGap > 0) {
516
- return { label: "Extract website data", count: extractGap };
517
- }
518
- const qualifyGap = computeBacklog(list.stageCounts.extracted, list.stageCounts.qualified);
519
- if (qualifyGap > 0) {
520
- return { label: "Qualify companies", count: qualifyGap };
521
- }
522
- const discoverGap = computeBacklog(list.stageCounts.qualified, list.stageCounts.discovered);
523
- if (discoverGap > 0) {
524
- return { label: "Discover contacts", count: discoverGap };
525
- }
526
- const verifyGap = computeBacklog(list.stageCounts.discovered, list.stageCounts.verified);
527
- if (verifyGap > 0) {
528
- return { label: "Verify emails", count: verifyGap };
529
- }
530
- const personalizeGap = computeBacklog(list.stageCounts.verified, list.stageCounts.personalized);
531
- if (personalizeGap > 0) {
532
- return { label: "Personalize outreach", count: personalizeGap };
533
- }
534
- const uploadGap = computeBacklog(list.stageCounts.personalized, list.stageCounts.uploaded);
535
- if (uploadGap > 0) {
536
- return { label: "Upload contacts", count: uploadGap };
537
- }
538
- return { label: "Complete", count: null };
539
- }
540
- function formatCountLabel(value, noun) {
541
- return `${value} ${noun}${value === 1 ? "" : "s"}`;
542
- }
543
- function getPrimaryAction({
544
- uploadBacklog,
545
- personalizationBacklog,
546
- verificationBacklog,
547
- deliverabilityRiskCount,
548
- activeListCount,
549
- totalLists
550
- }) {
551
- if (uploadBacklog > 0) {
552
- return {
553
- title: `Upload ${formatCountLabel(uploadBacklog, "personalized contact")}`,
554
- detail: "No verification blockers are left on the contacts already ready for outreach.",
555
- buttonLabel: "Review lists",
556
- buttonTo: "/lead-gen/lists",
557
- tone: "blue"
558
- };
559
- }
560
- if (personalizationBacklog > 0) {
561
- return {
562
- title: `Personalize ${formatCountLabel(personalizationBacklog, "verified contact")}`,
563
- detail: "Verification is done. The next bottleneck is drafting outreach copy.",
564
- buttonLabel: "Open lists",
565
- buttonTo: "/lead-gen/lists",
566
- tone: "blue"
567
- };
568
- }
569
- if (verificationBacklog > 0) {
570
- return {
571
- title: `Verify ${formatCountLabel(verificationBacklog, "discovered contact")}`,
572
- detail: "Email validation is the main blocker before personalization can continue.",
573
- buttonLabel: "Open lists",
574
- buttonTo: "/lead-gen/lists",
575
- tone: "orange"
576
- };
577
- }
578
- if (deliverabilityRiskCount > 0) {
579
- return {
580
- title: `Resolve ${formatCountLabel(deliverabilityRiskCount, "deliverability risk")}`,
581
- detail: "Risky, invalid, or bounced records are the main campaign health issue right now.",
582
- buttonLabel: "Review lists",
583
- buttonTo: "/lead-gen/lists",
584
- tone: "orange"
585
- };
586
- }
587
- if (activeListCount > 0) {
588
- return {
589
- title: `Monitor ${formatCountLabel(activeListCount, "active list")}`,
590
- detail: "Workflows are running and there are no immediate contact-stage backlogs to clear.",
591
- buttonLabel: "Open lists",
592
- buttonTo: "/lead-gen/lists",
593
- tone: "green"
594
- };
595
- }
596
- if (totalLists > 0) {
597
- return {
598
- title: "No blockers right now",
599
- detail: "Lists are idle and the current pipeline does not show an urgent follow-up step.",
600
- buttonLabel: "Review lists",
601
- buttonTo: "/lead-gen/lists",
602
- tone: "gray"
603
- };
604
- }
605
- return {
606
- title: "Create your first list",
607
- detail: "Lead gen telemetry will appear here once a list starts moving through the pipeline.",
608
- buttonLabel: "Open lists",
609
- buttonTo: "/lead-gen/lists",
610
- tone: "gray"
611
- };
612
- }
613
- function LeadGenOverviewPage() {
614
- const navigate = useNavigate();
615
- const telemetryQuery = useListsTelemetry();
616
- const listsQuery = useLists();
617
- const data = telemetryQuery.data;
618
- const listMetaById = new Map((listsQuery.data ?? []).map((list) => [list.id, list]));
619
- const stageTotals = data?.reduce(
620
- (acc, list) => ({
621
- populated: acc.populated + list.stageCounts.populated,
622
- extracted: acc.extracted + list.stageCounts.extracted,
623
- qualified: acc.qualified + list.stageCounts.qualified,
624
- discovered: acc.discovered + list.stageCounts.discovered,
625
- verified: acc.verified + list.stageCounts.verified,
626
- personalized: acc.personalized + list.stageCounts.personalized,
627
- uploaded: acc.uploaded + list.stageCounts.uploaded
628
- }),
629
- { populated: 0, extracted: 0, qualified: 0, discovered: 0, verified: 0, personalized: 0, uploaded: 0 }
630
- ) ?? { discovered: 0, verified: 0, personalized: 0, uploaded: 0 };
631
- const deliverabilityTotals = data?.reduce(
632
- (acc, list) => ({
633
- valid: acc.valid + list.deliverability.valid,
634
- risky: acc.risky + list.deliverability.risky,
635
- invalid: acc.invalid + list.deliverability.invalid,
636
- unknown: acc.unknown + list.deliverability.unknown,
637
- bounced: acc.bounced + list.deliverability.bounced
638
- }),
639
- { valid: 0, risky: 0, invalid: 0, unknown: 0, bounced: 0 }
640
- ) ?? { risky: 0, invalid: 0, bounced: 0 };
641
- const activeListCount = data?.filter((list) => (list.activeWorkflows?.length ?? 0) > 0).length ?? 0;
642
- const verificationBacklog = computeBacklog(stageTotals.discovered, stageTotals.verified);
643
- const personalizationBacklog = computeBacklog(stageTotals.verified, stageTotals.personalized);
644
- const uploadBacklog = computeBacklog(stageTotals.personalized, stageTotals.uploaded);
645
- const deliverabilityRiskCount = deliverabilityTotals.risky + deliverabilityTotals.invalid + deliverabilityTotals.bounced;
646
- const totalLists = data?.length ?? 0;
647
- const summaryLine = [
648
- `${totalLists} total ${totalLists === 1 ? "list" : "lists"}`,
649
- `${formatCountLabel(uploadBacklog, "contact")} ready for upload`,
650
- deliverabilityRiskCount === 0 ? "no risks" : `${formatCountLabel(deliverabilityRiskCount, "risk")}`
651
- ].join(" | ");
652
- const primaryAction = getPrimaryAction({
653
- uploadBacklog,
654
- personalizationBacklog,
655
- verificationBacklog,
656
- deliverabilityRiskCount,
657
- activeListCount,
658
- totalLists
659
- });
660
- const overviewRows = data?.map((list) => {
661
- const listMeta = listMetaById.get(list.listId);
662
- const nextFocus = getNextFocus(list);
663
- return {
664
- ...list,
665
- name: listMeta?.name ?? `List ${list.listId.slice(0, 8)}`,
666
- nextFocus,
667
- status: getOverviewStatus(list)
668
- };
669
- }).sort((a, b) => {
670
- const aActive = a.status.label === "Active Work" ? 1 : 0;
671
- const bActive = b.status.label === "Active Work" ? 1 : 0;
672
- if (aActive !== bActive) return bActive - aActive;
673
- const aBacklog = computeBacklog(a.stageCounts.discovered, a.stageCounts.verified) + computeBacklog(a.stageCounts.verified, a.stageCounts.personalized) + computeBacklog(a.stageCounts.personalized, a.stageCounts.uploaded);
674
- const bBacklog = computeBacklog(b.stageCounts.discovered, b.stageCounts.verified) + computeBacklog(b.stageCounts.verified, b.stageCounts.personalized) + computeBacklog(b.stageCounts.personalized, b.stageCounts.uploaded);
675
- if (aBacklog !== bBacklog) return bBacklog - aBacklog;
676
- return b.totalContacts - a.totalContacts;
677
- }) ?? [];
678
- return /* @__PURE__ */ jsx(SubshellContentContainer, { children: /* @__PURE__ */ jsx(PageContainer, { children: /* @__PURE__ */ jsxs(Stack, { children: [
679
- /* @__PURE__ */ jsx(PageTitleCaption, { title: "Lead Gen Overview", caption: summaryLine }),
680
- telemetryQuery.isLoading ? /* @__PURE__ */ jsx(Paper, { withBorder: true, children: /* @__PURE__ */ jsx(Center, { p: "xl", children: /* @__PURE__ */ jsx(Loader, {}) }) }) : telemetryQuery.isError ? /* @__PURE__ */ jsx(Paper, { withBorder: true, children: /* @__PURE__ */ jsx(CenteredErrorState, { error: telemetryQuery.error, title: "Failed to load list telemetry" }) }) : !data?.length ? /* @__PURE__ */ jsx(Paper, { withBorder: true, children: /* @__PURE__ */ jsx(Center, { h: 300, children: /* @__PURE__ */ jsx(Alert, { icon: /* @__PURE__ */ jsx(IconAlertCircle, { size: 16 }), color: "gray", variant: "light", children: "No lists yet." }) }) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
681
- /* @__PURE__ */ jsxs(SimpleGrid, { cols: { base: 1, sm: 3 }, children: [
682
- /* @__PURE__ */ jsx(StatCard, { variant: "hero", icon: IconPlayerPlay, value: totalLists, label: "Total Lists" }),
683
- /* @__PURE__ */ jsx(StatCard, { variant: "hero", icon: IconArrowRight, value: uploadBacklog, label: "Ready Contacts" }),
684
- /* @__PURE__ */ jsx(StatCard, { variant: "hero", icon: IconAlertCircle, value: deliverabilityRiskCount, label: "At Risk" })
685
- ] }),
686
- /* @__PURE__ */ jsx(Paper, { withBorder: true, p: "md", children: /* @__PURE__ */ jsxs(Stack, { gap: "md", children: [
687
- /* @__PURE__ */ jsx(CardHeader, { icon: /* @__PURE__ */ jsx(IconSparkles, { size: 18 }), title: "Next Action", subtitle: primaryAction.title }),
688
- /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", maw: 620, children: primaryAction.detail }),
689
- /* @__PURE__ */ jsxs(SimpleGrid, { cols: { base: 1, md: 2 }, children: [
690
- /* @__PURE__ */ jsxs(Card, { withBorder: true, p: "sm", children: [
691
- /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", tt: "uppercase", fw: 600, children: "Verification" }),
692
- /* @__PURE__ */ jsxs(Group, { justify: "space-between", align: "flex-end", mt: 6, children: [
693
- /* @__PURE__ */ jsx(Title, { order: 4, children: verificationBacklog }),
694
- /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: "waiting" })
695
- ] })
696
- ] }),
697
- /* @__PURE__ */ jsxs(Card, { withBorder: true, p: "sm", children: [
698
- /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", tt: "uppercase", fw: 600, children: "Personalization" }),
699
- /* @__PURE__ */ jsxs(Group, { justify: "space-between", align: "flex-end", mt: 6, children: [
700
- /* @__PURE__ */ jsx(Title, { order: 4, children: personalizationBacklog }),
701
- /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: "ready" })
702
- ] })
703
- ] })
704
- ] })
705
- ] }) }),
706
- /* @__PURE__ */ jsx(Paper, { withBorder: true, p: "md", children: /* @__PURE__ */ jsxs(Stack, { gap: "md", children: [
707
- /* @__PURE__ */ jsx(
708
- CardHeader,
709
- {
710
- icon: /* @__PURE__ */ jsx(IconListDetails, { size: 18 }),
711
- title: "List Snapshot",
712
- subtitle: "Ranked to surface active work first, then the largest unresolved backlogs."
713
- }
714
- ),
715
- /* @__PURE__ */ jsxs(Table, { children: [
716
- /* @__PURE__ */ jsx(Table.Thead, { children: /* @__PURE__ */ jsxs(Table.Tr, { children: [
717
- /* @__PURE__ */ jsx(Table.Th, { children: "List" }),
718
- /* @__PURE__ */ jsx(Table.Th, { children: "Companies" }),
719
- /* @__PURE__ */ jsx(Table.Th, { children: "Contacts" }),
720
- /* @__PURE__ */ jsx(Table.Th, { children: "Next Focus" }),
721
- /* @__PURE__ */ jsx(Table.Th, { children: "Status" })
722
- ] }) }),
723
- /* @__PURE__ */ jsx(Table.Tbody, { children: overviewRows.map((list) => /* @__PURE__ */ jsxs(
724
- Table.Tr,
725
- {
726
- style: { cursor: "pointer" },
727
- onClick: () => navigate({ to: "/lead-gen/lists/$listId", params: { listId: list.listId } }),
728
- children: [
729
- /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsxs(Stack, { gap: 2, children: [
730
- /* @__PURE__ */ jsx(Text, { size: "sm", fw: 600, children: list.name }),
731
- /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: list.listId })
732
- ] }) }),
733
- /* @__PURE__ */ jsx(Table.Td, { children: list.totalCompanies }),
734
- /* @__PURE__ */ jsx(Table.Td, { children: list.totalContacts }),
735
- /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsxs(Stack, { gap: 2, children: [
736
- /* @__PURE__ */ jsx(Text, { size: "sm", fw: 500, children: list.nextFocus.label }),
737
- /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: list.nextFocus.count == null ? "No outstanding count" : `${list.nextFocus.count} item${list.nextFocus.count === 1 ? "" : "s"} queued` })
738
- ] }) }),
739
- /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "light", color: list.status.color, children: list.status.label }) })
740
- ]
741
- },
742
- list.listId
743
- )) })
744
- ] })
745
- ] }) })
746
- ] })
747
- ] }) }) });
748
- }
749
- var PAGE_SIZE_DEFAULT = 20;
750
- var BUILD_TEMPLATE_SELECT_OPTIONS = PROSPECTING_BUILD_TEMPLATE_OPTIONS.map((template) => ({
751
- value: template.id,
752
- label: template.label
753
- }));
754
- var STATUS_FILTER_OPTIONS = [
755
- { value: "", label: "All statuses" },
756
- { value: "draft", label: "Draft" },
757
- { value: "enriching", label: "Enriching" },
758
- { value: "launched", label: "Launched" },
759
- { value: "closing", label: "Closing" },
760
- { value: "archived", label: "Archived" }
761
- ];
762
- function LeadGenListsPage() {
763
- const navigate = useNavigate();
764
- const [searchQuery, setSearchQuery] = useState("");
765
- const [statusFilter, setStatusFilter] = useState("");
766
- const [showCreateList, setShowCreateList] = useState(false);
767
- const [newListName, setNewListName] = useState("");
768
- const [newListDescription, setNewListDescription] = useState("");
769
- const [newBuildTemplateId, setNewBuildTemplateId] = useState(DEFAULT_PROSPECTING_BUILD_TEMPLATE_ID);
770
- const [showBatchDelete, setShowBatchDelete] = useState(false);
771
- const { data: lists, isLoading: listsLoading } = useLists();
772
- const { data: telemetry, isLoading: telemetryLoading } = useListsTelemetry();
773
- const createListMutation = useCreateList();
774
- const deleteListsMutation = useDeleteLists();
775
- const { sort, toggleSort } = useTableSort("created");
776
- const contactCountByListId = useMemo(
777
- () => new Map((telemetry ?? []).map((item) => [item.listId, item.totalContacts])),
778
- [telemetry]
779
- );
780
- const sortAccessors = useMemo(
781
- () => ({
782
- name: (list) => list.name,
783
- description: (list) => list.description || "",
784
- contacts: (list) => contactCountByListId.get(list.id) ?? 0,
785
- batches: (list) => list.batchIds.length,
786
- created: (list) => list.createdAt,
787
- status: (list) => list.status
788
- }),
789
- [contactCountByListId]
790
- );
791
- const filteredLists = useMemo(() => {
792
- if (!lists) return [];
793
- let result = lists;
794
- if (searchQuery.trim()) {
795
- const query = searchQuery.toLowerCase();
796
- result = result.filter((list) => list.name.toLowerCase().includes(query));
797
- }
798
- if (statusFilter) {
799
- result = result.filter((list) => list.status === statusFilter);
800
- }
801
- return result;
802
- }, [lists, searchQuery, statusFilter]);
803
- const sortedLists = useMemo(() => sortData(filteredLists, sort, sortAccessors), [filteredLists, sort, sortAccessors]);
804
- const pagination = usePaginationState(PAGE_SIZE_DEFAULT, [searchQuery, statusFilter], sortedLists.length);
805
- const paginatedLists = useMemo(
806
- () => sortedLists.slice(pagination.offset, pagination.offset + PAGE_SIZE_DEFAULT),
807
- [sortedLists, pagination.offset]
808
- );
809
- const selection = useTableSelection(paginatedLists, sortedLists);
810
- const selectedBuildTemplate = PROSPECTING_BUILD_TEMPLATE_OPTIONS.find((template) => template.id === newBuildTemplateId);
811
- function resetCreateListModal() {
812
- setNewListName("");
813
- setNewListDescription("");
814
- setNewBuildTemplateId(DEFAULT_PROSPECTING_BUILD_TEMPLATE_ID);
815
- setShowCreateList(false);
816
- }
817
- function handleCreateList() {
818
- const trimmedName = newListName.trim();
819
- if (!trimmedName) return;
820
- const body = {
821
- name: trimmedName,
822
- description: newListDescription.trim() || null,
823
- buildTemplateId: newBuildTemplateId
824
- };
825
- createListMutation.mutate(body, {
826
- onSuccess: (list) => {
827
- resetCreateListModal();
828
- navigate({ to: "/lead-gen/lists/$listId", params: { listId: list.id } });
829
- }
830
- });
831
- }
832
- return /* @__PURE__ */ jsx(SubshellContentContainer, { children: /* @__PURE__ */ jsxs(PageContainer, { children: [
833
- /* @__PURE__ */ jsx(Stack, { children: /* @__PURE__ */ jsx(
834
- PageTitleCaption,
835
- {
836
- title: "Lists",
837
- caption: "Lead lists and contact organization",
838
- rightSection: /* @__PURE__ */ jsx(
839
- Button,
840
- {
841
- leftSection: /* @__PURE__ */ jsx(IconPlus, { size: 16 }),
842
- onClick: () => setShowCreateList(true),
843
- variant: "light",
844
- size: "xs",
845
- children: "New List"
846
- }
847
- )
848
- }
849
- ) }),
850
- /* @__PURE__ */ jsx(Paper, { children: /* @__PURE__ */ jsxs(Stack, { children: [
851
- /* @__PURE__ */ jsxs(
852
- FilterBar,
853
- {
854
- actions: /* @__PURE__ */ jsx(
855
- TableSelectionToolbar,
856
- {
857
- selectedCount: selection.selectedCount,
858
- onDelete: () => setShowBatchDelete(true),
859
- isDeleting: deleteListsMutation.isPending
860
- }
861
- ),
862
- children: [
863
- /* @__PURE__ */ jsx(
864
- TextInput,
865
- {
866
- placeholder: "Search by name...",
867
- leftSection: /* @__PURE__ */ jsx(IconSearch, { size: 16 }),
868
- value: searchQuery,
869
- onChange: (e) => setSearchQuery(e.currentTarget.value)
870
- }
871
- ),
872
- /* @__PURE__ */ jsx(
873
- Select,
874
- {
875
- placeholder: "All statuses",
876
- data: STATUS_FILTER_OPTIONS,
877
- value: statusFilter,
878
- onChange: (value) => setStatusFilter(value ?? ""),
879
- clearable: true,
880
- size: "sm",
881
- w: 160
882
- }
883
- )
884
- ]
885
- }
886
- ),
887
- listsLoading || telemetryLoading ? /* @__PURE__ */ jsx(Center, { p: "xl", children: /* @__PURE__ */ jsx(Loader, {}) }) : !filteredLists.length ? /* @__PURE__ */ jsx(
888
- EmptyState,
889
- {
890
- icon: IconList,
891
- title: searchQuery.trim() || statusFilter ? "No lists match your filters" : "No lists yet",
892
- description: searchQuery.trim() || statusFilter ? void 0 : "Create one to get started.",
893
- action: searchQuery.trim() || statusFilter ? void 0 : {
894
- label: "Create List",
895
- onClick: () => setShowCreateList(true),
896
- icon: /* @__PURE__ */ jsx(IconPlus, { size: 16 })
897
- }
898
- }
899
- ) : /* @__PURE__ */ jsxs(Table, { children: [
900
- /* @__PURE__ */ jsx(Table.Thead, { children: /* @__PURE__ */ jsxs(Table.Tr, { children: [
901
- /* @__PURE__ */ jsx(Table.Th, { w: 40, children: /* @__PURE__ */ jsx(
902
- Checkbox,
903
- {
904
- checked: selection.isPageAllSelected,
905
- indeterminate: selection.isPagePartiallySelected,
906
- onChange: selection.togglePage
907
- }
908
- ) }),
909
- /* @__PURE__ */ jsx(SortableHeader, { column: "name", sort, onToggle: toggleSort, children: "Name" }),
910
- /* @__PURE__ */ jsx(SortableHeader, { column: "description", sort, onToggle: toggleSort, children: "Description" }),
911
- /* @__PURE__ */ jsx(SortableHeader, { column: "status", sort, onToggle: toggleSort, children: "Status" }),
912
- /* @__PURE__ */ jsx(Table.Th, { children: "Pipeline" }),
913
- /* @__PURE__ */ jsx(SortableHeader, { column: "contacts", sort, onToggle: toggleSort, children: "Contacts" }),
914
- /* @__PURE__ */ jsx(SortableHeader, { column: "batches", sort, onToggle: toggleSort, children: "Batches" }),
915
- /* @__PURE__ */ jsx(SortableHeader, { column: "created", sort, onToggle: toggleSort, children: "Created" }),
916
- /* @__PURE__ */ jsx(Table.Th, { children: "List" })
917
- ] }) }),
918
- /* @__PURE__ */ jsx(Table.Tbody, { children: paginatedLists.map((list) => {
919
- const pipelineLabel = list.metadata.buildPlanSnapshot?.templateLabel;
920
- return /* @__PURE__ */ jsxs(
921
- Table.Tr,
922
- {
923
- style: { cursor: "pointer" },
924
- onClick: () => navigate({ to: "/lead-gen/lists/$listId", params: { listId: list.id } }),
925
- children: [
926
- /* @__PURE__ */ jsx(Table.Td, { onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsx(
927
- Checkbox,
928
- {
929
- checked: selection.isSelected(list.id),
930
- onChange: () => selection.toggle(list.id)
931
- }
932
- ) }),
933
- /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsxs(Group, { gap: 6, wrap: "nowrap", children: [
934
- /* @__PURE__ */ jsx(Text, { fw: 500, children: list.name }),
935
- list.instantlyCampaignId ? /* @__PURE__ */ jsx(
936
- Anchor,
937
- {
938
- href: `https://app.instantly.ai/app/campaign/${list.instantlyCampaignId}/analytics`,
939
- target: "_blank",
940
- rel: "noreferrer",
941
- onClick: (e) => e.stopPropagation(),
942
- c: "dimmed",
943
- lh: 1,
944
- children: /* @__PURE__ */ jsx(IconExternalLink, { size: 16 })
945
- }
946
- ) : null
947
- ] }) }),
948
- /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: list.description || "-" }) }),
949
- /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Badge, { size: "sm", color: getStateKeyColor(list.status), variant: "light", children: list.status }) }),
950
- /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: pipelineLabel || "\u2014" }) }),
951
- /* @__PURE__ */ jsx(Table.Td, { children: contactCountByListId.get(list.id) ?? 0 }),
952
- /* @__PURE__ */ jsx(Table.Td, { children: list.batchIds.length }),
953
- /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: formatDate(list.createdAt) }) }),
954
- /* @__PURE__ */ jsx(Table.Td, { onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsx(
955
- Button,
956
- {
957
- size: "xs",
958
- variant: "subtle",
959
- leftSection: /* @__PURE__ */ jsx(IconList, { size: 14 }),
960
- onClick: () => navigate({ to: "/lead-gen/lists/$listId", params: { listId: list.id } }),
961
- children: "Open"
962
- }
963
- ) })
964
- ]
965
- },
966
- list.id
967
- );
968
- }) })
969
- ] }),
970
- sortedLists.length > PAGE_SIZE_DEFAULT && /* @__PURE__ */ jsx(Group, { justify: "center", children: /* @__PURE__ */ jsx(
971
- Pagination,
972
- {
973
- value: pagination.page,
974
- onChange: pagination.setPage,
975
- total: pagination.totalPages(sortedLists.length),
976
- size: "sm"
977
- }
978
- ) })
979
- ] }) }),
980
- /* @__PURE__ */ jsx(
981
- CustomModal,
982
- {
983
- opened: showCreateList,
984
- onClose: () => !createListMutation.isPending && resetCreateListModal(),
985
- size: "md",
986
- loading: createListMutation.isPending,
987
- children: /* @__PURE__ */ jsxs(Stack, { gap: "md", children: [
988
- /* @__PURE__ */ jsxs(Group, { gap: "sm", children: [
989
- /* @__PURE__ */ jsx(IconSparkles, { size: 24 }),
990
- /* @__PURE__ */ jsx(Title, { order: 4, children: "Create List" })
991
- ] }),
992
- /* @__PURE__ */ jsx(
993
- TextInput,
994
- {
995
- label: "List Name",
996
- placeholder: "e.g. Orange County Vets Q2",
997
- value: newListName,
998
- onChange: (event) => setNewListName(event.currentTarget.value),
999
- disabled: createListMutation.isPending,
1000
- required: true
1001
- }
1002
- ),
1003
- /* @__PURE__ */ jsx(
1004
- Textarea,
1005
- {
1006
- label: "Description",
1007
- placeholder: "Optional context for this list",
1008
- value: newListDescription,
1009
- onChange: (event) => setNewListDescription(event.currentTarget.value),
1010
- disabled: createListMutation.isPending,
1011
- minRows: 3
1012
- }
1013
- ),
1014
- /* @__PURE__ */ jsxs(Stack, { gap: 4, children: [
1015
- /* @__PURE__ */ jsx(
1016
- Select,
1017
- {
1018
- label: "Build Pipeline",
1019
- description: "Choose the sequence this list should follow.",
1020
- data: BUILD_TEMPLATE_SELECT_OPTIONS,
1021
- value: newBuildTemplateId,
1022
- onChange: (value) => setNewBuildTemplateId(value ?? DEFAULT_PROSPECTING_BUILD_TEMPLATE_ID),
1023
- disabled: createListMutation.isPending,
1024
- required: true
1025
- }
1026
- ),
1027
- selectedBuildTemplate?.description ? /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: selectedBuildTemplate.description }) : null
1028
- ] }),
1029
- /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: "The list will open in draft mode after creation so you can review configuration and run build steps from the list workspace." }),
1030
- /* @__PURE__ */ jsxs(Group, { justify: "flex-end", children: [
1031
- /* @__PURE__ */ jsx(Button, { variant: "light", onClick: resetCreateListModal, disabled: createListMutation.isPending, children: "Cancel" }),
1032
- /* @__PURE__ */ jsx(Button, { onClick: handleCreateList, loading: createListMutation.isPending, disabled: !newListName.trim(), children: "Create List" })
1033
- ] })
1034
- ] })
1035
- }
1036
- ),
1037
- /* @__PURE__ */ jsx(
1038
- CustomModal,
1039
- {
1040
- opened: showBatchDelete,
1041
- onClose: () => !deleteListsMutation.isPending && setShowBatchDelete(false),
1042
- size: "sm",
1043
- loading: deleteListsMutation.isPending,
1044
- children: /* @__PURE__ */ jsxs(Stack, { gap: "md", children: [
1045
- /* @__PURE__ */ jsxs(Group, { gap: "sm", children: [
1046
- /* @__PURE__ */ jsx(IconAlertTriangle, { size: 24, color: "var(--color-error)" }),
1047
- /* @__PURE__ */ jsxs(Title, { order: 4, children: [
1048
- "Delete ",
1049
- selection.selectedCount,
1050
- " Lists"
1051
- ] })
1052
- ] }),
1053
- /* @__PURE__ */ jsxs(Text, { size: "sm", children: [
1054
- "Are you sure you want to delete",
1055
- " ",
1056
- /* @__PURE__ */ jsx(Text, { span: true, fw: 600, children: selection.selectedCount }),
1057
- " ",
1058
- "selected lists?"
1059
- ] }),
1060
- /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: "This action cannot be undone." }),
1061
- /* @__PURE__ */ jsxs(Group, { justify: "flex-end", mt: "md", children: [
1062
- /* @__PURE__ */ jsx(
1063
- Button,
1064
- {
1065
- variant: "light",
1066
- onClick: () => setShowBatchDelete(false),
1067
- disabled: deleteListsMutation.isPending,
1068
- children: "Cancel"
1069
- }
1070
- ),
1071
- /* @__PURE__ */ jsx(
1072
- Button,
1073
- {
1074
- color: "red",
1075
- loading: deleteListsMutation.isPending,
1076
- onClick: () => {
1077
- deleteListsMutation.mutate([...selection.selectedIds], {
1078
- onSuccess: () => {
1079
- setShowBatchDelete(false);
1080
- selection.clear();
1081
- }
1082
- });
1083
- },
1084
- children: "Delete"
1085
- }
1086
- )
1087
- ] })
1088
- ] })
1089
- }
1090
- )
1091
- ] }) });
1092
- }
1093
-
1094
- // src/lib/lead-gen/legacy-pipeline.ts
1095
- function resolveLegacyListMemberPipelineLabel(member) {
1096
- const defs = LEAD_GEN_PIPELINE_DEFINITIONS["acq.list-member"];
1097
- if (!defs) return null;
1098
- const pipeline = findPipeline(defs, member.pipelineKey);
1099
- if (!pipeline) return null;
1100
- const stage = pipeline.stages.find((s) => s.stageKey === member.stageKey);
1101
- if (!stage) return null;
1102
- const state = stage.states.find((s) => s.stateKey === member.stateKey);
1103
- return {
1104
- stageLabel: stage.label,
1105
- stateLabel: state?.label ?? member.stateKey
1106
- };
1107
- }
1108
- var LEAD_GEN_STAGE_CATALOG = getLeadGenStageCatalog(canonicalOrganizationModel);
1109
- function formatDateTime(value) {
1110
- if (!value) return "Not yet";
1111
- return new Date(value).toLocaleString("en-US", {
1112
- month: "short",
1113
- day: "numeric",
1114
- year: "numeric",
1115
- hour: "numeric",
1116
- minute: "2-digit"
1117
- });
1118
- }
1119
- function getMemberStateColor(stateKey) {
1120
- const stage = LEAD_GEN_STAGE_CATALOG[stateKey];
1121
- if (stage?.entity === "contact") return "green";
1122
- if (stage?.entity === "company") return "blue";
1123
- return "gray";
1124
- }
1125
- function ArtifactsPanel({ listMemberId }) {
1126
- const { data, isLoading, error } = useArtifacts({ ownerKind: "list_member", ownerId: listMemberId });
1127
- const artifacts = data?.artifacts ?? [];
1128
- if (isLoading) {
1129
- return /* @__PURE__ */ jsx(Center, { p: "xl", children: /* @__PURE__ */ jsx(Loader, { size: "sm" }) });
1130
- }
1131
- if (error) {
1132
- return /* @__PURE__ */ jsx(CenteredErrorState, { error, title: "Failed to load artifacts" });
1133
- }
1134
- if (!artifacts.length) {
1135
- return /* @__PURE__ */ jsx(Alert, { icon: /* @__PURE__ */ jsx(IconDatabase, { size: 16 }), color: "gray", variant: "light", children: "No artifacts recorded for this member yet." });
1136
- }
1137
- return /* @__PURE__ */ jsx(Stack, { gap: "sm", children: artifacts.map((artifact) => /* @__PURE__ */ jsx(Card, { withBorder: true, children: /* @__PURE__ */ jsxs(Stack, { gap: "xs", children: [
1138
- /* @__PURE__ */ jsxs(Group, { justify: "space-between", children: [
1139
- /* @__PURE__ */ jsxs(Group, { gap: "xs", children: [
1140
- /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "light", color: "blue", children: artifact.kind }),
1141
- /* @__PURE__ */ jsxs(Text, { size: "xs", c: "dimmed", children: [
1142
- "v",
1143
- artifact.version
1144
- ] })
1145
- ] }),
1146
- /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: formatDateTime(artifact.createdAt) })
1147
- ] }),
1148
- /* @__PURE__ */ jsx(JsonViewer, { data: artifact.content, maxHeight: 240 })
1149
- ] }) }, artifact.id)) });
1150
- }
1151
- function ListMemberDrawerContent({ member, listId }) {
1152
- const transitionMember = useTransitionListMember();
1153
- const statefulItem = useMemo(
1154
- () => ({
1155
- id: member.id,
1156
- pipeline_key: member.pipelineKey,
1157
- stage_key: member.stageKey,
1158
- state_key: member.stateKey,
1159
- activity_log: member.activityLog
1160
- }),
1161
- [member]
1162
- );
1163
- const derivedActions = useDeriveActions(statefulItem);
1164
- const resolved = resolveLegacyListMemberPipelineLabel(member);
1165
- const contactName = [member.contact?.firstName, member.contact?.lastName].filter(Boolean).join(" ").trim() || member.contact?.email || "\u2014";
1166
- return /* @__PURE__ */ jsxs(Stack, { gap: "md", p: "md", children: [
1167
- /* @__PURE__ */ jsxs(Stack, { gap: 4, children: [
1168
- /* @__PURE__ */ jsx(Group, { gap: "xs", align: "center", children: resolved ? /* @__PURE__ */ jsxs(Fragment, { children: [
1169
- /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "light", color: "blue", children: resolved.stageLabel }),
1170
- /* @__PURE__ */ jsx(IconChevronRight, { size: 14, style: { color: "var(--color-text-dimmed)" } }),
1171
- /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "filled", color: getMemberStateColor(member.stateKey), children: resolved.stateLabel })
1172
- ] }) : /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "filled", color: getMemberStateColor(member.stateKey), children: member.stateKey }) }),
1173
- /* @__PURE__ */ jsx(Title, { order: 4, children: contactName }),
1174
- member.contact?.email && /* @__PURE__ */ jsxs(Group, { gap: "xs", children: [
1175
- /* @__PURE__ */ jsx(IconMail, { size: 14, style: { color: "var(--color-text-dimmed)" } }),
1176
- /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: member.contact.email })
1177
- ] }),
1178
- member.contact?.title && /* @__PURE__ */ jsxs(Group, { gap: "xs", children: [
1179
- /* @__PURE__ */ jsx(IconUser, { size: 14, style: { color: "var(--color-text-dimmed)" } }),
1180
- /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: member.contact.title })
1181
- ] })
1182
- ] }),
1183
- derivedActions.length > 0 && /* @__PURE__ */ jsx(Group, { gap: "xs", children: derivedActions.map((action) => /* @__PURE__ */ jsx(
1184
- Button,
1185
- {
1186
- variant: "light",
1187
- size: "xs",
1188
- loading: transitionMember.isPending,
1189
- onClick: () => {
1190
- transitionMember.mutate({
1191
- memberId: member.id,
1192
- listId,
1193
- pipelineKey: member.pipelineKey,
1194
- // Lead-gen action dispatch targets are resolved server-side via the action key.
1195
- // Stage/state keys are no longer embedded in the client-side Action shape.
1196
- stageKey: action.key,
1197
- stateKey: null
1198
- });
1199
- },
1200
- children: action.label
1201
- },
1202
- action.key
1203
- )) }),
1204
- /* @__PURE__ */ jsx(ArtifactsPanel, { listMemberId: member.id })
1205
- ] });
1206
- }
1207
- function CompanyMemberDrawerContent({ companyId: _companyId }) {
1208
- return /* @__PURE__ */ jsxs(Stack, { gap: "md", p: "md", children: [
1209
- /* @__PURE__ */ jsxs(Stack, { gap: 4, children: [
1210
- /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "light", color: "gray", children: "company member" }),
1211
- /* @__PURE__ */ jsx(Title, { order: 4, children: "Company" })
1212
- ] }),
1213
- /* @__PURE__ */ jsx(Alert, { icon: /* @__PURE__ */ jsx(IconAlertCircle, { size: 16 }), color: "blue", variant: "light", children: "Company-level pipeline transitions are not yet available in this view." })
1214
- ] });
1215
- }
1216
- function ContactMemberDrawerShell({ contactId, listId }) {
1217
- const { data: membersData, isLoading, error } = useListMembers({ listId });
1218
- const listMember = useMemo(() => {
1219
- if (!membersData?.members) return null;
1220
- return membersData.members.find((m) => m.contactId === contactId) ?? null;
1221
- }, [membersData, contactId]);
1222
- const directQuery = useListMember(listMember === null && !isLoading ? contactId : "");
1223
- const resolvedMember = listMember ?? directQuery.data ?? null;
1224
- if (isLoading) {
1225
- return /* @__PURE__ */ jsx(Center, { p: "xl", children: /* @__PURE__ */ jsx(Loader, { size: "sm" }) });
1226
- }
1227
- if (error) {
1228
- return /* @__PURE__ */ jsx(CenteredErrorState, { error, title: "Failed to load member detail" });
1229
- }
1230
- if (!resolvedMember) {
1231
- return /* @__PURE__ */ jsx(Stack, { gap: "md", p: "md", children: /* @__PURE__ */ jsx(Alert, { icon: /* @__PURE__ */ jsx(IconAlertCircle, { size: 16 }), color: "gray", variant: "light", children: "Member detail not found. The member may have been removed from this list." }) });
1232
- }
1233
- return /* @__PURE__ */ jsx(ListMemberDrawerContent, { member: resolvedMember, listId });
1234
- }
1235
- function ListMemberDrawer({ memberId, memberKind, listId, onClose }) {
1236
- const opened = !!memberId && !!memberKind;
1237
- return /* @__PURE__ */ jsxs(
1238
- Drawer,
1239
- {
1240
- opened,
1241
- onClose,
1242
- position: "right",
1243
- size: "lg",
1244
- title: "Member Detail",
1245
- styles: {
1246
- body: { padding: 0 }
1247
- },
1248
- children: [
1249
- opened && memberId && memberKind === "contact" && /* @__PURE__ */ jsx(ContactMemberDrawerShell, { contactId: memberId, listId }),
1250
- opened && memberId && memberKind === "company" && /* @__PURE__ */ jsx(CompanyMemberDrawerContent, { companyId: memberId })
1251
- ]
1252
- }
1253
- );
1254
- }
1255
- var LEAD_GEN_STAGE_CATALOG2 = getLeadGenStageCatalog(canonicalOrganizationModel);
1256
- function formatEventTime(timestamp) {
1257
- return new Date(timestamp).toLocaleTimeString("en-US", {
1258
- hour12: false,
1259
- hour: "2-digit",
1260
- minute: "2-digit",
1261
- second: "2-digit"
1262
- });
1263
- }
1264
- function formatId(executionId) {
1265
- return executionId.length > 10 ? `${executionId.slice(0, 8)}...` : executionId;
1266
- }
1267
- function getLogLevelColor(level) {
1268
- switch (level) {
1269
- case "error":
1270
- return "red";
1271
- case "warn":
1272
- return "yellow";
1273
- case "debug":
1274
- return "gray";
1275
- default:
1276
- return "blue";
1277
- }
1278
- }
1279
- function getStepStatusColor(status) {
1280
- switch (status) {
1281
- case "failed":
1282
- return "red";
1283
- case "done":
1284
- return "green";
1285
- default:
1286
- return "blue";
1287
- }
1288
- }
1289
- function getStepStatusLabel(status) {
1290
- switch (status) {
1291
- case "failed":
1292
- return "Failed";
1293
- case "done":
1294
- return "Done";
1295
- default:
1296
- return "Running";
1297
- }
1298
- }
1299
- function getEventColor(event) {
1300
- switch (event.type) {
1301
- case "new-execution":
1302
- return "blue";
1303
- case "execution-complete":
1304
- return event.data.success ? "green" : "red";
1305
- case "log":
1306
- return getLogLevelColor(event.data.log.level);
1307
- default:
1308
- return "gray";
1309
- }
1310
- }
1311
- function getEventLabel(event) {
1312
- switch (event.type) {
1313
- case "new-execution":
1314
- return "Execution started";
1315
- case "execution-complete":
1316
- return event.data.success ? "Execution completed" : "Execution failed";
1317
- case "log":
1318
- return event.data.log.message;
1319
- default:
1320
- return "Stream connected";
1321
- }
1322
- }
1323
- function hasExecutionId(event) {
1324
- return event.type !== "connected" && typeof event.executionId === "string" && event.executionId.length > 0;
1325
- }
1326
- function toTimelineEvents(events) {
1327
- return events.filter(hasExecutionId).map((event) => ({
1328
- executionId: event.executionId,
1329
- timestamp: event.timestamp,
1330
- label: getEventLabel(event),
1331
- color: getEventColor(event),
1332
- detail: event.type === "execution-complete" ? event.data.error : void 0
1333
- })).sort((a, b) => b.timestamp - a.timestamp).slice(0, 5);
1334
- }
1335
- function collectStepStatuses(events, streamingLogs) {
1336
- const statuses = /* @__PURE__ */ new Map();
1337
- const applyLog = (executionId, log) => {
1338
- const context = log.context;
1339
- if (!context || context.type !== "workflow") return;
1340
- if (!("stepId" in context)) return;
1341
- let status = null;
1342
- if (isStepStartedContext(context)) status = "running";
1343
- if (isStepCompletedContext(context)) status = "done";
1344
- if (isStepFailedContext(context)) status = "failed";
1345
- if (!status) return;
1346
- statuses.set(executionId, {
1347
- executionId,
1348
- stepId: context.stepId,
1349
- status,
1350
- timestamp: log.timestamp,
1351
- message: log.message
1352
- });
1353
- };
1354
- events.forEach((event) => {
1355
- if (event.type === "log") applyLog(event.executionId, event.data.log);
1356
- });
1357
- streamingLogs.forEach((logs, executionId) => {
1358
- logs.forEach((log) => applyLog(executionId, log));
1359
- });
1360
- return Array.from(statuses.values()).sort((a, b) => b.timestamp - a.timestamp);
1361
- }
1362
- function getExecutionId(execution) {
1363
- return "executionId" in execution ? execution.executionId : execution.id;
1364
- }
1365
- function getInFlightExecutions(data) {
1366
- if (Array.isArray(data)) return data;
1367
- if (data && typeof data === "object" && "executions" in data) {
1368
- const executions = data.executions;
1369
- return Array.isArray(executions) ? executions : [];
1370
- }
1371
- return [];
1372
- }
1373
- function getStageProgress(progress, stageKey) {
1374
- return progress?.byCompanyStage[stageKey] ?? progress?.byContactStage[stageKey];
1375
- }
1376
- function getStageLabel(stageKey) {
1377
- return LEAD_GEN_STAGE_CATALOG2[stageKey]?.label ?? stageKey;
1378
- }
1379
- function getDoneCount(stageProgress) {
1380
- return stageProgress?.attempted ?? 0;
1381
- }
1382
- function LeadGenLiveStatusPanel({
1383
- listId,
1384
- resourceId,
1385
- stageKey,
1386
- stepLabel,
1387
- enabled = true,
1388
- onRunningChange
1389
- }) {
1390
- const sse = useExecutionSSE(resourceId, {
1391
- enabled: enabled && Boolean(resourceId),
1392
- listId
1393
- });
1394
- const inFlightQuery = useInFlightExecutions(resourceId, {
1395
- enabled: enabled && Boolean(resourceId),
1396
- limit: 5,
1397
- refetchInterval: enabled && Boolean(resourceId) ? 2e3 : false
1398
- });
1399
- const inFlightExecutions = useMemo(() => getInFlightExecutions(inFlightQuery.data), [inFlightQuery.data]);
1400
- const activeExecutionIds = useMemo(() => {
1401
- const ids = new Set(sse.liveExecutions);
1402
- inFlightExecutions.forEach((execution) => {
1403
- const executionId = getExecutionId(execution);
1404
- if (executionId) ids.add(executionId);
1405
- });
1406
- return Array.from(ids);
1407
- }, [inFlightExecutions, sse.liveExecutions]);
1408
- const running = enabled && activeExecutionIds.length > 0;
1409
- const [showCompletedActivity, setShowCompletedActivity] = useState(false);
1410
- const progressQuery = useListProgress(listId, {
1411
- enabled: enabled && Boolean(listId),
1412
- refetchInterval: running ? 2e3 : false
1413
- });
1414
- useEffect(() => {
1415
- onRunningChange?.(running);
1416
- }, [onRunningChange, running]);
1417
- const stepStatuses = useMemo(
1418
- () => collectStepStatuses(sse.events, sse.streamingLogs),
1419
- [sse.events, sse.streamingLogs]
1420
- );
1421
- const latestStepStatus = stepStatuses[0];
1422
- const lastEvents = useMemo(() => toTimelineEvents(sse.events), [sse.events]);
1423
- const stageProgress = getStageProgress(progressQuery.data, stageKey);
1424
- const doneCount = getDoneCount(stageProgress);
1425
- const totalCount = stageProgress?.total ?? 0;
1426
- const failed = Boolean(sse.error) || sse.events.some((event) => event.type === "execution-complete" && !event.data.success);
1427
- const latestEvent = sse.latestEvent;
1428
- useEffect(() => {
1429
- if (running || failed || sse.error) {
1430
- setShowCompletedActivity(true);
1431
- return;
1432
- }
1433
- if (latestEvent?.type !== "execution-complete") return;
1434
- setShowCompletedActivity(true);
1435
- const timeoutId = window.setTimeout(() => setShowCompletedActivity(false), 1e4);
1436
- return () => window.clearTimeout(timeoutId);
1437
- }, [failed, latestEvent, running, sse.error]);
1438
- const hasActivity = activeExecutionIds.length > 0 || sse.streamingLogs.size > 0 || showCompletedActivity || Boolean(sse.error);
1439
- if (!enabled || !hasActivity && !sse.error && !failed) {
1440
- return null;
1441
- }
1442
- return /* @__PURE__ */ jsx(Paper, { withBorder: true, p: "sm", children: /* @__PURE__ */ jsxs(Stack, { gap: "xs", children: [
1443
- /* @__PURE__ */ jsx(
1444
- CardHeader,
1445
- {
1446
- icon: /* @__PURE__ */ jsx(IconTerminal2, { size: 16 }),
1447
- title: stepLabel ? `${stepLabel} status` : "Live status",
1448
- rightSection: /* @__PURE__ */ jsxs(Group, { gap: 6, children: [
1449
- /* @__PURE__ */ jsx(
1450
- Badge,
1451
- {
1452
- size: "xs",
1453
- variant: "light",
1454
- color: sse.connected ? "green" : running ? "yellow" : "gray",
1455
- leftSection: sse.connected ? /* @__PURE__ */ jsx(IconCircleCheck, { size: 9 }) : /* @__PURE__ */ jsx(IconCircleDot, { size: 9 }),
1456
- children: sse.connected ? "Connected" : running ? "Reconnecting" : "Idle"
1457
- }
1458
- ),
1459
- running ? /* @__PURE__ */ jsxs(Badge, { size: "xs", variant: "filled", color: "blue", leftSection: /* @__PURE__ */ jsx(IconPlayerPlay, { size: 9 }), children: [
1460
- activeExecutionIds.length,
1461
- " running"
1462
- ] }) : null
1463
- ] })
1464
- }
1465
- ),
1466
- sse.error ? /* @__PURE__ */ jsx(Alert, { icon: /* @__PURE__ */ jsx(IconAlertCircle, { size: 14 }), color: "red", variant: "light", p: "xs", children: /* @__PURE__ */ jsx(Text, { size: "xs", children: sse.error }) }) : null,
1467
- activeExecutionIds.length > 0 ? /* @__PURE__ */ jsx(Group, { gap: 6, children: activeExecutionIds.map((executionId) => /* @__PURE__ */ jsx(Badge, { size: "xs", variant: "light", color: "blue", ff: "monospace", children: formatId(executionId) }, executionId)) }) : null,
1468
- /* @__PURE__ */ jsxs(Box, { p: "xs", bg: "var(--surface-primary-subtle)", style: { borderRadius: "var(--mantine-radius-sm)" }, children: [
1469
- /* @__PURE__ */ jsxs(Group, { justify: "space-between", gap: "xs", wrap: "nowrap", children: [
1470
- /* @__PURE__ */ jsxs(Group, { gap: 6, wrap: "nowrap", style: { minWidth: 0 }, children: [
1471
- /* @__PURE__ */ jsx(IconProgressCheck, { size: 14 }),
1472
- /* @__PURE__ */ jsx(Text, { size: "xs", fw: 600, truncate: "end", children: getStageLabel(stageKey) })
1473
- ] }),
1474
- /* @__PURE__ */ jsxs(Text, { size: "xs", c: "dimmed", ff: "monospace", children: [
1475
- doneCount,
1476
- "/",
1477
- totalCount,
1478
- " done"
1479
- ] })
1480
- ] }),
1481
- /* @__PURE__ */ jsx(
1482
- Progress,
1483
- {
1484
- mt: 6,
1485
- size: "xs",
1486
- value: totalCount > 0 ? doneCount / totalCount * 100 : 0,
1487
- color: stageProgress && stageProgress.error > 0 ? "red" : "blue"
1488
- }
1489
- )
1490
- ] }),
1491
- latestStepStatus ? /* @__PURE__ */ jsxs(Group, { gap: 6, wrap: "nowrap", children: [
1492
- /* @__PURE__ */ jsx(Badge, { size: "xs", color: getStepStatusColor(latestStepStatus.status), variant: "light", children: getStepStatusLabel(latestStepStatus.status) }),
1493
- /* @__PURE__ */ jsx(Text, { size: "xs", fw: 500, truncate: "end", children: latestStepStatus.stepId }),
1494
- /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", truncate: "end", style: { flex: 1 }, children: latestStepStatus.message })
1495
- ] }) : running ? /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: "Waiting for workflow step events." }) : null,
1496
- lastEvents.length > 0 ? /* @__PURE__ */ jsx(Stack, { gap: 4, children: lastEvents.map((event) => /* @__PURE__ */ jsxs(Group, { gap: 6, wrap: "nowrap", children: [
1497
- /* @__PURE__ */ jsx(Badge, { size: "xs", variant: "dot", color: event.color, miw: 68, children: formatEventTime(event.timestamp) }),
1498
- /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", ff: "monospace", miw: 62, children: formatId(event.executionId) }),
1499
- /* @__PURE__ */ jsx(Text, { size: "xs", truncate: "end", style: { flex: 1 }, children: event.detail ?? event.label })
1500
- ] }, `${event.executionId}-${event.timestamp}-${event.label}`)) }) : null
1501
- ] }) });
1502
- }
1503
- var panelStyle = {
1504
- flex: 1,
1505
- minHeight: 0,
1506
- overflowY: "auto",
1507
- paddingTop: "var(--mantine-spacing-sm)"
1508
- };
1509
- function StepDetailRightColumn({
1510
- configuration,
1511
- records,
1512
- advanced,
1513
- runs,
1514
- action,
1515
- activeTab,
1516
- onTabChange
1517
- }) {
1518
- return /* @__PURE__ */ jsxs(
1519
- Stack,
1520
- {
1521
- gap: "sm",
1522
- style: {
1523
- flex: 1,
1524
- minHeight: 0
1525
- },
1526
- children: [
1527
- /* @__PURE__ */ jsxs(
1528
- Tabs,
1529
- {
1530
- value: activeTab,
1531
- onChange: (value) => {
1532
- if (value === "configuration" || value === "records" || value === "advanced" || value === "runs") {
1533
- onTabChange(value);
1534
- }
1535
- },
1536
- style: {
1537
- flex: 1,
1538
- minHeight: 0,
1539
- display: "flex",
1540
- flexDirection: "column"
1541
- },
1542
- children: [
1543
- /* @__PURE__ */ jsxs(Tabs.List, { children: [
1544
- /* @__PURE__ */ jsx(Tabs.Tab, { value: "configuration", children: "Configuration" }),
1545
- /* @__PURE__ */ jsx(Tabs.Tab, { value: "records", children: "Records" }),
1546
- /* @__PURE__ */ jsx(Tabs.Tab, { value: "advanced", children: "Advanced" }),
1547
- /* @__PURE__ */ jsx(Tabs.Tab, { value: "runs", children: "Runs" })
1548
- ] }),
1549
- /* @__PURE__ */ jsx(Tabs.Panel, { value: "configuration", style: panelStyle, children: configuration }),
1550
- /* @__PURE__ */ jsx(Tabs.Panel, { value: "records", style: panelStyle, children: records ?? /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: "No records view configured for this step." }) }),
1551
- /* @__PURE__ */ jsx(Tabs.Panel, { value: "advanced", style: panelStyle, children: advanced ?? /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: "No advanced settings for this step." }) }),
1552
- /* @__PURE__ */ jsx(Tabs.Panel, { value: "runs", style: panelStyle, children: runs })
1553
- ]
1554
- }
1555
- ),
1556
- /* @__PURE__ */ jsx(Stack, { gap: "xs", style: { flexShrink: 0 }, children: action })
1557
- ]
1558
- }
1559
- );
1560
- }
1561
- function ListBuilderIndexPage() {
1562
- const navigate = useNavigate();
1563
- const [query, setQuery] = useState("");
1564
- const listsQuery = useLists();
1565
- const lists = listsQuery.data ?? [];
1566
- const filteredLists = useMemo(() => {
1567
- const normalized = query.trim().toLowerCase();
1568
- if (!normalized) return lists;
1569
- return lists.filter((list) => list.name.toLowerCase().includes(normalized));
1570
- }, [lists, query]);
1571
- const openList = (listId) => {
1572
- navigate({ to: "/lead-gen/list-builder/$listId", params: { listId } });
1573
- };
1574
- return /* @__PURE__ */ jsx(SubshellContentContainer, { children: /* @__PURE__ */ jsx(PageContainer, { children: /* @__PURE__ */ jsxs(Stack, { children: [
1575
- /* @__PURE__ */ jsx(PageTitleCaption, { title: "List Builder", caption: "Choose a list to run lead-gen workflows and monitor progress." }),
1576
- /* @__PURE__ */ jsx(Paper, { withBorder: true, p: "md", children: /* @__PURE__ */ jsxs(Stack, { gap: "md", children: [
1577
- /* @__PURE__ */ jsx(
1578
- TextInput,
1579
- {
1580
- placeholder: "Search lists...",
1581
- leftSection: /* @__PURE__ */ jsx(IconSearch, { size: 16 }),
1582
- value: query,
1583
- onChange: (event) => setQuery(event.currentTarget.value)
1584
- }
1585
- ),
1586
- listsQuery.isLoading ? /* @__PURE__ */ jsx(Center, { p: "xl", children: /* @__PURE__ */ jsx(Loader, {}) }) : !filteredLists.length ? /* @__PURE__ */ jsx(
1587
- EmptyState,
1588
- {
1589
- icon: IconLayoutDashboard,
1590
- title: query.trim() ? "No lists match your search" : "No lists available",
1591
- description: query.trim() ? void 0 : "Create a list first, then open it in the builder."
1592
- }
1593
- ) : /* @__PURE__ */ jsxs(Table, { highlightOnHover: true, children: [
1594
- /* @__PURE__ */ jsx(Table.Thead, { children: /* @__PURE__ */ jsxs(Table.Tr, { children: [
1595
- /* @__PURE__ */ jsx(Table.Th, { children: "Name" }),
1596
- /* @__PURE__ */ jsx(Table.Th, { children: "Status" }),
1597
- /* @__PURE__ */ jsx(Table.Th, { children: "Batches" }),
1598
- /* @__PURE__ */ jsx(Table.Th, { children: "Created" })
1599
- ] }) }),
1600
- /* @__PURE__ */ jsx(Table.Tbody, { children: filteredLists.map((list) => /* @__PURE__ */ jsxs(
1601
- Table.Tr,
1602
- {
1603
- role: "button",
1604
- tabIndex: 0,
1605
- onClick: () => openList(list.id),
1606
- onKeyDown: (event) => {
1607
- if (event.key === "Enter" || event.key === " ") {
1608
- event.preventDefault();
1609
- openList(list.id);
1610
- }
1611
- },
1612
- style: { cursor: "pointer" },
1613
- children: [
1614
- /* @__PURE__ */ jsxs(Table.Td, { children: [
1615
- /* @__PURE__ */ jsx(Text, { fw: 500, children: list.name }),
1616
- /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: list.description ?? list.id })
1617
- ] }),
1618
- /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "light", color: getStateKeyColor(list.status), children: list.status }) }),
1619
- /* @__PURE__ */ jsx(Table.Td, { children: list.batchIds.length }),
1620
- /* @__PURE__ */ jsx(Table.Td, { children: formatDate(list.createdAt) })
1621
- ]
1622
- },
1623
- list.id
1624
- )) })
1625
- ] })
1626
- ] }) })
1627
- ] }) }) });
1628
- }
1629
-
1630
- // src/lib/lead-gen/stage-colors.ts
1631
- var FALLBACK_STAGE_COLORS = {
1632
- accent: "var(--color-text-dimmed)",
1633
- background: "color-mix(in srgb, var(--color-text-dimmed) 10%, transparent)",
1634
- border: "color-mix(in srgb, var(--color-border) 85%, var(--color-text-dimmed))",
1635
- text: "var(--color-text)"
1636
- };
1637
- var STAGE_COLOR_SETS = {
1638
- scraped: {
1639
- accent: "var(--color-text-subtle)",
1640
- background: "color-mix(in srgb, var(--color-text-subtle) 12%, transparent)",
1641
- border: "color-mix(in srgb, var(--color-border) 80%, var(--color-text-subtle))",
1642
- text: "var(--color-text)"
1643
- },
1644
- populated: {
1645
- accent: "var(--color-primary)",
1646
- background: "color-mix(in srgb, var(--color-primary) 12%, transparent)",
1647
- border: "color-mix(in srgb, var(--color-border) 70%, var(--color-primary))",
1648
- text: "var(--color-text)"
1649
- },
1650
- crawled: {
1651
- accent: "color-mix(in srgb, var(--color-primary) 70%, var(--color-text-subtle))",
1652
- background: "color-mix(in srgb, var(--color-primary) 9%, transparent)",
1653
- border: "color-mix(in srgb, var(--color-border) 72%, var(--color-primary))",
1654
- text: "var(--color-text)"
1655
- },
1656
- extracted: {
1657
- accent: "color-mix(in srgb, var(--color-primary) 78%, var(--color-success))",
1658
- background: "color-mix(in srgb, var(--color-primary) 10%, transparent)",
1659
- border: "color-mix(in srgb, var(--color-border) 65%, var(--color-primary))",
1660
- text: "var(--color-text)"
1661
- },
1662
- enriched: {
1663
- accent: "color-mix(in srgb, var(--color-success) 70%, var(--color-primary))",
1664
- background: "color-mix(in srgb, var(--color-success) 12%, transparent)",
1665
- border: "color-mix(in srgb, var(--color-border) 68%, var(--color-success))",
1666
- text: "var(--color-text)"
1667
- },
1668
- "decision-makers-enriched": {
1669
- accent: "color-mix(in srgb, var(--color-primary) 66%, var(--color-warning))",
1670
- background: "color-mix(in srgb, var(--color-primary) 12%, transparent)",
1671
- border: "color-mix(in srgb, var(--color-border) 66%, var(--color-primary))",
1672
- text: "var(--color-text)"
1673
- },
1674
- discovered: {
1675
- accent: "color-mix(in srgb, var(--color-primary) 70%, var(--color-warning))",
1676
- background: "color-mix(in srgb, var(--color-warning) 10%, transparent)",
1677
- border: "color-mix(in srgb, var(--color-border) 68%, var(--color-warning))",
1678
- text: "var(--color-text)"
1679
- },
1680
- verified: {
1681
- accent: "var(--color-success)",
1682
- background: "color-mix(in srgb, var(--color-success) 12%, transparent)",
1683
- border: "color-mix(in srgb, var(--color-border) 64%, var(--color-success))",
1684
- text: "var(--color-text)"
1685
- },
1686
- qualified: {
1687
- accent: "color-mix(in srgb, var(--color-success) 82%, var(--color-text))",
1688
- background: "color-mix(in srgb, var(--color-success) 14%, transparent)",
1689
- border: "color-mix(in srgb, var(--color-border) 62%, var(--color-success))",
1690
- text: "var(--color-text)"
1691
- },
1692
- personalized: {
1693
- accent: "color-mix(in srgb, var(--color-primary) 58%, var(--color-success))",
1694
- background: "color-mix(in srgb, var(--color-primary) 11%, transparent)",
1695
- border: "color-mix(in srgb, var(--color-border) 66%, var(--color-primary))",
1696
- text: "var(--color-text)"
1697
- },
1698
- uploaded: {
1699
- accent: "color-mix(in srgb, var(--color-warning) 72%, var(--color-primary))",
1700
- background: "color-mix(in srgb, var(--color-warning) 12%, transparent)",
1701
- border: "color-mix(in srgb, var(--color-border) 64%, var(--color-warning))",
1702
- text: "var(--color-text)"
1703
- },
1704
- interested: {
1705
- accent: "color-mix(in srgb, var(--color-success) 72%, var(--color-warning))",
1706
- background: "color-mix(in srgb, var(--color-success) 15%, transparent)",
1707
- border: "color-mix(in srgb, var(--color-border) 58%, var(--color-success))",
1708
- text: "var(--color-text)"
1709
- },
1710
- disqualified: {
1711
- accent: "var(--color-error)",
1712
- background: "color-mix(in srgb, var(--color-error) 10%, transparent)",
1713
- border: "color-mix(in srgb, var(--color-border) 66%, var(--color-error))",
1714
- text: "var(--color-text)"
1715
- },
1716
- invalid: {
1717
- accent: "color-mix(in srgb, var(--color-error) 72%, var(--color-text-dimmed))",
1718
- background: "color-mix(in srgb, var(--color-error) 8%, transparent)",
1719
- border: "color-mix(in srgb, var(--color-border) 78%, var(--color-error))",
1720
- text: "var(--color-text)"
1721
- }
1722
- };
1723
- function getLeadGenStageColor(stageKey) {
1724
- return STAGE_COLOR_SETS[stageKey] ?? FALLBACK_STAGE_COLORS;
1725
- }
1726
- function getLeadGenStageColorVar(stageKey, tone = "accent") {
1727
- return getLeadGenStageColor(stageKey)[tone];
1728
- }
1729
- var FALLBACK_STAGE_ORDER = 1e4;
1730
- var LEAD_GEN_STAGE_CATALOG3 = getLeadGenStageCatalog(canonicalOrganizationModel);
1731
- function unique(values) {
1732
- return Array.from(new Set(values.filter(Boolean)));
1733
- }
1734
- function configuredStages(pipelineConfig) {
1735
- return (pipelineConfig?.stages ?? []).filter((stage) => stage.enabled !== false).slice().sort((a, b) => {
1736
- const aOrder = a.order ?? LEAD_GEN_STAGE_CATALOG3[a.key]?.order ?? FALLBACK_STAGE_ORDER;
1737
- const bOrder = b.order ?? LEAD_GEN_STAGE_CATALOG3[b.key]?.order ?? FALLBACK_STAGE_ORDER;
1738
- return aOrder - bOrder || a.key.localeCompare(b.key);
1739
- });
1740
- }
1741
- function workflowStageKeys(actions) {
1742
- return unique((actions ?? []).flatMap((action) => [...action.stagesAffected]));
1743
- }
1744
- function sourceFor(stageKey, observedKeys, configKeys, workflowKeys) {
1745
- if (observedKeys.has(stageKey)) return "activity";
1746
- if (configKeys.has(stageKey)) return "config";
1747
- if (workflowKeys.has(stageKey)) return "workflow";
1748
- return "activity";
1749
- }
1750
- function sortStageNodes(nodes) {
1751
- return nodes.slice().sort((a, b) => a.order - b.order || a.key.localeCompare(b.key));
1752
- }
1753
- function buildLaneStages({
1754
- entity,
1755
- progress,
1756
- pipelineConfig,
1757
- actions
1758
- }) {
1759
- const stageMap = entity === "company" ? progress.byCompanyStage : progress.byContactStage;
1760
- const observedKeys = new Set(Object.keys(stageMap));
1761
- const configStages = configuredStages(pipelineConfig);
1762
- const configStageByKey = new Map(configStages.map((stage) => [stage.key, stage]));
1763
- const configKeys = new Set(configStages.map((stage) => stage.key));
1764
- const workflowKeys = new Set(workflowStageKeys(actions));
1765
- const stageKeys = unique([...observedKeys, ...configKeys, ...workflowKeys]);
1766
- return sortStageNodes(
1767
- stageKeys.flatMap((key) => {
1768
- const catalogEntry = LEAD_GEN_STAGE_CATALOG3[key];
1769
- const inferredEntity = catalogEntry?.entity ?? (observedKeys.has(key) ? entity : null);
1770
- if (inferredEntity !== entity) return [];
1771
- return [
1772
- {
1773
- key,
1774
- label: configStageByKey.get(key)?.label ?? catalogEntry?.label ?? key,
1775
- order: configStageByKey.get(key)?.order ?? catalogEntry?.order ?? FALLBACK_STAGE_ORDER,
1776
- entity,
1777
- source: sourceFor(key, observedKeys, configKeys, workflowKeys)
1778
- }
1779
- ];
1780
- })
1781
- );
1782
- }
1783
- function getStageProgress2(progress, stageKey, entity) {
1784
- const stageProgress = entity === "company" ? progress.byCompanyStage[stageKey] : progress.byContactStage[stageKey];
1785
- const total = stageProgress?.total ?? (entity === "company" ? progress.totalCompanies : progress.totalMembers);
1786
- return {
1787
- total,
1788
- attempted: stageProgress?.attempted ?? 0,
1789
- success: stageProgress?.success ?? 0,
1790
- noResult: stageProgress?.noResult ?? 0,
1791
- skipped: stageProgress?.skipped ?? 0,
1792
- error: stageProgress?.error ?? 0,
1793
- other: stageProgress?.other ?? 0,
1794
- notAttempted: stageProgress?.notAttempted ?? total
1795
- };
1796
- }
1797
- function percent(value, total) {
1798
- if (total <= 0) return 0;
1799
- return Math.max(0, Math.min(100, value / total * 100));
1800
- }
1801
- function SourceBadge({ source }) {
1802
- const label = source === "activity" ? "recorded" : source;
1803
- const color = source === "activity" ? "green" : source === "config" ? "blue" : "gray";
1804
- return /* @__PURE__ */ jsx(Badge, { size: "xs", variant: "light", color, children: label });
1805
- }
1806
- function ProgressRail({ counts, stageKey }) {
1807
- const accent = getLeadGenStageColorVar(stageKey);
1808
- const segments = [
1809
- { key: "success", value: counts.success, color: accent },
1810
- { key: "noResult", value: counts.noResult, color: "var(--color-text-subtle)" },
1811
- { key: "skipped", value: counts.skipped, color: "var(--color-warning)" },
1812
- { key: "error", value: counts.error, color: "var(--color-error)" },
1813
- { key: "other", value: counts.other, color: "var(--color-primary)" }
1814
- ].filter((segment) => segment.value > 0);
1815
- return /* @__PURE__ */ jsx(
1816
- Box,
1817
- {
1818
- h: 6,
1819
- bg: "var(--color-surface-hover)",
1820
- style: {
1821
- borderRadius: 999,
1822
- display: "flex",
1823
- overflow: "hidden",
1824
- opacity: counts.total > 0 ? 1 : 0.55
1825
- },
1826
- children: segments.length > 0 ? segments.map((segment) => /* @__PURE__ */ jsx(Box, { h: "100%", w: `${percent(segment.value, counts.total)}%`, bg: segment.color }, segment.key)) : /* @__PURE__ */ jsx(Box, { h: "100%", w: counts.total > 0 ? `${percent(counts.attempted, counts.total)}%` : "0%", bg: accent })
1827
- }
1828
- );
1829
- }
1830
- function TimelineLane({
1831
- title,
1832
- entity,
1833
- total,
1834
- stages,
1835
- progress,
1836
- emptyText
1837
- }) {
1838
- const Icon = entity === "company" ? IconBuilding : IconUsers;
1839
- return /* @__PURE__ */ jsxs(Stack, { gap: "sm", children: [
1840
- /* @__PURE__ */ jsxs(Group, { justify: "space-between", align: "center", gap: "xs", children: [
1841
- /* @__PURE__ */ jsxs(Group, { gap: "xs", children: [
1842
- /* @__PURE__ */ jsx(
1843
- Box,
1844
- {
1845
- w: 28,
1846
- h: 28,
1847
- style: {
1848
- alignItems: "center",
1849
- background: "color-mix(in srgb, var(--color-primary) 14%, transparent)",
1850
- border: "1px solid color-mix(in srgb, var(--color-border) 75%, var(--color-primary))",
1851
- borderRadius: 999,
1852
- color: "var(--color-primary)",
1853
- display: "flex",
1854
- justifyContent: "center"
1855
- },
1856
- children: /* @__PURE__ */ jsx(Icon, { size: 15 })
1857
- }
1858
- ),
1859
- /* @__PURE__ */ jsxs(Stack, { gap: 0, children: [
1860
- /* @__PURE__ */ jsx(Text, { size: "sm", fw: 700, children: title }),
1861
- /* @__PURE__ */ jsxs(Text, { size: "xs", c: "dimmed", children: [
1862
- total,
1863
- " ",
1864
- entity === "company" ? "companies" : "contacts"
1865
- ] })
1866
- ] })
1867
- ] }),
1868
- /* @__PURE__ */ jsxs(Badge, { size: "sm", variant: "outline", color: entity === "company" ? "blue" : "teal", children: [
1869
- stages.length,
1870
- " stages"
1871
- ] })
1872
- ] }),
1873
- stages.length === 0 ? /* @__PURE__ */ jsx(
1874
- Box,
1875
- {
1876
- p: "sm",
1877
- style: {
1878
- border: "1px dashed var(--color-border)",
1879
- borderRadius: "var(--mantine-radius-md)"
1880
- },
1881
- children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: emptyText })
1882
- }
1883
- ) : /* @__PURE__ */ jsx(ScrollArea, { type: "hover", offsetScrollbars: true, children: /* @__PURE__ */ jsx(
1884
- Box,
1885
- {
1886
- style: {
1887
- minWidth: Math.max(560, stages.length * 178),
1888
- padding: "8px 2px 2px"
1889
- },
1890
- children: /* @__PURE__ */ jsx(
1891
- Box,
1892
- {
1893
- style: {
1894
- alignItems: "start",
1895
- display: "grid",
1896
- gap: 0,
1897
- gridTemplateColumns: `repeat(${stages.length}, minmax(150px, 1fr))`,
1898
- position: "relative"
1899
- },
1900
- children: stages.map((stage, index) => {
1901
- const counts = getStageProgress2(progress, stage.key, entity);
1902
- const accent = getLeadGenStageColorVar(stage.key);
1903
- const completion = Math.round(percent(counts.success, counts.total));
1904
- const attempted = Math.round(percent(counts.attempted, counts.total));
1905
- return /* @__PURE__ */ jsxs(Stack, { gap: 8, px: "xs", style: { position: "relative", zIndex: 1 }, children: [
1906
- /* @__PURE__ */ jsxs(Group, { gap: 8, wrap: "nowrap", children: [
1907
- /* @__PURE__ */ jsx(
1908
- Box,
1909
- {
1910
- w: 44,
1911
- h: 44,
1912
- style: {
1913
- alignItems: "center",
1914
- background: getLeadGenStageColorVar(stage.key, "background"),
1915
- border: `1px solid ${getLeadGenStageColorVar(stage.key, "border")}`,
1916
- borderRadius: 999,
1917
- boxShadow: `0 0 0 4px ${getLeadGenStageColorVar(stage.key, "background")}`,
1918
- color: accent,
1919
- display: "flex",
1920
- flexShrink: 0,
1921
- fontSize: 13,
1922
- fontWeight: 800,
1923
- justifyContent: "center"
1924
- },
1925
- children: index + 1
1926
- }
1927
- ),
1928
- /* @__PURE__ */ jsxs(Stack, { gap: 1, style: { minWidth: 0 }, children: [
1929
- /* @__PURE__ */ jsxs(Group, { gap: 6, wrap: "nowrap", children: [
1930
- /* @__PURE__ */ jsx(Text, { size: "sm", fw: 700, lineClamp: 1, children: stage.label }),
1931
- /* @__PURE__ */ jsx(SourceBadge, { source: stage.source })
1932
- ] }),
1933
- /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", lineClamp: 1, children: counts.total > 0 ? `${counts.attempted} / ${counts.total} attempted` : `Ready for ${entity === "company" ? "company" : "contact"} rows` })
1934
- ] })
1935
- ] }),
1936
- /* @__PURE__ */ jsx(ProgressRail, { counts, stageKey: stage.key }),
1937
- /* @__PURE__ */ jsxs(Group, { gap: 10, wrap: "nowrap", children: [
1938
- /* @__PURE__ */ jsxs(Text, { size: "xs", c: "dimmed", children: [
1939
- counts.success,
1940
- " success"
1941
- ] }),
1942
- /* @__PURE__ */ jsxs(Text, { size: "xs", c: "dimmed", children: [
1943
- attempted,
1944
- "% attempted"
1945
- ] }),
1946
- counts.error > 0 ? /* @__PURE__ */ jsxs(Text, { size: "xs", c: "red", children: [
1947
- counts.error,
1948
- " errors"
1949
- ] }) : null
1950
- ] }),
1951
- /* @__PURE__ */ jsxs(Text, { size: "xs", c: "dimmed", children: [
1952
- completion,
1953
- "% complete"
1954
- ] })
1955
- ] }, stage.key);
1956
- })
1957
- }
1958
- )
1959
- }
1960
- ) })
1961
- ] });
1962
- }
1963
- function PipelineFunnel({ progress, pipelineConfig, actions }) {
1964
- const companyStages = buildLaneStages({ entity: "company", progress, pipelineConfig, actions });
1965
- const contactStages = buildLaneStages({ entity: "contact", progress, pipelineConfig, actions });
1966
- return /* @__PURE__ */ jsx(Paper, { withBorder: true, p: "md", children: /* @__PURE__ */ jsxs(Stack, { gap: "lg", children: [
1967
- /* @__PURE__ */ jsx(CardHeader, { icon: /* @__PURE__ */ jsx(IconListDetails, { size: 16 }), title: "Pipeline" }),
1968
- /* @__PURE__ */ jsx(
1969
- TimelineLane,
1970
- {
1971
- title: "Company pipeline",
1972
- entity: "company",
1973
- total: progress.totalCompanies,
1974
- stages: companyStages,
1975
- progress,
1976
- emptyText: "No company stages are configured, runnable, or recorded yet."
1977
- }
1978
- ),
1979
- /* @__PURE__ */ jsx(
1980
- TimelineLane,
1981
- {
1982
- title: "Contact pipeline",
1983
- entity: "contact",
1984
- total: progress.totalMembers,
1985
- stages: contactStages,
1986
- progress,
1987
- emptyText: "No contact stages are configured, runnable, or recorded yet."
1988
- }
1989
- )
1990
- ] }) });
1991
- }
1992
- function asRecord(value) {
1993
- return value && typeof value === "object" && !Array.isArray(value) ? value : {};
1994
- }
1995
- function selectedCount(selection) {
1996
- return selection.selectedCompanyIds.length + selection.selectedContactIds.length;
1997
- }
1998
- function buildInput(input, selection) {
1999
- const base = input && typeof input === "object" && !Array.isArray(input) ? input : {};
2000
- return {
2001
- ...base,
2002
- selectedCompanyIds: selection.selectedCompanyIds,
2003
- selectedContactIds: selection.selectedContactIds
2004
- };
2005
- }
2006
- function RunWorkflowModal({
2007
- opened,
2008
- onClose,
2009
- list,
2010
- actions,
2011
- selection,
2012
- initialResourceId,
2013
- title = "Run Workflow",
2014
- description = "Start a workflow immediately for this list.",
2015
- lockResourceSelection = false,
2016
- onResourceChange,
2017
- onSubmitted
2018
- }) {
2019
- const [selectedResourceId, setSelectedResourceId] = useState(
2020
- initialResourceId ?? actions[0]?.resourceId ?? null
2021
- );
2022
- const selectedAction = useMemo(
2023
- () => actions.find((action) => action.resourceId === selectedResourceId) ?? actions[0],
2024
- [actions, selectedResourceId]
2025
- );
2026
- const execution = useWorkflowExecution({
2027
- workflowId: selectedAction?.resourceId ?? "",
2028
- listId: list.id
2029
- });
2030
- useEffect(() => {
2031
- if (!opened) return;
2032
- const nextResourceId = initialResourceId ?? actions[0]?.resourceId ?? null;
2033
- setSelectedResourceId(nextResourceId);
2034
- onResourceChange?.(nextResourceId);
2035
- }, [actions, initialResourceId, onResourceChange, opened]);
2036
- const actionOptions = actions.map((action) => ({
2037
- value: action.resourceId,
2038
- label: action.label
2039
- }));
2040
- const handleResourceChange = (value) => {
2041
- setSelectedResourceId(value);
2042
- onResourceChange?.(value);
2043
- execution.reset();
2044
- };
2045
- const submitInput = async (input) => {
2046
- if (!selectedAction) return;
2047
- try {
2048
- const result = await execution.execute({
2049
- input: buildInput(input, selection)
2050
- });
2051
- onSubmitted?.(selectedAction.resourceId, result.executionId);
2052
- onClose();
2053
- } catch (error) {
2054
- showApiErrorNotification(error);
2055
- }
2056
- };
2057
- const count = selectedCount(selection);
2058
- return /* @__PURE__ */ jsx(
2059
- CustomModal,
2060
- {
2061
- opened,
2062
- onClose: () => !execution.isPending && onClose(),
2063
- size: "lg",
2064
- loading: execution.isPending,
2065
- children: /* @__PURE__ */ jsxs(Stack, { gap: "md", children: [
2066
- /* @__PURE__ */ jsxs(Group, { gap: "sm", children: [
2067
- /* @__PURE__ */ jsx(IconBolt, { size: 22 }),
2068
- /* @__PURE__ */ jsxs("div", { children: [
2069
- /* @__PURE__ */ jsx(Text, { fw: 600, children: title }),
2070
- /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: description })
2071
- ] })
2072
- ] }),
2073
- !actions.length ? /* @__PURE__ */ jsx(Alert, { icon: /* @__PURE__ */ jsx(IconAlertCircle, { size: 16 }), color: "gray", variant: "light", children: "No list builder workflows are registered." }) : /* @__PURE__ */ jsxs(Fragment, { children: [
2074
- /* @__PURE__ */ jsx(
2075
- Select,
2076
- {
2077
- label: "Workflow",
2078
- data: actionOptions,
2079
- value: selectedAction?.resourceId ?? null,
2080
- onChange: handleResourceChange,
2081
- disabled: execution.isPending || lockResourceSelection,
2082
- searchable: true
2083
- }
2084
- ),
2085
- selectedAction ? /* @__PURE__ */ jsxs(Stack, { gap: 6, children: [
2086
- /* @__PURE__ */ jsxs(Group, { gap: "xs", children: [
2087
- /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "light", children: selectedAction.category }),
2088
- selectedAction.stagesAffected.map((stage) => /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "outline", color: "gray", children: stage }, stage))
2089
- ] }),
2090
- /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: selectedAction.description })
2091
- ] }) : null,
2092
- count > 0 ? /* @__PURE__ */ jsxs(Alert, { color: "blue", variant: "light", children: [
2093
- "This run includes ",
2094
- selection.selectedCompanyIds.length,
2095
- " selected companies and",
2096
- " ",
2097
- selection.selectedContactIds.length,
2098
- " selected contacts."
2099
- ] }) : null,
2100
- selectedAction ? /* @__PURE__ */ jsx(
2101
- RunWorkflowConfigForm,
2102
- {
2103
- action: selectedAction,
2104
- list,
2105
- onSubmit: submitInput,
2106
- isSubmitting: execution.isPending
2107
- },
2108
- selectedAction.resourceId
2109
- ) : null
2110
- ] })
2111
- ] })
2112
- }
2113
- );
2114
- }
2115
- function RunWorkflowConfigForm({ action, list, onSubmit, isSubmitting }) {
2116
- const schema = action.schema;
2117
- const layout = action.layout;
2118
- const [config, setConfig] = useState(() => asRecord(action.defaultInput?.(list)));
2119
- const handleChange = (next) => {
2120
- setConfig(next);
2121
- };
2122
- const handleSubmit = async () => {
2123
- await onSubmit(config);
2124
- };
2125
- return /* @__PURE__ */ jsx(Stack, { gap: "md", children: /* @__PURE__ */ jsx(
2126
- StepConfigForm,
2127
- {
2128
- schema,
2129
- layout,
2130
- value: config,
2131
- onChange: (v) => handleChange(asRecord(v)),
2132
- actions: [{ id: "run", label: `Run ${action.label}`, loading: isSubmitting }],
2133
- onAction: async () => {
2134
- await handleSubmit();
2135
- },
2136
- disabled: isSubmitting
2137
- }
2138
- ) });
2139
- }
2140
- function formatDateTime2(value) {
2141
- if (!value) return "Not yet";
2142
- return new Date(value).toLocaleString("en-US", {
2143
- month: "short",
2144
- day: "numeric",
2145
- year: "numeric",
2146
- hour: "numeric",
2147
- minute: "2-digit"
2148
- });
2149
- }
2150
- function formatDuration(durationMs) {
2151
- if (durationMs == null) return "n/a";
2152
- if (durationMs < 1e3) return `${durationMs} ms`;
2153
- const seconds = durationMs / 1e3;
2154
- if (seconds < 60) return `${seconds.toFixed(1)} s`;
2155
- return `${Math.floor(seconds / 60)}m ${Math.round(seconds % 60)}s`;
2156
- }
2157
- function getStatusColor2(status) {
2158
- switch (status) {
2159
- case "completed":
2160
- case "success":
2161
- case "succeeded":
2162
- return "green";
2163
- case "running":
2164
- case "pending":
2165
- case "queued":
2166
- return "blue";
2167
- case "failed":
2168
- case "error":
2169
- case "cancelled":
2170
- return "red";
2171
- default:
2172
- return "gray";
2173
- }
2174
- }
2175
- function hasObjectContent(value) {
2176
- return typeof value === "object" && value !== null && Object.keys(value).length > 0;
2177
- }
2178
- function getRunDetails(run) {
2179
- const payload = hasObjectContent(run.payload) ? run.payload : void 0;
2180
- const input = run.input ?? run.inputs ?? payload?.input ?? payload?.inputs;
2181
- const config = run.configSnapshot ?? run.config_snapshot ?? run.config ?? payload?.config_snapshot ?? payload?.configSnapshot ?? payload?.config;
2182
- return { input, config };
2183
- }
2184
- function getSummaryKeys(value) {
2185
- if (!hasObjectContent(value)) return [];
2186
- return Object.keys(value).slice(0, 6);
2187
- }
2188
- function WorkflowRunsPanel({
2189
- listId,
2190
- title = "Workflow Runs",
2191
- resourceFilter,
2192
- onClearResourceFilter
2193
- }) {
2194
- const navigate = useNavigate();
2195
- const executionsQuery = useListExecutions(listId, resourceFilter ? { resourceId: resourceFilter } : {});
2196
- const [expandedExecutionId, setExpandedExecutionId] = useState(null);
2197
- const executions = useMemo(() => executionsQuery.data ?? [], [executionsQuery.data]);
2198
- const openWorkflow = (resourceId) => {
2199
- void navigate({ to: "/operations/resources/workflow/$workflowId", params: { workflowId: resourceId } });
2200
- };
2201
- return /* @__PURE__ */ jsxs(
2202
- TabSection,
2203
- {
2204
- icon: /* @__PURE__ */ jsx(IconPlayerPlay, { size: 16 }),
2205
- title,
2206
- description: "Execution history recorded for this lead-gen list.",
2207
- rightSection: /* @__PURE__ */ jsx(Tooltip, { label: "Refresh runs", children: /* @__PURE__ */ jsx(
2208
- ActionIcon,
2209
- {
2210
- variant: "subtle",
2211
- size: "sm",
2212
- "aria-label": "Refresh workflow runs",
2213
- loading: executionsQuery.isFetching,
2214
- onClick: () => void executionsQuery.refetch(),
2215
- children: /* @__PURE__ */ jsx(IconRefresh, { size: 14 })
2216
- }
2217
- ) }),
2218
- children: [
2219
- resourceFilter ? /* @__PURE__ */ jsxs(Group, { gap: "xs", mb: "xs", children: [
2220
- /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: "Filtered by workflow:" }),
2221
- /* @__PURE__ */ jsx(Pill, { withRemoveButton: true, onRemove: onClearResourceFilter, children: resourceFilter })
2222
- ] }) : null,
2223
- executionsQuery.isLoading ? /* @__PURE__ */ jsx(Center, { p: "lg", children: /* @__PURE__ */ jsx(Loader, { size: "sm" }) }) : !executions.length ? /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: resourceFilter ? `No workflow runs recorded for ${resourceFilter}.` : "No workflow runs recorded for this list." }) : /* @__PURE__ */ jsx(Table.ScrollContainer, { minWidth: 720, children: /* @__PURE__ */ jsxs(Table, { highlightOnHover: true, children: [
2224
- /* @__PURE__ */ jsx(Table.Thead, { children: /* @__PURE__ */ jsxs(Table.Tr, { children: [
2225
- /* @__PURE__ */ jsx(Table.Th, {}),
2226
- /* @__PURE__ */ jsx(Table.Th, { children: "Workflow" }),
2227
- /* @__PURE__ */ jsx(Table.Th, { children: "Status" }),
2228
- /* @__PURE__ */ jsx(Table.Th, { children: "Started" }),
2229
- /* @__PURE__ */ jsx(Table.Th, { children: "Completed" }),
2230
- /* @__PURE__ */ jsx(Table.Th, { children: "Duration" })
2231
- ] }) }),
2232
- /* @__PURE__ */ jsx(Table.Tbody, { children: executions.map((execution) => {
2233
- const details = getRunDetails(execution);
2234
- const hasDetails = details.input !== void 0 || details.config !== void 0;
2235
- const expanded = expandedExecutionId === execution.executionId;
2236
- return /* @__PURE__ */ jsxs(Fragment$1, { children: [
2237
- /* @__PURE__ */ jsxs(Table.Tr, { children: [
2238
- /* @__PURE__ */ jsx(Table.Td, { w: 32, children: /* @__PURE__ */ jsx(
2239
- ThemeIcon,
2240
- {
2241
- variant: "light",
2242
- color: "blue",
2243
- size: "md",
2244
- role: hasDetails ? "button" : void 0,
2245
- tabIndex: hasDetails ? 0 : void 0,
2246
- "aria-label": hasDetails ? expanded ? "Collapse run details" : "Expand run details" : void 0,
2247
- onClick: hasDetails ? () => setExpandedExecutionId(expanded ? null : execution.executionId) : void 0,
2248
- onKeyDown: hasDetails ? (event) => {
2249
- if (event.key === "Enter" || event.key === " ") {
2250
- event.preventDefault();
2251
- setExpandedExecutionId(expanded ? null : execution.executionId);
2252
- }
2253
- } : void 0,
2254
- style: {
2255
- cursor: hasDetails ? "pointer" : "default",
2256
- opacity: hasDetails ? 1 : 0.7
2257
- },
2258
- children: expanded ? /* @__PURE__ */ jsx(IconChevronDown, { size: 14 }) : /* @__PURE__ */ jsx(IconChevronRight, { size: 14 })
2259
- }
2260
- ) }),
2261
- /* @__PURE__ */ jsxs(Table.Td, { children: [
2262
- execution.resourceId ? /* @__PURE__ */ jsx(
2263
- Text,
2264
- {
2265
- fw: 500,
2266
- role: "button",
2267
- tabIndex: 0,
2268
- c: "blue",
2269
- onClick: () => openWorkflow(execution.resourceId),
2270
- onKeyDown: (event) => {
2271
- if (event.key === "Enter" || event.key === " ") {
2272
- event.preventDefault();
2273
- openWorkflow(execution.resourceId);
2274
- }
2275
- },
2276
- style: { cursor: "pointer" },
2277
- children: execution.resourceId
2278
- }
2279
- ) : /* @__PURE__ */ jsx(Text, { fw: 500, children: "Unknown workflow" }),
2280
- /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", ff: "monospace", children: execution.executionId })
2281
- ] }),
2282
- /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "light", color: getStatusColor2(execution.status), children: execution.status }) }),
2283
- /* @__PURE__ */ jsx(Table.Td, { children: formatDateTime2(execution.createdAt) }),
2284
- /* @__PURE__ */ jsx(Table.Td, { children: formatDateTime2(execution.completedAt) }),
2285
- /* @__PURE__ */ jsx(Table.Td, { children: formatDuration(execution.durationMs) })
2286
- ] }),
2287
- /* @__PURE__ */ jsx(Table.Tr, { children: /* @__PURE__ */ jsx(Table.Td, { colSpan: 6, p: 0, children: /* @__PURE__ */ jsx(Collapse, { in: expanded, children: /* @__PURE__ */ jsxs(Stack, { gap: "sm", p: "sm", children: [
2288
- details.input !== void 0 ? /* @__PURE__ */ jsxs(Stack, { gap: 4, children: [
2289
- /* @__PURE__ */ jsxs(Group, { gap: "xs", children: [
2290
- /* @__PURE__ */ jsx(Text, { size: "sm", fw: 600, children: "Input" }),
2291
- getSummaryKeys(details.input).map((key) => /* @__PURE__ */ jsx(Badge, { size: "xs", variant: "light", color: "gray", children: key }, key))
2292
- ] }),
2293
- /* @__PURE__ */ jsx(JsonViewer, { data: details.input, maxHeight: 220, fontSize: "0.75rem" })
2294
- ] }) : null,
2295
- details.config !== void 0 ? /* @__PURE__ */ jsxs(Stack, { gap: 4, children: [
2296
- /* @__PURE__ */ jsxs(Group, { gap: "xs", children: [
2297
- /* @__PURE__ */ jsx(Text, { size: "sm", fw: 600, children: "Config" }),
2298
- getSummaryKeys(details.config).map((key) => /* @__PURE__ */ jsx(Badge, { size: "xs", variant: "light", color: "gray", children: key }, key))
2299
- ] }),
2300
- /* @__PURE__ */ jsx(JsonViewer, { data: details.config, maxHeight: 220, fontSize: "0.75rem" })
2301
- ] }) : null
2302
- ] }) }) }) })
2303
- ] }, execution.executionId);
2304
- }) })
2305
- ] }) })
2306
- ]
2307
- }
2308
- );
2309
- }
2310
- var EMPTY_SELECTION = {
2311
- selectedCompanyIds: [],
2312
- selectedContactIds: []
2313
- };
2314
- function formatDateTime3(value) {
2315
- if (!value) return "Not yet";
2316
- return new Date(value).toLocaleString("en-US", {
2317
- month: "short",
2318
- day: "numeric",
2319
- year: "numeric",
2320
- hour: "numeric",
2321
- minute: "2-digit"
2322
- });
2323
- }
2324
- function getStatusColor3(status) {
2325
- switch (status) {
2326
- case "completed":
2327
- case "success":
2328
- case "launched":
2329
- return "green";
2330
- case "running":
2331
- case "pending":
2332
- case "enriching":
2333
- return "blue";
2334
- case "failed":
2335
- case "error":
2336
- case "archived":
2337
- return "red";
2338
- case "closing":
2339
- return "yellow";
2340
- default:
2341
- return "gray";
2342
- }
2343
- }
2344
- function ListBuilderPage({ listId }) {
2345
- const navigate = useNavigate();
2346
- const actions = useListActions();
2347
- const [runModalOpen, setRunModalOpen] = useState(false);
2348
- const [initialRunResourceId, setInitialRunResourceId] = useState(null);
2349
- const listQuery = useList(listId);
2350
- const progressQuery = useListProgress(listId);
2351
- const executionsQuery = useListExecutions(listId);
2352
- const isLoading = listQuery.isLoading || progressQuery.isLoading || executionsQuery.isLoading;
2353
- const error = listQuery.error ?? progressQuery.error ?? executionsQuery.error;
2354
- const executions = useMemo(() => executionsQuery.data ?? [], [executionsQuery.data]);
2355
- const openRunModal = (resourceId) => {
2356
- const nextResourceId = actions[0]?.resourceId ?? null;
2357
- setInitialRunResourceId(nextResourceId);
2358
- setRunModalOpen(true);
2359
- };
2360
- const closeRunModal = () => {
2361
- setRunModalOpen(false);
2362
- setInitialRunResourceId(null);
2363
- };
2364
- const copyListCommand = (id) => {
2365
- void navigator.clipboard.writeText(`/acquisition --lead-gen list ${id}`);
2366
- showSuccessNotification("Copied list command to clipboard");
2367
- };
2368
- const backButton = /* @__PURE__ */ jsx(
2369
- Button,
2370
- {
2371
- variant: "light",
2372
- size: "xs",
2373
- leftSection: /* @__PURE__ */ jsx(IconArrowLeft, { size: 16 }),
2374
- onClick: () => navigate({ to: "/lead-gen/lists" }),
2375
- children: "Lists"
2376
- }
2377
- );
2378
- if (isLoading) {
2379
- return /* @__PURE__ */ jsx(SubshellContentContainer, { children: /* @__PURE__ */ jsx(PageContainer, { children: /* @__PURE__ */ jsxs(Stack, { children: [
2380
- /* @__PURE__ */ jsx(PageTitleCaption, { title: "List Builder", caption: "Lead-gen workspace", rightSection: backButton }),
2381
- /* @__PURE__ */ jsx(Paper, { withBorder: true, children: /* @__PURE__ */ jsx(Center, { p: "xl", children: /* @__PURE__ */ jsx(Loader, {}) }) })
2382
- ] }) }) });
2383
- }
2384
- if (error) {
2385
- return /* @__PURE__ */ jsx(SubshellContentContainer, { children: /* @__PURE__ */ jsx(PageContainer, { children: /* @__PURE__ */ jsxs(Stack, { children: [
2386
- /* @__PURE__ */ jsx(PageTitleCaption, { title: "List Builder", caption: "Lead-gen workspace", rightSection: backButton }),
2387
- /* @__PURE__ */ jsx(Paper, { withBorder: true, children: /* @__PURE__ */ jsx(CenteredErrorState, { error, title: "Failed to load list builder" }) })
2388
- ] }) }) });
2389
- }
2390
- if (!listQuery.data || !progressQuery.data) {
2391
- return /* @__PURE__ */ jsx(SubshellContentContainer, { children: /* @__PURE__ */ jsx(PageContainer, { children: /* @__PURE__ */ jsxs(Stack, { children: [
2392
- /* @__PURE__ */ jsx(PageTitleCaption, { title: "List Not Found", caption: "The requested lead-gen list is unavailable." }),
2393
- /* @__PURE__ */ jsx(Paper, { withBorder: true, children: /* @__PURE__ */ jsx(Center, { h: 240, children: /* @__PURE__ */ jsx(Alert, { icon: /* @__PURE__ */ jsx(IconAlertCircle, { size: 16 }), color: "gray", variant: "light", children: "The requested list could not be found." }) }) })
2394
- ] }) }) });
2395
- }
2396
- const list = listQuery.data;
2397
- const progress = progressQuery.data;
2398
- return /* @__PURE__ */ jsxs(SubshellContentContainer, { children: [
2399
- /* @__PURE__ */ jsx(PageContainer, { children: /* @__PURE__ */ jsxs(Stack, { children: [
2400
- /* @__PURE__ */ jsx(
2401
- PageTitleCaption,
2402
- {
2403
- title: list.name,
2404
- caption: list.description ?? "Lead-gen list builder",
2405
- rightSection: /* @__PURE__ */ jsxs(Group, { gap: "xs", children: [
2406
- /* @__PURE__ */ jsx(Button, { size: "xs", leftSection: /* @__PURE__ */ jsx(IconBolt, { size: 16 }), onClick: () => openRunModal(), children: "Run Workflow" }),
2407
- backButton
2408
- ] })
2409
- }
2410
- ),
2411
- /* @__PURE__ */ jsxs(Group, { justify: "space-between", gap: "xs", children: [
2412
- /* @__PURE__ */ jsxs(Group, { gap: "xs", children: [
2413
- /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "filled", color: getStatusColor3(list.status), children: list.status }),
2414
- /* @__PURE__ */ jsxs(Text, { size: "sm", c: "dimmed", children: [
2415
- "Created ",
2416
- formatDateTime3(list.createdAt)
2417
- ] })
2418
- ] }),
2419
- /* @__PURE__ */ jsx(Group, { gap: "xs", children: /* @__PURE__ */ jsxs(Group, { gap: 4, wrap: "nowrap", onClick: () => copyListCommand(list.id), style: { cursor: "pointer" }, children: [
2420
- /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", ff: "monospace", children: list.id }),
2421
- /* @__PURE__ */ jsx(ActionIcon, { variant: "subtle", size: "sm", "aria-label": "Copy list command", children: /* @__PURE__ */ jsx(IconCopy, { size: 14 }) })
2422
- ] }) })
2423
- ] }),
2424
- /* @__PURE__ */ jsxs(SimpleGrid, { cols: { base: 1, sm: 2, lg: 4 }, children: [
2425
- /* @__PURE__ */ jsx(StatCard, { label: "Companies", value: progress.totalCompanies, icon: IconBuilding }),
2426
- /* @__PURE__ */ jsx(StatCard, { label: "Contacts", value: progress.totalMembers, icon: IconUsers }),
2427
- /* @__PURE__ */ jsx(StatCard, { label: "Workflows", value: actions.length, icon: IconBolt }),
2428
- /* @__PURE__ */ jsx(StatCard, { label: "Runs", value: executions.length, icon: IconPlayerPlay })
2429
- ] }),
2430
- /* @__PURE__ */ jsx(PipelineFunnel, { progress, pipelineConfig: list.pipelineConfig, actions }),
2431
- /* @__PURE__ */ jsx(WorkflowRunsPanel, { listId })
2432
- ] }) }),
2433
- /* @__PURE__ */ jsx(
2434
- RunWorkflowModal,
2435
- {
2436
- opened: runModalOpen,
2437
- onClose: closeRunModal,
2438
- list,
2439
- actions,
2440
- selection: EMPTY_SELECTION,
2441
- initialResourceId: initialRunResourceId,
2442
- onSubmitted: (_resourceId, executionId) => {
2443
- showSuccessNotification(`Workflow run started: ${executionId}`);
2444
- }
2445
- }
2446
- )
2447
- ] });
2448
- }
2449
-
2450
- // src/lib/lead-gen/processing-state.ts
2451
- var LEAD_GEN_STAGE_CATALOG4 = getLeadGenStageCatalog(canonicalOrganizationModel);
2452
- function isRecord(value) {
2453
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
2454
- }
2455
- function parseProcessingStageStatus(value) {
2456
- if (value === true || value === "success") return "success";
2457
- if (value === "no_result" || value === "noResult") return "noResult";
2458
- if (value === "skipped") return "skipped";
2459
- if (value === "error") return "error";
2460
- if (isRecord(value)) return parseProcessingStageStatus(value.status);
2461
- if (typeof value === "string") return "other";
2462
- return null;
2463
- }
2464
- function parseProcessingState(value) {
2465
- const parsed = {};
2466
- const stageStatuses = {};
2467
- const knownStageStatuses = {};
2468
- const unknownFields = {};
2469
- if (!isRecord(value)) {
2470
- Object.defineProperties(parsed, {
2471
- stageStatuses: { value: stageStatuses },
2472
- knownStageStatuses: { value: knownStageStatuses },
2473
- unknownFields: { value: unknownFields }
2474
- });
2475
- return parsed;
2476
- }
2477
- for (const [key, rawStatus] of Object.entries(value)) {
2478
- const parsedStatus = parseProcessingStageStatus(rawStatus);
2479
- const isKnownStage = Boolean(LEAD_GEN_STAGE_CATALOG4[key]);
2480
- if (!parsedStatus) {
2481
- unknownFields[key] = rawStatus;
2482
- continue;
2483
- }
2484
- stageStatuses[key] = parsedStatus;
2485
- parsed[key] = parsedStatus;
2486
- if (isKnownStage) {
2487
- knownStageStatuses[key] = parsedStatus;
2488
- } else {
2489
- unknownFields[key] = rawStatus;
2490
- }
2491
- }
2492
- Object.defineProperties(parsed, {
2493
- stageStatuses: { value: stageStatuses },
2494
- knownStageStatuses: { value: knownStageStatuses },
2495
- unknownFields: { value: unknownFields }
2496
- });
2497
- return parsed;
2498
- }
2499
- function readLeadGenProcessingState(row) {
2500
- if (!isRecord(row)) return null;
2501
- return row.processingState ?? row.processing_state ?? row.pipelineStatus ?? null;
2502
- }
2503
- function readLeadGenStateKey(row) {
2504
- if (!isRecord(row)) return null;
2505
- const stateKey = row.stateKey ?? row.state_key;
2506
- return typeof stateKey === "string" && stateKey.length > 0 ? stateKey : null;
2507
- }
2508
- function getDisplayLeadGenStageStateFor(row, entity) {
2509
- const parsed = parseProcessingState(readLeadGenProcessingState(row));
2510
- const latest = Object.values(LEAD_GEN_STAGE_CATALOG4).filter((stage) => stage.entity === entity && parsed.knownStageStatuses[stage.key]).sort((a, b) => b.order - a.order)[0];
2511
- if (latest) {
2512
- return {
2513
- stageKey: latest.key,
2514
- stageStatus: parsed.knownStageStatuses[latest.key]
2515
- };
2516
- }
2517
- const unknownStageKey = Object.keys(parsed.stageStatuses).find((stageKey) => !LEAD_GEN_STAGE_CATALOG4[stageKey]);
2518
- if (unknownStageKey) {
2519
- return {
2520
- stageKey: unknownStageKey,
2521
- stageStatus: parsed.stageStatuses[unknownStageKey]
2522
- };
2523
- }
2524
- const stateKey = readLeadGenStateKey(row);
2525
- return stateKey && LEAD_GEN_STAGE_CATALOG4[stateKey] ? { stageKey: stateKey, stageStatus: null } : { stageKey: null, stageStatus: null };
2526
- }
2527
-
2528
- // src/features/lead-gen/build-state.ts
2529
- var ORPHAN_STAGE_ORDER = 9999;
2530
- var LEAD_GEN_STAGE_CATALOG5 = getLeadGenStageCatalog(canonicalOrganizationModel);
2531
- function asRecord2(value) {
2532
- return value && typeof value === "object" && !Array.isArray(value) ? value : {};
2533
- }
2534
- function defineMvpBuildStep(step, emptyBlockedText) {
2535
- const description = "description" in step && typeof step.description === "string" ? step.description : `Run ${step.label.toLowerCase()} for this list.`;
2536
- return {
2537
- ...step,
2538
- description,
2539
- emptyBlockedText
2540
- };
2541
- }
2542
- var MVP_BUILD_STEPS = [
2543
- defineMvpBuildStep(
2544
- PROSPECTING_STEPS$1.localServices.sourceCompanies,
2545
- "No company source has been configured or populated yet."
2546
- ),
2547
- defineMvpBuildStep(
2548
- PROSPECTING_STEPS$1.localServices.analyzeWebsites,
2549
- "Source companies before website analysis can run."
2550
- ),
2551
- defineMvpBuildStep(
2552
- PROSPECTING_STEPS$1.localServices.qualifyCompanies,
2553
- "Analyze websites before qualification can run."
2554
- ),
2555
- defineMvpBuildStep(
2556
- PROSPECTING_STEPS$1.localServices.findContacts,
2557
- "Qualify companies before contact discovery can run."
2558
- ),
2559
- defineMvpBuildStep(PROSPECTING_STEPS$1.localServices.verifyEmails, "Find contacts before email verification can run."),
2560
- defineMvpBuildStep(PROSPECTING_STEPS$1.localServices.personalize, "Verify emails before personalization can run."),
2561
- defineMvpBuildStep(
2562
- PROSPECTING_STEPS$1.localServices.review,
2563
- "Personalize contacts before records are ready for review."
2564
- )
2565
- ];
2566
- function sortStageKeys(keys) {
2567
- return keys.slice().sort((a, b) => {
2568
- const oa = LEAD_GEN_STAGE_CATALOG5[a]?.order ?? ORPHAN_STAGE_ORDER;
2569
- const ob = LEAD_GEN_STAGE_CATALOG5[b]?.order ?? ORPHAN_STAGE_ORDER;
2570
- return oa - ob || a.localeCompare(b);
2571
- });
2572
- }
2573
- function getStageProgress3(progress, step) {
2574
- return step.primaryEntity === "company" ? progress.byCompanyStage[step.stageKey] : progress.byContactStage[step.stageKey];
2575
- }
2576
- function getEntityTotal(progress, entity) {
2577
- return entity === "company" ? progress.totalCompanies : progress.totalMembers;
2578
- }
2579
- function findActionForStep(actions, step) {
2580
- return actions.find((action) => action.actionKey === step.actionKey) ?? actions.find((action) => action.stagesAffected?.includes(step.stageKey));
2581
- }
2582
- function normalizeBuildPlanStep(step) {
2583
- const fallback = MVP_BUILD_STEPS.find((item) => item.id === step.id || item.stageKey === step.stageKey);
2584
- return {
2585
- id: step.id,
2586
- label: step.label,
2587
- description: step.description ?? fallback?.description ?? `Run ${step.label.toLowerCase()} for this list.`,
2588
- primaryEntity: step.primaryEntity,
2589
- outputs: step.outputs,
2590
- stageKey: step.stageKey,
2591
- recordEntity: step.recordEntity,
2592
- recordsStageKey: step.recordsStageKey,
2593
- recordSourceStageKey: step.recordSourceStageKey,
2594
- dependsOn: step.dependsOn,
2595
- dependencyMode: step.dependencyMode,
2596
- actionKey: step.actionKey,
2597
- defaultBatchSize: step.defaultBatchSize,
2598
- maxBatchSize: step.maxBatchSize,
2599
- recordColumns: step.recordColumns,
2600
- credentialRequirements: step.credentialRequirements,
2601
- emptyBlockedText: fallback?.emptyBlockedText ?? "Complete prerequisite build steps before this action can run."
2602
- };
2603
- }
2604
- function isCurrentBuildPlanStep(step) {
2605
- if (!step || typeof step !== "object") return false;
2606
- const candidate = step;
2607
- return (candidate.primaryEntity === "company" || candidate.primaryEntity === "contact") && Array.isArray(candidate.outputs) && candidate.outputs.length > 0 && candidate.dependencyMode === "per-record-eligibility";
2608
- }
2609
- function resolveBuildPlanSteps(list) {
2610
- const snapshot = list.metadata.buildPlanSnapshot;
2611
- const snapshotSteps = snapshot?.steps;
2612
- if (snapshotSteps?.length && snapshotSteps.every(isCurrentBuildPlanStep)) {
2613
- return snapshotSteps.map(normalizeBuildPlanStep);
2614
- }
2615
- if (snapshot?.templateId) {
2616
- const templateSnapshot = createBuildPlanSnapshotFromTemplateId(snapshot.templateId);
2617
- if (templateSnapshot?.steps.length) return templateSnapshot.steps.map(normalizeBuildPlanStep);
2618
- }
2619
- return MVP_BUILD_STEPS;
2620
- }
2621
- function getPrerequisiteSteps(step, byStepId) {
2622
- return (step.dependsOn ?? []).flatMap((dependencyId) => {
2623
- const dependency = byStepId.get(dependencyId);
2624
- return dependency ? [dependency] : [];
2625
- });
2626
- }
2627
- function getStepActionKind(step, ready, failed, action) {
2628
- if (!action) return "none";
2629
- if (failed > 0) return "retry_failed";
2630
- if (ready > 0) return "run_next_batch";
2631
- const isSeedStep = (step.dependsOn?.length ?? 0) === 0 && step.outputs.includes(step.primaryEntity);
2632
- if (isSeedStep) return "run_next_batch";
2633
- return "none";
2634
- }
2635
- function getStepActionLabel(kind) {
2636
- switch (kind) {
2637
- case "retry_failed":
2638
- return "Retry failed";
2639
- case "run_next_batch":
2640
- return "Run next batch";
2641
- case "advanced":
2642
- return "Advanced";
2643
- case "none":
2644
- default:
2645
- return "No action";
2646
- }
2647
- }
2648
- function createCompleteStageCounts(total) {
2649
- return {
2650
- total,
2651
- attempted: total,
2652
- success: total,
2653
- noResult: 0,
2654
- skipped: 0,
2655
- error: 0,
2656
- other: 0,
2657
- notAttempted: 0
2658
- };
2659
- }
2660
- function isStageComplete(entry) {
2661
- if (!entry || entry.total <= 0) return false;
2662
- return entry.error === 0 && entry.attempted >= entry.total && entry.success + entry.noResult + entry.skipped + entry.other >= entry.total;
2663
- }
2664
- function getExecutionInput(run) {
2665
- return asRecord2(run.input);
2666
- }
2667
- function isCompletedRun(run) {
2668
- return run.status === "completed" || run.status === "success" || run.status === "succeeded";
2669
- }
2670
- function getApprovedCompanyIds(run) {
2671
- const input = getExecutionInput(run);
2672
- const raw = input.approvedCompanyIds;
2673
- return Array.isArray(raw) ? raw.filter((value) => typeof value === "string") : [];
2674
- }
2675
- function hasApprovedExportRun(executions) {
2676
- return executions.some((run) => {
2677
- if (!isCompletedRun(run) || run.resourceId !== "lgn-06-export-list-workflow") return false;
2678
- const input = getExecutionInput(run);
2679
- return input.approved === true || getApprovedCompanyIds(run).length > 0;
2680
- });
2681
- }
2682
- function hasCompletedSourcingRun(step, action, executions) {
2683
- return executions.some((run) => isCompletedRun(run) && run.resourceId === (action?.resourceId ?? step.actionKey));
2684
- }
2685
- function deriveBusinessProgress(list, progress, actions = [], executions = []) {
2686
- const byCompanyStage = { ...progress.byCompanyStage };
2687
- const byContactStage = { ...progress.byContactStage };
2688
- const steps = resolveBuildPlanSteps(list);
2689
- for (const step of steps) {
2690
- const stageProgress = step.primaryEntity === "company" ? byCompanyStage[step.stageKey] : byContactStage[step.stageKey];
2691
- if (stageProgress || (step.dependsOn?.length ?? 0) > 0 || !step.outputs.includes("company")) continue;
2692
- if (progress.totalCompanies <= 0) continue;
2693
- const action = findActionForStep(actions, step);
2694
- if (hasCompletedSourcingRun(step, action, executions) || step.stageKey === "populated") {
2695
- byCompanyStage[step.stageKey] = createCompleteStageCounts(progress.totalCompanies);
2696
- }
2697
- }
2698
- if (!isStageComplete(byCompanyStage.uploaded) && hasApprovedExportRun(executions)) {
2699
- const exportedCount = Math.max(
2700
- ...executions.filter((run) => isCompletedRun(run) && run.resourceId === "lgn-06-export-list-workflow").map((run) => getApprovedCompanyIds(run).length),
2701
- 0
2702
- );
2703
- const total = exportedCount > 0 ? exportedCount : progress.totalCompanies;
2704
- if (total > 0) byCompanyStage.uploaded = createCompleteStageCounts(total);
2705
- }
2706
- return {
2707
- ...progress,
2708
- byCompanyStage,
2709
- byContactStage
2710
- };
2711
- }
2712
- function getStepRecommendedAction(step, action, kind) {
2713
- if (!action || kind === "none") return void 0;
2714
- return {
2715
- kind,
2716
- label: getStepActionLabel(kind),
2717
- actionKey: action.actionKey ?? step.actionKey,
2718
- defaultSize: step.defaultBatchSize,
2719
- maxSize: step.maxBatchSize
2720
- };
2721
- }
2722
- function deriveBuildStepStates(list, progress, actions) {
2723
- const byStepId = /* @__PURE__ */ new Map();
2724
- for (const step of resolveBuildPlanSteps(list)) {
2725
- const stageProgress = getStageProgress3(progress, step);
2726
- const entityTotal = getEntityTotal(progress, step.primaryEntity);
2727
- const prerequisites = getPrerequisiteSteps(step, byStepId);
2728
- const hasDependencies = (step.dependsOn?.length ?? 0) > 0;
2729
- const prerequisiteEligibleTotal = prerequisites.reduce((sum, prerequisite) => sum + prerequisite.complete, 0);
2730
- const eligibleTotal = hasDependencies ? entityTotal > 0 ? Math.min(entityTotal, prerequisiteEligibleTotal) : prerequisiteEligibleTotal : entityTotal > 0 ? entityTotal : 0;
2731
- const total = Math.max(stageProgress?.total ?? 0, entityTotal, eligibleTotal);
2732
- const complete = stageProgress ? stageProgress.success + stageProgress.noResult + stageProgress.skipped + stageProgress.other : 0;
2733
- const failed = stageProgress?.error ?? 0;
2734
- const ready = Math.max(0, eligibleTotal - complete - failed);
2735
- const blocked = hasDependencies ? Math.max(total - eligibleTotal - complete - failed, 0) : 0;
2736
- const status = failed > 0 ? "failed" : ready > 0 ? "ready" : blocked > 0 || total === 0 ? "blocked" : "complete";
2737
- const action = findActionForStep(actions, step);
2738
- const nextActionKind = getStepActionKind(step, ready, failed, action);
2739
- const state = {
2740
- ...step,
2741
- status,
2742
- ready,
2743
- complete,
2744
- failed,
2745
- blocked,
2746
- nextActionKind,
2747
- recommendedAction: getStepRecommendedAction(step, action, nextActionKind),
2748
- action
2749
- };
2750
- byStepId.set(step.id, state);
2751
- }
2752
- return Array.from(byStepId.values());
2753
- }
2754
- function getRecommendedBuildStep(steps) {
2755
- return steps.find((step) => step.nextActionKind === "retry_failed") ?? steps.find((step) => step.nextActionKind === "run_next_batch") ?? null;
2756
- }
2757
- function resolveBuildState(list, progress, actions) {
2758
- const steps = deriveBuildStepStates(list, progress, actions);
2759
- const recommendedStep = getRecommendedBuildStep(steps);
2760
- return {
2761
- steps,
2762
- recommendedStep,
2763
- recommendedAction: recommendedStep?.recommendedAction ?? null
2764
- };
2765
- }
2766
- var LEAD_GEN_STAGE_CATALOG6 = getLeadGenStageCatalog(canonicalOrganizationModel);
2767
- function formatDateTime4(value) {
2768
- if (!value) return "Not yet";
2769
- return new Date(value).toLocaleString("en-US", {
2770
- month: "short",
2771
- day: "numeric",
2772
- year: "numeric",
2773
- hour: "numeric",
2774
- minute: "2-digit"
2775
- });
2776
- }
2777
- function contactDisplayName(firstName, lastName) {
2778
- const full = [firstName, lastName].filter(Boolean).join(" ").trim();
2779
- return full || "\u2014";
2780
- }
2781
- function getStatusColor4(status) {
2782
- switch (status) {
2783
- case "draft":
2784
- return "gray";
2785
- case "enriching":
2786
- return "blue";
2787
- case "launched":
2788
- return "green";
2789
- case "closing":
2790
- return "yellow";
2791
- case "archived":
2792
- return "red";
2793
- default:
2794
- return "gray";
2795
- }
2796
- }
2797
- function getMemberStateColor2(stateKey) {
2798
- const stage = LEAD_GEN_STAGE_CATALOG6[stateKey];
2799
- if (stage?.entity === "contact") return "green";
2800
- if (stage?.entity === "company") return "blue";
2801
- return "gray";
2802
- }
2803
- function displayMemberStageFor(row, kind) {
2804
- const stage = getDisplayLeadGenStageStateFor(row, kind);
2805
- if (!stage.stageKey) return null;
2806
- const catalogStage = LEAD_GEN_STAGE_CATALOG6[stage.stageKey];
2807
- return {
2808
- key: stage.stageKey,
2809
- label: catalogStage?.label ?? stage.stageKey
2810
- };
2811
- }
2812
- function asRecord3(value) {
2813
- return value && typeof value === "object" && !Array.isArray(value) ? value : {};
2814
- }
2815
- function valueAtPath(source, path) {
2816
- return path.split(".").reduce((current, segment) => {
2817
- if (!current || typeof current !== "object") return void 0;
2818
- return current[segment];
2819
- }, source);
2820
- }
2821
- function compactText(value) {
2822
- if (value === null || value === void 0 || value === "") return "-";
2823
- if (Array.isArray(value)) {
2824
- if (!value.length) return "-";
2825
- return value.map((item) => {
2826
- if (item && typeof item === "object") {
2827
- const labelled = item;
2828
- return String(labelled.name ?? labelled.label ?? labelled.title ?? labelled.id ?? JSON.stringify(item));
2829
- }
2830
- return String(item);
2831
- }).join(", ");
2832
- }
2833
- if (typeof value === "object") return JSON.stringify(value);
2834
- return String(value);
2835
- }
2836
- function getRecordDisplayName(row) {
2837
- if (row.entity === "company") return row.company?.name ?? "Company";
2838
- const contact = row.contact;
2839
- const full = [contact?.firstName, contact?.lastName].filter(Boolean).join(" ").trim();
2840
- return full || contact?.email || "Contact";
2841
- }
2842
- function getRecordColumnSource(row) {
2843
- if (row.entity === "company") {
2844
- return {
2845
- ...row,
2846
- company: row.company ? {
2847
- ...row.company,
2848
- enrichmentData: row.enrichmentData,
2849
- processingState: row.processingState
2850
- } : null
2851
- };
2852
- }
2853
- return {
2854
- ...row,
2855
- contact: row.contact ? {
2856
- ...row.contact,
2857
- name: getRecordDisplayName(row),
2858
- enrichmentData: row.enrichmentData,
2859
- processingState: row.processingState
2860
- } : null
2861
- };
2862
- }
2863
- function getStageEntry(row, stageKey) {
2864
- const entry = valueAtPath(row.processingState, stageKey);
2865
- return asRecord3(entry);
2866
- }
2867
- function getRecordStageStatus(row, stageKey) {
2868
- const status = getStageEntry(row, stageKey).status;
2869
- return typeof status === "string" ? status : "pending";
2870
- }
2871
- function getStageFailureReason(row, stageKey) {
2872
- const data = asRecord3(getStageEntry(row, stageKey).data);
2873
- const error = data.error ?? data.reason ?? data.failureReason ?? data.disqualifiedReason;
2874
- return compactText(error);
2875
- }
2876
- function getRecordStatusColor(status) {
2877
- switch (status) {
2878
- case "success":
2879
- return "green";
2880
- case "error":
2881
- case "failed":
2882
- return "red";
2883
- case "skipped":
2884
- case "no_result":
2885
- return "yellow";
2886
- case "pending":
2887
- return "gray";
2888
- default:
2889
- return "blue";
2890
- }
2891
- }
2892
- function renderRecordValue(value, column) {
2893
- if (value === null || value === void 0 || value === "") {
2894
- return /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: "-" });
2895
- }
2896
- switch (column.renderType) {
2897
- case "badge":
2898
- return /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "light", color: column.badgeColor ?? getRecordStatusColor(String(value)), children: compactText(value) });
2899
- case "datetime":
2900
- return compactText(typeof value === "string" ? formatDateTime4(value) : value);
2901
- case "count":
2902
- return Array.isArray(value) ? value.length : compactText(value);
2903
- case "json":
2904
- return /* @__PURE__ */ jsx(Text, { size: "xs", lineClamp: 2, children: compactText(value) });
2905
- case "text":
2906
- default:
2907
- return /* @__PURE__ */ jsx(Text, { size: "sm", lineClamp: 2, children: compactText(value) });
2908
- }
2909
- }
2910
- function getRunStatusColor(status) {
2911
- switch (status) {
2912
- case "completed":
2913
- case "success":
2914
- case "succeeded":
2915
- return "green";
2916
- case "running":
2917
- case "pending":
2918
- case "queued":
2919
- return "blue";
2920
- case "failed":
2921
- case "error":
2922
- case "cancelled":
2923
- return "red";
2924
- default:
2925
- return "gray";
2926
- }
2927
- }
2928
- function getRunInput(run) {
2929
- const payload = asRecord3(run.payload);
2930
- return run.input ?? run.inputs ?? payload.input ?? payload.inputs;
2931
- }
2932
- function hasRunInput(run) {
2933
- const input = getRunInput(run);
2934
- return input !== void 0 && input !== null;
2935
- }
2936
- function sanitizeStepInput(value, _actionKey) {
2937
- return value;
2938
- }
2939
- function getDefaultStepInput(list, action) {
2940
- return asRecord3(action?.defaultInput?.(list));
2941
- }
2942
- function getInitialStepInput(list, action) {
2943
- return getDefaultStepInput(list, action);
2944
- }
2945
- function mergeStepRunInput(list, action, inputAtSubmit) {
2946
- return {
2947
- ...getDefaultStepInput(list, action),
2948
- ...inputAtSubmit
2949
- };
2950
- }
2951
- var EMPTY_SELECTION2 = {
2952
- selectedCompanyIds: [],
2953
- selectedContactIds: []
2954
- };
2955
- function getStageStatus(entry) {
2956
- if (!entry || (entry.total ?? 0) === 0) return "not-started";
2957
- if ((entry.error ?? 0) > 0) return "errors";
2958
- if ((entry.attempted ?? 0) >= (entry.total ?? 0) && (entry.success ?? 0) > 0) return "complete";
2959
- return "in-progress";
2960
- }
2961
- function getStageColors(status) {
2962
- switch (status) {
2963
- case "complete":
2964
- return {
2965
- bg: "var(--color-success)",
2966
- fg: "white",
2967
- border: "var(--color-success)",
2968
- ring: "color-mix(in srgb, var(--color-success) 22%, transparent)"
2969
- };
2970
- case "errors":
2971
- return {
2972
- bg: "var(--color-error)",
2973
- fg: "white",
2974
- border: "var(--color-error)",
2975
- ring: "color-mix(in srgb, var(--color-error) 22%, transparent)"
2976
- };
2977
- case "in-progress":
2978
- return {
2979
- bg: "var(--color-primary)",
2980
- fg: "white",
2981
- border: "var(--color-primary)",
2982
- ring: "color-mix(in srgb, var(--color-primary) 22%, transparent)"
2983
- };
2984
- case "not-started":
2985
- default:
2986
- return {
2987
- bg: "color-mix(in srgb, var(--color-primary) 28%, var(--mantine-color-body))",
2988
- fg: "color-mix(in srgb, var(--color-primary) 55%, var(--color-text-dimmed))",
2989
- border: "color-mix(in srgb, var(--color-primary) 45%, var(--color-border))",
2990
- ring: "color-mix(in srgb, var(--color-primary) 10%, transparent)"
2991
- };
2992
- }
2993
- }
2994
- var STAGE_NODE_WIDTH = 110;
2995
- var STAGE_CIRCLE_SIZE = 36;
2996
- function StageNode({
2997
- index,
2998
- stageKey,
2999
- fallbackLabel,
3000
- entry
3001
- }) {
3002
- const label = LEAD_GEN_STAGE_CATALOG6[stageKey]?.label ?? fallbackLabel ?? stageKey;
3003
- const status = getStageStatus(entry);
3004
- const colors = getStageColors(status);
3005
- const total = entry?.total ?? 0;
3006
- const attempted = entry?.attempted ?? 0;
3007
- const success = entry?.success ?? 0;
3008
- const done = entry ? entry.success + entry.noResult + entry.skipped + entry.other : 0;
3009
- const summary = entry ? status === "complete" ? `${done}/${total} done` : status === "errors" ? `${entry.error} failed` : `${attempted}/${total} started` : "Not started";
3010
- const tooltipLines = entry ? [
3011
- `${attempted} / ${total} attempted`,
3012
- `${success} success`,
3013
- (entry.noResult ?? 0) > 0 ? `${entry.noResult} no result` : null,
3014
- (entry.skipped ?? 0) > 0 ? `${entry.skipped} skipped` : null,
3015
- (entry.error ?? 0) > 0 ? `${entry.error} errors` : null,
3016
- (entry.notAttempted ?? 0) > 0 ? `${entry.notAttempted} not attempted` : null
3017
- ].filter(Boolean) : ["Not started"];
3018
- return /* @__PURE__ */ jsxs(
3019
- Stack,
3020
- {
3021
- gap: 6,
3022
- align: "center",
3023
- style: {
3024
- width: STAGE_NODE_WIDTH,
3025
- flex: "0 0 auto",
3026
- padding: "0 4px",
3027
- position: "relative",
3028
- zIndex: 1
3029
- },
3030
- children: [
3031
- /* @__PURE__ */ jsx(Tooltip, { label: tooltipLines.join(" \xB7 "), position: "top", withArrow: true, multiline: true, maw: 260, children: /* @__PURE__ */ jsx(
3032
- Box,
3033
- {
3034
- style: {
3035
- width: STAGE_CIRCLE_SIZE,
3036
- height: STAGE_CIRCLE_SIZE,
3037
- borderRadius: "50%",
3038
- backgroundColor: colors.bg,
3039
- border: `1.5px solid ${colors.border}`,
3040
- display: "flex",
3041
- alignItems: "center",
3042
- justifyContent: "center",
3043
- boxShadow: `0 0 0 4px ${colors.ring}`,
3044
- transition: "all var(--duration-normal) var(--easing)"
3045
- },
3046
- children: /* @__PURE__ */ jsx(Box, { style: { color: colors.fg, lineHeight: 1, display: "flex", alignItems: "center" }, children: status === "complete" ? "OK" : index + 1 })
3047
- }
3048
- ) }),
3049
- /* @__PURE__ */ jsxs(Stack, { gap: 2, align: "center", style: { minWidth: 0, maxWidth: "100%" }, children: [
3050
- /* @__PURE__ */ jsx(
3051
- Text,
3052
- {
3053
- size: "xs",
3054
- fw: status === "not-started" ? 500 : 600,
3055
- ta: "center",
3056
- lineClamp: 1,
3057
- style: {
3058
- maxWidth: "100%",
3059
- color: status === "not-started" ? "var(--color-text-dimmed)" : "var(--color-text)",
3060
- letterSpacing: 0.2
3061
- },
3062
- children: label
3063
- }
3064
- ),
3065
- /* @__PURE__ */ jsx(Text, { size: "xs", style: { color: "var(--color-text-subtle)", fontVariantNumeric: "tabular-nums" }, children: summary })
3066
- ] })
3067
- ]
3068
- }
3069
- );
3070
- }
3071
- function PipelineStepper({
3072
- plannedSteps,
3073
- byStage,
3074
- emptyText
3075
- }) {
3076
- const stageKeys = plannedSteps.length > 0 ? plannedSteps.map((s) => s.stageKey) : sortStageKeys(Object.keys(byStage));
3077
- const labelByKey = new Map(plannedSteps.map((s) => [s.stageKey, s.label]));
3078
- if (stageKeys.length === 0) {
3079
- return /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: emptyText });
3080
- }
3081
- return /* @__PURE__ */ jsx(Box, { style: { overflowX: "auto", padding: "4px 0" }, children: /* @__PURE__ */ jsxs(
3082
- Box,
3083
- {
3084
- style: {
3085
- position: "relative",
3086
- display: "flex",
3087
- justifyContent: "space-between",
3088
- alignItems: "flex-start",
3089
- minWidth: stageKeys.length * STAGE_NODE_WIDTH
3090
- },
3091
- children: [
3092
- /* @__PURE__ */ jsx(
3093
- Box,
3094
- {
3095
- style: {
3096
- position: "absolute",
3097
- top: STAGE_CIRCLE_SIZE / 2 - 1,
3098
- left: STAGE_NODE_WIDTH / 2,
3099
- right: STAGE_NODE_WIDTH / 2,
3100
- height: 2,
3101
- backgroundColor: "var(--color-border)"
3102
- }
3103
- }
3104
- ),
3105
- stageKeys.map((key, index) => /* @__PURE__ */ jsx(StageNode, { index, stageKey: key, fallbackLabel: labelByKey.get(key), entry: byStage[key] }, key))
3106
- ]
3107
- }
3108
- ) });
3109
- }
3110
- function PipelineStagesCard({ list, progress }) {
3111
- const plannedSteps = resolveBuildPlanSteps(list);
3112
- const templateLabel = list.metadata.buildPlanSnapshot?.templateLabel;
3113
- const combinedByStage = { ...progress.byCompanyStage, ...progress.byContactStage };
3114
- const hasAnyContent = plannedSteps.length > 0 || Object.keys(combinedByStage).length > 0;
3115
- return /* @__PURE__ */ jsx(Card, { withBorder: true, children: /* @__PURE__ */ jsxs(Stack, { gap: "md", children: [
3116
- /* @__PURE__ */ jsxs(Group, { justify: "space-between", align: "center", children: [
3117
- /* @__PURE__ */ jsxs(Stack, { gap: 2, children: [
3118
- /* @__PURE__ */ jsx(Title, { order: 5, children: "List Build Progress" }),
3119
- templateLabel && /* @__PURE__ */ jsxs(Text, { size: "xs", c: "dimmed", children: [
3120
- "Template: ",
3121
- templateLabel
3122
- ] })
3123
- ] }),
3124
- /* @__PURE__ */ jsxs(Group, { gap: "xs", children: [
3125
- /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "filled", color: getStatusColor4(list.status), children: list.status }),
3126
- list.launchedAt && /* @__PURE__ */ jsxs(Text, { size: "xs", c: "dimmed", children: [
3127
- "Launched ",
3128
- formatDateTime4(list.launchedAt)
3129
- ] }),
3130
- list.completedAt && /* @__PURE__ */ jsxs(Text, { size: "xs", c: "dimmed", children: [
3131
- "Completed ",
3132
- formatDateTime4(list.completedAt)
3133
- ] })
3134
- ] })
3135
- ] }),
3136
- !hasAnyContent ? /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: "No pipeline stages configured yet \u2014 plan a build template to populate this list." }) : /* @__PURE__ */ jsxs(Stack, { gap: "xs", children: [
3137
- /* @__PURE__ */ jsxs(Text, { size: "sm", fw: 600, children: [
3138
- "Pipeline (",
3139
- progress.totalCompanies,
3140
- " companies \xB7 ",
3141
- progress.totalMembers,
3142
- " contacts)"
3143
- ] }),
3144
- /* @__PURE__ */ jsx(
3145
- PipelineStepper,
3146
- {
3147
- plannedSteps,
3148
- byStage: combinedByStage,
3149
- emptyText: "No pipeline activity recorded yet."
3150
- }
3151
- )
3152
- ] })
3153
- ] }) });
3154
- }
3155
- function ListConfigCard({ list }) {
3156
- const icp = list.icp ?? {};
3157
- const scraping = list.scrapingConfig ?? {};
3158
- const hasIcp = Object.values(icp).some((v) => v !== void 0 && v !== null && v !== "");
3159
- const hasScraping = Object.values(scraping).some((v) => v !== void 0 && v !== null && v !== "");
3160
- if (!hasIcp && !hasScraping) {
3161
- return /* @__PURE__ */ jsx(Card, { withBorder: true, children: /* @__PURE__ */ jsxs(Stack, { gap: "xs", children: [
3162
- /* @__PURE__ */ jsx(Title, { order: 5, children: "List Config" }),
3163
- /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: "No ICP rubric or scraping criteria recorded for this list." })
3164
- ] }) });
3165
- }
3166
- return /* @__PURE__ */ jsx(Card, { withBorder: true, children: /* @__PURE__ */ jsxs(Stack, { gap: "sm", children: [
3167
- /* @__PURE__ */ jsx(Title, { order: 5, children: "List Config" }),
3168
- (hasIcp || hasScraping) && /* @__PURE__ */ jsxs(SimpleGrid, { cols: { base: 1, md: 2 }, spacing: "md", children: [
3169
- hasIcp && /* @__PURE__ */ jsxs(Stack, { gap: 6, children: [
3170
- /* @__PURE__ */ jsx(Text, { size: "sm", fw: 600, children: "ICP Rubric" }),
3171
- icp.qualificationRubricKey && /* @__PURE__ */ jsxs(Group, { gap: "xs", children: [
3172
- /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: "Rubric Key" }),
3173
- /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "light", children: icp.qualificationRubricKey })
3174
- ] }),
3175
- icp.targetDescription && /* @__PURE__ */ jsxs(Stack, { gap: 2, children: [
3176
- /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: "Target" }),
3177
- /* @__PURE__ */ jsx(Text, { size: "sm", children: icp.targetDescription })
3178
- ] }),
3179
- (icp.minReviewCount !== void 0 || icp.minRating !== void 0) && /* @__PURE__ */ jsxs(Group, { gap: "md", children: [
3180
- icp.minReviewCount !== void 0 && /* @__PURE__ */ jsxs(Text, { size: "xs", c: "dimmed", children: [
3181
- "Min reviews:",
3182
- " ",
3183
- /* @__PURE__ */ jsx(Text, { span: true, size: "xs", c: "bright", children: icp.minReviewCount })
3184
- ] }),
3185
- icp.minRating !== void 0 && /* @__PURE__ */ jsxs(Text, { size: "xs", c: "dimmed", children: [
3186
- "Min rating:",
3187
- " ",
3188
- /* @__PURE__ */ jsx(Text, { span: true, size: "xs", c: "bright", children: icp.minRating })
3189
- ] })
3190
- ] }),
3191
- icp.excludeFranchises !== void 0 && /* @__PURE__ */ jsxs(Text, { size: "xs", c: "dimmed", children: [
3192
- "Exclude franchises:",
3193
- " ",
3194
- /* @__PURE__ */ jsx(Text, { span: true, size: "xs", c: "bright", children: String(icp.excludeFranchises) })
3195
- ] }),
3196
- icp.customRules && /* @__PURE__ */ jsxs(Stack, { gap: 2, children: [
3197
- /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: "Custom rules" }),
3198
- /* @__PURE__ */ jsx(Text, { size: "sm", children: icp.customRules })
3199
- ] })
3200
- ] }),
3201
- hasScraping && /* @__PURE__ */ jsxs(Stack, { gap: 6, children: [
3202
- /* @__PURE__ */ jsx(Text, { size: "sm", fw: 600, children: "Scraping Criteria" }),
3203
- scraping.vertical && /* @__PURE__ */ jsxs(Group, { gap: "xs", children: [
3204
- /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: "Vertical" }),
3205
- /* @__PURE__ */ jsx(Text, { size: "sm", children: scraping.vertical })
3206
- ] }),
3207
- scraping.geography && /* @__PURE__ */ jsxs(Group, { gap: "xs", children: [
3208
- /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: "Geography" }),
3209
- /* @__PURE__ */ jsx(Text, { size: "sm", children: scraping.geography })
3210
- ] }),
3211
- scraping.size && /* @__PURE__ */ jsxs(Group, { gap: "xs", children: [
3212
- /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: "Size" }),
3213
- /* @__PURE__ */ jsx(Text, { size: "sm", children: scraping.size })
3214
- ] }),
3215
- scraping.apifyInput && Object.keys(scraping.apifyInput).length > 0 && /* @__PURE__ */ jsxs(Stack, { gap: 2, children: [
3216
- /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: "Apify input" }),
3217
- /* @__PURE__ */ jsx(JsonViewer, { data: scraping.apifyInput, maxHeight: 160, fontSize: "0.75rem" })
3218
- ] })
3219
- ] })
3220
- ] })
3221
- ] }) });
3222
- }
3223
- function OverviewTab({ list, progress }) {
3224
- const hasMetadata = list.metadata && Object.keys(list.metadata).length > 0;
3225
- return /* @__PURE__ */ jsxs(
3226
- TabSection,
3227
- {
3228
- icon: /* @__PURE__ */ jsx(IconBuilding, { size: 16 }),
3229
- title: "Overview",
3230
- description: "Build status and source details for this list.",
3231
- children: [
3232
- /* @__PURE__ */ jsxs(SimpleGrid, { cols: { base: 1, sm: 2 }, children: [
3233
- /* @__PURE__ */ jsx(StatCard, { label: "Companies", value: progress.totalCompanies, icon: IconBuilding }),
3234
- /* @__PURE__ */ jsx(StatCard, { label: "Members", value: progress.totalMembers, icon: IconUsers })
3235
- ] }),
3236
- /* @__PURE__ */ jsx(PipelineStagesCard, { list, progress }),
3237
- /* @__PURE__ */ jsx(ListConfigCard, { list }),
3238
- hasMetadata && /* @__PURE__ */ jsx(Card, { withBorder: true, children: /* @__PURE__ */ jsxs(Stack, { gap: "xs", children: [
3239
- /* @__PURE__ */ jsx(Title, { order: 5, children: "Metadata" }),
3240
- /* @__PURE__ */ jsx(JsonViewer, { data: list.metadata, maxHeight: 320 })
3241
- ] }) })
3242
- ]
3243
- }
3244
- );
3245
- }
3246
- function getBuildToneStyle(tone) {
3247
- switch (tone) {
3248
- case "complete":
3249
- return {
3250
- backgroundColor: "color-mix(in srgb, var(--color-success) 16%, transparent)",
3251
- borderColor: "color-mix(in srgb, var(--color-success) 34%, var(--color-border))",
3252
- color: "var(--color-success)"
3253
- };
3254
- case "failed":
3255
- return {
3256
- backgroundColor: "color-mix(in srgb, var(--color-error) 14%, transparent)",
3257
- borderColor: "color-mix(in srgb, var(--color-error) 38%, var(--color-border))",
3258
- color: "var(--color-error)"
3259
- };
3260
- case "ready":
3261
- return {
3262
- backgroundColor: "color-mix(in srgb, var(--color-primary) 14%, transparent)",
3263
- borderColor: "var(--border-primary-muted)",
3264
- color: "var(--color-primary)"
3265
- };
3266
- case "blocked":
3267
- case "neutral":
3268
- default:
3269
- return {
3270
- backgroundColor: "color-mix(in srgb, var(--color-surface-hover) 70%, transparent)",
3271
- borderColor: "var(--color-border)",
3272
- color: "var(--color-text-dimmed)"
3273
- };
3274
- }
3275
- }
3276
- function StatusPill({ label, tone }) {
3277
- return /* @__PURE__ */ jsx(
3278
- Box,
3279
- {
3280
- component: "span",
3281
- px: 8,
3282
- py: 3,
3283
- style: {
3284
- ...getBuildToneStyle(tone),
3285
- borderRadius: 999,
3286
- borderStyle: "solid",
3287
- borderWidth: 1,
3288
- display: "inline-flex",
3289
- fontSize: 11,
3290
- fontWeight: 700,
3291
- lineHeight: 1,
3292
- whiteSpace: "nowrap"
3293
- },
3294
- children: label
3295
- }
3296
- );
3297
- }
3298
- function StepCounterLine({ step }) {
3299
- const items = [
3300
- step.ready > 0 ? { label: "ready", value: step.ready, color: "var(--color-primary)" } : null,
3301
- step.complete > 0 ? { label: "done", value: step.complete, color: "var(--color-success)" } : null,
3302
- step.failed > 0 ? { label: "failed", value: step.failed, color: "var(--color-error)" } : null,
3303
- step.blocked > 0 ? { label: "waiting", value: step.blocked, color: "var(--color-text-dimmed)" } : null
3304
- ].filter((item) => item !== null);
3305
- if (!items.length) {
3306
- return /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: "No records yet" });
3307
- }
3308
- return /* @__PURE__ */ jsx(Group, { gap: "md", wrap: "nowrap", children: items.map((item) => /* @__PURE__ */ jsxs(Text, { size: "xs", fw: 700, style: { color: item.color }, children: [
3309
- item.value,
3310
- " ",
3311
- item.label
3312
- ] }, item.label)) });
3313
- }
3314
- function BuildStepProgressBar({ step }) {
3315
- const segments = [
3316
- { key: "complete", value: step.complete, color: "var(--color-success)" },
3317
- { key: "ready", value: step.ready, color: "var(--color-primary)" },
3318
- { key: "failed", value: step.failed, color: "var(--color-error)" },
3319
- { key: "blocked", value: step.blocked, color: "var(--color-border)" }
3320
- ].filter((segment) => segment.value > 0);
3321
- const total = segments.reduce((sum, segment) => sum + segment.value, 0);
3322
- return /* @__PURE__ */ jsx(
3323
- Box,
3324
- {
3325
- h: 8,
3326
- "aria-label": `${step.label} progress: ${step.complete} complete, ${step.ready} ready, ${step.failed} failed, ${step.blocked} blocked`,
3327
- style: {
3328
- backgroundColor: "color-mix(in srgb, var(--color-border) 65%, transparent)",
3329
- borderRadius: 999,
3330
- display: "flex",
3331
- overflow: "hidden"
3332
- },
3333
- children: segments.map((segment) => /* @__PURE__ */ jsx(Box, { h: "100%", w: `${total > 0 ? segment.value / total * 100 : 0}%`, bg: segment.color }, segment.key))
3334
- }
3335
- );
3336
- }
3337
- function getDisplayStep(steps, recommendedStep) {
3338
- return recommendedStep ?? steps.find((step) => step.status !== "complete") ?? steps[steps.length - 1] ?? null;
3339
- }
3340
- function getBuildTabDescription(steps, currentStep) {
3341
- if (!steps.length) return "No build steps are available for this list.";
3342
- if (steps.every((step) => step.status === "complete")) return "List build complete.";
3343
- if (!currentStep) return "No build steps need attention right now.";
3344
- if (currentStep.status === "ready") return `${currentStep.label} is ready to run.`;
3345
- if (currentStep.status === "failed") return `${currentStep.label} needs attention.`;
3346
- return `${currentStep.label} is waiting on earlier work.`;
3347
- }
3348
- var RECORDS_PAGE_SIZE = 25;
3349
- var EXPORT_WORKFLOW_ID = "lgn-06-export-list-workflow";
3350
- function setValueAtInputPath(source, path, value) {
3351
- const segments = path.split(".").filter(Boolean);
3352
- if (!segments.length) return source;
3353
- const next = { ...source };
3354
- let cursor = next;
3355
- segments.forEach((segment, index) => {
3356
- if (index === segments.length - 1) {
3357
- cursor[segment] = value;
3358
- return;
3359
- }
3360
- const current = cursor[segment];
3361
- const child = current && typeof current === "object" && !Array.isArray(current) ? { ...current } : {};
3362
- cursor[segment] = child;
3363
- cursor = child;
3364
- });
3365
- return next;
3366
- }
3367
- function credentialMatchesRequirement(credential, requirement) {
3368
- const provider = requirement.provider.toLowerCase();
3369
- const credentialProvider = credential.provider?.toLowerCase() ?? "";
3370
- const credentialType = credential.type.toLowerCase();
3371
- const credentialName = credential.name.toLowerCase();
3372
- if (credentialProvider === provider || credentialType === provider || credentialName.startsWith(`${provider}-`) || credentialName.includes(`-${provider}`) || credentialName.includes(`${provider}-`)) {
3373
- return true;
3374
- }
3375
- return credentialType === requirement.credentialType;
3376
- }
3377
- function getMatchingCredentials(credentials, requirement) {
3378
- return credentials.filter((credential) => credentialMatchesRequirement(credential, requirement));
3379
- }
3380
- function getSelectedCredential(credentials, selections, requirement) {
3381
- const selectedId = selections[requirement.key];
3382
- return credentials.find((credential) => credential.id === selectedId) ?? null;
3383
- }
3384
- function getClickUpRequirement(step) {
3385
- return step.credentialRequirements?.find((requirement) => requirement.provider === "clickup") ?? null;
3386
- }
3387
- function filterCredentialRequirementFields(layout, requirements) {
3388
- if (!layout || requirements.length === 0) return layout;
3389
- const credentialInputPaths = new Set(requirements.map((requirement) => requirement.inputPath).filter(Boolean));
3390
- if (!credentialInputPaths.size) return layout;
3391
- const filterSection = (section) => {
3392
- const fields = section.fields.filter((field) => !credentialInputPaths.has(field.path));
3393
- return fields.length > 0 ? { ...section, fields } : null;
3394
- };
3395
- return {
3396
- ...layout,
3397
- sections: layout.sections.map(filterSection).filter((section) => section !== null),
3398
- advanced: layout.advanced ? filterSection(layout.advanced) ?? void 0 : void 0
3399
- };
3400
- }
3401
- function hasMainConfigFields(layout) {
3402
- return Boolean(layout?.sections.some((section) => section.fields.length > 0));
3403
- }
3404
- function hasAdvancedConfigFields(layout) {
3405
- return Boolean(layout?.advanced?.fields.length);
3406
- }
3407
- function formatCredentialVerificationDetails(details) {
3408
- if (!details) return null;
3409
- const entries = Object.entries(details).filter(([, value]) => value !== null && value !== void 0 && value !== "").slice(0, 4);
3410
- if (!entries.length) return null;
3411
- return entries.map(([key, value]) => `${key}: ${compactText(value)}`).join(" | ");
3412
- }
3413
- function CredentialRequirementsPanel({
3414
- requirements,
3415
- credentials,
3416
- credentialsLoading,
3417
- selections,
3418
- verificationByCredentialId,
3419
- verifyingCredentialId,
3420
- onSelect,
3421
- onVerify
3422
- }) {
3423
- if (!requirements.length) return null;
3424
- return /* @__PURE__ */ jsxs(Stack, { gap: "xs", children: [
3425
- /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", fw: 700, tt: "uppercase", children: "Credentials" }),
3426
- requirements.map((requirement) => {
3427
- const matchingCredentials = getMatchingCredentials(credentials, requirement);
3428
- const selectedCredential = getSelectedCredential(credentials, selections, requirement);
3429
- const verification = selectedCredential ? verificationByCredentialId[selectedCredential.id] : null;
3430
- return /* @__PURE__ */ jsxs(Stack, { gap: 6, children: [
3431
- /* @__PURE__ */ jsxs(Group, { gap: "xs", align: "flex-end", wrap: "nowrap", children: [
3432
- /* @__PURE__ */ jsx(
3433
- Select,
3434
- {
3435
- label: requirement.label,
3436
- description: `${requirement.provider} credential for ${requirement.inputPath}`,
3437
- placeholder: credentialsLoading ? "Loading credentials..." : "Select credential",
3438
- data: matchingCredentials.map((credential) => ({
3439
- value: credential.id,
3440
- label: `${credential.name} (${credential.provider ?? credential.type})`
3441
- })),
3442
- value: selectedCredential?.id ?? null,
3443
- onChange: (credentialId) => {
3444
- onSelect(requirement, credentials.find((credential) => credential.id === credentialId) ?? null);
3445
- },
3446
- required: requirement.required,
3447
- disabled: credentialsLoading || matchingCredentials.length === 0,
3448
- style: { flex: 1 }
3449
- }
3450
- ),
3451
- /* @__PURE__ */ jsx(
3452
- Button,
3453
- {
3454
- size: "xs",
3455
- variant: "light",
3456
- disabled: !selectedCredential,
3457
- loading: selectedCredential ? verifyingCredentialId === selectedCredential.id : false,
3458
- onClick: () => selectedCredential && onVerify(selectedCredential),
3459
- children: "Test credential"
3460
- }
3461
- )
3462
- ] }),
3463
- matchingCredentials.length === 0 ? /* @__PURE__ */ jsx(
3464
- Alert,
3465
- {
3466
- color: requirement.required ? "yellow" : "gray",
3467
- variant: "light",
3468
- icon: /* @__PURE__ */ jsx(IconAlertCircle, { size: 16 }),
3469
- children: /* @__PURE__ */ jsxs(Text, { size: "sm", children: [
3470
- "No matching ",
3471
- requirement.provider,
3472
- " credentials found. Add one in",
3473
- " ",
3474
- /* @__PURE__ */ jsx(Text, { component: "a", href: "/settings/credentials", size: "sm", fw: 600, children: "Settings -> Credentials" }),
3475
- "."
3476
- ] })
3477
- }
3478
- ) : null,
3479
- verification ? /* @__PURE__ */ jsx(
3480
- Alert,
3481
- {
3482
- color: verification.status === "success" ? "green" : "red",
3483
- variant: "light",
3484
- icon: /* @__PURE__ */ jsx(IconAlertCircle, { size: 16 }),
3485
- children: /* @__PURE__ */ jsxs(Stack, { gap: 2, children: [
3486
- /* @__PURE__ */ jsx(Text, { size: "sm", children: verification.message }),
3487
- verification.checkedAt ? /* @__PURE__ */ jsxs(Text, { size: "xs", c: "dimmed", children: [
3488
- "Checked ",
3489
- formatDateTime4(verification.checkedAt)
3490
- ] }) : null,
3491
- formatCredentialVerificationDetails(verification.details) ? /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: formatCredentialVerificationDetails(verification.details) }) : null
3492
- ] })
3493
- }
3494
- ) : null
3495
- ] }, requirement.key);
3496
- })
3497
- ] });
3498
- }
3499
- function BuildStepRunningWatcher({
3500
- resourceId,
3501
- onRunningChange
3502
- }) {
3503
- const inFlightQuery = useInFlightExecutions(resourceId, {
3504
- enabled: Boolean(resourceId),
3505
- limit: 5,
3506
- refetchInterval: 2e3
3507
- });
3508
- const isRunning = (inFlightQuery.data?.executions.length ?? 0) > 0;
3509
- useEffect(() => {
3510
- onRunningChange(resourceId, isRunning);
3511
- return () => onRunningChange(resourceId, false);
3512
- }, [isRunning, onRunningChange, resourceId]);
3513
- return null;
3514
- }
3515
- function StepRecordsPanel({
3516
- list,
3517
- step,
3518
- credentialSelections,
3519
- credentials,
3520
- clickupListId
3521
- }) {
3522
- const queryClient = useQueryClient();
3523
- const [page, setPage] = useState(1);
3524
- const [approvalByCompanyId, setApprovalByCompanyId] = useState({});
3525
- const navigate = useNavigate();
3526
- const recordsStageKey = step.recordSourceStageKey ?? step.recordsStageKey ?? step.stageKey;
3527
- const recordsEntity = step.recordEntity ?? step.primaryEntity;
3528
- const columns = step.recordColumns?.[recordsEntity] ?? [];
3529
- const recordsQuery = useListRecords(list.id, {
3530
- entity: recordsEntity,
3531
- stage: recordsStageKey,
3532
- limit: RECORDS_PAGE_SIZE,
3533
- offset: (page - 1) * RECORDS_PAGE_SIZE
3534
- });
3535
- const exportWorkflowId = step.action?.resourceId ?? EXPORT_WORKFLOW_ID;
3536
- const exportExecution = useWorkflowExecution({
3537
- workflowId: exportWorkflowId,
3538
- listId: list.id
3539
- });
3540
- useEffect(() => {
3541
- setPage(1);
3542
- setApprovalByCompanyId({});
3543
- }, [step.id]);
3544
- const rows = recordsQuery.data?.data ?? [];
3545
- const total = recordsQuery.data?.total ?? 0;
3546
- const totalPages = Math.max(1, Math.ceil(total / RECORDS_PAGE_SIZE));
3547
- const isReviewExportStep = step.primaryEntity === "company" && (step.actionKey === "lead-gen.export.list" || exportWorkflowId === EXPORT_WORKFLOW_ID);
3548
- const approvedCompanyIds = Object.entries(approvalByCompanyId).filter(([, status]) => status === "approved").map(([companyId]) => companyId);
3549
- const rejectedCompanyIds = Object.entries(approvalByCompanyId).filter(([, status]) => status === "rejected").map(([companyId]) => companyId);
3550
- const clickupRequirement = getClickUpRequirement(step);
3551
- const clickupCredential = clickupRequirement ? getSelectedCredential(credentials, credentialSelections, clickupRequirement) : null;
3552
- const isClickUpExportPath = isReviewExportStep && Boolean(clickupRequirement);
3553
- const normalizedClickupListId = clickupListId.trim();
3554
- const canExport = approvedCompanyIds.length > 0 && (!isClickUpExportPath || Boolean(clickupCredential) && normalizedClickupListId.length > 0);
3555
- const exportDisabledReason = approvedCompanyIds.length === 0 ? "Approve at least one company before export." : isClickUpExportPath && !clickupCredential ? "Select a ClickUp credential in Configuration." : isClickUpExportPath && !normalizedClickupListId ? "Enter a ClickUp List ID in Configuration." : null;
3556
- const setApproval = (companyId, status) => {
3557
- setApprovalByCompanyId((current) => ({ ...current, [companyId]: status }));
3558
- };
3559
- const openRecordDetail = (row) => {
3560
- if (row.entity === "company") {
3561
- void navigate({ to: "/crm/companies/$companyId", params: { companyId: row.companyId } });
3562
- return;
3563
- }
3564
- void navigate({ to: "/crm/contacts/$contactId", params: { contactId: row.contactId } });
3565
- };
3566
- const exportApproved = async () => {
3567
- try {
3568
- const result = await exportExecution.execute({
3569
- input: {
3570
- listId: list.id,
3571
- mode: "export",
3572
- approved: true,
3573
- ...isClickUpExportPath ? {
3574
- destination: "clickup",
3575
- clickupCredential: clickupCredential?.name,
3576
- clickupListId: normalizedClickupListId
3577
- } : {},
3578
- approvedCompanyIds,
3579
- rejectedCompanyIds
3580
- }
3581
- });
3582
- showSuccessNotification(`Export workflow run started: ${result.executionId}`);
3583
- await queryClient.invalidateQueries({ queryKey: acquisitionListKeys.all });
3584
- } catch (error) {
3585
- showApiErrorNotification(error);
3586
- }
3587
- };
3588
- if (!columns.length) {
3589
- return /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: "No record columns are configured for this step." });
3590
- }
3591
- if (recordsQuery.isLoading) {
3592
- return /* @__PURE__ */ jsx(Center, { p: "md", children: /* @__PURE__ */ jsx(Loader, { size: "sm" }) });
3593
- }
3594
- if (recordsQuery.error) {
3595
- return /* @__PURE__ */ jsx(CenteredErrorState, { error: recordsQuery.error, title: "Failed to load records" });
3596
- }
3597
- if (!rows.length) {
3598
- return /* @__PURE__ */ jsx(Stack, { gap: "xs", children: /* @__PURE__ */ jsxs(Text, { size: "sm", c: "dimmed", children: [
3599
- "No records found for ",
3600
- LEAD_GEN_STAGE_CATALOG6[recordsStageKey]?.label ?? recordsStageKey,
3601
- "."
3602
- ] }) });
3603
- }
3604
- return /* @__PURE__ */ jsxs(Stack, { gap: "sm", children: [
3605
- /* @__PURE__ */ jsxs(Group, { justify: "space-between", gap: "xs", children: [
3606
- /* @__PURE__ */ jsxs(Stack, { gap: 2, children: [
3607
- /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", fw: 700, tt: "uppercase", children: "Records" }),
3608
- /* @__PURE__ */ jsxs(Text, { size: "xs", c: "dimmed", children: [
3609
- total,
3610
- " ",
3611
- recordsEntity,
3612
- " records from ",
3613
- LEAD_GEN_STAGE_CATALOG6[recordsStageKey]?.label ?? recordsStageKey
3614
- ] })
3615
- ] }),
3616
- recordsQuery.isFetching ? /* @__PURE__ */ jsx(Loader, { size: "xs" }) : null
3617
- ] }),
3618
- /* @__PURE__ */ jsx(Box, { style: { overflowX: "auto" }, children: /* @__PURE__ */ jsxs(Table, { highlightOnHover: true, children: [
3619
- /* @__PURE__ */ jsx(Table.Thead, { children: /* @__PURE__ */ jsxs(Table.Tr, { children: [
3620
- columns.map((column) => /* @__PURE__ */ jsx(Table.Th, { style: column.width ? { width: column.width } : void 0, children: column.label }, column.key)),
3621
- /* @__PURE__ */ jsx(Table.Th, { children: "Status" }),
3622
- /* @__PURE__ */ jsx(Table.Th, { children: "Failure reason" }),
3623
- isReviewExportStep ? /* @__PURE__ */ jsx(Table.Th, { children: "Approval" }) : null
3624
- ] }) }),
3625
- /* @__PURE__ */ jsx(Table.Tbody, { children: rows.map((row) => {
3626
- const columnSource = getRecordColumnSource(row);
3627
- const status = getRecordStageStatus(row, recordsStageKey);
3628
- const companyId = row.entity === "company" ? row.companyId : null;
3629
- return /* @__PURE__ */ jsxs(Table.Tr, { onClick: () => openRecordDetail(row), style: { cursor: "pointer" }, children: [
3630
- columns.map((column) => /* @__PURE__ */ jsx(Table.Td, { children: renderRecordValue(valueAtPath(columnSource, column.path), column) }, column.key)),
3631
- /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "light", color: getRecordStatusColor(status), children: status }) }),
3632
- /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", lineClamp: 2, children: getStageFailureReason(row, recordsStageKey) }) }),
3633
- isReviewExportStep ? /* @__PURE__ */ jsx(Table.Td, { children: companyId ? /* @__PURE__ */ jsxs(Group, { gap: 4, wrap: "nowrap", "aria-label": `Approval for ${getRecordDisplayName(row)}`, children: [
3634
- /* @__PURE__ */ jsx(
3635
- Button,
3636
- {
3637
- size: "compact-xs",
3638
- variant: approvalByCompanyId[companyId] === "approved" ? "filled" : "light",
3639
- color: "green",
3640
- onClick: (event) => {
3641
- event.stopPropagation();
3642
- setApproval(companyId, "approved");
3643
- },
3644
- children: "Approve"
3645
- }
3646
- ),
3647
- /* @__PURE__ */ jsx(
3648
- Button,
3649
- {
3650
- size: "compact-xs",
3651
- variant: approvalByCompanyId[companyId] === "rejected" ? "filled" : "light",
3652
- color: "red",
3653
- onClick: (event) => {
3654
- event.stopPropagation();
3655
- setApproval(companyId, "rejected");
3656
- },
3657
- children: "Reject"
3658
- }
3659
- )
3660
- ] }) : /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: "-" }) }) : null
3661
- ] }, row.id);
3662
- }) })
3663
- ] }) }),
3664
- /* @__PURE__ */ jsxs(Group, { justify: "space-between", gap: "xs", children: [
3665
- /* @__PURE__ */ jsx(Pagination, { size: "sm", total: totalPages, value: page, onChange: setPage, disabled: recordsQuery.isFetching }),
3666
- isReviewExportStep ? /* @__PURE__ */ jsxs(Group, { gap: "xs", children: [
3667
- /* @__PURE__ */ jsxs(Text, { size: "xs", c: "dimmed", children: [
3668
- approvedCompanyIds.length,
3669
- " approved, ",
3670
- rejectedCompanyIds.length,
3671
- " rejected"
3672
- ] }),
3673
- exportDisabledReason ? /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: exportDisabledReason }) : null,
3674
- /* @__PURE__ */ jsx(
3675
- Button,
3676
- {
3677
- size: "xs",
3678
- leftSection: /* @__PURE__ */ jsx(IconPlayerPlay, { size: 14 }),
3679
- loading: exportExecution.isPending,
3680
- disabled: !canExport,
3681
- onClick: () => void exportApproved(),
3682
- children: isClickUpExportPath ? "Export to ClickUp" : "Export approved"
3683
- }
3684
- )
3685
- ] }) : null
3686
- ] })
3687
- ] });
3688
- }
3689
- function BuildTab({
3690
- list,
3691
- steps,
3692
- recommendedStep,
3693
- onRunStep,
3694
- onRetryStep,
3695
- onViewRunLog
3696
- }) {
3697
- const [selectedStepId, setSelectedStepId] = useState(null);
3698
- const [stepInputFormState, setStepInputFormState] = useState({});
3699
- const [stepInputError, setStepInputError] = useState(null);
3700
- const [stepInputInitialized, setStepInputInitialized] = useState(false);
3701
- const [activeRightColumnTab, setActiveRightColumnTab] = useState("configuration");
3702
- const [credentialSelections, setCredentialSelections] = useState({});
3703
- const [credentialVerificationById, setCredentialVerificationById] = useState({});
3704
- const [clickupListId, setClickupListId] = useState("");
3705
- const [runningResources, setRunningResources] = useState({});
3706
- const [stepInputResetKey, setStepInputResetKey] = useState(0);
3707
- const currentStep = getDisplayStep(steps, recommendedStep);
3708
- const selectedStep = steps.find((step) => step.id === selectedStepId) ?? currentStep;
3709
- const selectedStepIndex = selectedStep ? steps.findIndex((step) => step.id === selectedStep.id) : -1;
3710
- const selectedActionKind = selectedStep?.nextActionKind ?? "none";
3711
- const selectedCapabilityKey = selectedStep?.action?.actionKey ?? selectedStep?.actionKey;
3712
- const selectedResourceId = selectedStep?.action?.resourceId ?? null;
3713
- const stepSchema = selectedStep?.action?.schema;
3714
- const stepLayout = selectedStep?.action?.layout;
3715
- const credentialRequirements = selectedStep?.credentialRequirements ?? [];
3716
- const visibleStepLayout = useMemo(
3717
- () => filterCredentialRequirementFields(stepLayout, credentialRequirements),
3718
- [stepLayout, credentialRequirements]
3719
- );
3720
- const showMainStepConfig = stepSchema && hasMainConfigFields(visibleStepLayout);
3721
- const showAdvancedStepConfig = stepSchema && hasAdvancedConfigFields(visibleStepLayout);
3722
- const recentRunsQuery = useListExecutions(list.id, { resourceId: selectedResourceId, limit: 5 });
3723
- const recentRuns = useMemo(() => recentRunsQuery.data ?? [], [recentRunsQuery.data]);
3724
- const credentialsQuery = useCredentials();
3725
- const credentials = credentialsQuery.data ?? [];
3726
- const verifyCredential = useVerifyCredential();
3727
- const canRunSelectedAction = !!selectedStep?.action && (selectedActionKind === "retry_failed" || selectedActionKind === "run_next_batch");
3728
- const selectedResourceIsRunning = selectedResourceId ? runningResources[selectedResourceId] ?? false : false;
3729
- const selectedActionDisabled = !canRunSelectedAction || selectedResourceIsRunning;
3730
- const selectedActionLabel = selectedResourceIsRunning ? "Running..." : selectedActionKind === "none" ? "No action" : getStepActionLabel(selectedActionKind);
3731
- const selectedActionIcon = selectedActionKind === "retry_failed" ? /* @__PURE__ */ jsx(IconRefresh, { size: 14 }) : /* @__PURE__ */ jsx(IconPlayerPlay, { size: 14 });
3732
- const handleResourceRunningChange = useCallback((resourceId, isRunning) => {
3733
- setRunningResources((current) => {
3734
- if (current[resourceId] === isRunning) return current;
3735
- return { ...current, [resourceId]: isRunning };
3736
- });
3737
- }, []);
3738
- const handleSelectedResourceRunningChange = useCallback(
3739
- (isRunning) => {
3740
- if (!selectedResourceId) return;
3741
- handleResourceRunningChange(selectedResourceId, isRunning);
3742
- },
3743
- [handleResourceRunningChange, selectedResourceId]
3744
- );
3745
- useEffect(() => {
3746
- setStepInputFormState(getInitialStepInput(list, selectedStep?.action));
3747
- setStepInputError(null);
3748
- setStepInputInitialized(false);
3749
- setStepInputResetKey((k) => k + 1);
3750
- }, [list, selectedStep?.action]);
3751
- useEffect(() => {
3752
- if (stepInputInitialized) return;
3753
- if (recentRunsQuery.isLoading) return;
3754
- if (recentRuns.length > 0) {
3755
- const lastInput = getRunInput(recentRuns[0]);
3756
- if (lastInput) {
3757
- setStepInputFormState(sanitizeStepInput(asRecord3(lastInput)));
3758
- setStepInputResetKey((k) => k + 1);
3759
- }
3760
- }
3761
- setStepInputInitialized(true);
3762
- }, [stepInputInitialized, recentRunsQuery.isLoading, recentRuns, selectedCapabilityKey]);
3763
- const handleStepInputChange = (next) => {
3764
- setStepInputFormState(next);
3765
- if (stepInputError) setStepInputError(null);
3766
- };
3767
- const handleCredentialSelect = (requirement, credential) => {
3768
- setCredentialSelections((current) => ({
3769
- ...current,
3770
- [requirement.key]: credential?.id ?? null
3771
- }));
3772
- setStepInputFormState((current) => setValueAtInputPath(current, requirement.inputPath, credential?.name ?? ""));
3773
- if (stepInputError) setStepInputError(null);
3774
- };
3775
- const handleVerifyCredential = (credential) => {
3776
- verifyCredential.mutate(credential.id, {
3777
- onSuccess: (result) => {
3778
- setCredentialVerificationById((current) => ({
3779
- ...current,
3780
- [credential.id]: {
3781
- status: result.ok ? "success" : "error",
3782
- checkedAt: result.checkedAt,
3783
- message: result.message ?? (result.ok ? "Credential verified." : "Credential verification failed."),
3784
- details: result.details
3785
- }
3786
- }));
3787
- },
3788
- onError: (error) => {
3789
- setCredentialVerificationById((current) => ({
3790
- ...current,
3791
- [credential.id]: {
3792
- status: "error",
3793
- message: error instanceof Error ? error.message : "Credential verification failed."
3794
- }
3795
- }));
3796
- }
3797
- });
3798
- };
3799
- const handleClickupListIdChange = (value) => {
3800
- setClickupListId(value);
3801
- setStepInputFormState((current) => setValueAtInputPath(current, "clickupListId", value));
3802
- if (stepInputError) setStepInputError(null);
3803
- };
3804
- const handleSelectedAction = () => {
3805
- if (!selectedStep || selectedActionDisabled) return;
3806
- if (stepSchema) {
3807
- const parsed = stepSchema.safeParse(stepInputFormState);
3808
- if (!parsed.success) {
3809
- const firstIssue = parsed.error.issues[0];
3810
- setStepInputError(firstIssue?.message ?? "Validation failed");
3811
- return;
3812
- }
3813
- }
3814
- setStepInputError(null);
3815
- const submittedInput = stepSchema ? stepInputFormState : {};
3816
- if (selectedActionKind === "retry_failed") {
3817
- onRetryStep(selectedStep, submittedInput);
3818
- return;
3819
- }
3820
- onRunStep(selectedStep, submittedInput);
3821
- };
3822
- const loadRecentRunInput = (run) => {
3823
- setStepInputFormState(sanitizeStepInput(asRecord3(getRunInput(run))));
3824
- setStepInputResetKey((k) => k + 1);
3825
- setActiveRightColumnTab("configuration");
3826
- };
3827
- const openRecentRunLog = (run) => {
3828
- const resourceId = run.resourceId ?? selectedResourceId;
3829
- if (!resourceId) return;
3830
- onViewRunLog(resourceId, run.executionId);
3831
- };
3832
- return /* @__PURE__ */ jsx(TabSection, { icon: /* @__PURE__ */ jsx(IconBolt, { size: 16 }), title: "Build", description: getBuildTabDescription(steps, currentStep), children: /* @__PURE__ */ jsxs(SimpleGrid, { cols: { base: 1, md: 2 }, spacing: "md", children: [
3833
- /* @__PURE__ */ jsx(Card, { withBorder: true, style: { height: "100%", display: "flex", flexDirection: "column" }, children: /* @__PURE__ */ jsxs(Stack, { gap: "xs", style: { flex: 1, minHeight: 0 }, children: [
3834
- steps.map(
3835
- (step) => step.action?.resourceId ? /* @__PURE__ */ jsx(
3836
- BuildStepRunningWatcher,
3837
- {
3838
- resourceId: step.action.resourceId,
3839
- onRunningChange: handleResourceRunningChange
3840
- },
3841
- `running-${step.id}`
3842
- ) : null
3843
- ),
3844
- steps.map((step, index) => {
3845
- const isCurrent = currentStep?.id === step.id;
3846
- const isSelected = selectedStep?.id === step.id;
3847
- const stepResourceId = step.action?.resourceId ?? null;
3848
- const isStepRunning = stepResourceId ? runningResources[stepResourceId] ?? false : false;
3849
- const isWaiting = step.status === "blocked";
3850
- const isPassiveWaiting = isWaiting && !isSelected && !isCurrent;
3851
- const stepTone = isStepRunning ? "ready" : isSelected || isCurrent ? step.status : step.status === "complete" || isWaiting ? step.status : "neutral";
3852
- return /* @__PURE__ */ jsx(
3853
- UnstyledButton,
3854
- {
3855
- onClick: () => setSelectedStepId(step.id),
3856
- style: {
3857
- backgroundColor: isSelected ? "var(--surface-primary-strong)" : isStepRunning ? "color-mix(in srgb, var(--color-primary) 12%, transparent)" : isPassiveWaiting ? "color-mix(in srgb, var(--color-surface-hover) 38%, transparent)" : "transparent",
3858
- border: isSelected ? "var(--active-border)" : isStepRunning ? "1px solid var(--border-primary-muted)" : "1px solid var(--color-border)",
3859
- borderStyle: isPassiveWaiting && !isStepRunning ? "dashed" : "solid",
3860
- borderRadius: 8,
3861
- display: "block",
3862
- opacity: isPassiveWaiting && !isStepRunning ? 0.72 : 1,
3863
- padding: 12,
3864
- textAlign: "left",
3865
- width: "100%"
3866
- },
3867
- children: /* @__PURE__ */ jsxs(Group, { gap: "sm", align: "flex-start", wrap: "nowrap", children: [
3868
- /* @__PURE__ */ jsx(
3869
- Box,
3870
- {
3871
- w: 28,
3872
- h: 28,
3873
- style: {
3874
- alignItems: "center",
3875
- ...getBuildToneStyle(stepTone),
3876
- borderRadius: 999,
3877
- borderStyle: "solid",
3878
- borderWidth: 1,
3879
- display: "flex",
3880
- flexShrink: 0,
3881
- fontWeight: 700,
3882
- justifyContent: "center"
3883
- },
3884
- children: index + 1
3885
- }
3886
- ),
3887
- /* @__PURE__ */ jsxs(Stack, { gap: 4, style: { minWidth: 0, flex: 1 }, children: [
3888
- /* @__PURE__ */ jsxs(Group, { gap: "xs", justify: "space-between", wrap: "nowrap", children: [
3889
- /* @__PURE__ */ jsx(Text, { fw: 700, c: isPassiveWaiting && !isStepRunning ? "dimmed" : void 0, truncate: true, children: step.label }),
3890
- isStepRunning ? /* @__PURE__ */ jsx(StatusPill, { label: "Running", tone: "ready" }) : null,
3891
- !isStepRunning && isCurrent ? /* @__PURE__ */ jsx(StatusPill, { label: "Current", tone: step.status }) : null,
3892
- !isStepRunning && !isCurrent && isWaiting ? /* @__PURE__ */ jsx(StatusPill, { label: "Waiting", tone: "blocked" }) : null
3893
- ] }),
3894
- /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", lineClamp: 2, children: step.description }),
3895
- /* @__PURE__ */ jsx(StepCounterLine, { step })
3896
- ] })
3897
- ] })
3898
- },
3899
- step.id
3900
- );
3901
- })
3902
- ] }) }),
3903
- selectedStep ? /* @__PURE__ */ jsx(Card, { withBorder: true, style: { height: "100%", display: "flex", flexDirection: "column" }, children: /* @__PURE__ */ jsxs(Stack, { gap: "md", style: { flex: 1, minHeight: 0 }, children: [
3904
- /* @__PURE__ */ jsxs(Stack, { gap: 2, style: { minWidth: 0, flexShrink: 0 }, children: [
3905
- /* @__PURE__ */ jsxs(Text, { size: "xs", c: "dimmed", fw: 700, tt: "uppercase", children: [
3906
- "Step ",
3907
- selectedStepIndex + 1,
3908
- " details"
3909
- ] }),
3910
- /* @__PURE__ */ jsx(Title, { order: 5, children: selectedStep.label })
3911
- ] }),
3912
- /* @__PURE__ */ jsx(BuildStepProgressBar, { step: selectedStep }),
3913
- /* @__PURE__ */ jsx(
3914
- StepDetailRightColumn,
3915
- {
3916
- activeTab: activeRightColumnTab,
3917
- onTabChange: setActiveRightColumnTab,
3918
- configuration: /* @__PURE__ */ jsxs(Stack, { gap: "sm", children: [
3919
- selectedResourceId && selectedStep.stageKey ? /* @__PURE__ */ jsx(
3920
- LeadGenLiveStatusPanel,
3921
- {
3922
- listId: list.id,
3923
- resourceId: selectedResourceId,
3924
- stageKey: selectedStep.stageKey,
3925
- stepLabel: selectedStep.label,
3926
- enabled: true,
3927
- onRunningChange: handleSelectedResourceRunningChange
3928
- },
3929
- selectedResourceId
3930
- ) : null,
3931
- selectedStep.status === "blocked" ? /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: selectedStep.emptyBlockedText }) : null,
3932
- selectedStep.outputs.includes(selectedStep.primaryEntity) && selectedStep.ready === 0 && selectedStep.complete === 0 && selectedStep.failed === 0 && selectedStep.blocked === 0 ? /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: "Run this step to populate members." }) : null,
3933
- !selectedStep.outputs.includes(selectedStep.primaryEntity) && selectedStep.recommendedAction?.defaultSize !== void 0 && selectedStep.recommendedAction.maxSize !== void 0 ? /* @__PURE__ */ jsxs(Text, { size: "xs", c: "dimmed", children: [
3934
- "Batch size: ",
3935
- selectedStep.recommendedAction.defaultSize,
3936
- " default,",
3937
- " ",
3938
- selectedStep.recommendedAction.maxSize,
3939
- " max."
3940
- ] }) : null,
3941
- showMainStepConfig && visibleStepLayout ? /* @__PURE__ */ jsx(
3942
- StepConfigForm,
3943
- {
3944
- slot: "main",
3945
- schema: stepSchema,
3946
- layout: visibleStepLayout,
3947
- value: stepInputFormState,
3948
- onChange: (v) => handleStepInputChange(asRecord3(v) ?? {}),
3949
- disabled: !canRunSelectedAction
3950
- },
3951
- `${selectedResourceId ?? "none"}-main-${stepInputResetKey}`
3952
- ) : !credentialRequirements.length ? /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: "No configuration for this step." }) : null,
3953
- /* @__PURE__ */ jsx(
3954
- CredentialRequirementsPanel,
3955
- {
3956
- requirements: credentialRequirements,
3957
- credentials,
3958
- credentialsLoading: credentialsQuery.isLoading,
3959
- selections: credentialSelections,
3960
- verificationByCredentialId: credentialVerificationById,
3961
- verifyingCredentialId: verifyCredential.isPending ? verifyCredential.variables ?? null : null,
3962
- onSelect: handleCredentialSelect,
3963
- onVerify: handleVerifyCredential
3964
- }
3965
- ),
3966
- selectedStep.actionKey === "lead-gen.export.list" || getClickUpRequirement(selectedStep) ? /* @__PURE__ */ jsx(
3967
- TextInput,
3968
- {
3969
- label: "ClickUp List ID",
3970
- description: "Destination List ID for approved company tasks.",
3971
- placeholder: "123456789",
3972
- value: clickupListId,
3973
- onChange: (event) => handleClickupListIdChange(event.currentTarget.value)
3974
- }
3975
- ) : null
3976
- ] }),
3977
- advanced: showAdvancedStepConfig && visibleStepLayout ? /* @__PURE__ */ jsx(
3978
- StepConfigForm,
3979
- {
3980
- slot: "advanced",
3981
- schema: stepSchema,
3982
- layout: visibleStepLayout,
3983
- value: stepInputFormState,
3984
- onChange: (v) => handleStepInputChange(asRecord3(v) ?? {}),
3985
- disabled: !canRunSelectedAction
3986
- },
3987
- `${selectedResourceId ?? "none"}-advanced-${stepInputResetKey}`
3988
- ) : void 0,
3989
- records: /* @__PURE__ */ jsx(
3990
- StepRecordsPanel,
3991
- {
3992
- list,
3993
- step: selectedStep,
3994
- credentialSelections,
3995
- credentials,
3996
- clickupListId
3997
- },
3998
- selectedStep.id
3999
- ),
4000
- runs: selectedResourceId ? /* @__PURE__ */ jsxs(Stack, { gap: "xs", children: [
4001
- /* @__PURE__ */ jsxs(Group, { justify: "space-between", gap: "xs", children: [
4002
- /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", fw: 700, tt: "uppercase", children: "Recent runs" }),
4003
- recentRunsQuery.isFetching ? /* @__PURE__ */ jsx(Loader, { size: "xs" }) : null
4004
- ] }),
4005
- !recentRuns.length && !recentRunsQuery.isLoading ? /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: "No runs yet for this step." }) : /* @__PURE__ */ jsx(Stack, { gap: 4, children: recentRuns.map((run) => {
4006
- const canUseSettings = hasRunInput(run);
4007
- return /* @__PURE__ */ jsx(
4008
- Box,
4009
- {
4010
- style: {
4011
- border: "1px solid var(--color-border)",
4012
- borderRadius: 8,
4013
- padding: "8px 10px",
4014
- width: "100%"
4015
- },
4016
- children: /* @__PURE__ */ jsxs(Group, { justify: "space-between", gap: "xs", wrap: "nowrap", children: [
4017
- /* @__PURE__ */ jsx(
4018
- UnstyledButton,
4019
- {
4020
- onClick: () => openRecentRunLog(run),
4021
- style: { flex: 1, minWidth: 0, textAlign: "left" },
4022
- children: /* @__PURE__ */ jsxs(Group, { justify: "space-between", gap: "xs", wrap: "nowrap", children: [
4023
- /* @__PURE__ */ jsxs(Stack, { gap: 2, style: { minWidth: 0 }, children: [
4024
- /* @__PURE__ */ jsx(Text, { size: "sm", fw: 600, truncate: true, children: formatDateTime4(run.createdAt) }),
4025
- /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", ff: "monospace", truncate: true, children: run.executionId })
4026
- ] }),
4027
- /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "light", color: getRunStatusColor(run.status), children: run.status })
4028
- ] })
4029
- }
4030
- ),
4031
- /* @__PURE__ */ jsx(Tooltip, { label: "Use this run's settings", children: /* @__PURE__ */ jsx(
4032
- Button,
4033
- {
4034
- size: "xs",
4035
- variant: "subtle",
4036
- leftSection: /* @__PURE__ */ jsx(IconSettings, { size: 12 }),
4037
- disabled: !canUseSettings,
4038
- onClick: () => loadRecentRunInput(run),
4039
- children: "Use settings"
4040
- }
4041
- ) })
4042
- ] })
4043
- },
4044
- run.executionId
4045
- );
4046
- }) })
4047
- ] }) : /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: "No runs available for this step." }),
4048
- action: /* @__PURE__ */ jsxs(Fragment, { children: [
4049
- stepInputError ? /* @__PURE__ */ jsx(Alert, { color: "red", icon: /* @__PURE__ */ jsx(IconAlertCircle, { size: 16 }), variant: "light", children: stepInputError }) : null,
4050
- /* @__PURE__ */ jsx(Group, { gap: "xs", justify: "flex-end", wrap: "nowrap", children: /* @__PURE__ */ jsx(
4051
- Button,
4052
- {
4053
- leftSection: selectedActionIcon,
4054
- disabled: selectedActionDisabled,
4055
- onClick: handleSelectedAction,
4056
- children: selectedActionLabel
4057
- }
4058
- ) })
4059
- ] })
4060
- }
4061
- )
4062
- ] }) }) : null
4063
- ] }) });
4064
- }
4065
- function MembersTab({
4066
- listId,
4067
- progress,
4068
- onMemberClick
4069
- }) {
4070
- const [memberTab, setMemberTab] = useState("contacts");
4071
- const contactsQuery = useContacts({ listId, limit: 100, offset: 0 });
4072
- const companiesQuery = useCompanies({ listId, limit: 100, offset: 0 });
4073
- const contacts = contactsQuery.data?.data ?? [];
4074
- const companies = companiesQuery.data?.data ?? [];
4075
- return /* @__PURE__ */ jsxs(
4076
- TabSection,
4077
- {
4078
- icon: /* @__PURE__ */ jsx(IconAddressBook, { size: 16 }),
4079
- title: "Members",
4080
- description: "Contacts and companies attached to this list.",
4081
- rightSection: /* @__PURE__ */ jsx(
4082
- SegmentedControl,
4083
- {
4084
- value: memberTab,
4085
- onChange: (value) => setMemberTab(value),
4086
- size: "xs",
4087
- data: [
4088
- { label: `Members (${progress.totalMembers})`, value: "contacts" },
4089
- { label: `Companies (${progress.totalCompanies})`, value: "companies" }
4090
- ]
4091
- }
4092
- ),
4093
- children: [
4094
- memberTab === "contacts" && /* @__PURE__ */ jsx(Fragment, { children: contactsQuery.isLoading ? /* @__PURE__ */ jsx(Center, { p: "md", children: /* @__PURE__ */ jsx(Loader, { size: "sm" }) }) : !contacts.length ? /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: "No contacts attached to this list yet." }) : /* @__PURE__ */ jsxs(Table, { highlightOnHover: true, children: [
4095
- /* @__PURE__ */ jsx(Table.Thead, { children: /* @__PURE__ */ jsxs(Table.Tr, { children: [
4096
- /* @__PURE__ */ jsx(Table.Th, { children: "Name" }),
4097
- /* @__PURE__ */ jsx(Table.Th, { children: "Email" }),
4098
- /* @__PURE__ */ jsx(Table.Th, { children: "Title" }),
4099
- /* @__PURE__ */ jsx(Table.Th, { children: "Company" }),
4100
- /* @__PURE__ */ jsx(Table.Th, { children: "Status" }),
4101
- /* @__PURE__ */ jsx(Table.Th, { children: "State" }),
4102
- /* @__PURE__ */ jsx(Table.Th, { children: "Created" })
4103
- ] }) }),
4104
- /* @__PURE__ */ jsx(Table.Tbody, { children: contacts.map((contact) => {
4105
- const handleRowClick = () => onMemberClick?.(contact.id, "contact");
4106
- const memberStage = displayMemberStageFor(contact, "contact");
4107
- return /* @__PURE__ */ jsxs(Table.Tr, { onClick: handleRowClick, style: { cursor: "pointer" }, children: [
4108
- /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { fw: 500, children: contactDisplayName(contact.firstName, contact.lastName) }) }),
4109
- /* @__PURE__ */ jsx(Table.Td, { children: contact.email }),
4110
- /* @__PURE__ */ jsx(Table.Td, { children: contact.title ?? "\u2014" }),
4111
- /* @__PURE__ */ jsx(Table.Td, { children: contact.company?.name ?? "\u2014" }),
4112
- /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "light", color: getStatusColor4(contact.status), children: contact.status }) }),
4113
- /* @__PURE__ */ jsx(Table.Td, { children: memberStage ? /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "dot", color: getMemberStateColor2(memberStage.key), children: memberStage.label }) : /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: "\u2014" }) }),
4114
- /* @__PURE__ */ jsx(Table.Td, { children: formatDateTime4(contact.createdAt) })
4115
- ] }, contact.id);
4116
- }) })
4117
- ] }) }),
4118
- memberTab === "companies" && /* @__PURE__ */ jsx(Fragment, { children: companiesQuery.isLoading ? /* @__PURE__ */ jsx(Center, { p: "md", children: /* @__PURE__ */ jsx(Loader, { size: "sm" }) }) : !companies.length ? /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: "No companies attached to this list yet." }) : /* @__PURE__ */ jsxs(Table, { highlightOnHover: true, children: [
4119
- /* @__PURE__ */ jsx(Table.Thead, { children: /* @__PURE__ */ jsxs(Table.Tr, { children: [
4120
- /* @__PURE__ */ jsx(Table.Th, { children: "Name" }),
4121
- /* @__PURE__ */ jsx(Table.Th, { children: "Domain" }),
4122
- /* @__PURE__ */ jsx(Table.Th, { children: "Segment" }),
4123
- /* @__PURE__ */ jsx(Table.Th, { children: "Contacts" }),
4124
- /* @__PURE__ */ jsx(Table.Th, { children: "Status" }),
4125
- /* @__PURE__ */ jsx(Table.Th, { children: "State" }),
4126
- /* @__PURE__ */ jsx(Table.Th, { children: "Created" })
4127
- ] }) }),
4128
- /* @__PURE__ */ jsx(Table.Tbody, { children: companies.map((company) => {
4129
- const handleRowClick = () => onMemberClick?.(company.id, "company");
4130
- const memberStage = displayMemberStageFor(company, "company");
4131
- return /* @__PURE__ */ jsxs(Table.Tr, { onClick: handleRowClick, style: { cursor: "pointer" }, children: [
4132
- /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { fw: 500, children: company.name }) }),
4133
- /* @__PURE__ */ jsx(Table.Td, { children: company.domain ?? "\u2014" }),
4134
- /* @__PURE__ */ jsx(Table.Td, { children: company.segment ?? "\u2014" }),
4135
- /* @__PURE__ */ jsx(Table.Td, { children: company.contactCount }),
4136
- /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "light", color: getStatusColor4(company.status), children: company.status }) }),
4137
- /* @__PURE__ */ jsx(Table.Td, { children: memberStage ? /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "dot", color: getMemberStateColor2(memberStage.key), children: memberStage.label }) : /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: "\u2014" }) }),
4138
- /* @__PURE__ */ jsx(Table.Td, { children: formatDateTime4(company.createdAt) })
4139
- ] }, company.id);
4140
- }) })
4141
- ] }) })
4142
- ]
4143
- }
4144
- );
4145
- }
4146
- function ListDetailHeader({
4147
- title,
4148
- caption,
4149
- rightSection
4150
- }) {
4151
- return /* @__PURE__ */ jsx(PageTitleCaption, { title, caption, rightSection });
4152
- }
4153
- function LeadGenListDetailPage({ listId }) {
4154
- const navigate = useNavigate();
4155
- const actions = useListActions();
4156
- const rawSearch = useSearch({ strict: false });
4157
- const memberId = rawSearch.member ?? null;
4158
- const memberKind = rawSearch.memberKind ?? null;
4159
- const handleMemberClick = (id, kind) => {
4160
- const params = new URLSearchParams(window.location.search);
4161
- params.set("member", id);
4162
- params.set("memberKind", kind);
4163
- window.history.pushState(null, "", `${window.location.pathname}?${params.toString()}`);
4164
- };
4165
- const handleDrawerClose = () => {
4166
- const params = new URLSearchParams(window.location.search);
4167
- params.delete("member");
4168
- params.delete("memberKind");
4169
- const qs = params.toString();
4170
- window.history.pushState(null, "", qs ? `${window.location.pathname}?${qs}` : window.location.pathname);
4171
- };
4172
- const listQuery = useList(listId);
4173
- const progressQuery = useListProgress(listId);
4174
- const executionsQuery = useListExecutions(listId);
4175
- const deleteListMutation = useDeleteList();
4176
- const [deleteModalOpen, setDeleteModalOpen] = useState(false);
4177
- const [runModalOpen, setRunModalOpen] = useState(false);
4178
- const [activeTab, setActiveTab] = useState("overview");
4179
- const [runsResourceFilter, setRunsResourceFilter] = useState(null);
4180
- const [initialRunResourceId, setInitialRunResourceId] = useState(null);
4181
- const [pendingStepRunInput, setPendingStepRunInput] = useState(null);
4182
- const runModalActions = useMemo(() => {
4183
- if (!pendingStepRunInput) return actions;
4184
- return actions.map((action) => {
4185
- if (action.resourceId !== pendingStepRunInput.resourceId) return action;
4186
- return {
4187
- ...action,
4188
- defaultInput: (nextList) => mergeStepRunInput(nextList, action, pendingStepRunInput.inputAtSubmit)
4189
- };
4190
- });
4191
- }, [actions, pendingStepRunInput]);
4192
- const isLoading = listQuery.isLoading || progressQuery.isLoading || executionsQuery.isLoading;
4193
- const error = listQuery.error ?? progressQuery.error ?? executionsQuery.error;
4194
- const backButton = /* @__PURE__ */ jsx(
4195
- Button,
4196
- {
4197
- variant: "light",
4198
- size: "xs",
4199
- leftSection: /* @__PURE__ */ jsx(IconArrowLeft, { size: 16 }),
4200
- onClick: () => navigate({ to: "/lead-gen/lists" }),
4201
- children: "Lists"
4202
- }
4203
- );
4204
- if (isLoading) {
4205
- return /* @__PURE__ */ jsx(SubshellContentContainer, { children: /* @__PURE__ */ jsx(PageContainer, { children: /* @__PURE__ */ jsxs(Stack, { children: [
4206
- /* @__PURE__ */ jsx(
4207
- ListDetailHeader,
4208
- {
4209
- title: "List Detail",
4210
- caption: "Configuration, progress, and execution history for a single lead-gen list",
4211
- rightSection: backButton
4212
- }
4213
- ),
4214
- /* @__PURE__ */ jsx(Paper, { withBorder: true, children: /* @__PURE__ */ jsx(Center, { p: "xl", children: /* @__PURE__ */ jsx(Loader, {}) }) })
4215
- ] }) }) });
4216
- }
4217
- if (error) {
4218
- return /* @__PURE__ */ jsx(SubshellContentContainer, { children: /* @__PURE__ */ jsx(PageContainer, { children: /* @__PURE__ */ jsxs(Stack, { children: [
4219
- /* @__PURE__ */ jsx(
4220
- ListDetailHeader,
4221
- {
4222
- title: "List Detail",
4223
- caption: "Configuration, progress, and execution history for a single lead-gen list",
4224
- rightSection: backButton
4225
- }
4226
- ),
4227
- /* @__PURE__ */ jsx(Paper, { withBorder: true, children: /* @__PURE__ */ jsx(CenteredErrorState, { error, title: "Failed to load list detail" }) })
4228
- ] }) }) });
4229
- }
4230
- if (!listQuery.data || !progressQuery.data) {
4231
- return /* @__PURE__ */ jsx(SubshellContentContainer, { children: /* @__PURE__ */ jsx(PageContainer, { children: /* @__PURE__ */ jsxs(Stack, { children: [
4232
- /* @__PURE__ */ jsx(
4233
- ListDetailHeader,
4234
- {
4235
- title: "List Not Found",
4236
- caption: "The requested lead-gen list is unavailable.",
4237
- rightSection: backButton
4238
- }
4239
- ),
4240
- /* @__PURE__ */ jsx(Paper, { withBorder: true, children: /* @__PURE__ */ jsx(Center, { h: 240, children: /* @__PURE__ */ jsx(Alert, { icon: /* @__PURE__ */ jsx(IconAlertCircle, { size: 16 }), color: "gray", variant: "light", children: "The requested list could not be found." }) }) })
4241
- ] }) }) });
4242
- }
4243
- const list = listQuery.data;
4244
- const progress = progressQuery.data;
4245
- const executions = executionsQuery.data ?? [];
4246
- const businessProgress = deriveBusinessProgress(list, progress, actions, executions);
4247
- const currentBuildTemplateLabel = list.metadata.buildPlanSnapshot?.templateLabel ?? "Default lead generation";
4248
- const buildResolution = resolveBuildState(list, businessProgress, actions);
4249
- const buildSteps = buildResolution.steps;
4250
- const recommendedBuildStep = buildResolution.recommendedStep;
4251
- const openRunModal = (resourceId) => {
4252
- const nextResourceId = resourceId ?? actions[0]?.resourceId ?? null;
4253
- setInitialRunResourceId(nextResourceId);
4254
- setRunModalOpen(true);
4255
- };
4256
- const closeRunModal = () => {
4257
- setRunModalOpen(false);
4258
- setInitialRunResourceId(null);
4259
- setPendingStepRunInput(null);
4260
- };
4261
- const openStepRun = (step, inputAtSubmit) => {
4262
- if (step.action?.schema) {
4263
- setPendingStepRunInput({
4264
- resourceId: step.action.resourceId,
4265
- inputAtSubmit
4266
- });
4267
- } else {
4268
- setPendingStepRunInput(null);
4269
- }
4270
- openRunModal(step.action?.resourceId ?? null);
4271
- };
4272
- const handleCopyListCommand = () => {
4273
- void navigator.clipboard.writeText(`/acquisition --lead-gen list ${list.id}`);
4274
- showSuccessNotification("Copied list command to clipboard");
4275
- };
4276
- const handleDeleteList = () => {
4277
- deleteListMutation.mutate(list.id, {
4278
- onSuccess: () => {
4279
- setDeleteModalOpen(false);
4280
- void navigate({ to: "/lead-gen/lists" });
4281
- }
4282
- });
4283
- };
4284
- return /* @__PURE__ */ jsxs(SubshellContentContainer, { children: [
4285
- /* @__PURE__ */ jsx(PageContainer, { children: /* @__PURE__ */ jsxs(Stack, { children: [
4286
- /* @__PURE__ */ jsx(
4287
- ListDetailHeader,
4288
- {
4289
- title: list.name,
4290
- caption: list.description ?? "Configuration, progress, and execution history for this list"
4291
- }
4292
- ),
4293
- /* @__PURE__ */ jsxs(Group, { justify: "space-between", gap: "xs", wrap: "nowrap", children: [
4294
- /* @__PURE__ */ jsxs(Group, { gap: "xs", children: [
4295
- /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "filled", color: getStatusColor4(list.status), children: list.status }),
4296
- /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "light", color: "violet", children: currentBuildTemplateLabel }),
4297
- /* @__PURE__ */ jsxs(Text, { size: "sm", c: "dimmed", children: [
4298
- "Created ",
4299
- formatDateTime4(list.createdAt)
4300
- ] })
4301
- ] }),
4302
- /* @__PURE__ */ jsxs(Group, { gap: "xs", children: [
4303
- /* @__PURE__ */ jsxs(Group, { gap: 4, wrap: "nowrap", onClick: handleCopyListCommand, style: { cursor: "pointer" }, children: [
4304
- /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", ff: "monospace", children: list.id }),
4305
- /* @__PURE__ */ jsx(ActionIcon, { variant: "subtle", size: "sm", "aria-label": "Copy list command", children: /* @__PURE__ */ jsx(IconCopy, { size: 14 }) })
4306
- ] }),
4307
- /* @__PURE__ */ jsx(Tooltip, { label: "Delete list", children: /* @__PURE__ */ jsx(
4308
- ActionIcon,
4309
- {
4310
- variant: "subtle",
4311
- color: "red",
4312
- size: "sm",
4313
- "aria-label": "Delete list",
4314
- loading: deleteListMutation.isPending,
4315
- onClick: () => setDeleteModalOpen(true),
4316
- children: /* @__PURE__ */ jsx(IconTrash, { size: 14 })
4317
- }
4318
- ) }),
4319
- backButton
4320
- ] })
4321
- ] }),
4322
- /* @__PURE__ */ jsx(Paper, { withBorder: true, children: /* @__PURE__ */ jsxs(Tabs, { value: activeTab, onChange: setActiveTab, children: [
4323
- /* @__PURE__ */ jsxs(Tabs.List, { children: [
4324
- /* @__PURE__ */ jsx(Tabs.Tab, { value: "overview", leftSection: /* @__PURE__ */ jsx(IconBuilding, { size: 14 }), children: "Overview" }),
4325
- /* @__PURE__ */ jsx(Tabs.Tab, { value: "members", leftSection: /* @__PURE__ */ jsx(IconUsers, { size: 14 }), children: "Members" }),
4326
- /* @__PURE__ */ jsx(Tabs.Tab, { value: "build", leftSection: /* @__PURE__ */ jsx(IconBolt, { size: 14 }), children: "Build" }),
4327
- /* @__PURE__ */ jsx(Tabs.Tab, { value: "runs", leftSection: /* @__PURE__ */ jsx(IconPlayerPlay, { size: 14 }), children: "Runs" })
4328
- ] }),
4329
- /* @__PURE__ */ jsx(Tabs.Panel, { value: "overview", pt: "sm", children: /* @__PURE__ */ jsx(OverviewTab, { list, progress: businessProgress }) }),
4330
- /* @__PURE__ */ jsx(Tabs.Panel, { value: "members", pt: "sm", children: /* @__PURE__ */ jsx(MembersTab, { listId, progress: businessProgress, onMemberClick: handleMemberClick }) }),
4331
- /* @__PURE__ */ jsx(Tabs.Panel, { value: "build", pt: "sm", children: /* @__PURE__ */ jsx(
4332
- BuildTab,
4333
- {
4334
- list,
4335
- steps: buildSteps,
4336
- recommendedStep: recommendedBuildStep,
4337
- onRunStep: openStepRun,
4338
- onRetryStep: openStepRun,
4339
- onViewRunLog: (resourceId, executionId) => {
4340
- void navigate({
4341
- to: "/operations/resources/workflow/$workflowId",
4342
- params: { workflowId: resourceId },
4343
- search: { exec: executionId }
4344
- });
4345
- }
4346
- }
4347
- ) }),
4348
- /* @__PURE__ */ jsx(Tabs.Panel, { value: "runs", pt: "sm", children: /* @__PURE__ */ jsx(
4349
- WorkflowRunsPanel,
4350
- {
4351
- listId,
4352
- title: "Runs",
4353
- resourceFilter: runsResourceFilter,
4354
- onClearResourceFilter: () => setRunsResourceFilter(null)
4355
- }
4356
- ) })
4357
- ] }) })
4358
- ] }) }),
4359
- /* @__PURE__ */ jsx(ListMemberDrawer, { memberId, memberKind, listId, onClose: handleDrawerClose }),
4360
- /* @__PURE__ */ jsx(
4361
- CustomModal,
4362
- {
4363
- opened: deleteModalOpen,
4364
- onClose: () => !deleteListMutation.isPending && setDeleteModalOpen(false),
4365
- size: "sm",
4366
- loading: deleteListMutation.isPending,
4367
- children: /* @__PURE__ */ jsxs(Stack, { gap: "md", children: [
4368
- /* @__PURE__ */ jsxs(Group, { gap: "sm", children: [
4369
- /* @__PURE__ */ jsx(IconTrash, { size: 22, color: "var(--color-error)" }),
4370
- /* @__PURE__ */ jsx(Title, { order: 4, children: "Delete list" })
4371
- ] }),
4372
- /* @__PURE__ */ jsxs(Text, { size: "sm", children: [
4373
- "Are you sure you want to delete",
4374
- " ",
4375
- /* @__PURE__ */ jsx(Text, { span: true, fw: 600, children: list.name }),
4376
- "?"
4377
- ] }),
4378
- /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: "This removes the list and its list membership links. Companies and contacts remain in the shared lead-gen database." }),
4379
- /* @__PURE__ */ jsxs(Group, { justify: "flex-end", mt: "md", children: [
4380
- /* @__PURE__ */ jsx(Button, { variant: "light", onClick: () => setDeleteModalOpen(false), disabled: deleteListMutation.isPending, children: "Cancel" }),
4381
- /* @__PURE__ */ jsx(Button, { color: "red", loading: deleteListMutation.isPending, onClick: handleDeleteList, children: "Delete" })
4382
- ] })
4383
- ] })
4384
- }
4385
- ),
4386
- /* @__PURE__ */ jsx(
4387
- RunWorkflowModal,
4388
- {
4389
- opened: runModalOpen,
4390
- onClose: closeRunModal,
4391
- list,
4392
- actions: runModalActions,
4393
- selection: EMPTY_SELECTION2,
4394
- initialResourceId: initialRunResourceId,
4395
- title: "Run build step",
4396
- description: "Run the selected bounded build workflow for this list.",
4397
- lockResourceSelection: true,
4398
- onSubmitted: (_, executionId) => {
4399
- showSuccessNotification(`Workflow run started: ${executionId}`);
4400
- }
4401
- }
4402
- )
4403
- ] });
4404
- }
4405
- var PAGE_SIZE_DEFAULT2 = 20;
4406
- function LeadGenCompaniesPage() {
4407
- const [companySearch, setCompanySearch] = useState("");
4408
- const [segmentFilter, setSegmentFilter] = useState(null);
4409
- const [categoryFilter, setCategoryFilter] = useState(null);
4410
- const [statusFilter, setStatusFilter] = useState(null);
4411
- const [showBatchDelete, setShowBatchDelete] = useState(false);
4412
- const navigate = useNavigate();
4413
- const pagination = usePaginationState(PAGE_SIZE_DEFAULT2, [companySearch, segmentFilter, categoryFilter, statusFilter]);
4414
- const { data: companyFacets } = useCompanyFacets();
4415
- const {
4416
- data: pagedCompanies,
4417
- isLoading: pagedCompaniesLoading,
4418
- error: pagedCompaniesError
4419
- } = useCompanies({
4420
- search: companySearch || void 0,
4421
- segment: segmentFilter || void 0,
4422
- category: categoryFilter || void 0,
4423
- status: statusFilter || void 0,
4424
- limit: PAGE_SIZE_DEFAULT2,
4425
- offset: pagination.offset
4426
- });
4427
- const deleteCompaniesMutation = useDeleteCompanies();
4428
- const companyRows = pagedCompanies?.data ?? [];
4429
- const selection = useTableSelection(companyRows);
4430
- useEffect(() => {
4431
- if ((pagedCompanies?.total ?? 0) === 0) return;
4432
- if (companyRows.length > 0 || pagination.page === 1) return;
4433
- pagination.setPage(pagination.page - 1);
4434
- }, [companyRows.length, pagedCompanies?.total, pagination.page, pagination.setPage]);
4435
- function handleBatchDelete() {
4436
- deleteCompaniesMutation.mutate([...selection.selectedIds], {
4437
- onSuccess: () => {
4438
- selection.clear();
4439
- setShowBatchDelete(false);
4440
- }
4441
- });
4442
- }
4443
- return /* @__PURE__ */ jsx(SubshellContentContainer, { children: /* @__PURE__ */ jsxs(PageContainer, { children: [
4444
- /* @__PURE__ */ jsx(Stack, { children: /* @__PURE__ */ jsx(PageTitleCaption, { title: "Companies", caption: "Company records and enrichment tracking" }) }),
4445
- /* @__PURE__ */ jsx(Paper, { children: /* @__PURE__ */ jsxs(Stack, { children: [
4446
- /* @__PURE__ */ jsxs(
4447
- FilterBar,
4448
- {
4449
- actions: /* @__PURE__ */ jsx(
4450
- TableSelectionToolbar,
4451
- {
4452
- selectedCount: selection.selectedCount,
4453
- onDelete: () => setShowBatchDelete(true),
4454
- isDeleting: deleteCompaniesMutation.isPending
4455
- }
4456
- ),
4457
- children: [
4458
- /* @__PURE__ */ jsx(
4459
- Select,
4460
- {
4461
- placeholder: "All Segments",
4462
- data: companyFacets?.segments ?? [],
4463
- value: segmentFilter,
4464
- onChange: (value) => setSegmentFilter(value),
4465
- style: { minWidth: 180 },
4466
- size: "sm",
4467
- clearable: true
4468
- }
4469
- ),
4470
- /* @__PURE__ */ jsx(
4471
- Select,
4472
- {
4473
- placeholder: "All Categories",
4474
- data: companyFacets?.categories ?? [],
4475
- value: categoryFilter,
4476
- onChange: (value) => setCategoryFilter(value),
4477
- style: { minWidth: 160 },
4478
- size: "sm",
4479
- clearable: true
4480
- }
4481
- ),
4482
- /* @__PURE__ */ jsx(
4483
- Select,
4484
- {
4485
- placeholder: "All Statuses",
4486
- data: (companyFacets?.statuses ?? []).map((status) => ({
4487
- value: status,
4488
- label: status[0].toUpperCase() + status.slice(1)
4489
- })),
4490
- value: statusFilter,
4491
- onChange: (value) => setStatusFilter(value),
4492
- style: { minWidth: 160 },
4493
- size: "sm",
4494
- clearable: true
4495
- }
4496
- ),
4497
- /* @__PURE__ */ jsx(
4498
- TextInput,
4499
- {
4500
- placeholder: "Search by name or domain...",
4501
- leftSection: /* @__PURE__ */ jsx(IconSearch, { size: 16 }),
4502
- style: { minWidth: 250 },
4503
- size: "sm",
4504
- value: companySearch,
4505
- onChange: (e) => setCompanySearch(e.target.value)
4506
- }
4507
- )
4508
- ]
4509
- }
4510
- ),
4511
- pagedCompaniesLoading ? /* @__PURE__ */ jsx(Center, { p: "xl", children: /* @__PURE__ */ jsx(Loader, {}) }) : pagedCompaniesError ? /* @__PURE__ */ jsx(CenteredErrorState, { error: pagedCompaniesError, title: "Failed to load companies" }) : !companyRows.length ? /* @__PURE__ */ jsx(EmptyState, { icon: IconBuildingFactory2, title: "No companies yet", description: "Add one to get started." }) : /* @__PURE__ */ jsxs(Table, { children: [
4512
- /* @__PURE__ */ jsx(Table.Thead, { children: /* @__PURE__ */ jsxs(Table.Tr, { children: [
4513
- /* @__PURE__ */ jsx(Table.Th, { w: 40, children: /* @__PURE__ */ jsx(
4514
- Checkbox,
4515
- {
4516
- checked: selection.isPageAllSelected,
4517
- indeterminate: selection.isPagePartiallySelected,
4518
- onChange: selection.togglePage
4519
- }
4520
- ) }),
4521
- /* @__PURE__ */ jsx(Table.Th, { children: "Company" }),
4522
- /* @__PURE__ */ jsx(Table.Th, { children: "Domain" }),
4523
- /* @__PURE__ */ jsx(Table.Th, { children: "Segment" }),
4524
- /* @__PURE__ */ jsx(Table.Th, { children: "Category" }),
4525
- /* @__PURE__ */ jsx(Table.Th, { children: "Employees" }),
4526
- /* @__PURE__ */ jsx(Table.Th, { children: "Enrichment" }),
4527
- /* @__PURE__ */ jsx(Table.Th, { children: "Contacts" })
4528
- ] }) }),
4529
- /* @__PURE__ */ jsx(Table.Tbody, { children: companyRows.map((company) => {
4530
- const enrichStatus = getEnrichmentStatus(company.enrichmentData);
4531
- return /* @__PURE__ */ jsxs(
4532
- Table.Tr,
4533
- {
4534
- style: { cursor: "pointer" },
4535
- onClick: () => navigate({ to: "/crm/companies/$companyId", params: { companyId: company.id } }),
4536
- children: [
4537
- /* @__PURE__ */ jsx(
4538
- Table.Td,
4539
- {
4540
- onClick: (e) => {
4541
- e.stopPropagation();
4542
- selection.toggle(company.id);
4543
- },
4544
- children: /* @__PURE__ */ jsx(
4545
- Checkbox,
4546
- {
4547
- checked: selection.isSelected(company.id),
4548
- onChange: () => selection.toggle(company.id),
4549
- onClick: (e) => e.stopPropagation()
4550
- }
4551
- )
4552
- }
4553
- ),
4554
- /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { fw: 500, children: company.name }) }),
4555
- /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: company.domain || "-" }) }),
4556
- /* @__PURE__ */ jsx(Table.Td, { children: company.segment || "-" }),
4557
- /* @__PURE__ */ jsx(Table.Td, { children: company.category || "-" }),
4558
- /* @__PURE__ */ jsx(Table.Td, { children: company.numEmployees || "-" }),
4559
- /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Badge, { variant: "light", color: getEnrichmentColor(enrichStatus), size: "sm", children: enrichStatus }) }),
4560
- /* @__PURE__ */ jsx(Table.Td, { children: company.contactCount })
4561
- ]
4562
- },
4563
- company.id
4564
- );
4565
- }) })
4566
- ] }),
4567
- (pagedCompanies?.total ?? 0) > PAGE_SIZE_DEFAULT2 ? /* @__PURE__ */ jsx(Group, { justify: "center", children: /* @__PURE__ */ jsx(
4568
- Pagination,
4569
- {
4570
- value: pagination.page,
4571
- onChange: pagination.setPage,
4572
- total: pagination.totalPages(pagedCompanies?.total ?? 0),
4573
- size: "sm"
4574
- }
4575
- ) }) : null
4576
- ] }) }),
4577
- /* @__PURE__ */ jsx(CustomModal, { opened: showBatchDelete, onClose: () => setShowBatchDelete(false), size: "sm", children: /* @__PURE__ */ jsxs(Stack, { gap: "md", children: [
4578
- /* @__PURE__ */ jsxs(Group, { gap: "sm", children: [
4579
- /* @__PURE__ */ jsx(IconAlertTriangle, { size: 20, color: "var(--mantine-color-red-6)" }),
4580
- /* @__PURE__ */ jsxs(Title, { order: 5, children: [
4581
- "Delete ",
4582
- selection.selectedCount,
4583
- " companies?"
4584
- ] })
4585
- ] }),
4586
- /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: "This will permanently delete the selected companies and cannot be undone." }),
4587
- /* @__PURE__ */ jsxs(Group, { justify: "flex-end", gap: "sm", children: [
4588
- /* @__PURE__ */ jsx(Button, { variant: "default", size: "sm", onClick: () => setShowBatchDelete(false), children: "Cancel" }),
4589
- /* @__PURE__ */ jsx(Button, { color: "red", size: "sm", loading: deleteCompaniesMutation.isPending, onClick: handleBatchDelete, children: "Delete" })
4590
- ] })
4591
- ] }) })
4592
- ] }) });
4593
- }
4594
- var PAGE_SIZE_DEFAULT3 = 20;
4595
- function LeadGenContactsPage() {
4596
- const [contactSearch, setContactSearch] = useState("");
4597
- const [statusFilter, setStatusFilter] = useState(null);
4598
- const [listFilter, setListFilter] = useState(null);
4599
- const [showBatchDelete, setShowBatchDelete] = useState(false);
4600
- const navigate = useNavigate();
4601
- const pagination = usePaginationState(PAGE_SIZE_DEFAULT3, [contactSearch, statusFilter, listFilter]);
4602
- const { data: lists } = useLists();
4603
- const {
4604
- data: contacts,
4605
- isLoading: contactsLoading,
4606
- error: contactsError
4607
- } = useContacts({
4608
- search: contactSearch || void 0,
4609
- contactStatus: statusFilter || void 0,
4610
- listId: listFilter || void 0,
4611
- limit: PAGE_SIZE_DEFAULT3,
4612
- offset: pagination.offset
4613
- });
4614
- const deleteContactsMutation = useDeleteContacts();
4615
- const contactRows = contacts?.data ?? [];
4616
- const selection = useTableSelection(contactRows);
4617
- useEffect(() => {
4618
- if ((contacts?.total ?? 0) === 0) return;
4619
- if (contactRows.length > 0 || pagination.page === 1) return;
4620
- pagination.setPage(pagination.page - 1);
4621
- }, [contactRows.length, contacts?.total, pagination.page, pagination.setPage]);
4622
- function handleBatchDelete() {
4623
- deleteContactsMutation.mutate([...selection.selectedIds], {
4624
- onSuccess: () => {
4625
- selection.clear();
4626
- setShowBatchDelete(false);
4627
- }
4628
- });
4629
- }
4630
- return /* @__PURE__ */ jsx(SubshellContentContainer, { children: /* @__PURE__ */ jsxs(PageContainer, { children: [
4631
- /* @__PURE__ */ jsx(Stack, { children: /* @__PURE__ */ jsx(PageTitleCaption, { title: "Contacts", caption: "Contact records and outreach tracking" }) }),
4632
- /* @__PURE__ */ jsx(Paper, { children: /* @__PURE__ */ jsxs(Stack, { children: [
4633
- /* @__PURE__ */ jsxs(
4634
- FilterBar,
4635
- {
4636
- actions: /* @__PURE__ */ jsx(
4637
- TableSelectionToolbar,
4638
- {
4639
- selectedCount: selection.selectedCount,
4640
- onDelete: () => setShowBatchDelete(true),
4641
- isDeleting: deleteContactsMutation.isPending
4642
- }
4643
- ),
4644
- children: [
4645
- /* @__PURE__ */ jsx(
4646
- Select,
4647
- {
4648
- placeholder: "All Statuses",
4649
- data: [
4650
- { value: "active", label: "Active" },
4651
- { value: "invalid", label: "Invalid" }
4652
- ],
4653
- value: statusFilter,
4654
- onChange: (value) => setStatusFilter(value),
4655
- style: { minWidth: 160 },
4656
- size: "sm",
4657
- clearable: true
4658
- }
4659
- ),
4660
- /* @__PURE__ */ jsx(
4661
- Select,
4662
- {
4663
- placeholder: "All Lists",
4664
- data: (lists ?? []).map((list) => ({ value: list.id, label: list.name })),
4665
- value: listFilter,
4666
- onChange: (value) => setListFilter(value),
4667
- style: { minWidth: 200 },
4668
- size: "sm",
4669
- clearable: true
4670
- }
4671
- ),
4672
- /* @__PURE__ */ jsx(
4673
- TextInput,
4674
- {
4675
- placeholder: "Search by name or email...",
4676
- leftSection: /* @__PURE__ */ jsx(IconSearch, { size: 16 }),
4677
- style: { minWidth: 250 },
4678
- size: "sm",
4679
- value: contactSearch,
4680
- onChange: (e) => setContactSearch(e.target.value)
4681
- }
4682
- )
4683
- ]
4684
- }
4685
- ),
4686
- contactsLoading ? /* @__PURE__ */ jsx(Center, { p: "xl", children: /* @__PURE__ */ jsx(Loader, {}) }) : contactsError ? /* @__PURE__ */ jsx(CenteredErrorState, { error: contactsError, title: "Failed to load contacts" }) : !contactRows.length ? /* @__PURE__ */ jsx(EmptyState, { icon: IconUsers, title: "No contacts yet", description: "Add one to get started." }) : /* @__PURE__ */ jsxs(Table, { children: [
4687
- /* @__PURE__ */ jsx(Table.Thead, { children: /* @__PURE__ */ jsxs(Table.Tr, { children: [
4688
- /* @__PURE__ */ jsx(Table.Th, { w: 40, children: /* @__PURE__ */ jsx(
4689
- Checkbox,
4690
- {
4691
- checked: selection.isPageAllSelected,
4692
- indeterminate: selection.isPagePartiallySelected,
4693
- onChange: selection.togglePage
4694
- }
4695
- ) }),
4696
- /* @__PURE__ */ jsx(Table.Th, { children: "Name" }),
4697
- /* @__PURE__ */ jsx(Table.Th, { children: "Email" }),
4698
- /* @__PURE__ */ jsx(Table.Th, { children: "Company" }),
4699
- /* @__PURE__ */ jsx(Table.Th, { children: "Title" }),
4700
- /* @__PURE__ */ jsx(Table.Th, { children: "Status" }),
4701
- /* @__PURE__ */ jsx(Table.Th, { children: "Personalized" })
4702
- ] }) }),
4703
- /* @__PURE__ */ jsx(Table.Tbody, { children: contactRows.map((contact) => {
4704
- const fullName = [contact.firstName, contact.lastName].filter(Boolean).join(" ") || contact.email;
4705
- const hasPersonalization = !!contact.openingLine;
4706
- return /* @__PURE__ */ jsxs(
4707
- Table.Tr,
4708
- {
4709
- style: { cursor: "pointer" },
4710
- onClick: () => navigate({ to: "/crm/contacts/$contactId", params: { contactId: contact.id } }),
4711
- children: [
4712
- /* @__PURE__ */ jsx(
4713
- Table.Td,
4714
- {
4715
- onClick: (e) => {
4716
- e.stopPropagation();
4717
- selection.toggle(contact.id);
4718
- },
4719
- children: /* @__PURE__ */ jsx(
4720
- Checkbox,
4721
- {
4722
- checked: selection.isSelected(contact.id),
4723
- onChange: () => selection.toggle(contact.id),
4724
- onClick: (e) => e.stopPropagation()
4725
- }
4726
- )
4727
- }
4728
- ),
4729
- /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { fw: 500, children: fullName }) }),
4730
- /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: contact.email }) }),
4731
- /* @__PURE__ */ jsx(Table.Td, { children: contact.company?.name || "-" }),
4732
- /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: contact.title || "-" }) }),
4733
- /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Badge, { variant: "light", color: getStatusColor(contact.status), size: "sm", children: contact.status || "unknown" }) }),
4734
- /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Badge, { variant: "light", color: hasPersonalization ? "green" : "gray", size: "sm", children: hasPersonalization ? "Yes" : "No" }) })
4735
- ]
4736
- },
4737
- contact.id
4738
- );
4739
- }) })
4740
- ] }),
4741
- (contacts?.total ?? 0) > PAGE_SIZE_DEFAULT3 ? /* @__PURE__ */ jsx(Group, { justify: "center", children: /* @__PURE__ */ jsx(
4742
- Pagination,
4743
- {
4744
- value: pagination.page,
4745
- onChange: pagination.setPage,
4746
- total: pagination.totalPages(contacts?.total ?? 0),
4747
- size: "sm"
4748
- }
4749
- ) }) : null
4750
- ] }) }),
4751
- /* @__PURE__ */ jsx(CustomModal, { opened: showBatchDelete, onClose: () => setShowBatchDelete(false), size: "sm", children: /* @__PURE__ */ jsxs(Stack, { gap: "md", children: [
4752
- /* @__PURE__ */ jsxs(Group, { gap: "sm", children: [
4753
- /* @__PURE__ */ jsx(IconAlertTriangle, { size: 20, color: "var(--mantine-color-red-6)" }),
4754
- /* @__PURE__ */ jsxs(Title, { order: 5, children: [
4755
- "Delete ",
4756
- selection.selectedCount,
4757
- " contacts?"
4758
- ] })
4759
- ] }),
4760
- /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: "This will permanently delete the selected contacts and cannot be undone." }),
4761
- /* @__PURE__ */ jsxs(Group, { justify: "flex-end", gap: "sm", children: [
4762
- /* @__PURE__ */ jsx(Button, { variant: "default", size: "sm", onClick: () => setShowBatchDelete(false), children: "Cancel" }),
4763
- /* @__PURE__ */ jsx(Button, { color: "red", size: "sm", loading: deleteContactsMutation.isPending, onClick: handleBatchDelete, children: "Delete" })
4764
- ] })
4765
- ] }) })
4766
- ] }) });
4767
- }
4768
-
4769
- export { LEAD_GEN_ROUTE_LINKS, LeadGenCompaniesPage, LeadGenContactsPage, LeadGenListDetailPage, LeadGenListsPage, LeadGenOverviewPage, LeadGenRouteShell, LeadGenSidebar, LeadGenSidebarTop, ListBuilderIndexPage, ListBuilderPage, ORPHAN_STAGE_ORDER, RunWorkflowModal, StepConfigForm, TabSection, deriveBusinessProgress, formatDate, getEnrichmentColor, getEnrichmentStatus, getStateKeyColor, getStatusColor, getStepActionLabel, leadGenManifest, resolveBuildPlanSteps, resolveBuildState, sortStageKeys, useDeleteLists };