/** * Application Admin Handlers (Export/Purge) */ import { EmbedBuilder, AttachmentBuilder, type ChatInputCommandInteraction, } from 'discord.js'; import type { EllyClient } from '../../../client/EllyClient.ts'; import { ApplicationRepository, type Application } from '../../../database/repositories/ApplicationRepository.ts'; import { PermissionLevel } from '../../../types/index.ts'; /** * Handle exporting applications to CSV */ export async function handleExport( interaction: ChatInputCommandInteraction, client: EllyClient, repo: ApplicationRepository ): Promise { const member = interaction.guild?.members.cache.get(interaction.user.id); if (!member || !client.permissions.hasPermission(member, PermissionLevel.Admin)) { await interaction.reply({ content: '❌ You need Admin permission to export applications.', ephemeral: true, }); return; } const statusFilter = interaction.options.getString('status') ?? 'all'; await interaction.deferReply({ ephemeral: true }); // Get applications let applications: Application[]; if (statusFilter === 'all') { applications = await repo.getAll(); } else { applications = await repo.getByStatus(statusFilter as Application['status']); } if (applications.length === 0) { await interaction.editReply({ content: '📭 No applications to export.', }); return; } // Create CSV content const headers = [ 'ID', 'User ID', 'MC Username', 'Status', 'Discord Age', 'Timezone', 'Activity', 'Why Join', 'Experience', 'Reviewed By', 'Created At', 'Reviewed At', ]; const rows = applications.map((app) => [ app.id, app.userId, app.minecraftUsername, app.status, app.discordAge ?? '', app.timezone ?? '', app.activity ?? '', `"${(app.whyJoin ?? '').replace(/"/g, '""')}"`, `"${(app.experience ?? '').replace(/"/g, '""')}"`, app.reviewedBy ?? '', new Date(app.createdAt).toISOString(), app.reviewedAt ? new Date(app.reviewedAt).toISOString() : '', ]); const csv = [headers.join(','), ...rows.map((r) => r.join(','))].join('\n'); // Create attachment const buffer = new TextEncoder().encode(csv); const attachment = new AttachmentBuilder(buffer, { name: `applications_${statusFilter}_${Date.now()}.csv`, }); const embed = new EmbedBuilder() .setColor(0x57f287) .setTitle('📤 Applications Exported') .addFields( { name: 'Filter', value: statusFilter === 'all' ? 'All' : statusFilter, inline: true }, { name: 'Count', value: String(applications.length), inline: true }, { name: 'Format', value: 'CSV', inline: true } ) .setFooter({ text: `Exported by ${interaction.user.tag}` }) .setTimestamp(); await interaction.editReply({ embeds: [embed], files: [attachment], }); // Log export const logChannel = client.channels_cache.applicationLogs; if (logChannel) { await logChannel.send({ embeds: [ new EmbedBuilder() .setColor(0x3498db) .setTitle('📤 Applications Exported') .addFields( { name: 'Exported By', value: interaction.user.tag, inline: true }, { name: 'Filter', value: statusFilter, inline: true }, { name: 'Count', value: String(applications.length), inline: true } ) .setTimestamp(), ], }); } } /** * Handle purging old applications */ export async function handlePurge( interaction: ChatInputCommandInteraction, client: EllyClient, repo: ApplicationRepository ): Promise { const member = interaction.guild?.members.cache.get(interaction.user.id); if (!member || !client.permissions.hasPermission(member, PermissionLevel.Admin)) { await interaction.reply({ content: '❌ You need Admin permission to purge applications.', ephemeral: true, }); return; } const days = interaction.options.getInteger('days', true); const statusFilter = interaction.options.getString('status'); await interaction.deferReply({ ephemeral: true }); const cutoff = Date.now() - days * 24 * 60 * 60 * 1000; // Get applications to purge const allApplications = await repo.getAll(); let toPurge = allApplications.filter((a) => a.createdAt < cutoff); // Apply status filter if (statusFilter === 'denied') { toPurge = toPurge.filter((a) => a.status === 'denied'); } else if (statusFilter === 'reviewed') { toPurge = toPurge.filter((a) => a.status !== 'pending'); } if (toPurge.length === 0) { await interaction.editReply({ content: '📭 No applications match the purge criteria.', }); return; } // Confirm purge const { ActionRowBuilder, ButtonBuilder, ButtonStyle, ComponentType } = await import('discord.js'); const confirmEmbed = new EmbedBuilder() .setColor(0xed4245) .setTitle('⚠️ Confirm Purge') .setDescription( `You are about to permanently delete **${toPurge.length}** applications.\n\n` + `**Criteria:**\n` + `• Older than: ${days} days\n` + `• Status filter: ${statusFilter ?? 'All reviewed'}\n\n` + `This action cannot be undone!` ) .setTimestamp(); const row = new ActionRowBuilder().addComponents( new ButtonBuilder() .setCustomId('purge:confirm') .setLabel(`Delete ${toPurge.length} Applications`) .setStyle(ButtonStyle.Danger), new ButtonBuilder() .setCustomId('purge:cancel') .setLabel('Cancel') .setStyle(ButtonStyle.Secondary) ); const response = await interaction.editReply({ embeds: [confirmEmbed], components: [row], }); try { const buttonInteraction = await response.awaitMessageComponent({ componentType: ComponentType.Button, filter: (i) => i.user.id === interaction.user.id, time: 30000, }); if (buttonInteraction.customId === 'purge:cancel') { await buttonInteraction.update({ embeds: [ new EmbedBuilder() .setColor(0x3498db) .setTitle('❌ Purge Cancelled') .setDescription('No applications were deleted.') .setTimestamp(), ], components: [], }); return; } // Perform purge let deleted = 0; for (const app of toPurge) { const success = await repo.delete(app.id); if (success) deleted++; } // Also delete associated notes const notes = client.database.get>('application_notes') ?? []; const purgedIds = new Set(toPurge.map((a) => a.id)); const remainingNotes = notes.filter((n) => !purgedIds.has(n.appId)); client.database.set('application_notes', remainingNotes); const resultEmbed = new EmbedBuilder() .setColor(0x57f287) .setTitle('🗑️ Purge Complete') .addFields( { name: 'Deleted', value: String(deleted), inline: true }, { name: 'Notes Removed', value: String(notes.length - remainingNotes.length), inline: true }, { name: 'Criteria', value: `Older than ${days} days`, inline: true } ) .setFooter({ text: `Purged by ${interaction.user.tag}` }) .setTimestamp(); await buttonInteraction.update({ embeds: [resultEmbed], components: [], }); // Log purge const logChannel = client.channels_cache.applicationLogs; if (logChannel) { await logChannel.send({ embeds: [ new EmbedBuilder() .setColor(0xed4245) .setTitle('🗑️ Applications Purged') .addFields( { name: 'Purged By', value: interaction.user.tag, inline: true }, { name: 'Count', value: String(deleted), inline: true }, { name: 'Criteria', value: `Older than ${days} days, ${statusFilter ?? 'all reviewed'}`, inline: true } ) .setTimestamp(), ], }); } } catch { await interaction.editReply({ embeds: [ new EmbedBuilder() .setColor(0xed4245) .setTitle('⏰ Timed Out') .setDescription('Purge cancelled due to timeout.') .setTimestamp(), ], components: [], }); } }