astro-tractstack 2.0.1 → 2.0.3

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.
@@ -2,8 +2,9 @@ import { useEffect, useRef, useState } from 'react';
2
2
  import * as d3 from 'd3';
3
3
  import { sankey, sankeyLinkHorizontal } from 'd3-sankey';
4
4
 
5
- const MAX_HEIGHT = 1200;
6
- const COMPRESSED_HEIGHT = 384; // Fixed height for compressed view
5
+ const MAX_HEIGHT = 1600;
6
+ const COMPRESSED_HEIGHT = 256;
7
+ const MIN_DIAGRAM_WIDTH = 800; // Define a minimum width for the diagram
7
8
 
8
9
  const colors = [
9
10
  '#ef4444',
@@ -52,7 +53,7 @@ const SankeyDiagram = ({ data, isLoading = false }: SankeyDiagramProps) => {
52
53
  const containerRef = useRef<HTMLDivElement | null>(null);
53
54
  const hasScrolledRef = useRef(false);
54
55
  const [dimensions, setDimensions] = useState({
55
- width: 800,
56
+ width: MIN_DIAGRAM_WIDTH,
56
57
  height: 500,
57
58
  });
58
59
  const [isExpanded, setIsExpanded] = useState(false);
@@ -60,7 +61,11 @@ const SankeyDiagram = ({ data, isLoading = false }: SankeyDiagramProps) => {
60
61
  useEffect(() => {
61
62
  const updateDimensions = () => {
62
63
  if (containerRef.current) {
63
- const containerWidth = containerRef.current.offsetWidth;
64
+ // Ensure the diagram width is the larger of the container or our defined minimum
65
+ const containerWidth = Math.max(
66
+ MIN_DIAGRAM_WIDTH,
67
+ containerRef.current.offsetWidth
68
+ );
64
69
  const nodeCount = data.nodes.length || 1;
65
70
  const optimalHeight = nodeCount * (40 + 10) + 50;
66
71
  const finalHeight = Math.min(MAX_HEIGHT, optimalHeight);
@@ -245,7 +250,6 @@ const SankeyDiagram = ({ data, isLoading = false }: SankeyDiagramProps) => {
245
250
 
246
251
  return (
247
252
  <div ref={containerRef} className="relative w-full">
248
- {/* Expand/Compress Controls */}
249
253
  <div className="mb-3 flex items-center justify-between">
250
254
  <div className="text-sm text-gray-600">
251
255
  {data.nodes.length} nodes • {data.links.length} connections
@@ -292,23 +296,21 @@ const SankeyDiagram = ({ data, isLoading = false }: SankeyDiagramProps) => {
292
296
  </button>
293
297
  </div>
294
298
 
295
- {/* Compression Warning */}
296
299
  {needsCompression && (
297
300
  <div className="mb-2 rounded bg-amber-50 px-3 py-2 text-sm text-amber-800">
298
301
  <strong>Compressed view</strong> - click anywhere to expand!
299
302
  </div>
300
303
  )}
301
304
 
302
- {/* SVG Container - Clickable when compressed */}
303
305
  <div
304
- className={`transition-all duration-300 ${
306
+ className={`overflow-x-auto transition-all duration-300 md:overflow-visible ${
305
307
  needsCompression
306
308
  ? 'cursor-pointer hover:bg-gray-50 hover:shadow-md'
307
309
  : ''
308
310
  }`}
309
311
  style={{
310
312
  height: `${displayHeight}px`,
311
- overflow: 'hidden',
313
+ overflowY: 'hidden',
312
314
  }}
313
315
  onClick={needsCompression ? handleExpand : undefined}
314
316
  role={needsCompression ? 'button' : undefined}
@@ -333,7 +335,7 @@ const SankeyDiagram = ({ data, isLoading = false }: SankeyDiagramProps) => {
333
335
  height={dimensions.height}
334
336
  style={{
335
337
  display: 'block',
336
- width: '100%',
338
+ minWidth: `${dimensions.width}px`, // Ensure SVG itself doesn't shrink
337
339
  height: `${dimensions.height}px`,
338
340
  transform: needsCompression
339
341
  ? `scaleY(${displayHeight / dimensions.height})`
@@ -345,6 +347,10 @@ const SankeyDiagram = ({ data, isLoading = false }: SankeyDiagramProps) => {
345
347
  ></svg>
346
348
  </div>
347
349
 
350
+ <div className="mt-2 text-center text-xs text-gray-500 md:hidden">
351
+ &larr; Scroll to see full journey map &rarr;
352
+ </div>
353
+
348
354
  {isLoading && (
349
355
  <div className="absolute inset-0 flex items-center justify-center rounded bg-black bg-opacity-80">
350
356
  <div className="flex items-center space-x-2 text-white">
@@ -1,4 +1,4 @@
1
- import { useState, useEffect, useCallback, useMemo, Component } from 'react';
1
+ import { useState, useCallback, useMemo, Component } from 'react';
2
2
  import type { ReactNode } from 'react';
3
3
  import { useStore } from '@nanostores/react';
4
4
  import { epinetCustomFilters } from '@/stores/analytics';
@@ -104,6 +104,30 @@ export default function StoryKeepDashboard_Analytics({
104
104
  error: null,
105
105
  });
106
106
 
107
+ const handleBeliefFilterChange = (beliefSlug: string, value: string) => {
108
+ const tenantId = window.TRACTSTACK_CONFIG?.tenantId || 'default';
109
+ const currentFilters = epinetCustomFilters.get();
110
+ let newFilters = [...(currentFilters.appliedFilters || [])];
111
+
112
+ if (value === 'All') {
113
+ newFilters = newFilters.filter((f) => f.beliefSlug !== beliefSlug);
114
+ } else {
115
+ const existingIndex = newFilters.findIndex(
116
+ (f) => f.beliefSlug === beliefSlug
117
+ );
118
+ if (existingIndex > -1) {
119
+ newFilters[existingIndex] = { beliefSlug, value };
120
+ } else {
121
+ newFilters.push({ beliefSlug, value });
122
+ }
123
+ }
124
+
125
+ epinetCustomFilters.set(tenantId, {
126
+ ...currentFilters,
127
+ appliedFilters: newFilters,
128
+ });
129
+ };
130
+
107
131
  // Duration helper for UI
108
132
  const currentDurationHelper = useMemo(():
109
133
  | 'daily'
@@ -440,6 +464,10 @@ export default function StoryKeepDashboard_Analytics({
440
464
  analytics.isLoading || analytics.status === 'loading'
441
465
  }
442
466
  hourlyNodeActivity={analytics.hourlyNodeActivity}
467
+ // MODIFICATION: Read availableFilters from the store, not local state
468
+ availableFilters={$epinetCustomFilters.availableFilters}
469
+ appliedFilters={$epinetCustomFilters.appliedFilters}
470
+ onBeliefFilterChange={handleBeliefFilterChange}
443
471
  />
444
472
  </div>
445
473
  </ErrorBoundary>
@@ -455,6 +483,10 @@ export default function StoryKeepDashboard_Analytics({
455
483
  analytics.isLoading || analytics.status === 'loading'
456
484
  }
457
485
  hourlyNodeActivity={analytics.hourlyNodeActivity}
486
+ // MODIFICATION: Read availableFilters from the store, not local state
487
+ availableFilters={$epinetCustomFilters.availableFilters}
488
+ appliedFilters={$epinetCustomFilters.appliedFilters}
489
+ onBeliefFilterChange={handleBeliefFilterChange}
458
490
  />
459
491
  </>
460
492
  )
@@ -468,6 +500,10 @@ export default function StoryKeepDashboard_Analytics({
468
500
  fullContentMap={fullContentMap}
469
501
  isLoading={analytics.isLoading || analytics.status === 'loading'}
470
502
  hourlyNodeActivity={analytics.hourlyNodeActivity}
503
+ // MODIFICATION: Read availableFilters from the store, not local state
504
+ availableFilters={$epinetCustomFilters.availableFilters}
505
+ appliedFilters={$epinetCustomFilters.appliedFilters}
506
+ onBeliefFilterChange={handleBeliefFilterChange}
471
507
  />
472
508
  </>
473
509
  )}
@@ -39,36 +39,25 @@ const FIELD_TYPES = [
39
39
  { value: 'image', label: 'Image' },
40
40
  ];
41
41
 
42
- const KnownResourceForm = ({
42
+ const KnownResourceFormRenderer = ({
43
43
  categorySlug,
44
44
  contentMap,
45
45
  onClose,
46
- }: KnownResourceFormProps) => {
46
+ brandConfig,
47
+ }: KnownResourceFormProps & { brandConfig: BrandConfig }) => {
47
48
  const [newFieldName, setNewFieldName] = useState('');
48
49
  const [showAddField, setShowAddField] = useState(false);
49
- const [brandConfig, setBrandConfig] = useState<BrandConfig | null>(null);
50
- const [loading, setLoading] = useState(false);
51
-
52
- useEffect(() => {
53
- if (!brandConfig && !loading) {
54
- setLoading(true);
55
- getBrandConfig(window.TRACTSTACK_CONFIG?.tenantId || 'default')
56
- .then(setBrandConfig)
57
- .catch(console.error)
58
- .finally(() => setLoading(false));
59
- }
60
- }, [brandConfig, loading]);
61
50
 
62
- const knownResources = brandConfig?.KNOWN_RESOURCES || {};
63
51
  const isCreate = categorySlug === 'new';
64
- const currentCategory = isCreate ? {} : knownResources[categorySlug] || {};
52
+ const knownResources = brandConfig.KNOWN_RESOURCES || {};
53
+ const currentCategory = isCreate ? {} : knownResources[categorySlug];
65
54
 
66
55
  const hasExistingResources =
67
56
  !isCreate && contentMap.some((item) => item.categorySlug === categorySlug);
68
57
 
69
58
  const initialState: KnownResourceState = {
70
59
  categorySlug: isCreate ? '' : categorySlug,
71
- fields: isCreate ? {} : currentCategory,
60
+ fields: currentCategory,
72
61
  };
73
62
 
74
63
  const validator = (state: KnownResourceState): FieldErrors => {
@@ -90,8 +79,6 @@ const KnownResourceForm = ({
90
79
  validator,
91
80
  onSave: async (data) => {
92
81
  try {
93
- // Update known resources in brand config
94
- if (!brandConfig) throw new Error('Brand config not loaded');
95
82
  const brandState = convertToLocalState(brandConfig);
96
83
  const updatedKnownResources = {
97
84
  ...brandState.knownResources,
@@ -108,7 +95,6 @@ const KnownResourceForm = ({
108
95
  updatedBrandState
109
96
  );
110
97
 
111
- // Call success callback after save (original pattern)
112
98
  setTimeout(() => {
113
99
  onClose?.(true);
114
100
  }, 1000);
@@ -179,7 +165,6 @@ const KnownResourceForm = ({
179
165
 
180
166
  return (
181
167
  <div className="space-y-8">
182
- {/* Header */}
183
168
  <div className="border-b border-gray-200 pb-4">
184
169
  <h2 className="text-2xl font-bold text-gray-900">
185
170
  {isCreate ? 'Create Resource Category' : `Edit ${categorySlug}`}
@@ -200,7 +185,6 @@ const KnownResourceForm = ({
200
185
  </div>
201
186
 
202
187
  <div className="space-y-6">
203
- {/* Category Name */}
204
188
  <StringInput
205
189
  label="Category Name"
206
190
  value={formState.state.categorySlug}
@@ -214,7 +198,6 @@ const KnownResourceForm = ({
214
198
  Must be lowercase with hyphens. Cannot be changed after creation.
215
199
  </p>
216
200
 
217
- {/* Fields Section */}
218
201
  <div className="space-y-6">
219
202
  <div className="flex items-center justify-between">
220
203
  <h3 className="text-lg font-bold text-gray-900">Fields</h3>
@@ -228,7 +211,6 @@ const KnownResourceForm = ({
228
211
  </button>
229
212
  </div>
230
213
 
231
- {/* Add Field Form */}
232
214
  {showAddField && (
233
215
  <div className="rounded-lg border border-gray-200 bg-gray-50 p-4">
234
216
  <h4 className="mb-3 text-sm font-bold text-gray-900">
@@ -264,7 +246,6 @@ const KnownResourceForm = ({
264
246
  </div>
265
247
  )}
266
248
 
267
- {/* Existing Fields */}
268
249
  {Object.keys(formState.state.fields).length === 0 ? (
269
250
  <div className="py-6 text-center text-gray-500">
270
251
  No fields defined yet. Click "Add Field" to create your first
@@ -275,7 +256,6 @@ const KnownResourceForm = ({
275
256
  {Object.entries(formState.state.fields).map(
276
257
  ([fieldName, fieldDef]) => {
277
258
  const locked = isFieldLocked(fieldName);
278
-
279
259
  return (
280
260
  <div
281
261
  key={fieldName}
@@ -304,7 +284,6 @@ const KnownResourceForm = ({
304
284
  </button>
305
285
  )}
306
286
  </div>
307
-
308
287
  <div className="grid grid-cols-1 gap-4 md:grid-cols-3">
309
288
  <EnumSelect
310
289
  label="Type"
@@ -315,7 +294,6 @@ const KnownResourceForm = ({
315
294
  options={FIELD_TYPES}
316
295
  disabled={locked}
317
296
  />
318
-
319
297
  <BooleanToggle
320
298
  label="Optional"
321
299
  value={fieldDef.optional || false}
@@ -324,7 +302,6 @@ const KnownResourceForm = ({
324
302
  }
325
303
  disabled={locked}
326
304
  />
327
-
328
305
  {fieldDef.type === 'categoryReference' && (
329
306
  <EnumSelect
330
307
  label="Reference Category"
@@ -341,7 +318,6 @@ const KnownResourceForm = ({
341
318
  disabled={locked}
342
319
  />
343
320
  )}
344
-
345
321
  {fieldDef.type === 'number' && (
346
322
  <>
347
323
  <NumberInput
@@ -372,7 +348,6 @@ const KnownResourceForm = ({
372
348
  </div>
373
349
  </div>
374
350
 
375
- {/* Save/Cancel Bar */}
376
351
  <UnsavedChangesBar
377
352
  formState={formState}
378
353
  message="You have unsaved resource category changes"
@@ -380,7 +355,6 @@ const KnownResourceForm = ({
380
355
  cancelLabel="Discard Changes"
381
356
  />
382
357
 
383
- {/* Cancel Navigation Button */}
384
358
  <div className="flex justify-start">
385
359
  <button
386
360
  type="button"
@@ -394,4 +368,60 @@ const KnownResourceForm = ({
394
368
  );
395
369
  };
396
370
 
371
+ const KnownResourceForm = ({
372
+ categorySlug,
373
+ contentMap,
374
+ onClose,
375
+ }: KnownResourceFormProps) => {
376
+ const [brandConfig, setBrandConfig] = useState<BrandConfig | null>(null);
377
+ const [loading, setLoading] = useState(true);
378
+ const [error, setError] = useState<string | null>(null);
379
+
380
+ useEffect(() => {
381
+ getBrandConfig(window.TRACTSTACK_CONFIG?.tenantId || 'default')
382
+ .then(setBrandConfig)
383
+ .catch((err) => {
384
+ console.error('Failed to load brand configuration:', err);
385
+ setError(
386
+ 'Could not load resource category configuration. Please try again.'
387
+ );
388
+ })
389
+ .finally(() => setLoading(false));
390
+ }, []);
391
+
392
+ if (loading) {
393
+ return (
394
+ <div className="py-12 text-center text-gray-500">
395
+ Loading configuration...
396
+ </div>
397
+ );
398
+ }
399
+
400
+ if (error) {
401
+ return (
402
+ <div className="rounded-md bg-red-50 p-4 text-center text-red-700">
403
+ {error}
404
+ </div>
405
+ );
406
+ }
407
+
408
+ const isCreate = categorySlug === 'new';
409
+ if (!isCreate && !(brandConfig?.KNOWN_RESOURCES || {})[categorySlug]) {
410
+ return (
411
+ <div className="py-12 text-center text-gray-500">
412
+ Resource category "{categorySlug}" not found.
413
+ </div>
414
+ );
415
+ }
416
+
417
+ return (
418
+ <KnownResourceFormRenderer
419
+ categorySlug={categorySlug}
420
+ contentMap={contentMap}
421
+ onClose={onClose}
422
+ brandConfig={brandConfig!}
423
+ />
424
+ );
425
+ };
426
+
397
427
  export default KnownResourceForm;
@@ -20,13 +20,12 @@ interface FetchAnalyticsProps {
20
20
  onAnalyticsUpdate: (analytics: AnalyticsState) => void;
21
21
  }
22
22
 
23
- // Global singleton state to prevent multi-component conflicts
24
23
  class AnalyticsService {
25
24
  private static instance: AnalyticsService;
26
25
  private isInitialized = false;
27
26
  private activeRequest: AbortController | null = null;
28
27
  private requestCache = new Map<string, { data: any; timestamp: number }>();
29
- private readonly CACHE_TTL = 5000; // 5 seconds
28
+ private readonly CACHE_TTL = 5000;
30
29
  private readonly DEBOUNCE_MS = 300;
31
30
  private debounceTimer: NodeJS.Timeout | null = null;
32
31
  private floodProtection = {
@@ -34,7 +33,7 @@ class AnalyticsService {
34
33
  windowStart: 0,
35
34
  isBlocked: false,
36
35
  };
37
- private readonly FLOOD_WINDOW_MS = 10000; // 10 seconds
36
+ private readonly FLOOD_WINDOW_MS = 10000;
38
37
  private readonly FLOOD_THRESHOLD = 5;
39
38
 
40
39
  static getInstance(): AnalyticsService {
@@ -51,35 +50,25 @@ class AnalyticsService {
51
50
 
52
51
  private isFloodBlocked(): boolean {
53
52
  const now = Date.now();
54
-
55
- // Reset window if needed
56
53
  if (now - this.floodProtection.windowStart > this.FLOOD_WINDOW_MS) {
57
54
  this.floodProtection.requestCount = 0;
58
55
  this.floodProtection.windowStart = now;
59
56
  this.floodProtection.isBlocked = false;
60
57
  }
61
-
62
- // Check if blocked
63
58
  if (this.floodProtection.isBlocked) {
64
59
  if (VERBOSE) console.log('🚫 Request blocked by flood protection');
65
60
  return true;
66
61
  }
67
-
68
- // Increment counter and check threshold
69
62
  this.floodProtection.requestCount++;
70
63
  if (this.floodProtection.requestCount > this.FLOOD_THRESHOLD) {
71
64
  this.floodProtection.isBlocked = true;
72
65
  if (VERBOSE) console.log('🚨 Flood protection activated');
73
-
74
- // Auto-unblock after delay
75
66
  setTimeout(() => {
76
67
  this.floodProtection.isBlocked = false;
77
68
  if (VERBOSE) console.log('✅ Flood protection deactivated');
78
- }, 30000); // 30 second cooldown
79
-
69
+ }, 30000);
80
70
  return true;
81
71
  }
82
-
83
72
  return false;
84
73
  }
85
74
 
@@ -94,8 +83,6 @@ class AnalyticsService {
94
83
 
95
84
  private setCachedResponse(cacheKey: string, data: any): void {
96
85
  this.requestCache.set(cacheKey, { data, timestamp: Date.now() });
97
-
98
- // Cleanup old cache entries
99
86
  const cutoff = Date.now() - this.CACHE_TTL;
100
87
  for (const [key, value] of this.requestCache.entries()) {
101
88
  if (value.timestamp < cutoff) {
@@ -108,35 +95,29 @@ class AnalyticsService {
108
95
  filters: any,
109
96
  onUpdate: (data: AnalyticsState) => void
110
97
  ): Promise<void> {
111
- // Flood protection
112
98
  if (this.isFloodBlocked()) {
113
99
  return;
114
100
  }
115
101
 
116
102
  try {
117
- // Cancel any existing request
118
103
  if (this.activeRequest) {
119
104
  this.activeRequest.abort();
120
105
  if (VERBOSE) console.log('🛑 Cancelled previous request');
121
106
  }
122
107
 
123
- // Create new abort controller
124
108
  this.activeRequest = new AbortController();
125
109
 
126
- // Build URL parameters
127
110
  const params = new URLSearchParams();
128
111
  if (filters.startTimeUTC && filters.endTimeUTC) {
129
112
  const now = new Date();
130
113
  const startTime = new Date(filters.startTimeUTC);
131
114
  const endTime = new Date(filters.endTimeUTC);
132
-
133
115
  const startHour = Math.ceil(
134
116
  (now.getTime() - startTime.getTime()) / (1000 * 60 * 60)
135
117
  );
136
118
  const endHour = Math.floor(
137
119
  (now.getTime() - endTime.getTime()) / (1000 * 60 * 60)
138
120
  );
139
-
140
121
  params.append('startHour', startHour.toString());
141
122
  params.append('endHour', endHour.toString());
142
123
  }
@@ -146,16 +127,18 @@ class AnalyticsService {
146
127
  if (filters.selectedUserId)
147
128
  params.append('userId', filters.selectedUserId);
148
129
 
149
- const cacheKey = this.getCacheKey(params);
130
+ // MODIFICATION: Properly format appliedFilters for the backend
131
+ if (filters.appliedFilters && filters.appliedFilters.length > 0) {
132
+ params.append('appliedFilters', JSON.stringify(filters.appliedFilters));
133
+ }
150
134
 
151
- // Check cache first
135
+ const cacheKey = this.getCacheKey(params);
152
136
  const cachedData = this.getCachedResponse(cacheKey);
153
137
  if (cachedData) {
154
138
  onUpdate(cachedData);
155
139
  return;
156
140
  }
157
141
 
158
- // Set loading state
159
142
  onUpdate({
160
143
  dashboard: null,
161
144
  leads: null,
@@ -167,23 +150,17 @@ class AnalyticsService {
167
150
  error: null,
168
151
  });
169
152
 
170
- // Make request using existing TractStackAPI
171
153
  const api = new TractStackAPI(
172
154
  window.TRACTSTACK_CONFIG?.tenantId || 'default'
173
155
  );
174
156
  const endpoint = `/api/v1/analytics/all${params.toString() ? `?${params.toString()}` : ''}`;
175
-
176
157
  if (VERBOSE) console.log('🔥 Making API request', { endpoint });
177
158
 
178
159
  const response = await api.get(endpoint);
179
-
180
- if (!response.success) {
160
+ if (!response.success)
181
161
  throw new Error(response.error || 'Failed to fetch analytics data');
182
- }
183
-
184
162
  const data = response.data;
185
163
 
186
- // Check if data is still loading - implement polling logic
187
164
  const isStillLoading =
188
165
  data?.status === 'loading' ||
189
166
  data?.status === 'refreshing' ||
@@ -196,8 +173,6 @@ class AnalyticsService {
196
173
 
197
174
  if (isStillLoading) {
198
175
  if (VERBOSE) console.log('⏳ Backend data still loading, will poll...');
199
-
200
- // Update with partial data but keep loading state
201
176
  const partialAnalytics = {
202
177
  dashboard: data.dashboard,
203
178
  leads: data.leads,
@@ -208,27 +183,20 @@ class AnalyticsService {
208
183
  error: null,
209
184
  isLoading: true,
210
185
  };
211
-
212
186
  onUpdate(partialAnalytics);
213
187
 
214
- // Schedule polling retry - use the cache key to prevent multiple polls
215
188
  const pollKey = `poll_${cacheKey}`;
216
189
  if (!this.requestCache.has(pollKey)) {
217
190
  this.requestCache.set(pollKey, { data: null, timestamp: Date.now() });
218
-
219
191
  setTimeout(() => {
220
192
  this.requestCache.delete(pollKey);
221
- // Clear the main cache entry to force fresh request
222
193
  this.requestCache.delete(cacheKey);
223
- // Retry the fetch
224
194
  this.fetchAnalytics(filters, onUpdate);
225
195
  }, 2000);
226
196
  }
227
-
228
197
  return;
229
198
  }
230
199
 
231
- // Process successful response
232
200
  const analyticsData = {
233
201
  dashboard: data.dashboard,
234
202
  leads: data.leads,
@@ -240,23 +208,23 @@ class AnalyticsService {
240
208
  isLoading: false,
241
209
  };
242
210
 
243
- // Cache the response
244
211
  this.setCachedResponse(cacheKey, analyticsData);
245
-
246
- // Update caller
247
212
  onUpdate(analyticsData);
248
213
 
249
- if (VERBOSE) console.log('✅ Analytics request completed successfully');
214
+ // MODIFICATION: Correctly extract top-level availableFilters and update the store
215
+ epinetCustomFilters.set(window.TRACTSTACK_CONFIG?.tenantId || 'default', {
216
+ ...filters,
217
+ availableFilters: data.availableFilters || [],
218
+ });
250
219
 
220
+ if (VERBOSE) console.log('✅ Analytics request completed successfully');
251
221
  this.activeRequest = null;
252
222
  } catch (error) {
253
223
  if (error instanceof Error && error.name === 'AbortError') {
254
224
  if (VERBOSE) console.log('🔄 Request aborted');
255
225
  return;
256
226
  }
257
-
258
227
  console.error('❌ Analytics fetch error:', error);
259
-
260
228
  onUpdate({
261
229
  dashboard: null,
262
230
  leads: null,
@@ -268,7 +236,6 @@ class AnalyticsService {
268
236
  error instanceof Error ? error.message : 'Unknown error occurred',
269
237
  isLoading: false,
270
238
  });
271
-
272
239
  this.activeRequest = null;
273
240
  }
274
241
  }
@@ -286,13 +253,9 @@ class AnalyticsService {
286
253
 
287
254
  initializeFilters(tenantId: string): void {
288
255
  if (this.isInitialized) return;
289
-
290
256
  if (VERBOSE) console.log('🏁 Initializing analytics filters');
291
-
292
257
  const nowUTC = new Date();
293
258
  const oneWeekAgoUTC = new Date(nowUTC.getTime() - 7 * 24 * 60 * 60 * 1000);
294
-
295
- // Only set if not already initialized to prevent store churn
296
259
  const current = epinetCustomFilters.get();
297
260
  if (!current.enabled) {
298
261
  epinetCustomFilters.set(tenantId, {
@@ -301,19 +264,15 @@ class AnalyticsService {
301
264
  selectedUserId: null,
302
265
  startTimeUTC: oneWeekAgoUTC.toISOString(),
303
266
  endTimeUTC: nowUTC.toISOString(),
267
+ availableFilters: [],
268
+ appliedFilters: [],
304
269
  });
305
270
  }
306
-
307
271
  this.isInitialized = true;
308
272
  }
309
273
 
310
274
  debouncedFetch(filters: any, onUpdate: (data: AnalyticsState) => void): void {
311
- // Clear existing debounce timer
312
- if (this.debounceTimer) {
313
- clearTimeout(this.debounceTimer);
314
- }
315
-
316
- // Set new debounced fetch
275
+ if (this.debounceTimer) clearTimeout(this.debounceTimer);
317
276
  this.debounceTimer = setTimeout(() => {
318
277
  this.fetchAnalytics(filters, onUpdate);
319
278
  }, this.DEBOUNCE_MS);
@@ -329,29 +288,21 @@ export default function FetchAnalytics({
329
288
 
330
289
  if (VERBOSE) {
331
290
  console.log('🔄 FetchAnalytics render', {
332
- filters: {
333
- startTimeUTC: $epinetCustomFilters.startTimeUTC,
334
- endTimeUTC: $epinetCustomFilters.endTimeUTC,
335
- visitorType: $epinetCustomFilters.visitorType,
336
- selectedUserId: $epinetCustomFilters.selectedUserId,
337
- },
291
+ filters: { ...$epinetCustomFilters },
338
292
  });
339
293
  }
340
294
 
341
- // Cleanup on unmount
342
295
  useEffect(() => {
343
296
  return () => {
344
297
  analyticsService.current.cleanup();
345
298
  };
346
299
  }, []);
347
300
 
348
- // Initialize filters once
349
301
  useEffect(() => {
350
302
  const tenantId = window.TRACTSTACK_CONFIG?.tenantId || 'default';
351
303
  analyticsService.current.initializeFilters(tenantId);
352
304
  }, []);
353
305
 
354
- // Debounced fetch when filters change
355
306
  useEffect(() => {
356
307
  if (
357
308
  !$epinetCustomFilters.enabled ||
@@ -362,15 +313,14 @@ export default function FetchAnalytics({
362
313
  return;
363
314
  }
364
315
 
365
- // Create stable filter signature to prevent unnecessary fetches
366
316
  const filtersSignature = JSON.stringify({
367
317
  startTimeUTC: $epinetCustomFilters.startTimeUTC,
368
318
  endTimeUTC: $epinetCustomFilters.endTimeUTC,
369
319
  visitorType: $epinetCustomFilters.visitorType,
370
320
  selectedUserId: $epinetCustomFilters.selectedUserId,
321
+ appliedFilters: $epinetCustomFilters.appliedFilters,
371
322
  });
372
323
 
373
- // Skip if filters haven't actually changed
374
324
  if (filtersSignature === lastFiltersRef.current) {
375
325
  return;
376
326
  }
@@ -389,6 +339,7 @@ export default function FetchAnalytics({
389
339
  $epinetCustomFilters.selectedUserId,
390
340
  $epinetCustomFilters.startTimeUTC,
391
341
  $epinetCustomFilters.endTimeUTC,
342
+ $epinetCustomFilters.appliedFilters,
392
343
  onAnalyticsUpdate,
393
344
  ]);
394
345