@inspirer-dev/crm-dashboard 1.0.43 → 1.0.44

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 (38) hide show
  1. package/admin/src/components/StepFlowBuilder/flow-canvas/FlowCanvas.tsx +6 -0
  2. package/admin/src/components/StepFlowBuilder/index.tsx +3 -6
  3. package/admin/src/components/StepFlowBuilder/nodes/BranchNode.tsx +3 -0
  4. package/admin/src/components/StepFlowBuilder/nodes/EntryNode.tsx +1 -0
  5. package/admin/src/components/StepFlowBuilder/nodes/ExitNode.tsx +1 -0
  6. package/admin/src/components/StepFlowBuilder/nodes/MessageNode.tsx +2 -0
  7. package/admin/src/components/StepFlowBuilder/nodes/WaitNode.tsx +2 -0
  8. package/admin/src/components/StepFlowBuilder/panels/BranchConfig.tsx +5 -6
  9. package/admin/src/components/StepFlowBuilder/panels/MessageConfig.tsx +6 -7
  10. package/admin/src/components/TriggerConfigField/index.tsx +37 -16
  11. package/admin/src/hooks/api/index.ts +7 -0
  12. package/admin/src/hooks/api/types.ts +127 -0
  13. package/admin/src/hooks/api/use-anti-spam-logs.ts +35 -0
  14. package/admin/src/hooks/api/use-campaign-mutations.ts +82 -0
  15. package/admin/src/hooks/api/use-campaigns.ts +65 -0
  16. package/admin/src/hooks/api/use-crm-logs.ts +42 -0
  17. package/admin/src/hooks/api/use-dashboard-stats.ts +30 -0
  18. package/admin/src/hooks/api/use-filter-options.ts +28 -0
  19. package/admin/src/lib/react-query/QueryProvider.tsx +17 -0
  20. package/admin/src/lib/react-query/index.ts +3 -0
  21. package/admin/src/lib/react-query/query-client.ts +13 -0
  22. package/admin/src/lib/react-query/query-keys.ts +22 -0
  23. package/admin/src/pages/HomePage/components/AntiSpamLogsTable.tsx +11 -44
  24. package/admin/src/pages/HomePage/components/CampaignBuilder/index.tsx +36 -75
  25. package/admin/src/pages/HomePage/components/LogsTable.tsx +21 -115
  26. package/admin/src/pages/HomePage/components/StatsView.tsx +16 -124
  27. package/admin/src/pages/HomePage/index.tsx +3 -0
  28. package/dist/_chunks/{FlowCanvas-BvC4CsIX.mjs → FlowCanvas-C7Ea3Xdf.mjs} +32 -18
  29. package/dist/_chunks/{FlowCanvas-BW6aa6oF.js → FlowCanvas-xt_QOMvb.js} +32 -40
  30. package/dist/_chunks/{index--y6vzvC5.mjs → index--24mAFWS.mjs} +616 -451
  31. package/dist/_chunks/{index-CwfIGC3K.js → index-B0JQWUst.js} +19 -4
  32. package/dist/_chunks/{index-D4m1lj0p.mjs → index-BuHNQ3cQ.mjs} +19 -4
  33. package/dist/_chunks/{index-vqLSa44X.mjs → index-CthLsTpm.mjs} +4 -5
  34. package/dist/_chunks/{index-DBzHyMTU.js → index-DCsbQA3x.js} +301 -234
  35. package/dist/_chunks/{index-1FgA66KH.js → index-DDY39e5N.js} +3 -26
  36. package/dist/admin/index.js +3 -3
  37. package/dist/admin/index.mjs +3 -3
  38. package/package.json +3 -1
@@ -490,6 +490,12 @@ const FlowCanvas: React.FC<FlowCanvasProps> = ({
490
490
  .react-flow__edge.temp {
491
491
  pointer-events: none;
492
492
  }
493
+ .react-flow__node {
494
+ overflow: visible !important;
495
+ }
496
+ .react-flow__handle {
497
+ z-index: 100 !important;
498
+ }
493
499
  `}
494
500
  </style>
495
501
  <ReactFlow
@@ -2,7 +2,7 @@ import React, { forwardRef, useCallback, useEffect, useMemo, useState, useRef }
2
2
  import { ReactFlowProvider } from 'reactflow';
3
3
  import 'reactflow/dist/style.css';
4
4
  import { Box, Field, Flex, Typography, Badge } from '@strapi/design-system';
5
- import { useForm, unstable_useContentManagerContext as useContentManagerContext } from '@strapi/strapi/admin';
5
+ import { useForm, unstable_useContentManagerContext as useContentManagerContext, useFetchClient } from '@strapi/strapi/admin';
6
6
 
7
7
  import type { StepFlowBuilderProps, FlowStep, CampaignContext, EntrySegment } from './types';
8
8
  import type { ValidationResult } from './validation';
@@ -22,6 +22,7 @@ const StepFlowBuilderInner = forwardRef<HTMLDivElement, StepFlowBuilderProps>(
22
22
  const syncTimeoutRef = useRef<NodeJS.Timeout | null>(null);
23
23
 
24
24
  const form = useForm('StepFlowBuilder', (state) => state);
25
+ const { get } = useFetchClient();
25
26
 
26
27
  let documentId: string | undefined;
27
28
  let model: string | undefined;
@@ -40,11 +41,7 @@ const StepFlowBuilderInner = forwardRef<HTMLDivElement, StepFlowBuilderProps>(
40
41
 
41
42
  const fetchCampaignData = async () => {
42
43
  try {
43
- const { useFetchClient } = await import('@strapi/strapi/admin');
44
- const { get } = useFetchClient();
45
-
46
44
  const url = `/content-manager/collection-types/${model}/${documentId}`;
47
-
48
45
  const response = await get(url, { params: { populate: 'entrySegment' } });
49
46
 
50
47
  const data = response?.data as { entrySegment?: { id: number; name: string } } | undefined;
@@ -63,7 +60,7 @@ const StepFlowBuilderInner = forwardRef<HTMLDivElement, StepFlowBuilderProps>(
63
60
  };
64
61
 
65
62
  fetchCampaignData();
66
- }, [documentId, model, hasFetched]);
63
+ }, [documentId, model, hasFetched, get]);
67
64
 
68
65
  const currentSegment = useMemo<EntrySegment | null>(() => {
69
66
  const values = form?.values as Record<string, unknown> | undefined;
@@ -43,6 +43,7 @@ const BranchNode: React.FC<NodeProps<FlowNodeData>> = ({ id, data, selected }) =
43
43
  height: HANDLE_SIZE,
44
44
  border: '3px solid white',
45
45
  boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
46
+ top: -8,
46
47
  }}
47
48
  />
48
49
  <div
@@ -145,6 +146,7 @@ const BranchNode: React.FC<NodeProps<FlowNodeData>> = ({ id, data, selected }) =
145
146
  border: '3px solid white',
146
147
  boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
147
148
  left: '30%',
149
+ bottom: -8,
148
150
  }}
149
151
  />
150
152
  <Handle
@@ -158,6 +160,7 @@ const BranchNode: React.FC<NodeProps<FlowNodeData>> = ({ id, data, selected }) =
158
160
  border: '3px solid white',
159
161
  boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
160
162
  left: '70%',
163
+ bottom: -8,
161
164
  }}
162
165
  />
163
166
  </>
@@ -124,6 +124,7 @@ const EntryNode: React.FC<NodeProps<FlowNodeData>> = ({ selected }) => {
124
124
  height: HANDLE_SIZE,
125
125
  border: '3px solid white',
126
126
  boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
127
+ bottom: -8,
127
128
  }}
128
129
  />
129
130
  </>
@@ -42,6 +42,7 @@ const ExitNode: React.FC<NodeProps<FlowNodeData>> = ({ id, data, selected }) =>
42
42
  height: HANDLE_SIZE,
43
43
  border: '3px solid white',
44
44
  boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
45
+ top: -8,
45
46
  }}
46
47
  />
47
48
  <div
@@ -45,6 +45,7 @@ const MessageNode: React.FC<NodeProps<FlowNodeData>> = ({ id, data, selected })
45
45
  height: HANDLE_SIZE,
46
46
  border: '3px solid white',
47
47
  boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
48
+ top: -8,
48
49
  }}
49
50
  />
50
51
  <div
@@ -167,6 +168,7 @@ const MessageNode: React.FC<NodeProps<FlowNodeData>> = ({ id, data, selected })
167
168
  height: HANDLE_SIZE,
168
169
  border: '3px solid white',
169
170
  boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
171
+ bottom: -8,
170
172
  }}
171
173
  />
172
174
  </>
@@ -45,6 +45,7 @@ const WaitNode: React.FC<NodeProps<FlowNodeData>> = ({ id, data, selected }) =>
45
45
  height: HANDLE_SIZE,
46
46
  border: '3px solid white',
47
47
  boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
48
+ top: -8,
48
49
  }}
49
50
  />
50
51
  <div
@@ -147,6 +148,7 @@ const WaitNode: React.FC<NodeProps<FlowNodeData>> = ({ id, data, selected }) =>
147
148
  height: HANDLE_SIZE,
148
149
  border: '3px solid white',
149
150
  boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
151
+ bottom: -8,
150
152
  }}
151
153
  />
152
154
  </>
@@ -8,6 +8,7 @@ import {
8
8
  Loader,
9
9
  NumberInput,
10
10
  } from '@strapi/design-system';
11
+ import { useFetchClient } from '@strapi/strapi/admin';
11
12
  import type { FlowNodeData, SegmentOption } from '../types';
12
13
  import { useFlowTheme } from '../hooks/useFlowTheme';
13
14
 
@@ -19,6 +20,7 @@ interface BranchConfigProps {
19
20
 
20
21
  const BranchConfig: React.FC<BranchConfigProps> = ({ data, onUpdate, disabled }) => {
21
22
  const theme = useFlowTheme();
23
+ const { get } = useFetchClient();
22
24
  const [segments, setSegments] = useState<SegmentOption[]>([]);
23
25
  const [loading, setLoading] = useState(true);
24
26
  const [fetchError, setFetchError] = useState(false);
@@ -28,9 +30,6 @@ const BranchConfig: React.FC<BranchConfigProps> = ({ data, onUpdate, disabled })
28
30
  useEffect(() => {
29
31
  const fetchSegments = async () => {
30
32
  try {
31
- const { useFetchClient } = await import('@strapi/strapi/admin');
32
- const { get } = useFetchClient();
33
-
34
33
  const response = await get('/content-manager/collection-types/api::crm-segment.crm-segment', {
35
34
  params: {
36
35
  pageSize: 100,
@@ -52,10 +51,10 @@ const BranchConfig: React.FC<BranchConfigProps> = ({ data, onUpdate, disabled })
52
51
  }
53
52
  };
54
53
  fetchSegments();
55
- }, []);
54
+ }, [get]);
56
55
 
57
- const handleSegmentChange = (value: string) => {
58
- const segment = segments.find((s) => s.id.toString() === value);
56
+ const handleSegmentChange = (value: string | number) => {
57
+ const segment = segments.find((s) => s.id.toString() === value.toString());
59
58
  onUpdate({
60
59
  config: {
61
60
  ...data.config,
@@ -10,6 +10,7 @@ import {
10
10
  Loader,
11
11
  } from '@strapi/design-system';
12
12
  import { Plus, Trash } from '@strapi/icons';
13
+ import { useFetchClient } from '@strapi/strapi/admin';
13
14
  import type { FlowNodeData, CampaignVariant, ChannelType, TemplateOption } from '../types';
14
15
  import { CHANNEL_OPTIONS } from '../constants';
15
16
  import { useFlowTheme } from '../hooks/useFlowTheme';
@@ -22,6 +23,7 @@ interface MessageConfigProps {
22
23
 
23
24
  const MessageConfig: React.FC<MessageConfigProps> = ({ data, onUpdate, disabled }) => {
24
25
  const theme = useFlowTheme();
26
+ const { get } = useFetchClient();
25
27
  const [variants, setVariants] = useState<CampaignVariant[]>(data.config.variants || []);
26
28
  const [templates, setTemplates] = useState<TemplateOption[]>([]);
27
29
  const [loadingTemplates, setLoadingTemplates] = useState(true);
@@ -35,9 +37,6 @@ const MessageConfig: React.FC<MessageConfigProps> = ({ data, onUpdate, disabled
35
37
  useEffect(() => {
36
38
  const fetchTemplates = async () => {
37
39
  try {
38
- const { useFetchClient } = await import('@strapi/strapi/admin');
39
- const { get } = useFetchClient();
40
-
41
40
  const response = await get(
42
41
  '/content-manager/collection-types/api::crm-template.crm-template',
43
42
  {
@@ -64,13 +63,13 @@ const MessageConfig: React.FC<MessageConfigProps> = ({ data, onUpdate, disabled
64
63
  }
65
64
  };
66
65
  fetchTemplates();
67
- }, [channel]);
66
+ }, [channel, get]);
68
67
 
69
- const handleChannelChange = (value: string) => {
68
+ const handleChannelChange = (value: string | number) => {
70
69
  onUpdate({
71
70
  config: {
72
71
  ...data.config,
73
- channel: value as ChannelType,
72
+ channel: value.toString() as ChannelType,
74
73
  },
75
74
  });
76
75
  };
@@ -343,7 +342,7 @@ const MessageConfig: React.FC<MessageConfigProps> = ({ data, onUpdate, disabled
343
342
  ) : (
344
343
  <SingleSelect
345
344
  value={variant.template?.id?.toString() || ''}
346
- onChange={(value: string) => handleTemplateChange(index, value)}
345
+ onChange={(value: string | number) => handleTemplateChange(index, value.toString())}
347
346
  disabled={disabled}
348
347
  placeholder="Select template..."
349
348
  size="S"
@@ -353,6 +353,7 @@ const SectionHeader: React.FC<{
353
353
  const TriggerConfigField = forwardRef<HTMLDivElement, TriggerConfigFieldProps>(
354
354
  ({ name, value, onChange, intlLabel, disabled, error, required, hint }, ref) => {
355
355
  const [config, setConfig] = useState<TriggerConfig>(() => parseConfig(value));
356
+ const [eventSearchText, setEventSearchText] = useState('');
356
357
  const colors = useThemeColors();
357
358
 
358
359
  useEffect(() => {
@@ -463,27 +464,47 @@ const TriggerConfigField = forwardRef<HTMLDivElement, TriggerConfigFieldProps>(
463
464
  placeholder="Search events..."
464
465
  value={config.eventName || ''}
465
466
  textValue={config.eventName ? getEventLabel(config.eventName) : ''}
466
- onChange={(val: string) => handleUpdate({ eventName: val })}
467
- onClear={() => handleUpdate({ eventName: '' })}
467
+ onChange={(val: string) => {
468
+ handleUpdate({ eventName: val });
469
+ setEventSearchText('');
470
+ }}
471
+ onTextValueChange={(text: string) => setEventSearchText(text)}
472
+ onClear={() => {
473
+ handleUpdate({ eventName: '' });
474
+ setEventSearchText('');
475
+ }}
468
476
  disabled={disabled}
469
477
  creatable
470
478
  createMessage={(v: string) => `Use custom event: "${v}"`}
471
- onCreateOption={(v: string) => handleUpdate({ eventName: v })}
479
+ onCreateOption={(v: string) => {
480
+ handleUpdate({ eventName: v });
481
+ setEventSearchText('');
482
+ }}
472
483
  >
473
484
  {Object.entries(ANALYTICS_EVENTS).flatMap(([category, events]) =>
474
- events.map((event) => (
475
- <ComboboxOption key={event.value} value={event.value}>
476
- <Flex gap={2} alignItems="center">
477
- <Badge size="S" backgroundColor="neutral150" textColor="neutral600">
478
- {category}
479
- </Badge>
480
- <span>{event.label}</span>
481
- <Typography variant="pi" textColor="neutral400">
482
- {event.value}
483
- </Typography>
484
- </Flex>
485
- </ComboboxOption>
486
- ))
485
+ events
486
+ .filter((event) => {
487
+ if (!eventSearchText) return true;
488
+ const search = eventSearchText.toLowerCase();
489
+ return (
490
+ event.label.toLowerCase().includes(search) ||
491
+ event.value.toLowerCase().includes(search) ||
492
+ category.toLowerCase().includes(search)
493
+ );
494
+ })
495
+ .map((event) => (
496
+ <ComboboxOption key={event.value} value={event.value}>
497
+ <Flex gap={2} alignItems="center">
498
+ <Badge size="S" backgroundColor="neutral150" textColor="neutral600">
499
+ {category}
500
+ </Badge>
501
+ <span>{event.label}</span>
502
+ <Typography variant="pi" textColor="neutral400">
503
+ {event.value}
504
+ </Typography>
505
+ </Flex>
506
+ </ComboboxOption>
507
+ ))
487
508
  )}
488
509
  </Combobox>
489
510
  <Typography
@@ -0,0 +1,7 @@
1
+ export * from './types';
2
+ export { useFilterOptions } from './use-filter-options';
3
+ export { useDashboardStats } from './use-dashboard-stats';
4
+ export { useCrmLogs } from './use-crm-logs';
5
+ export { useAntiSpamLogs } from './use-anti-spam-logs';
6
+ export { useCampaigns, useSegments, useTemplates } from './use-campaigns';
7
+ export { useCreateCampaign, useUpdateCampaign, useDeleteCampaign } from './use-campaign-mutations';
@@ -0,0 +1,127 @@
1
+ export interface FilterOption {
2
+ id: string;
3
+ name: string;
4
+ }
5
+
6
+ export interface FilterOptions {
7
+ campaigns: FilterOption[];
8
+ segments: FilterOption[];
9
+ statuses?: string[];
10
+ }
11
+
12
+ export interface StatsFilters {
13
+ dateFrom: Date | undefined;
14
+ dateTo: Date | undefined;
15
+ campaigns: string[];
16
+ segments: string[];
17
+ }
18
+
19
+ export interface LogsFilters {
20
+ search: string;
21
+ statuses: string[];
22
+ campaigns: string[];
23
+ segments: string[];
24
+ scheduledFrom: Date | undefined;
25
+ scheduledTo: Date | undefined;
26
+ sentFrom: Date | undefined;
27
+ sentTo: Date | undefined;
28
+ }
29
+
30
+ export interface DashboardTotals {
31
+ sent: number;
32
+ clicks: number;
33
+ unique_clickers: number;
34
+ click_rate: number;
35
+ deposits_1h: number;
36
+ deposit_sum_1h: number;
37
+ deposit_rate: number;
38
+ }
39
+
40
+ export interface CampaignMetrics {
41
+ campaign_id: string;
42
+ campaign_name: string;
43
+ sent: number;
44
+ clicks: number;
45
+ unique_clickers: number;
46
+ click_rate: number;
47
+ deposits_1h: number;
48
+ deposit_sum_1h: number;
49
+ deposit_rate: number;
50
+ }
51
+
52
+ export interface VariantMetrics {
53
+ campaign_id: string;
54
+ campaign_name: string;
55
+ variant_id: string | null;
56
+ variant_name: string;
57
+ sent: number;
58
+ clicks: number;
59
+ unique_clickers: number;
60
+ click_rate: number;
61
+ deposits_1h: number;
62
+ deposit_sum_1h: number;
63
+ deposit_rate: number;
64
+ }
65
+
66
+ export interface DashboardStats {
67
+ period: { from: string; to: string };
68
+ totals: DashboardTotals;
69
+ by_campaign: CampaignMetrics[];
70
+ by_variant: VariantMetrics[];
71
+ }
72
+
73
+ export interface CrmLog {
74
+ timestamp: string;
75
+ user_id: string;
76
+ campaign_id: string;
77
+ campaign_name: string;
78
+ segment_id?: string;
79
+ segment_name?: string;
80
+ status: string;
81
+ error_reason?: string;
82
+ scheduled_at?: string;
83
+ sent_at?: string;
84
+ }
85
+
86
+ export interface CrmLogsResponse {
87
+ data: CrmLog[];
88
+ total: number;
89
+ }
90
+
91
+ export interface AntiSpamLog {
92
+ timestamp: string;
93
+ user_id: string;
94
+ campaign_id: string;
95
+ campaign_name?: string;
96
+ reason: string;
97
+ }
98
+
99
+ export interface AntiSpamLogsResponse {
100
+ data: AntiSpamLog[];
101
+ total: number;
102
+ }
103
+
104
+ export interface CampaignVariant {
105
+ name: string;
106
+ templateId: string;
107
+ weight: number;
108
+ }
109
+
110
+ export interface Campaign {
111
+ id?: number;
112
+ documentId?: string;
113
+ name: string;
114
+ slug: string;
115
+ isActive: boolean;
116
+ triggerType: 'event' | 'scheduled' | 'segment_entry';
117
+ triggerEventName?: string;
118
+ segmentIds: string[];
119
+ variants: CampaignVariant[];
120
+ cooldownHours: number;
121
+ priority: number;
122
+ dailyCapPerUser: number;
123
+ ignoreQuietHours: boolean;
124
+ cancelConditions?: unknown;
125
+ startDate?: string;
126
+ endDate?: string;
127
+ }
@@ -0,0 +1,35 @@
1
+ import { useQuery, keepPreviousData } from '@tanstack/react-query';
2
+ import { crmKeys } from '../../lib/react-query';
3
+ import getBackendUrl from '../../utils/getBackendUrl';
4
+ import type { AntiSpamLogsResponse } from './types';
5
+
6
+ const fetchAntiSpamLogs = async (search: string, page: number): Promise<AntiSpamLogsResponse> => {
7
+ const params = new URLSearchParams({
8
+ page: String(page),
9
+ pageSize: '20',
10
+ });
11
+
12
+ if (search) params.set('search', search);
13
+
14
+ const backendUrl = getBackendUrl();
15
+ const res = await fetch(new URL(`/api/crm/anti-spam-logs?${params}`, backendUrl).toString());
16
+
17
+ if (!res.ok) {
18
+ throw new Error('Failed to fetch anti-spam logs');
19
+ }
20
+
21
+ const data = await res.json();
22
+ return {
23
+ data: data.data || [],
24
+ total: data.total || 0,
25
+ };
26
+ };
27
+
28
+ export const useAntiSpamLogs = (search: string, page: number) => {
29
+ return useQuery({
30
+ queryKey: crmKeys.antiSpamLogsList(search, page),
31
+ queryFn: () => fetchAntiSpamLogs(search, page),
32
+ staleTime: 30 * 1000,
33
+ placeholderData: keepPreviousData,
34
+ });
35
+ };
@@ -0,0 +1,82 @@
1
+ import { useMutation, useQueryClient } from '@tanstack/react-query';
2
+ import { crmKeys } from '../../lib/react-query';
3
+ import getBackendUrl from '../../utils/getBackendUrl';
4
+ import type { Campaign } from './types';
5
+
6
+ type CreateCampaignInput = Omit<Campaign, 'id' | 'documentId'>;
7
+ type UpdateCampaignInput = Partial<Campaign> & { documentId: string };
8
+
9
+ const createCampaign = async (input: CreateCampaignInput): Promise<Campaign> => {
10
+ const backendUrl = getBackendUrl();
11
+ const res = await fetch(new URL('/api/crm/campaigns', backendUrl).toString(), {
12
+ method: 'POST',
13
+ headers: { 'Content-Type': 'application/json' },
14
+ body: JSON.stringify(input),
15
+ });
16
+
17
+ if (!res.ok) {
18
+ throw new Error('Failed to create campaign');
19
+ }
20
+
21
+ return res.json();
22
+ };
23
+
24
+ const updateCampaign = async (input: UpdateCampaignInput): Promise<Campaign> => {
25
+ const { documentId, ...data } = input;
26
+ const backendUrl = getBackendUrl();
27
+ const res = await fetch(new URL(`/api/crm/campaigns/${documentId}`, backendUrl).toString(), {
28
+ method: 'PUT',
29
+ headers: { 'Content-Type': 'application/json' },
30
+ body: JSON.stringify(data),
31
+ });
32
+
33
+ if (!res.ok) {
34
+ throw new Error('Failed to update campaign');
35
+ }
36
+
37
+ return res.json();
38
+ };
39
+
40
+ const deleteCampaign = async (documentId: string): Promise<void> => {
41
+ const backendUrl = getBackendUrl();
42
+ const res = await fetch(new URL(`/api/crm/campaigns/${documentId}`, backendUrl).toString(), {
43
+ method: 'DELETE',
44
+ });
45
+
46
+ if (!res.ok) {
47
+ throw new Error('Failed to delete campaign');
48
+ }
49
+ };
50
+
51
+ export const useCreateCampaign = () => {
52
+ const queryClient = useQueryClient();
53
+
54
+ return useMutation({
55
+ mutationFn: createCampaign,
56
+ onSuccess: () => {
57
+ queryClient.invalidateQueries({ queryKey: crmKeys.campaigns() });
58
+ },
59
+ });
60
+ };
61
+
62
+ export const useUpdateCampaign = () => {
63
+ const queryClient = useQueryClient();
64
+
65
+ return useMutation({
66
+ mutationFn: updateCampaign,
67
+ onSuccess: () => {
68
+ queryClient.invalidateQueries({ queryKey: crmKeys.campaigns() });
69
+ },
70
+ });
71
+ };
72
+
73
+ export const useDeleteCampaign = () => {
74
+ const queryClient = useQueryClient();
75
+
76
+ return useMutation({
77
+ mutationFn: deleteCampaign,
78
+ onSuccess: () => {
79
+ queryClient.invalidateQueries({ queryKey: crmKeys.campaigns() });
80
+ },
81
+ });
82
+ };
@@ -0,0 +1,65 @@
1
+ import { useQuery } from '@tanstack/react-query';
2
+ import { crmKeys } from '../../lib/react-query';
3
+ import getBackendUrl from '../../utils/getBackendUrl';
4
+ import type { Campaign } from './types';
5
+ import type { CrmSegment, CrmTemplate } from '../../types/crm';
6
+
7
+ const fetchCampaigns = async (): Promise<Campaign[]> => {
8
+ const backendUrl = getBackendUrl();
9
+ const res = await fetch(new URL('/api/crm/campaigns', backendUrl).toString());
10
+
11
+ if (!res.ok) {
12
+ throw new Error('Failed to fetch campaigns');
13
+ }
14
+
15
+ const data = await res.json();
16
+ return Array.isArray(data) ? data : data?.data || [];
17
+ };
18
+
19
+ const fetchSegments = async (): Promise<CrmSegment[]> => {
20
+ const backendUrl = getBackendUrl();
21
+ const res = await fetch(new URL('/api/crm/segments', backendUrl).toString());
22
+
23
+ if (!res.ok) {
24
+ throw new Error('Failed to fetch segments');
25
+ }
26
+
27
+ const data = await res.json();
28
+ return Array.isArray(data) ? data : data?.data || [];
29
+ };
30
+
31
+ const fetchTemplates = async (): Promise<CrmTemplate[]> => {
32
+ const backendUrl = getBackendUrl();
33
+ const res = await fetch(new URL('/api/crm/templates', backendUrl).toString());
34
+
35
+ if (!res.ok) {
36
+ throw new Error('Failed to fetch templates');
37
+ }
38
+
39
+ const data = await res.json();
40
+ return Array.isArray(data) ? data : data?.data || [];
41
+ };
42
+
43
+ export const useCampaigns = () => {
44
+ return useQuery({
45
+ queryKey: crmKeys.campaigns(),
46
+ queryFn: fetchCampaigns,
47
+ staleTime: 2 * 60 * 1000,
48
+ });
49
+ };
50
+
51
+ export const useSegments = () => {
52
+ return useQuery({
53
+ queryKey: crmKeys.segments(),
54
+ queryFn: fetchSegments,
55
+ staleTime: 2 * 60 * 1000,
56
+ });
57
+ };
58
+
59
+ export const useTemplates = () => {
60
+ return useQuery({
61
+ queryKey: crmKeys.templates(),
62
+ queryFn: fetchTemplates,
63
+ staleTime: 2 * 60 * 1000,
64
+ });
65
+ };
@@ -0,0 +1,42 @@
1
+ import { useQuery, keepPreviousData } from '@tanstack/react-query';
2
+ import { crmKeys } from '../../lib/react-query';
3
+ import getBackendUrl from '../../utils/getBackendUrl';
4
+ import type { LogsFilters, CrmLogsResponse } from './types';
5
+
6
+ const fetchCrmLogs = async (filters: LogsFilters, page: number): Promise<CrmLogsResponse> => {
7
+ const params = new URLSearchParams({
8
+ page: String(page),
9
+ pageSize: '20',
10
+ });
11
+
12
+ if (filters.search) params.set('search', filters.search.trim());
13
+ if (filters.statuses.length > 0) params.set('status', filters.statuses.join(','));
14
+ if (filters.campaigns.length > 0) params.set('campaignIds', filters.campaigns.join(','));
15
+ if (filters.segments.length > 0) params.set('segmentIds', filters.segments.join(','));
16
+ if (filters.scheduledFrom) params.set('scheduledFrom', filters.scheduledFrom.toISOString());
17
+ if (filters.scheduledTo) params.set('scheduledTo', filters.scheduledTo.toISOString());
18
+ if (filters.sentFrom) params.set('sentFrom', filters.sentFrom.toISOString());
19
+ if (filters.sentTo) params.set('sentTo', filters.sentTo.toISOString());
20
+
21
+ const backendUrl = getBackendUrl();
22
+ const res = await fetch(new URL(`/api/crm/logs?${params}`, backendUrl).toString());
23
+
24
+ if (!res.ok) {
25
+ throw new Error('Failed to fetch CRM logs');
26
+ }
27
+
28
+ const data = await res.json();
29
+ return {
30
+ data: data.data || [],
31
+ total: data.total || 0,
32
+ };
33
+ };
34
+
35
+ export const useCrmLogs = (filters: LogsFilters, page: number) => {
36
+ return useQuery({
37
+ queryKey: crmKeys.logsList(filters, page),
38
+ queryFn: () => fetchCrmLogs(filters, page),
39
+ staleTime: 30 * 1000,
40
+ placeholderData: keepPreviousData,
41
+ });
42
+ };