@inspirer-dev/crm-dashboard 1.0.10 → 1.0.12

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.
@@ -0,0 +1,333 @@
1
+ import React, { forwardRef, useState, useEffect } from 'react';
2
+ import {
3
+ Box,
4
+ Field,
5
+ Flex,
6
+ TextInput,
7
+ Typography,
8
+ SingleSelect,
9
+ SingleSelectOption,
10
+ Card,
11
+ CardContent,
12
+ Checkbox,
13
+ } from '@strapi/design-system';
14
+
15
+ interface TriggerConfig {
16
+ type: 'db_based' | 'event_based';
17
+ eventName?: string;
18
+ delayValue?: number;
19
+ delayUnit?: 'seconds' | 'minutes' | 'hours';
20
+ scheduleType?: 'daily' | 'weekly' | 'cron';
21
+ scheduleTime?: string;
22
+ scheduleDays?: number[];
23
+ scheduleCron?: string;
24
+ }
25
+
26
+ interface TriggerConfigFieldProps {
27
+ name: string;
28
+ value?: string | null;
29
+ onChange: (event: { target: { name: string; value: string } }) => void;
30
+ intlLabel: {
31
+ id: string;
32
+ defaultMessage: string;
33
+ };
34
+ attribute?: unknown;
35
+ disabled?: boolean;
36
+ error?: string;
37
+ required?: boolean;
38
+ hint?: string;
39
+ }
40
+
41
+ const DEFAULT_CONFIG: TriggerConfig = {
42
+ type: 'event_based',
43
+ eventName: '',
44
+ delayValue: 10,
45
+ delayUnit: 'minutes',
46
+ scheduleType: 'daily',
47
+ scheduleTime: '12:00',
48
+ scheduleDays: [1, 2, 3, 4, 5],
49
+ scheduleCron: '0 12 * * *',
50
+ };
51
+
52
+ const WEEKDAYS = [
53
+ { value: 0, label: 'Sun' },
54
+ { value: 1, label: 'Mon' },
55
+ { value: 2, label: 'Tue' },
56
+ { value: 3, label: 'Wed' },
57
+ { value: 4, label: 'Thu' },
58
+ { value: 5, label: 'Fri' },
59
+ { value: 6, label: 'Sat' },
60
+ ];
61
+
62
+ const parseConfig = (value: string | null | undefined): TriggerConfig => {
63
+ if (!value) return DEFAULT_CONFIG;
64
+ try {
65
+ const parsed = JSON.parse(value);
66
+ return {
67
+ type: parsed.type || 'event_based',
68
+ eventName: parsed.eventName || '',
69
+ delayValue: parsed.delayValue ?? 10,
70
+ delayUnit: parsed.delayUnit || 'minutes',
71
+ scheduleType: parsed.scheduleType || 'daily',
72
+ scheduleTime: parsed.scheduleTime || '12:00',
73
+ scheduleDays: parsed.scheduleDays || [1, 2, 3, 4, 5],
74
+ scheduleCron: parsed.scheduleCron || '0 12 * * *',
75
+ };
76
+ } catch {
77
+ return DEFAULT_CONFIG;
78
+ }
79
+ };
80
+
81
+ const serializeConfig = (config: TriggerConfig): string => {
82
+ return JSON.stringify(config);
83
+ };
84
+
85
+ const TriggerConfigField = forwardRef<HTMLDivElement, TriggerConfigFieldProps>(
86
+ ({ name, value, onChange, intlLabel, disabled, error, required, hint }, ref) => {
87
+ const [config, setConfig] = useState<TriggerConfig>(() => parseConfig(value));
88
+
89
+ useEffect(() => {
90
+ const parsed = parseConfig(value);
91
+ setConfig(parsed);
92
+ }, [value]);
93
+
94
+ const handleUpdate = (updates: Partial<TriggerConfig>) => {
95
+ const newConfig = { ...config, ...updates };
96
+ setConfig(newConfig);
97
+ onChange({
98
+ target: {
99
+ name,
100
+ value: serializeConfig(newConfig),
101
+ },
102
+ });
103
+ };
104
+
105
+ const toggleDay = (day: number) => {
106
+ const days = config.scheduleDays || [];
107
+ const newDays = days.includes(day)
108
+ ? days.filter((d) => d !== day)
109
+ : [...days, day].sort((a, b) => a - b);
110
+ handleUpdate({ scheduleDays: newDays });
111
+ };
112
+
113
+ const isEventBased = config.type === 'event_based';
114
+ const isDbBased = config.type === 'db_based';
115
+
116
+ return (
117
+ <Field.Root name={name} error={error} required={required} hint={hint} ref={ref}>
118
+ <Flex direction="column" gap={3}>
119
+ <Field.Label>{intlLabel?.defaultMessage || 'Trigger Configuration'}</Field.Label>
120
+
121
+ <Box>
122
+ <Typography variant="pi" textColor="neutral600" style={{ marginBottom: '8px', display: 'block' }}>
123
+ Campaign Type
124
+ </Typography>
125
+ <Flex gap={2}>
126
+ <Box
127
+ padding={3}
128
+ background={isEventBased ? 'primary100' : 'neutral100'}
129
+ hasRadius
130
+ style={{
131
+ cursor: disabled ? 'not-allowed' : 'pointer',
132
+ border: `2px solid ${isEventBased ? '#4945ff' : '#dcdce4'}`,
133
+ flex: 1,
134
+ textAlign: 'center',
135
+ }}
136
+ onClick={() => !disabled && handleUpdate({ type: 'event_based' })}
137
+ >
138
+ <Typography
139
+ variant="omega"
140
+ fontWeight={isEventBased ? 'bold' : 'regular'}
141
+ textColor={isEventBased ? 'primary600' : 'neutral600'}
142
+ >
143
+ Event-Based
144
+ </Typography>
145
+ <Typography variant="pi" textColor="neutral500">
146
+ Fires on user action
147
+ </Typography>
148
+ </Box>
149
+ <Box
150
+ padding={3}
151
+ background={isDbBased ? 'primary100' : 'neutral100'}
152
+ hasRadius
153
+ style={{
154
+ cursor: disabled ? 'not-allowed' : 'pointer',
155
+ border: `2px solid ${isDbBased ? '#4945ff' : '#dcdce4'}`,
156
+ flex: 1,
157
+ textAlign: 'center',
158
+ }}
159
+ onClick={() => !disabled && handleUpdate({ type: 'db_based' })}
160
+ >
161
+ <Typography
162
+ variant="omega"
163
+ fontWeight={isDbBased ? 'bold' : 'regular'}
164
+ textColor={isDbBased ? 'primary600' : 'neutral600'}
165
+ >
166
+ DB-Based (Scheduled)
167
+ </Typography>
168
+ <Typography variant="pi" textColor="neutral500">
169
+ Runs on schedule
170
+ </Typography>
171
+ </Box>
172
+ </Flex>
173
+ </Box>
174
+
175
+ {isEventBased && (
176
+ <Card background="neutral100">
177
+ <CardContent>
178
+ <Flex direction="column" gap={4} padding={4}>
179
+ <Typography variant="delta">Event Configuration</Typography>
180
+
181
+ <Field.Root name={`${name}-eventName`}>
182
+ <Field.Label>Event Name</Field.Label>
183
+ <TextInput
184
+ placeholder="e.g., gg-case-deposit"
185
+ value={config.eventName || ''}
186
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
187
+ handleUpdate({ eventName: e.target.value })
188
+ }
189
+ disabled={disabled}
190
+ />
191
+ <Field.Hint>
192
+ The event that triggers this campaign (e.g., gg-case-deposit, gg-deposit-suggestion-selected)
193
+ </Field.Hint>
194
+ </Field.Root>
195
+
196
+ <Box>
197
+ <Typography variant="pi" textColor="neutral800" style={{ marginBottom: '8px', display: 'block' }}>
198
+ Delay Before Sending
199
+ </Typography>
200
+ <Flex gap={2} alignItems="flex-start">
201
+ <Box style={{ width: '120px' }}>
202
+ <TextInput
203
+ type="number"
204
+ value={String(config.delayValue || 0)}
205
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
206
+ handleUpdate({ delayValue: parseInt(e.target.value, 10) || 0 })
207
+ }
208
+ disabled={disabled}
209
+ aria-label="Delay value"
210
+ />
211
+ </Box>
212
+ <Box style={{ width: '150px' }}>
213
+ <SingleSelect
214
+ value={config.delayUnit || 'minutes'}
215
+ onChange={(val: string) =>
216
+ handleUpdate({ delayUnit: val as TriggerConfig['delayUnit'] })
217
+ }
218
+ disabled={disabled}
219
+ >
220
+ <SingleSelectOption value="seconds">seconds</SingleSelectOption>
221
+ <SingleSelectOption value="minutes">minutes</SingleSelectOption>
222
+ <SingleSelectOption value="hours">hours</SingleSelectOption>
223
+ </SingleSelect>
224
+ </Box>
225
+ </Flex>
226
+ <Typography variant="pi" textColor="neutral500" style={{ marginTop: '4px' }}>
227
+ Wait this long after the event before checking segment rules and sending
228
+ </Typography>
229
+ </Box>
230
+ </Flex>
231
+ </CardContent>
232
+ </Card>
233
+ )}
234
+
235
+ {isDbBased && (
236
+ <Card background="neutral100">
237
+ <CardContent>
238
+ <Flex direction="column" gap={4} padding={4}>
239
+ <Typography variant="delta">Schedule Configuration</Typography>
240
+
241
+ <Field.Root name={`${name}-scheduleType`}>
242
+ <Field.Label>Schedule Type</Field.Label>
243
+ <SingleSelect
244
+ value={config.scheduleType || 'daily'}
245
+ onChange={(val: string) =>
246
+ handleUpdate({ scheduleType: val as TriggerConfig['scheduleType'] })
247
+ }
248
+ disabled={disabled}
249
+ >
250
+ <SingleSelectOption value="daily">Daily</SingleSelectOption>
251
+ <SingleSelectOption value="weekly">Weekly</SingleSelectOption>
252
+ <SingleSelectOption value="cron">Custom (Cron)</SingleSelectOption>
253
+ </SingleSelect>
254
+ </Field.Root>
255
+
256
+ {(config.scheduleType === 'daily' || config.scheduleType === 'weekly') && (
257
+ <Field.Root name={`${name}-scheduleTime`}>
258
+ <Field.Label>Time of Day (UTC)</Field.Label>
259
+ <TextInput
260
+ type="time"
261
+ value={config.scheduleTime || '12:00'}
262
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
263
+ handleUpdate({ scheduleTime: e.target.value })
264
+ }
265
+ disabled={disabled}
266
+ />
267
+ </Field.Root>
268
+ )}
269
+
270
+ {config.scheduleType === 'weekly' && (
271
+ <Box>
272
+ <Typography variant="pi" textColor="neutral800" style={{ marginBottom: '8px', display: 'block' }}>
273
+ Days of Week
274
+ </Typography>
275
+ <Flex gap={2} wrap="wrap">
276
+ {WEEKDAYS.map((day) => (
277
+ <Box
278
+ key={day.value}
279
+ padding={2}
280
+ background={(config.scheduleDays || []).includes(day.value) ? 'primary100' : 'neutral100'}
281
+ hasRadius
282
+ style={{
283
+ cursor: disabled ? 'not-allowed' : 'pointer',
284
+ border: `1px solid ${(config.scheduleDays || []).includes(day.value) ? '#4945ff' : '#dcdce4'}`,
285
+ minWidth: '50px',
286
+ textAlign: 'center',
287
+ }}
288
+ onClick={() => !disabled && toggleDay(day.value)}
289
+ >
290
+ <Typography
291
+ variant="omega"
292
+ textColor={(config.scheduleDays || []).includes(day.value) ? 'primary600' : 'neutral600'}
293
+ >
294
+ {day.label}
295
+ </Typography>
296
+ </Box>
297
+ ))}
298
+ </Flex>
299
+ </Box>
300
+ )}
301
+
302
+ {config.scheduleType === 'cron' && (
303
+ <Field.Root name={`${name}-scheduleCron`}>
304
+ <Field.Label>Cron Expression</Field.Label>
305
+ <TextInput
306
+ placeholder="0 12 * * *"
307
+ value={config.scheduleCron || ''}
308
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
309
+ handleUpdate({ scheduleCron: e.target.value })
310
+ }
311
+ disabled={disabled}
312
+ />
313
+ <Field.Hint>
314
+ Standard cron format: minute hour day month weekday (e.g., "0 12 * * *" = daily at 12:00 UTC)
315
+ </Field.Hint>
316
+ </Field.Root>
317
+ )}
318
+ </Flex>
319
+ </CardContent>
320
+ </Card>
321
+ )}
322
+
323
+ {error && <Field.Error>{error}</Field.Error>}
324
+ {hint && <Field.Hint>{hint}</Field.Hint>}
325
+ </Flex>
326
+ </Field.Root>
327
+ );
328
+ }
329
+ );
330
+
331
+ TriggerConfigField.displayName = 'TriggerConfigField';
332
+
333
+ export default TriggerConfigField;
@@ -1,4 +1,4 @@
1
- import { Message } from '@strapi/icons';
1
+ import { Message, Clock, Cross, Link } from '@strapi/icons';
2
2
 
3
3
  const PLUGIN_ID = 'crm-dashboard';
4
4
 
@@ -7,7 +7,6 @@ export default {
7
7
  app.customFields.register({
8
8
  name: 'rules',
9
9
  pluginId: PLUGIN_ID,
10
- plugin: PLUGIN_ID,
11
10
  type: 'json',
12
11
  intlLabel: {
13
12
  id: `${PLUGIN_ID}.rules.label`,
@@ -30,6 +29,81 @@ export default {
30
29
  },
31
30
  });
32
31
 
32
+ app.customFields.register({
33
+ name: 'trigger-config',
34
+ pluginId: PLUGIN_ID,
35
+ type: 'json',
36
+ intlLabel: {
37
+ id: `${PLUGIN_ID}.trigger-config.label`,
38
+ defaultMessage: 'Trigger Configuration',
39
+ },
40
+ intlDescription: {
41
+ id: `${PLUGIN_ID}.trigger-config.description`,
42
+ defaultMessage: 'Configure segment type (scheduled or event-triggered) and trigger settings',
43
+ },
44
+ icon: Clock,
45
+ components: {
46
+ Input: async () =>
47
+ import(
48
+ /* webpackChunkName: "crm-trigger-config" */ './components/TriggerConfigField/index'
49
+ ),
50
+ },
51
+ options: {
52
+ base: [],
53
+ advanced: [],
54
+ },
55
+ });
56
+
57
+ app.customFields.register({
58
+ name: 'cancel-conditions',
59
+ pluginId: PLUGIN_ID,
60
+ type: 'json',
61
+ intlLabel: {
62
+ id: `${PLUGIN_ID}.cancel-conditions.label`,
63
+ defaultMessage: 'Cancel Conditions',
64
+ },
65
+ intlDescription: {
66
+ id: `${PLUGIN_ID}.cancel-conditions.description`,
67
+ defaultMessage: 'Conditions that will cancel sending (checked right before send)',
68
+ },
69
+ icon: Cross,
70
+ components: {
71
+ Input: async () =>
72
+ import(
73
+ /* webpackChunkName: "crm-cancel-conditions" */ './components/CancelConditionsField/index'
74
+ ),
75
+ },
76
+ options: {
77
+ base: [],
78
+ advanced: [],
79
+ },
80
+ });
81
+
82
+ app.customFields.register({
83
+ name: 'telegram-buttons',
84
+ pluginId: PLUGIN_ID,
85
+ type: 'json',
86
+ intlLabel: {
87
+ id: `${PLUGIN_ID}.telegram-buttons.label`,
88
+ defaultMessage: 'Buttons',
89
+ },
90
+ intlDescription: {
91
+ id: `${PLUGIN_ID}.telegram-buttons.description`,
92
+ defaultMessage: 'Build Telegram inline buttons (text + URL)',
93
+ },
94
+ icon: Link,
95
+ components: {
96
+ Input: async () =>
97
+ import(
98
+ /* webpackChunkName: "crm-telegram-buttons" */ './components/ButtonsBuilder/index'
99
+ ),
100
+ },
101
+ options: {
102
+ base: [],
103
+ advanced: [],
104
+ },
105
+ });
106
+
33
107
  app.addMenuLink({
34
108
  to: `/plugins/${PLUGIN_ID}`,
35
109
  icon: Message,