@startsimpli/funnels 0.1.2

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 (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +564 -0
  3. package/dist/client-3ESO2NHy.d.ts +310 -0
  4. package/dist/client-CZu03ACp.d.cts +310 -0
  5. package/dist/components/index.cjs +3243 -0
  6. package/dist/components/index.cjs.map +1 -0
  7. package/dist/components/index.css +198 -0
  8. package/dist/components/index.css.map +1 -0
  9. package/dist/components/index.d.cts +726 -0
  10. package/dist/components/index.d.ts +726 -0
  11. package/dist/components/index.js +3196 -0
  12. package/dist/components/index.js.map +1 -0
  13. package/dist/core/index.cjs +500 -0
  14. package/dist/core/index.cjs.map +1 -0
  15. package/dist/core/index.d.cts +359 -0
  16. package/dist/core/index.d.ts +359 -0
  17. package/dist/core/index.js +486 -0
  18. package/dist/core/index.js.map +1 -0
  19. package/dist/hooks/index.cjs +21 -0
  20. package/dist/hooks/index.cjs.map +1 -0
  21. package/dist/hooks/index.d.cts +11 -0
  22. package/dist/hooks/index.d.ts +11 -0
  23. package/dist/hooks/index.js +19 -0
  24. package/dist/hooks/index.js.map +1 -0
  25. package/dist/index-BGDEXbuz.d.cts +434 -0
  26. package/dist/index-BGDEXbuz.d.ts +434 -0
  27. package/dist/index.cjs +4499 -0
  28. package/dist/index.cjs.map +1 -0
  29. package/dist/index.css +198 -0
  30. package/dist/index.css.map +1 -0
  31. package/dist/index.d.cts +99 -0
  32. package/dist/index.d.ts +99 -0
  33. package/dist/index.js +4421 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/store/index.cjs +391 -0
  36. package/dist/store/index.cjs.map +1 -0
  37. package/dist/store/index.d.cts +225 -0
  38. package/dist/store/index.d.ts +225 -0
  39. package/dist/store/index.js +388 -0
  40. package/dist/store/index.js.map +1 -0
  41. package/package.json +122 -0
package/dist/index.cjs ADDED
@@ -0,0 +1,4499 @@
1
+ 'use strict';
2
+
3
+ var zustand = require('zustand');
4
+ var react = require('react');
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+ var react$1 = require('@xyflow/react');
7
+ require('@xyflow/react/dist/style.css');
8
+ var core = require('@dnd-kit/core');
9
+ var sortable = require('@dnd-kit/sortable');
10
+ var utilities = require('@dnd-kit/utilities');
11
+
12
+ // src/types/index.ts
13
+ function isFunnel(value) {
14
+ const f = value;
15
+ return typeof f === "object" && f !== null && typeof f.id === "string" && typeof f.name === "string" && ["draft", "active", "paused", "archived"].includes(f.status) && Array.isArray(f.stages);
16
+ }
17
+ function isStage(value) {
18
+ const s = value;
19
+ return typeof s === "object" && s !== null && typeof s.id === "string" && typeof s.name === "string" && typeof s.order === "number" && ["AND", "OR"].includes(s.filter_logic) && Array.isArray(s.rules);
20
+ }
21
+ function isFilterRule(value) {
22
+ const r = value;
23
+ return typeof r === "object" && r !== null && typeof r.field_path === "string" && typeof r.operator === "string" && r.value !== void 0;
24
+ }
25
+ function isFunnelRun(value) {
26
+ const r = value;
27
+ return typeof r === "object" && r !== null && typeof r.id === "string" && typeof r.funnel_id === "string" && ["pending", "running", "completed", "failed", "cancelled"].includes(r.status);
28
+ }
29
+ function isFunnelResult(value) {
30
+ const r = value;
31
+ return typeof r === "object" && r !== null && r.entity !== void 0 && typeof r.matched === "boolean" && Array.isArray(r.accumulated_tags);
32
+ }
33
+ function isFieldDefinition(value) {
34
+ const f = value;
35
+ return typeof f === "object" && f !== null && typeof f.name === "string" && typeof f.label === "string" && typeof f.type === "string" && Array.isArray(f.operators);
36
+ }
37
+ function getValidOperators(fieldType) {
38
+ switch (fieldType) {
39
+ case "string":
40
+ return [
41
+ "eq",
42
+ "ne",
43
+ "contains",
44
+ "not_contains",
45
+ "startswith",
46
+ "endswith",
47
+ "matches",
48
+ "in",
49
+ "not_in",
50
+ "isnull",
51
+ "isnotnull"
52
+ ];
53
+ case "number":
54
+ return [
55
+ "eq",
56
+ "ne",
57
+ "gt",
58
+ "lt",
59
+ "gte",
60
+ "lte",
61
+ "in",
62
+ "not_in",
63
+ "isnull",
64
+ "isnotnull"
65
+ ];
66
+ case "boolean":
67
+ return ["eq", "ne", "is_true", "is_false", "isnull", "isnotnull"];
68
+ case "date":
69
+ return [
70
+ "eq",
71
+ "ne",
72
+ "gt",
73
+ "lt",
74
+ "gte",
75
+ "lte",
76
+ "isnull",
77
+ "isnotnull"
78
+ ];
79
+ case "array":
80
+ return [
81
+ "in",
82
+ "not_in",
83
+ "has_any",
84
+ "has_all",
85
+ "isnull",
86
+ "isnotnull"
87
+ ];
88
+ case "tag":
89
+ return ["has_tag", "not_has_tag"];
90
+ case "object":
91
+ return ["isnull", "isnotnull"];
92
+ case "any":
93
+ default:
94
+ return [
95
+ "eq",
96
+ "ne",
97
+ "gt",
98
+ "lt",
99
+ "gte",
100
+ "lte",
101
+ "contains",
102
+ "not_contains",
103
+ "startswith",
104
+ "endswith",
105
+ "in",
106
+ "not_in",
107
+ "isnull",
108
+ "isnotnull"
109
+ ];
110
+ }
111
+ }
112
+ function isValidOperator(operator, fieldType) {
113
+ const validOps = getValidOperators(fieldType);
114
+ return validOps.includes(operator);
115
+ }
116
+ function validateFilterRule(rule) {
117
+ const errors = [];
118
+ if (!rule.field_path) {
119
+ errors.push("field_path is required");
120
+ }
121
+ if (!rule.operator) {
122
+ errors.push("operator is required");
123
+ }
124
+ const nullOps = ["isnull", "isnotnull", "is_true", "is_false"];
125
+ if (!nullOps.includes(rule.operator) && rule.value === void 0) {
126
+ errors.push(`value is required for operator '${rule.operator}'`);
127
+ }
128
+ return errors;
129
+ }
130
+ function validateStage(stage) {
131
+ const errors = [];
132
+ if (!stage.name) {
133
+ errors.push("name is required");
134
+ }
135
+ if (typeof stage.order !== "number") {
136
+ errors.push("order must be a number");
137
+ }
138
+ if (!["AND", "OR"].includes(stage.filter_logic)) {
139
+ errors.push("filter_logic must be AND or OR");
140
+ }
141
+ if (!Array.isArray(stage.rules)) {
142
+ errors.push("rules must be an array");
143
+ } else {
144
+ stage.rules.forEach((rule, i) => {
145
+ const ruleErrors = validateFilterRule(rule);
146
+ ruleErrors.forEach((err) => errors.push(`rules[${i}]: ${err}`));
147
+ });
148
+ }
149
+ return errors;
150
+ }
151
+ function validateFunnel(funnel) {
152
+ const errors = [];
153
+ if (!funnel.name) {
154
+ errors.push("name is required");
155
+ }
156
+ if (!["draft", "active", "paused", "archived"].includes(funnel.status)) {
157
+ errors.push("status must be draft, active, paused, or archived");
158
+ }
159
+ if (!Array.isArray(funnel.stages)) {
160
+ errors.push("stages must be an array");
161
+ } else {
162
+ funnel.stages.forEach((stage, i) => {
163
+ const stageErrors = validateStage(stage);
164
+ stageErrors.forEach((err) => errors.push(`stages[${i}]: ${err}`));
165
+ });
166
+ const orders = funnel.stages.map((s) => s.order).sort((a, b) => a - b);
167
+ const expectedOrders = Array.from({ length: orders.length }, (_, i) => i);
168
+ if (JSON.stringify(orders) !== JSON.stringify(expectedOrders)) {
169
+ errors.push("stage orders must be sequential starting from 0");
170
+ }
171
+ }
172
+ return errors;
173
+ }
174
+
175
+ // src/core/field-resolver.ts
176
+ var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
177
+ function parseFieldPath(fieldPath) {
178
+ const segments = [];
179
+ let current = "";
180
+ let inBracket = false;
181
+ for (let i = 0; i < fieldPath.length; i++) {
182
+ const char = fieldPath[i];
183
+ if (char === "[") {
184
+ if (current) {
185
+ segments.push(current);
186
+ current = "";
187
+ }
188
+ inBracket = true;
189
+ } else if (char === "]") {
190
+ if (current) {
191
+ segments.push(current);
192
+ current = "";
193
+ }
194
+ inBracket = false;
195
+ } else if (char === "." && !inBracket) {
196
+ if (current) {
197
+ segments.push(current);
198
+ current = "";
199
+ }
200
+ } else {
201
+ current += char;
202
+ }
203
+ }
204
+ if (current) {
205
+ segments.push(current);
206
+ }
207
+ return segments;
208
+ }
209
+ function resolveField(entity, fieldPath) {
210
+ if (entity == null) {
211
+ return void 0;
212
+ }
213
+ if (!fieldPath || fieldPath.trim() === "") {
214
+ return void 0;
215
+ }
216
+ const segments = parseFieldPath(fieldPath);
217
+ let current = entity;
218
+ for (const segment of segments) {
219
+ if (current == null) {
220
+ return void 0;
221
+ }
222
+ if (DANGEROUS_KEYS.has(segment)) {
223
+ return void 0;
224
+ }
225
+ if (typeof current === "object" && segment in current) {
226
+ current = current[segment];
227
+ } else {
228
+ return void 0;
229
+ }
230
+ }
231
+ return current;
232
+ }
233
+ function setField(entity, fieldPath, value) {
234
+ if (entity == null) {
235
+ throw new Error("Cannot set field on null or undefined entity");
236
+ }
237
+ if (!fieldPath || fieldPath.trim() === "") {
238
+ throw new Error("Field path cannot be empty");
239
+ }
240
+ const segments = parseFieldPath(fieldPath);
241
+ let current = entity;
242
+ for (let i = 0; i < segments.length - 1; i++) {
243
+ const segment = segments[i];
244
+ const nextSegment = segments[i + 1];
245
+ if (DANGEROUS_KEYS.has(segment)) {
246
+ throw new Error(`Dangerous field path segment: "${segment}"`);
247
+ }
248
+ if (!(segment in current)) {
249
+ const isNextArray = /^\d+$/.test(nextSegment);
250
+ current[segment] = isNextArray ? [] : {};
251
+ }
252
+ current = current[segment];
253
+ }
254
+ const lastSegment = segments[segments.length - 1];
255
+ if (DANGEROUS_KEYS.has(lastSegment)) {
256
+ throw new Error(`Dangerous field path segment: "${lastSegment}"`);
257
+ }
258
+ current[lastSegment] = value;
259
+ }
260
+ function hasField(entity, fieldPath) {
261
+ const value = resolveField(entity, fieldPath);
262
+ return value !== void 0;
263
+ }
264
+ function getFields(entity, fieldPaths) {
265
+ const result = {};
266
+ for (const path of fieldPaths) {
267
+ result[path] = resolveField(entity, path);
268
+ }
269
+ return result;
270
+ }
271
+
272
+ // src/core/operators.ts
273
+ function applyOperator(operator, actual, expected) {
274
+ switch (operator) {
275
+ // ========================================================================
276
+ // Equality
277
+ // ========================================================================
278
+ case "eq":
279
+ return compareValues(actual, expected, (a, e) => a === e);
280
+ case "ne":
281
+ return compareValues(actual, expected, (a, e) => a !== e);
282
+ // ========================================================================
283
+ // Comparison (numbers, dates, strings)
284
+ // ========================================================================
285
+ case "gt":
286
+ return compareValues(actual, expected, (a, e) => a > e);
287
+ case "lt":
288
+ return compareValues(actual, expected, (a, e) => a < e);
289
+ case "gte":
290
+ return compareValues(actual, expected, (a, e) => a >= e);
291
+ case "lte":
292
+ return compareValues(actual, expected, (a, e) => a <= e);
293
+ // ========================================================================
294
+ // String operations (case-insensitive)
295
+ // ========================================================================
296
+ case "contains":
297
+ if (actual == null) return false;
298
+ if (expected == null) return false;
299
+ return String(actual).toLowerCase().includes(String(expected).toLowerCase());
300
+ case "not_contains":
301
+ if (actual == null) return true;
302
+ if (expected == null) return true;
303
+ return !String(actual).toLowerCase().includes(String(expected).toLowerCase());
304
+ case "startswith":
305
+ if (actual == null) return false;
306
+ if (expected == null) return false;
307
+ return String(actual).toLowerCase().startsWith(String(expected).toLowerCase());
308
+ case "endswith":
309
+ if (actual == null) return false;
310
+ if (expected == null) return false;
311
+ return String(actual).toLowerCase().endsWith(String(expected).toLowerCase());
312
+ case "matches":
313
+ if (actual == null) return false;
314
+ if (expected == null) return false;
315
+ try {
316
+ const regex = new RegExp(String(expected));
317
+ return regex.test(String(actual));
318
+ } catch {
319
+ return false;
320
+ }
321
+ // ========================================================================
322
+ // Array/Set operations
323
+ // ========================================================================
324
+ case "in":
325
+ if (!Array.isArray(expected)) return false;
326
+ return expected.some((item) => compareValues(actual, item, (a, e) => a === e));
327
+ case "not_in":
328
+ if (!Array.isArray(expected)) return true;
329
+ return !expected.some((item) => compareValues(actual, item, (a, e) => a === e));
330
+ case "has_any":
331
+ if (!Array.isArray(actual)) return false;
332
+ if (!Array.isArray(expected)) return false;
333
+ return expected.some(
334
+ (expectedItem) => actual.some((actualItem) => compareValues(actualItem, expectedItem, (a, e) => a === e))
335
+ );
336
+ case "has_all":
337
+ if (!Array.isArray(actual)) return false;
338
+ if (!Array.isArray(expected)) return false;
339
+ return expected.every(
340
+ (expectedItem) => actual.some((actualItem) => compareValues(actualItem, expectedItem, (a, e) => a === e))
341
+ );
342
+ // ========================================================================
343
+ // Null checks
344
+ // ========================================================================
345
+ case "isnull":
346
+ return actual === null || actual === void 0;
347
+ case "isnotnull":
348
+ return actual !== null && actual !== void 0;
349
+ // ========================================================================
350
+ // Tag operations (tags are arrays of strings)
351
+ // ========================================================================
352
+ case "has_tag":
353
+ if (!Array.isArray(actual)) return false;
354
+ if (expected == null) return false;
355
+ const expectedTag = String(expected).toLowerCase();
356
+ return actual.some((tag) => String(tag).toLowerCase() === expectedTag);
357
+ case "not_has_tag":
358
+ if (!Array.isArray(actual)) return true;
359
+ if (expected == null) return true;
360
+ const expectedTagNot = String(expected).toLowerCase();
361
+ return !actual.some((tag) => String(tag).toLowerCase() === expectedTagNot);
362
+ // ========================================================================
363
+ // Boolean
364
+ // ========================================================================
365
+ case "is_true":
366
+ return actual === true;
367
+ case "is_false":
368
+ return actual === false;
369
+ default:
370
+ throw new Error(`Unknown operator: ${operator}`);
371
+ }
372
+ }
373
+ function compareValues(actual, expected, compare) {
374
+ if (actual === null || actual === void 0) {
375
+ return expected === null || expected === void 0;
376
+ }
377
+ if (expected === null || expected === void 0) {
378
+ return false;
379
+ }
380
+ if (isDate(actual) && isDate(expected)) {
381
+ const actualTs = toTimestamp(actual);
382
+ const expectedTs = toTimestamp(expected);
383
+ if (actualTs === null || expectedTs === null) return false;
384
+ return compare(actualTs, expectedTs);
385
+ }
386
+ if (isDate(actual) || isDate(expected)) {
387
+ const actualTs = toTimestamp(actual);
388
+ const expectedTs = toTimestamp(expected);
389
+ if (actualTs === null || expectedTs === null) return false;
390
+ return compare(actualTs, expectedTs);
391
+ }
392
+ if (typeof actual === "number" && typeof expected === "number") {
393
+ return compare(actual, expected);
394
+ }
395
+ if (isNumeric(actual) && isNumeric(expected)) {
396
+ return compare(Number(actual), Number(expected));
397
+ }
398
+ return compare(actual, expected);
399
+ }
400
+ function isDate(value) {
401
+ if (value instanceof Date) return true;
402
+ if (typeof value !== "string") return false;
403
+ const datePattern = /^\d{4}-\d{2}-\d{2}|^\d{1,2}\/\d{1,2}\/\d{4}/;
404
+ return datePattern.test(value);
405
+ }
406
+ function toTimestamp(value) {
407
+ if (value instanceof Date) {
408
+ const ts = value.getTime();
409
+ return isNaN(ts) ? null : ts;
410
+ }
411
+ if (typeof value === "string" || typeof value === "number") {
412
+ const date = new Date(value);
413
+ const ts = date.getTime();
414
+ return isNaN(ts) ? null : ts;
415
+ }
416
+ return null;
417
+ }
418
+ function isNumeric(value) {
419
+ if (typeof value === "number") return !isNaN(value);
420
+ if (typeof value !== "string") return false;
421
+ return !isNaN(Number(value)) && value.trim() !== "";
422
+ }
423
+
424
+ // src/core/evaluator.ts
425
+ function evaluateRule(entity, rule) {
426
+ const actualValue = resolveField(entity, rule.field_path);
427
+ const result = applyOperator(rule.operator, actualValue, rule.value);
428
+ return rule.negate ? !result : result;
429
+ }
430
+ function evaluateRuleWithResult(entity, rule) {
431
+ try {
432
+ const actualValue = resolveField(entity, rule.field_path);
433
+ const operatorResult = applyOperator(rule.operator, actualValue, rule.value);
434
+ const matched = rule.negate ? !operatorResult : operatorResult;
435
+ return {
436
+ field_path: rule.field_path,
437
+ operator: rule.operator,
438
+ value: rule.value,
439
+ actual_value: actualValue,
440
+ matched
441
+ };
442
+ } catch (error) {
443
+ return {
444
+ field_path: rule.field_path,
445
+ operator: rule.operator,
446
+ value: rule.value,
447
+ actual_value: void 0,
448
+ matched: false,
449
+ error: error instanceof Error ? error.message : String(error)
450
+ };
451
+ }
452
+ }
453
+ function evaluateRulesAND(entity, rules) {
454
+ if (!rules || rules.length === 0) return true;
455
+ return rules.every((rule) => evaluateRule(entity, rule));
456
+ }
457
+ function evaluateRulesOR(entity, rules) {
458
+ if (!rules || rules.length === 0) return true;
459
+ return rules.some((rule) => evaluateRule(entity, rule));
460
+ }
461
+ function evaluateRules(entity, rules, logic = "AND") {
462
+ return logic === "AND" ? evaluateRulesAND(entity, rules) : evaluateRulesOR(entity, rules);
463
+ }
464
+ function evaluateRulesWithResults(entity, rules, logic = "AND") {
465
+ const ruleResults = rules.map((rule) => evaluateRuleWithResult(entity, rule));
466
+ const matched = logic === "AND" ? ruleResults.every((r) => r.matched) : ruleResults.some((r) => r.matched);
467
+ return {
468
+ matched,
469
+ logic,
470
+ rule_results: ruleResults
471
+ };
472
+ }
473
+ function filterEntities(entities, rules, logic = "AND") {
474
+ if (!entities || entities.length === 0) return [];
475
+ if (!rules || rules.length === 0) return entities;
476
+ return entities.filter((entity) => evaluateRules(entity, rules, logic));
477
+ }
478
+
479
+ // src/core/engine.ts
480
+ function evaluateRule2(_entity, _rule) {
481
+ throw new Error("Not implemented - BEAD: fund-your-startup-a0b8. evaluateRule must import from rule evaluator.");
482
+ }
483
+ var FunnelEngine = class {
484
+ /**
485
+ * Execute a funnel on a set of entities
486
+ *
487
+ * @param funnel - The funnel definition to execute
488
+ * @param entities - Input entities to process
489
+ * @returns ExecutionResult with matched/excluded entities and stats
490
+ */
491
+ execute(funnel, entities) {
492
+ const startTime = Date.now();
493
+ const results = entities.map((entity) => ({
494
+ entity,
495
+ matched: true,
496
+ // Start as matched, exclude as needed
497
+ accumulated_tags: [],
498
+ context: {},
499
+ stage_results: []
500
+ }));
501
+ const stageStats = {};
502
+ const errors = [];
503
+ const sortedStages = [...funnel.stages].sort((a, b) => a.order - b.order);
504
+ for (const stage of sortedStages) {
505
+ const stageStartTime = Date.now();
506
+ const inputEntities = results.filter((r) => r.matched && !r.excluded_at_stage);
507
+ const stats = {
508
+ stage_id: stage.id,
509
+ stage_name: stage.name,
510
+ input_count: inputEntities.length,
511
+ matched_count: 0,
512
+ not_matched_count: 0,
513
+ excluded_count: 0,
514
+ tagged_count: 0,
515
+ continued_count: 0,
516
+ error_count: 0
517
+ };
518
+ for (const result of inputEntities) {
519
+ try {
520
+ const stageResult = this.processStage(stage, result.entity);
521
+ result.stage_results.push(stageResult);
522
+ if (stageResult.matched) {
523
+ stats.matched_count++;
524
+ } else {
525
+ stats.not_matched_count++;
526
+ }
527
+ if (stageResult.tags_added && stageResult.tags_added.length > 0) {
528
+ result.accumulated_tags.push(...stageResult.tags_added);
529
+ stats.tagged_count++;
530
+ }
531
+ if (stageResult.context_added) {
532
+ result.context = { ...result.context, ...stageResult.context_added };
533
+ }
534
+ if (stageResult.excluded) {
535
+ result.matched = false;
536
+ result.excluded_at_stage = stage.id;
537
+ stats.excluded_count++;
538
+ } else if (stageResult.continued) {
539
+ stats.continued_count++;
540
+ }
541
+ } catch (error) {
542
+ stats.error_count++;
543
+ errors.push(`Stage ${stage.name}: ${error instanceof Error ? error.message : String(error)}`);
544
+ }
545
+ }
546
+ stats.duration_ms = Date.now() - stageStartTime;
547
+ stageStats[stage.id] = stats;
548
+ }
549
+ if (funnel.completion_tags && funnel.completion_tags.length > 0) {
550
+ for (const result of results) {
551
+ if (result.matched) {
552
+ result.accumulated_tags.push(...funnel.completion_tags);
553
+ }
554
+ }
555
+ }
556
+ const matched = results.filter((r) => r.matched);
557
+ const excluded = results.filter((r) => !r.matched);
558
+ const totalTagged = results.filter((r) => r.accumulated_tags.length > 0).length;
559
+ return {
560
+ matched,
561
+ excluded,
562
+ total_input: entities.length,
563
+ total_matched: matched.length,
564
+ total_excluded: excluded.length,
565
+ total_tagged: totalTagged,
566
+ stage_stats: stageStats,
567
+ duration_ms: Date.now() - startTime,
568
+ errors: errors.length > 0 ? errors : void 0
569
+ };
570
+ }
571
+ /**
572
+ * Process a single entity through a stage
573
+ *
574
+ * @param stage - The stage to process
575
+ * @param entity - The entity to evaluate
576
+ * @returns StageResult with match status and actions taken
577
+ */
578
+ processStage(stage, entity) {
579
+ const ruleResults = [];
580
+ let matched = false;
581
+ if (stage.custom_evaluator) {
582
+ try {
583
+ matched = stage.custom_evaluator(entity);
584
+ } catch (error) {
585
+ matched = false;
586
+ }
587
+ } else if (stage.rules.length === 0) {
588
+ matched = true;
589
+ } else {
590
+ for (const rule of stage.rules) {
591
+ const ruleResult = evaluateRule2();
592
+ ruleResults.push(ruleResult);
593
+ }
594
+ if (stage.filter_logic === "AND") {
595
+ matched = ruleResults.every((r) => r.matched);
596
+ } else {
597
+ matched = ruleResults.some((r) => r.matched);
598
+ }
599
+ }
600
+ let action;
601
+ let tagsAdded = [];
602
+ let contextAdded;
603
+ let excluded = false;
604
+ let continued = false;
605
+ if (matched) {
606
+ action = stage.match_action;
607
+ if (stage.match_tags && stage.match_tags.length > 0) {
608
+ tagsAdded = [...stage.match_tags];
609
+ }
610
+ if (stage.match_context) {
611
+ contextAdded = stage.match_context;
612
+ }
613
+ switch (stage.match_action) {
614
+ case "continue":
615
+ continued = true;
616
+ break;
617
+ case "tag":
618
+ excluded = true;
619
+ break;
620
+ case "tag_continue":
621
+ continued = true;
622
+ break;
623
+ case "output":
624
+ continued = false;
625
+ break;
626
+ }
627
+ } else {
628
+ action = stage.no_match_action;
629
+ if (stage.no_match_tags && stage.no_match_tags.length > 0) {
630
+ tagsAdded = [...stage.no_match_tags];
631
+ }
632
+ switch (stage.no_match_action) {
633
+ case "continue":
634
+ continued = true;
635
+ break;
636
+ case "exclude":
637
+ excluded = true;
638
+ break;
639
+ case "tag_exclude":
640
+ excluded = true;
641
+ break;
642
+ }
643
+ }
644
+ return {
645
+ stage_id: stage.id,
646
+ stage_name: stage.name,
647
+ matched,
648
+ rule_results: ruleResults.length > 0 ? ruleResults : void 0,
649
+ action,
650
+ tags_added: tagsAdded.length > 0 ? tagsAdded : void 0,
651
+ context_added: contextAdded,
652
+ excluded,
653
+ continued
654
+ };
655
+ }
656
+ };
657
+
658
+ // src/api/adapter.ts
659
+ function createApiError(message, status, response, cause) {
660
+ const error = new Error(message);
661
+ error.name = "ApiError";
662
+ error.status = status;
663
+ error.response = response;
664
+ error.cause = cause;
665
+ if (response?.code) {
666
+ error.code = response.code;
667
+ }
668
+ return error;
669
+ }
670
+ function isApiError(error) {
671
+ return error instanceof Error && error.name === "ApiError";
672
+ }
673
+
674
+ // src/api/default-adapter.ts
675
+ var FetchAdapter = class {
676
+ constructor(config = {}) {
677
+ this.config = {
678
+ headers: config.headers || {},
679
+ timeout: config.timeout || 3e4,
680
+ parseResponse: config.parseResponse || ((res) => res.json()),
681
+ onError: config.onError || (() => {
682
+ })
683
+ };
684
+ }
685
+ /**
686
+ * Build fetch options
687
+ */
688
+ buildOptions(method, data, params) {
689
+ const options = {
690
+ method,
691
+ headers: {
692
+ "Content-Type": "application/json",
693
+ ...this.config.headers
694
+ }
695
+ };
696
+ if (data !== void 0) {
697
+ options.body = JSON.stringify(data);
698
+ }
699
+ return options;
700
+ }
701
+ /**
702
+ * Build URL with query params
703
+ */
704
+ buildUrl(url, params) {
705
+ if (!params || Object.keys(params).length === 0) {
706
+ return url;
707
+ }
708
+ const searchParams = new URLSearchParams();
709
+ Object.entries(params).forEach(([key, value]) => {
710
+ if (value !== void 0 && value !== null) {
711
+ if (Array.isArray(value)) {
712
+ value.forEach((v) => searchParams.append(key, String(v)));
713
+ } else {
714
+ searchParams.append(key, String(value));
715
+ }
716
+ }
717
+ });
718
+ const queryString = searchParams.toString();
719
+ return queryString ? `${url}?${queryString}` : url;
720
+ }
721
+ /**
722
+ * Fetch with timeout
723
+ */
724
+ async fetchWithTimeout(url, options) {
725
+ const controller = new AbortController();
726
+ const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
727
+ try {
728
+ const response = await fetch(url, {
729
+ ...options,
730
+ signal: controller.signal
731
+ });
732
+ clearTimeout(timeoutId);
733
+ return response;
734
+ } catch (error) {
735
+ clearTimeout(timeoutId);
736
+ if (error instanceof Error && error.name === "AbortError") {
737
+ throw createApiError(
738
+ `Request timeout after ${this.config.timeout}ms`,
739
+ void 0,
740
+ void 0,
741
+ error
742
+ );
743
+ }
744
+ throw error;
745
+ }
746
+ }
747
+ /**
748
+ * Handle response
749
+ */
750
+ async handleResponse(response) {
751
+ if (response.ok) {
752
+ if (response.status === 204) {
753
+ return void 0;
754
+ }
755
+ try {
756
+ return await this.config.parseResponse(response);
757
+ } catch (error2) {
758
+ throw createApiError(
759
+ "Failed to parse response",
760
+ response.status,
761
+ void 0,
762
+ error2
763
+ );
764
+ }
765
+ }
766
+ let errorBody;
767
+ try {
768
+ errorBody = await response.json();
769
+ } catch {
770
+ errorBody = { detail: response.statusText };
771
+ }
772
+ const error = createApiError(
773
+ errorBody.detail || errorBody.message || `HTTP ${response.status}`,
774
+ response.status,
775
+ errorBody
776
+ );
777
+ this.config.onError(error);
778
+ throw error;
779
+ }
780
+ /**
781
+ * Execute request
782
+ */
783
+ async request(method, url, data, params) {
784
+ const fullUrl = this.buildUrl(url, params);
785
+ const options = this.buildOptions(method, data, params);
786
+ try {
787
+ const response = await this.fetchWithTimeout(fullUrl, options);
788
+ return await this.handleResponse(response);
789
+ } catch (error) {
790
+ if (error.name === "ApiError") {
791
+ throw error;
792
+ }
793
+ const apiError = createApiError(
794
+ "Network request failed",
795
+ void 0,
796
+ void 0,
797
+ error
798
+ );
799
+ this.config.onError(apiError);
800
+ throw apiError;
801
+ }
802
+ }
803
+ /**
804
+ * HTTP GET
805
+ */
806
+ async get(url, params) {
807
+ return this.request("GET", url, void 0, params);
808
+ }
809
+ /**
810
+ * HTTP POST
811
+ */
812
+ async post(url, data) {
813
+ return this.request("POST", url, data);
814
+ }
815
+ /**
816
+ * HTTP PATCH
817
+ */
818
+ async patch(url, data) {
819
+ return this.request("PATCH", url, data);
820
+ }
821
+ /**
822
+ * HTTP DELETE
823
+ */
824
+ async delete(url) {
825
+ return this.request("DELETE", url);
826
+ }
827
+ };
828
+
829
+ // src/api/client.ts
830
+ var FunnelApiClient = class {
831
+ constructor(adapter, baseUrl) {
832
+ this.adapter = adapter;
833
+ this.baseUrl = baseUrl;
834
+ this.baseUrl = baseUrl.replace(/\/$/, "");
835
+ }
836
+ /**
837
+ * Build full URL for endpoint
838
+ */
839
+ url(path) {
840
+ return `${this.baseUrl}${path}`;
841
+ }
842
+ // ============================================================================
843
+ // Funnel CRUD
844
+ // ============================================================================
845
+ /**
846
+ * List funnels with optional filters
847
+ *
848
+ * @param filters - Optional filters (status, owner, pagination, etc)
849
+ * @returns Paginated list of funnels
850
+ */
851
+ async listFunnels(filters) {
852
+ return this.adapter.get(
853
+ this.url("/api/v1/funnels/"),
854
+ filters
855
+ );
856
+ }
857
+ /**
858
+ * Get single funnel by ID
859
+ *
860
+ * @param id - Funnel ID
861
+ * @returns Funnel detail
862
+ * @throws ApiError with status 404 if not found
863
+ */
864
+ async getFunnel(id) {
865
+ return this.adapter.get(this.url(`/api/v1/funnels/${id}/`));
866
+ }
867
+ /**
868
+ * Create new funnel
869
+ *
870
+ * @param data - Funnel creation data
871
+ * @returns Created funnel
872
+ * @throws ApiError with status 400 if validation fails
873
+ */
874
+ async createFunnel(data) {
875
+ return this.adapter.post(this.url("/api/v1/funnels/"), data);
876
+ }
877
+ /**
878
+ * Update existing funnel
879
+ *
880
+ * @param id - Funnel ID
881
+ * @param data - Funnel update data
882
+ * @returns Updated funnel
883
+ * @throws ApiError with status 404 if not found, 400 if validation fails
884
+ */
885
+ async updateFunnel(id, data) {
886
+ return this.adapter.patch(
887
+ this.url(`/api/v1/funnels/${id}/`),
888
+ data
889
+ );
890
+ }
891
+ /**
892
+ * Delete funnel
893
+ *
894
+ * @param id - Funnel ID
895
+ * @throws ApiError with status 404 if not found
896
+ */
897
+ async deleteFunnel(id) {
898
+ return this.adapter.delete(this.url(`/api/v1/funnels/${id}/`));
899
+ }
900
+ // ============================================================================
901
+ // Stage CRUD
902
+ // ============================================================================
903
+ /**
904
+ * Create stage in funnel
905
+ *
906
+ * @param funnelId - Funnel ID
907
+ * @param data - Stage creation data
908
+ * @returns Created stage
909
+ * @throws ApiError with status 404 if funnel not found, 400 if validation fails
910
+ */
911
+ async createStage(funnelId, data) {
912
+ return this.adapter.post(
913
+ this.url(`/api/v1/funnels/${funnelId}/stages/`),
914
+ data
915
+ );
916
+ }
917
+ /**
918
+ * Update stage
919
+ *
920
+ * @param funnelId - Funnel ID
921
+ * @param stageId - Stage ID
922
+ * @param data - Stage update data
923
+ * @returns Updated stage
924
+ * @throws ApiError with status 404 if not found, 400 if validation fails
925
+ */
926
+ async updateStage(funnelId, stageId, data) {
927
+ return this.adapter.patch(
928
+ this.url(`/api/v1/funnels/${funnelId}/stages/${stageId}/`),
929
+ data
930
+ );
931
+ }
932
+ /**
933
+ * Delete stage
934
+ *
935
+ * @param funnelId - Funnel ID
936
+ * @param stageId - Stage ID
937
+ * @throws ApiError with status 404 if not found
938
+ */
939
+ async deleteStage(funnelId, stageId) {
940
+ return this.adapter.delete(
941
+ this.url(`/api/v1/funnels/${funnelId}/stages/${stageId}/`)
942
+ );
943
+ }
944
+ // ============================================================================
945
+ // Run Operations
946
+ // ============================================================================
947
+ /**
948
+ * Trigger funnel run
949
+ *
950
+ * @param funnelId - Funnel ID
951
+ * @param options - Optional run configuration (trigger_type, metadata, etc)
952
+ * @returns Created funnel run (status: pending or running)
953
+ * @throws ApiError with status 404 if funnel not found, 400 if validation fails
954
+ */
955
+ async runFunnel(funnelId, options) {
956
+ return this.adapter.post(
957
+ this.url(`/api/v1/funnels/${funnelId}/run/`),
958
+ options || {}
959
+ );
960
+ }
961
+ /**
962
+ * Get funnel run history
963
+ *
964
+ * @param funnelId - Funnel ID
965
+ * @param filters - Optional filters (status, pagination, etc)
966
+ * @returns List of funnel runs
967
+ * @throws ApiError with status 404 if funnel not found
968
+ */
969
+ async getFunnelRuns(funnelId, filters) {
970
+ return this.adapter.get(
971
+ this.url(`/api/v1/funnels/${funnelId}/runs/`),
972
+ filters
973
+ );
974
+ }
975
+ /**
976
+ * Get single run detail
977
+ *
978
+ * @param runId - Run ID
979
+ * @returns Funnel run detail
980
+ * @throws ApiError with status 404 if not found
981
+ */
982
+ async getFunnelRun(runId) {
983
+ return this.adapter.get(this.url(`/api/v1/funnel-runs/${runId}/`));
984
+ }
985
+ /**
986
+ * Get run results (entities that were processed)
987
+ *
988
+ * @param runId - Run ID
989
+ * @param filters - Optional filters (matched, pagination, etc)
990
+ * @returns Paginated list of results
991
+ * @throws ApiError with status 404 if run not found
992
+ */
993
+ async getFunnelResults(runId, filters) {
994
+ return this.adapter.get(
995
+ this.url(`/api/v1/funnel-runs/${runId}/results/`),
996
+ filters
997
+ );
998
+ }
999
+ /**
1000
+ * Cancel running funnel
1001
+ *
1002
+ * @param runId - Run ID
1003
+ * @returns Updated run with status 'cancelled'
1004
+ * @throws ApiError with status 404 if not found, 400 if already completed
1005
+ */
1006
+ async cancelFunnelRun(runId) {
1007
+ return this.adapter.post(
1008
+ this.url(`/api/v1/funnel-runs/${runId}/cancel/`),
1009
+ {}
1010
+ );
1011
+ }
1012
+ // ============================================================================
1013
+ // Client-Side Preview (Local Evaluation)
1014
+ // ============================================================================
1015
+ /**
1016
+ * Preview funnel with sample entities (client-side evaluation)
1017
+ *
1018
+ * Useful for testing funnel logic before running on full dataset.
1019
+ * Does NOT hit the server - evaluates locally.
1020
+ *
1021
+ * Note: This requires the evaluation engine to be available client-side.
1022
+ * If not available, this will throw an error.
1023
+ *
1024
+ * @param funnel - Funnel definition
1025
+ * @param sampleEntities - Sample entities to test
1026
+ * @returns Preview results showing which entities would match/exclude
1027
+ */
1028
+ async previewFunnel(funnel, sampleEntities) {
1029
+ throw new Error(
1030
+ "Client-side preview requires evaluation engine. Use server-side preview endpoint instead: POST /api/v1/funnels/{id}/preview/"
1031
+ );
1032
+ }
1033
+ /**
1034
+ * Server-side preview (recommended)
1035
+ *
1036
+ * Send sample entities to server for evaluation.
1037
+ * Useful for testing funnel logic before running on full dataset.
1038
+ *
1039
+ * @param funnelId - Funnel ID
1040
+ * @param sampleEntities - Sample entities to test
1041
+ * @returns Preview results
1042
+ * @throws ApiError with status 404 if funnel not found
1043
+ */
1044
+ async previewFunnelServer(funnelId, sampleEntities) {
1045
+ return this.adapter.post(
1046
+ this.url(`/api/v1/funnels/${funnelId}/preview/`),
1047
+ { entities: sampleEntities }
1048
+ );
1049
+ }
1050
+ };
1051
+
1052
+ // src/store/types.ts
1053
+ var createInitialState = () => ({
1054
+ funnels: [],
1055
+ selectedFunnel: null,
1056
+ selectedStage: null,
1057
+ runs: [],
1058
+ pagination: {
1059
+ count: 0,
1060
+ next: null,
1061
+ previous: null,
1062
+ currentPage: 1,
1063
+ pageSize: 20
1064
+ },
1065
+ isLoading: false,
1066
+ error: null,
1067
+ isDirty: false,
1068
+ rollbackState: null
1069
+ });
1070
+
1071
+ // src/store/create-funnel-store.ts
1072
+ function createFunnelStore(apiClient) {
1073
+ return zustand.create((set, get) => ({
1074
+ // Initialize state
1075
+ ...createInitialState(),
1076
+ // =========================================================================
1077
+ // Funnel Actions
1078
+ // =========================================================================
1079
+ loadFunnels: async (filters) => {
1080
+ set({ isLoading: true, error: null });
1081
+ try {
1082
+ const response = await apiClient.listFunnels(filters);
1083
+ set({
1084
+ funnels: response.results,
1085
+ pagination: {
1086
+ count: response.count,
1087
+ next: response.next,
1088
+ previous: response.previous,
1089
+ currentPage: filters?.page || 1,
1090
+ pageSize: filters?.page_size || 20
1091
+ },
1092
+ isLoading: false
1093
+ });
1094
+ } catch (error) {
1095
+ set({
1096
+ error,
1097
+ isLoading: false
1098
+ });
1099
+ throw error;
1100
+ }
1101
+ },
1102
+ selectFunnel: (id) => {
1103
+ const { funnels } = get();
1104
+ if (id === null) {
1105
+ set({ selectedFunnel: null, selectedStage: null });
1106
+ return;
1107
+ }
1108
+ const funnel = funnels.find((f) => f.id === id);
1109
+ set({
1110
+ selectedFunnel: funnel || null,
1111
+ selectedStage: null
1112
+ // Clear stage selection when changing funnels
1113
+ });
1114
+ },
1115
+ createFunnel: async (data) => {
1116
+ set({ isLoading: true, error: null });
1117
+ try {
1118
+ const funnel = await apiClient.createFunnel(data);
1119
+ set((state) => ({
1120
+ funnels: [...state.funnels, funnel],
1121
+ pagination: {
1122
+ ...state.pagination,
1123
+ count: state.pagination.count + 1
1124
+ },
1125
+ isLoading: false
1126
+ }));
1127
+ return funnel;
1128
+ } catch (error) {
1129
+ set({
1130
+ error,
1131
+ isLoading: false
1132
+ });
1133
+ throw error;
1134
+ }
1135
+ },
1136
+ updateFunnel: async (id, data) => {
1137
+ get()._saveRollbackState();
1138
+ set((state) => ({
1139
+ funnels: state.funnels.map(
1140
+ (f) => f.id === id ? { ...f, ...data } : f
1141
+ ),
1142
+ selectedFunnel: state.selectedFunnel?.id === id ? { ...state.selectedFunnel, ...data } : state.selectedFunnel,
1143
+ isDirty: false
1144
+ // Clear dirty flag on save
1145
+ }));
1146
+ try {
1147
+ const updated = await apiClient.updateFunnel(id, data);
1148
+ set((state) => ({
1149
+ funnels: state.funnels.map((f) => f.id === id ? updated : f),
1150
+ selectedFunnel: state.selectedFunnel?.id === id ? updated : state.selectedFunnel
1151
+ }));
1152
+ get()._clearRollback();
1153
+ return updated;
1154
+ } catch (error) {
1155
+ get()._rollback();
1156
+ set({ error });
1157
+ throw error;
1158
+ }
1159
+ },
1160
+ deleteFunnel: async (id) => {
1161
+ get()._saveRollbackState();
1162
+ set((state) => ({
1163
+ funnels: state.funnels.filter((f) => f.id !== id),
1164
+ selectedFunnel: state.selectedFunnel?.id === id ? null : state.selectedFunnel,
1165
+ pagination: {
1166
+ ...state.pagination,
1167
+ count: state.pagination.count - 1
1168
+ }
1169
+ }));
1170
+ try {
1171
+ await apiClient.deleteFunnel(id);
1172
+ get()._clearRollback();
1173
+ } catch (error) {
1174
+ get()._rollback();
1175
+ set({ error });
1176
+ throw error;
1177
+ }
1178
+ },
1179
+ duplicateFunnel: async (id) => {
1180
+ const { funnels } = get();
1181
+ const funnel = funnels.find((f) => f.id === id);
1182
+ if (!funnel) {
1183
+ throw new Error(`Funnel ${id} not found`);
1184
+ }
1185
+ const copy = {
1186
+ name: `${funnel.name} (Copy)`,
1187
+ description: funnel.description,
1188
+ status: "draft",
1189
+ // Always create as draft
1190
+ input_type: funnel.input_type,
1191
+ stages: funnel.stages.map((stage, index) => ({
1192
+ ...stage,
1193
+ order: index
1194
+ // Preserve order
1195
+ })),
1196
+ completion_tags: funnel.completion_tags,
1197
+ metadata: funnel.metadata
1198
+ };
1199
+ return get().createFunnel(copy);
1200
+ },
1201
+ // =========================================================================
1202
+ // Stage Actions
1203
+ // =========================================================================
1204
+ selectStage: (stageId) => {
1205
+ const { selectedFunnel } = get();
1206
+ if (!selectedFunnel) {
1207
+ set({ selectedStage: null });
1208
+ return;
1209
+ }
1210
+ if (stageId === null) {
1211
+ set({ selectedStage: null });
1212
+ return;
1213
+ }
1214
+ const stage = selectedFunnel.stages.find((s) => s.id === stageId);
1215
+ set({ selectedStage: stage || null });
1216
+ },
1217
+ createStage: async (funnelId, data) => {
1218
+ set({ isLoading: true, error: null });
1219
+ try {
1220
+ const stage = await apiClient.createStage(funnelId, data);
1221
+ set((state) => ({
1222
+ funnels: state.funnels.map(
1223
+ (f) => f.id === funnelId ? { ...f, stages: [...f.stages, stage] } : f
1224
+ ),
1225
+ selectedFunnel: state.selectedFunnel?.id === funnelId ? { ...state.selectedFunnel, stages: [...state.selectedFunnel.stages, stage] } : state.selectedFunnel,
1226
+ isLoading: false
1227
+ }));
1228
+ return stage;
1229
+ } catch (error) {
1230
+ set({
1231
+ error,
1232
+ isLoading: false
1233
+ });
1234
+ throw error;
1235
+ }
1236
+ },
1237
+ updateStage: async (funnelId, stageId, data) => {
1238
+ get()._saveRollbackState();
1239
+ set((state) => ({
1240
+ funnels: state.funnels.map(
1241
+ (f) => f.id === funnelId ? {
1242
+ ...f,
1243
+ stages: f.stages.map(
1244
+ (s) => s.id === stageId ? { ...s, ...data } : s
1245
+ )
1246
+ } : f
1247
+ ),
1248
+ selectedFunnel: state.selectedFunnel?.id === funnelId ? {
1249
+ ...state.selectedFunnel,
1250
+ stages: state.selectedFunnel.stages.map(
1251
+ (s) => s.id === stageId ? { ...s, ...data } : s
1252
+ )
1253
+ } : state.selectedFunnel,
1254
+ selectedStage: state.selectedStage?.id === stageId ? { ...state.selectedStage, ...data } : state.selectedStage,
1255
+ isDirty: false
1256
+ }));
1257
+ try {
1258
+ const updated = await apiClient.updateStage(
1259
+ funnelId,
1260
+ stageId,
1261
+ data
1262
+ );
1263
+ set((state) => ({
1264
+ funnels: state.funnels.map(
1265
+ (f) => f.id === funnelId ? {
1266
+ ...f,
1267
+ stages: f.stages.map((s) => s.id === stageId ? updated : s)
1268
+ } : f
1269
+ ),
1270
+ selectedFunnel: state.selectedFunnel?.id === funnelId ? {
1271
+ ...state.selectedFunnel,
1272
+ stages: state.selectedFunnel.stages.map(
1273
+ (s) => s.id === stageId ? updated : s
1274
+ )
1275
+ } : state.selectedFunnel,
1276
+ selectedStage: state.selectedStage?.id === stageId ? updated : state.selectedStage
1277
+ }));
1278
+ get()._clearRollback();
1279
+ return updated;
1280
+ } catch (error) {
1281
+ get()._rollback();
1282
+ set({ error });
1283
+ throw error;
1284
+ }
1285
+ },
1286
+ deleteStage: async (funnelId, stageId) => {
1287
+ get()._saveRollbackState();
1288
+ set((state) => ({
1289
+ funnels: state.funnels.map(
1290
+ (f) => f.id === funnelId ? {
1291
+ ...f,
1292
+ stages: f.stages.filter((s) => s.id !== stageId)
1293
+ } : f
1294
+ ),
1295
+ selectedFunnel: state.selectedFunnel?.id === funnelId ? {
1296
+ ...state.selectedFunnel,
1297
+ stages: state.selectedFunnel.stages.filter((s) => s.id !== stageId)
1298
+ } : state.selectedFunnel,
1299
+ selectedStage: state.selectedStage?.id === stageId ? null : state.selectedStage
1300
+ }));
1301
+ try {
1302
+ await apiClient.deleteStage(funnelId, stageId);
1303
+ get()._clearRollback();
1304
+ } catch (error) {
1305
+ get()._rollback();
1306
+ set({ error });
1307
+ throw error;
1308
+ }
1309
+ },
1310
+ reorderStages: async (funnelId, stageIds) => {
1311
+ const { selectedFunnel } = get();
1312
+ if (!selectedFunnel || selectedFunnel.id !== funnelId) {
1313
+ throw new Error("Funnel must be selected to reorder stages");
1314
+ }
1315
+ get()._saveRollbackState();
1316
+ const reorderedStages = stageIds.map((id, index) => {
1317
+ const stage = selectedFunnel.stages.find((s) => s.id === id);
1318
+ return stage ? { ...stage, order: index } : null;
1319
+ }).filter((s) => s !== null);
1320
+ set((state) => ({
1321
+ funnels: state.funnels.map(
1322
+ (f) => f.id === funnelId ? { ...f, stages: reorderedStages } : f
1323
+ ),
1324
+ selectedFunnel: { ...selectedFunnel, stages: reorderedStages },
1325
+ isDirty: false
1326
+ }));
1327
+ try {
1328
+ await Promise.all(
1329
+ reorderedStages.map(
1330
+ (stage) => apiClient.updateStage(funnelId, stage.id, { order: stage.order })
1331
+ )
1332
+ );
1333
+ get()._clearRollback();
1334
+ } catch (error) {
1335
+ get()._rollback();
1336
+ set({ error });
1337
+ throw error;
1338
+ }
1339
+ },
1340
+ // =========================================================================
1341
+ // Run Actions
1342
+ // =========================================================================
1343
+ runFunnel: async (id, options) => {
1344
+ set({ isLoading: true, error: null });
1345
+ try {
1346
+ const run = await apiClient.runFunnel(id, options);
1347
+ set((state) => ({
1348
+ runs: [run, ...state.runs],
1349
+ isLoading: false
1350
+ }));
1351
+ return run;
1352
+ } catch (error) {
1353
+ set({
1354
+ error,
1355
+ isLoading: false
1356
+ });
1357
+ throw error;
1358
+ }
1359
+ },
1360
+ loadRuns: async (funnelId, filters) => {
1361
+ set({ isLoading: true, error: null });
1362
+ try {
1363
+ const response = await apiClient.getFunnelRuns(funnelId, filters);
1364
+ set({
1365
+ runs: response.results,
1366
+ isLoading: false
1367
+ });
1368
+ } catch (error) {
1369
+ set({
1370
+ error,
1371
+ isLoading: false
1372
+ });
1373
+ throw error;
1374
+ }
1375
+ },
1376
+ cancelRun: async (runId) => {
1377
+ set({ isLoading: true, error: null });
1378
+ try {
1379
+ const run = await apiClient.cancelFunnelRun(runId);
1380
+ set((state) => ({
1381
+ runs: state.runs.map((r) => r.id === runId ? run : r),
1382
+ isLoading: false
1383
+ }));
1384
+ return run;
1385
+ } catch (error) {
1386
+ set({
1387
+ error,
1388
+ isLoading: false
1389
+ });
1390
+ throw error;
1391
+ }
1392
+ },
1393
+ // =========================================================================
1394
+ // UI State Actions
1395
+ // =========================================================================
1396
+ setDirty: (dirty) => {
1397
+ set({ isDirty: dirty });
1398
+ },
1399
+ clearError: () => {
1400
+ set({ error: null });
1401
+ },
1402
+ reset: () => {
1403
+ set(createInitialState());
1404
+ },
1405
+ // =========================================================================
1406
+ // Internal Actions (Optimistic Updates)
1407
+ // =========================================================================
1408
+ _saveRollbackState: () => {
1409
+ const { funnels, selectedFunnel } = get();
1410
+ set({
1411
+ rollbackState: {
1412
+ funnels: JSON.parse(JSON.stringify(funnels)),
1413
+ selectedFunnel: selectedFunnel ? JSON.parse(JSON.stringify(selectedFunnel)) : null
1414
+ }
1415
+ });
1416
+ },
1417
+ _rollback: () => {
1418
+ const { rollbackState } = get();
1419
+ if (rollbackState) {
1420
+ set({
1421
+ funnels: rollbackState.funnels,
1422
+ selectedFunnel: rollbackState.selectedFunnel,
1423
+ rollbackState: null
1424
+ });
1425
+ }
1426
+ },
1427
+ _clearRollback: () => {
1428
+ set({ rollbackState: null });
1429
+ }
1430
+ }));
1431
+ }
1432
+ function useDebouncedValue(value, delay = 300) {
1433
+ const [debouncedValue, setDebouncedValue] = react.useState(value);
1434
+ react.useEffect(() => {
1435
+ const handler = setTimeout(() => {
1436
+ setDebouncedValue(value);
1437
+ }, delay);
1438
+ return () => {
1439
+ clearTimeout(handler);
1440
+ };
1441
+ }, [value, delay]);
1442
+ return debouncedValue;
1443
+ }
1444
+ function PreviewStats({
1445
+ totalMatched,
1446
+ totalExcluded,
1447
+ matchPercentage,
1448
+ className = ""
1449
+ }) {
1450
+ const total = totalMatched + totalExcluded;
1451
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `space-y-2 ${className}`, children: [
1452
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative h-8 bg-gray-200 rounded-lg overflow-hidden", children: [
1453
+ /* @__PURE__ */ jsxRuntime.jsx(
1454
+ "div",
1455
+ {
1456
+ className: "absolute inset-y-0 left-0 bg-gradient-to-r from-green-500 to-green-600 transition-all duration-300 flex items-center justify-center",
1457
+ style: { width: `${matchPercentage}%` },
1458
+ role: "progressbar",
1459
+ "aria-valuenow": matchPercentage,
1460
+ "aria-valuemin": 0,
1461
+ "aria-valuemax": 100,
1462
+ "aria-label": `${totalMatched} of ${total} matched (${matchPercentage}%)`,
1463
+ children: matchPercentage > 15 && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs font-semibold text-white", children: [
1464
+ totalMatched.toLocaleString(),
1465
+ "/",
1466
+ total.toLocaleString(),
1467
+ " (",
1468
+ matchPercentage,
1469
+ "%)"
1470
+ ] })
1471
+ }
1472
+ ),
1473
+ matchPercentage <= 15 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs font-semibold text-gray-600", children: [
1474
+ totalMatched.toLocaleString(),
1475
+ "/",
1476
+ total.toLocaleString(),
1477
+ " (",
1478
+ matchPercentage,
1479
+ "%)"
1480
+ ] }) })
1481
+ ] }),
1482
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between text-sm", children: [
1483
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
1484
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-3 h-3 rounded-sm bg-green-500" }),
1485
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-medium text-gray-700", children: [
1486
+ totalMatched.toLocaleString(),
1487
+ " Matched"
1488
+ ] })
1489
+ ] }),
1490
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
1491
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-3 h-3 rounded-sm bg-gray-300" }),
1492
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-medium text-gray-700", children: [
1493
+ totalExcluded.toLocaleString(),
1494
+ " Excluded"
1495
+ ] })
1496
+ ] })
1497
+ ] })
1498
+ ] });
1499
+ }
1500
+ function StageBreakdown({
1501
+ stageStats,
1502
+ stages,
1503
+ className = ""
1504
+ }) {
1505
+ const sortedStages = [...stages].sort((a, b) => a.order - b.order);
1506
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, children: [
1507
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-sm font-semibold text-gray-700 mb-3", children: "Stage Breakdown" }),
1508
+ /* @__PURE__ */ jsxRuntime.jsx("ol", { className: "space-y-2", children: sortedStages.map((stage, index) => {
1509
+ const stats = stageStats[stage.id];
1510
+ if (!stats) return null;
1511
+ const isLast = index === sortedStages.length - 1;
1512
+ const excludedCount = stats.excluded_count;
1513
+ const remainingCount = stats.remaining_count;
1514
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1515
+ "li",
1516
+ {
1517
+ className: "flex items-center justify-between py-2 px-3 bg-gray-50 rounded-lg",
1518
+ children: [
1519
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 flex-1 min-w-0", children: [
1520
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-shrink-0 w-6 h-6 flex items-center justify-center bg-blue-100 text-blue-700 rounded-full text-xs font-bold", children: index + 1 }),
1521
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium text-gray-900 truncate", children: stage.name })
1522
+ ] }),
1523
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 text-sm", children: [
1524
+ excludedCount > 0 && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-red-600 font-medium", children: [
1525
+ "-",
1526
+ excludedCount.toLocaleString()
1527
+ ] }),
1528
+ /* @__PURE__ */ jsxRuntime.jsxs(
1529
+ "span",
1530
+ {
1531
+ className: `font-semibold ${isLast ? "text-green-600" : "text-gray-700"}`,
1532
+ children: [
1533
+ remainingCount.toLocaleString(),
1534
+ " ",
1535
+ isLast ? "final" : "left"
1536
+ ]
1537
+ }
1538
+ )
1539
+ ] })
1540
+ ]
1541
+ },
1542
+ stage.id
1543
+ );
1544
+ }) })
1545
+ ] });
1546
+ }
1547
+ function defaultEntityRenderer(entity) {
1548
+ if (entity.name) {
1549
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1550
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "font-medium text-gray-900", children: entity.name }),
1551
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm text-gray-600 mt-1", children: Object.keys(entity).filter((key) => key !== "name").slice(0, 3).map((key) => /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "mr-2", children: [
1552
+ key,
1553
+ ": ",
1554
+ String(entity[key]).slice(0, 20)
1555
+ ] }, key)) })
1556
+ ] });
1557
+ }
1558
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm text-gray-700 font-mono", children: /* @__PURE__ */ jsxRuntime.jsxs("pre", { className: "whitespace-pre-wrap break-all", children: [
1559
+ JSON.stringify(entity, null, 2).slice(0, 150),
1560
+ JSON.stringify(entity, null, 2).length > 150 ? "..." : ""
1561
+ ] }) });
1562
+ }
1563
+ function EntityCard({
1564
+ entity,
1565
+ renderEntity = defaultEntityRenderer,
1566
+ className = ""
1567
+ }) {
1568
+ return /* @__PURE__ */ jsxRuntime.jsx(
1569
+ "article",
1570
+ {
1571
+ className: `p-3 bg-white border border-gray-200 rounded-lg shadow-sm hover:border-gray-300 transition-colors ${className}`,
1572
+ children: renderEntity(entity)
1573
+ }
1574
+ );
1575
+ }
1576
+ function LoadingPreview() {
1577
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "animate-pulse", role: "status", "aria-live": "polite", children: [
1578
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "sr-only", children: "Loading preview..." }),
1579
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2 mb-6", children: [
1580
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-8 bg-gray-200 rounded-lg" }),
1581
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-between", children: [
1582
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-4 w-32 bg-gray-200 rounded" }),
1583
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-4 w-32 bg-gray-200 rounded" })
1584
+ ] })
1585
+ ] }),
1586
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-6", children: [
1587
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-5 w-40 bg-gray-200 rounded mb-3" }),
1588
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-2", children: [1, 2, 3].map((i) => /* @__PURE__ */ jsxRuntime.jsxs(
1589
+ "div",
1590
+ {
1591
+ className: "h-12 bg-gray-100 rounded-lg flex items-center px-3 gap-3",
1592
+ children: [
1593
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-6 h-6 bg-gray-200 rounded-full" }),
1594
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-4 bg-gray-200 rounded flex-1" }),
1595
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-4 w-16 bg-gray-200 rounded" })
1596
+ ]
1597
+ },
1598
+ i
1599
+ )) })
1600
+ ] }),
1601
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
1602
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-5 w-48 bg-gray-200 rounded mb-3" }),
1603
+ [1, 2, 3].map((i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "h-20 bg-gray-100 border border-gray-200 rounded-lg p-3", children: [
1604
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-4 bg-gray-200 rounded w-3/4 mb-2" }),
1605
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-3 bg-gray-200 rounded w-1/2" })
1606
+ ] }, i))
1607
+ ] })
1608
+ ] });
1609
+ }
1610
+ function convertToPreviewResult(execResult, maxEntities = 10) {
1611
+ const { matched, total_matched, total_excluded, stage_stats } = execResult;
1612
+ const total = total_matched + total_excluded;
1613
+ const matchPercentage = total > 0 ? Math.round(total_matched / total * 100) : 0;
1614
+ const previewEntities = matched.slice(0, maxEntities).map((r) => r.entity);
1615
+ const previewStageStats = {};
1616
+ Object.entries(stage_stats).forEach(([stageId, stats]) => {
1617
+ previewStageStats[stageId] = {
1618
+ stage_id: stats.stage_id,
1619
+ stage_name: stats.stage_name,
1620
+ input_count: stats.input_count,
1621
+ excluded_count: stats.excluded_count,
1622
+ remaining_count: stats.input_count - stats.excluded_count
1623
+ };
1624
+ });
1625
+ return {
1626
+ totalMatched: total_matched,
1627
+ totalExcluded: total_excluded,
1628
+ matchPercentage,
1629
+ previewEntities,
1630
+ stageStats: previewStageStats
1631
+ };
1632
+ }
1633
+ function FunnelPreview({
1634
+ funnel,
1635
+ sampleEntities,
1636
+ onPreview,
1637
+ renderEntity,
1638
+ maxPreviewEntities = 10,
1639
+ className = ""
1640
+ }) {
1641
+ const [result, setResult] = react.useState(null);
1642
+ const [isComputing, setIsComputing] = react.useState(false);
1643
+ const debouncedFunnel = useDebouncedValue(funnel, 300);
1644
+ react.useEffect(() => {
1645
+ async function compute() {
1646
+ setIsComputing(true);
1647
+ try {
1648
+ const engine = new FunnelEngine();
1649
+ const execResult = engine.execute(debouncedFunnel, sampleEntities);
1650
+ const previewResult = convertToPreviewResult(
1651
+ execResult,
1652
+ maxPreviewEntities
1653
+ );
1654
+ setResult(previewResult);
1655
+ if (onPreview) {
1656
+ onPreview(previewResult);
1657
+ }
1658
+ } catch (error) {
1659
+ console.error("Preview computation failed:", error);
1660
+ setResult({
1661
+ totalMatched: 0,
1662
+ totalExcluded: sampleEntities.length,
1663
+ matchPercentage: 0,
1664
+ previewEntities: [],
1665
+ stageStats: {}
1666
+ });
1667
+ } finally {
1668
+ setIsComputing(false);
1669
+ }
1670
+ }
1671
+ compute();
1672
+ }, [debouncedFunnel, sampleEntities, maxPreviewEntities, onPreview]);
1673
+ if (isComputing && !result) {
1674
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className, children: /* @__PURE__ */ jsxRuntime.jsx(LoadingPreview, {}) });
1675
+ }
1676
+ if (!result) {
1677
+ return null;
1678
+ }
1679
+ const { totalMatched, totalExcluded, matchPercentage, previewEntities, stageStats } = result;
1680
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, role: "region", "aria-label": "Funnel preview", children: [
1681
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-semibold text-gray-900 mb-4", children: "Preview Results" }),
1682
+ /* @__PURE__ */ jsxRuntime.jsx(
1683
+ PreviewStats,
1684
+ {
1685
+ totalMatched,
1686
+ totalExcluded,
1687
+ matchPercentage,
1688
+ className: "mb-6"
1689
+ }
1690
+ ),
1691
+ funnel.stages.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
1692
+ StageBreakdown,
1693
+ {
1694
+ stageStats,
1695
+ stages: funnel.stages,
1696
+ className: "mb-6"
1697
+ }
1698
+ ),
1699
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1700
+ /* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "text-sm font-semibold text-gray-700 mb-3", children: [
1701
+ "Sample Matches (",
1702
+ Math.min(previewEntities.length, maxPreviewEntities),
1703
+ " of",
1704
+ " ",
1705
+ totalMatched.toLocaleString(),
1706
+ ")"
1707
+ ] }),
1708
+ previewEntities.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-8 text-center bg-gray-50 rounded-lg border-2 border-dashed border-gray-300", children: [
1709
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-600", children: "No entities matched this funnel" }),
1710
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-500 mt-1", children: "Try adjusting your filter rules" })
1711
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
1712
+ previewEntities.map((entity, index) => /* @__PURE__ */ jsxRuntime.jsx(
1713
+ EntityCard,
1714
+ {
1715
+ entity,
1716
+ renderEntity
1717
+ },
1718
+ index
1719
+ )),
1720
+ totalMatched > maxPreviewEntities && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center py-2 text-sm text-gray-500", children: [
1721
+ "+ ",
1722
+ (totalMatched - maxPreviewEntities).toLocaleString(),
1723
+ " more..."
1724
+ ] })
1725
+ ] })
1726
+ ] }),
1727
+ isComputing && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 bg-white bg-opacity-75 flex items-center justify-center rounded-lg", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 text-gray-600", children: [
1728
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-5 h-5 border-2 border-gray-300 border-t-blue-600 rounded-full animate-spin" }),
1729
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium", children: "Updating preview..." })
1730
+ ] }) })
1731
+ ] });
1732
+ }
1733
+ var statusConfig = {
1734
+ active: {
1735
+ color: "text-green-800",
1736
+ bgColor: "bg-green-100",
1737
+ label: "ACTIVE"
1738
+ },
1739
+ draft: {
1740
+ color: "text-yellow-800",
1741
+ bgColor: "bg-yellow-100",
1742
+ label: "DRAFT"
1743
+ },
1744
+ paused: {
1745
+ color: "text-gray-800",
1746
+ bgColor: "bg-gray-100",
1747
+ label: "PAUSED"
1748
+ },
1749
+ archived: {
1750
+ color: "text-red-800",
1751
+ bgColor: "bg-red-100",
1752
+ label: "ARCHIVED"
1753
+ }
1754
+ };
1755
+ function StatusBadge({ status, className = "" }) {
1756
+ const config = statusConfig[status];
1757
+ return /* @__PURE__ */ jsxRuntime.jsx(
1758
+ "span",
1759
+ {
1760
+ className: `inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${config.bgColor} ${config.color} ${className}`,
1761
+ children: config.label
1762
+ }
1763
+ );
1764
+ }
1765
+ function StageIndicator({
1766
+ order,
1767
+ name,
1768
+ ruleCount,
1769
+ isLast = false,
1770
+ className = ""
1771
+ }) {
1772
+ const circledNumber = order < 20 ? String.fromCharCode(9312 + order) : `(${order + 1})`;
1773
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex items-start gap-2 ${className}`, children: [
1774
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center", children: [
1775
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-shrink-0 w-6 h-6 rounded-full bg-blue-100 text-blue-800 flex items-center justify-center text-sm font-medium", children: circledNumber }),
1776
+ !isLast && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-0.5 h-6 bg-gray-200 mt-1" })
1777
+ ] }),
1778
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 pt-0.5 min-w-0", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-baseline justify-between gap-2", children: [
1779
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium text-gray-900 truncate", children: name }),
1780
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-gray-500 whitespace-nowrap", children: [
1781
+ ruleCount,
1782
+ " ",
1783
+ ruleCount === 1 ? "rule" : "rules"
1784
+ ] })
1785
+ ] }) })
1786
+ ] });
1787
+ }
1788
+ function MatchBar({ matched, total, className = "" }) {
1789
+ const percentage = total > 0 ? Math.round(matched / total * 100) : 0;
1790
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `space-y-1 ${className}`, children: [
1791
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative h-6 bg-gray-200 rounded-md overflow-hidden", children: /* @__PURE__ */ jsxRuntime.jsx(
1792
+ "div",
1793
+ {
1794
+ className: "absolute inset-y-0 left-0 bg-gradient-to-r from-green-500 to-green-600 transition-all duration-300",
1795
+ style: { width: `${percentage}%` },
1796
+ role: "progressbar",
1797
+ "aria-valuenow": percentage,
1798
+ "aria-valuemin": 0,
1799
+ "aria-valuemax": 100,
1800
+ "aria-label": `${matched} of ${total} matched`
1801
+ }
1802
+ ) }),
1803
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-right", children: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-sm font-medium text-gray-700", children: [
1804
+ matched.toLocaleString(),
1805
+ " matched"
1806
+ ] }) })
1807
+ ] });
1808
+ }
1809
+ function FunnelStats({
1810
+ input,
1811
+ matched,
1812
+ excluded,
1813
+ className = ""
1814
+ }) {
1815
+ const stats = [
1816
+ {
1817
+ label: "INPUT",
1818
+ value: input,
1819
+ color: "text-blue-600",
1820
+ bgColor: "bg-blue-50"
1821
+ },
1822
+ {
1823
+ label: "MATCHED",
1824
+ value: matched,
1825
+ color: "text-green-600",
1826
+ bgColor: "bg-green-50"
1827
+ },
1828
+ {
1829
+ label: "EXCLUDED",
1830
+ value: excluded,
1831
+ color: "text-red-600",
1832
+ bgColor: "bg-red-50"
1833
+ }
1834
+ ];
1835
+ return /* @__PURE__ */ jsxRuntime.jsx("dl", { className: `grid grid-cols-3 gap-2 ${className}`, children: stats.map(({ label, value, color, bgColor }) => /* @__PURE__ */ jsxRuntime.jsxs(
1836
+ "div",
1837
+ {
1838
+ className: `${bgColor} rounded-lg px-3 py-2.5 text-center`,
1839
+ children: [
1840
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "text-xs font-medium text-gray-600 mb-1", children: label }),
1841
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { className: `text-lg font-bold ${color}`, children: value.toLocaleString() })
1842
+ ]
1843
+ },
1844
+ label
1845
+ )) });
1846
+ }
1847
+ function FunnelCard({
1848
+ funnel,
1849
+ latestRun,
1850
+ onViewFlow,
1851
+ onEdit,
1852
+ className = ""
1853
+ }) {
1854
+ const stats = latestRun ? {
1855
+ input: latestRun.total_input,
1856
+ matched: latestRun.total_matched,
1857
+ excluded: latestRun.total_excluded
1858
+ } : {
1859
+ input: 0,
1860
+ matched: 0,
1861
+ excluded: 0
1862
+ };
1863
+ const handleViewFlow = () => {
1864
+ if (onViewFlow) {
1865
+ onViewFlow(funnel);
1866
+ }
1867
+ };
1868
+ const hasRun = latestRun && latestRun.status === "completed";
1869
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1870
+ "article",
1871
+ {
1872
+ className: `bg-white rounded-lg border border-gray-200 shadow-sm hover:shadow-md transition-shadow duration-200 ${className}`,
1873
+ "aria-label": `Funnel: ${funnel.name}`,
1874
+ children: [
1875
+ /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "px-6 pt-5 pb-3 border-b border-gray-100", children: [
1876
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start justify-between gap-3", children: [
1877
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-lg font-semibold text-gray-900 flex-1 min-w-0", children: funnel.name }),
1878
+ /* @__PURE__ */ jsxRuntime.jsx(StatusBadge, { status: funnel.status })
1879
+ ] }),
1880
+ funnel.description && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 text-sm text-gray-600 line-clamp-2", children: funnel.description })
1881
+ ] }),
1882
+ /* @__PURE__ */ jsxRuntime.jsx(
1883
+ "section",
1884
+ {
1885
+ className: "px-6 py-4 space-y-0",
1886
+ "aria-label": "Funnel stages",
1887
+ children: funnel.stages.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm text-gray-500 italic py-4 text-center", children: "No stages defined" }) : funnel.stages.map((stage, index) => /* @__PURE__ */ jsxRuntime.jsx(
1888
+ StageIndicator,
1889
+ {
1890
+ order: index,
1891
+ name: stage.name,
1892
+ ruleCount: stage.rules.length,
1893
+ isLast: index === funnel.stages.length - 1
1894
+ },
1895
+ stage.id
1896
+ ))
1897
+ }
1898
+ ),
1899
+ hasRun && /* @__PURE__ */ jsxRuntime.jsx(
1900
+ "section",
1901
+ {
1902
+ className: "px-6 py-4 border-t border-gray-100",
1903
+ "aria-label": "Match results",
1904
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1905
+ MatchBar,
1906
+ {
1907
+ matched: stats.matched,
1908
+ total: stats.input
1909
+ }
1910
+ )
1911
+ }
1912
+ ),
1913
+ hasRun && /* @__PURE__ */ jsxRuntime.jsx(
1914
+ "section",
1915
+ {
1916
+ className: "px-6 py-4 border-t border-gray-100",
1917
+ "aria-label": "Funnel statistics",
1918
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1919
+ FunnelStats,
1920
+ {
1921
+ input: stats.input,
1922
+ matched: stats.matched,
1923
+ excluded: stats.excluded
1924
+ }
1925
+ )
1926
+ }
1927
+ ),
1928
+ !hasRun && /* @__PURE__ */ jsxRuntime.jsx(
1929
+ "section",
1930
+ {
1931
+ className: "px-6 py-4 border-t border-gray-100 text-center",
1932
+ "aria-label": "Funnel status",
1933
+ children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-500", children: latestRun?.status === "failed" ? "Last run failed" : latestRun?.status === "running" ? "Running..." : "No runs yet" })
1934
+ }
1935
+ ),
1936
+ /* @__PURE__ */ jsxRuntime.jsx("footer", { className: "px-6 py-4 border-t border-gray-100", children: /* @__PURE__ */ jsxRuntime.jsxs(
1937
+ "button",
1938
+ {
1939
+ onClick: handleViewFlow,
1940
+ className: "w-full inline-flex items-center justify-center gap-2 px-4 py-2.5 bg-gray-50 hover:bg-gray-100 text-gray-900 text-sm font-medium rounded-md transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500",
1941
+ "aria-label": `View flow details for ${funnel.name}`,
1942
+ children: [
1943
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "View Flow" }),
1944
+ /* @__PURE__ */ jsxRuntime.jsx(
1945
+ "svg",
1946
+ {
1947
+ className: "w-4 h-4 transition-transform group-hover:translate-x-0.5",
1948
+ fill: "none",
1949
+ viewBox: "0 0 24 24",
1950
+ stroke: "currentColor",
1951
+ "aria-hidden": "true",
1952
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1953
+ "path",
1954
+ {
1955
+ strokeLinecap: "round",
1956
+ strokeLinejoin: "round",
1957
+ strokeWidth: 2,
1958
+ d: "M13 7l5 5m0 0l-5 5m5-5H6"
1959
+ }
1960
+ )
1961
+ }
1962
+ )
1963
+ ]
1964
+ }
1965
+ ) })
1966
+ ]
1967
+ }
1968
+ );
1969
+ }
1970
+ function getStageColor(stage) {
1971
+ const matchAction = stage.match_action;
1972
+ const noMatchAction = stage.no_match_action;
1973
+ if (matchAction === "output") {
1974
+ return "#22c55e";
1975
+ }
1976
+ if (noMatchAction === "exclude" || noMatchAction === "tag_exclude") {
1977
+ return "#ef4444";
1978
+ }
1979
+ if (matchAction === "tag" || matchAction === "tag_continue") {
1980
+ return "#eab308";
1981
+ }
1982
+ return "#3b82f6";
1983
+ }
1984
+ function getActionLabel(stage) {
1985
+ const matchAction = stage.match_action;
1986
+ const noMatchAction = stage.no_match_action;
1987
+ if (matchAction === "output") return "Output";
1988
+ if (noMatchAction === "exclude") return "Exclude Non-Matches";
1989
+ if (noMatchAction === "tag_exclude") return "Tag & Exclude";
1990
+ if (matchAction === "tag") return "Tag Matches";
1991
+ if (matchAction === "tag_continue") return "Tag & Continue";
1992
+ return "Continue";
1993
+ }
1994
+ function StageNode({ data }) {
1995
+ const { stage, stats, onStageClick } = data;
1996
+ const color = getStageColor(stage);
1997
+ const actionLabel = getActionLabel(stage);
1998
+ const handleClick = () => {
1999
+ if (onStageClick) {
2000
+ onStageClick(stage);
2001
+ }
2002
+ };
2003
+ const handleKeyDown = (event) => {
2004
+ if (event.key === "Enter" || event.key === " ") {
2005
+ event.preventDefault();
2006
+ handleClick();
2007
+ }
2008
+ };
2009
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2010
+ /* @__PURE__ */ jsxRuntime.jsx(
2011
+ react$1.Handle,
2012
+ {
2013
+ type: "target",
2014
+ position: react$1.Position.Top,
2015
+ style: { background: color, opacity: 0 },
2016
+ isConnectable: false
2017
+ }
2018
+ ),
2019
+ /* @__PURE__ */ jsxRuntime.jsxs(
2020
+ "div",
2021
+ {
2022
+ className: "stage-node",
2023
+ onClick: handleClick,
2024
+ onKeyDown: handleKeyDown,
2025
+ role: "button",
2026
+ tabIndex: 0,
2027
+ "aria-label": `Stage ${stage.order + 1}: ${stage.name}`,
2028
+ style: {
2029
+ borderColor: color,
2030
+ borderWidth: "2px",
2031
+ borderStyle: "solid"
2032
+ },
2033
+ children: [
2034
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "stage-number", style: { color }, children: getCircledNumber(stage.order + 1) }),
2035
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "stage-name", title: stage.name, children: stage.name }),
2036
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "stage-rules", children: [
2037
+ stage.rules.length,
2038
+ " ",
2039
+ stage.rules.length === 1 ? "rule" : "rules"
2040
+ ] }),
2041
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "stage-action", style: { color }, children: actionLabel }),
2042
+ stats && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "stage-stats", children: [
2043
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "stat-row", children: [
2044
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "stat-label", children: "Input:" }),
2045
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "stat-value", children: stats.input_count })
2046
+ ] }),
2047
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "stat-row", children: [
2048
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "stat-label", children: "Matched:" }),
2049
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "stat-value text-green-600", children: stats.matched_count })
2050
+ ] }),
2051
+ stats.excluded_count > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "stat-row", children: [
2052
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "stat-label", children: "Excluded:" }),
2053
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "stat-value text-red-600", children: stats.excluded_count })
2054
+ ] })
2055
+ ] }),
2056
+ stage.description && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "stage-description", title: stage.description, children: stage.description.length > 50 ? `${stage.description.substring(0, 50)}...` : stage.description })
2057
+ ]
2058
+ }
2059
+ ),
2060
+ /* @__PURE__ */ jsxRuntime.jsx(
2061
+ react$1.Handle,
2062
+ {
2063
+ type: "source",
2064
+ position: react$1.Position.Bottom,
2065
+ style: { background: color, opacity: 0 },
2066
+ isConnectable: false
2067
+ }
2068
+ )
2069
+ ] });
2070
+ }
2071
+ function FlowLegend() {
2072
+ const [isExpanded, setIsExpanded] = react.useState(true);
2073
+ const legendItems = [
2074
+ { color: "#3b82f6", label: "Continue" },
2075
+ { color: "#ef4444", label: "Exclude" },
2076
+ { color: "#eab308", label: "Tag" },
2077
+ { color: "#22c55e", label: "Output" }
2078
+ ];
2079
+ return /* @__PURE__ */ jsxRuntime.jsx(react$1.Panel, { position: "bottom-right", className: "flow-legend-panel", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flow-legend", children: [
2080
+ /* @__PURE__ */ jsxRuntime.jsxs(
2081
+ "button",
2082
+ {
2083
+ className: "legend-toggle",
2084
+ onClick: () => setIsExpanded(!isExpanded),
2085
+ "aria-label": isExpanded ? "Collapse legend" : "Expand legend",
2086
+ children: [
2087
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "legend-title", children: "Legend" }),
2088
+ /* @__PURE__ */ jsxRuntime.jsx(
2089
+ "svg",
2090
+ {
2091
+ className: `legend-chevron ${isExpanded ? "expanded" : ""}`,
2092
+ width: "12",
2093
+ height: "12",
2094
+ viewBox: "0 0 12 12",
2095
+ fill: "none",
2096
+ xmlns: "http://www.w3.org/2000/svg",
2097
+ children: /* @__PURE__ */ jsxRuntime.jsx(
2098
+ "path",
2099
+ {
2100
+ d: "M3 4.5L6 7.5L9 4.5",
2101
+ stroke: "currentColor",
2102
+ strokeWidth: "1.5",
2103
+ strokeLinecap: "round",
2104
+ strokeLinejoin: "round"
2105
+ }
2106
+ )
2107
+ }
2108
+ )
2109
+ ]
2110
+ }
2111
+ ),
2112
+ isExpanded && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "legend-items", children: legendItems.map((item) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "legend-item", children: [
2113
+ /* @__PURE__ */ jsxRuntime.jsx(
2114
+ "div",
2115
+ {
2116
+ className: "legend-color",
2117
+ style: {
2118
+ backgroundColor: item.color,
2119
+ border: `2px solid ${item.color}`
2120
+ }
2121
+ }
2122
+ ),
2123
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "legend-label", children: item.label })
2124
+ ] }, item.label)) })
2125
+ ] }) });
2126
+ }
2127
+ function getExcludedCount(runData, fromStageId, toStageId) {
2128
+ const fromStats = runData.stage_stats[fromStageId];
2129
+ const toStats = runData.stage_stats[toStageId];
2130
+ if (!fromStats || !toStats) return 0;
2131
+ return fromStats.continued_count - toStats.input_count;
2132
+ }
2133
+ function getCircledNumber(num) {
2134
+ const circledNumbers = ["\u2460", "\u2461", "\u2462", "\u2463", "\u2464", "\u2465", "\u2466", "\u2467", "\u2468", "\u2469"];
2135
+ return num <= 10 ? circledNumbers[num - 1] : `${num}`;
2136
+ }
2137
+ var VERTICAL_SPACING = 180;
2138
+ var HORIZONTAL_CENTER = 250;
2139
+ function FunnelVisualFlow({
2140
+ funnel,
2141
+ runData,
2142
+ onStageClick,
2143
+ onEdgeClick,
2144
+ className = "",
2145
+ height = 600
2146
+ }) {
2147
+ const nodeTypes = react.useMemo(
2148
+ () => ({
2149
+ stageNode: StageNode
2150
+ }),
2151
+ []
2152
+ );
2153
+ const initialNodes = react.useMemo(() => {
2154
+ return funnel.stages.map((stage, index) => {
2155
+ const stats = runData?.stage_stats?.[stage.id];
2156
+ return {
2157
+ id: stage.id,
2158
+ type: "stageNode",
2159
+ position: { x: HORIZONTAL_CENTER, y: index * VERTICAL_SPACING },
2160
+ data: {
2161
+ stage,
2162
+ stats,
2163
+ onStageClick
2164
+ }
2165
+ };
2166
+ });
2167
+ }, [funnel.stages, runData, onStageClick]);
2168
+ const initialEdges = react.useMemo(() => {
2169
+ if (funnel.stages.length < 2) return [];
2170
+ return funnel.stages.slice(0, -1).map((stage, index) => {
2171
+ const nextStage = funnel.stages[index + 1];
2172
+ const excludedCount = runData ? getExcludedCount(runData, stage.id, nextStage.id) : void 0;
2173
+ return {
2174
+ id: `${stage.id}-${nextStage.id}`,
2175
+ source: stage.id,
2176
+ target: nextStage.id,
2177
+ label: excludedCount !== void 0 ? `-${excludedCount}` : "",
2178
+ animated: true,
2179
+ style: { stroke: "#94a3b8", strokeWidth: 2 },
2180
+ labelStyle: { fill: "#ef4444", fontWeight: 600 },
2181
+ labelBgStyle: { fill: "#fef2f2", fillOpacity: 0.9 }
2182
+ };
2183
+ });
2184
+ }, [funnel.stages, runData]);
2185
+ const [nodes, , onNodesChange] = react$1.useNodesState(initialNodes);
2186
+ const [edges, , onEdgesChange] = react$1.useEdgesState(initialEdges);
2187
+ const handleEdgeClick = react.useCallback(
2188
+ (event, edge) => {
2189
+ if (onEdgeClick) {
2190
+ onEdgeClick(edge.source, edge.target);
2191
+ }
2192
+ },
2193
+ [onEdgeClick]
2194
+ );
2195
+ if (funnel.stages.length === 0) {
2196
+ return /* @__PURE__ */ jsxRuntime.jsx(
2197
+ "div",
2198
+ {
2199
+ className: `funnel-visual-flow-empty ${className}`,
2200
+ style: { height },
2201
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "empty-state", children: [
2202
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-500 text-sm", children: "No stages to visualize" }),
2203
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-400 text-xs mt-1", children: "Add stages to see the funnel flow" })
2204
+ ] })
2205
+ }
2206
+ );
2207
+ }
2208
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: `funnel-visual-flow ${className}`, style: { height }, children: /* @__PURE__ */ jsxRuntime.jsxs(
2209
+ react$1.ReactFlow,
2210
+ {
2211
+ nodes,
2212
+ edges,
2213
+ onNodesChange,
2214
+ onEdgesChange,
2215
+ onEdgeClick: handleEdgeClick,
2216
+ nodeTypes,
2217
+ fitView: true,
2218
+ fitViewOptions: {
2219
+ padding: 0.2,
2220
+ includeHiddenNodes: false
2221
+ },
2222
+ minZoom: 0.5,
2223
+ maxZoom: 1.5,
2224
+ defaultViewport: { x: 0, y: 0, zoom: 1 },
2225
+ nodesDraggable: false,
2226
+ nodesConnectable: false,
2227
+ elementsSelectable: true,
2228
+ children: [
2229
+ /* @__PURE__ */ jsxRuntime.jsx(react$1.Background, { variant: react$1.BackgroundVariant.Dots, gap: 16, size: 1 }),
2230
+ /* @__PURE__ */ jsxRuntime.jsx(react$1.Controls, { showInteractive: false }),
2231
+ /* @__PURE__ */ jsxRuntime.jsx(FlowLegend, {})
2232
+ ]
2233
+ }
2234
+ ) });
2235
+ }
2236
+ function LogicToggle({ logic, onChange, className = "" }) {
2237
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex items-center gap-4 ${className}`, children: [
2238
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium text-gray-700", children: "Logic:" }),
2239
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex items-center gap-2 cursor-pointer", children: [
2240
+ /* @__PURE__ */ jsxRuntime.jsx(
2241
+ "input",
2242
+ {
2243
+ type: "radio",
2244
+ name: "filter-logic",
2245
+ value: "AND",
2246
+ checked: logic === "AND",
2247
+ onChange: (e) => onChange(e.target.value),
2248
+ className: "w-4 h-4 text-blue-600 border-gray-300 focus:ring-blue-500"
2249
+ }
2250
+ ),
2251
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-sm text-gray-700", children: [
2252
+ "AND ",
2253
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-500", children: "(all must match)" })
2254
+ ] })
2255
+ ] }),
2256
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex items-center gap-2 cursor-pointer", children: [
2257
+ /* @__PURE__ */ jsxRuntime.jsx(
2258
+ "input",
2259
+ {
2260
+ type: "radio",
2261
+ name: "filter-logic",
2262
+ value: "OR",
2263
+ checked: logic === "OR",
2264
+ onChange: (e) => onChange(e.target.value),
2265
+ className: "w-4 h-4 text-blue-600 border-gray-300 focus:ring-blue-500"
2266
+ }
2267
+ ),
2268
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-sm text-gray-700", children: [
2269
+ "OR ",
2270
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-500", children: "(any can match)" })
2271
+ ] })
2272
+ ] })
2273
+ ] });
2274
+ }
2275
+ function FieldSelector({
2276
+ fields,
2277
+ value,
2278
+ onChange,
2279
+ error,
2280
+ className = ""
2281
+ }) {
2282
+ const grouped = fields.reduce((acc, field) => {
2283
+ const category = field.category || "Other";
2284
+ if (!acc[category]) {
2285
+ acc[category] = [];
2286
+ }
2287
+ acc[category].push(field);
2288
+ return acc;
2289
+ }, {});
2290
+ const categories = Object.keys(grouped).sort();
2291
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex flex-col gap-1 ${className}`, children: [
2292
+ /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "field-selector", className: "text-xs font-medium text-gray-700", children: "Field" }),
2293
+ /* @__PURE__ */ jsxRuntime.jsxs(
2294
+ "select",
2295
+ {
2296
+ id: "field-selector",
2297
+ value,
2298
+ onChange: (e) => onChange(e.target.value),
2299
+ className: `
2300
+ w-full px-3 py-2 text-sm border rounded-md
2301
+ bg-white
2302
+ focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
2303
+ ${error ? "border-red-500" : "border-gray-300"}
2304
+ `,
2305
+ children: [
2306
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: "Select a field..." }),
2307
+ categories.map((category) => /* @__PURE__ */ jsxRuntime.jsx("optgroup", { label: category, children: grouped[category].map((field) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: field.name, children: field.label }, field.name)) }, category))
2308
+ ]
2309
+ }
2310
+ ),
2311
+ error && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-red-600", children: error })
2312
+ ] });
2313
+ }
2314
+
2315
+ // src/components/FilterRuleEditor/constants.ts
2316
+ var OPERATOR_LABELS = {
2317
+ // Equality
2318
+ eq: "equals",
2319
+ ne: "not equals",
2320
+ // Comparison
2321
+ gt: "greater than",
2322
+ lt: "less than",
2323
+ gte: "greater or equal",
2324
+ lte: "less or equal",
2325
+ // String operations
2326
+ contains: "contains",
2327
+ not_contains: "does not contain",
2328
+ startswith: "starts with",
2329
+ endswith: "ends with",
2330
+ matches: "matches regex",
2331
+ // Array/Set operations
2332
+ in: "is one of",
2333
+ not_in: "is not one of",
2334
+ has_any: "has any of",
2335
+ has_all: "has all of",
2336
+ // Null checks
2337
+ isnull: "is empty",
2338
+ isnotnull: "is not empty",
2339
+ // Tag operations
2340
+ has_tag: "has tag",
2341
+ not_has_tag: "does not have tag",
2342
+ // Boolean
2343
+ is_true: "is true",
2344
+ is_false: "is false"
2345
+ };
2346
+ var NULL_VALUE_OPERATORS = [
2347
+ "isnull",
2348
+ "isnotnull",
2349
+ "is_true",
2350
+ "is_false"
2351
+ ];
2352
+ var MULTI_VALUE_OPERATORS = [
2353
+ "in",
2354
+ "not_in",
2355
+ "has_any",
2356
+ "has_all"
2357
+ ];
2358
+ function OperatorSelector({
2359
+ operators,
2360
+ value,
2361
+ onChange,
2362
+ disabled = false,
2363
+ error,
2364
+ className = ""
2365
+ }) {
2366
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex flex-col gap-1 ${className}`, children: [
2367
+ /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "operator-selector", className: "text-xs font-medium text-gray-700", children: "Operator" }),
2368
+ /* @__PURE__ */ jsxRuntime.jsxs(
2369
+ "select",
2370
+ {
2371
+ id: "operator-selector",
2372
+ value,
2373
+ onChange: (e) => onChange(e.target.value),
2374
+ disabled,
2375
+ className: `
2376
+ w-full px-3 py-2 text-sm border rounded-md
2377
+ bg-white
2378
+ focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
2379
+ disabled:bg-gray-100 disabled:cursor-not-allowed disabled:text-gray-500
2380
+ ${error ? "border-red-500" : "border-gray-300"}
2381
+ `,
2382
+ children: [
2383
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: "Select operator..." }),
2384
+ operators.map((op) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: op, children: OPERATOR_LABELS[op] }, op))
2385
+ ]
2386
+ }
2387
+ ),
2388
+ error && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-red-600", children: error })
2389
+ ] });
2390
+ }
2391
+ function TextValueInput({
2392
+ value,
2393
+ onChange,
2394
+ placeholder = "Enter text...",
2395
+ error,
2396
+ className = ""
2397
+ }) {
2398
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex flex-col gap-1 ${className}`, children: [
2399
+ /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "text-value", className: "text-xs font-medium text-gray-700", children: "Value" }),
2400
+ /* @__PURE__ */ jsxRuntime.jsx(
2401
+ "input",
2402
+ {
2403
+ id: "text-value",
2404
+ type: "text",
2405
+ value: value || "",
2406
+ onChange: (e) => onChange(e.target.value),
2407
+ placeholder,
2408
+ className: `
2409
+ w-full px-3 py-2 text-sm border rounded-md
2410
+ focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
2411
+ ${error ? "border-red-500" : "border-gray-300"}
2412
+ `
2413
+ }
2414
+ ),
2415
+ error && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-red-600", children: error })
2416
+ ] });
2417
+ }
2418
+ function NumberValueInput({
2419
+ value,
2420
+ onChange,
2421
+ min,
2422
+ max,
2423
+ placeholder = "Enter number...",
2424
+ error,
2425
+ className = ""
2426
+ }) {
2427
+ const handleChange = (e) => {
2428
+ const val = e.target.value;
2429
+ if (val === "") {
2430
+ onChange(null);
2431
+ } else {
2432
+ const num = parseFloat(val);
2433
+ if (!isNaN(num)) {
2434
+ onChange(num);
2435
+ }
2436
+ }
2437
+ };
2438
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex flex-col gap-1 ${className}`, children: [
2439
+ /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "number-value", className: "text-xs font-medium text-gray-700", children: "Value" }),
2440
+ /* @__PURE__ */ jsxRuntime.jsx(
2441
+ "input",
2442
+ {
2443
+ id: "number-value",
2444
+ type: "number",
2445
+ value: value ?? "",
2446
+ onChange: handleChange,
2447
+ min,
2448
+ max,
2449
+ placeholder,
2450
+ className: `
2451
+ w-full px-3 py-2 text-sm border rounded-md
2452
+ focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
2453
+ ${error ? "border-red-500" : "border-gray-300"}
2454
+ `
2455
+ }
2456
+ ),
2457
+ error && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-red-600", children: error })
2458
+ ] });
2459
+ }
2460
+ function DateValueInput({
2461
+ value,
2462
+ onChange,
2463
+ min,
2464
+ max,
2465
+ placeholder = "Select date...",
2466
+ error,
2467
+ className = ""
2468
+ }) {
2469
+ const handleChange = (e) => {
2470
+ const val = e.target.value;
2471
+ onChange(val || null);
2472
+ };
2473
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex flex-col gap-1 ${className}`, children: [
2474
+ /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "date-value", className: "text-xs font-medium text-gray-700", children: "Value" }),
2475
+ /* @__PURE__ */ jsxRuntime.jsx(
2476
+ "input",
2477
+ {
2478
+ id: "date-value",
2479
+ type: "date",
2480
+ value: value || "",
2481
+ onChange: handleChange,
2482
+ min,
2483
+ max,
2484
+ placeholder,
2485
+ className: `
2486
+ w-full px-3 py-2 text-sm border rounded-md
2487
+ focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
2488
+ ${error ? "border-red-500" : "border-gray-300"}
2489
+ `
2490
+ }
2491
+ ),
2492
+ error && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-red-600", children: error })
2493
+ ] });
2494
+ }
2495
+ function BooleanValueInput({
2496
+ value,
2497
+ onChange,
2498
+ label = "True",
2499
+ error,
2500
+ className = ""
2501
+ }) {
2502
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex flex-col gap-1 ${className}`, children: [
2503
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-gray-700", children: "Value" }),
2504
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex items-center gap-2 cursor-pointer", children: [
2505
+ /* @__PURE__ */ jsxRuntime.jsx(
2506
+ "input",
2507
+ {
2508
+ type: "checkbox",
2509
+ checked: value,
2510
+ onChange: (e) => onChange(e.target.checked),
2511
+ className: "w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
2512
+ }
2513
+ ),
2514
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-gray-700", children: label })
2515
+ ] }),
2516
+ error && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-red-600", children: error })
2517
+ ] });
2518
+ }
2519
+ function ChoiceValueInput({
2520
+ value,
2521
+ onChange,
2522
+ choices,
2523
+ placeholder = "Select option...",
2524
+ error,
2525
+ className = ""
2526
+ }) {
2527
+ const getChoiceValue = (choice) => {
2528
+ if (typeof choice === "string") return choice;
2529
+ return choice.value || choice;
2530
+ };
2531
+ const getChoiceLabel = (choice) => {
2532
+ if (typeof choice === "string") return choice;
2533
+ return choice.label || choice.value || String(choice);
2534
+ };
2535
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex flex-col gap-1 ${className}`, children: [
2536
+ /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "choice-value", className: "text-xs font-medium text-gray-700", children: "Value" }),
2537
+ /* @__PURE__ */ jsxRuntime.jsxs(
2538
+ "select",
2539
+ {
2540
+ id: "choice-value",
2541
+ value: value || "",
2542
+ onChange: (e) => onChange(e.target.value),
2543
+ className: `
2544
+ w-full px-3 py-2 text-sm border rounded-md
2545
+ bg-white
2546
+ focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
2547
+ ${error ? "border-red-500" : "border-gray-300"}
2548
+ `,
2549
+ children: [
2550
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: placeholder }),
2551
+ choices.map((choice, index) => {
2552
+ const val = getChoiceValue(choice);
2553
+ const label = getChoiceLabel(choice);
2554
+ return /* @__PURE__ */ jsxRuntime.jsx("option", { value: val, children: label }, `${val}-${index}`);
2555
+ })
2556
+ ]
2557
+ }
2558
+ ),
2559
+ error && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-red-600", children: error })
2560
+ ] });
2561
+ }
2562
+ function MultiChoiceValueInput({
2563
+ value = [],
2564
+ onChange,
2565
+ choices,
2566
+ placeholder = "Select options...",
2567
+ error,
2568
+ className = ""
2569
+ }) {
2570
+ const getChoiceValue = (choice) => {
2571
+ if (typeof choice === "string") return choice;
2572
+ return choice.value || choice;
2573
+ };
2574
+ const getChoiceLabel = (choice) => {
2575
+ if (typeof choice === "string") return choice;
2576
+ return choice.label || choice.value || String(choice);
2577
+ };
2578
+ const handleAdd = (newValue) => {
2579
+ if (newValue && !value.includes(newValue)) {
2580
+ onChange([...value, newValue]);
2581
+ }
2582
+ };
2583
+ const handleRemove = (removeValue) => {
2584
+ onChange(value.filter((v) => v !== removeValue));
2585
+ };
2586
+ const getValueLabel = (val) => {
2587
+ const choice = choices.find((c) => getChoiceValue(c) === val);
2588
+ return choice ? getChoiceLabel(choice) : val;
2589
+ };
2590
+ const availableChoices = choices.filter(
2591
+ (choice) => !value.includes(getChoiceValue(choice))
2592
+ );
2593
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex flex-col gap-2 ${className}`, children: [
2594
+ /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "multi-choice-value", className: "text-xs font-medium text-gray-700", children: "Values" }),
2595
+ value.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-1.5", children: value.map((val) => /* @__PURE__ */ jsxRuntime.jsxs(
2596
+ "span",
2597
+ {
2598
+ className: "inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-blue-800 bg-blue-100 rounded",
2599
+ children: [
2600
+ getValueLabel(val),
2601
+ /* @__PURE__ */ jsxRuntime.jsx(
2602
+ "button",
2603
+ {
2604
+ type: "button",
2605
+ onClick: () => handleRemove(val),
2606
+ className: "hover:text-blue-900 focus:outline-none",
2607
+ "aria-label": `Remove ${getValueLabel(val)}`,
2608
+ children: "\xD7"
2609
+ }
2610
+ )
2611
+ ]
2612
+ },
2613
+ val
2614
+ )) }),
2615
+ /* @__PURE__ */ jsxRuntime.jsxs(
2616
+ "select",
2617
+ {
2618
+ id: "multi-choice-value",
2619
+ value: "",
2620
+ onChange: (e) => handleAdd(e.target.value),
2621
+ className: `
2622
+ w-full px-3 py-2 text-sm border rounded-md
2623
+ bg-white
2624
+ focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
2625
+ ${error ? "border-red-500" : "border-gray-300"}
2626
+ `,
2627
+ children: [
2628
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: placeholder }),
2629
+ availableChoices.map((choice, index) => {
2630
+ const val = getChoiceValue(choice);
2631
+ const label = getChoiceLabel(choice);
2632
+ return /* @__PURE__ */ jsxRuntime.jsx("option", { value: val, children: label }, `${val}-${index}`);
2633
+ })
2634
+ ]
2635
+ }
2636
+ ),
2637
+ error && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-red-600", children: error })
2638
+ ] });
2639
+ }
2640
+ function RuleRow({
2641
+ rule,
2642
+ onChange,
2643
+ onRemove,
2644
+ fieldRegistry,
2645
+ className = ""
2646
+ }) {
2647
+ const selectedField = fieldRegistry.find((f) => f.name === rule.field_path);
2648
+ const availableOperators = selectedField?.operators || [];
2649
+ const needsValue = rule.operator && !NULL_VALUE_OPERATORS.includes(rule.operator);
2650
+ const needsMultiValue = rule.operator && MULTI_VALUE_OPERATORS.includes(rule.operator);
2651
+ const handleFieldChange = (fieldName) => {
2652
+ const field = fieldRegistry.find((f) => f.name === fieldName);
2653
+ onChange({
2654
+ ...rule,
2655
+ field_path: fieldName,
2656
+ operator: field?.operators[0] || "",
2657
+ value: null
2658
+ });
2659
+ };
2660
+ const handleOperatorChange = (operator) => {
2661
+ onChange({
2662
+ ...rule,
2663
+ operator,
2664
+ value: MULTI_VALUE_OPERATORS.includes(operator) ? [] : null
2665
+ });
2666
+ };
2667
+ const handleValueChange = (value) => {
2668
+ onChange({
2669
+ ...rule,
2670
+ value
2671
+ });
2672
+ };
2673
+ const renderValueInput = () => {
2674
+ if (!needsValue) return null;
2675
+ if (!selectedField) return null;
2676
+ const { type, constraints } = selectedField;
2677
+ if (needsMultiValue) {
2678
+ if (constraints?.choices) {
2679
+ return /* @__PURE__ */ jsxRuntime.jsx(
2680
+ MultiChoiceValueInput,
2681
+ {
2682
+ value: Array.isArray(rule.value) ? rule.value : [],
2683
+ onChange: handleValueChange,
2684
+ choices: constraints.choices
2685
+ }
2686
+ );
2687
+ }
2688
+ return /* @__PURE__ */ jsxRuntime.jsx(
2689
+ TextValueInput,
2690
+ {
2691
+ value: Array.isArray(rule.value) ? rule.value.join(", ") : "",
2692
+ onChange: (val) => handleValueChange(val.split(",").map((v) => v.trim())),
2693
+ placeholder: "Enter values, comma-separated..."
2694
+ }
2695
+ );
2696
+ }
2697
+ switch (type) {
2698
+ case "string":
2699
+ if (constraints?.choices && rule.operator === "eq") {
2700
+ return /* @__PURE__ */ jsxRuntime.jsx(
2701
+ ChoiceValueInput,
2702
+ {
2703
+ value: rule.value || "",
2704
+ onChange: handleValueChange,
2705
+ choices: constraints.choices
2706
+ }
2707
+ );
2708
+ }
2709
+ return /* @__PURE__ */ jsxRuntime.jsx(
2710
+ TextValueInput,
2711
+ {
2712
+ value: rule.value || "",
2713
+ onChange: handleValueChange
2714
+ }
2715
+ );
2716
+ case "number":
2717
+ return /* @__PURE__ */ jsxRuntime.jsx(
2718
+ NumberValueInput,
2719
+ {
2720
+ value: rule.value,
2721
+ onChange: handleValueChange,
2722
+ min: constraints?.min_value,
2723
+ max: constraints?.max_value
2724
+ }
2725
+ );
2726
+ case "date":
2727
+ return /* @__PURE__ */ jsxRuntime.jsx(
2728
+ DateValueInput,
2729
+ {
2730
+ value: rule.value || null,
2731
+ onChange: handleValueChange,
2732
+ min: constraints?.min_value,
2733
+ max: constraints?.max_value
2734
+ }
2735
+ );
2736
+ case "boolean":
2737
+ return /* @__PURE__ */ jsxRuntime.jsx(
2738
+ BooleanValueInput,
2739
+ {
2740
+ value: rule.value || false,
2741
+ onChange: handleValueChange
2742
+ }
2743
+ );
2744
+ case "tag":
2745
+ return /* @__PURE__ */ jsxRuntime.jsx(
2746
+ TextValueInput,
2747
+ {
2748
+ value: rule.value || "",
2749
+ onChange: handleValueChange,
2750
+ placeholder: "Enter tag name..."
2751
+ }
2752
+ );
2753
+ default:
2754
+ return /* @__PURE__ */ jsxRuntime.jsx(
2755
+ TextValueInput,
2756
+ {
2757
+ value: rule.value || "",
2758
+ onChange: handleValueChange
2759
+ }
2760
+ );
2761
+ }
2762
+ };
2763
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2764
+ "div",
2765
+ {
2766
+ className: `
2767
+ relative group
2768
+ border border-gray-200 rounded-lg p-4
2769
+ bg-white hover:shadow-sm transition-shadow
2770
+ ${className}
2771
+ `,
2772
+ children: [
2773
+ /* @__PURE__ */ jsxRuntime.jsx(
2774
+ "button",
2775
+ {
2776
+ type: "button",
2777
+ onClick: onRemove,
2778
+ className: "\n absolute top-2 right-2\n w-6 h-6 flex items-center justify-center\n text-gray-400 hover:text-red-600 hover:bg-red-50\n rounded transition-colors\n focus:outline-none focus:ring-2 focus:ring-red-500\n ",
2779
+ "aria-label": "Remove rule",
2780
+ children: "\xD7"
2781
+ }
2782
+ ),
2783
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3 pr-8", children: [
2784
+ /* @__PURE__ */ jsxRuntime.jsx(
2785
+ FieldSelector,
2786
+ {
2787
+ fields: fieldRegistry,
2788
+ value: rule.field_path,
2789
+ onChange: handleFieldChange
2790
+ }
2791
+ ),
2792
+ /* @__PURE__ */ jsxRuntime.jsx(
2793
+ OperatorSelector,
2794
+ {
2795
+ operators: availableOperators,
2796
+ value: rule.operator || "",
2797
+ onChange: handleOperatorChange,
2798
+ disabled: !rule.field_path
2799
+ }
2800
+ ),
2801
+ needsValue && renderValueInput()
2802
+ ] })
2803
+ ]
2804
+ }
2805
+ );
2806
+ }
2807
+ function FilterRuleEditor({
2808
+ rules,
2809
+ onChange,
2810
+ fieldRegistry,
2811
+ logic = "AND",
2812
+ onLogicChange,
2813
+ className = ""
2814
+ }) {
2815
+ const handleAddRule = () => {
2816
+ const newRule = {
2817
+ field_path: "",
2818
+ operator: "eq",
2819
+ value: null
2820
+ };
2821
+ onChange([...rules, newRule]);
2822
+ };
2823
+ const handleUpdateRule = (index, updatedRule) => {
2824
+ const newRules = [...rules];
2825
+ newRules[index] = updatedRule;
2826
+ onChange(newRules);
2827
+ };
2828
+ const handleRemoveRule = (index) => {
2829
+ const newRules = rules.filter((_, i) => i !== index);
2830
+ onChange(newRules);
2831
+ };
2832
+ const ruleErrors = rules.map((rule) => validateFilterRule(rule));
2833
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex flex-col gap-4 ${className}`, children: [
2834
+ onLogicChange && /* @__PURE__ */ jsxRuntime.jsx(LogicToggle, { logic, onChange: onLogicChange }),
2835
+ rules.length === 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center justify-center py-12 border-2 border-dashed border-gray-300 rounded-lg bg-gray-50", children: [
2836
+ /* @__PURE__ */ jsxRuntime.jsx(
2837
+ "svg",
2838
+ {
2839
+ className: "w-12 h-12 text-gray-400 mb-3",
2840
+ fill: "none",
2841
+ viewBox: "0 0 24 24",
2842
+ stroke: "currentColor",
2843
+ children: /* @__PURE__ */ jsxRuntime.jsx(
2844
+ "path",
2845
+ {
2846
+ strokeLinecap: "round",
2847
+ strokeLinejoin: "round",
2848
+ strokeWidth: 2,
2849
+ d: "M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"
2850
+ }
2851
+ )
2852
+ }
2853
+ ),
2854
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-600 mb-4", children: "No filter rules yet" }),
2855
+ /* @__PURE__ */ jsxRuntime.jsxs(
2856
+ "button",
2857
+ {
2858
+ type: "button",
2859
+ onClick: handleAddRule,
2860
+ className: "\n inline-flex items-center gap-2\n px-4 py-2\n text-sm font-medium text-white\n bg-blue-600 hover:bg-blue-700\n rounded-md\n focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2\n transition-colors\n ",
2861
+ children: [
2862
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-lg", children: "+" }),
2863
+ "Add First Rule"
2864
+ ]
2865
+ }
2866
+ )
2867
+ ] }),
2868
+ rules.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-3", children: rules.map((rule, index) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2869
+ /* @__PURE__ */ jsxRuntime.jsx(
2870
+ RuleRow,
2871
+ {
2872
+ rule,
2873
+ onChange: (updatedRule) => handleUpdateRule(index, updatedRule),
2874
+ onRemove: () => handleRemoveRule(index),
2875
+ fieldRegistry
2876
+ }
2877
+ ),
2878
+ index < rules.length - 1 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center py-2", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "px-3 py-1 text-xs font-semibold text-gray-700 bg-gray-100 border border-gray-300 rounded-full", children: logic }) }),
2879
+ ruleErrors[index].length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-2 px-4 py-2 bg-red-50 border border-red-200 rounded text-sm text-red-700", children: ruleErrors[index].map((error, i) => /* @__PURE__ */ jsxRuntime.jsx("div", { children: error }, i)) })
2880
+ ] }, index)) }),
2881
+ rules.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(
2882
+ "button",
2883
+ {
2884
+ type: "button",
2885
+ onClick: handleAddRule,
2886
+ className: "\n w-full\n flex items-center justify-center gap-2\n px-4 py-3\n text-sm font-medium text-blue-600\n bg-white hover:bg-blue-50\n border-2 border-dashed border-blue-300 hover:border-blue-400\n rounded-lg\n focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2\n transition-colors\n ",
2887
+ "aria-label": `Add rule ${rules.length + 1}`,
2888
+ children: [
2889
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xl", children: "+" }),
2890
+ "Add Rule"
2891
+ ]
2892
+ }
2893
+ ),
2894
+ rules.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between text-xs text-gray-500", children: [
2895
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
2896
+ rules.length,
2897
+ " ",
2898
+ rules.length === 1 ? "rule" : "rules"
2899
+ ] }),
2900
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: logic === "AND" ? "All rules must match" : "Any rule can match" })
2901
+ ] })
2902
+ ] });
2903
+ }
2904
+ var MATCH_ACTIONS = [
2905
+ {
2906
+ value: "continue",
2907
+ label: "Continue",
2908
+ description: "Continue to next stage without tagging"
2909
+ },
2910
+ {
2911
+ value: "tag",
2912
+ label: "Tag & Stop",
2913
+ description: "Add tags and stop processing"
2914
+ },
2915
+ {
2916
+ value: "tag_continue",
2917
+ label: "Tag & Continue",
2918
+ description: "Add tags and continue to next stage"
2919
+ },
2920
+ {
2921
+ value: "output",
2922
+ label: "Output",
2923
+ description: "Add to output and stop processing"
2924
+ }
2925
+ ];
2926
+ var NO_MATCH_ACTIONS = [
2927
+ {
2928
+ value: "continue",
2929
+ label: "Continue",
2930
+ description: "Continue to next stage"
2931
+ },
2932
+ {
2933
+ value: "exclude",
2934
+ label: "Exclude",
2935
+ description: "Exclude from output and stop processing"
2936
+ },
2937
+ {
2938
+ value: "tag_exclude",
2939
+ label: "Tag & Exclude",
2940
+ description: "Add tags, exclude from output, and stop"
2941
+ }
2942
+ ];
2943
+ function StageActions({
2944
+ stage,
2945
+ onMatchActionChange,
2946
+ onNoMatchActionChange
2947
+ }) {
2948
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "stage-actions", children: [
2949
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "form-group", children: [
2950
+ /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: `match-action-${stage.id}`, className: "form-label", children: "Action on Match" }),
2951
+ /* @__PURE__ */ jsxRuntime.jsx(
2952
+ "select",
2953
+ {
2954
+ id: `match-action-${stage.id}`,
2955
+ value: stage.match_action,
2956
+ onChange: (e) => onMatchActionChange(e.target.value),
2957
+ className: "form-select",
2958
+ children: MATCH_ACTIONS.map((option) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: option.value, children: option.label }, option.value))
2959
+ }
2960
+ ),
2961
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "form-hint", children: MATCH_ACTIONS.find((a) => a.value === stage.match_action)?.description })
2962
+ ] }),
2963
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "form-group", children: [
2964
+ /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: `no-match-action-${stage.id}`, className: "form-label", children: "Action on No Match" }),
2965
+ /* @__PURE__ */ jsxRuntime.jsx(
2966
+ "select",
2967
+ {
2968
+ id: `no-match-action-${stage.id}`,
2969
+ value: stage.no_match_action,
2970
+ onChange: (e) => onNoMatchActionChange(e.target.value),
2971
+ className: "form-select",
2972
+ children: NO_MATCH_ACTIONS.map((option) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: option.value, children: option.label }, option.value))
2973
+ }
2974
+ ),
2975
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "form-hint", children: NO_MATCH_ACTIONS.find((a) => a.value === stage.no_match_action)?.description })
2976
+ ] })
2977
+ ] });
2978
+ }
2979
+ function TagInput({
2980
+ tags,
2981
+ onChange,
2982
+ placeholder = "Add tag...",
2983
+ className = ""
2984
+ }) {
2985
+ const [inputValue, setInputValue] = react.useState("");
2986
+ const addTag = react.useCallback((tag) => {
2987
+ const trimmed = tag.trim().toLowerCase();
2988
+ if (!trimmed) {
2989
+ return;
2990
+ }
2991
+ if (tags.includes(trimmed)) {
2992
+ return;
2993
+ }
2994
+ onChange([...tags, trimmed]);
2995
+ setInputValue("");
2996
+ }, [tags, onChange]);
2997
+ const removeTag = react.useCallback((index) => {
2998
+ onChange(tags.filter((_, i) => i !== index));
2999
+ }, [tags, onChange]);
3000
+ const handleKeyDown = react.useCallback((e) => {
3001
+ if (e.key === "Enter" || e.key === ",") {
3002
+ e.preventDefault();
3003
+ addTag(inputValue);
3004
+ } else if (e.key === "Backspace" && !inputValue && tags.length > 0) {
3005
+ removeTag(tags.length - 1);
3006
+ }
3007
+ }, [inputValue, tags, addTag, removeTag]);
3008
+ const handleBlur = react.useCallback(() => {
3009
+ if (inputValue) {
3010
+ addTag(inputValue);
3011
+ }
3012
+ }, [inputValue, addTag]);
3013
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `tag-input ${className}`, children: [
3014
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "tag-input-container", children: [
3015
+ tags.map((tag, index) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "tag-chip", children: [
3016
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "tag-text", children: tag }),
3017
+ /* @__PURE__ */ jsxRuntime.jsx(
3018
+ "button",
3019
+ {
3020
+ type: "button",
3021
+ onClick: () => removeTag(index),
3022
+ className: "tag-remove",
3023
+ "aria-label": `Remove tag ${tag}`,
3024
+ children: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "14", height: "14", viewBox: "0 0 14 14", fill: "none", children: /* @__PURE__ */ jsxRuntime.jsx(
3025
+ "path",
3026
+ {
3027
+ d: "M4 4l6 6M10 4l-6 6",
3028
+ stroke: "currentColor",
3029
+ strokeWidth: "1.5",
3030
+ strokeLinecap: "round"
3031
+ }
3032
+ ) })
3033
+ }
3034
+ )
3035
+ ] }, index)),
3036
+ /* @__PURE__ */ jsxRuntime.jsx(
3037
+ "input",
3038
+ {
3039
+ type: "text",
3040
+ value: inputValue,
3041
+ onChange: (e) => setInputValue(e.target.value),
3042
+ onKeyDown: handleKeyDown,
3043
+ onBlur: handleBlur,
3044
+ placeholder: tags.length === 0 ? placeholder : "",
3045
+ className: "tag-input-field"
3046
+ }
3047
+ )
3048
+ ] }),
3049
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "tag-input-hint", children: "Press Enter or comma to add tags" })
3050
+ ] });
3051
+ }
3052
+ function useDebounce(callback, delay) {
3053
+ const [timeoutId, setTimeoutId] = react.useState(null);
3054
+ return react.useCallback(
3055
+ ((...args) => {
3056
+ if (timeoutId) {
3057
+ clearTimeout(timeoutId);
3058
+ }
3059
+ const newTimeoutId = setTimeout(() => {
3060
+ callback(...args);
3061
+ }, delay);
3062
+ setTimeoutId(newTimeoutId);
3063
+ }),
3064
+ [callback, delay, timeoutId]
3065
+ );
3066
+ }
3067
+ function StageForm({
3068
+ stage,
3069
+ onUpdate,
3070
+ fieldRegistry
3071
+ }) {
3072
+ const [name, setName] = react.useState(stage.name);
3073
+ const [description, setDescription] = react.useState(stage.description || "");
3074
+ const debouncedUpdateName = useDebounce((value) => {
3075
+ onUpdate({ ...stage, name: value });
3076
+ }, 300);
3077
+ const debouncedUpdateDescription = useDebounce((value) => {
3078
+ onUpdate({ ...stage, description: value });
3079
+ }, 500);
3080
+ const handleNameChange = react.useCallback((e) => {
3081
+ const value = e.target.value;
3082
+ setName(value);
3083
+ debouncedUpdateName(value);
3084
+ }, [debouncedUpdateName]);
3085
+ const handleDescriptionChange = react.useCallback((e) => {
3086
+ const value = e.target.value;
3087
+ setDescription(value);
3088
+ debouncedUpdateDescription(value);
3089
+ }, [debouncedUpdateDescription]);
3090
+ const handleFilterLogicChange = react.useCallback((logic) => {
3091
+ onUpdate({ ...stage, filter_logic: logic });
3092
+ }, [stage, onUpdate]);
3093
+ const handleMatchActionChange = react.useCallback((action) => {
3094
+ onUpdate({ ...stage, match_action: action });
3095
+ }, [stage, onUpdate]);
3096
+ const handleNoMatchActionChange = react.useCallback((action) => {
3097
+ onUpdate({ ...stage, no_match_action: action });
3098
+ }, [stage, onUpdate]);
3099
+ const handleMatchTagsChange = react.useCallback((tags) => {
3100
+ onUpdate({ ...stage, match_tags: tags });
3101
+ }, [stage, onUpdate]);
3102
+ const handleNoMatchTagsChange = react.useCallback((tags) => {
3103
+ onUpdate({ ...stage, no_match_tags: tags });
3104
+ }, [stage, onUpdate]);
3105
+ const handleRulesChange = react.useCallback((rules) => {
3106
+ onUpdate({ ...stage, rules });
3107
+ }, [stage, onUpdate]);
3108
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "stage-form", children: [
3109
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "form-group", children: [
3110
+ /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: `stage-name-${stage.id}`, className: "form-label", children: "Stage Name" }),
3111
+ /* @__PURE__ */ jsxRuntime.jsx(
3112
+ "input",
3113
+ {
3114
+ id: `stage-name-${stage.id}`,
3115
+ type: "text",
3116
+ value: name,
3117
+ onChange: handleNameChange,
3118
+ className: "form-input",
3119
+ placeholder: "e.g., High ICP Score",
3120
+ required: true
3121
+ }
3122
+ )
3123
+ ] }),
3124
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "form-group", children: [
3125
+ /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: `stage-desc-${stage.id}`, className: "form-label", children: "Description" }),
3126
+ /* @__PURE__ */ jsxRuntime.jsx(
3127
+ "textarea",
3128
+ {
3129
+ id: `stage-desc-${stage.id}`,
3130
+ value: description,
3131
+ onChange: handleDescriptionChange,
3132
+ className: "form-textarea",
3133
+ placeholder: "Describe the purpose of this stage...",
3134
+ rows: 3
3135
+ }
3136
+ )
3137
+ ] }),
3138
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "form-group", children: [
3139
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "form-label", children: "Filter Logic" }),
3140
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "filter-logic-toggle", children: [
3141
+ /* @__PURE__ */ jsxRuntime.jsxs(
3142
+ "button",
3143
+ {
3144
+ type: "button",
3145
+ onClick: () => handleFilterLogicChange("AND"),
3146
+ className: `toggle-button ${stage.filter_logic === "AND" ? "active" : ""}`,
3147
+ children: [
3148
+ /* @__PURE__ */ jsxRuntime.jsx(
3149
+ "input",
3150
+ {
3151
+ type: "radio",
3152
+ name: `filter-logic-${stage.id}`,
3153
+ value: "AND",
3154
+ checked: stage.filter_logic === "AND",
3155
+ onChange: () => handleFilterLogicChange("AND"),
3156
+ className: "sr-only"
3157
+ }
3158
+ ),
3159
+ "AND"
3160
+ ]
3161
+ }
3162
+ ),
3163
+ /* @__PURE__ */ jsxRuntime.jsxs(
3164
+ "button",
3165
+ {
3166
+ type: "button",
3167
+ onClick: () => handleFilterLogicChange("OR"),
3168
+ className: `toggle-button ${stage.filter_logic === "OR" ? "active" : ""}`,
3169
+ children: [
3170
+ /* @__PURE__ */ jsxRuntime.jsx(
3171
+ "input",
3172
+ {
3173
+ type: "radio",
3174
+ name: `filter-logic-${stage.id}`,
3175
+ value: "OR",
3176
+ checked: stage.filter_logic === "OR",
3177
+ onChange: () => handleFilterLogicChange("OR"),
3178
+ className: "sr-only"
3179
+ }
3180
+ ),
3181
+ "OR"
3182
+ ]
3183
+ }
3184
+ )
3185
+ ] }),
3186
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "form-hint", children: stage.filter_logic === "AND" ? "All rules must match for this stage to pass" : "At least one rule must match for this stage to pass" })
3187
+ ] }),
3188
+ /* @__PURE__ */ jsxRuntime.jsx(
3189
+ StageActions,
3190
+ {
3191
+ stage,
3192
+ onMatchActionChange: handleMatchActionChange,
3193
+ onNoMatchActionChange: handleNoMatchActionChange
3194
+ }
3195
+ ),
3196
+ (stage.match_action === "tag" || stage.match_action === "tag_continue") && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "form-group", children: [
3197
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "form-label", children: "Tags on Match" }),
3198
+ /* @__PURE__ */ jsxRuntime.jsx(
3199
+ TagInput,
3200
+ {
3201
+ tags: stage.match_tags || [],
3202
+ onChange: handleMatchTagsChange,
3203
+ placeholder: "Add tag..."
3204
+ }
3205
+ ),
3206
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "form-hint", children: "Tags to add when rules match" })
3207
+ ] }),
3208
+ stage.no_match_action === "tag_exclude" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "form-group", children: [
3209
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "form-label", children: "Tags on No Match" }),
3210
+ /* @__PURE__ */ jsxRuntime.jsx(
3211
+ TagInput,
3212
+ {
3213
+ tags: stage.no_match_tags || [],
3214
+ onChange: handleNoMatchTagsChange,
3215
+ placeholder: "Add tag..."
3216
+ }
3217
+ ),
3218
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "form-hint", children: "Tags to add when rules don't match" })
3219
+ ] }),
3220
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "form-group", children: [
3221
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rules-header", children: /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "form-label", children: [
3222
+ "Filter Rules (",
3223
+ stage.rules.length,
3224
+ ")"
3225
+ ] }) }),
3226
+ /* @__PURE__ */ jsxRuntime.jsx(
3227
+ FilterRuleEditor,
3228
+ {
3229
+ rules: stage.rules,
3230
+ onChange: handleRulesChange,
3231
+ fieldRegistry
3232
+ }
3233
+ )
3234
+ ] })
3235
+ ] });
3236
+ }
3237
+ function StageCard({
3238
+ stage,
3239
+ expanded,
3240
+ onToggleExpanded,
3241
+ onUpdate,
3242
+ onRemove,
3243
+ fieldRegistry,
3244
+ error,
3245
+ showWarnings = false
3246
+ }) {
3247
+ const {
3248
+ attributes,
3249
+ listeners,
3250
+ setNodeRef,
3251
+ transform,
3252
+ transition,
3253
+ isDragging
3254
+ } = sortable.useSortable({ id: stage.id });
3255
+ const style = {
3256
+ transform: utilities.CSS.Transform.toString(transform),
3257
+ transition,
3258
+ opacity: isDragging ? 0.5 : 1
3259
+ };
3260
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3261
+ "div",
3262
+ {
3263
+ ref: setNodeRef,
3264
+ style,
3265
+ className: `stage-card ${isDragging ? "dragging" : ""} ${error ? "error" : ""}`,
3266
+ children: [
3267
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "stage-header", children: [
3268
+ /* @__PURE__ */ jsxRuntime.jsx(
3269
+ "button",
3270
+ {
3271
+ ...attributes,
3272
+ ...listeners,
3273
+ className: "drag-handle",
3274
+ "aria-label": "Drag to reorder",
3275
+ children: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", children: /* @__PURE__ */ jsxRuntime.jsx(
3276
+ "path",
3277
+ {
3278
+ d: "M7 4h6M7 10h6M7 16h6",
3279
+ stroke: "currentColor",
3280
+ strokeWidth: "2",
3281
+ strokeLinecap: "round"
3282
+ }
3283
+ ) })
3284
+ }
3285
+ ),
3286
+ /* @__PURE__ */ jsxRuntime.jsxs(
3287
+ "button",
3288
+ {
3289
+ onClick: onToggleExpanded,
3290
+ className: "stage-title-button",
3291
+ "aria-expanded": expanded,
3292
+ children: [
3293
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "stage-number", children: [
3294
+ "Stage ",
3295
+ stage.order + 1,
3296
+ ":"
3297
+ ] }),
3298
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "stage-name", children: stage.name || "Untitled Stage" }),
3299
+ /* @__PURE__ */ jsxRuntime.jsx(
3300
+ "svg",
3301
+ {
3302
+ width: "20",
3303
+ height: "20",
3304
+ viewBox: "0 0 20 20",
3305
+ fill: "none",
3306
+ className: `expand-icon ${expanded ? "expanded" : ""}`,
3307
+ children: /* @__PURE__ */ jsxRuntime.jsx(
3308
+ "path",
3309
+ {
3310
+ d: "M6 8l4 4 4-4",
3311
+ stroke: "currentColor",
3312
+ strokeWidth: "2",
3313
+ strokeLinecap: "round",
3314
+ strokeLinejoin: "round"
3315
+ }
3316
+ )
3317
+ }
3318
+ )
3319
+ ]
3320
+ }
3321
+ ),
3322
+ /* @__PURE__ */ jsxRuntime.jsx(
3323
+ "button",
3324
+ {
3325
+ onClick: onRemove,
3326
+ className: "delete-button",
3327
+ "aria-label": "Delete stage",
3328
+ children: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", children: /* @__PURE__ */ jsxRuntime.jsx(
3329
+ "path",
3330
+ {
3331
+ d: "M6 6l8 8M14 6l-8 8",
3332
+ stroke: "currentColor",
3333
+ strokeWidth: "2",
3334
+ strokeLinecap: "round"
3335
+ }
3336
+ ) })
3337
+ }
3338
+ )
3339
+ ] }),
3340
+ error && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "error-message", children: [
3341
+ /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", children: [
3342
+ /* @__PURE__ */ jsxRuntime.jsx(
3343
+ "path",
3344
+ {
3345
+ d: "M8 1l7 13H1L8 1z",
3346
+ stroke: "currentColor",
3347
+ strokeWidth: "2",
3348
+ strokeLinejoin: "round"
3349
+ }
3350
+ ),
3351
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M8 6v3M8 11h.01", stroke: "currentColor", strokeWidth: "2" })
3352
+ ] }),
3353
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: error })
3354
+ ] }),
3355
+ showWarnings && !error && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "warning-message", children: [
3356
+ /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", children: [
3357
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "8", cy: "8", r: "7", stroke: "currentColor", strokeWidth: "2" }),
3358
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M8 5v3M8 10h.01", stroke: "currentColor", strokeWidth: "2" })
3359
+ ] }),
3360
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Stage has no filter rules" })
3361
+ ] }),
3362
+ !expanded && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "stage-summary", children: [
3363
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "summary-item", children: [
3364
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "summary-label", children: "Rules:" }),
3365
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "summary-value", children: stage.rules.length })
3366
+ ] }),
3367
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "summary-item", children: [
3368
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "summary-label", children: "Logic:" }),
3369
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "summary-value", children: stage.filter_logic })
3370
+ ] }),
3371
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "summary-item", children: [
3372
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "summary-label", children: "On Match:" }),
3373
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "summary-value", children: stage.match_action })
3374
+ ] }),
3375
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "summary-item", children: [
3376
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "summary-label", children: "On No Match:" }),
3377
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "summary-value", children: stage.no_match_action })
3378
+ ] })
3379
+ ] }),
3380
+ expanded && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "stage-form-wrapper", children: /* @__PURE__ */ jsxRuntime.jsx(
3381
+ StageForm,
3382
+ {
3383
+ stage,
3384
+ onUpdate,
3385
+ fieldRegistry
3386
+ }
3387
+ ) })
3388
+ ]
3389
+ }
3390
+ );
3391
+ }
3392
+ function AddStageButton({
3393
+ onClick,
3394
+ position,
3395
+ className = ""
3396
+ }) {
3397
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3398
+ "button",
3399
+ {
3400
+ type: "button",
3401
+ onClick,
3402
+ className: `add-stage-button ${position} ${className}`,
3403
+ "aria-label": `Add stage ${position === "top" ? "at top" : position === "bottom" ? "at bottom" : "below"}`,
3404
+ children: [
3405
+ /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", children: /* @__PURE__ */ jsxRuntime.jsx(
3406
+ "path",
3407
+ {
3408
+ d: "M10 5v10M5 10h10",
3409
+ stroke: "currentColor",
3410
+ strokeWidth: "2",
3411
+ strokeLinecap: "round"
3412
+ }
3413
+ ) }),
3414
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
3415
+ position === "top" && "Add Stage",
3416
+ position === "bottom" && "Add Stage Below",
3417
+ position === "inline" && "Add Stage"
3418
+ ] })
3419
+ ]
3420
+ }
3421
+ );
3422
+ }
3423
+ function generateStageId() {
3424
+ return `stage-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
3425
+ }
3426
+ function createEmptyStage(order) {
3427
+ return {
3428
+ id: generateStageId(),
3429
+ order,
3430
+ name: `Stage ${order + 1}`,
3431
+ description: "",
3432
+ filter_logic: "AND",
3433
+ rules: [],
3434
+ match_action: "continue",
3435
+ no_match_action: "continue",
3436
+ match_tags: [],
3437
+ no_match_tags: []
3438
+ };
3439
+ }
3440
+ function validateStageName(name, stages, currentStageId) {
3441
+ const trimmedName = name.trim();
3442
+ if (!trimmedName) {
3443
+ return "Stage name is required";
3444
+ }
3445
+ const duplicate = stages.find(
3446
+ (s) => s.id !== currentStageId && s.name.trim().toLowerCase() === trimmedName.toLowerCase()
3447
+ );
3448
+ if (duplicate) {
3449
+ return "Stage name must be unique";
3450
+ }
3451
+ return null;
3452
+ }
3453
+ function FunnelStageBuilder({
3454
+ funnel,
3455
+ onUpdate,
3456
+ fieldRegistry,
3457
+ className = ""
3458
+ }) {
3459
+ const [expandedStages, setExpandedStages] = react.useState(
3460
+ new Set(funnel.stages.map((s) => s.id))
3461
+ );
3462
+ const [errors, setErrors] = react.useState(/* @__PURE__ */ new Map());
3463
+ const sensors = core.useSensors(
3464
+ core.useSensor(core.PointerSensor),
3465
+ core.useSensor(core.KeyboardSensor, {
3466
+ coordinateGetter: sortable.sortableKeyboardCoordinates
3467
+ })
3468
+ );
3469
+ const toggleExpanded = react.useCallback((stageId) => {
3470
+ setExpandedStages((prev) => {
3471
+ const next = new Set(prev);
3472
+ if (next.has(stageId)) {
3473
+ next.delete(stageId);
3474
+ } else {
3475
+ next.add(stageId);
3476
+ }
3477
+ return next;
3478
+ });
3479
+ }, []);
3480
+ const handleAddStage = react.useCallback((insertAfterIndex) => {
3481
+ const newOrder = insertAfterIndex !== void 0 ? insertAfterIndex + 1 : funnel.stages.length;
3482
+ const newStage = createEmptyStage(newOrder);
3483
+ const updatedStages = funnel.stages.map((stage) => {
3484
+ if (stage.order >= newOrder) {
3485
+ return { ...stage, order: stage.order + 1 };
3486
+ }
3487
+ return stage;
3488
+ });
3489
+ updatedStages.splice(newOrder, 0, newStage);
3490
+ setExpandedStages((prev) => new Set(prev).add(newStage.id));
3491
+ onUpdate({
3492
+ ...funnel,
3493
+ stages: updatedStages
3494
+ });
3495
+ }, [funnel, onUpdate]);
3496
+ const handleRemoveStage = react.useCallback((stageId) => {
3497
+ const stageIndex = funnel.stages.findIndex((s) => s.id === stageId);
3498
+ if (stageIndex === -1) return;
3499
+ const updatedStages = funnel.stages.filter((s) => s.id !== stageId);
3500
+ updatedStages.forEach((stage, index) => {
3501
+ stage.order = index;
3502
+ });
3503
+ setExpandedStages((prev) => {
3504
+ const next = new Set(prev);
3505
+ next.delete(stageId);
3506
+ return next;
3507
+ });
3508
+ setErrors((prev) => {
3509
+ const next = new Map(prev);
3510
+ next.delete(stageId);
3511
+ return next;
3512
+ });
3513
+ onUpdate({
3514
+ ...funnel,
3515
+ stages: updatedStages
3516
+ });
3517
+ }, [funnel, onUpdate]);
3518
+ const handleUpdateStage = react.useCallback((updatedStage) => {
3519
+ const nameError = validateStageName(updatedStage.name, funnel.stages, updatedStage.id);
3520
+ setErrors((prev) => {
3521
+ const next = new Map(prev);
3522
+ if (nameError) {
3523
+ next.set(updatedStage.id, nameError);
3524
+ } else {
3525
+ next.delete(updatedStage.id);
3526
+ }
3527
+ return next;
3528
+ });
3529
+ const updatedStages = funnel.stages.map(
3530
+ (stage) => stage.id === updatedStage.id ? updatedStage : stage
3531
+ );
3532
+ onUpdate({
3533
+ ...funnel,
3534
+ stages: updatedStages
3535
+ });
3536
+ }, [funnel, onUpdate]);
3537
+ const handleDragEnd = react.useCallback((event) => {
3538
+ const { active, over } = event;
3539
+ if (!over || active.id === over.id) {
3540
+ return;
3541
+ }
3542
+ const oldIndex = funnel.stages.findIndex((s) => s.id === active.id);
3543
+ const newIndex = funnel.stages.findIndex((s) => s.id === over.id);
3544
+ if (oldIndex === -1 || newIndex === -1) {
3545
+ return;
3546
+ }
3547
+ const reorderedStages = sortable.arrayMove(funnel.stages, oldIndex, newIndex);
3548
+ reorderedStages.forEach((stage, index) => {
3549
+ stage.order = index;
3550
+ });
3551
+ onUpdate({
3552
+ ...funnel,
3553
+ stages: reorderedStages
3554
+ });
3555
+ }, [funnel, onUpdate]);
3556
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `funnel-stage-builder ${className}`, children: [
3557
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-4", children: /* @__PURE__ */ jsxRuntime.jsx(AddStageButton, { onClick: () => handleAddStage(), position: "top" }) }),
3558
+ /* @__PURE__ */ jsxRuntime.jsx(
3559
+ core.DndContext,
3560
+ {
3561
+ sensors,
3562
+ collisionDetection: core.closestCenter,
3563
+ onDragEnd: handleDragEnd,
3564
+ children: /* @__PURE__ */ jsxRuntime.jsx(
3565
+ sortable.SortableContext,
3566
+ {
3567
+ items: funnel.stages.map((s) => s.id),
3568
+ strategy: sortable.verticalListSortingStrategy,
3569
+ children: funnel.stages.map((stage, index) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "stage-wrapper", children: [
3570
+ /* @__PURE__ */ jsxRuntime.jsx(
3571
+ StageCard,
3572
+ {
3573
+ stage,
3574
+ expanded: expandedStages.has(stage.id),
3575
+ onToggleExpanded: () => toggleExpanded(stage.id),
3576
+ onUpdate: handleUpdateStage,
3577
+ onRemove: () => handleRemoveStage(stage.id),
3578
+ fieldRegistry,
3579
+ error: errors.get(stage.id),
3580
+ showWarnings: stage.rules.length === 0
3581
+ }
3582
+ ),
3583
+ index < funnel.stages.length - 1 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "stage-arrow", children: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", children: /* @__PURE__ */ jsxRuntime.jsx(
3584
+ "path",
3585
+ {
3586
+ d: "M12 5v14m0 0l-4-4m4 4l4-4",
3587
+ stroke: "currentColor",
3588
+ strokeWidth: "2",
3589
+ strokeLinecap: "round",
3590
+ strokeLinejoin: "round"
3591
+ }
3592
+ ) }) })
3593
+ ] }, stage.id))
3594
+ }
3595
+ )
3596
+ }
3597
+ ),
3598
+ funnel.stages.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-4", children: /* @__PURE__ */ jsxRuntime.jsx(
3599
+ AddStageButton,
3600
+ {
3601
+ onClick: () => handleAddStage(funnel.stages.length - 1),
3602
+ position: "bottom"
3603
+ }
3604
+ ) }),
3605
+ funnel.stages.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "empty-state", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-500 text-center py-8", children: "No stages yet. Add your first stage to get started." }) })
3606
+ ] });
3607
+ }
3608
+ function RunFilters({
3609
+ filters,
3610
+ onFiltersChange,
3611
+ className = ""
3612
+ }) {
3613
+ const updateFilter = (key, value) => {
3614
+ onFiltersChange({ ...filters, [key]: value });
3615
+ };
3616
+ const clearFilters = () => {
3617
+ onFiltersChange({
3618
+ status: "all",
3619
+ trigger_type: "all",
3620
+ date_range: "month"
3621
+ });
3622
+ };
3623
+ const hasActiveFilters = filters.status !== "all" || filters.trigger_type !== "all" || filters.date_range !== "month";
3624
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3625
+ "div",
3626
+ {
3627
+ className: `flex items-center gap-3 p-3 bg-gray-50 border-b border-gray-200 ${className}`,
3628
+ children: [
3629
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
3630
+ /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "status-filter", className: "text-sm font-medium text-gray-700", children: "Status:" }),
3631
+ /* @__PURE__ */ jsxRuntime.jsxs(
3632
+ "select",
3633
+ {
3634
+ id: "status-filter",
3635
+ value: filters.status || "all",
3636
+ onChange: (e) => updateFilter("status", e.target.value),
3637
+ className: "px-2 py-1 text-sm border border-gray-300 rounded-md bg-white focus:outline-none focus:ring-2 focus:ring-blue-500",
3638
+ children: [
3639
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "all", children: "All" }),
3640
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "completed", children: "Complete" }),
3641
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "running", children: "Running" }),
3642
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "failed", children: "Failed" }),
3643
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "pending", children: "Pending" }),
3644
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "cancelled", children: "Cancelled" })
3645
+ ]
3646
+ }
3647
+ )
3648
+ ] }),
3649
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
3650
+ /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "trigger-filter", className: "text-sm font-medium text-gray-700", children: "Trigger:" }),
3651
+ /* @__PURE__ */ jsxRuntime.jsxs(
3652
+ "select",
3653
+ {
3654
+ id: "trigger-filter",
3655
+ value: filters.trigger_type || "all",
3656
+ onChange: (e) => updateFilter("trigger_type", e.target.value),
3657
+ className: "px-2 py-1 text-sm border border-gray-300 rounded-md bg-white focus:outline-none focus:ring-2 focus:ring-blue-500",
3658
+ children: [
3659
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "all", children: "All" }),
3660
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "manual", children: "Manual" }),
3661
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "scheduled", children: "Scheduled" }),
3662
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "webhook", children: "Webhook" }),
3663
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "api", children: "API" })
3664
+ ]
3665
+ }
3666
+ )
3667
+ ] }),
3668
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
3669
+ /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "date-filter", className: "text-sm font-medium text-gray-700", children: "Date:" }),
3670
+ /* @__PURE__ */ jsxRuntime.jsxs(
3671
+ "select",
3672
+ {
3673
+ id: "date-filter",
3674
+ value: filters.date_range || "month",
3675
+ onChange: (e) => updateFilter("date_range", e.target.value),
3676
+ className: "px-2 py-1 text-sm border border-gray-300 rounded-md bg-white focus:outline-none focus:ring-2 focus:ring-blue-500",
3677
+ children: [
3678
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "all", children: "All time" }),
3679
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "today", children: "Today" }),
3680
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "week", children: "Last 7 days" }),
3681
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "month", children: "Last 30 days" })
3682
+ ]
3683
+ }
3684
+ )
3685
+ ] }),
3686
+ hasActiveFilters && /* @__PURE__ */ jsxRuntime.jsx(
3687
+ "button",
3688
+ {
3689
+ onClick: clearFilters,
3690
+ className: "ml-auto px-3 py-1 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500",
3691
+ children: "Clear filters"
3692
+ }
3693
+ )
3694
+ ]
3695
+ }
3696
+ );
3697
+ }
3698
+ var statusConfig2 = {
3699
+ completed: {
3700
+ icon: "\u2713",
3701
+ label: "Complete",
3702
+ color: "text-green-800",
3703
+ bgColor: "bg-green-100"
3704
+ },
3705
+ running: {
3706
+ icon: "\u23F8",
3707
+ label: "Running",
3708
+ color: "text-blue-800",
3709
+ bgColor: "bg-blue-100",
3710
+ spinning: true
3711
+ },
3712
+ failed: {
3713
+ icon: "\u2717",
3714
+ label: "Failed",
3715
+ color: "text-red-800",
3716
+ bgColor: "bg-red-100"
3717
+ },
3718
+ pending: {
3719
+ icon: "\u25CB",
3720
+ label: "Pending",
3721
+ color: "text-yellow-800",
3722
+ bgColor: "bg-yellow-100"
3723
+ },
3724
+ cancelled: {
3725
+ icon: "\xD7",
3726
+ label: "Cancelled",
3727
+ color: "text-gray-800",
3728
+ bgColor: "bg-gray-100"
3729
+ }
3730
+ };
3731
+ function RunStatusBadge({ status, className = "" }) {
3732
+ const config = statusConfig2[status];
3733
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3734
+ "span",
3735
+ {
3736
+ className: `inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-medium ${config.bgColor} ${config.color} ${className}`,
3737
+ children: [
3738
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: config.spinning ? "animate-spin" : "", "aria-hidden": "true", children: config.icon }),
3739
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: config.label })
3740
+ ]
3741
+ }
3742
+ );
3743
+ }
3744
+ function RunActions({
3745
+ run,
3746
+ onViewDetails,
3747
+ onViewResults,
3748
+ onReRun,
3749
+ onCancel,
3750
+ className = ""
3751
+ }) {
3752
+ const [isOpen, setIsOpen] = react.useState(false);
3753
+ const dropdownRef = react.useRef(null);
3754
+ react.useEffect(() => {
3755
+ const handleClickOutside = (event) => {
3756
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
3757
+ setIsOpen(false);
3758
+ }
3759
+ };
3760
+ if (isOpen) {
3761
+ document.addEventListener("mousedown", handleClickOutside);
3762
+ return () => document.removeEventListener("mousedown", handleClickOutside);
3763
+ }
3764
+ }, [isOpen]);
3765
+ react.useEffect(() => {
3766
+ const handleEscape = (event) => {
3767
+ if (event.key === "Escape" && isOpen) {
3768
+ setIsOpen(false);
3769
+ }
3770
+ };
3771
+ if (isOpen) {
3772
+ document.addEventListener("keydown", handleEscape);
3773
+ return () => document.removeEventListener("keydown", handleEscape);
3774
+ }
3775
+ }, [isOpen]);
3776
+ const canCancel = run.status === "pending" || run.status === "running";
3777
+ const canViewResults = run.status === "completed";
3778
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `relative ${className}`, ref: dropdownRef, children: [
3779
+ /* @__PURE__ */ jsxRuntime.jsx(
3780
+ "button",
3781
+ {
3782
+ onClick: () => setIsOpen(!isOpen),
3783
+ className: "p-1 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded focus:outline-none focus:ring-2 focus:ring-blue-500",
3784
+ "aria-label": "Run actions",
3785
+ "aria-haspopup": "true",
3786
+ "aria-expanded": isOpen,
3787
+ children: /* @__PURE__ */ jsxRuntime.jsx(
3788
+ "svg",
3789
+ {
3790
+ className: "w-5 h-5",
3791
+ fill: "none",
3792
+ stroke: "currentColor",
3793
+ viewBox: "0 0 24 24",
3794
+ children: /* @__PURE__ */ jsxRuntime.jsx(
3795
+ "path",
3796
+ {
3797
+ strokeLinecap: "round",
3798
+ strokeLinejoin: "round",
3799
+ strokeWidth: 2,
3800
+ d: "M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z"
3801
+ }
3802
+ )
3803
+ }
3804
+ )
3805
+ }
3806
+ ),
3807
+ isOpen && /* @__PURE__ */ jsxRuntime.jsx(
3808
+ "div",
3809
+ {
3810
+ className: "absolute right-0 mt-1 w-48 bg-white rounded-md shadow-lg border border-gray-200 z-10",
3811
+ role: "menu",
3812
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "py-1", children: [
3813
+ /* @__PURE__ */ jsxRuntime.jsxs(
3814
+ "button",
3815
+ {
3816
+ onClick: () => {
3817
+ onViewDetails(run);
3818
+ setIsOpen(false);
3819
+ },
3820
+ className: "w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2",
3821
+ role: "menuitem",
3822
+ children: [
3823
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\u{1F441}" }),
3824
+ "View Details"
3825
+ ]
3826
+ }
3827
+ ),
3828
+ /* @__PURE__ */ jsxRuntime.jsxs(
3829
+ "button",
3830
+ {
3831
+ onClick: () => {
3832
+ onViewResults(run);
3833
+ setIsOpen(false);
3834
+ },
3835
+ disabled: !canViewResults,
3836
+ className: "w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-white",
3837
+ role: "menuitem",
3838
+ children: [
3839
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\u{1F4CA}" }),
3840
+ "View Results"
3841
+ ]
3842
+ }
3843
+ ),
3844
+ /* @__PURE__ */ jsxRuntime.jsxs(
3845
+ "button",
3846
+ {
3847
+ onClick: () => {
3848
+ onReRun(run);
3849
+ setIsOpen(false);
3850
+ },
3851
+ className: "w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2",
3852
+ role: "menuitem",
3853
+ children: [
3854
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\u21BB" }),
3855
+ "Re-run"
3856
+ ]
3857
+ }
3858
+ ),
3859
+ canCancel && onCancel && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3860
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-t border-gray-200 my-1" }),
3861
+ /* @__PURE__ */ jsxRuntime.jsxs(
3862
+ "button",
3863
+ {
3864
+ onClick: () => {
3865
+ if (confirm("Are you sure you want to cancel this run?")) {
3866
+ onCancel(run);
3867
+ setIsOpen(false);
3868
+ }
3869
+ },
3870
+ className: "w-full px-4 py-2 text-left text-sm text-red-600 hover:bg-red-50 flex items-center gap-2",
3871
+ role: "menuitem",
3872
+ children: [
3873
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\xD7" }),
3874
+ "Cancel Run"
3875
+ ]
3876
+ }
3877
+ )
3878
+ ] })
3879
+ ] })
3880
+ }
3881
+ )
3882
+ ] });
3883
+ }
3884
+
3885
+ // src/components/FunnelRunHistory/utils.ts
3886
+ function formatDuration(ms) {
3887
+ if (ms === void 0 || ms === null) return "-";
3888
+ if (ms === 0) return "0ms";
3889
+ const seconds = Math.floor(ms / 1e3);
3890
+ const minutes = Math.floor(seconds / 60);
3891
+ const hours = Math.floor(minutes / 60);
3892
+ if (hours > 0) {
3893
+ const remainingMinutes = minutes % 60;
3894
+ return remainingMinutes > 0 ? `${hours}h ${remainingMinutes}m` : `${hours}h`;
3895
+ }
3896
+ if (minutes > 0) {
3897
+ const remainingSeconds = seconds % 60;
3898
+ return remainingSeconds > 0 ? `${minutes}m ${remainingSeconds}s` : `${minutes}m`;
3899
+ }
3900
+ if (seconds > 0) {
3901
+ return `${seconds}s`;
3902
+ }
3903
+ return `${ms}ms`;
3904
+ }
3905
+ function formatRelativeTime(date) {
3906
+ const now = /* @__PURE__ */ new Date();
3907
+ const then = new Date(date);
3908
+ const diffMs = now.getTime() - then.getTime();
3909
+ const diffSeconds = Math.floor(diffMs / 1e3);
3910
+ const diffMinutes = Math.floor(diffSeconds / 60);
3911
+ const diffHours = Math.floor(diffMinutes / 60);
3912
+ const diffDays = Math.floor(diffHours / 24);
3913
+ if (diffDays > 0) {
3914
+ return `${diffDays}d ago`;
3915
+ }
3916
+ if (diffHours > 0) {
3917
+ return `${diffHours}h ago`;
3918
+ }
3919
+ if (diffMinutes > 0) {
3920
+ return `${diffMinutes}m ago`;
3921
+ }
3922
+ return "Just now";
3923
+ }
3924
+ function calculateMatchRate(matched, total) {
3925
+ if (total === 0) return 0;
3926
+ return Math.round(matched / total * 100);
3927
+ }
3928
+ function formatNumber(num) {
3929
+ return num.toLocaleString();
3930
+ }
3931
+ function formatFullTimestamp(date) {
3932
+ const d = new Date(date);
3933
+ return d.toLocaleString("en-US", {
3934
+ year: "numeric",
3935
+ month: "short",
3936
+ day: "numeric",
3937
+ hour: "2-digit",
3938
+ minute: "2-digit",
3939
+ second: "2-digit"
3940
+ });
3941
+ }
3942
+ function RunRow({
3943
+ run,
3944
+ onViewDetails,
3945
+ onViewResults,
3946
+ onReRun,
3947
+ onCancel
3948
+ }) {
3949
+ const matchRate = run.status === "completed" ? calculateMatchRate(run.total_matched, run.total_input) : null;
3950
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3951
+ "tr",
3952
+ {
3953
+ onClick: () => onViewDetails(run),
3954
+ onKeyDown: (e) => {
3955
+ if (e.key === "Enter") {
3956
+ onViewDetails(run);
3957
+ }
3958
+ },
3959
+ tabIndex: 0,
3960
+ className: "border-b border-gray-200 hover:bg-gray-50 cursor-pointer transition-colors focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500",
3961
+ children: [
3962
+ /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-3", children: /* @__PURE__ */ jsxRuntime.jsx(
3963
+ "span",
3964
+ {
3965
+ className: "text-sm text-gray-900",
3966
+ title: formatFullTimestamp(run.started_at),
3967
+ children: formatRelativeTime(run.started_at)
3968
+ }
3969
+ ) }),
3970
+ /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-3", children: /* @__PURE__ */ jsxRuntime.jsx(RunStatusBadge, { status: run.status }) }),
3971
+ /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-3", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-gray-700 capitalize", children: run.trigger_type }) }),
3972
+ /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-3", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-gray-900", children: formatDuration(run.duration_ms) }) }),
3973
+ /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-3 text-right", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium text-gray-900", children: formatNumber(run.total_input) }) }),
3974
+ /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-3 text-right", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium text-green-600", children: run.status === "completed" ? formatNumber(run.total_matched) : "-" }) }),
3975
+ /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-3 text-right", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium text-gray-900", children: matchRate !== null ? `${matchRate}%` : "-" }) }),
3976
+ /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-3", onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsxRuntime.jsx(
3977
+ RunActions,
3978
+ {
3979
+ run,
3980
+ onViewDetails,
3981
+ onViewResults,
3982
+ onReRun,
3983
+ onCancel
3984
+ }
3985
+ ) })
3986
+ ]
3987
+ }
3988
+ );
3989
+ }
3990
+ function StageBreakdownList({
3991
+ stages,
3992
+ className = ""
3993
+ }) {
3994
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: `space-y-3 ${className}`, children: stages.map((stage, index) => {
3995
+ const delta = stage.matched_count - stage.input_count;
3996
+ const matchRate = stage.input_count > 0 ? Math.round(stage.matched_count / stage.input_count * 100) : 0;
3997
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3998
+ "div",
3999
+ {
4000
+ className: "p-3 bg-gray-50 rounded-lg border border-gray-200",
4001
+ children: [
4002
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mb-2", children: [
4003
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex items-center justify-center w-6 h-6 text-xs font-bold text-white bg-blue-600 rounded-full", children: index + 1 }),
4004
+ /* @__PURE__ */ jsxRuntime.jsx("h4", { className: "text-sm font-semibold text-gray-900", children: stage.stage_name })
4005
+ ] }),
4006
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-3 gap-2 text-center", children: [
4007
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
4008
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-gray-600 mb-1", children: "Input" }),
4009
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-lg font-bold text-blue-600", children: formatNumber(stage.input_count) })
4010
+ ] }),
4011
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
4012
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-gray-600 mb-1", children: "Matched" }),
4013
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-lg font-bold text-green-600", children: formatNumber(stage.matched_count) })
4014
+ ] }),
4015
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
4016
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-gray-600 mb-1", children: "Rate" }),
4017
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-lg font-bold text-gray-700", children: [
4018
+ matchRate,
4019
+ "%"
4020
+ ] })
4021
+ ] })
4022
+ ] }),
4023
+ delta !== 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-2 pt-2 border-t border-gray-200", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-center gap-1 text-sm", children: [
4024
+ /* @__PURE__ */ jsxRuntime.jsxs(
4025
+ "span",
4026
+ {
4027
+ className: `font-medium ${delta > 0 ? "text-green-600" : "text-red-600"}`,
4028
+ children: [
4029
+ delta > 0 ? "\u25B2" : "\u25BC",
4030
+ " ",
4031
+ formatNumber(Math.abs(delta))
4032
+ ]
4033
+ }
4034
+ ),
4035
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-500", children: delta > 0 ? "added" : "excluded" })
4036
+ ] }) }),
4037
+ stage.error_count && stage.error_count > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-2 pt-2 border-t border-gray-200", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-sm text-red-600 text-center", children: [
4038
+ "\u26A0 ",
4039
+ formatNumber(stage.error_count),
4040
+ " errors"
4041
+ ] }) })
4042
+ ]
4043
+ },
4044
+ stage.stage_id
4045
+ );
4046
+ }) });
4047
+ }
4048
+ function RunDetailsModal({
4049
+ run,
4050
+ onClose,
4051
+ onViewResults,
4052
+ onReRun
4053
+ }) {
4054
+ const modalRef = react.useRef(null);
4055
+ react.useEffect(() => {
4056
+ const handleEscape = (e) => {
4057
+ if (e.key === "Escape") {
4058
+ onClose();
4059
+ }
4060
+ };
4061
+ if (run) {
4062
+ document.addEventListener("keydown", handleEscape);
4063
+ document.body.style.overflow = "hidden";
4064
+ return () => {
4065
+ document.removeEventListener("keydown", handleEscape);
4066
+ document.body.style.overflow = "unset";
4067
+ };
4068
+ }
4069
+ }, [run, onClose]);
4070
+ react.useEffect(() => {
4071
+ if (run && modalRef.current) {
4072
+ const focusableElements = modalRef.current.querySelectorAll(
4073
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
4074
+ );
4075
+ const firstElement = focusableElements[0];
4076
+ const lastElement = focusableElements[focusableElements.length - 1];
4077
+ firstElement?.focus();
4078
+ const handleTab = (e) => {
4079
+ if (e.key !== "Tab") return;
4080
+ if (e.shiftKey && document.activeElement === firstElement) {
4081
+ e.preventDefault();
4082
+ lastElement?.focus();
4083
+ } else if (!e.shiftKey && document.activeElement === lastElement) {
4084
+ e.preventDefault();
4085
+ firstElement?.focus();
4086
+ }
4087
+ };
4088
+ document.addEventListener("keydown", handleTab);
4089
+ return () => document.removeEventListener("keydown", handleTab);
4090
+ }
4091
+ }, [run]);
4092
+ if (!run) return null;
4093
+ const stageStatsArray = Object.values(run.stage_stats);
4094
+ return /* @__PURE__ */ jsxRuntime.jsx(
4095
+ "div",
4096
+ {
4097
+ className: "fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50",
4098
+ onClick: onClose,
4099
+ role: "dialog",
4100
+ "aria-modal": "true",
4101
+ "aria-labelledby": "modal-title",
4102
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
4103
+ "div",
4104
+ {
4105
+ ref: modalRef,
4106
+ onClick: (e) => e.stopPropagation(),
4107
+ className: "bg-white rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-hidden flex flex-col",
4108
+ children: [
4109
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4 border-b border-gray-200", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
4110
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { id: "modal-title", className: "text-lg font-semibold text-gray-900", children: "Run Details" }),
4111
+ /* @__PURE__ */ jsxRuntime.jsx(
4112
+ "button",
4113
+ {
4114
+ onClick: onClose,
4115
+ className: "p-1 text-gray-400 hover:text-gray-600 rounded focus:outline-none focus:ring-2 focus:ring-blue-500",
4116
+ "aria-label": "Close modal",
4117
+ children: /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-5 h-5", fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ jsxRuntime.jsx(
4118
+ "path",
4119
+ {
4120
+ fillRule: "evenodd",
4121
+ d: "M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z",
4122
+ clipRule: "evenodd"
4123
+ }
4124
+ ) })
4125
+ }
4126
+ )
4127
+ ] }) }),
4128
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4 overflow-y-auto flex-1", children: [
4129
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-6", children: [
4130
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-4 mb-4", children: [
4131
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
4132
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-gray-600 mb-1", children: "Status" }),
4133
+ /* @__PURE__ */ jsxRuntime.jsx(RunStatusBadge, { status: run.status })
4134
+ ] }),
4135
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
4136
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-gray-600 mb-1", children: "Duration" }),
4137
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm font-medium text-gray-900", children: formatDuration(run.duration_ms) })
4138
+ ] }),
4139
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
4140
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-gray-600 mb-1", children: "Started" }),
4141
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm font-medium text-gray-900", children: /* @__PURE__ */ jsxRuntime.jsx("span", { title: formatFullTimestamp(run.started_at), children: formatRelativeTime(run.started_at) }) })
4142
+ ] }),
4143
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
4144
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-gray-600 mb-1", children: "Triggered by" }),
4145
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-sm font-medium text-gray-900", children: [
4146
+ run.triggered_by || "System",
4147
+ " (",
4148
+ run.trigger_type,
4149
+ ")"
4150
+ ] })
4151
+ ] })
4152
+ ] }),
4153
+ run.status === "failed" && run.error && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-3 bg-red-50 border border-red-200 rounded-lg", children: [
4154
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm font-medium text-red-800 mb-1", children: "Error" }),
4155
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm text-red-700", children: run.error })
4156
+ ] })
4157
+ ] }),
4158
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
4159
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-sm font-semibold text-gray-900 mb-3", children: "Stage Breakdown" }),
4160
+ stageStatsArray.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(StageBreakdownList, { stages: stageStatsArray }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm text-gray-500 text-center py-4", children: "No stage data available" })
4161
+ ] })
4162
+ ] }),
4163
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4 border-t border-gray-200 flex items-center justify-end gap-3", children: [
4164
+ /* @__PURE__ */ jsxRuntime.jsx(
4165
+ "button",
4166
+ {
4167
+ onClick: onClose,
4168
+ className: "px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500",
4169
+ children: "Close"
4170
+ }
4171
+ ),
4172
+ onReRun && /* @__PURE__ */ jsxRuntime.jsx(
4173
+ "button",
4174
+ {
4175
+ onClick: () => {
4176
+ onReRun(run);
4177
+ onClose();
4178
+ },
4179
+ className: "px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500",
4180
+ children: "\u21BB Re-run"
4181
+ }
4182
+ ),
4183
+ onViewResults && run.status === "completed" && /* @__PURE__ */ jsxRuntime.jsx(
4184
+ "button",
4185
+ {
4186
+ onClick: () => {
4187
+ onViewResults(run);
4188
+ onClose();
4189
+ },
4190
+ className: "px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500",
4191
+ children: "View Results"
4192
+ }
4193
+ )
4194
+ ] })
4195
+ ]
4196
+ }
4197
+ )
4198
+ }
4199
+ );
4200
+ }
4201
+ function FunnelRunHistory({
4202
+ funnelId,
4203
+ apiClient,
4204
+ onViewResults,
4205
+ className = ""
4206
+ }) {
4207
+ const [runs, setRuns] = react.useState([]);
4208
+ const [isLoading, setIsLoading] = react.useState(true);
4209
+ const [error, setError] = react.useState(null);
4210
+ const [selectedRun, setSelectedRun] = react.useState(null);
4211
+ const [filters, setFilters] = react.useState({
4212
+ status: "all",
4213
+ trigger_type: "all",
4214
+ date_range: "month"
4215
+ });
4216
+ const [pagination, setPagination] = react.useState({
4217
+ page: 1,
4218
+ page_size: 10,
4219
+ total: 0
4220
+ });
4221
+ const [isRefreshing, setIsRefreshing] = react.useState(false);
4222
+ const loadRuns = react.useCallback(async () => {
4223
+ try {
4224
+ setError(null);
4225
+ const params = {
4226
+ page: pagination.page,
4227
+ page_size: pagination.page_size,
4228
+ ordering: "-started_at"
4229
+ // Most recent first
4230
+ };
4231
+ if (filters.status && filters.status !== "all") {
4232
+ params.status = filters.status;
4233
+ }
4234
+ if (filters.trigger_type && filters.trigger_type !== "all") {
4235
+ params.trigger_type = filters.trigger_type;
4236
+ }
4237
+ const response = await apiClient.getFunnelRuns(funnelId, params);
4238
+ setRuns(response.results);
4239
+ setPagination((prev) => ({
4240
+ ...prev,
4241
+ total: response.count
4242
+ }));
4243
+ } catch (err) {
4244
+ setError(err instanceof Error ? err.message : "Failed to load runs");
4245
+ console.error("Failed to load funnel runs:", err);
4246
+ } finally {
4247
+ setIsLoading(false);
4248
+ setIsRefreshing(false);
4249
+ }
4250
+ }, [funnelId, apiClient, pagination.page, pagination.page_size, filters]);
4251
+ react.useEffect(() => {
4252
+ loadRuns();
4253
+ }, [loadRuns]);
4254
+ react.useEffect(() => {
4255
+ const hasActiveRuns = runs.some(
4256
+ (r) => r.status === "pending" || r.status === "running"
4257
+ );
4258
+ if (hasActiveRuns) {
4259
+ const interval = setInterval(() => {
4260
+ setIsRefreshing(true);
4261
+ loadRuns();
4262
+ }, 5e3);
4263
+ return () => clearInterval(interval);
4264
+ }
4265
+ }, [runs, loadRuns]);
4266
+ const handleRefresh = () => {
4267
+ setIsRefreshing(true);
4268
+ loadRuns();
4269
+ };
4270
+ const handleReRun = async (run) => {
4271
+ try {
4272
+ await apiClient.runFunnel(funnelId, {
4273
+ trigger_type: "manual",
4274
+ metadata: { re_run_of: run.id }
4275
+ });
4276
+ loadRuns();
4277
+ } catch (err) {
4278
+ console.error("Failed to re-run funnel:", err);
4279
+ alert("Failed to re-run funnel. Please try again.");
4280
+ }
4281
+ };
4282
+ const handleCancel = async (run) => {
4283
+ try {
4284
+ await apiClient.cancelFunnelRun(run.id);
4285
+ loadRuns();
4286
+ } catch (err) {
4287
+ console.error("Failed to cancel run:", err);
4288
+ alert("Failed to cancel run. Please try again.");
4289
+ }
4290
+ };
4291
+ const handleViewResults = (run) => {
4292
+ if (onViewResults) {
4293
+ onViewResults(run);
4294
+ } else {
4295
+ setSelectedRun(run);
4296
+ }
4297
+ };
4298
+ const totalPages = Math.ceil(pagination.total / pagination.page_size);
4299
+ const canGoBack = pagination.page > 1;
4300
+ const canGoForward = pagination.page < totalPages;
4301
+ const handlePreviousPage = () => {
4302
+ if (canGoBack) {
4303
+ setPagination((prev) => ({ ...prev, page: prev.page - 1 }));
4304
+ }
4305
+ };
4306
+ const handleNextPage = () => {
4307
+ if (canGoForward) {
4308
+ setPagination((prev) => ({ ...prev, page: prev.page + 1 }));
4309
+ }
4310
+ };
4311
+ const startIndex = (pagination.page - 1) * pagination.page_size + 1;
4312
+ const endIndex = Math.min(
4313
+ pagination.page * pagination.page_size,
4314
+ pagination.total
4315
+ );
4316
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `bg-white rounded-lg border border-gray-200 ${className}`, children: [
4317
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4 border-b border-gray-200 flex items-center justify-between", children: [
4318
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-semibold text-gray-900", children: "Funnel Run History" }),
4319
+ /* @__PURE__ */ jsxRuntime.jsxs(
4320
+ "button",
4321
+ {
4322
+ onClick: handleRefresh,
4323
+ disabled: isRefreshing,
4324
+ className: "px-3 py-1.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus:ring-2 focus:ring-blue-500",
4325
+ "aria-label": "Refresh run history",
4326
+ children: [
4327
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: isRefreshing ? "animate-spin inline-block" : "", children: "\u21BB" }),
4328
+ " ",
4329
+ "Refresh"
4330
+ ]
4331
+ }
4332
+ )
4333
+ ] }),
4334
+ /* @__PURE__ */ jsxRuntime.jsx(RunFilters, { filters, onFiltersChange: setFilters }),
4335
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "w-full", children: [
4336
+ /* @__PURE__ */ jsxRuntime.jsx("thead", { className: "bg-gray-50 border-b border-gray-200", children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
4337
+ /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-left text-xs font-medium text-gray-600 uppercase tracking-wider", children: "Date" }),
4338
+ /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-left text-xs font-medium text-gray-600 uppercase tracking-wider", children: "Status" }),
4339
+ /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-left text-xs font-medium text-gray-600 uppercase tracking-wider", children: "Trigger" }),
4340
+ /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-left text-xs font-medium text-gray-600 uppercase tracking-wider", children: "Duration" }),
4341
+ /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-right text-xs font-medium text-gray-600 uppercase tracking-wider", children: "Input" }),
4342
+ /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-right text-xs font-medium text-gray-600 uppercase tracking-wider", children: "Matched" }),
4343
+ /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-right text-xs font-medium text-gray-600 uppercase tracking-wider", children: "%" }),
4344
+ /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-right text-xs font-medium text-gray-600 uppercase tracking-wider", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "sr-only", children: "Actions" }) })
4345
+ ] }) }),
4346
+ /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: isLoading && runs.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsx("td", { colSpan: 8, className: "px-4 py-12 text-center", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-gray-500", children: "Loading runs..." }) }) }) : error ? /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsxs("td", { colSpan: 8, className: "px-4 py-12 text-center", children: [
4347
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-red-600", children: [
4348
+ "Error: ",
4349
+ error
4350
+ ] }),
4351
+ /* @__PURE__ */ jsxRuntime.jsx(
4352
+ "button",
4353
+ {
4354
+ onClick: loadRuns,
4355
+ className: "mt-2 text-sm text-blue-600 hover:text-blue-700 underline",
4356
+ children: "Try again"
4357
+ }
4358
+ )
4359
+ ] }) }) : runs.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsx("td", { colSpan: 8, className: "px-4 py-12 text-center", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-gray-500", children: "No runs found. Run this funnel to see history." }) }) }) : runs.map((run) => /* @__PURE__ */ jsxRuntime.jsx(
4360
+ RunRow,
4361
+ {
4362
+ run,
4363
+ onViewDetails: setSelectedRun,
4364
+ onViewResults: handleViewResults,
4365
+ onReRun: handleReRun,
4366
+ onCancel: handleCancel
4367
+ },
4368
+ run.id
4369
+ )) })
4370
+ ] }) }),
4371
+ runs.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-3 border-t border-gray-200 flex items-center justify-between", children: [
4372
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-sm text-gray-600", children: [
4373
+ "Showing ",
4374
+ startIndex,
4375
+ "-",
4376
+ endIndex,
4377
+ " of ",
4378
+ pagination.total
4379
+ ] }),
4380
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
4381
+ /* @__PURE__ */ jsxRuntime.jsx(
4382
+ "button",
4383
+ {
4384
+ onClick: handlePreviousPage,
4385
+ disabled: !canGoBack,
4386
+ className: "px-3 py-1 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus:ring-2 focus:ring-blue-500",
4387
+ "aria-label": "Previous page",
4388
+ children: "\u2039"
4389
+ }
4390
+ ),
4391
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-sm text-gray-600", children: [
4392
+ "Page ",
4393
+ pagination.page,
4394
+ " of ",
4395
+ totalPages
4396
+ ] }),
4397
+ /* @__PURE__ */ jsxRuntime.jsx(
4398
+ "button",
4399
+ {
4400
+ onClick: handleNextPage,
4401
+ disabled: !canGoForward,
4402
+ className: "px-3 py-1 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus:ring-2 focus:ring-blue-500",
4403
+ "aria-label": "Next page",
4404
+ children: "\u203A"
4405
+ }
4406
+ )
4407
+ ] })
4408
+ ] }),
4409
+ /* @__PURE__ */ jsxRuntime.jsx(
4410
+ RunDetailsModal,
4411
+ {
4412
+ run: selectedRun,
4413
+ onClose: () => setSelectedRun(null),
4414
+ onViewResults,
4415
+ onReRun: handleReRun
4416
+ }
4417
+ )
4418
+ ] });
4419
+ }
4420
+
4421
+ exports.AddStageButton = AddStageButton;
4422
+ exports.BooleanValueInput = BooleanValueInput;
4423
+ exports.ChoiceValueInput = ChoiceValueInput;
4424
+ exports.DateValueInput = DateValueInput;
4425
+ exports.EntityCard = EntityCard;
4426
+ exports.FetchAdapter = FetchAdapter;
4427
+ exports.FieldSelector = FieldSelector;
4428
+ exports.FilterRuleEditor = FilterRuleEditor;
4429
+ exports.FlowLegend = FlowLegend;
4430
+ exports.FunnelApiClient = FunnelApiClient;
4431
+ exports.FunnelCard = FunnelCard;
4432
+ exports.FunnelEngine = FunnelEngine;
4433
+ exports.FunnelPreview = FunnelPreview;
4434
+ exports.FunnelRunHistory = FunnelRunHistory;
4435
+ exports.FunnelStageBuilder = FunnelStageBuilder;
4436
+ exports.FunnelStats = FunnelStats;
4437
+ exports.FunnelVisualFlow = FunnelVisualFlow;
4438
+ exports.LoadingPreview = LoadingPreview;
4439
+ exports.LogicToggle = LogicToggle;
4440
+ exports.MULTI_VALUE_OPERATORS = MULTI_VALUE_OPERATORS;
4441
+ exports.MatchBar = MatchBar;
4442
+ exports.MultiChoiceValueInput = MultiChoiceValueInput;
4443
+ exports.NULL_VALUE_OPERATORS = NULL_VALUE_OPERATORS;
4444
+ exports.NumberValueInput = NumberValueInput;
4445
+ exports.OPERATOR_LABELS = OPERATOR_LABELS;
4446
+ exports.OperatorSelector = OperatorSelector;
4447
+ exports.PreviewStats = PreviewStats;
4448
+ exports.RuleRow = RuleRow;
4449
+ exports.RunActions = RunActions;
4450
+ exports.RunDetailsModal = RunDetailsModal;
4451
+ exports.RunFilters = RunFilters;
4452
+ exports.RunRow = RunRow;
4453
+ exports.RunStatusBadge = RunStatusBadge;
4454
+ exports.StageActions = StageActions;
4455
+ exports.StageBreakdown = StageBreakdown;
4456
+ exports.StageBreakdownList = StageBreakdownList;
4457
+ exports.StageCard = StageCard;
4458
+ exports.StageForm = StageForm;
4459
+ exports.StageIndicator = StageIndicator;
4460
+ exports.StageNode = StageNode;
4461
+ exports.StatusBadge = StatusBadge;
4462
+ exports.TagInput = TagInput;
4463
+ exports.TextValueInput = TextValueInput;
4464
+ exports.applyOperator = applyOperator;
4465
+ exports.calculateMatchRate = calculateMatchRate;
4466
+ exports.createApiError = createApiError;
4467
+ exports.createFunnelStore = createFunnelStore;
4468
+ exports.createInitialState = createInitialState;
4469
+ exports.evaluateRule = evaluateRule;
4470
+ exports.evaluateRuleWithResult = evaluateRuleWithResult;
4471
+ exports.evaluateRules = evaluateRules;
4472
+ exports.evaluateRulesAND = evaluateRulesAND;
4473
+ exports.evaluateRulesOR = evaluateRulesOR;
4474
+ exports.evaluateRulesWithResults = evaluateRulesWithResults;
4475
+ exports.filterEntities = filterEntities;
4476
+ exports.formatDuration = formatDuration;
4477
+ exports.formatFullTimestamp = formatFullTimestamp;
4478
+ exports.formatNumber = formatNumber;
4479
+ exports.formatRelativeTime = formatRelativeTime;
4480
+ exports.getCircledNumber = getCircledNumber;
4481
+ exports.getFields = getFields;
4482
+ exports.getValidOperators = getValidOperators;
4483
+ exports.hasField = hasField;
4484
+ exports.isApiError = isApiError;
4485
+ exports.isFieldDefinition = isFieldDefinition;
4486
+ exports.isFilterRule = isFilterRule;
4487
+ exports.isFunnel = isFunnel;
4488
+ exports.isFunnelResult = isFunnelResult;
4489
+ exports.isFunnelRun = isFunnelRun;
4490
+ exports.isStage = isStage;
4491
+ exports.isValidOperator = isValidOperator;
4492
+ exports.resolveField = resolveField;
4493
+ exports.setField = setField;
4494
+ exports.useDebouncedValue = useDebouncedValue;
4495
+ exports.validateFilterRule = validateFilterRule;
4496
+ exports.validateFunnel = validateFunnel;
4497
+ exports.validateStage = validateStage;
4498
+ //# sourceMappingURL=index.cjs.map
4499
+ //# sourceMappingURL=index.cjs.map