feat(economy): refactor editor message components for offers and areas
This commit is contained in:
@@ -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
|
|
||||||
|
|
||||||
@@ -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.
|
|
||||||
@@ -46,19 +46,23 @@ export const command: CommandMessage = {
|
|||||||
availableTo: existing?.availableTo ? new Date(existing.availableTo).toISOString() : '',
|
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)'}`,
|
content: `📊 Editor Nivel Área: \`${areaKey}\` nivel ${levelNum} ${existing ? '(editar)' : '(nuevo)'}`,
|
||||||
components: [ { type: 1, components: [
|
components: [
|
||||||
{ type: 2, style: ButtonStyle.Primary, label: 'Requisitos', custom_id: 'gl_req' },
|
{ type: 1, components: [
|
||||||
{ type: 2, style: ButtonStyle.Secondary, label: 'Recompensas', custom_id: 'gl_rewards' },
|
{ type: 2, style: ButtonStyle.Primary, label: 'Requisitos', custom_id: 'gl_req' },
|
||||||
{ type: 2, style: ButtonStyle.Secondary, label: 'Mobs', custom_id: 'gl_mobs' },
|
{ type: 2, style: ButtonStyle.Secondary, label: 'Recompensas', custom_id: 'gl_rewards' },
|
||||||
{ type: 2, style: ButtonStyle.Secondary, label: 'Ventana', custom_id: 'gl_window' },
|
{ type: 2, style: ButtonStyle.Secondary, label: 'Mobs', custom_id: 'gl_mobs' },
|
||||||
{ type: 2, style: ButtonStyle.Success, label: 'Guardar', custom_id: 'gl_save' },
|
{ type: 2, style: ButtonStyle.Secondary, label: 'Ventana', custom_id: 'gl_window' },
|
||||||
{ type: 2, style: ButtonStyle.Danger, label: 'Cancelar', custom_id: 'gl_cancel' },
|
] },
|
||||||
] } ],
|
{ 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) => {
|
collector.on('collect', async (i: MessageComponentInteraction) => {
|
||||||
try {
|
try {
|
||||||
if (!i.isButton()) return;
|
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 });
|
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 });
|
await sub.reply({ content: '✅ Ventana actualizada.', flags: MessageFlags.Ephemeral });
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,15 +32,19 @@ export const command: CommandMessage = {
|
|||||||
|
|
||||||
const editorMsg = await message.channel.send({
|
const editorMsg = await message.channel.send({
|
||||||
content: `🛒 Editor de Oferta (crear)`,
|
content: `🛒 Editor de Oferta (crear)`,
|
||||||
components: [ { type: 1, components: [
|
components: [
|
||||||
{ type: 2, style: ButtonStyle.Primary, label: 'Base', custom_id: 'of_base' },
|
{ type: 1, components: [
|
||||||
{ type: 2, style: ButtonStyle.Secondary, label: 'Precio (JSON)', custom_id: 'of_price' },
|
{ type: 2, style: ButtonStyle.Primary, label: 'Base', custom_id: 'of_base' },
|
||||||
{ type: 2, style: ButtonStyle.Secondary, label: 'Ventana', custom_id: 'of_window' },
|
{ type: 2, style: ButtonStyle.Secondary, label: 'Precio (JSON)', custom_id: 'of_price' },
|
||||||
{ type: 2, style: ButtonStyle.Secondary, label: 'Límites', custom_id: 'of_limits' },
|
{ type: 2, style: ButtonStyle.Secondary, label: 'Ventana', custom_id: 'of_window' },
|
||||||
{ type: 2, style: ButtonStyle.Secondary, label: 'Meta (JSON)', custom_id: 'of_meta' },
|
{ type: 2, style: ButtonStyle.Secondary, label: 'Límites', custom_id: 'of_limits' },
|
||||||
{ type: 2, style: ButtonStyle.Success, label: 'Guardar', custom_id: 'of_save' },
|
{ type: 2, style: ButtonStyle.Secondary, label: 'Meta (JSON)', custom_id: 'of_meta' },
|
||||||
{ type: 2, style: ButtonStyle.Danger, label: 'Cancelar', custom_id: 'of_cancel' },
|
]},
|
||||||
] } ],
|
{ 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)=> i.user.id === message.author.id });
|
||||||
@@ -123,4 +127,3 @@ async function showLimitsModal(i: ButtonInteraction, state: OfferState) {
|
|||||||
await i.showModal(modal);
|
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 {}
|
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 {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,20 +49,24 @@ export const command: CommandMessage = {
|
|||||||
metadata: offer.metadata ?? {},
|
metadata: offer.metadata ?? {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const editorMsg = await message.channel.send({
|
const editorMsg = await (message.channel as any).send({
|
||||||
content: `🛒 Editor de Oferta (editar): ${offerId}`,
|
content: `🛒 Editor de Oferta (editar): ${offerId}`,
|
||||||
components: [ { type: 1, components: [
|
components: [
|
||||||
{ type: 2, style: ButtonStyle.Primary, label: 'Base', custom_id: 'of_base' },
|
{ type: 1, components: [
|
||||||
{ type: 2, style: ButtonStyle.Secondary, label: 'Precio (JSON)', custom_id: 'of_price' },
|
{ type: 2, style: ButtonStyle.Primary, label: 'Base', custom_id: 'of_base' },
|
||||||
{ type: 2, style: ButtonStyle.Secondary, label: 'Ventana', custom_id: 'of_window' },
|
{ type: 2, style: ButtonStyle.Secondary, label: 'Precio (JSON)', custom_id: 'of_price' },
|
||||||
{ type: 2, style: ButtonStyle.Secondary, label: 'Límites', custom_id: 'of_limits' },
|
{ type: 2, style: ButtonStyle.Secondary, label: 'Ventana', custom_id: 'of_window' },
|
||||||
{ type: 2, style: ButtonStyle.Secondary, label: 'Meta (JSON)', custom_id: 'of_meta' },
|
{ type: 2, style: ButtonStyle.Secondary, label: 'Límites', custom_id: 'of_limits' },
|
||||||
{ type: 2, style: ButtonStyle.Success, label: 'Guardar', custom_id: 'of_save' },
|
{ type: 2, style: ButtonStyle.Secondary, label: 'Meta (JSON)', custom_id: 'of_meta' },
|
||||||
{ type: 2, style: ButtonStyle.Danger, label: 'Cancelar', custom_id: 'of_cancel' },
|
] },
|
||||||
] } ],
|
{ 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) => {
|
collector.on('collect', async (i: MessageComponentInteraction) => {
|
||||||
try {
|
try {
|
||||||
if (!i.isButton()) return;
|
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 });
|
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);
|
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 {}
|
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 {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user