Add budget spending chart command
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,6 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
config.json
|
config.json
|
||||||
|
config.test.json
|
||||||
old/
|
old/
|
||||||
bot.log
|
bot.log
|
||||||
data/
|
data/
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
|
const { SlashCommandBuilder, EmbedBuilder, AttachmentBuilder } = require('discord.js');
|
||||||
|
const axios = require('axios').default;
|
||||||
const { HOUSEHOLD_IDS } = require('../../core/constants.js');
|
const { HOUSEHOLD_IDS } = require('../../core/constants.js');
|
||||||
|
|
||||||
|
const CHART_COLORS = {
|
||||||
|
[HOUSEHOLD_IDS.MINZ]: '#4e79a7',
|
||||||
|
[HOUSEHOLD_IDS.MIFFY]: '#f28e2b',
|
||||||
|
};
|
||||||
|
const DISPLAY_NAMES = {
|
||||||
|
[HOUSEHOLD_IDS.MINZ]: 'Minz',
|
||||||
|
[HOUSEHOLD_IDS.MIFFY]: 'Miffy',
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
category: 'utility',
|
category: 'utility',
|
||||||
global: true,
|
global: true,
|
||||||
@@ -43,6 +53,15 @@ module.exports = {
|
|||||||
option.setName('user')
|
option.setName('user')
|
||||||
.setRequired(false)
|
.setRequired(false)
|
||||||
.setDescription('User to view stats for'))
|
.setDescription('User to view stats for'))
|
||||||
|
)
|
||||||
|
.addSubcommand(subcommand =>
|
||||||
|
subcommand
|
||||||
|
.setName('chart')
|
||||||
|
.setDescription('Render a bar chart comparing Minz and Miffy spendings over time')
|
||||||
|
.addIntegerOption(option =>
|
||||||
|
option.setName('weeks')
|
||||||
|
.setRequired(false)
|
||||||
|
.setDescription('Number of recent weeks to include (default 8)'))
|
||||||
),
|
),
|
||||||
async execute(interaction) {
|
async execute(interaction) {
|
||||||
let db = await interaction.client.localDB;
|
let db = await interaction.client.localDB;
|
||||||
@@ -159,6 +178,70 @@ module.exports = {
|
|||||||
|
|
||||||
await interaction.reply({ embeds: [statsEmbed] });
|
await interaction.reply({ embeds: [statsEmbed] });
|
||||||
break;
|
break;
|
||||||
|
case 'chart': {
|
||||||
|
const weeks = Math.min(Math.max(interaction.options.getInteger('weeks') ?? 8, 1), 52);
|
||||||
|
|
||||||
|
const chartPeriods = db.prepare(`
|
||||||
|
SELECT id, start_date, end_date
|
||||||
|
FROM weekly_budgets
|
||||||
|
ORDER BY start_date DESC
|
||||||
|
LIMIT ?
|
||||||
|
`).all(weeks).reverse();
|
||||||
|
|
||||||
|
if (chartPeriods.length === 0) {
|
||||||
|
await interaction.reply("No budget periods on record.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await interaction.deferReply();
|
||||||
|
|
||||||
|
const spentByUserAndPeriod = db.prepare(`
|
||||||
|
SELECT COALESCE(SUM(amount), 0) as total
|
||||||
|
FROM grocery_spendings
|
||||||
|
WHERE budget_id = ? AND discord_id = ?
|
||||||
|
`);
|
||||||
|
|
||||||
|
const labels = chartPeriods.map(period => period.start_date.slice(5));
|
||||||
|
const datasets = [HOUSEHOLD_IDS.MINZ, HOUSEHOLD_IDS.MIFFY].map(userId => ({
|
||||||
|
label: DISPLAY_NAMES[userId],
|
||||||
|
backgroundColor: CHART_COLORS[userId],
|
||||||
|
data: chartPeriods.map(period => spentByUserAndPeriod.get(period.id, userId).total),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const chartConfig = {
|
||||||
|
type: 'bar',
|
||||||
|
data: { labels, datasets },
|
||||||
|
options: {
|
||||||
|
title: { display: true, text: 'Weekly Grocery Spending (€)' },
|
||||||
|
scales: {
|
||||||
|
yAxes: [{ ticks: { beginAtZero: true }, scaleLabel: { display: true, labelString: '€' } }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post('https://quickchart.io/chart', {
|
||||||
|
chart: chartConfig,
|
||||||
|
width: 800,
|
||||||
|
height: 400,
|
||||||
|
backgroundColor: 'white',
|
||||||
|
}, { responseType: 'arraybuffer' });
|
||||||
|
|
||||||
|
const attachment = new AttachmentBuilder(Buffer.from(response.data), { name: 'budget-chart.png' });
|
||||||
|
const chartEmbed = new EmbedBuilder()
|
||||||
|
.setColor(0x0099ff)
|
||||||
|
.setTitle('Weekly Grocery Spending')
|
||||||
|
.setDescription(`Last ${chartPeriods.length} week(s): \`${chartPeriods[0].start_date}\` to \`${chartPeriods[chartPeriods.length - 1].end_date}\``)
|
||||||
|
.setImage('attachment://budget-chart.png')
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
await interaction.editReply({ embeds: [chartEmbed], files: [attachment] });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[budget chart] Failed to render chart:', error);
|
||||||
|
await interaction.editReply("Failed to render the chart, sorry!");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
await interaction.reply(`Sub mismatch. Switching on ${interaction.options.getSubcommand()}`);
|
await interaction.reply(`Sub mismatch. Switching on ${interaction.options.getSubcommand()}`);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const { REST, Routes } = require('discord.js');
|
const { REST, Routes } = require('discord.js');
|
||||||
const { clientId, guildId, token } = require('./config.json');
|
const { clientId, guildId, token } = require(process.env.CONFIG_FILE ? `./${process.env.CONFIG_FILE}` : './config.json');
|
||||||
const fs = require('node:fs');
|
const fs = require('node:fs');
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
|
|
||||||
|
|||||||
2
main.js
2
main.js
@@ -1,7 +1,7 @@
|
|||||||
const fs = require('node:fs');
|
const fs = require('node:fs');
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const { Client, Collection, Events, GatewayIntentBits, Options, Partials } = require('discord.js');
|
const { Client, Collection, Events, GatewayIntentBits, Options, Partials } = require('discord.js');
|
||||||
const { token } = require('./config.json');
|
const { token } = require(process.env.CONFIG_FILE ? `./${process.env.CONFIG_FILE}` : './config.json');
|
||||||
const createDbConnection = require('./core/db.js');
|
const createDbConnection = require('./core/db.js');
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user