@inspirer-dev/crm-dashboard 1.0.89 → 1.0.91
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/RulesBuilder/constants.ts +16 -0
- 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-ClbsgJmF.mjs → index-BXWSCH8D.mjs} +1 -1
- package/dist/_chunks/{index-LXBoz7PC.mjs → index-B_cJDVsP.mjs} +481 -7
- package/dist/_chunks/{index-DKJtyGq7.mjs → index-CFfWhleR.mjs} +1 -1
- package/dist/_chunks/{index-DRXMKPXI.js → index-D6jz5MgX.js} +478 -4
- package/dist/_chunks/{index-T7VXjklK.js → index-DGSELI7S.js} +1 -1
- package/dist/_chunks/{index-CapXG1AZ.js → index-hY_mczO-.js} +1 -1
- package/dist/_chunks/{utils-CAs_GSGv.js → utils-CIIhL4KH.js} +16 -0
- package/dist/_chunks/{utils-BHneBJ7U.mjs → utils-DwR0IuyK.mjs} +16 -0
- package/dist/admin/index.js +3 -3
- package/dist/admin/index.mjs +3 -3
- package/dist/server/index.js +222 -2
- package/dist/server/index.mjs +222 -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 +134 -2
|
@@ -5,7 +5,7 @@ const React = require("react");
|
|
|
5
5
|
const designSystem = require("@strapi/design-system");
|
|
6
6
|
const icons = require("@strapi/icons");
|
|
7
7
|
const styledComponents = require("styled-components");
|
|
8
|
-
const utils = require("./utils-
|
|
8
|
+
const utils = require("./utils-CIIhL4KH.js");
|
|
9
9
|
const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
|
|
10
10
|
const React__default = /* @__PURE__ */ _interopDefault(React);
|
|
11
11
|
const useThemeColors = () => {
|
|
@@ -5,7 +5,7 @@ const React = require("react");
|
|
|
5
5
|
const designSystem = require("@strapi/design-system");
|
|
6
6
|
const icons = require("@strapi/icons");
|
|
7
7
|
const styledComponents = require("styled-components");
|
|
8
|
-
const utils = require("./utils-
|
|
8
|
+
const utils = require("./utils-CIIhL4KH.js");
|
|
9
9
|
const mapRawButton = (b) => ({
|
|
10
10
|
id: typeof b?.id === "string" ? b.id : utils.generateId(),
|
|
11
11
|
text: typeof b?.text === "string" ? b.text : "",
|
|
@@ -233,6 +233,22 @@ const METRICS = [
|
|
|
233
233
|
description: "User opted out of TG messages",
|
|
234
234
|
operators: booleanOperators
|
|
235
235
|
},
|
|
236
|
+
{
|
|
237
|
+
key: "has_whatsapp",
|
|
238
|
+
label: "Has WhatsApp",
|
|
239
|
+
category: "status",
|
|
240
|
+
valueType: "boolean",
|
|
241
|
+
description: "WhatsApp connected",
|
|
242
|
+
operators: booleanOperators
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
key: "whatsapp_opt_out",
|
|
246
|
+
label: "WhatsApp Opt-Out",
|
|
247
|
+
category: "status",
|
|
248
|
+
valueType: "boolean",
|
|
249
|
+
description: "User opted out of WhatsApp messages",
|
|
250
|
+
operators: booleanOperators
|
|
251
|
+
},
|
|
236
252
|
{
|
|
237
253
|
key: "has_opened_welcome_case",
|
|
238
254
|
label: "Has Opened Welcome Case",
|
|
@@ -232,6 +232,22 @@ const METRICS = [
|
|
|
232
232
|
description: "User opted out of TG messages",
|
|
233
233
|
operators: booleanOperators
|
|
234
234
|
},
|
|
235
|
+
{
|
|
236
|
+
key: "has_whatsapp",
|
|
237
|
+
label: "Has WhatsApp",
|
|
238
|
+
category: "status",
|
|
239
|
+
valueType: "boolean",
|
|
240
|
+
description: "WhatsApp connected",
|
|
241
|
+
operators: booleanOperators
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
key: "whatsapp_opt_out",
|
|
245
|
+
label: "WhatsApp Opt-Out",
|
|
246
|
+
category: "status",
|
|
247
|
+
valueType: "boolean",
|
|
248
|
+
description: "User opted out of WhatsApp messages",
|
|
249
|
+
operators: booleanOperators
|
|
250
|
+
},
|
|
235
251
|
{
|
|
236
252
|
key: "has_opened_welcome_case",
|
|
237
253
|
label: "Has Opened Welcome Case",
|
package/dist/admin/index.js
CHANGED
|
@@ -35,7 +35,7 @@ const index = {
|
|
|
35
35
|
components: {
|
|
36
36
|
Input: async () => Promise.resolve().then(() => require(
|
|
37
37
|
/* webpackChunkName: "crm-rules-builder" */
|
|
38
|
-
"../_chunks/index-
|
|
38
|
+
"../_chunks/index-DGSELI7S.js"
|
|
39
39
|
))
|
|
40
40
|
},
|
|
41
41
|
options: {
|
|
@@ -107,7 +107,7 @@ const index = {
|
|
|
107
107
|
components: {
|
|
108
108
|
Input: async () => Promise.resolve().then(() => require(
|
|
109
109
|
/* webpackChunkName: "crm-telegram-buttons" */
|
|
110
|
-
"../_chunks/index-
|
|
110
|
+
"../_chunks/index-hY_mczO-.js"
|
|
111
111
|
))
|
|
112
112
|
},
|
|
113
113
|
options: {
|
|
@@ -173,7 +173,7 @@ const index = {
|
|
|
173
173
|
Component: async () => {
|
|
174
174
|
const component = await Promise.resolve().then(() => require(
|
|
175
175
|
/* webpackChunkName: "crm-dashboard-page" */
|
|
176
|
-
"../_chunks/index-
|
|
176
|
+
"../_chunks/index-D6jz5MgX.js"
|
|
177
177
|
));
|
|
178
178
|
return component;
|
|
179
179
|
},
|
package/dist/admin/index.mjs
CHANGED
|
@@ -34,7 +34,7 @@ const index = {
|
|
|
34
34
|
components: {
|
|
35
35
|
Input: async () => import(
|
|
36
36
|
/* webpackChunkName: "crm-rules-builder" */
|
|
37
|
-
"../_chunks/index-
|
|
37
|
+
"../_chunks/index-CFfWhleR.mjs"
|
|
38
38
|
)
|
|
39
39
|
},
|
|
40
40
|
options: {
|
|
@@ -106,7 +106,7 @@ const index = {
|
|
|
106
106
|
components: {
|
|
107
107
|
Input: async () => import(
|
|
108
108
|
/* webpackChunkName: "crm-telegram-buttons" */
|
|
109
|
-
"../_chunks/index-
|
|
109
|
+
"../_chunks/index-BXWSCH8D.mjs"
|
|
110
110
|
)
|
|
111
111
|
},
|
|
112
112
|
options: {
|
|
@@ -172,7 +172,7 @@ const index = {
|
|
|
172
172
|
Component: async () => {
|
|
173
173
|
const component = await import(
|
|
174
174
|
/* webpackChunkName: "crm-dashboard-page" */
|
|
175
|
-
"../_chunks/index-
|
|
175
|
+
"../_chunks/index-B_cJDVsP.mjs"
|
|
176
176
|
);
|
|
177
177
|
return component;
|
|
178
178
|
},
|
package/dist/server/index.js
CHANGED
|
@@ -61,6 +61,53 @@ const controller = ({ strapi }) => ({
|
|
|
61
61
|
},
|
|
62
62
|
async getAntiSpamLogs(ctx) {
|
|
63
63
|
ctx.body = await strapi.plugin("crm-dashboard").service("service").getAntiSpamLogs(ctx.query);
|
|
64
|
+
},
|
|
65
|
+
async listManualPushTemplates(ctx) {
|
|
66
|
+
ctx.body = await strapi.plugin("crm-dashboard").service("service").listManualPushTemplates();
|
|
67
|
+
},
|
|
68
|
+
async dispatchManualPush(ctx) {
|
|
69
|
+
try {
|
|
70
|
+
ctx.body = await strapi.plugin("crm-dashboard").service("service").dispatchManualPush(ctx.request.body);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
const status = err?.response?.status || 500;
|
|
73
|
+
ctx.status = status;
|
|
74
|
+
ctx.body = {
|
|
75
|
+
error: "Dispatch failed",
|
|
76
|
+
status,
|
|
77
|
+
backend: err?.response?.data ?? err?.message ?? null
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
async dispatchTestManualPush(ctx) {
|
|
82
|
+
try {
|
|
83
|
+
ctx.body = await strapi.plugin("crm-dashboard").service("service").dispatchTestManualPush(ctx.request.body);
|
|
84
|
+
} catch (err) {
|
|
85
|
+
const status = err?.response?.status || 500;
|
|
86
|
+
ctx.status = status;
|
|
87
|
+
ctx.body = {
|
|
88
|
+
error: "Dispatch test failed",
|
|
89
|
+
status,
|
|
90
|
+
backend: err?.response?.data ?? err?.message ?? null
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
async getManualPushHistory(ctx) {
|
|
95
|
+
try {
|
|
96
|
+
ctx.body = await strapi.plugin("crm-dashboard").service("service").getManualPushHistory(ctx.query);
|
|
97
|
+
} catch (err) {
|
|
98
|
+
const status = err?.response?.status || 500;
|
|
99
|
+
ctx.status = status;
|
|
100
|
+
ctx.body = { error: "Failed to load history", status };
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
async getManualPushStats(ctx) {
|
|
104
|
+
try {
|
|
105
|
+
ctx.body = await strapi.plugin("crm-dashboard").service("service").getManualPushStats(ctx.params.manualPushId);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
const status = err?.response?.status || 500;
|
|
108
|
+
ctx.status = status;
|
|
109
|
+
ctx.body = { error: "Failed to load stats", status };
|
|
110
|
+
}
|
|
64
111
|
}
|
|
65
112
|
});
|
|
66
113
|
const controllers = {
|
|
@@ -108,6 +155,61 @@ const adminRoutes = [
|
|
|
108
155
|
scope: ["plugin::crm-dashboard.access"]
|
|
109
156
|
}
|
|
110
157
|
}
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
method: "GET",
|
|
161
|
+
path: "/manual-pushes/templates",
|
|
162
|
+
handler: "controller.listManualPushTemplates",
|
|
163
|
+
config: {
|
|
164
|
+
policies: [],
|
|
165
|
+
auth: {
|
|
166
|
+
scope: ["plugin::crm-dashboard.access"]
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
method: "POST",
|
|
172
|
+
path: "/manual-pushes/dispatch",
|
|
173
|
+
handler: "controller.dispatchManualPush",
|
|
174
|
+
config: {
|
|
175
|
+
policies: [],
|
|
176
|
+
auth: {
|
|
177
|
+
scope: ["plugin::crm-dashboard.access"]
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
method: "POST",
|
|
183
|
+
path: "/manual-pushes/dispatch-test",
|
|
184
|
+
handler: "controller.dispatchTestManualPush",
|
|
185
|
+
config: {
|
|
186
|
+
policies: [],
|
|
187
|
+
auth: {
|
|
188
|
+
scope: ["plugin::crm-dashboard.access"]
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
method: "GET",
|
|
194
|
+
path: "/manual-pushes/history",
|
|
195
|
+
handler: "controller.getManualPushHistory",
|
|
196
|
+
config: {
|
|
197
|
+
policies: [],
|
|
198
|
+
auth: {
|
|
199
|
+
scope: ["plugin::crm-dashboard.access"]
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
method: "GET",
|
|
205
|
+
path: "/manual-pushes/:manualPushId/stats",
|
|
206
|
+
handler: "controller.getManualPushStats",
|
|
207
|
+
config: {
|
|
208
|
+
policies: [],
|
|
209
|
+
auth: {
|
|
210
|
+
scope: ["plugin::crm-dashboard.access"]
|
|
211
|
+
}
|
|
212
|
+
}
|
|
111
213
|
}
|
|
112
214
|
];
|
|
113
215
|
const routes = {
|
|
@@ -2490,6 +2592,19 @@ const {
|
|
|
2490
2592
|
mergeConfig
|
|
2491
2593
|
} = axios;
|
|
2492
2594
|
const API_URL = process.env.API_URL || "http://localhost:3100";
|
|
2595
|
+
const STATISTICS_SECRET_KEY = process.env.STATISTICS_SECRET_KEY || "";
|
|
2596
|
+
const authHeaders = () => STATISTICS_SECRET_KEY ? { Authorization: `Bearer ${STATISTICS_SECRET_KEY}` } : {};
|
|
2597
|
+
const formatAxiosError = (err) => {
|
|
2598
|
+
const e = err;
|
|
2599
|
+
const status = e?.response?.status;
|
|
2600
|
+
const data = e?.response?.data;
|
|
2601
|
+
const text = data && typeof data === "object" ? JSON.stringify(data) : String(data ?? "");
|
|
2602
|
+
return [
|
|
2603
|
+
status ? `status=${status}` : "",
|
|
2604
|
+
text ? `body=${text}` : "",
|
|
2605
|
+
e?.message ? `msg=${e.message}` : ""
|
|
2606
|
+
].filter(Boolean).join(" | ");
|
|
2607
|
+
};
|
|
2493
2608
|
const service = ({ strapi }) => ({
|
|
2494
2609
|
async getLogs(query) {
|
|
2495
2610
|
try {
|
|
@@ -2497,7 +2612,10 @@ const service = ({ strapi }) => ({
|
|
|
2497
2612
|
return data;
|
|
2498
2613
|
} catch (err) {
|
|
2499
2614
|
strapi.log.error("Failed to fetch CRM logs from backend", err);
|
|
2500
|
-
return {
|
|
2615
|
+
return {
|
|
2616
|
+
error: "Failed to fetch logs",
|
|
2617
|
+
details: err instanceof Error ? err.message : String(err)
|
|
2618
|
+
};
|
|
2501
2619
|
}
|
|
2502
2620
|
},
|
|
2503
2621
|
async getAntiSpamLogs(query) {
|
|
@@ -2506,7 +2624,109 @@ const service = ({ strapi }) => ({
|
|
|
2506
2624
|
return data;
|
|
2507
2625
|
} catch (err) {
|
|
2508
2626
|
strapi.log.error("Failed to fetch Anti-spam logs from backend", err);
|
|
2509
|
-
return {
|
|
2627
|
+
return {
|
|
2628
|
+
error: "Failed to fetch logs",
|
|
2629
|
+
details: err instanceof Error ? err.message : String(err)
|
|
2630
|
+
};
|
|
2631
|
+
}
|
|
2632
|
+
},
|
|
2633
|
+
async listManualPushTemplates() {
|
|
2634
|
+
try {
|
|
2635
|
+
const entries = await strapi.documents("api::manual-push.manual-push").findMany({
|
|
2636
|
+
status: "published",
|
|
2637
|
+
locale: "ru",
|
|
2638
|
+
populate: { image: { fields: ["url", "alternativeText", "width", "height"] } },
|
|
2639
|
+
sort: { updatedAt: "desc" },
|
|
2640
|
+
pagination: { pageSize: 200 }
|
|
2641
|
+
});
|
|
2642
|
+
const enriched = await Promise.all(
|
|
2643
|
+
(entries ?? []).map(async (e) => {
|
|
2644
|
+
let enEntry = null;
|
|
2645
|
+
try {
|
|
2646
|
+
enEntry = await strapi.documents("api::manual-push.manual-push").findOne({
|
|
2647
|
+
documentId: e.documentId,
|
|
2648
|
+
status: "published",
|
|
2649
|
+
locale: "en"
|
|
2650
|
+
});
|
|
2651
|
+
} catch {
|
|
2652
|
+
enEntry = null;
|
|
2653
|
+
}
|
|
2654
|
+
const locales = ["ru"];
|
|
2655
|
+
if (enEntry) locales.push("en");
|
|
2656
|
+
return {
|
|
2657
|
+
documentId: e.documentId,
|
|
2658
|
+
name: e.name,
|
|
2659
|
+
body: e.body,
|
|
2660
|
+
bodyEn: enEntry?.body ?? null,
|
|
2661
|
+
image: e.image ? {
|
|
2662
|
+
url: e.image.url,
|
|
2663
|
+
alternativeText: e.image.alternativeText,
|
|
2664
|
+
width: e.image.width,
|
|
2665
|
+
height: e.image.height
|
|
2666
|
+
} : null,
|
|
2667
|
+
buttonLabel: e.buttonLabel ?? null,
|
|
2668
|
+
buttonUrl: e.buttonUrl ?? null,
|
|
2669
|
+
buttonLabelEn: enEntry?.buttonLabel ?? null,
|
|
2670
|
+
buttonUrlEn: enEntry?.buttonUrl ?? null,
|
|
2671
|
+
testUserIds: Array.isArray(e.testUserIds) ? e.testUserIds : [],
|
|
2672
|
+
locales,
|
|
2673
|
+
updatedAt: e.updatedAt,
|
|
2674
|
+
createdAt: e.createdAt
|
|
2675
|
+
};
|
|
2676
|
+
})
|
|
2677
|
+
);
|
|
2678
|
+
return { data: enriched };
|
|
2679
|
+
} catch (err) {
|
|
2680
|
+
strapi.log.error("Failed to list manual-push templates", err);
|
|
2681
|
+
return {
|
|
2682
|
+
error: "Failed to list templates",
|
|
2683
|
+
details: err instanceof Error ? err.message : String(err)
|
|
2684
|
+
};
|
|
2685
|
+
}
|
|
2686
|
+
},
|
|
2687
|
+
async dispatchManualPush(body) {
|
|
2688
|
+
try {
|
|
2689
|
+
const { data } = await axios.post(`${API_URL}/crm/admin/manual-pushes/dispatch`, body, {
|
|
2690
|
+
headers: authHeaders()
|
|
2691
|
+
});
|
|
2692
|
+
return data;
|
|
2693
|
+
} catch (err) {
|
|
2694
|
+
strapi.log.error(`Failed to dispatch manual push: ${formatAxiosError(err)}`);
|
|
2695
|
+
throw err;
|
|
2696
|
+
}
|
|
2697
|
+
},
|
|
2698
|
+
async dispatchTestManualPush(body) {
|
|
2699
|
+
try {
|
|
2700
|
+
const { data } = await axios.post(`${API_URL}/crm/admin/manual-pushes/dispatch-test`, body, {
|
|
2701
|
+
headers: authHeaders()
|
|
2702
|
+
});
|
|
2703
|
+
return data;
|
|
2704
|
+
} catch (err) {
|
|
2705
|
+
strapi.log.error(`Failed to dispatch test manual push: ${formatAxiosError(err)}`);
|
|
2706
|
+
throw err;
|
|
2707
|
+
}
|
|
2708
|
+
},
|
|
2709
|
+
async getManualPushHistory(query) {
|
|
2710
|
+
try {
|
|
2711
|
+
const { data } = await axios.get(`${API_URL}/crm/admin/manual-pushes`, {
|
|
2712
|
+
params: query,
|
|
2713
|
+
headers: authHeaders()
|
|
2714
|
+
});
|
|
2715
|
+
return data;
|
|
2716
|
+
} catch (err) {
|
|
2717
|
+
strapi.log.error(`Failed to fetch manual push history: ${formatAxiosError(err)}`);
|
|
2718
|
+
throw err;
|
|
2719
|
+
}
|
|
2720
|
+
},
|
|
2721
|
+
async getManualPushStats(manualPushId) {
|
|
2722
|
+
try {
|
|
2723
|
+
const { data } = await axios.get(`${API_URL}/crm/admin/manual-pushes/${manualPushId}/stats`, {
|
|
2724
|
+
headers: authHeaders()
|
|
2725
|
+
});
|
|
2726
|
+
return data;
|
|
2727
|
+
} catch (err) {
|
|
2728
|
+
strapi.log.error(`Failed to fetch manual push stats: ${formatAxiosError(err)}`);
|
|
2729
|
+
throw err;
|
|
2510
2730
|
}
|
|
2511
2731
|
}
|
|
2512
2732
|
});
|
package/dist/server/index.mjs
CHANGED
|
@@ -60,6 +60,53 @@ const controller = ({ strapi }) => ({
|
|
|
60
60
|
},
|
|
61
61
|
async getAntiSpamLogs(ctx) {
|
|
62
62
|
ctx.body = await strapi.plugin("crm-dashboard").service("service").getAntiSpamLogs(ctx.query);
|
|
63
|
+
},
|
|
64
|
+
async listManualPushTemplates(ctx) {
|
|
65
|
+
ctx.body = await strapi.plugin("crm-dashboard").service("service").listManualPushTemplates();
|
|
66
|
+
},
|
|
67
|
+
async dispatchManualPush(ctx) {
|
|
68
|
+
try {
|
|
69
|
+
ctx.body = await strapi.plugin("crm-dashboard").service("service").dispatchManualPush(ctx.request.body);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
const status = err?.response?.status || 500;
|
|
72
|
+
ctx.status = status;
|
|
73
|
+
ctx.body = {
|
|
74
|
+
error: "Dispatch failed",
|
|
75
|
+
status,
|
|
76
|
+
backend: err?.response?.data ?? err?.message ?? null
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
async dispatchTestManualPush(ctx) {
|
|
81
|
+
try {
|
|
82
|
+
ctx.body = await strapi.plugin("crm-dashboard").service("service").dispatchTestManualPush(ctx.request.body);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
const status = err?.response?.status || 500;
|
|
85
|
+
ctx.status = status;
|
|
86
|
+
ctx.body = {
|
|
87
|
+
error: "Dispatch test failed",
|
|
88
|
+
status,
|
|
89
|
+
backend: err?.response?.data ?? err?.message ?? null
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
async getManualPushHistory(ctx) {
|
|
94
|
+
try {
|
|
95
|
+
ctx.body = await strapi.plugin("crm-dashboard").service("service").getManualPushHistory(ctx.query);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
const status = err?.response?.status || 500;
|
|
98
|
+
ctx.status = status;
|
|
99
|
+
ctx.body = { error: "Failed to load history", status };
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
async getManualPushStats(ctx) {
|
|
103
|
+
try {
|
|
104
|
+
ctx.body = await strapi.plugin("crm-dashboard").service("service").getManualPushStats(ctx.params.manualPushId);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
const status = err?.response?.status || 500;
|
|
107
|
+
ctx.status = status;
|
|
108
|
+
ctx.body = { error: "Failed to load stats", status };
|
|
109
|
+
}
|
|
63
110
|
}
|
|
64
111
|
});
|
|
65
112
|
const controllers = {
|
|
@@ -107,6 +154,61 @@ const adminRoutes = [
|
|
|
107
154
|
scope: ["plugin::crm-dashboard.access"]
|
|
108
155
|
}
|
|
109
156
|
}
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
method: "GET",
|
|
160
|
+
path: "/manual-pushes/templates",
|
|
161
|
+
handler: "controller.listManualPushTemplates",
|
|
162
|
+
config: {
|
|
163
|
+
policies: [],
|
|
164
|
+
auth: {
|
|
165
|
+
scope: ["plugin::crm-dashboard.access"]
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
method: "POST",
|
|
171
|
+
path: "/manual-pushes/dispatch",
|
|
172
|
+
handler: "controller.dispatchManualPush",
|
|
173
|
+
config: {
|
|
174
|
+
policies: [],
|
|
175
|
+
auth: {
|
|
176
|
+
scope: ["plugin::crm-dashboard.access"]
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
method: "POST",
|
|
182
|
+
path: "/manual-pushes/dispatch-test",
|
|
183
|
+
handler: "controller.dispatchTestManualPush",
|
|
184
|
+
config: {
|
|
185
|
+
policies: [],
|
|
186
|
+
auth: {
|
|
187
|
+
scope: ["plugin::crm-dashboard.access"]
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
method: "GET",
|
|
193
|
+
path: "/manual-pushes/history",
|
|
194
|
+
handler: "controller.getManualPushHistory",
|
|
195
|
+
config: {
|
|
196
|
+
policies: [],
|
|
197
|
+
auth: {
|
|
198
|
+
scope: ["plugin::crm-dashboard.access"]
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
method: "GET",
|
|
204
|
+
path: "/manual-pushes/:manualPushId/stats",
|
|
205
|
+
handler: "controller.getManualPushStats",
|
|
206
|
+
config: {
|
|
207
|
+
policies: [],
|
|
208
|
+
auth: {
|
|
209
|
+
scope: ["plugin::crm-dashboard.access"]
|
|
210
|
+
}
|
|
211
|
+
}
|
|
110
212
|
}
|
|
111
213
|
];
|
|
112
214
|
const routes = {
|
|
@@ -2489,6 +2591,19 @@ const {
|
|
|
2489
2591
|
mergeConfig
|
|
2490
2592
|
} = axios;
|
|
2491
2593
|
const API_URL = process.env.API_URL || "http://localhost:3100";
|
|
2594
|
+
const STATISTICS_SECRET_KEY = process.env.STATISTICS_SECRET_KEY || "";
|
|
2595
|
+
const authHeaders = () => STATISTICS_SECRET_KEY ? { Authorization: `Bearer ${STATISTICS_SECRET_KEY}` } : {};
|
|
2596
|
+
const formatAxiosError = (err) => {
|
|
2597
|
+
const e = err;
|
|
2598
|
+
const status = e?.response?.status;
|
|
2599
|
+
const data = e?.response?.data;
|
|
2600
|
+
const text = data && typeof data === "object" ? JSON.stringify(data) : String(data ?? "");
|
|
2601
|
+
return [
|
|
2602
|
+
status ? `status=${status}` : "",
|
|
2603
|
+
text ? `body=${text}` : "",
|
|
2604
|
+
e?.message ? `msg=${e.message}` : ""
|
|
2605
|
+
].filter(Boolean).join(" | ");
|
|
2606
|
+
};
|
|
2492
2607
|
const service = ({ strapi }) => ({
|
|
2493
2608
|
async getLogs(query) {
|
|
2494
2609
|
try {
|
|
@@ -2496,7 +2611,10 @@ const service = ({ strapi }) => ({
|
|
|
2496
2611
|
return data;
|
|
2497
2612
|
} catch (err) {
|
|
2498
2613
|
strapi.log.error("Failed to fetch CRM logs from backend", err);
|
|
2499
|
-
return {
|
|
2614
|
+
return {
|
|
2615
|
+
error: "Failed to fetch logs",
|
|
2616
|
+
details: err instanceof Error ? err.message : String(err)
|
|
2617
|
+
};
|
|
2500
2618
|
}
|
|
2501
2619
|
},
|
|
2502
2620
|
async getAntiSpamLogs(query) {
|
|
@@ -2505,7 +2623,109 @@ const service = ({ strapi }) => ({
|
|
|
2505
2623
|
return data;
|
|
2506
2624
|
} catch (err) {
|
|
2507
2625
|
strapi.log.error("Failed to fetch Anti-spam logs from backend", err);
|
|
2508
|
-
return {
|
|
2626
|
+
return {
|
|
2627
|
+
error: "Failed to fetch logs",
|
|
2628
|
+
details: err instanceof Error ? err.message : String(err)
|
|
2629
|
+
};
|
|
2630
|
+
}
|
|
2631
|
+
},
|
|
2632
|
+
async listManualPushTemplates() {
|
|
2633
|
+
try {
|
|
2634
|
+
const entries = await strapi.documents("api::manual-push.manual-push").findMany({
|
|
2635
|
+
status: "published",
|
|
2636
|
+
locale: "ru",
|
|
2637
|
+
populate: { image: { fields: ["url", "alternativeText", "width", "height"] } },
|
|
2638
|
+
sort: { updatedAt: "desc" },
|
|
2639
|
+
pagination: { pageSize: 200 }
|
|
2640
|
+
});
|
|
2641
|
+
const enriched = await Promise.all(
|
|
2642
|
+
(entries ?? []).map(async (e) => {
|
|
2643
|
+
let enEntry = null;
|
|
2644
|
+
try {
|
|
2645
|
+
enEntry = await strapi.documents("api::manual-push.manual-push").findOne({
|
|
2646
|
+
documentId: e.documentId,
|
|
2647
|
+
status: "published",
|
|
2648
|
+
locale: "en"
|
|
2649
|
+
});
|
|
2650
|
+
} catch {
|
|
2651
|
+
enEntry = null;
|
|
2652
|
+
}
|
|
2653
|
+
const locales = ["ru"];
|
|
2654
|
+
if (enEntry) locales.push("en");
|
|
2655
|
+
return {
|
|
2656
|
+
documentId: e.documentId,
|
|
2657
|
+
name: e.name,
|
|
2658
|
+
body: e.body,
|
|
2659
|
+
bodyEn: enEntry?.body ?? null,
|
|
2660
|
+
image: e.image ? {
|
|
2661
|
+
url: e.image.url,
|
|
2662
|
+
alternativeText: e.image.alternativeText,
|
|
2663
|
+
width: e.image.width,
|
|
2664
|
+
height: e.image.height
|
|
2665
|
+
} : null,
|
|
2666
|
+
buttonLabel: e.buttonLabel ?? null,
|
|
2667
|
+
buttonUrl: e.buttonUrl ?? null,
|
|
2668
|
+
buttonLabelEn: enEntry?.buttonLabel ?? null,
|
|
2669
|
+
buttonUrlEn: enEntry?.buttonUrl ?? null,
|
|
2670
|
+
testUserIds: Array.isArray(e.testUserIds) ? e.testUserIds : [],
|
|
2671
|
+
locales,
|
|
2672
|
+
updatedAt: e.updatedAt,
|
|
2673
|
+
createdAt: e.createdAt
|
|
2674
|
+
};
|
|
2675
|
+
})
|
|
2676
|
+
);
|
|
2677
|
+
return { data: enriched };
|
|
2678
|
+
} catch (err) {
|
|
2679
|
+
strapi.log.error("Failed to list manual-push templates", err);
|
|
2680
|
+
return {
|
|
2681
|
+
error: "Failed to list templates",
|
|
2682
|
+
details: err instanceof Error ? err.message : String(err)
|
|
2683
|
+
};
|
|
2684
|
+
}
|
|
2685
|
+
},
|
|
2686
|
+
async dispatchManualPush(body) {
|
|
2687
|
+
try {
|
|
2688
|
+
const { data } = await axios.post(`${API_URL}/crm/admin/manual-pushes/dispatch`, body, {
|
|
2689
|
+
headers: authHeaders()
|
|
2690
|
+
});
|
|
2691
|
+
return data;
|
|
2692
|
+
} catch (err) {
|
|
2693
|
+
strapi.log.error(`Failed to dispatch manual push: ${formatAxiosError(err)}`);
|
|
2694
|
+
throw err;
|
|
2695
|
+
}
|
|
2696
|
+
},
|
|
2697
|
+
async dispatchTestManualPush(body) {
|
|
2698
|
+
try {
|
|
2699
|
+
const { data } = await axios.post(`${API_URL}/crm/admin/manual-pushes/dispatch-test`, body, {
|
|
2700
|
+
headers: authHeaders()
|
|
2701
|
+
});
|
|
2702
|
+
return data;
|
|
2703
|
+
} catch (err) {
|
|
2704
|
+
strapi.log.error(`Failed to dispatch test manual push: ${formatAxiosError(err)}`);
|
|
2705
|
+
throw err;
|
|
2706
|
+
}
|
|
2707
|
+
},
|
|
2708
|
+
async getManualPushHistory(query) {
|
|
2709
|
+
try {
|
|
2710
|
+
const { data } = await axios.get(`${API_URL}/crm/admin/manual-pushes`, {
|
|
2711
|
+
params: query,
|
|
2712
|
+
headers: authHeaders()
|
|
2713
|
+
});
|
|
2714
|
+
return data;
|
|
2715
|
+
} catch (err) {
|
|
2716
|
+
strapi.log.error(`Failed to fetch manual push history: ${formatAxiosError(err)}`);
|
|
2717
|
+
throw err;
|
|
2718
|
+
}
|
|
2719
|
+
},
|
|
2720
|
+
async getManualPushStats(manualPushId) {
|
|
2721
|
+
try {
|
|
2722
|
+
const { data } = await axios.get(`${API_URL}/crm/admin/manual-pushes/${manualPushId}/stats`, {
|
|
2723
|
+
headers: authHeaders()
|
|
2724
|
+
});
|
|
2725
|
+
return data;
|
|
2726
|
+
} catch (err) {
|
|
2727
|
+
strapi.log.error(`Failed to fetch manual push stats: ${formatAxiosError(err)}`);
|
|
2728
|
+
throw err;
|
|
2509
2729
|
}
|
|
2510
2730
|
}
|
|
2511
2731
|
});
|
package/dist/style.css
CHANGED
|
@@ -328,6 +328,7 @@
|
|
|
328
328
|
--tab-icon-stats-gradient: linear-gradient(135deg, #e0e7ff 0%, #c7d2fe 100%);
|
|
329
329
|
--tab-icon-logs-gradient: linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%);
|
|
330
330
|
--tab-icon-antispam-gradient: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
|
|
331
|
+
--tab-icon-manual-pushes-gradient: linear-gradient(135deg, #e0e7ff 0%, #b4b1ff 100%);
|
|
331
332
|
|
|
332
333
|
min-height: 100vh;
|
|
333
334
|
background: var(--crm-bg-secondary);
|
|
@@ -361,6 +362,7 @@
|
|
|
361
362
|
--tab-icon-stats-gradient: linear-gradient(135deg, rgba(123, 121, 255, 0.2) 0%, rgba(99, 102, 241, 0.3) 100%);
|
|
362
363
|
--tab-icon-logs-gradient: linear-gradient(135deg, rgba(16, 185, 129, 0.2) 0%, rgba(52, 211, 153, 0.3) 100%);
|
|
363
364
|
--tab-icon-antispam-gradient: linear-gradient(135deg, rgba(245, 158, 11, 0.2) 0%, rgba(251, 191, 36, 0.3) 100%);
|
|
365
|
+
--tab-icon-manual-pushes-gradient: linear-gradient(135deg, rgba(123, 121, 255, 0.2) 0%, rgba(165, 163, 255, 0.3) 100%);
|
|
364
366
|
}
|
|
365
367
|
|
|
366
368
|
.dashboard-header {
|
|
@@ -462,6 +464,11 @@
|
|
|
462
464
|
color: var(--crm-warning);
|
|
463
465
|
}
|
|
464
466
|
|
|
467
|
+
.tab-icon.manual-pushes {
|
|
468
|
+
background: var(--tab-icon-manual-pushes-gradient);
|
|
469
|
+
color: var(--crm-accent);
|
|
470
|
+
}
|
|
471
|
+
|
|
465
472
|
.tab-content-wrapper {
|
|
466
473
|
display: flex;
|
|
467
474
|
flex-direction: column;
|
|
@@ -515,6 +522,11 @@
|
|
|
515
522
|
color: var(--crm-warning);
|
|
516
523
|
}
|
|
517
524
|
|
|
525
|
+
.tab-badge.manual-pushes {
|
|
526
|
+
background: var(--crm-accent-light);
|
|
527
|
+
color: var(--crm-accent);
|
|
528
|
+
}
|
|
529
|
+
|
|
518
530
|
.tab-card.active .tab-badge.stats {
|
|
519
531
|
background: var(--crm-accent);
|
|
520
532
|
color: white;
|
|
@@ -530,6 +542,11 @@
|
|
|
530
542
|
color: white;
|
|
531
543
|
}
|
|
532
544
|
|
|
545
|
+
.tab-card.active .tab-badge.manual-pushes {
|
|
546
|
+
background: var(--crm-accent);
|
|
547
|
+
color: white;
|
|
548
|
+
}
|
|
549
|
+
|
|
533
550
|
.tab-content {
|
|
534
551
|
margin: 0 24px;
|
|
535
552
|
padding: 24px;
|