@inspirer-dev/crm-dashboard 1.0.90 → 1.0.92
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/hooks/api/index.ts +17 -0
- package/admin/src/hooks/api/use-manual-pushes.ts +177 -0
- package/admin/src/lib/react-query/query-keys.ts +5 -0
- package/admin/src/pages/HomePage/HomePage.css +17 -0
- package/admin/src/pages/HomePage/components/ManualPushesView.tsx +560 -0
- package/admin/src/pages/HomePage/components/index.ts +1 -0
- package/admin/src/pages/HomePage/index.tsx +14 -3
- package/dist/_chunks/{index-LXBoz7PC.mjs → index-B_cJDVsP.mjs} +481 -7
- package/dist/_chunks/{index-DRXMKPXI.js → index-D6jz5MgX.js} +478 -4
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/server/index.js +229 -2
- package/dist/server/index.mjs +229 -2
- package/dist/style.css +17 -0
- package/package.json +1 -1
- package/server/src/controllers/controller.ts +64 -0
- package/server/src/routes/index.ts +55 -0
- package/server/src/services/service.ts +141 -2
|
@@ -11,3 +11,20 @@ export { useCohortData } from './use-cohort-data';
|
|
|
11
11
|
export { useSegmentStats } from './use-segment-stats';
|
|
12
12
|
export { useSendTimeData } from './use-send-times';
|
|
13
13
|
export { useABTestData } from './use-ab-tests';
|
|
14
|
+
export {
|
|
15
|
+
useManualPushTemplates,
|
|
16
|
+
useManualPushHistory,
|
|
17
|
+
useManualPushStats,
|
|
18
|
+
useDispatchManualPush,
|
|
19
|
+
useDispatchTestManualPush,
|
|
20
|
+
} from './use-manual-pushes';
|
|
21
|
+
export type {
|
|
22
|
+
ManualPushTemplate,
|
|
23
|
+
ManualPushHistoryItem,
|
|
24
|
+
ManualPushStats,
|
|
25
|
+
ManualPushCounts,
|
|
26
|
+
ManualPushSegmentSnapshot,
|
|
27
|
+
DispatchPayload,
|
|
28
|
+
DispatchTestPayload,
|
|
29
|
+
DispatchResult,
|
|
30
|
+
} from './use-manual-pushes';
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
2
|
+
import { useFetchClient } from '@strapi/strapi/admin';
|
|
3
|
+
import { crmKeys } from '../../lib/react-query';
|
|
4
|
+
|
|
5
|
+
const PLUGIN_BASE = '/crm-dashboard';
|
|
6
|
+
|
|
7
|
+
export interface ManualPushImage {
|
|
8
|
+
url: string;
|
|
9
|
+
alternativeText?: string | null;
|
|
10
|
+
width?: number;
|
|
11
|
+
height?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ManualPushTemplate {
|
|
15
|
+
documentId: string;
|
|
16
|
+
name: string;
|
|
17
|
+
body: string;
|
|
18
|
+
bodyEn: string | null;
|
|
19
|
+
image: ManualPushImage | null;
|
|
20
|
+
buttonLabel: string | null;
|
|
21
|
+
buttonUrl: string | null;
|
|
22
|
+
buttonLabelEn: string | null;
|
|
23
|
+
buttonUrlEn: string | null;
|
|
24
|
+
testUserIds: number[];
|
|
25
|
+
locales: string[];
|
|
26
|
+
updatedAt: string;
|
|
27
|
+
createdAt: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ManualPushCounts {
|
|
31
|
+
planned: number;
|
|
32
|
+
sent: number;
|
|
33
|
+
failed: number;
|
|
34
|
+
cancelled: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ManualPushSegmentSnapshot {
|
|
38
|
+
userIds?: number[];
|
|
39
|
+
languages?: ('ru' | 'en')[];
|
|
40
|
+
lastActiveAfter?: string;
|
|
41
|
+
balanceMin?: number;
|
|
42
|
+
balanceMax?: number;
|
|
43
|
+
registeredAfter?: string;
|
|
44
|
+
registeredBefore?: string;
|
|
45
|
+
excludeOptedOut?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ManualPushHistoryItem {
|
|
49
|
+
manualPushId: string;
|
|
50
|
+
strapiDocumentId: string;
|
|
51
|
+
dispatchedAt: string;
|
|
52
|
+
dispatchedBy: string | null;
|
|
53
|
+
totalQueued: number;
|
|
54
|
+
counts: ManualPushCounts;
|
|
55
|
+
segmentSnapshot: ManualPushSegmentSnapshot;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ManualPushFailure {
|
|
59
|
+
logId: number;
|
|
60
|
+
userId: number;
|
|
61
|
+
errorReason: string | null;
|
|
62
|
+
updatedAt: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface ManualPushStats {
|
|
66
|
+
manualPushId: string;
|
|
67
|
+
strapiDocumentId: string;
|
|
68
|
+
dispatchedAt: string;
|
|
69
|
+
dispatchedBy: string | null;
|
|
70
|
+
totalQueued: number;
|
|
71
|
+
counts: ManualPushCounts;
|
|
72
|
+
recentFailures: ManualPushFailure[];
|
|
73
|
+
segmentSnapshot: ManualPushSegmentSnapshot;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface DispatchResult {
|
|
77
|
+
manualPushId: string | null;
|
|
78
|
+
totalQueued: number;
|
|
79
|
+
dryRun: boolean;
|
|
80
|
+
approximateCount: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface DispatchPayload {
|
|
84
|
+
strapiDocumentId: string;
|
|
85
|
+
segment: ManualPushSegmentSnapshot;
|
|
86
|
+
scheduledAt?: string;
|
|
87
|
+
dryRun?: boolean;
|
|
88
|
+
dispatchedBy?: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface DispatchTestPayload {
|
|
92
|
+
strapiDocumentId: string;
|
|
93
|
+
dispatchedBy?: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const extractBackendError = (data: any): string => {
|
|
97
|
+
if (!data) return 'Unknown error';
|
|
98
|
+
if (data.backend?.message) return data.backend.message;
|
|
99
|
+
if (typeof data.backend === 'string') return data.backend;
|
|
100
|
+
if (data.error) return data.error;
|
|
101
|
+
return JSON.stringify(data);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export const useManualPushTemplates = () => {
|
|
105
|
+
const { get } = useFetchClient();
|
|
106
|
+
return useQuery({
|
|
107
|
+
queryKey: crmKeys.manualPushTemplates(),
|
|
108
|
+
queryFn: async (): Promise<ManualPushTemplate[]> => {
|
|
109
|
+
const { data } = await get(`${PLUGIN_BASE}/manual-pushes/templates`);
|
|
110
|
+
if (data?.error) throw new Error(data.error);
|
|
111
|
+
return data?.data ?? [];
|
|
112
|
+
},
|
|
113
|
+
staleTime: 60 * 1000,
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const useManualPushHistory = () => {
|
|
118
|
+
const { get } = useFetchClient();
|
|
119
|
+
return useQuery({
|
|
120
|
+
queryKey: crmKeys.manualPushHistory(),
|
|
121
|
+
queryFn: async (): Promise<ManualPushHistoryItem[]> => {
|
|
122
|
+
const { data } = await get(`${PLUGIN_BASE}/manual-pushes/history`, {
|
|
123
|
+
params: { limit: 50 },
|
|
124
|
+
});
|
|
125
|
+
if (Array.isArray(data)) return data;
|
|
126
|
+
if (data?.data && Array.isArray(data.data)) return data.data;
|
|
127
|
+
return [];
|
|
128
|
+
},
|
|
129
|
+
staleTime: 15 * 1000,
|
|
130
|
+
refetchInterval: 10 * 1000,
|
|
131
|
+
});
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export const useManualPushStats = (manualPushId: string | null) => {
|
|
135
|
+
const { get } = useFetchClient();
|
|
136
|
+
return useQuery({
|
|
137
|
+
queryKey: crmKeys.manualPushStats(manualPushId ?? ''),
|
|
138
|
+
queryFn: async (): Promise<ManualPushStats> => {
|
|
139
|
+
const { data } = await get(`${PLUGIN_BASE}/manual-pushes/${manualPushId}/stats`);
|
|
140
|
+
return data;
|
|
141
|
+
},
|
|
142
|
+
enabled: Boolean(manualPushId),
|
|
143
|
+
refetchInterval: 5 * 1000,
|
|
144
|
+
});
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export const useDispatchManualPush = () => {
|
|
148
|
+
const { post } = useFetchClient();
|
|
149
|
+
const queryClient = useQueryClient();
|
|
150
|
+
return useMutation<DispatchResult, Error, DispatchPayload>({
|
|
151
|
+
mutationFn: async (payload) => {
|
|
152
|
+
const { data } = await post(`${PLUGIN_BASE}/manual-pushes/dispatch`, payload);
|
|
153
|
+
if (data?.error) throw new Error(extractBackendError(data));
|
|
154
|
+
return data;
|
|
155
|
+
},
|
|
156
|
+
onSuccess: (_data, variables) => {
|
|
157
|
+
if (!variables.dryRun) {
|
|
158
|
+
queryClient.invalidateQueries({ queryKey: crmKeys.manualPushHistory() });
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export const useDispatchTestManualPush = () => {
|
|
165
|
+
const { post } = useFetchClient();
|
|
166
|
+
const queryClient = useQueryClient();
|
|
167
|
+
return useMutation<DispatchResult, Error, DispatchTestPayload>({
|
|
168
|
+
mutationFn: async (payload) => {
|
|
169
|
+
const { data } = await post(`${PLUGIN_BASE}/manual-pushes/dispatch-test`, payload);
|
|
170
|
+
if (data?.error) throw new Error(extractBackendError(data));
|
|
171
|
+
return data;
|
|
172
|
+
},
|
|
173
|
+
onSuccess: () => {
|
|
174
|
+
queryClient.invalidateQueries({ queryKey: crmKeys.manualPushHistory() });
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
};
|
|
@@ -22,4 +22,9 @@ export const crmKeys = {
|
|
|
22
22
|
campaigns: () => [...crmKeys.all, 'campaigns'] as const,
|
|
23
23
|
segments: () => [...crmKeys.all, 'segments'] as const,
|
|
24
24
|
templates: () => [...crmKeys.all, 'templates'] as const,
|
|
25
|
+
|
|
26
|
+
manualPushTemplates: () => [...crmKeys.all, 'manual-pushes', 'templates'] as const,
|
|
27
|
+
manualPushHistory: () => [...crmKeys.all, 'manual-pushes', 'history'] as const,
|
|
28
|
+
manualPushStats: (manualPushId: string) =>
|
|
29
|
+
[...crmKeys.all, 'manual-pushes', 'stats', manualPushId] as const,
|
|
25
30
|
} as const;
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
--tab-icon-stats-gradient: linear-gradient(135deg, #e0e7ff 0%, #c7d2fe 100%);
|
|
27
27
|
--tab-icon-logs-gradient: linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%);
|
|
28
28
|
--tab-icon-antispam-gradient: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
|
|
29
|
+
--tab-icon-manual-pushes-gradient: linear-gradient(135deg, #e0e7ff 0%, #b4b1ff 100%);
|
|
29
30
|
|
|
30
31
|
min-height: 100vh;
|
|
31
32
|
background: var(--crm-bg-secondary);
|
|
@@ -59,6 +60,7 @@
|
|
|
59
60
|
--tab-icon-stats-gradient: linear-gradient(135deg, rgba(123, 121, 255, 0.2) 0%, rgba(99, 102, 241, 0.3) 100%);
|
|
60
61
|
--tab-icon-logs-gradient: linear-gradient(135deg, rgba(16, 185, 129, 0.2) 0%, rgba(52, 211, 153, 0.3) 100%);
|
|
61
62
|
--tab-icon-antispam-gradient: linear-gradient(135deg, rgba(245, 158, 11, 0.2) 0%, rgba(251, 191, 36, 0.3) 100%);
|
|
63
|
+
--tab-icon-manual-pushes-gradient: linear-gradient(135deg, rgba(123, 121, 255, 0.2) 0%, rgba(165, 163, 255, 0.3) 100%);
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
.dashboard-header {
|
|
@@ -160,6 +162,11 @@
|
|
|
160
162
|
color: var(--crm-warning);
|
|
161
163
|
}
|
|
162
164
|
|
|
165
|
+
.tab-icon.manual-pushes {
|
|
166
|
+
background: var(--tab-icon-manual-pushes-gradient);
|
|
167
|
+
color: var(--crm-accent);
|
|
168
|
+
}
|
|
169
|
+
|
|
163
170
|
.tab-content-wrapper {
|
|
164
171
|
display: flex;
|
|
165
172
|
flex-direction: column;
|
|
@@ -213,6 +220,11 @@
|
|
|
213
220
|
color: var(--crm-warning);
|
|
214
221
|
}
|
|
215
222
|
|
|
223
|
+
.tab-badge.manual-pushes {
|
|
224
|
+
background: var(--crm-accent-light);
|
|
225
|
+
color: var(--crm-accent);
|
|
226
|
+
}
|
|
227
|
+
|
|
216
228
|
.tab-card.active .tab-badge.stats {
|
|
217
229
|
background: var(--crm-accent);
|
|
218
230
|
color: white;
|
|
@@ -228,6 +240,11 @@
|
|
|
228
240
|
color: white;
|
|
229
241
|
}
|
|
230
242
|
|
|
243
|
+
.tab-card.active .tab-badge.manual-pushes {
|
|
244
|
+
background: var(--crm-accent);
|
|
245
|
+
color: white;
|
|
246
|
+
}
|
|
247
|
+
|
|
231
248
|
.tab-content {
|
|
232
249
|
margin: 0 24px;
|
|
233
250
|
padding: 24px;
|