@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.
- package/admin/src/components/ButtonsBuilder/index.tsx +259 -0
- package/admin/src/components/CancelConditionsField/constants.ts +104 -0
- package/admin/src/components/CancelConditionsField/index.tsx +381 -0
- package/admin/src/components/TriggerConfigField/index.tsx +333 -0
- package/admin/src/index.ts +76 -2
- package/dist/_chunks/{index-CNvqpnNt.mjs → index-BSDO36Pf.mjs} +1 -468
- package/dist/_chunks/index-Bnjm_sYk.mjs +339 -0
- package/dist/_chunks/index-Cf8DZYT6.js +341 -0
- package/dist/_chunks/index-CiwOzO0B.js +377 -0
- package/dist/_chunks/index-DFqEb9sm.mjs +253 -0
- package/dist/_chunks/index-DRJ5o0cz.js +253 -0
- package/dist/_chunks/index-XE6toVNT.mjs +170 -0
- package/dist/_chunks/index-d16UivTb.js +170 -0
- package/dist/_chunks/utils-C6_ndVAZ.mjs +491 -0
- package/dist/_chunks/utils-CmonL0io.js +490 -0
- package/dist/admin/index.js +73 -2
- package/dist/admin/index.mjs +74 -3
- package/dist/server/index.js +15 -0
- package/dist/server/index.mjs +15 -0
- package/package.json +1 -1
- package/server/src/register.ts +18 -0
- package/dist/_chunks/index-DIwnSzER.js +0 -844
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import React, { forwardRef, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Badge,
|
|
4
|
+
Box,
|
|
5
|
+
Button,
|
|
6
|
+
Field,
|
|
7
|
+
Flex,
|
|
8
|
+
IconButton,
|
|
9
|
+
TextInput,
|
|
10
|
+
Typography,
|
|
11
|
+
Tooltip,
|
|
12
|
+
} from '@strapi/design-system';
|
|
13
|
+
import { Plus, Trash, ArrowUp, ArrowDown } from '@strapi/icons';
|
|
14
|
+
import { generateId } from '../RulesBuilder/utils';
|
|
15
|
+
|
|
16
|
+
type TelegramButton = {
|
|
17
|
+
id: string;
|
|
18
|
+
text: string;
|
|
19
|
+
url: string;
|
|
20
|
+
row?: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
interface ButtonsBuilderProps {
|
|
24
|
+
name: string;
|
|
25
|
+
value?: string | null;
|
|
26
|
+
onChange: (event: { target: { name: string; value: string } }) => void;
|
|
27
|
+
intlLabel: {
|
|
28
|
+
id: string;
|
|
29
|
+
defaultMessage: string;
|
|
30
|
+
};
|
|
31
|
+
attribute?: unknown;
|
|
32
|
+
disabled?: boolean;
|
|
33
|
+
error?: string;
|
|
34
|
+
required?: boolean;
|
|
35
|
+
hint?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const parseButtons = (value: string | null | undefined): TelegramButton[] => {
|
|
39
|
+
if (!value) return [];
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(value);
|
|
42
|
+
if (!Array.isArray(parsed)) return [];
|
|
43
|
+
return parsed.map((b) => ({
|
|
44
|
+
id: typeof b?.id === 'string' ? b.id : generateId(),
|
|
45
|
+
text: typeof b?.text === 'string' ? b.text : '',
|
|
46
|
+
url: typeof b?.url === 'string' ? b.url : '',
|
|
47
|
+
row: typeof b?.row === 'number' ? b.row : 0,
|
|
48
|
+
}));
|
|
49
|
+
} catch {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const serializeButtons = (buttons: TelegramButton[]): string => JSON.stringify(buttons);
|
|
55
|
+
|
|
56
|
+
const isValidUrl = (url: string): boolean => {
|
|
57
|
+
try {
|
|
58
|
+
const u = new URL(url);
|
|
59
|
+
return u.protocol === 'http:' || u.protocol === 'https:';
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const ButtonsBuilder = forwardRef<HTMLDivElement, ButtonsBuilderProps>(
|
|
66
|
+
({ name, value, onChange, intlLabel, disabled, error, required, hint }, ref) => {
|
|
67
|
+
const [buttons, setButtons] = useState<TelegramButton[]>(() => parseButtons(value));
|
|
68
|
+
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
setButtons(parseButtons(value));
|
|
71
|
+
}, [value]);
|
|
72
|
+
|
|
73
|
+
const update = (next: TelegramButton[]) => {
|
|
74
|
+
setButtons(next);
|
|
75
|
+
onChange({ target: { name, value: serializeButtons(next) } });
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const addButton = () => {
|
|
79
|
+
update([
|
|
80
|
+
...buttons,
|
|
81
|
+
{ id: generateId(), text: 'Button', url: 'https://cases.gg', row: 0 },
|
|
82
|
+
]);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const updateButton = (id: string, patch: Partial<TelegramButton>) => {
|
|
86
|
+
update(buttons.map((b) => (b.id === id ? { ...b, ...patch } : b)));
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const deleteButton = (id: string) => {
|
|
90
|
+
update(buttons.filter((b) => b.id !== id));
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const move = (from: number, to: number) => {
|
|
94
|
+
if (to < 0 || to >= buttons.length) return;
|
|
95
|
+
const copy = [...buttons];
|
|
96
|
+
const [item] = copy.splice(from, 1);
|
|
97
|
+
copy.splice(to, 0, item);
|
|
98
|
+
update(copy);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const previewRows = useMemo(() => {
|
|
102
|
+
const rows = new Map<number, TelegramButton[]>();
|
|
103
|
+
for (const b of buttons) {
|
|
104
|
+
const row = b.row ?? 0;
|
|
105
|
+
rows.set(row, [...(rows.get(row) || []), b]);
|
|
106
|
+
}
|
|
107
|
+
return Array.from(rows.entries())
|
|
108
|
+
.sort((a, b) => a[0] - b[0])
|
|
109
|
+
.map(([row, items]) => ({ row, items }));
|
|
110
|
+
}, [buttons]);
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<Field.Root name={name} error={error} required={required} hint={hint} ref={ref}>
|
|
114
|
+
<Flex direction="column" gap={3}>
|
|
115
|
+
<Field.Label>{intlLabel?.defaultMessage || 'Buttons'}</Field.Label>
|
|
116
|
+
<Typography variant="pi" textColor="neutral600">
|
|
117
|
+
Build Telegram inline keyboard buttons (text + URL). Stored as JSON.
|
|
118
|
+
</Typography>
|
|
119
|
+
|
|
120
|
+
<Flex direction="column" gap={2}>
|
|
121
|
+
{buttons.map((btn, idx) => {
|
|
122
|
+
const urlOk = btn.url.length === 0 ? true : isValidUrl(btn.url);
|
|
123
|
+
return (
|
|
124
|
+
<Flex
|
|
125
|
+
key={btn.id}
|
|
126
|
+
gap={2}
|
|
127
|
+
alignItems="flex-start"
|
|
128
|
+
padding={3}
|
|
129
|
+
background="neutral0"
|
|
130
|
+
hasRadius
|
|
131
|
+
style={{ border: '1px solid #dcdce4' }}
|
|
132
|
+
>
|
|
133
|
+
<Box style={{ flex: 2, minWidth: 180 }}>
|
|
134
|
+
<Field.Root name={`${name}.${btn.id}.text`}>
|
|
135
|
+
<Field.Label>Text</Field.Label>
|
|
136
|
+
<TextInput
|
|
137
|
+
value={btn.text}
|
|
138
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
139
|
+
updateButton(btn.id, { text: e.target.value })
|
|
140
|
+
}
|
|
141
|
+
disabled={disabled}
|
|
142
|
+
/>
|
|
143
|
+
</Field.Root>
|
|
144
|
+
</Box>
|
|
145
|
+
|
|
146
|
+
<Box style={{ flex: 3, minWidth: 220 }}>
|
|
147
|
+
<Field.Root name={`${name}.${btn.id}.url`}>
|
|
148
|
+
<Field.Label>URL</Field.Label>
|
|
149
|
+
<TextInput
|
|
150
|
+
value={btn.url}
|
|
151
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
152
|
+
updateButton(btn.id, { url: e.target.value })
|
|
153
|
+
}
|
|
154
|
+
disabled={disabled}
|
|
155
|
+
/>
|
|
156
|
+
{!urlOk && (
|
|
157
|
+
<Typography variant="pi" textColor="danger600" style={{ marginTop: 4 }}>
|
|
158
|
+
Please enter a valid http/https URL
|
|
159
|
+
</Typography>
|
|
160
|
+
)}
|
|
161
|
+
</Field.Root>
|
|
162
|
+
</Box>
|
|
163
|
+
|
|
164
|
+
<Box style={{ width: 90 }}>
|
|
165
|
+
<Field.Root name={`${name}.${btn.id}.row`}>
|
|
166
|
+
<Field.Label>Row</Field.Label>
|
|
167
|
+
<TextInput
|
|
168
|
+
type="number"
|
|
169
|
+
value={String(btn.row ?? 0)}
|
|
170
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
171
|
+
updateButton(btn.id, { row: parseInt(e.target.value, 10) || 0 })
|
|
172
|
+
}
|
|
173
|
+
disabled={disabled}
|
|
174
|
+
/>
|
|
175
|
+
</Field.Root>
|
|
176
|
+
</Box>
|
|
177
|
+
|
|
178
|
+
<Flex direction="column" gap={1} paddingTop={6}>
|
|
179
|
+
<Tooltip label="Move up">
|
|
180
|
+
<IconButton
|
|
181
|
+
onClick={() => move(idx, idx - 1)}
|
|
182
|
+
label="Move up"
|
|
183
|
+
variant="ghost"
|
|
184
|
+
disabled={disabled || idx === 0}
|
|
185
|
+
>
|
|
186
|
+
<ArrowUp />
|
|
187
|
+
</IconButton>
|
|
188
|
+
</Tooltip>
|
|
189
|
+
<Tooltip label="Move down">
|
|
190
|
+
<IconButton
|
|
191
|
+
onClick={() => move(idx, idx + 1)}
|
|
192
|
+
label="Move down"
|
|
193
|
+
variant="ghost"
|
|
194
|
+
disabled={disabled || idx === buttons.length - 1}
|
|
195
|
+
>
|
|
196
|
+
<ArrowDown />
|
|
197
|
+
</IconButton>
|
|
198
|
+
</Tooltip>
|
|
199
|
+
<Tooltip label="Delete button">
|
|
200
|
+
<IconButton
|
|
201
|
+
onClick={() => deleteButton(btn.id)}
|
|
202
|
+
label="Delete button"
|
|
203
|
+
variant="ghost"
|
|
204
|
+
disabled={disabled}
|
|
205
|
+
>
|
|
206
|
+
<Trash />
|
|
207
|
+
</IconButton>
|
|
208
|
+
</Tooltip>
|
|
209
|
+
</Flex>
|
|
210
|
+
</Flex>
|
|
211
|
+
);
|
|
212
|
+
})}
|
|
213
|
+
|
|
214
|
+
{buttons.length === 0 && (
|
|
215
|
+
<Box padding={4} background="neutral0" hasRadius style={{ border: '1px dashed #dcdce4' }}>
|
|
216
|
+
<Typography variant="omega" textColor="neutral600">
|
|
217
|
+
No buttons. Add one to create an inline keyboard.
|
|
218
|
+
</Typography>
|
|
219
|
+
</Box>
|
|
220
|
+
)}
|
|
221
|
+
</Flex>
|
|
222
|
+
|
|
223
|
+
<Flex gap={2}>
|
|
224
|
+
<Button startIcon={<Plus />} onClick={addButton} disabled={disabled} variant="secondary">
|
|
225
|
+
Add button
|
|
226
|
+
</Button>
|
|
227
|
+
</Flex>
|
|
228
|
+
|
|
229
|
+
{buttons.length > 0 && (
|
|
230
|
+
<Box paddingTop={2}>
|
|
231
|
+
<Typography variant="pi" textColor="neutral600" style={{ marginBottom: 8, display: 'block' }}>
|
|
232
|
+
Preview
|
|
233
|
+
</Typography>
|
|
234
|
+
<Flex direction="column" gap={2}>
|
|
235
|
+
{previewRows.map((row) => (
|
|
236
|
+
<Flex key={row.row} gap={1} wrap="wrap">
|
|
237
|
+
{row.items.map((b) => (
|
|
238
|
+
<Badge key={b.id} backgroundColor="primary100">
|
|
239
|
+
{b.text || '(empty)'}
|
|
240
|
+
</Badge>
|
|
241
|
+
))}
|
|
242
|
+
</Flex>
|
|
243
|
+
))}
|
|
244
|
+
</Flex>
|
|
245
|
+
</Box>
|
|
246
|
+
)}
|
|
247
|
+
|
|
248
|
+
{error && <Field.Error>{error}</Field.Error>}
|
|
249
|
+
{hint && <Field.Hint>{hint}</Field.Hint>}
|
|
250
|
+
</Flex>
|
|
251
|
+
</Field.Root>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
ButtonsBuilder.displayName = 'ButtonsBuilder';
|
|
257
|
+
|
|
258
|
+
export default ButtonsBuilder;
|
|
259
|
+
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { RuleOperator } from '../RulesBuilder/types';
|
|
2
|
+
|
|
3
|
+
export interface CancelMetricDefinition {
|
|
4
|
+
key: string;
|
|
5
|
+
label: string;
|
|
6
|
+
valueType: 'boolean' | 'number' | 'time_ago';
|
|
7
|
+
description: string;
|
|
8
|
+
operators: RuleOperator[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const booleanOperators: RuleOperator[] = ['$eq', '$ne'];
|
|
12
|
+
const numberOperators: RuleOperator[] = ['$eq', '$ne', '$gt', '$lt', '$gte', '$lte'];
|
|
13
|
+
const timeAgoOperators: RuleOperator[] = ['$gt', '$lt', '$gte', '$lte'];
|
|
14
|
+
|
|
15
|
+
export const CANCEL_METRICS: CancelMetricDefinition[] = [
|
|
16
|
+
{
|
|
17
|
+
key: 'has_deposit_since_trigger',
|
|
18
|
+
label: 'Made Deposit',
|
|
19
|
+
valueType: 'boolean',
|
|
20
|
+
description: 'User made a successful deposit after the trigger event',
|
|
21
|
+
operators: booleanOperators,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
key: 'has_activity_since_trigger',
|
|
25
|
+
label: 'Had Activity',
|
|
26
|
+
valueType: 'boolean',
|
|
27
|
+
description: 'User had any activity (case open, upgrade, battle, etc.) after trigger',
|
|
28
|
+
operators: booleanOperators,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
key: 'has_session_since_trigger',
|
|
32
|
+
label: 'Returned to Site',
|
|
33
|
+
valueType: 'boolean',
|
|
34
|
+
description: 'User had a new session/visit after the trigger event',
|
|
35
|
+
operators: booleanOperators,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
key: 'balance_sufficient',
|
|
39
|
+
label: 'Balance Sufficient',
|
|
40
|
+
valueType: 'boolean',
|
|
41
|
+
description: 'User balance is now sufficient for the intended action',
|
|
42
|
+
operators: booleanOperators,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
key: 'is_authenticated',
|
|
46
|
+
label: 'Is Authenticated',
|
|
47
|
+
valueType: 'boolean',
|
|
48
|
+
description: 'User has logged in / authenticated',
|
|
49
|
+
operators: booleanOperators,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
key: 'battle_completed',
|
|
53
|
+
label: 'Battle Completed',
|
|
54
|
+
valueType: 'boolean',
|
|
55
|
+
description: 'The battle was completed or continued',
|
|
56
|
+
operators: booleanOperators,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
key: 'upgrade_completed',
|
|
60
|
+
label: 'Upgrade Completed',
|
|
61
|
+
valueType: 'boolean',
|
|
62
|
+
description: 'The upgrade was completed or retried',
|
|
63
|
+
operators: booleanOperators,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
key: 'contract_completed',
|
|
67
|
+
label: 'Contract Completed',
|
|
68
|
+
valueType: 'boolean',
|
|
69
|
+
description: 'The contract was completed',
|
|
70
|
+
operators: booleanOperators,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
key: 'deposit_count',
|
|
74
|
+
label: 'Deposit Count',
|
|
75
|
+
valueType: 'number',
|
|
76
|
+
description: 'Total number of deposits (e.g., for checking if 2nd deposit exists)',
|
|
77
|
+
operators: numberOperators,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
key: 'case_open_count_since_trigger',
|
|
81
|
+
label: 'Cases Opened Since Trigger',
|
|
82
|
+
valueType: 'number',
|
|
83
|
+
description: 'Number of cases opened after the trigger event',
|
|
84
|
+
operators: numberOperators,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
key: 'time_since_trigger',
|
|
88
|
+
label: 'Time Since Trigger',
|
|
89
|
+
valueType: 'time_ago',
|
|
90
|
+
description: 'How long since the trigger event fired',
|
|
91
|
+
operators: timeAgoOperators,
|
|
92
|
+
},
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
export const CANCEL_TIME_UNITS = [
|
|
96
|
+
{ value: 'minutes_ago', label: 'minutes' },
|
|
97
|
+
{ value: 'hours_ago', label: 'hours' },
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
export const DEFAULT_CANCEL_RULE: Omit<{ id: string; field: string; operator: RuleOperator; value: boolean }, 'id'> = {
|
|
101
|
+
field: 'has_deposit_since_trigger',
|
|
102
|
+
operator: '$eq',
|
|
103
|
+
value: true,
|
|
104
|
+
};
|