From 7af57ff7860b94410c8bc923ec98587c54b27cc9 Mon Sep 17 00:00:00 2001 From: shni Date: Sun, 5 Oct 2025 02:46:26 -0500 Subject: [PATCH] feat(economy): refactor editor message components for offers and areas --- LEADERBOARD_ADMIN_SYSTEM.md | 196 ---------------------- discordjs-helper.prompt.md | 14 -- src/commands/messages/game/areaNivel.ts | 27 +-- src/commands/messages/game/offerCreate.ts | 23 +-- src/commands/messages/game/offerEdit.ts | 29 ++-- 5 files changed, 44 insertions(+), 245 deletions(-) delete mode 100644 LEADERBOARD_ADMIN_SYSTEM.md delete mode 100644 discordjs-helper.prompt.md diff --git a/LEADERBOARD_ADMIN_SYSTEM.md b/LEADERBOARD_ADMIN_SYSTEM.md deleted file mode 100644 index cb5038f..0000000 --- a/LEADERBOARD_ADMIN_SYSTEM.md +++ /dev/null @@ -1,196 +0,0 @@ -# Sistema de Gestión de Puntos del Leaderboard - -## 📋 Descripción - -Se ha implementado un sistema completo de gestión de puntos administrativos para el comando `leaderboard`. Los administradores ahora pueden modificar los puntos de cualquier usuario directamente desde el leaderboard. - -## 🎯 Características Implementadas - -### 1. **Botón de Gestión de Puntos** (Solo para Administradores) -- Aparece únicamente para usuarios con permiso `ManageGuild` -- Se muestra junto al botón "Refrescar" en el leaderboard -- Emoji: ⚙️ -- Label: "Gestionar Puntos" - -### 2. **Select Menu de Usuarios** -- Muestra hasta 25 usuarios con más puntos en el servidor -- Cada opción muestra: - - Nombre del usuario - - Puntos totales, semanales y mensuales actuales -- Ordenado por puntos totales (descendente) - -### 3. **Modal de Modificación de Puntos** -- Tres campos de entrada opcionales: - - **Puntos Totales** - - **Puntos Semanales** - - **Puntos Mensuales** - -#### Sintaxis de Modificación: -- `+50` → Añade 50 puntos -- `-25` → Quita 25 puntos -- `=100` → Establece exactamente 100 puntos -- `100` → Establece exactamente 100 puntos (sin símbolo) - -### 4. **Confirmación Visual** -- Embed con código de color verde -- Muestra los valores antes y después del cambio -- Incluye timestamp y nombre del administrador que hizo el cambio -- Mensaje efímero (solo visible para el administrador) - -## 📁 Archivos Creados - -``` -src/components/ -├── buttons/ -│ ├── ldManagePoints.ts ← Botón principal de gestión -│ └── ldRefresh.ts ← Actualizado para mostrar botón admin -├── selectmenus/ -│ └── ldSelectUser.ts ← Select menu para elegir usuario -└── modals/ - └── ldPointsModal.ts ← Modal para modificar puntos -``` - -## 🔒 Seguridad - -### Verificaciones de Permisos: -1. **En el leaderboard**: Solo muestra el botón si el usuario tiene `ManageGuild` -2. **En el botón**: Verifica permisos antes de mostrar el select menu -3. **En el modal**: Verifica permisos antes de modificar la base de datos - -### Validaciones: -- Los puntos no pueden ser negativos (mínimo: 0) -- Se requiere al menos un campo con valor para procesar -- Manejo de errores en todas las etapas -- Logs detallados de errores - -## 🚀 Cómo Usar - -### Para Administradores: - -1. **Ejecuta el comando leaderboard:** - ``` - !leaderboard - ``` - o - ``` - !ld - ``` - -2. **Verás el botón "⚙️ Gestionar Puntos"** - - Click en el botón - -3. **Selecciona el usuario del menú desplegable** - - Muestra nombre y estadísticas actuales - -4. **Ingresa los cambios en el modal:** - - Ejemplos: - - Puntos Totales: `+100` (añade 100) - - Puntos Semanales: `-50` (quita 50) - - Puntos Mensuales: `=75` (establece a 75) - -5. **Confirma el cambio** - - Verás un embed con los valores actualizados - - El leaderboard se puede refrescar para ver los cambios - -### Para Usuarios Normales: -- Solo verán el botón "Refrescar" -- No tienen acceso a la gestión de puntos - -## 🔄 Flujo del Sistema - -``` -Usuario Admin presiona "Gestionar Puntos" - ↓ -Sistema verifica permisos - ↓ -Muestra lista de usuarios con puntos (Select Menu) - ↓ -Admin selecciona un usuario - ↓ -Se abre modal con 3 campos de entrada - ↓ -Admin ingresa modificaciones (+/-/=) - ↓ -Sistema actualiza la base de datos - ↓ -Muestra embed de confirmación - ↓ -Admin puede refrescar el leaderboard para ver cambios -``` - -## 💾 Cambios en Base de Datos - -### Modelo `PartnershipStats`: -- Se modifica directamente el registro existente -- Si no existe, se crea uno nuevo con valores base en 0 -- Campos modificables: - - `totalPoints` - - `weeklyPoints` - - `monthlyPoints` - -## 📊 Ejemplo de Uso - -### Caso 1: Añadir puntos de bonificación -``` -Usuario: "Juan" -Puntos actuales: 150 -Acción: +50 en Puntos Totales -Resultado: 200 puntos totales -``` - -### Caso 2: Corregir error de conteo -``` -Usuario: "María" -Puntos semanales: 85 -Acción: =80 en Puntos Semanales -Resultado: 80 puntos semanales -``` - -### Caso 3: Penalización -``` -Usuario: "Pedro" -Puntos mensuales: 120 -Acción: -30 en Puntos Mensuales -Resultado: 90 puntos mensuales -``` - -## ⚠️ Notas Importantes - -1. **Los cambios son inmediatos** y afectan todas las tablas del leaderboard -2. **No hay sistema de deshacer** - confirma antes de aplicar cambios -3. **Los puntos mínimos son 0** - no pueden ser negativos -4. **Límite de 25 usuarios** en el select menu (limitación de Discord) -5. **Todos los mensajes son efímeros** - solo el admin los ve - -## 🧪 Testing - -Para probar el sistema: -1. Asegúrate de tener permisos de `ManageGuild` -2. Ejecuta `!leaderboard` -3. Verifica que aparezca el botón de gestión -4. Prueba modificar puntos de un usuario de prueba -5. Refresca el leaderboard para ver los cambios - -## 🐛 Troubleshooting - -**Problema:** No veo el botón de gestión -- **Solución:** Verifica que tengas permisos de administrador del servidor - -**Problema:** El select menu está vacío -- **Solución:** Asegúrate de que haya al menos un usuario con puntos en el servidor - -**Problema:** Los cambios no se reflejan -- **Solución:** Presiona el botón "Refrescar" para actualizar el leaderboard - -## 📝 Logs - -Todos los errores se registran con: -```typescript -logger.error({ err: e }, 'Descripción del error') -``` - -Los logs incluyen: -- Errores al cargar usuarios -- Errores al procesar selecciones -- Errores al actualizar puntos en la base de datos - diff --git a/discordjs-helper.prompt.md b/discordjs-helper.prompt.md deleted file mode 100644 index 0c5115b..0000000 --- a/discordjs-helper.prompt.md +++ /dev/null @@ -1,14 +0,0 @@ -# Prompt: Discord.js Expert Mode (Post-June 2025) - -Whenever assisting with Discord.js or related dependencies: - -- Always check official resources: - - https://discordjs.guide - - https://github.com/discordjs/discord.js - - https://github.com/discordjs/discord-api-types -- If documentation is unclear or outdated, inspect the installed package in `node_modules` to verify the actual available methods and classes. -- Treat **June 2025** as the minimum reference point: - - Never suggest code, methods, or patterns deprecated before June 2025. - - Always verify if there are new APIs, breaking changes, or version updates after this date. -- Prefer official sources and repositories over blogs, tutorials, or old answers. -- Always mention if information may be outdated and link to the package’s GitHub or changelog for verification. diff --git a/src/commands/messages/game/areaNivel.ts b/src/commands/messages/game/areaNivel.ts index 30d419c..8f90397 100644 --- a/src/commands/messages/game/areaNivel.ts +++ b/src/commands/messages/game/areaNivel.ts @@ -46,19 +46,23 @@ export const command: CommandMessage = { availableTo: existing?.availableTo ? new Date(existing.availableTo).toISOString() : '', }; - const editorMsg = await message.channel.send({ + const editorMsg = await (message.channel as any).send({ content: `📊 Editor Nivel Área: \`${areaKey}\` nivel ${levelNum} ${existing ? '(editar)' : '(nuevo)'}`, - components: [ { type: 1, components: [ - { type: 2, style: ButtonStyle.Primary, label: 'Requisitos', custom_id: 'gl_req' }, - { type: 2, style: ButtonStyle.Secondary, label: 'Recompensas', custom_id: 'gl_rewards' }, - { type: 2, style: ButtonStyle.Secondary, label: 'Mobs', custom_id: 'gl_mobs' }, - { type: 2, style: ButtonStyle.Secondary, label: 'Ventana', custom_id: 'gl_window' }, - { type: 2, style: ButtonStyle.Success, label: 'Guardar', custom_id: 'gl_save' }, - { type: 2, style: ButtonStyle.Danger, label: 'Cancelar', custom_id: 'gl_cancel' }, - ] } ], + components: [ + { type: 1, components: [ + { type: 2, style: ButtonStyle.Primary, label: 'Requisitos', custom_id: 'gl_req' }, + { type: 2, style: ButtonStyle.Secondary, label: 'Recompensas', custom_id: 'gl_rewards' }, + { type: 2, style: ButtonStyle.Secondary, label: 'Mobs', custom_id: 'gl_mobs' }, + { type: 2, style: ButtonStyle.Secondary, label: 'Ventana', custom_id: 'gl_window' }, + ] }, + { type: 1, components: [ + { type: 2, style: ButtonStyle.Success, label: 'Guardar', custom_id: 'gl_save' }, + { type: 2, style: ButtonStyle.Danger, label: 'Cancelar', custom_id: 'gl_cancel' }, + ] }, + ], }); - const collector = editorMsg.createMessageComponentCollector({ time: 30*60_000, filter: (i)=> i.user.id === message.author.id }); + const collector = editorMsg.createMessageComponentCollector({ time: 30*60_000, filter: (i: MessageComponentInteraction)=> i.user.id === message.author.id }); collector.on('collect', async (i: MessageComponentInteraction) => { try { if (!i.isButton()) return; @@ -93,7 +97,7 @@ export const command: CommandMessage = { if (!i.deferred && !i.replied) await i.reply({ content: '❌ Error procesando la acción.', flags: MessageFlags.Ephemeral }); } }); - collector.on('end', async (_c,r)=> { if (r==='time') { try { await editorMsg.edit({ content:'⏰ Editor expirado.', components: [] }); } catch {} } }); + collector.on('end', async (_c: any,r: string)=> { if (r==='time') { try { await editorMsg.edit({ content:'⏰ Editor expirado.', components: [] }); } catch {} } }); } }; @@ -126,4 +130,3 @@ async function showWindowModal(i: ButtonInteraction, state: LevelState) { await sub.reply({ content: '✅ Ventana actualizada.', flags: MessageFlags.Ephemeral }); } catch {} } - diff --git a/src/commands/messages/game/offerCreate.ts b/src/commands/messages/game/offerCreate.ts index 45b9b27..74e96ba 100644 --- a/src/commands/messages/game/offerCreate.ts +++ b/src/commands/messages/game/offerCreate.ts @@ -32,15 +32,19 @@ export const command: CommandMessage = { const editorMsg = await message.channel.send({ content: `🛒 Editor de Oferta (crear)`, - components: [ { type: 1, components: [ - { type: 2, style: ButtonStyle.Primary, label: 'Base', custom_id: 'of_base' }, - { type: 2, style: ButtonStyle.Secondary, label: 'Precio (JSON)', custom_id: 'of_price' }, - { type: 2, style: ButtonStyle.Secondary, label: 'Ventana', custom_id: 'of_window' }, - { type: 2, style: ButtonStyle.Secondary, label: 'Límites', custom_id: 'of_limits' }, - { type: 2, style: ButtonStyle.Secondary, label: 'Meta (JSON)', custom_id: 'of_meta' }, - { type: 2, style: ButtonStyle.Success, label: 'Guardar', custom_id: 'of_save' }, - { type: 2, style: ButtonStyle.Danger, label: 'Cancelar', custom_id: 'of_cancel' }, - ] } ], + components: [ + { type: 1, components: [ + { type: 2, style: ButtonStyle.Primary, label: 'Base', custom_id: 'of_base' }, + { type: 2, style: ButtonStyle.Secondary, label: 'Precio (JSON)', custom_id: 'of_price' }, + { type: 2, style: ButtonStyle.Secondary, label: 'Ventana', custom_id: 'of_window' }, + { type: 2, style: ButtonStyle.Secondary, label: 'Límites', custom_id: 'of_limits' }, + { type: 2, style: ButtonStyle.Secondary, label: 'Meta (JSON)', custom_id: 'of_meta' }, + ]}, + { type: 1, components: [ + { type: 2, style: ButtonStyle.Success, label: 'Guardar', custom_id: 'of_save' }, + { type: 2, style: ButtonStyle.Danger, label: 'Cancelar', custom_id: 'of_cancel' }, + ]}, + ], }); const collector = editorMsg.createMessageComponentCollector({ time: 30*60_000, filter: (i)=> i.user.id === message.author.id }); @@ -123,4 +127,3 @@ async function showLimitsModal(i: ButtonInteraction, state: OfferState) { await i.showModal(modal); try { const sub = await i.awaitModalSubmit({ time: 300_000 }); const lim = sub.components.getTextInputValue('limit').trim(); const st = sub.components.getTextInputValue('stock').trim(); state.perUserLimit = lim ? Math.max(0, parseInt(lim,10)||0) : null; state.stock = st ? Math.max(0, parseInt(st,10)||0) : null; await sub.reply({ content: '✅ Límites actualizados.', flags: MessageFlags.Ephemeral }); } catch {} } - diff --git a/src/commands/messages/game/offerEdit.ts b/src/commands/messages/game/offerEdit.ts index ad4a71c..cd21c01 100644 --- a/src/commands/messages/game/offerEdit.ts +++ b/src/commands/messages/game/offerEdit.ts @@ -49,20 +49,24 @@ export const command: CommandMessage = { metadata: offer.metadata ?? {}, }; - const editorMsg = await message.channel.send({ + const editorMsg = await (message.channel as any).send({ content: `🛒 Editor de Oferta (editar): ${offerId}`, - components: [ { type: 1, components: [ - { type: 2, style: ButtonStyle.Primary, label: 'Base', custom_id: 'of_base' }, - { type: 2, style: ButtonStyle.Secondary, label: 'Precio (JSON)', custom_id: 'of_price' }, - { type: 2, style: ButtonStyle.Secondary, label: 'Ventana', custom_id: 'of_window' }, - { type: 2, style: ButtonStyle.Secondary, label: 'Límites', custom_id: 'of_limits' }, - { type: 2, style: ButtonStyle.Secondary, label: 'Meta (JSON)', custom_id: 'of_meta' }, - { type: 2, style: ButtonStyle.Success, label: 'Guardar', custom_id: 'of_save' }, - { type: 2, style: ButtonStyle.Danger, label: 'Cancelar', custom_id: 'of_cancel' }, - ] } ], + components: [ + { type: 1, components: [ + { type: 2, style: ButtonStyle.Primary, label: 'Base', custom_id: 'of_base' }, + { type: 2, style: ButtonStyle.Secondary, label: 'Precio (JSON)', custom_id: 'of_price' }, + { type: 2, style: ButtonStyle.Secondary, label: 'Ventana', custom_id: 'of_window' }, + { type: 2, style: ButtonStyle.Secondary, label: 'Límites', custom_id: 'of_limits' }, + { type: 2, style: ButtonStyle.Secondary, label: 'Meta (JSON)', custom_id: 'of_meta' }, + ] }, + { type: 1, components: [ + { type: 2, style: ButtonStyle.Success, label: 'Guardar', custom_id: 'of_save' }, + { type: 2, style: ButtonStyle.Danger, label: 'Cancelar', custom_id: 'of_cancel' }, + ] }, + ], }); - const collector = editorMsg.createMessageComponentCollector({ time: 30*60_000, filter: (i)=> i.user.id === message.author.id }); + const collector = editorMsg.createMessageComponentCollector({ time: 30*60_000, filter: (i: MessageComponentInteraction)=> i.user.id === message.author.id }); collector.on('collect', async (i: MessageComponentInteraction) => { try { if (!i.isButton()) return; @@ -103,7 +107,7 @@ export const command: CommandMessage = { if (!i.deferred && !i.replied) await i.reply({ content: '❌ Error procesando la acción.', flags: MessageFlags.Ephemeral }); } }); - collector.on('end', async (_c,r)=> { if (r==='time') { try { await editorMsg.edit({ content:'⏰ Editor expirado.', components: [] }); } catch {} } }); + collector.on('end', async (_c: any,r: string)=> { if (r==='time') { try { await editorMsg.edit({ content:'⏰ Editor expirado.', components: [] }); } catch {} } }); } }; @@ -142,4 +146,3 @@ async function showLimitsModal(i: ButtonInteraction, state: OfferState) { await i.showModal(modal); try { const sub = await i.awaitModalSubmit({ time: 300_000 }); const lim = sub.components.getTextInputValue('limit').trim(); const st = sub.components.getTextInputValue('stock').trim(); state.perUserLimit = lim ? Math.max(0, parseInt(lim,10)||0) : null; state.stock = st ? Math.max(0, parseInt(st,10)||0) : null; await sub.reply({ content: '✅ Límites actualizados.', flags: MessageFlags.Ephemeral }); } catch {} } -