feat: implement admin point management system with user selection and modal input

This commit is contained in:
2025-10-03 20:48:33 -05:00
parent 76d4f57e77
commit ebb16a55f3
6 changed files with 563 additions and 7 deletions

View File

@@ -3,6 +3,7 @@
import { CommandMessage } from "../../../core/types/commands";
import { prisma } from "../../../core/database/prisma";
import type { Message } from "discord.js";
import { PermissionFlagsBits } from "discord.js";
const MAX_ENTRIES = 10;
@@ -40,7 +41,7 @@ function codeBlock(lines: string[]): string {
].join('\n');
}
export async function buildLeaderboardPanel(message: Message) {
export async function buildLeaderboardPanel(message: Message, isAdmin: boolean = false) {
const guild = message.guild!;
const guildId = guild.id;
const userId = message.author.id;
@@ -99,6 +100,18 @@ export async function buildLeaderboardPanel(message: Message) {
const now = new Date();
const ts = now.toISOString().replace('T', ' ').split('.')[0];
// Botón base que todos ven
const buttons: any[] = [
{ type: 2, style: 2, emoji: '1420539242643193896', label: 'Refrescar', custom_id: 'ld_refresh' }
];
// Si es admin, añadir botón de gestión
if (isAdmin) {
buttons.push(
{ type: 2, style: 1, emoji: '⚙️', label: 'Gestionar Puntos', custom_id: 'ld_manage_points' }
);
}
// @ts-ignore - estructura de Display Components V2
const panel = {
type: 17,
@@ -126,9 +139,7 @@ export async function buildLeaderboardPanel(message: Message) {
{ type: 14, divider: false, spacing: 1 },
{
type: 1,
components: [
{ type: 2, style: 2, emoji: '1420539242643193896', label: 'Refrescar', custom_id: 'ld_refresh' }
]
components: buttons
}
]
};
@@ -150,7 +161,11 @@ export const command: CommandMessage = {
return;
}
const panel = await buildLeaderboardPanel(message);
// Verificar si el usuario es administrador
const member = await message.guild.members.fetch(message.author.id);
const isAdmin = member.permissions.has(PermissionFlagsBits.ManageGuild);
const panel = await buildLeaderboardPanel(message, isAdmin);
await message.reply({
// @ts-ignore Flag de componentes V2
flags: 32768,

View File

@@ -0,0 +1,90 @@
import logger from "../../core/lib/logger";
import {
ButtonInteraction,
MessageFlags,
PermissionFlagsBits,
StringSelectMenuBuilder,
ActionRowBuilder
} from 'discord.js';
import { prisma } from '../../core/database/prisma';
export default {
customId: 'ld_manage_points',
run: async (interaction: ButtonInteraction) => {
if (!interaction.guild) {
return interaction.reply({
content: '❌ Solo disponible en servidores.',
flags: MessageFlags.Ephemeral
});
}
// Verificar permisos de administrador
const member = await interaction.guild.members.fetch(interaction.user.id);
if (!member.permissions.has(PermissionFlagsBits.ManageGuild)) {
return interaction.reply({
content: '❌ Solo los administradores pueden gestionar puntos.',
flags: MessageFlags.Ephemeral
});
}
try {
// Obtener todos los usuarios con puntos en este servidor
const stats = await prisma.partnershipStats.findMany({
where: { guildId: interaction.guild.id },
orderBy: { totalPoints: 'desc' },
take: 25 // Discord limita a 25 opciones en select menus
});
if (stats.length === 0) {
return interaction.reply({
content: '❌ No hay usuarios con puntos en este servidor todavía.',
flags: MessageFlags.Ephemeral
});
}
// Construir opciones del select menu
const options = await Promise.all(
stats.map(async (stat) => {
let displayName = 'Usuario desconocido';
try {
const member = await interaction.guild!.members.fetch(stat.userId);
displayName = member.displayName || member.user.username;
} catch {
try {
const user = await interaction.client.users.fetch(stat.userId);
displayName = user.username;
} catch {
// Mantener el nombre por defecto
}
}
return {
label: displayName,
description: `Total: ${stat.totalPoints} | Semanal: ${stat.weeklyPoints} | Mensual: ${stat.monthlyPoints}`,
value: stat.userId
};
})
);
const selectMenu = new StringSelectMenuBuilder()
.setCustomId('ld_select_user')
.setPlaceholder('Selecciona un usuario para gestionar sus puntos')
.addOptions(options);
const row = new ActionRowBuilder<StringSelectMenuBuilder>()
.addComponents(selectMenu);
await interaction.reply({
content: '### ⚙️ Gestión de Puntos\nSelecciona el usuario al que deseas modificar los puntos:',
components: [row],
flags: MessageFlags.Ephemeral
});
} catch (e) {
logger.error({ err: e }, 'Error en ldManagePoints');
await interaction.reply({
content: '❌ Error al cargar la lista de usuarios.',
flags: MessageFlags.Ephemeral
});
}
}
};

View File

@@ -1,5 +1,5 @@
import logger from "../../core/lib/logger";
import { ButtonInteraction, MessageFlags } from 'discord.js';
import { ButtonInteraction, MessageFlags, PermissionFlagsBits } from 'discord.js';
import { buildLeaderboardPanel } from '../../commands/messages/alliaces/leaderboard';
export default {
@@ -10,9 +10,14 @@ export default {
}
try {
await interaction.deferUpdate();
// Verificar si el usuario es administrador
const member = await interaction.guild.members.fetch(interaction.user.id);
const isAdmin = member.permissions.has(PermissionFlagsBits.ManageGuild);
// Reusar el builder esperando un objeto con guild y author
const fakeMessage: any = { guild: interaction.guild, author: interaction.user };
const panel = await buildLeaderboardPanel(fakeMessage);
const panel = await buildLeaderboardPanel(fakeMessage, isAdmin);
await interaction.message.edit({ components: [panel] });
} catch (e) {
// @ts-ignore

View File

@@ -0,0 +1,167 @@
import logger from "../../core/lib/logger";
import {
ModalSubmitInteraction,
MessageFlags,
PermissionFlagsBits,
EmbedBuilder
} from 'discord.js';
import { prisma } from '../../core/database/prisma';
export default {
customId: 'ld_points_modal',
run: async (interaction: ModalSubmitInteraction) => {
if (!interaction.guild) {
return interaction.reply({
content: '❌ Solo disponible en servidores.',
flags: MessageFlags.Ephemeral
});
}
// Verificar permisos
const member = await interaction.guild.members.fetch(interaction.user.id);
if (!member.permissions.has(PermissionFlagsBits.ManageGuild)) {
return interaction.reply({
content: '❌ Solo los administradores pueden gestionar puntos.',
flags: MessageFlags.Ephemeral
});
}
try {
// Extraer el userId del customId (formato: ld_points_modal:userId)
const userId = interaction.customId.split(':')[1];
if (!userId) {
return interaction.reply({
content: '❌ Error al identificar el usuario.',
flags: MessageFlags.Ephemeral
});
}
// Obtener valores del modal
const totalInput = interaction.fields.getTextInputValue('total_points').trim();
const weeklyInput = interaction.fields.getTextInputValue('weekly_points').trim();
const monthlyInput = interaction.fields.getTextInputValue('monthly_points').trim();
// Si no se ingresó nada, retornar
if (!totalInput && !weeklyInput && !monthlyInput) {
return interaction.reply({
content: '❌ Debes ingresar al menos un valor para modificar.',
flags: MessageFlags.Ephemeral
});
}
// Obtener o crear el registro de stats del usuario
let stats = await prisma.partnershipStats.findUnique({
where: {
userId_guildId: {
userId,
guildId: interaction.guild.id
}
}
});
if (!stats) {
// Crear nuevo registro si no existe
stats = await prisma.partnershipStats.create({
data: {
userId,
guildId: interaction.guild.id,
totalPoints: 0,
weeklyPoints: 0,
monthlyPoints: 0
}
});
}
// Función para parsear el input y calcular el nuevo valor
const calculateNewValue = (input: string, currentValue: number): number => {
if (!input) return currentValue;
const firstChar = input[0];
const numValue = parseInt(input.substring(1)) || 0;
if (firstChar === '+') {
return Math.max(0, currentValue + numValue);
} else if (firstChar === '-') {
return Math.max(0, currentValue - numValue);
} else if (firstChar === '=') {
return Math.max(0, numValue);
} else {
// Si no tiene símbolo, tratar como valor absoluto
const parsedValue = parseInt(input);
return isNaN(parsedValue) ? currentValue : Math.max(0, parsedValue);
}
};
// Calcular nuevos valores
const newTotalPoints = calculateNewValue(totalInput, stats.totalPoints);
const newWeeklyPoints = calculateNewValue(weeklyInput, stats.weeklyPoints);
const newMonthlyPoints = calculateNewValue(monthlyInput, stats.monthlyPoints);
// Actualizar en base de datos
const updatedStats = await prisma.partnershipStats.update({
where: {
userId_guildId: {
userId,
guildId: interaction.guild.id
}
},
data: {
totalPoints: newTotalPoints,
weeklyPoints: newWeeklyPoints,
monthlyPoints: newMonthlyPoints
}
});
// Obtener nombre del usuario
let userName = 'Usuario';
try {
const targetMember = await interaction.guild.members.fetch(userId);
userName = targetMember.displayName || targetMember.user.username;
} catch {
try {
const user = await interaction.client.users.fetch(userId);
userName = user.username;
} catch {
userName = userId;
}
}
// Crear embed de confirmación
const embed = new EmbedBuilder()
.setColor(0x00ff00)
.setTitle('✅ Puntos Actualizados')
.setDescription(`Se han actualizado los puntos de **${userName}**`)
.addFields(
{
name: '📊 Puntos Totales',
value: `${stats.totalPoints} → **${newTotalPoints}**`,
inline: true
},
{
name: '📅 Puntos Semanales',
value: `${stats.weeklyPoints} → **${newWeeklyPoints}**`,
inline: true
},
{
name: '🗓️ Puntos Mensuales',
value: `${stats.monthlyPoints} → **${newMonthlyPoints}**`,
inline: true
}
)
.setFooter({ text: `Modificado por ${interaction.user.username}` })
.setTimestamp();
await interaction.reply({
embeds: [embed],
flags: MessageFlags.Ephemeral
});
} catch (e) {
logger.error({ err: e }, 'Error en ldPointsModal');
await interaction.reply({
content: '❌ Error al actualizar los puntos.',
flags: MessageFlags.Ephemeral
});
}
}
};

View File

@@ -0,0 +1,83 @@
import logger from "../../core/lib/logger";
import {
StringSelectMenuInteraction,
MessageFlags,
ModalBuilder,
TextInputBuilder,
TextInputStyle,
ActionRowBuilder
} from 'discord.js';
export default {
customId: 'ld_select_user',
run: async (interaction: StringSelectMenuInteraction) => {
if (!interaction.guild) {
return interaction.reply({
content: '❌ Solo disponible en servidores.',
flags: MessageFlags.Ephemeral
});
}
try {
const selectedUserId = interaction.values[0];
// Obtener información del usuario seleccionado para mostrar en el modal
let userName = 'Usuario';
try {
const member = await interaction.guild.members.fetch(selectedUserId);
userName = member.displayName || member.user.username;
} catch {
try {
const user = await interaction.client.users.fetch(selectedUserId);
userName = user.username;
} catch {
userName = selectedUserId;
}
}
// Crear modal para ingresar la cantidad de puntos
const modal = new ModalBuilder()
.setCustomId(`ld_points_modal:${selectedUserId}`)
.setTitle(`Gestionar puntos de ${userName}`);
// Input para puntos totales
const totalInput = new TextInputBuilder()
.setCustomId('total_points')
.setLabel('Puntos Totales (+ para añadir, - para quitar)')
.setPlaceholder('Ejemplo: +50 o -25 o =100 (para establecer)')
.setStyle(TextInputStyle.Short)
.setRequired(false);
// Input para puntos semanales
const weeklyInput = new TextInputBuilder()
.setCustomId('weekly_points')
.setLabel('Puntos Semanales (+ para añadir, - para quitar)')
.setPlaceholder('Ejemplo: +10 o -5 o =50 (para establecer)')
.setStyle(TextInputStyle.Short)
.setRequired(false);
// Input para puntos mensuales
const monthlyInput = new TextInputBuilder()
.setCustomId('monthly_points')
.setLabel('Puntos Mensuales (+ para añadir, - para quitar)')
.setPlaceholder('Ejemplo: +20 o -10 o =75 (para establecer)')
.setStyle(TextInputStyle.Short)
.setRequired(false);
// Añadir los inputs al modal
modal.addComponents(
new ActionRowBuilder<TextInputBuilder>().addComponents(totalInput),
new ActionRowBuilder<TextInputBuilder>().addComponents(weeklyInput),
new ActionRowBuilder<TextInputBuilder>().addComponents(monthlyInput)
);
await interaction.showModal(modal);
} catch (e) {
logger.error({ err: e }, 'Error en ldSelectUser');
await interaction.reply({
content: '❌ Error al procesar la selección.',
flags: MessageFlags.Ephemeral
});
}
}
};