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 = '

Aucun message enregistré.

'; } else { messagesHTML = messages.map(msg => { const msgDate = new Date(msg.timestamp).toLocaleString('fr-FR'); const attachmentsHTML = msg.attachments ? `
📎 Pièces jointes: ${msg.attachments.split(', ').map(url => `${url}`).join(', ')}
` : ''; return `
${escapeHtml(msg.userTag)} ${msgDate}
${escapeHtml(msg.content || '*Message vide*').replace(/\n/g, '
')}
${attachmentsHTML}
`; }).join(''); } let candidatureHTML = ''; if (candidatureResponses && candidatureResponses.length > 0) { candidatureHTML = `

📝 Réponses aux Questions de Candidature

${candidatureResponses.map(response => `
Question ${response.questionNumber}
${escapeHtml(response.question)}
${escapeHtml(response.response || '*Pas de réponse*').replace(/\n/g, '
')}
`).join('')}
`; } // 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 ` Transcription - ${ticket.ticketId}

${typeEmojis[ticket.type] || '🎫'} Transcription du Ticket

${ticket.ticketId}
Type
${typeEmojis[ticket.type] || '🎫'} ${ticket.type}
Créé par
${escapeHtml(ticket.userTag)}
Créé le
${dateStr}
Statut
${ticket.status}
${closedDateStr ? `
Fermé le
${closedDateStr}
` : ''}

💬 Messages

${messagesHTML}
${candidatureHTML}
`; } function escapeHtml(text) { if (!text) return ''; const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; 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: ``, 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: ``, 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;