277 lines
8.1 KiB
TypeScript
277 lines
8.1 KiB
TypeScript
/**
|
|
* 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<void> {
|
|
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<void> {
|
|
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<ButtonBuilder>().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<Array<{ appId: string }>>('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: [],
|
|
});
|
|
}
|
|
}
|