@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.
- package/admin/src/components/StepFlowBuilder/flow-canvas/FlowCanvas.tsx +6 -0
- package/admin/src/components/StepFlowBuilder/index.tsx +3 -6
- package/admin/src/components/StepFlowBuilder/nodes/BranchNode.tsx +3 -0
- package/admin/src/components/StepFlowBuilder/nodes/EntryNode.tsx +1 -0
- package/admin/src/components/StepFlowBuilder/nodes/ExitNode.tsx +1 -0
- package/admin/src/components/StepFlowBuilder/nodes/MessageNode.tsx +2 -0
- package/admin/src/components/StepFlowBuilder/nodes/WaitNode.tsx +2 -0
- package/admin/src/components/StepFlowBuilder/panels/BranchConfig.tsx +5 -6
- package/admin/src/components/StepFlowBuilder/panels/MessageConfig.tsx +6 -7
- package/admin/src/components/TriggerConfigField/index.tsx +37 -16
- package/admin/src/hooks/api/index.ts +7 -0
- package/admin/src/hooks/api/types.ts +127 -0
- package/admin/src/hooks/api/use-anti-spam-logs.ts +35 -0
- package/admin/src/hooks/api/use-campaign-mutations.ts +82 -0
- package/admin/src/hooks/api/use-campaigns.ts +65 -0
- package/admin/src/hooks/api/use-crm-logs.ts +42 -0
- package/admin/src/hooks/api/use-dashboard-stats.ts +30 -0
- package/admin/src/hooks/api/use-filter-options.ts +28 -0
- package/admin/src/lib/react-query/QueryProvider.tsx +17 -0
- package/admin/src/lib/react-query/index.ts +3 -0
- package/admin/src/lib/react-query/query-client.ts +13 -0
- package/admin/src/lib/react-query/query-keys.ts +22 -0
- package/admin/src/pages/HomePage/components/AntiSpamLogsTable.tsx +11 -44
- package/admin/src/pages/HomePage/components/CampaignBuilder/index.tsx +36 -75
- package/admin/src/pages/HomePage/components/LogsTable.tsx +21 -115
- package/admin/src/pages/HomePage/components/StatsView.tsx +16 -124
- package/admin/src/pages/HomePage/index.tsx +3 -0
- package/dist/_chunks/{FlowCanvas-BvC4CsIX.mjs → FlowCanvas-C7Ea3Xdf.mjs} +32 -18
- package/dist/_chunks/{FlowCanvas-BW6aa6oF.js → FlowCanvas-xt_QOMvb.js} +32 -40
- package/dist/_chunks/{index--y6vzvC5.mjs → index--24mAFWS.mjs} +616 -451
- package/dist/_chunks/{index-CwfIGC3K.js → index-B0JQWUst.js} +19 -4
- package/dist/_chunks/{index-D4m1lj0p.mjs → index-BuHNQ3cQ.mjs} +19 -4
- package/dist/_chunks/{index-vqLSa44X.mjs → index-CthLsTpm.mjs} +4 -5
- package/dist/_chunks/{index-DBzHyMTU.js → index-DCsbQA3x.js} +301 -234
- package/dist/_chunks/{index-1FgA66KH.js → index-DDY39e5N.js} +3 -26
- package/dist/admin/index.js +3 -3
- package/dist/admin/index.mjs +3 -3
- 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
|
</>
|
|
@@ -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) =>
|
|
467
|
-
|
|
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) =>
|
|
479
|
+
onCreateOption={(v: string) => {
|
|
480
|
+
handleUpdate({ eventName: v });
|
|
481
|
+
setEventSearchText('');
|
|
482
|
+
}}
|
|
472
483
|
>
|
|
473
484
|
{Object.entries(ANALYTICS_EVENTS).flatMap(([category, events]) =>
|
|
474
|
-
events
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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
|
+
};
|