@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.
@@ -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;