Files
Femboy-Croissant-Bot/commands/ticket/ticket.js
2026-03-15 12:22:42 +01:00

673 lines
31 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const {
SlashCommandBuilder,
PermissionFlagsBits,
EmbedBuilder,
ChannelType,
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
ModalBuilder,
TextInputBuilder,
TextInputStyle,
MessageFlags
} = require('discord.js');
const db = require('../../functions/database/db.js');
const { colors } = require('../../utils/constants');
const fs = require('fs');
const path = require('path');
// Questions pré-définies pour les candidatures
const CANDIDATURE_QUESTIONS = [
'Pourquoi souhaites-tu rejoindre le staff ?',
'Quelles sont tes disponibilités ?',
'As-tu déjà de l\'expérience en modération ?',
'Comment gérerais-tu un conflit entre membres ?',
'Qu\'est-ce qui te motive à aider la communauté ?'
];
// Mapping des types de tickets vers des préfixes d'ID
const TICKET_TYPE_PREFIXES = {
'Support': 'SUPP',
'Plainte': 'PLNT',
'Plainte Staff': 'PLST',
'Candidature': 'CAND',
'Problème Technique': 'TECH'
};
function generateTicketId(type = 'Support') {
const prefix = TICKET_TYPE_PREFIXES[type] || 'TICK';
const timestamp = Date.now().toString(36).toUpperCase();
const random = Math.random().toString(36).substring(2, 6).toUpperCase();
return `${prefix}-${timestamp}-${random}`;
}
async function getCandidatureResponses(ticketId) {
const [responses] = await db.query(
'SELECT * FROM candidature_responses WHERE ticketId = ? ORDER BY questionNumber ASC',
[ticketId]
);
return responses;
}
function generateTranscriptHTML(ticket, messages, candidatureResponses, useRelativeCss = false) {
const date = new Date(ticket.createdAt);
const dateStr = date.toLocaleDateString('fr-FR', { year: 'numeric', month: '2-digit', day: '2-digit' });
const closedDateStr = ticket.closedAt ? new Date(ticket.closedAt).toLocaleDateString('fr-FR', { year: 'numeric', month: '2-digit', day: '2-digit' }) : null;
const typeEmojis = {
'Support': '💬',
'Plainte': '📢',
'Plainte Staff': '⚠️',
'Candidature': '📝',
'Problème Technique': '🔧'
};
const statusColors = {
'Ouvert': '#10b981',
'Fermé': '#ef4444',
'En attente': '#f59e0b'
};
let messagesHTML = '';
if (messages.length === 0) {
messagesHTML = '<p class="no-messages">Aucun message enregistré.</p>';
} else {
messagesHTML = messages.map(msg => {
const msgDate = new Date(msg.timestamp).toLocaleString('fr-FR');
const attachmentsHTML = msg.attachments
? `<div class="attachments">📎 Pièces jointes: ${msg.attachments.split(', ').map(url => `<a href="${url}" target="_blank">${url}</a>`).join(', ')}</div>`
: '';
return `
<div class="message">
<div class="message-header">
<span class="message-author">${escapeHtml(msg.userTag)}</span>
<span class="message-date">${msgDate}</span>
</div>
<div class="message-content">${escapeHtml(msg.content || '*Message vide*').replace(/\n/g, '<br>')}</div>
${attachmentsHTML}
</div>
`;
}).join('');
}
let candidatureHTML = '';
if (candidatureResponses && candidatureResponses.length > 0) {
candidatureHTML = `
<section class="candidature-section">
<h2>📝 Réponses aux Questions de Candidature</h2>
${candidatureResponses.map(response => `
<div class="question-item">
<div class="question-label">Question ${response.questionNumber}</div>
<div class="question-text">${escapeHtml(response.question)}</div>
<div class="response">${escapeHtml(response.response || '*Pas de réponse*').replace(/\n/g, '<br>')}</div>
</div>
`).join('')}
</section>
`;
}
// Déterminer l'URL du CSS
let cssUrl;
if (useRelativeCss) {
cssUrl = '../../transcript.css';
} else {
const webUrl = process.env.TRANSCRIPT_WEB_URL || 'transcript.syxpi.fr';
cssUrl = `https://${webUrl}/static/transcript.css`;
}
return `<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Transcription - ${ticket.ticketId}</title>
<link rel="stylesheet" href="${cssUrl}">
</head>
<body>
<div class="container">
<div class="header">
<h1>${typeEmojis[ticket.type] || '🎫'} Transcription du Ticket</h1>
<div class="ticket-id">${ticket.ticketId}</div>
</div>
<div class="info-section">
<div class="info-grid">
<div class="info-item">
<div class="info-label">Type</div>
<div class="info-value">${typeEmojis[ticket.type] || '🎫'} ${ticket.type}</div>
</div>
<div class="info-item">
<div class="info-label">Créé par</div>
<div class="info-value">${escapeHtml(ticket.userTag)}</div>
</div>
<div class="info-item">
<div class="info-label">Créé le</div>
<div class="info-value">${dateStr}</div>
</div>
<div class="info-item">
<div class="info-label">Statut</div>
<div class="info-value">
<span class="status-badge" style="background: ${statusColors[ticket.status]}; color: white;">
${ticket.status}
</span>
</div>
</div>
${closedDateStr ? `
<div class="info-item">
<div class="info-label">Fermé le</div>
<div class="info-value">${closedDateStr}</div>
</div>
` : ''}
</div>
</div>
<div class="messages-section">
<h2>💬 Messages</h2>
${messagesHTML}
</div>
${candidatureHTML}
<div class="footer">
<p>Transcription générée le ${new Date().toLocaleString('fr-FR')}</p>
<p>Femboy Croissant Bot • • Système de Tickets</p>
</div>
</div>
</body>
</html>`;
}
function escapeHtml(text) {
if (!text) return '';
const map = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' };
return text.replace(/[&<>"']/g, m => map[m]);
}
const commandModule = {
category: 'ticket',
data: new SlashCommandBuilder()
.setName('ticket')
.setDescription('Gérer les tickets')
.addSubcommand(subcommand => subcommand.setName('setup').setDescription('Configurer (Admin)').addChannelOption(o => o.setName('channel').setDescription('Salon')))
.addSubcommand(subcommand => subcommand.setName('close').setDescription('Fermer un ticket').addStringOption(o => o.setName('reason').setDescription('Raison')))
.addSubcommand(subcommand => subcommand.setName('transcript').setDescription('Générer transcript'))
.addSubcommand(subcommand => subcommand.setName('reopen').setDescription('Rouvrir un ticket'))
.addSubcommand(subcommand => subcommand.setName('add').setDescription('Ajouter utilisateur').addUserOption(o => o.setName('user').setDescription('Utilisateur').setRequired(true)))
.addSubcommand(subcommand => subcommand.setName('claim').setDescription('Réclamer un ticket')),
async execute(interaction) {
const subcommand = interaction.options.getSubcommand();
if (subcommand === 'setup') await handleSetup(interaction);
else if (subcommand === 'close') await handleClose(interaction);
else if (subcommand === 'transcript') await handleTranscript(interaction);
else if (subcommand === 'reopen') await handleReopen(interaction);
else if (subcommand === 'add') await handleAdd(interaction);
else if (subcommand === 'claim') await handleClaim(interaction);
},
};
async function handleCreate(interaction, ticketType) {
// Si l'interaction est un ModalSubmit, on a déjà répondu dans interactionCreate ? Non, on va defer ici.
// Attention : handleCreate est appelé après le ModalSubmit.
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
const type = ticketType || 'Support';
// Récupérer le sujet s'il vient d'un modal
let subject = 'Aucun sujet précisé';
if (interaction.isModalSubmit() && interaction.fields) {
try {
subject = interaction.fields.getTextInputValue('ticket_subject');
} catch (e) { /* Pas de champ subject (ex: commande slash sans modal) */ }
}
try {
const [existingTickets] = await db.query(
'SELECT * FROM tickets WHERE userId = ? AND guildId = ? AND status = ?',
[interaction.user.id, interaction.guild.id, 'Ouvert']
);
if (existingTickets.length > 0) {
const existingTicket = existingTickets[0];
const channel = interaction.guild.channels.cache.get(existingTicket.channelId);
return interaction.editReply({
content: `❌ Tu as déjà un ticket ouvert : ${channel ? channel.toString() : `ID: ${existingTicket.ticketId}`}`
});
}
let ticketCategory = interaction.guild.channels.cache.find(
ch => ch.type === ChannelType.GuildCategory && ch.name.toLowerCase().includes('ticket')
);
if (!ticketCategory) {
try {
ticketCategory = await interaction.guild.channels.create({
name: '🎫 Tickets',
type: ChannelType.GuildCategory,
permissionOverwrites: [
{ id: interaction.guild.id, deny: [PermissionFlagsBits.ViewChannel] },
{ id: interaction.client.user.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.ManageChannels, PermissionFlagsBits.SendMessages] },
],
});
} catch (err) { ticketCategory = null; }
}
const ticketId = generateTicketId(type);
const cleanUsername = interaction.user.username.toLowerCase().replace(/[^a-z0-9-]/g, '').substring(0, 20);
const typeName = type.toLowerCase().replace(/\s+/g, '-').substring(0, 20);
const channelName = `${typeName}-${cleanUsername}`.substring(0, 100);
let channel;
// Permissions initiales : Le user voit le salon
const userPerms = [
{ id: interaction.guild.id, deny: [PermissionFlagsBits.ViewChannel] },
{ id: interaction.user.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages, PermissionFlagsBits.AttachFiles] },
{ id: interaction.client.user.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages, PermissionFlagsBits.ManageMessages, PermissionFlagsBits.EmbedLinks, PermissionFlagsBits.AttachFiles] },
];
try {
channel = await interaction.guild.channels.create({
name: channelName,
type: ChannelType.GuildText,
parent: ticketCategory?.id,
permissionOverwrites: userPerms,
});
} catch (createErr) {
channel = await interaction.guild.channels.create({
name: channelName,
type: ChannelType.GuildText,
permissionOverwrites: userPerms,
});
}
const modRoles = interaction.guild.roles.cache.filter(role => role.permissions.has(PermissionFlagsBits.ManageMessages) && !role.managed);
for (const role of modRoles.values()) {
await channel.permissionOverwrites.edit(role.id, { ViewChannel: true, SendMessages: true, ReadMessageHistory: true });
}
await db.query(
`INSERT INTO tickets (ticketId, channelId, userId, userTag, guildId, type, status, createdAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[ticketId, channel.id, interaction.user.id, interaction.user.tag, interaction.guild.id, type, 'Ouvert', Date.now()]
);
const typeEmojis = { 'Support': '💬', 'Plainte': '📢', 'Plainte Staff': '⚠️', 'Candidature': '📝', 'Problème Technique': '🔧' };
const welcomeEmbed = new EmbedBuilder()
.setColor(colors.success)
.setTitle(`${typeEmojis[type] || '🎫'} Ticket Ouvert : ${type}`)
.setDescription(`👋 Bonjour ${interaction.user.toString()} !\n\nUn membre de l'équipe va prendre en charge votre demande.`)
.addFields(
{ name: '📋 Sujet de la demande', value: `\`\`\`${subject}\`\`\``, inline: false },
{ name: '🆔 ID', value: `\`${ticketId}\``, inline: true },
{ name: '📅 Date', value: `<t:${Math.floor(Date.now() / 1000)}:R>`, inline: true }
)
.setThumbnail(interaction.user.displayAvatarURL({ dynamic: true }))
.setFooter({ text: 'Femboy Croissant Bot • Support', iconURL: interaction.client.user.displayAvatarURL() })
.setTimestamp();
const row = new ActionRowBuilder().addComponents(
new ButtonBuilder().setCustomId(`ticket_close_${ticketId}`).setLabel('Fermer').setStyle(ButtonStyle.Danger).setEmoji('🔒'),
new ButtonBuilder().setCustomId(`ticket_claim_${ticketId}`).setLabel('Réclamer').setStyle(ButtonStyle.Secondary).setEmoji('👋')
);
await channel.send({ content: `${interaction.user} <@&${modRoles.first()?.id || interaction.user.id}>`, embeds: [welcomeEmbed], components: [row] });
await interaction.editReply({ content: `✅ Ticket créé avec succès ! ${channel.toString()}` });
} catch (err) {
console.error('Erreur creation ticket:', err);
await interaction.editReply({ content: `❌ Erreur: ${err.message}` });
}
}
// 1. Point d'entrée pour la fermeture (Bouton ou Slash)
async function handleClose(interaction) {
// 1. Vérification Permissions (User ne doit pas pouvoir fermer)
if (!interaction.member.permissions.has(PermissionFlagsBits.ManageMessages)) {
return interaction.reply({ content: '❌ Seuls les membres du staff peuvent fermer un ticket.', flags: MessageFlags.Ephemeral });
}
if (interaction.isButton()) {
const modal = new ModalBuilder().setCustomId('ticket_close_reason_modal').setTitle('Fermeture du Ticket');
const input = new TextInputBuilder().setCustomId('reason').setLabel('Raison').setStyle(TextInputStyle.Paragraph).setRequired(true);
modal.addComponents(new ActionRowBuilder().addComponents(input));
await interaction.showModal(modal);
return;
}
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
let reason = interaction.options?.getString('reason') || 'Aucune raison fournie';
await processTicketClose(interaction, reason);
}
async function handleCloseModal(interaction) {
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
// Check permission again for security
if (!interaction.member.permissions.has(PermissionFlagsBits.ManageMessages)) {
return interaction.editReply({ content: '❌ Action non autorisée.' });
}
const reason = interaction.fields.getTextInputValue('reason') || 'Aucune raison fournie';
await processTicketClose(interaction, reason);
}
// 3. Logique commune de fermeture
async function processTicketClose(interaction, reason) {
try {
const [tickets] = await db.query('SELECT * FROM tickets WHERE channelId = ?', [interaction.channel.id]);
if (!tickets.length) return interaction.editReply({ content: '❌ Erreur ticket.' });
const ticket = tickets[0];
await db.query('UPDATE tickets SET status = ?, closedAt = ?, closedBy = ? WHERE ticketId = ?', ['Fermé', Date.now(), interaction.user.id, ticket.ticketId]);
// --- GENERATION TRANSCRIPT AUTO ---
try {
const [messages] = await db.query('SELECT * FROM ticket_messages WHERE ticketId = ? ORDER BY timestamp', [ticket.ticketId]);
const candidatureResponses = ticket.type === 'Candidature' ? await getCandidatureResponses(ticket.ticketId) : null;
const htmlContent = generateTranscriptHTML(ticket, messages, candidatureResponses, true);
// Structure dossiers
const date = new Date(ticket.createdAt);
const dateFolder = date.toISOString().split('T')[0];
const typeFolder = ticket.type.toLowerCase().replace(/\s+/g, '-');
const cleanUsername = ticket.userTag.replace(/[^a-zA-Z0-9-_]/g, '-').toLowerCase();
const transcriptsBaseDir = path.join(process.cwd(), 'transcripts');
const dateDir = path.join(transcriptsBaseDir, typeFolder, dateFolder);
if (!fs.existsSync(dateDir)) fs.mkdirSync(dateDir, { recursive: true });
// Nom de fichier unique avec ID du ticket
const fileName = `${dateFolder}-${cleanUsername}-${ticket.ticketId}.html`;
const filePath = path.join(dateDir, fileName);
fs.writeFileSync(filePath, htmlContent, 'utf8');
// Chemin relatif pour la DB (ex: transcripts/support/2025-01-01/file.html)
const dbPath = `transcripts/${typeFolder}/${dateFolder}/${fileName}`;
await db.query('UPDATE tickets SET transcriptPath = ? WHERE ticketId = ?', [dbPath, ticket.ticketId]);
} catch (transcriptErr) {
console.error('Erreur génération transcript auto:', transcriptErr);
}
// ----------------------------------
const closeEmbed = new EmbedBuilder()
.setTitle('🔒 Ticket Fermé')
.setColor(colors.error)
.setDescription(`Ticket clôturé par ${interaction.user}.`)
.addFields(
{ name: '📝 Raison', value: `\`\`\`${reason}\`\`\``, inline: false },
{ name: '👮 Géré par', value: ticket.claimedBy ? `<@${ticket.claimedBy}>` : '> *Personne*', inline: true }
);
// BOUTONS DE GESTION APRES FERMETURE
const row = new ActionRowBuilder().addComponents(
new ButtonBuilder().setCustomId(`ticket_reopen_${ticket.ticketId}`).setLabel('Rouvrir').setStyle(ButtonStyle.Success).setEmoji('🔓'),
new ButtonBuilder().setCustomId(`ticket_delete_${ticket.ticketId}`).setLabel('Supprimer').setStyle(ButtonStyle.Danger).setEmoji('🗑️')
);
const closeMsg = await interaction.channel.send({ embeds: [closeEmbed], components: [row] });
if (!interaction.channel.name.startsWith('🔒-')) {
await interaction.channel.setName(`🔒-${interaction.channel.name}`).catch(() => {});
}
// VISIBILITE : On retire la vue au membre
await interaction.channel.permissionOverwrites.edit(ticket.userId, { ViewChannel: false });
// Désactivation des ANCIENS boutons (Sauf le nouveau message de fermeture)
try {
const messages = await interaction.channel.messages.fetch({ limit: 20 });
const oldMessages = messages.filter(m => m.author.id === interaction.client.user.id && m.components.length > 0 && m.id !== closeMsg.id);
for (const oldMsg of oldMessages.values()) {
const newRows = oldMsg.components.map(row => {
const newRow = new ActionRowBuilder();
row.components.forEach(c => newRow.addComponents(ButtonBuilder.from(c).setDisabled(true)));
return newRow;
});
await oldMsg.edit({ components: newRows });
}
} catch (e) {}
await interaction.editReply({ content: '✅ Ticket fermé.' });
} catch (err) { await interaction.editReply({ content: `❌ Erreur: ${err.message}` }); }
}
async function handleTranscript(interaction) {
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
try {
const [tickets] = await db.query('SELECT * FROM tickets WHERE channelId = ?', [interaction.channel.id]);
if (tickets.length === 0) return interaction.editReply({ content: '❌ Ce canal n\'est pas un ticket.' });
const ticket = tickets[0];
const [messages] = await db.query('SELECT * FROM ticket_messages WHERE ticketId = ? ORDER BY timestamp', [ticket.ticketId]);
// Structure dossiers
const date = new Date(ticket.createdAt);
const dateFolder = date.toISOString().split('T')[0];
const typeFolder = ticket.type.toLowerCase().replace(/\s+/g, '-');
const cleanUsername = ticket.userTag.replace(/[^a-zA-Z0-9-_]/g, '-').toLowerCase();
const transcriptsBaseDir = path.join(process.cwd(), 'transcripts');
const dateDir = path.join(transcriptsBaseDir, typeFolder, dateFolder);
if (!fs.existsSync(dateDir)) fs.mkdirSync(dateDir, { recursive: true });
// Génération
const candidatureResponses = ticket.type === 'Candidature' ? await getCandidatureResponses(ticket.ticketId) : null;
const htmlContent = generateTranscriptHTML(ticket, messages, candidatureResponses, true);
// Nom de fichier unique avec ID du ticket
const fileName = `${dateFolder}-${cleanUsername}-${ticket.ticketId}.html`;
const filePath = path.join(dateDir, fileName);
fs.writeFileSync(filePath, htmlContent, 'utf8');
const webPath = `transcripts/${typeFolder}/${dateFolder}/${fileName}`;
const webUrl = process.env.TRANSCRIPT_WEB_URL || 'transcript.syxpi.fr';
// Mise à jour DB
const dbPath = `transcripts/${typeFolder}/${dateFolder}/${fileName}`;
await db.query('UPDATE tickets SET transcriptPath = ? WHERE ticketId = ?', [dbPath, ticket.ticketId]);
await interaction.editReply({
content: `✅ **Transcription générée !**\n\n📄 **Fichier local:** \`${fileName}\`\n🌐 **Lien Web:** https://${webUrl}/${webPath}`,
files: [{ attachment: filePath, name: fileName }]
});
} catch (err) {
console.error(err);
await interaction.editReply({ content: `❌ Erreur: ${err.message}` });
}
}
async function handleReopen(interaction) {
if (interaction.isButton()) {
const modal = new ModalBuilder().setCustomId('ticket_reopen_modal').setTitle('Réouverture du Ticket');
const input = new TextInputBuilder().setCustomId('reason').setLabel('Raison').setStyle(TextInputStyle.Paragraph).setRequired(true);
modal.addComponents(new ActionRowBuilder().addComponents(input));
await interaction.showModal(modal);
return;
}
await handleReopenModal(interaction, "Commande Slash");
}
async function handleReopenModal(interaction, slashReason = null) {
if (!slashReason) await interaction.deferReply({ flags: MessageFlags.Ephemeral });
const reason = slashReason || interaction.fields.getTextInputValue('reason');
try {
const [tickets] = await db.query('SELECT * FROM tickets WHERE channelId = ?', [interaction.channel.id]);
const ticket = tickets[0];
await db.query('UPDATE tickets SET status = ?, closedAt = NULL, closedBy = NULL WHERE ticketId = ?', ['Ouvert', ticket.ticketId]);
// Restituer la vue
await interaction.channel.permissionOverwrites.edit(ticket.userId, { ViewChannel: true, SendMessages: true });
if (interaction.channel.name.startsWith('🔒-')) {
await interaction.channel.setName(interaction.channel.name.replace('🔒-', ''));
}
const embed = new EmbedBuilder()
.setTitle('🔓 Ticket Réouvert')
.setColor(colors.success)
.setDescription(`Le ticket a été rouvert par ${interaction.user}.`)
.addFields({ name: '📝 Raison', value: `\`\`\`${reason}\`\`\`` });
// IMPORTANT : On remet le bouton FERMER
const row = new ActionRowBuilder().addComponents(
new ButtonBuilder().setCustomId(`ticket_close_${ticket.ticketId}`).setLabel('Fermer').setStyle(ButtonStyle.Danger).setEmoji('🔒')
);
await interaction.channel.send({ embeds: [embed], components: [row] });
// Supprimer le message de fermeture précédent
try {
const messages = await interaction.channel.messages.fetch({ limit: 10 });
const closeMsg = messages.find(m => m.embeds[0]?.title === '🔒 Ticket Fermé');
if (closeMsg) await closeMsg.delete();
} catch (e) {}
const msg = { content: '✅ Ticket rouvert.' };
if (interaction.replied || interaction.deferred) await interaction.editReply(msg);
else await interaction.reply({ ...msg, flags: MessageFlags.Ephemeral });
} catch (err) { console.error(err); }
}
async function handleAdd(interaction) {
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
try {
const user = interaction.options.getUser('user');
await interaction.channel.permissionOverwrites.edit(user.id, { ViewChannel: true, SendMessages: true, ReadMessageHistory: true });
const embed = new EmbedBuilder()
.setTitle('👤 Utilisateur Ajouté')
.setColor(colors.info)
.setDescription(`${user.toString()} a été ajouté au ticket par ${interaction.user.toString()}.`)
.setTimestamp();
await interaction.channel.send({ embeds: [embed] });
await interaction.editReply({ content: `${user.tag} ajouté.` });
} catch (err) {
await interaction.editReply({ content: `❌ Erreur: ${err.message}` });
}
}
async function handleClaim(interaction) {
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
if (!interaction.member.permissions.has(PermissionFlagsBits.ManageMessages)) return interaction.editReply({ content: '❌ Staff uniquement.' });
try {
const [tickets] = await db.query('SELECT * FROM tickets WHERE channelId = ?', [interaction.channel.id]);
if (!tickets.length) return interaction.editReply({ content: '❌ Pas un ticket.' });
const ticket = tickets[0];
if (ticket.claimedBy === interaction.user.id) {
await db.query('UPDATE tickets SET claimedBy = NULL, claimedByTag = NULL WHERE ticketId = ?', [ticket.ticketId]);
await interaction.channel.send({ embeds: [new EmbedBuilder().setColor(colors.info).setDescription(`🛑 **Ticket libéré** par ${interaction.user.toString()}.`)] });
return interaction.editReply({ content: '✅ Ticket libéré.' });
}
if (ticket.claimedBy) return interaction.editReply({ content: `❌ Déjà réclamé par quelqu'un d'autre.` });
await db.query('UPDATE tickets SET claimedBy = ?, claimedByTag = ?, claimedAt = ? WHERE ticketId = ?', [interaction.user.id, interaction.user.tag, Date.now(), ticket.ticketId]);
const embed = new EmbedBuilder()
.setColor(colors.success)
.setTitle('👋 Ticket Pris en Charge')
.setDescription(`Ce ticket est désormais géré par ${interaction.user.toString()}.`)
.setThumbnail(interaction.user.displayAvatarURL({ dynamic: true }))
.setTimestamp();
await interaction.channel.send({ embeds: [embed] });
await interaction.editReply({ content: '✅ Ticket réclamé.' });
} catch (err) {
await interaction.editReply({ content: `❌ Erreur: ${err.message}` });
}
}
async function handleDelete(interaction) {
// Suppression directe sans modal
await handleDeleteModal(interaction, "Suppression demandée par l'utilisateur");
}
async function handleDeleteModal(interaction, slashReason = null) {
// Si l'interaction n'a pas été différée ou répondue, on le fait
if (!interaction.deferred && !interaction.replied) {
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
}
try {
const [tickets] = await db.query('SELECT * FROM tickets WHERE channelId = ?', [interaction.channel.id]);
if (tickets.length) {
await db.query('UPDATE tickets SET status = ?, closedAt = ?, closedBy = ? WHERE ticketId = ?', ['Supprimé', Date.now(), interaction.user.id, tickets[0].ticketId]);
}
await interaction.channel.delete();
} catch (err) { console.error(err); }
}
// Fonction spécifique pour le submit du modal
async function handleCandidatureModalSubmit(interaction) {
const ticketId = generateTicketId('Candidature');
const channelName = `candidature-${interaction.user.username.replace(/[^a-z0-9]/gi, '').substring(0,15)}`;
try {
let category = interaction.guild.channels.cache.find(c => c.type === ChannelType.GuildCategory && c.name.toLowerCase().includes('ticket'));
const channel = await interaction.guild.channels.create({
name: channelName, type: ChannelType.GuildText, parent: category?.id,
permissionOverwrites: [
{ id: interaction.guild.id, deny: [PermissionFlagsBits.ViewChannel] },
{ id: interaction.user.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages] },
{ id: interaction.client.user.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.ManageChannels] }
]
});
await db.query(`INSERT INTO tickets (ticketId, channelId, userId, userTag, guildId, type, status, createdAt) VALUES (?, ?, ?, ?, ?, 'Candidature', 'Ouvert', ?)`, [ticketId, channel.id, interaction.user.id, interaction.user.tag, interaction.guild.id, Date.now()]);
const fields = [];
for (let i = 0; i < Math.min(CANDIDATURE_QUESTIONS.length, 5); i++) {
const val = interaction.fields.getTextInputValue(`cand_q${i+1}`);
// Correction: Utilisation de Date.now() au lieu de new Date() pour le timestamp
await db.query('INSERT INTO candidature_responses (ticketId, questionNumber, question, response, timestamp) VALUES (?, ?, ?, ?, ?)', [ticketId, i+1, CANDIDATURE_QUESTIONS[i], val, Date.now()]);
// Utiliser la question complète comme nom de champ
fields.push({ name: CANDIDATURE_QUESTIONS[i], value: val.substring(0, 1024) });
}
const embed = new EmbedBuilder()
.setTitle('📝 Nouvelle Candidature Staff')
.setColor(colors.warning)
.setDescription(`Candidature soumise par ${interaction.user.toString()} (\`${interaction.user.tag}\`)`)
.setThumbnail(interaction.user.displayAvatarURL({ dynamic: true }))
.addFields(fields)
.addFields(
{ name: '━━━━━━━━━━━━━━━━', value: ' **Informations**' },
{ name: '🆔 ID Ticket', value: `\`${ticketId}\``, inline: true },
{ name: '📅 Soumis le', value: `<t:${Math.floor(Date.now() / 1000)}:F>`, inline: true }
)
.setFooter({ text: 'Femboy Croissant Bot • Recrutement', iconURL: interaction.client.user.displayAvatarURL() })
.setTimestamp();
const row = new ActionRowBuilder().addComponents(
new ButtonBuilder().setCustomId(`ticket_close_${ticketId}`).setLabel('Fermer').setStyle(ButtonStyle.Danger).setEmoji('🔒'),
new ButtonBuilder().setCustomId(`ticket_claim_${ticketId}`).setLabel('Traiter').setStyle(ButtonStyle.Success).setEmoji('✅')
);
await channel.send({ content: '@here Nouvelle candidature', embeds: [embed], components: [row] });
await interaction.reply({ content: `✅ Candidature envoyée : ${channel.toString()}`, flags: MessageFlags.Ephemeral });
} catch (e) { console.error(e); }
}
// Exports
commandModule.handleCreate = handleCreate;
commandModule.handleClose = handleClose;
commandModule.handleCloseModal = handleCloseModal;
commandModule.handleReopen = handleReopen;
commandModule.handleReopenModal = handleReopenModal;
commandModule.handleDelete = handleDelete;
commandModule.handleDeleteModal = handleDeleteModal;
commandModule.handleAdd = handleAdd;
commandModule.handleClaim = handleClaim;
commandModule.handleTranscript = handleTranscript;
commandModule.handleCandidatureModalSubmit = handleCandidatureModalSubmit;
module.exports = commandModule;
module.exports.CANDIDATURE_QUESTIONS = CANDIDATURE_QUESTIONS;
module.exports.generateTicketId = generateTicketId;