(Feat): Added a minimal pikanetwork client
This commit is contained in:
276
src/commands/applications/handlers/admin.ts
Normal file
276
src/commands/applications/handlers/admin.ts
Normal file
@@ -0,0 +1,276 @@
|
||||
/**
|
||||
* 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: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user