Merge branch 'main' into 'dev'
# Conflicts: # commandes/ticket/__init__.py
This commit is contained in:
commit
9f92d0d212
8 changed files with 761 additions and 149 deletions
|
|
@ -76,7 +76,6 @@ class TicketPanelView(disnake.ui.View):
|
|||
label="🎫 Créer un ticket",
|
||||
custom_id="ticket_create"
|
||||
)
|
||||
create_btn.callback = self._create_callback
|
||||
self.add_item(create_btn)
|
||||
|
||||
# Mes tickets (si l'utilisateur a des tickets)
|
||||
|
|
@ -85,7 +84,6 @@ class TicketPanelView(disnake.ui.View):
|
|||
label="📂 Mes tickets",
|
||||
custom_id="ticket_my_tickets"
|
||||
)
|
||||
my_tickets_btn.callback = self._my_tickets_callback
|
||||
self.add_item(my_tickets_btn)
|
||||
|
||||
# Analytics (staff only)
|
||||
|
|
@ -94,7 +92,6 @@ class TicketPanelView(disnake.ui.View):
|
|||
label="📊 Analytics",
|
||||
custom_id="ticket_analytics"
|
||||
)
|
||||
analytics_btn.callback = self._analytics_callback
|
||||
self.add_item(analytics_btn)
|
||||
|
||||
# Staff Ratings (staff only)
|
||||
|
|
@ -547,9 +544,12 @@ class CategorySelectionView(disnake.ui.View):
|
|||
ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
# Show priority modal
|
||||
modal = TicketPriorityModal(self.bot, self.config, category)
|
||||
|
||||
# Check if this is a recruitment category
|
||||
if category.is_recruitment and category.questions:
|
||||
modal = RecruitmentQuestionsModal(self.bot, self.config, category)
|
||||
else:
|
||||
modal = TicketPriorityModal(self.bot, self.config, category)
|
||||
await interaction.response.send_modal(modal)
|
||||
except Exception as e:
|
||||
kuby_logger.error(f"[CategorySelection] Error: {e}", exc_info=True)
|
||||
|
|
@ -598,7 +598,7 @@ class TicketPriorityModal(disnake.ui.Modal):
|
|||
async def on_error(self, interaction: disnake.Interaction, error: Exception):
|
||||
"""Handle errors in the modal"""
|
||||
await interaction.response.send_message("Une erreur inattendue s'est produite lors de la création du ticket.", ephemeral=True)
|
||||
|
||||
|
||||
async def _create_ticket(self, interaction: disnake.Interaction, priority: Priority, reason: str):
|
||||
"""Crée le ticket"""
|
||||
guild = interaction.guild
|
||||
|
|
@ -707,6 +707,206 @@ class TicketPriorityModal(disnake.ui.Modal):
|
|||
await interaction.followup.send(f"Ticket créé: {ticket_channel.mention}", ephemeral=True)
|
||||
|
||||
|
||||
class RecruitmentQuestionsModal(disnake.ui.Modal):
|
||||
"""Modal pour les questions de recrutement"""
|
||||
|
||||
def __init__(self, bot, config, category: TicketCategory):
|
||||
self.bot = bot
|
||||
self.config = config
|
||||
self.category = category
|
||||
|
||||
# Construire dynamiquement les inputs de questions
|
||||
components = []
|
||||
self.question_inputs = {}
|
||||
|
||||
for i, question in enumerate(category.questions):
|
||||
input_field = disnake.ui.TextInput(
|
||||
label=f"Question {i+1}",
|
||||
custom_id=f"recruitment_q_{i}",
|
||||
placeholder=question,
|
||||
required=True,
|
||||
max_length=1000,
|
||||
style=disnake.TextInputStyle.paragraph
|
||||
)
|
||||
self.question_inputs[f"recruitment_q_{i}"] = input_field
|
||||
components.append(input_field)
|
||||
|
||||
super().__init__(title=f"Recrutement: {category.name}", components=components)
|
||||
|
||||
async def callback(self, interaction: disnake.ModalInteraction):
|
||||
await interaction.response.defer(ephemeral=True)
|
||||
try:
|
||||
# Collecter les réponses
|
||||
responses = {}
|
||||
for key, input_field in self.question_inputs.items():
|
||||
responses[key] = interaction.text_values.get(key, "")
|
||||
|
||||
# Créer le ticket avec les réponses stockées
|
||||
await self._create_recruitment_ticket(interaction, responses)
|
||||
except Exception as e:
|
||||
await interaction.followup.send(f"Erreur lors de la création du ticket: {e}", ephemeral=True)
|
||||
|
||||
async def _create_recruitment_ticket(self, interaction: disnake.Interaction, responses: Dict[str, str]):
|
||||
"""Crée le ticket de recrutement"""
|
||||
guild = interaction.guild
|
||||
user = interaction.user
|
||||
|
||||
storage = get_storage()
|
||||
|
||||
# Build permissions
|
||||
overwrites = {
|
||||
guild.default_role: disnake.PermissionOverwrite(read_messages=False),
|
||||
user: disnake.PermissionOverwrite(read_messages=True, send_messages=True),
|
||||
guild.me: disnake.PermissionOverwrite(read_messages=True, send_messages=True)
|
||||
}
|
||||
|
||||
# Add staff roles (category-specific roles override global ones)
|
||||
staff_roles_to_add = self.category.staff_roles if self.category.staff_roles else self.config.staff_roles
|
||||
for role_id in staff_roles_to_add:
|
||||
try:
|
||||
role = guild.get_role(int(role_id))
|
||||
if role and role not in overwrites:
|
||||
overwrites[role] = disnake.PermissionOverwrite(read_messages=True, send_messages=True)
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
|
||||
# Create channel name
|
||||
safe_username = "".join(c for c in user.display_name if not c.isdigit()).strip()
|
||||
if not safe_username:
|
||||
safe_username = "user"
|
||||
safe_category = "".join(c for c in self.category.name if not c.isdigit()).strip()
|
||||
channel_name = f"recrutement-{safe_username}"
|
||||
|
||||
try:
|
||||
category_channel = None
|
||||
if self.category.discord_category_id:
|
||||
try:
|
||||
category_channel = guild.get_channel(int(self.category.discord_category_id))
|
||||
if category_channel and not isinstance(category_channel, disnake.CategoryChannel):
|
||||
category_channel = None
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
if not category_channel and self.config.default_category:
|
||||
try:
|
||||
category_channel = guild.get_channel(int(self.config.default_category))
|
||||
if category_channel and not isinstance(category_channel, disnake.CategoryChannel):
|
||||
category_channel = None
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
ticket_channel = await guild.create_text_channel(
|
||||
name=channel_name,
|
||||
category=category_channel,
|
||||
overwrites=overwrites,
|
||||
topic=f"Recrutement de {user} | Catégorie: {self.category.name}"
|
||||
)
|
||||
except Exception as e:
|
||||
await interaction.followup.send(f"Erreur: {e}", ephemeral=True)
|
||||
return
|
||||
|
||||
# Create ticket data
|
||||
ticket_data = TicketData(
|
||||
channel_id=ticket_channel.id,
|
||||
guild_id=guild.id,
|
||||
user_id=user.id,
|
||||
category_id=self.category.id,
|
||||
priority=Priority.NORMAL,
|
||||
status=TicketStatus.OPEN,
|
||||
reason="Recrutement",
|
||||
recruitment_responses=responses
|
||||
)
|
||||
storage.save_ticket(ticket_data)
|
||||
|
||||
# Build mentions for staff
|
||||
mentions = [user.mention]
|
||||
for role_id in staff_roles_to_add:
|
||||
mentions.append(f"<@&{role_id}>")
|
||||
|
||||
mention_content = " ".join(mentions)
|
||||
|
||||
# Send welcome message in ticket channel
|
||||
welcome_text = f"Bienvenue {user.mention} !\n\nMerci de votre intérêt pour rejoindre notre équipe.\n\n"
|
||||
welcome_text += "Nos responsables vont analyser vos réponses et vous contacteront.\n\n"
|
||||
|
||||
# Afficher les questions et réponses
|
||||
welcome_text += "**Vos réponses:**\n"
|
||||
for i, question in enumerate(self.category.questions):
|
||||
key = f"recruitment_q_{i}"
|
||||
response = responses.get(key, "Non répondu")
|
||||
welcome_text += f"> **{question}**\n> {response}\n\n"
|
||||
|
||||
# Create container with management buttons and recruitment responses
|
||||
sections = [
|
||||
disnake.ui.TextDisplay(content=welcome_text),
|
||||
disnake.ui.Separator(divider=True)
|
||||
]
|
||||
|
||||
# Add management buttons if staff
|
||||
sections.append(
|
||||
disnake.ui.Section(
|
||||
"Actions",
|
||||
accessory=disnake.ui.Button(
|
||||
label="🔒 Fermer (Accepté/Refusé)",
|
||||
style=disnake.ButtonStyle.green,
|
||||
custom_id="recruitment_close"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
container = disnake.ui.Container(*sections)
|
||||
await ticket_channel.send(components=[disnake.ui.Container(
|
||||
disnake.ui.TextDisplay(content=mention_content),
|
||||
disnake.ui.Separator(divider=True),
|
||||
*sections
|
||||
)])
|
||||
|
||||
# Envoyer les réponses dans le salon staff si configuré
|
||||
if self.category.responses_channel_id:
|
||||
try:
|
||||
responses_channel = guild.get_channel(self.category.responses_channel_id)
|
||||
if responses_channel:
|
||||
embed = disnake.Embed(
|
||||
title=f"📝 Nouveau Candidature - {user}",
|
||||
description=f"**Candidature dans** {ticket_channel.mention}",
|
||||
color=disnake.Color.blue(),
|
||||
timestamp=datetime.now()
|
||||
)
|
||||
embed.set_thumbnail(url=user.display_avatar.url)
|
||||
|
||||
for i, question in enumerate(self.category.questions):
|
||||
key = f"recruitment_q_{i}"
|
||||
response = responses.get(key, "Non répondu")
|
||||
embed.add_field(
|
||||
name=f"Q{i+1}: {question}",
|
||||
value=response,
|
||||
inline=False
|
||||
)
|
||||
|
||||
embed.add_field(name="Candidat", value=user.mention, inline=True)
|
||||
embed.add_field(name="Discord ID", value=str(user.id), inline=True)
|
||||
|
||||
await responses_channel.send(embed=embed)
|
||||
except Exception as e:
|
||||
print(f"Error sending recruitment responses to staff channel: {e}")
|
||||
|
||||
# Log
|
||||
if self.config.log_channel_id:
|
||||
log_channel = guild.get_channel(self.config.log_channel_id)
|
||||
if log_channel:
|
||||
log_embed = disnake.Embed(
|
||||
title="🎓 Nouveau Ticket de Recrutement",
|
||||
color=disnake.Color.purple(),
|
||||
timestamp=datetime.now()
|
||||
)
|
||||
log_embed.add_field(name="Utilisateur", value=user.mention, inline=True)
|
||||
log_embed.add_field(name="Catégorie", value=self.category.name, inline=True)
|
||||
log_embed.add_field(name="Salon", value=ticket_channel.mention, inline=True)
|
||||
await log_channel.send(embed=log_embed)
|
||||
|
||||
await interaction.followup.send(f"Ticket créé: {ticket_channel.mention}", ephemeral=True)
|
||||
|
||||
|
||||
class SetCategoryModal(disnake.ui.Modal):
|
||||
"""Modal pour définir la catégorie Discord où les tickets seront créés"""
|
||||
|
||||
|
|
@ -2106,31 +2306,84 @@ class AddCategoryModal(disnake.ui.Modal):
|
|||
self.name_input = disnake.ui.TextInput(label="Nom", custom_id="add_cat_name", placeholder="Ex: Support", required=True)
|
||||
self.desc_input = disnake.ui.TextInput(label="Description", custom_id="add_cat_desc", placeholder="Description...", required=False, style=disnake.TextInputStyle.paragraph)
|
||||
self.emoji_input = disnake.ui.TextInput(label="Emoji", custom_id="add_cat_emoji", placeholder="🎫", required=False, max_length=5)
|
||||
super().__init__(title="Ajouter une Catégorie", components=[self.name_input, self.desc_input, self.emoji_input])
|
||||
# Recruitment fields
|
||||
self.is_recruitment_input = disnake.ui.TextInput(
|
||||
label="Recrutement? (oui/non)",
|
||||
custom_id="add_cat_recruitment",
|
||||
placeholder="non",
|
||||
required=False,
|
||||
max_length=5
|
||||
)
|
||||
self.questions_input = disnake.ui.TextInput(
|
||||
label="Questions recrutement (une par ligne)",
|
||||
custom_id="add_cat_questions",
|
||||
placeholder="Quel age avez-vous?\nPourquoi voulez-vous rejoindre?\nExperiences precedentes?",
|
||||
required=False,
|
||||
style=disnake.TextInputStyle.paragraph
|
||||
)
|
||||
self.responses_channel_input = disnake.ui.TextInput(
|
||||
label="ID salon pour les réponses",
|
||||
custom_id="add_cat_responses_channel",
|
||||
placeholder="ID du salon staff où envoyer les réponses",
|
||||
required=False,
|
||||
max_length=20
|
||||
)
|
||||
super().__init__(
|
||||
title="Ajouter une Catégorie",
|
||||
components=[
|
||||
self.name_input,
|
||||
self.desc_input,
|
||||
self.emoji_input,
|
||||
self.is_recruitment_input,
|
||||
self.questions_input,
|
||||
self.responses_channel_input
|
||||
]
|
||||
)
|
||||
self.bot = bot
|
||||
self.storage = storage
|
||||
self.config = config
|
||||
|
||||
|
||||
async def callback(self, interaction: disnake.ModalInteraction):
|
||||
import uuid
|
||||
|
||||
|
||||
cat_id = str(uuid.uuid4())[:8]
|
||||
|
||||
|
||||
name_val = interaction.text_values.get("add_cat_name", "Nouvelle Catégorie")
|
||||
desc_val = interaction.text_values.get("add_cat_desc", "")
|
||||
emoji_val = interaction.text_values.get("add_cat_emoji", "🎫")
|
||||
|
||||
is_recruitment_val = interaction.text_values.get("add_cat_recruitment", "non")
|
||||
questions_val = interaction.text_values.get("add_cat_questions", "")
|
||||
responses_channel_val = interaction.text_values.get("add_cat_responses_channel", "")
|
||||
|
||||
is_recruitment = is_recruitment_val.lower() in ["oui", "yes", "o", "y", "1", "true"]
|
||||
questions = [q.strip() for q in questions_val.split("\n") if q.strip()] if questions_val else []
|
||||
responses_channel_id = None
|
||||
if responses_channel_val and responses_channel_val.isdigit():
|
||||
responses_channel_id = int(responses_channel_val)
|
||||
|
||||
category = TicketCategory(
|
||||
id=cat_id,
|
||||
name=name_val,
|
||||
description=desc_val,
|
||||
emoji=emoji_val
|
||||
emoji=emoji_val,
|
||||
is_recruitment=is_recruitment,
|
||||
questions=questions,
|
||||
responses_channel_id=responses_channel_id
|
||||
)
|
||||
|
||||
|
||||
self.config.categories.append(category)
|
||||
self.storage.save_config(interaction.guild_id, self.config)
|
||||
|
||||
await interaction.response.send_message(f"Catégorie **{name_val}** créée!", ephemeral=True)
|
||||
|
||||
recruitment_info = ""
|
||||
if is_recruitment:
|
||||
recruitment_info = f"\n✅ Mode recrutement activé avec {len(questions)} question(s)"
|
||||
if responses_channel_id:
|
||||
recruitment_info += f"\n📝 Salon réponses: <#{responses_channel_id}>"
|
||||
|
||||
await interaction.response.send_message(
|
||||
f"Catégorie **{name_val}** créée!{recruitment_info}",
|
||||
ephemeral=True
|
||||
)
|
||||
|
||||
|
||||
class EditCategoryModal(disnake.ui.Modal):
|
||||
|
|
@ -2655,18 +2908,27 @@ class PermissionConfigView(disnake.ui.View):
|
|||
# Determine which list to update
|
||||
if self.config_type == "staff":
|
||||
target_list = self.config.staff_roles
|
||||
else:
|
||||
elif self.config_type == "config":
|
||||
target_list = self.config.config_roles
|
||||
|
||||
else:
|
||||
# Category-specific staff roles (config_type = "cat_{cat_id}")
|
||||
cat_id = self.config_type.replace("cat_", "")
|
||||
category = self.config.get_category(cat_id)
|
||||
if category:
|
||||
target_list = category.staff_roles
|
||||
else:
|
||||
await interaction.response.send_message("Catégorie introuvable.", ephemeral=True)
|
||||
return
|
||||
|
||||
# Get new selected IDs
|
||||
new_ids = interaction.data.get('values', [])
|
||||
added_count = 0
|
||||
|
||||
|
||||
for new_id in new_ids:
|
||||
if new_id not in target_list:
|
||||
target_list.append(new_id)
|
||||
added_count += 1
|
||||
|
||||
|
||||
self.storage.save_config(interaction.guild_id, self.config)
|
||||
|
||||
|
||||
|
|
@ -2683,17 +2945,20 @@ class PermissionConfigView(disnake.ui.View):
|
|||
try:
|
||||
if self.config_type == "staff":
|
||||
target_list = self.config.staff_users
|
||||
else:
|
||||
elif self.config_type == "config":
|
||||
target_list = self.config.config_users
|
||||
|
||||
else:
|
||||
# Category-specific staff - users not separately managed per category, use global staff_users
|
||||
target_list = self.config.staff_users
|
||||
|
||||
new_ids = [int(uid) for uid in interaction.data.get('values', [])]
|
||||
added_count = 0
|
||||
|
||||
|
||||
for new_id in new_ids:
|
||||
if new_id not in target_list:
|
||||
target_list.append(new_id)
|
||||
added_count += 1
|
||||
|
||||
|
||||
self.storage.save_config(interaction.guild_id, self.config)
|
||||
|
||||
|
||||
|
|
@ -2714,9 +2979,20 @@ class PermissionConfigView(disnake.ui.View):
|
|||
await interaction.response.edit_message(components=self.get_components_v2())
|
||||
|
||||
def get_components_v2(self):
|
||||
title = "🛡️ Permissions Staff" if self.config_type == "staff" else "⚙️ Permissions Config"
|
||||
desc = "Configurez qui peut gérer les tickets." if self.config_type == "staff" else "Configurez qui peut modifier les paramètres."
|
||||
|
||||
if self.config_type == "staff":
|
||||
title = "🛡️ Permissions Staff"
|
||||
desc = "Configurez qui peut gérer les tickets."
|
||||
elif self.config_type == "config":
|
||||
title = "⚙️ Permissions Config"
|
||||
desc = "Configurez qui peut modifier les paramètres."
|
||||
else:
|
||||
# Category-specific staff roles
|
||||
cat_id = self.config_type.replace("cat_", "")
|
||||
category = self.config.get_category(cat_id)
|
||||
cat_name = category.name if category else f"Catégorie {cat_id}"
|
||||
title = f"🛡️ Permissions Staff - {cat_name}"
|
||||
desc = f"Configurez les rôles staff ayant accès à la catégorie '{cat_name}'."
|
||||
|
||||
sections = [
|
||||
disnake.ui.TextDisplay(content=f"# {title}\n{desc}"),
|
||||
disnake.ui.Separator(divider=True),
|
||||
|
|
@ -2731,17 +3007,26 @@ class PermissionConfigView(disnake.ui.View):
|
|||
def _add_remove_select(self):
|
||||
# Build options from current config
|
||||
options = []
|
||||
|
||||
|
||||
if self.config_type == "staff":
|
||||
roles = self.config.staff_roles
|
||||
users = self.config.staff_users
|
||||
else:
|
||||
elif self.config_type == "config":
|
||||
roles = self.config.config_roles
|
||||
users = self.config.config_users
|
||||
|
||||
else:
|
||||
# Category-specific staff roles
|
||||
cat_id = self.config_type.replace("cat_", "")
|
||||
category = self.config.get_category(cat_id)
|
||||
if category:
|
||||
roles = category.staff_roles
|
||||
else:
|
||||
roles = []
|
||||
users = self.config.staff_users # Category uses global staff_users
|
||||
|
||||
# Limit to 25 items for removal dropdown (API Limit)
|
||||
# If more, we might need pagination or just show first 25.
|
||||
|
||||
|
||||
count = 0
|
||||
for r_id in roles:
|
||||
if count >= 25: break
|
||||
|
|
@ -2754,7 +3039,7 @@ class PermissionConfigView(disnake.ui.View):
|
|||
description="Cliquez pour retirer"
|
||||
))
|
||||
count += 1
|
||||
|
||||
|
||||
for u_id in users:
|
||||
if count >= 25: break
|
||||
user = self.guild.get_member(u_id)
|
||||
|
|
@ -2766,7 +3051,7 @@ class PermissionConfigView(disnake.ui.View):
|
|||
description="Cliquez pour retirer"
|
||||
))
|
||||
count += 1
|
||||
|
||||
|
||||
if options:
|
||||
select = disnake.ui.Select(
|
||||
placeholder="Sélectionnez pour RETIRER des permissions...",
|
||||
|
|
@ -2782,23 +3067,40 @@ class PermissionConfigView(disnake.ui.View):
|
|||
async def _remove_callback(self, interaction: disnake.Interaction):
|
||||
try:
|
||||
values = interaction.data.get('values', [])
|
||||
|
||||
|
||||
removed_count = 0
|
||||
for val in values:
|
||||
prefix, id_str = val.split('_')
|
||||
id_val = int(id_str)
|
||||
|
||||
if prefix == 'r': # Role
|
||||
target_list = self.config.staff_roles if self.config_type == "staff" else self.config.config_roles
|
||||
|
||||
if prefix == 'r': # Role
|
||||
if self.config_type == "staff":
|
||||
target_list = self.config.staff_roles
|
||||
elif self.config_type == "config":
|
||||
target_list = self.config.config_roles
|
||||
else:
|
||||
# Category-specific staff roles
|
||||
cat_id = self.config_type.replace("cat_", "")
|
||||
category = self.config.get_category(cat_id)
|
||||
if category:
|
||||
target_list = category.staff_roles
|
||||
else:
|
||||
continue
|
||||
if str(id_val) in target_list:
|
||||
target_list.remove(str(id_val))
|
||||
removed_count += 1
|
||||
elif prefix == 'u': # User
|
||||
target_list = self.config.staff_users if self.config_type == "staff" else self.config.config_users
|
||||
elif prefix == 'u': # User
|
||||
if self.config_type == "staff":
|
||||
target_list = self.config.staff_users
|
||||
elif self.config_type == "config":
|
||||
target_list = self.config.config_users
|
||||
else:
|
||||
# Category-specific staff uses global staff_users
|
||||
target_list = self.config.staff_users
|
||||
if id_val in target_list:
|
||||
target_list.remove(id_val)
|
||||
removed_count += 1
|
||||
|
||||
|
||||
self.storage.save_config(interaction.guild_id, self.config)
|
||||
|
||||
|
||||
|
|
@ -2894,13 +3196,40 @@ class TicketCommands(commands.Cog):
|
|||
else:
|
||||
cat_id = parts[1] if len(parts) > 1 else None
|
||||
|
||||
category = next((c for c in config.categories if c.id == cat_id), None)
|
||||
cat_id = parts[1]
|
||||
category = next((c for c in config.categories if str(c.id) == str(cat_id)), None)
|
||||
if not category:
|
||||
await inter.response.send_message("Catégorie non trouvée.", ephemeral=True)
|
||||
return
|
||||
|
||||
# ouvrir directement le modal de priorité (même logique que CategorySelectionView)
|
||||
modal = TicketPriorityModal(self.bot, config, category)
|
||||
# Check if recruitment category with questions
|
||||
if category.is_recruitment and category.questions:
|
||||
modal = RecruitmentQuestionsModal(self.bot, config, category)
|
||||
else:
|
||||
modal = TicketPriorityModal(self.bot, config, category)
|
||||
|
||||
await inter.response.send_modal(modal)
|
||||
except Exception as e:
|
||||
kuby_logger.error(f"[TicketSystem] cat| routing error: {e}", exc_info=True)
|
||||
if not inter.response.is_done():
|
||||
await inter.response.send_message(f"Erreur interne: {e}", ephemeral=True)
|
||||
|
||||
# backward compatibility (legacy cat_<id>_<suffix>)
|
||||
elif custom_id.startswith("cat_"):
|
||||
try:
|
||||
parts = str(custom_id).split("_")
|
||||
cat_id = "_".join(parts[1:-1]) if len(parts) >= 3 else (parts[1] if len(parts) > 1 else None)
|
||||
|
||||
category = next((c for c in config.categories if str(c.id) == str(cat_id)), None)
|
||||
if not category:
|
||||
await inter.response.send_message("Catégorie non trouvée.", ephemeral=True)
|
||||
return
|
||||
|
||||
if category.is_recruitment and category.questions:
|
||||
modal = RecruitmentQuestionsModal(self.bot, config, category)
|
||||
else:
|
||||
modal = TicketPriorityModal(self.bot, config, category)
|
||||
|
||||
await inter.response.send_modal(modal)
|
||||
except Exception as e:
|
||||
kuby_logger.error(f"[TicketSystem] catv2_ routing error: {e}", exc_info=True)
|
||||
|
|
@ -2933,6 +3262,80 @@ class TicketCommands(commands.Cog):
|
|||
view = TicketManagementView(self.bot, self.storage, config, ticket)
|
||||
await view._transcript_callback(inter)
|
||||
|
||||
# === Recrutement: Fermeture avec Accepté/Refusé ===
|
||||
elif custom_id == "recruitment_close":
|
||||
ticket = self.storage.load_ticket(inter.channel.id)
|
||||
if not ticket:
|
||||
await inter.response.send_message("Ticket non trouvé.", ephemeral=True)
|
||||
return
|
||||
|
||||
category = config.get_category(ticket.category_id)
|
||||
if not category or not category.is_recruitment:
|
||||
# Si pas un ticket recrutement, utiliser la fermeture normale
|
||||
view = TicketManagementView(self.bot, self.storage, config, ticket)
|
||||
await view._close_callback(inter)
|
||||
return
|
||||
|
||||
# Vérifier que c'est du staff
|
||||
if not is_staff(inter, config, category):
|
||||
await inter.response.send_message("Réservé au staff.", ephemeral=True)
|
||||
return
|
||||
|
||||
await inter.response.send_message(
|
||||
"Choisissez la décision pour cette candidature:",
|
||||
components=[
|
||||
disnake.ui.Container(
|
||||
disnake.ui.Section(
|
||||
"# 🎓 Décision de Recrutement",
|
||||
accessory=disnake.ui.Button(
|
||||
label="✅ Accepter",
|
||||
style=disnake.ButtonStyle.green,
|
||||
custom_id="recruitment_accept"
|
||||
)
|
||||
),
|
||||
disnake.ui.Separator(divider=True),
|
||||
disnake.ui.Section(
|
||||
"Refuser la candidature",
|
||||
accessory=disnake.ui.Button(
|
||||
label="❌ Refuser",
|
||||
style=disnake.ButtonStyle.red,
|
||||
custom_id="recruitment_refuse"
|
||||
)
|
||||
)
|
||||
)
|
||||
],
|
||||
flags=disnake.MessageFlags(is_components_v2=True),
|
||||
ephemeral=True
|
||||
)
|
||||
|
||||
elif custom_id == "recruitment_accept":
|
||||
ticket = self.storage.load_ticket(inter.channel.id) if inter.channel else None
|
||||
if not ticket:
|
||||
await inter.response.send_message("Ticket non trouvé.", ephemeral=True)
|
||||
return
|
||||
|
||||
category = config.get_category(ticket.category_id)
|
||||
if not is_staff(inter, config, category):
|
||||
await inter.response.send_message("Réservé au staff.", ephemeral=True)
|
||||
return
|
||||
|
||||
await inter.response.defer()
|
||||
await self._send_recruitment_decision(inter, ticket, category, accepted=True)
|
||||
|
||||
elif custom_id == "recruitment_refuse":
|
||||
ticket = self.storage.load_ticket(inter.channel.id) if inter.channel else None
|
||||
if not ticket:
|
||||
await inter.response.send_message("Ticket non trouvé.", ephemeral=True)
|
||||
return
|
||||
|
||||
category = config.get_category(ticket.category_id)
|
||||
if not is_staff(inter, config, category):
|
||||
await inter.response.send_message("Réservé au staff.", ephemeral=True)
|
||||
return
|
||||
|
||||
await inter.response.defer()
|
||||
await self._send_recruitment_decision(inter, ticket, category, accepted=False)
|
||||
|
||||
# === Fermeture via composants V2 (sans view=, donc custom_id) ===
|
||||
elif custom_id == "close_cancel":
|
||||
await inter.response.defer()
|
||||
|
|
@ -3258,18 +3661,93 @@ class TicketCommands(commands.Cog):
|
|||
print(f"[Ticket] Error registering views for guild {guild_id}: {e}")
|
||||
except Exception as e:
|
||||
print(f"[Ticket] Error in _register_all_views: {e}")
|
||||
|
||||
|
||||
async def _send_recruitment_decision(self, inter: disnake.Interaction, ticket, category, accepted: bool):
|
||||
"""Envoie la décision de recrutement (Accepté/Refusé) au candidat via DM"""
|
||||
try:
|
||||
guild = inter.guild
|
||||
user = guild.get_member(ticket.user_id)
|
||||
if not user:
|
||||
try:
|
||||
user = await guild.fetch_member(ticket.user_id)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Déterminer le message selon la décision
|
||||
if accepted:
|
||||
title = "✅ Candidature Acceptée !"
|
||||
color = disnake.Color.green()
|
||||
description = (
|
||||
f"Félicitations {user.mention} !\n\n"
|
||||
f"Votre candidature pour **Boga Life** a été acceptée.\n\n"
|
||||
f"Un responsable va bientôt vous contacter pour les prochaines étapes.\n\n"
|
||||
f"Bienvenue dans l'équipe ! 🎉"
|
||||
)
|
||||
else:
|
||||
title = "❌ Candidature Refusée"
|
||||
color = disnake.Color.red()
|
||||
description = (
|
||||
f"Bonjour {user.mention},\n\n"
|
||||
f"Après analyse de votre candidature, nous avons le regret de vous informer "
|
||||
f"que celle-ci n'a pas été retenue pour le poste.\n\n"
|
||||
f"Cette décision ne remet pas en cause vos qualités. N'hésitez pas à repostuler "
|
||||
f"ultérieurement si vous êtes toujours intéressé(e).\n\n"
|
||||
f"Nous vous souhaitons bonne continuation."
|
||||
)
|
||||
|
||||
embed = disnake.Embed(
|
||||
title=title,
|
||||
description=description,
|
||||
color=color,
|
||||
timestamp=datetime.now()
|
||||
)
|
||||
embed.set_footer(text="Boga Life - Système de Recrutement")
|
||||
|
||||
# Envoyer le DM à l'utilisateur
|
||||
if user:
|
||||
try:
|
||||
await user.send(embed=embed)
|
||||
await inter.followup.send("✅ Décision envoyée au candidat par DM.", ephemeral=True)
|
||||
except disnake.Forbidden:
|
||||
await inter.followup.send("⚠️ Impossible d'envoyer un DM - l'utilisateur a ses messages privés fermés.", ephemeral=True)
|
||||
except Exception as e:
|
||||
await inter.followup.send(f"⚠️ Erreur lors de l'envoi du DM: {e}", ephemeral=True)
|
||||
else:
|
||||
await inter.followup.send("⚠️ Utilisateur introuvable.", ephemeral=True)
|
||||
|
||||
# Fermer le ticket
|
||||
ticket.status = TicketStatus.CLOSED
|
||||
ticket.closed_at = datetime.now()
|
||||
self.storage.save_ticket(ticket)
|
||||
|
||||
# Envoyer un message dans le salon du ticket
|
||||
close_embed = disnake.Embed(
|
||||
title=f"🔒 Ticket Fermé - {'Accepté' if accepted else 'Refusé'}",
|
||||
description=f"Candidature traitée par {inter.user.mention}",
|
||||
color=color
|
||||
)
|
||||
close_embed.add_field(name="Decision", value="✅ Acceptee" if accepted else "❌ Refusee", inline=True)
|
||||
|
||||
# Supprimer le salon
|
||||
try:
|
||||
await inter.channel.delete(reason=f"Recrutement termine - {'Accepte' if accepted else 'Refuse'} par {inter.user}")
|
||||
except Exception as e:
|
||||
await inter.channel.send(embed=close_embed)
|
||||
print(f"Error deleting recruitment channel: {e}")
|
||||
|
||||
except Exception as e:
|
||||
kuby_logger.error(f"[TicketSystem] _send_recruitment_decision failed: {e}", exc_info=True)
|
||||
if not inter.response.is_done():
|
||||
await inter.followup.send("❌ Erreur lors de la decision de recrutement.", ephemeral=True)
|
||||
|
||||
# === COMMANDES ===
|
||||
|
||||
@commands.slash_command(name="ticket", description="Ouvre le panel des tickets")
|
||||
async def ticket_main(self, inter: disnake.ApplicationCommandInteraction):
|
||||
"""Ouvre le panel des tickets"""
|
||||
await inter.response.defer(ephemeral=True)
|
||||
|
||||
config = self.storage.load_config(inter.guild.id)
|
||||
|
||||
view = TicketPanelView(self.bot, config, self.storage)
|
||||
await inter.edit_original_response(components=view.get_components_v2())
|
||||
await inter.response.send_message(components=view.get_components_v2(), view=view, ephemeral=True)
|
||||
|
||||
@commands.slash_command(name="ticketpanel", description="Envoie le panel de création de tickets dans un canal")
|
||||
@commands.has_permissions(administrator=True)
|
||||
|
|
|
|||
|
|
@ -60,7 +60,10 @@ class TicketCategory:
|
|||
priority_enabled: bool = True,
|
||||
allow_claims: bool = True,
|
||||
survey_enabled: bool = True,
|
||||
discord_category_id: Optional[int] = None
|
||||
discord_category_id: Optional[int] = None,
|
||||
is_recruitment: bool = False,
|
||||
questions: List[str] = None,
|
||||
responses_channel_id: Optional[int] = None
|
||||
):
|
||||
self.id = id
|
||||
self.name = name
|
||||
|
|
@ -76,7 +79,10 @@ class TicketCategory:
|
|||
self.allow_claims = allow_claims
|
||||
self.survey_enabled = survey_enabled
|
||||
self.discord_category_id = discord_category_id
|
||||
|
||||
self.is_recruitment = is_recruitment
|
||||
self.questions = questions or []
|
||||
self.responses_channel_id = responses_channel_id
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert category to dictionary for storage"""
|
||||
return {
|
||||
|
|
@ -93,7 +99,10 @@ class TicketCategory:
|
|||
"priority_enabled": self.priority_enabled,
|
||||
"allow_claims": self.allow_claims,
|
||||
"survey_enabled": self.survey_enabled,
|
||||
"discord_category_id": self.discord_category_id
|
||||
"discord_category_id": self.discord_category_id,
|
||||
"is_recruitment": self.is_recruitment,
|
||||
"questions": self.questions,
|
||||
"responses_channel_id": self.responses_channel_id
|
||||
}
|
||||
|
||||
@classmethod
|
||||
|
|
@ -113,7 +122,10 @@ class TicketCategory:
|
|||
priority_enabled=data.get("priority_enabled", True),
|
||||
allow_claims=data.get("allow_claims", True),
|
||||
survey_enabled=data.get("survey_enabled", True),
|
||||
discord_category_id=data.get("discord_category_id")
|
||||
discord_category_id=data.get("discord_category_id"),
|
||||
is_recruitment=data.get("is_recruitment", False),
|
||||
questions=data.get("questions", []),
|
||||
responses_channel_id=data.get("responses_channel_id")
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -188,7 +200,8 @@ class TicketData:
|
|||
transcript_path: Optional[str] = None
|
||||
rating: Optional[int] = None
|
||||
feedback: Optional[str] = None
|
||||
|
||||
recruitment_responses: Dict[str, str] = field(default_factory=dict)
|
||||
|
||||
@property
|
||||
def is_open(self) -> bool:
|
||||
return self.status in [TicketStatus.OPEN, TicketStatus.CLAIMED]
|
||||
|
|
@ -214,9 +227,10 @@ class TicketData:
|
|||
"reason": self.reason,
|
||||
"transcript_path": self.transcript_path,
|
||||
"rating": self.rating,
|
||||
"feedback": self.feedback
|
||||
"feedback": self.feedback,
|
||||
"recruitment_responses": self.recruitment_responses
|
||||
}
|
||||
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'TicketData':
|
||||
messages = [TicketMessage.from_dict(m) for m in data.get("messages", [])]
|
||||
|
|
@ -234,7 +248,8 @@ class TicketData:
|
|||
reason=data.get("reason", ""),
|
||||
transcript_path=data.get("transcript_path"),
|
||||
rating=data.get("rating"),
|
||||
feedback=data.get("feedback")
|
||||
feedback=data.get("feedback"),
|
||||
recruitment_responses=data.get("recruitment_responses", {})
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue