Skip to content

Commit 59729eb

Browse files
deadlyjackAjit Kumargreptile-apps[bot]
authored
feat: deprecate Ace editor, add top-developers/provider-status analyt… (#61)
* feat: deprecate Ace editor, add top-developers/provider-status analytics charts - Set default editor to CodeMirror; disallow Ace for new plugins - Add Top Developers (INR) and Provider vs Status charts to admin dashboard - Add Razorpay revenue to monthly revenue analytics - Fix purchase_order.state type: TEXT → INTEGER NOT NULL - Add percentage labels to doughnut charts * Update server/apis/admin.js Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * fix: replace template-literal SQL constants with parameterized placeholders in analytics queries Convert all interpolated class constants in /api/admin/analytics queries to `?` placeholders with values array, matching the Entity.execSql parameterized query pattern. Also remove unused `rzpRaw` from response. --------- Co-authored-by: Ajit Kumar <dellevenjack@gmail> Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
1 parent 6f89a95 commit 59729eb

8 files changed

Lines changed: 262 additions & 49 deletions

File tree

.babelrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@
1212
}
1313
]
1414
],
15-
"compact": true
15+
"compact": false
1616
}

client/pages/admin/index.js

Lines changed: 166 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,8 @@ function Dashboard() {
161161
const paymentsCanvas = Ref();
162162
const paymentStatusCanvas = Ref();
163163
const editorCanvas = Ref();
164+
const topDevCanvas = Ref();
165+
const providerStatusCanvas = Ref();
164166

165167
ref.append(
166168
<div className='dashboard-grid'>
@@ -183,6 +185,18 @@ function Dashboard() {
183185
<canvas ref={paymentsCanvas} />
184186
</div>
185187
</div>
188+
<div className='chart-card'>
189+
<h3>Top Developers (INR)</h3>
190+
<div className='chart-container'>
191+
<canvas ref={topDevCanvas} />
192+
</div>
193+
</div>
194+
<div className='chart-card'>
195+
<h3>Orders: Provider vs Status</h3>
196+
<div className='chart-container'>
197+
<canvas ref={providerStatusCanvas} />
198+
</div>
199+
</div>
186200
<div className='chart-card'>
187201
<h3>Payment Status</h3>
188202
<div className='chart-container chart-container--small'>
@@ -200,6 +214,8 @@ function Dashboard() {
200214

201215
initChart(revenueCanvas, lineChartConfig(analytics.monthlyRevenue, 'Revenue (INR)', '#22c55e'));
202216
initChart(paymentsCanvas, lineChartConfig(analytics.monthlyPayments, 'Payments (INR)', '#3b82f6'));
217+
initChart(topDevCanvas, horizontalBarChartConfig(analytics.topDevelopers, 'name', 'total'));
218+
initChart(providerStatusCanvas, providerStatusChartConfig(analytics.providerStatus));
203219
initChart(paymentStatusCanvas, doughnutChartConfig(analytics.paymentStatus, 'status', 'count'));
204220
initChart(editorCanvas, doughnutChartConfig(analytics.editorDistribution, 'editor', 'count'));
205221
} catch {
@@ -235,8 +251,8 @@ function lineChartConfig(rows, label, color = '#3b82f6') {
235251
type: 'line',
236252
data: {
237253
labels: months.map((m) => {
238-
const [_, mo] = m.split('-');
239-
return monthNames[Number(mo) - 1].slice(0, 3);
254+
const [yr, mo] = m.split('-');
255+
return `${monthNames[Number(mo) - 1].slice(0, 3)} '${yr.slice(2)}`;
240256
}),
241257
datasets: [
242258
{
@@ -259,6 +275,7 @@ function lineChartConfig(rows, label, color = '#3b82f6') {
259275
function doughnutChartConfig(rows, labelKey, valueKey) {
260276
const labels = rows.map((r) => r[labelKey]);
261277
const values = rows.map((r) => Number(r[valueKey]) || 0);
278+
const total = values.reduce((a, b) => a + b, 0) || 1;
262279
const colors = ['#3b82f6', '#22c55e', '#f59e0b', '#ef4444', '#8b5cf6'];
263280

264281
return {
@@ -275,14 +292,160 @@ function doughnutChartConfig(rows, labelKey, valueKey) {
275292
],
276293
},
277294
options: {
278-
...chartBaseOptions(),
295+
responsive: true,
296+
maintainAspectRatio: false,
279297
plugins: {
280298
legend: {
281299
position: 'bottom',
282300
labels: { color: 'rgba(255,255,255,0.7)', padding: 16, font: { size: 12 } },
283301
},
284302
},
285303
},
304+
plugins: [
305+
{
306+
id: 'doughnutPercentLabels',
307+
afterDatasetsDraw(chart) {
308+
const { ctx, data: chartData } = chart;
309+
const dataset = chartData.datasets[0];
310+
const meta = chart.getDatasetMeta(0);
311+
ctx.save();
312+
ctx.font = 'bold 14px sans-serif';
313+
ctx.textAlign = 'center';
314+
ctx.textBaseline = 'middle';
315+
for (let i = 0; i < dataset.data.length; i++) {
316+
const value = dataset.data[i];
317+
if (value === 0) continue;
318+
const pct = Math.round((value / total) * 100);
319+
const arc = meta.data[i];
320+
const { x, y } = arc.tooltipPosition(true);
321+
ctx.fillStyle = '#ffffff';
322+
ctx.shadowColor = 'rgba(0,0,0,0.6)';
323+
ctx.shadowBlur = 3;
324+
ctx.fillText(`${pct}%`, x, y);
325+
ctx.shadowBlur = 0;
326+
}
327+
ctx.restore();
328+
},
329+
},
330+
],
331+
};
332+
}
333+
334+
function horizontalBarChartConfig(rows, labelKey, valueKey) {
335+
const names = rows.map((r) => r[labelKey] || 'Unknown');
336+
const values = rows.map((r) => Number(r[valueKey]) || 0);
337+
const colors = ['#22c55e', '#3b82f6', '#f59e0b', '#8b5cf6', '#ef4444', '#06b6d4', '#a855f7', '#ec4899', '#84cc16', '#f97316'];
338+
339+
return {
340+
type: 'bar',
341+
data: {
342+
labels: names,
343+
datasets: [
344+
{
345+
label: 'Earnings (INR)',
346+
data: values,
347+
backgroundColor: names.map((_, i) => colors[i % colors.length]),
348+
borderColor: 'rgba(0,0,0,0.2)',
349+
borderWidth: 1,
350+
borderRadius: 4,
351+
},
352+
],
353+
},
354+
options: {
355+
indexAxis: 'y',
356+
responsive: true,
357+
maintainAspectRatio: false,
358+
plugins: {
359+
legend: { display: false },
360+
},
361+
scales: {
362+
x: {
363+
ticks: { color: 'rgba(255,255,255,0.5)', font: { size: 11 } },
364+
grid: { color: 'rgba(255,255,255,0.06)' },
365+
beginAtZero: true,
366+
},
367+
y: {
368+
ticks: { color: 'rgba(255,255,255,0.7)', font: { size: 12 }, padding: 8 },
369+
grid: { display: false },
370+
},
371+
},
372+
},
373+
};
374+
}
375+
376+
function providerStatusChartConfig(rows) {
377+
const providerLabels = [...new Set(rows.map((r) => r.provider))];
378+
const statuses = [...new Set(rows.map((r) => r.status))];
379+
const colorMap = { Successful: '#22c55e', Failed: '#ef4444', Other: '#f59e0b' };
380+
const providerNameMap = { google_play: 'Google Play', razorpay: 'Razorpay' };
381+
382+
const dataMap = {};
383+
for (const row of rows) {
384+
if (!dataMap[row.status]) dataMap[row.status] = {};
385+
dataMap[row.status][row.provider] = (dataMap[row.status][row.provider] || 0) + Number(row.count);
386+
}
387+
388+
const datasets = statuses.map((status) => ({
389+
label: status,
390+
data: providerLabels.map((p) => dataMap[status]?.[p] || 0),
391+
backgroundColor: colorMap[status] || '#8b5cf6',
392+
borderColor: 'rgba(0,0,0,0.2)',
393+
borderWidth: 1,
394+
}));
395+
396+
return {
397+
type: 'bar',
398+
data: {
399+
labels: providerLabels.map((p) => providerNameMap[p] || p),
400+
datasets,
401+
},
402+
options: {
403+
responsive: true,
404+
maintainAspectRatio: false,
405+
plugins: {
406+
legend: {
407+
position: 'bottom',
408+
labels: { color: 'rgba(255,255,255,0.7)', padding: 16, font: { size: 12 } },
409+
},
410+
},
411+
scales: {
412+
x: {
413+
ticks: { color: 'rgba(255,255,255,0.5)', font: { size: 11 } },
414+
grid: { color: 'rgba(255,255,255,0.06)' },
415+
},
416+
y: {
417+
ticks: { color: 'rgba(255,255,255,0.5)', font: { size: 11 } },
418+
grid: { color: 'rgba(255,255,255,0.06)' },
419+
beginAtZero: true,
420+
},
421+
},
422+
},
423+
plugins: [
424+
{
425+
id: 'barValueLabels',
426+
afterDatasetsDraw(chart) {
427+
const { ctx } = chart;
428+
ctx.save();
429+
ctx.font = 'bold 11px sans-serif';
430+
ctx.textAlign = 'center';
431+
ctx.textBaseline = 'bottom';
432+
ctx.fillStyle = '#ffffff';
433+
ctx.shadowColor = 'rgba(0,0,0,0.5)';
434+
ctx.shadowBlur = 2;
435+
for (const ds of chart.data.datasets) {
436+
const meta = chart.getDatasetMeta(chart.data.datasets.indexOf(ds));
437+
for (let i = 0; i < ds.data.length; i++) {
438+
const value = ds.data[i];
439+
if (!value) continue;
440+
const { x, y } = meta.data[i];
441+
ctx.fillText(value, x, y - 2);
442+
}
443+
}
444+
ctx.shadowBlur = 0;
445+
ctx.restore();
446+
},
447+
},
448+
],
286449
};
287450
}
288451

client/pages/publishPlugin/index.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -116,15 +116,17 @@ export default async function PublishPlugin({ mode = 'publish', id }) {
116116
<th>Editor Type</th>
117117
<td>
118118
<select name='supported_editor' id='editor_type' value={plugin?.supported_editor || 'cm'}>
119-
<option value='ace' selected={plugin?.supported_editor === 'ace'}>
120-
Ace
121-
</option>
122-
<option value='cm' selected={plugin?.supported_editor === 'cm'}>
119+
<option value='cm' selected={plugin?.supported_editor === 'cm' || !plugin}>
123120
CodeMirror
124121
</option>
125122
<option value='all' selected={plugin?.supported_editor === 'all'}>
126123
Both
127124
</option>
125+
{plugin?.supported_editor === 'ace' && (
126+
<option value='ace' selected>
127+
Ace
128+
</option>
129+
)}
128130
</select>
129131
</td>
130132
</tr>

client/pages/updatePluginEditor/index.js

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,14 @@ export default async function UpdatePluginEditor({ id }) {
4848

4949
let currentEditorText = 'Unknown';
5050
switch (plugin.supported_editor) {
51-
case 'ace':
52-
currentEditorText = 'Ace Editor (Legacy)';
53-
break;
5451
case 'cm':
5552
currentEditorText = 'CodeMirror (Recommended)';
5653
break;
5754
case 'all':
58-
currentEditorText = 'Both Ace & CodeMirror';
55+
currentEditorText = 'Both CodeMirror & Ace';
56+
break;
57+
case 'ace':
58+
currentEditorText = 'Ace Editor (Legacy)';
5959
break;
6060
default:
6161
currentEditorText = 'Not Set';
@@ -85,10 +85,9 @@ export default async function UpdatePluginEditor({ id }) {
8585
<span>Important Information</span>
8686
</div>
8787
<p>
88-
<strong>Ace Editor will be deprecated soon.</strong> We strongly recommend updating your plugin to support CodeMirror for the best
89-
experience and future compatibility.
88+
<strong>Ace Editor is no longer accepted for new plugins.</strong> All new plugins must support CodeMirror.
9089
</p>
91-
<p>If you're creating a new plugin, please develop it with CodeMirror support from the start.</p>
90+
<p>Please update your plugin to support CodeMirror for the best experience and future compatibility.</p>
9291
</div>
9392

9493
<AjaxForm
@@ -119,23 +118,10 @@ export default async function UpdatePluginEditor({ id }) {
119118
<input type='radio' name='supported_editor' value='all' defaultChecked={plugin.supported_editor === 'all'} />
120119
<div className='option-content'>
121120
<div className='option-header'>
122-
<strong>Both Ace & CodeMirror</strong>
121+
<strong>Both CodeMirror &amp; Ace</strong>
123122
<span className='badge'>Universal</span>
124123
</div>
125-
<p className='option-description'>
126-
Support both editors for maximum compatibility. Choose this if your plugin needs to work with users still using Ace editor.
127-
</p>
128-
</div>
129-
</label>
130-
131-
<label className='radio-option deprecated'>
132-
<input type='radio' name='supported_editor' value='ace' defaultChecked={plugin.supported_editor === 'ace'} />
133-
<div className='option-content'>
134-
<div className='option-header'>
135-
<strong>Ace Editor Only</strong>
136-
<span className='badge deprecated'>Deprecated</span>
137-
</div>
138-
<p className='option-description'>Legacy editor support. Will be removed in future versions. Not recommended for continued use.</p>
124+
<p className='option-description'>Support both editors for maximum compatibility with existing Ace users.</p>
139125
</div>
140126
</label>
141127
</div>

0 commit comments

Comments
 (0)