From eb07cb67a2091a153f632a35ef30dac7fe3d0903 Mon Sep 17 00:00:00 2001 From: Mathis Date: Fri, 3 Jul 2026 22:59:31 +0200 Subject: [PATCH] =?UTF-8?q?Fix:=20cr=C3=A9ation=20de=20cat=C3=A9gorie=20sy?= =?UTF-8?q?st=C3=A8me=20de=20ticket=20kuby?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- commandes/ticket/__init__.py | 501 ++++++++++++++++++++------------ commandes/ticket/data/models.py | 4 + 2 files changed, 320 insertions(+), 185 deletions(-) diff --git a/commandes/ticket/__init__.py b/commandes/ticket/__init__.py index 4516f05..9dbaa87 100644 --- a/commandes/ticket/__init__.py +++ b/commandes/ticket/__init__.py @@ -41,6 +41,9 @@ from src.logger import kuby_logger # V2 RICH COMPONENTS LOADED - 14:10 kuby_logger.info("đŸŽ« [TicketSystem] Chargement de la version V2 Rich Components...") +# Discord limite le nombre de composants par message ; 25 couvre les selects Discord. +MAX_PANEL_CATEGORIES = 25 + # Add a task for sending feedback reminders class FeedbackReminderTask: def __init__(self, bot): @@ -153,12 +156,8 @@ class TicketPanelView(disnake.ui.View): async def _create_callback(self, interaction: disnake.Interaction): """Affiche les catĂ©gories pour crĂ©er un ticket""" - # Reload config to ensure we have latest permissions/categories + self.storage.invalidate_cache(interaction.guild_id) self.config = self.storage.load_config(interaction.guild_id) - - # Reload config to ensure we have latest permissions/categories - self.config = self.storage.load_config(interaction.guild_id) - if not self.config.enabled: await interaction.response.send_message("Le systĂšme de tickets est dĂ©sactivĂ©.", ephemeral=True) @@ -167,45 +166,26 @@ class TicketPanelView(disnake.ui.View): if not self.config.categories: await interaction.response.send_message("Aucune catĂ©gorie configurĂ©e. Utilisez la configuration.", ephemeral=True) return - - # Build V2 Container for category selection - sections = [ - disnake.ui.TextDisplay(content="## đŸŽ« CrĂ©er un Ticket"), - disnake.ui.Separator(), - ] - - for cat in self.config.categories[:5]: - desc = cat.description or "Aucune description" - sections.append( - disnake.ui.Section( - f"**{cat.emoji} {cat.name}**\n{desc}", - accessory=disnake.ui.Button( - label=cat.name, - style=disnake.ButtonStyle.primary, - custom_id=f"catv2_{cat.id}_{int(disnake.utils.time_snowflake(disnake.utils.utcnow()) / 1000)}" - ) - ) - ) - + + components = build_category_selection_components(self.config) try: await interaction.response.send_message( - components=[disnake.ui.Container(*sections)], + components=components, flags=disnake.MessageFlags(is_components_v2=True), ephemeral=True ) except Exception as e: import traceback traceback.print_exc() - # Fallback: texte simple avec view legacy if interaction.response.is_done(): await interaction.followup.send( - f"**đŸŽ« CrĂ©er un Ticket**\n\nSĂ©lectionnez une catĂ©gorie ci-dessous:", + "**đŸŽ« CrĂ©er un Ticket**\n\nSĂ©lectionnez une catĂ©gorie ci-dessous:", view=CategorySelectionView(self.bot, self.config), ephemeral=True ) else: await interaction.response.send_message( - f"**đŸŽ« CrĂ©er un Ticket**\n\nSĂ©lectionnez une catĂ©gorie ci-dessous:", + "**đŸŽ« CrĂ©er un Ticket**\n\nSĂ©lectionnez une catĂ©gorie ci-dessous:", view=CategorySelectionView(self.bot, self.config), ephemeral=True ) @@ -477,6 +457,104 @@ class TicketPanelView(disnake.ui.View): return stats +async def refresh_ticket_panel(bot, storage, guild_id: int) -> bool: + """Met Ă  jour le message du panel de crĂ©ation de tickets s'il est enregistrĂ©.""" + config = storage.load_config(guild_id) + if not config.panel_channel_id or not config.panel_message_id: + return False + + try: + channel = bot.get_channel(config.panel_channel_id) + if channel is None: + channel = await bot.fetch_channel(config.panel_channel_id) + message = await channel.fetch_message(config.panel_message_id) + view = TicketPanelView(bot, config, storage) + await message.edit(components=view.get_components_v2()) + return True + except Exception as e: + kuby_logger.warning(f"[TicketSystem] Échec du rafraĂźchissement du panel: {e}") + return False + + +def build_category_selection_components(config: GuildTicketConfig) -> list: + """Construit le sĂ©lecteur de catĂ©gories (menu dĂ©roulant, jusqu'Ă  25 options). + + Un Container V2 ne peut contenir que 10 composants enfants ; une Section+Separator + par catĂ©gorie dĂ©passait rapidement cette limite. Le menu dĂ©roulant affiche toutes + les catĂ©gories dans un seul composant. + """ + categories = config.categories[:MAX_PANEL_CATEGORIES] + if not categories: + return [] + + options = [] + for cat in categories: + option_kwargs = { + "label": cat.name[:100], + "value": str(cat.id)[:100], + } + description = (cat.description or "").strip() + if description: + option_kwargs["description"] = description[:100] + if cat.emoji: + option_kwargs["emoji"] = cat.emoji + options.append(disnake.SelectOption(**option_kwargs)) + + category_select = disnake.ui.StringSelect( + custom_id="ticket_category_select", + placeholder="Choisissez une catĂ©gorie...", + options=options, + min_values=1, + max_values=1, + ) + + return [ + disnake.ui.Container( + disnake.ui.TextDisplay( + content="# 📂 CrĂ©er un Ticket\n\n" + "SĂ©lectionnez la catĂ©gorie correspondant Ă  votre demande." + ), + disnake.ui.ActionRow(category_select), + ) + ] + + +async def open_ticket_for_category(bot, interaction: disnake.Interaction, config: GuildTicketConfig, category_id: str): + """Ouvre le modal de crĂ©ation de ticket pour la catĂ©gorie choisie.""" + category = config.get_category(category_id) + if not category: + await interaction.response.send_message("CatĂ©gorie non trouvĂ©e.", ephemeral=True) + return + + storage = get_storage() + user_tickets = storage.get_user_tickets(interaction.guild_id, interaction.user.id, open_only=True) + + active_tickets = [] + for ticket in user_tickets: + if interaction.guild.get_channel(ticket.channel_id): + active_tickets.append(ticket) + else: + ticket.status = TicketStatus.CLOSED + storage.save_ticket(ticket) + + if len(active_tickets) >= config.max_tickets_per_user: + await interaction.response.send_message( + f"Vous avez atteint la limite de {config.max_tickets_per_user} tickets.", + ephemeral=True + ) + return + + if category.is_recruitment and category.questions: + modal = RecruitmentQuestionsModal(bot, config, category) + else: + modal = TicketPriorityModal(bot, config, category) + + if hasattr(modal, "title") and modal.title: + modal.title = str(modal.title)[:45] + + await interaction.response.send_modal(modal) + + class CategoryButton(disnake.ui.Button): def __init__(self, category, view_instance): super().__init__( @@ -502,7 +580,7 @@ class CategorySelectionView(disnake.ui.View): self.id_gen = int(datetime.now().timestamp()) # Ajouter boutons de catĂ©gories - for category in config.categories[:5]: + for category in config.categories[:MAX_PANEL_CATEGORIES]: self.add_item(CategoryButton(category, self)) def _is_staff(self, interaction: disnake.Interaction) -> bool: @@ -517,40 +595,7 @@ class CategorySelectionView(disnake.ui.View): async def _on_category_select(self, interaction: disnake.Interaction, category_id: str): """Handle category selection""" try: - - - category = self.config.get_category(category_id) - if not category: - await interaction.response.send_message("CatĂ©gorie non trouvĂ©e.", ephemeral=True) - return - - storage = get_storage() - - # Check limits - user_tickets = storage.get_user_tickets(interaction.guild_id, interaction.user.id, open_only=True) - - active_tickets = [] - for t in user_tickets: - if interaction.guild.get_channel(t.channel_id): - active_tickets.append(t) - else: - # Automatically clean up phantom tickets - t.status = TicketStatus.CLOSED - storage.save_ticket(t) - - if len(active_tickets) >= self.config.max_tickets_per_user: - await interaction.response.send_message( - f"Vous avez atteint la limite de {self.config.max_tickets_per_user} tickets.", - ephemeral=True - ) - return - - # 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) + await open_ticket_for_category(self.bot, interaction, self.config, category_id) except Exception as e: kuby_logger.error(f"[CategorySelection] Error: {e}", exc_info=True) try: @@ -720,10 +765,13 @@ class RecruitmentQuestionsModal(disnake.ui.Modal): self.question_inputs = {} for i, question in enumerate(category.questions): + # Discord constraints: + # - placeholder length <= 100 + # We clamp the dynamic placeholder (questions come from config). input_field = disnake.ui.TextInput( - label=f"Question {i+1}", + label=f"Question {i+1}", # <= 45 by construction custom_id=f"recruitment_q_{i}", - placeholder=question, + placeholder=_clamp_text(question, 100, ""), required=True, max_length=1000, style=disnake.TextInputStyle.paragraph @@ -2050,6 +2098,7 @@ class CategoriesSetupView(disnake.ui.View): # Remove category self.config.categories = [cat for cat in self.config.categories if cat.id != cat_id] self.storage.save_config(interaction.guild_id, self.config) + await refresh_ticket_panel(self.bot, self.storage, interaction.guild_id) await interaction.response.send_message("✅ CatĂ©gorie supprimĂ©e!", ephemeral=True) @@ -2300,34 +2349,92 @@ class ConfigPanelView(disnake.ui.View): await interaction.response.send_modal(modal) +def _clamp_text(s: Any, max_len: int, default: str = "") -> str: + """Discord TextInput constraints: + - label: 1..45 + - placeholder: <=100 + """ + if s is None: + return default + s = str(s) + if max_len <= 0: + return "" + if len(s) > max_len: + s = s[:max_len] + return s + + class AddCategoryModal(disnake.ui.Modal): """Modal pour ajouter une catĂ©gorie""" def __init__(self, bot, storage, config: GuildTicketConfig): - 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) + # label max 45, placeholder max 100 + name_label = _clamp_text("Nom", 45, "Nom") + name_placeholder = _clamp_text("Ex: Support", 100, "") + + desc_label = _clamp_text("Description", 45, "Description") + desc_placeholder = _clamp_text("Description...", 100, "") + + emoji_label = _clamp_text("Emoji", 45, "Emoji") + emoji_placeholder = _clamp_text("đŸŽ«", 100, "đŸŽ«") + + recruitment_label = _clamp_text("Recrutement? (oui/non)", 45, "Recrutement?") + recruitment_placeholder = _clamp_text("non", 100, "non") + + questions_label = _clamp_text( + "Questions recrutement (+ optionnel: ID salon rĂ©ponses)", + 45, + "Questions recrutement" + ) + # This was the most likely culprit: multi-line placeholder very long + questions_placeholder = _clamp_text( + "Quel Ăąge avez-vous? / Pourquoi vouloir rejoindre? / ExpĂ©riences? / Optionnel: ID salon rĂ©ponses: 123456789012345678", + 100, + "" + ) + + self.name_input = disnake.ui.TextInput( + label=name_label, + custom_id="add_cat_name", + placeholder=name_placeholder, + required=True, + ) + + self.desc_input = disnake.ui.TextInput( + label=desc_label, + custom_id="add_cat_desc", + placeholder=desc_placeholder, + required=False, + style=disnake.TextInputStyle.paragraph, + ) + + self.emoji_input = disnake.ui.TextInput( + label=emoji_label, + custom_id="add_cat_emoji", + placeholder=emoji_placeholder, + required=False, + max_length=5, + ) + # Recruitment fields self.is_recruitment_input = disnake.ui.TextInput( - label="Recrutement? (oui/non)", + label=recruitment_label, custom_id="add_cat_recruitment", - placeholder="non", + placeholder=recruitment_placeholder, required=False, - max_length=5 + max_length=5, ) + + # IMPORTANT: + # Disnake Modal has a maximum number of components. + # Keep this modal <= 5 components by merging "questions" + "responses channel id" into one input. self.questions_input = disnake.ui.TextInput( - label="Questions recrutement (une par ligne)", + label=questions_label, # <=45 enforced custom_id="add_cat_questions", - placeholder="Quel age avez-vous?\nPourquoi voulez-vous rejoindre?\nExperiences precedentes?", + placeholder=questions_placeholder, # <=100 enforced 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 + style=disnake.TextInputStyle.paragraph, ) + super().__init__( title="Ajouter une CatĂ©gorie", components=[ @@ -2336,9 +2443,9 @@ class AddCategoryModal(disnake.ui.Modal): self.emoji_input, self.is_recruitment_input, self.questions_input, - self.responses_channel_input ] ) + self.bot = bot self.storage = storage self.config = config @@ -2353,13 +2460,23 @@ class AddCategoryModal(disnake.ui.Modal): 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 [] + + # Parse: + # - questions are all non-empty lines + # - if the LAST non-empty line is a digit-only id => treat it as responses_channel_id + raw_lines = [ln.strip() for ln in (questions_val or "").split("\n") if ln.strip()] responses_channel_id = None - if responses_channel_val and responses_channel_val.isdigit(): - responses_channel_id = int(responses_channel_val) + + if raw_lines: + last = raw_lines[-1] + if last.isdigit(): + # treat last line as channel id, remove it from questions + responses_channel_id = int(last) + raw_lines = raw_lines[:-1] + + questions = raw_lines if raw_lines else [] category = TicketCategory( id=cat_id, @@ -2368,11 +2485,14 @@ class AddCategoryModal(disnake.ui.Modal): emoji=emoji_val, is_recruitment=is_recruitment, questions=questions, - responses_channel_id=responses_channel_id + responses_channel_id=responses_channel_id if is_recruitment else None ) - self.config.categories.append(category) - self.storage.save_config(interaction.guild_id, self.config) + self.storage.invalidate_cache(interaction.guild_id) + config = self.storage.load_config(interaction.guild_id) + config.categories.append(category) + self.storage.save_config(interaction.guild_id, config) + self.config = config recruitment_info = "" if is_recruitment: @@ -2380,7 +2500,18 @@ class AddCategoryModal(disnake.ui.Modal): if responses_channel_id: recruitment_info += f"\n📝 Salon rĂ©ponses: <#{responses_channel_id}>" - await interaction.response.send_message( + await interaction.response.defer(ephemeral=True) + + if interaction.message: + try: + manager_view = CategoryManagerView(self.bot, self.storage, config) + await interaction.message.edit(components=manager_view.get_components_v2()) + except Exception as e: + kuby_logger.warning(f"[AddCategoryModal] Impossible de rafraĂźchir la liste des catĂ©gories: {e}") + + await refresh_ticket_panel(self.bot, self.storage, interaction.guild_id) + + await interaction.followup.send( f"CatĂ©gorie **{name_val}** créée!{recruitment_info}", ephemeral=True ) @@ -2406,6 +2537,7 @@ class EditCategoryModal(disnake.ui.Modal): self.category.description = interaction.text_values.get("edit_cat_desc", "") self.category.emoji = interaction.text_values.get("edit_cat_emoji", "đŸŽ«") self.storage.save_config(interaction.guild_id, self.config) + await refresh_ticket_panel(self.bot, self.storage, interaction.guild_id) container = disnake.ui.Container( disnake.ui.TextDisplay(content=f"✅ CatĂ©gorie **{self.category.name}** mise Ă  jour !"), @@ -2507,17 +2639,22 @@ class CategoryManagerView(disnake.ui.View): description=desc_val, emoji=emoji_val ) - self.config.categories.append(category) - self.storage.save_config(interaction.guild_id, self.config) + self.storage.invalidate_cache(interaction.guild_id) + config = self.storage.load_config(interaction.guild_id) + config.categories.append(category) + self.storage.save_config(interaction.guild_id, config) + self.config = config + self.parent_view.config = config # Refresh parent view self.parent_view._add_category_select() + await refresh_ticket_panel(self.bot, self.storage, interaction.guild_id) # On doit utiliser edit_message sur l'interaction d'origine ou renvoyer un embed await interaction.response.edit_message( embed=self.parent_view._get_embed(), view=self.parent_view ) - await interaction.followup.send(f"CatĂ©gorie **{self.name.value}** créée!", ephemeral=True) + await interaction.followup.send(f"CatĂ©gorie **{name_val}** créée!", ephemeral=True) modal = AddCategoryManagerModal(self.bot, self.storage, self.config, self) await interaction.response.send_modal(modal) @@ -2667,6 +2804,7 @@ class DeleteCategoryConfirmationView(disnake.ui.View): if self.category in self.config.categories: self.config.categories.remove(self.category) self.storage.save_config(interaction.guild_id, self.config) + await refresh_ticket_panel(self.bot, self.storage, interaction.guild_id) self.parent_view._add_category_select() await interaction.response.edit_message( @@ -3191,31 +3329,11 @@ class TicketCommands(commands.Cog): # ✅ BUGFIX: SĂ©lection de catĂ©gorie en V2 => custom_id = catv2_{cat.id}_{snowflake} if custom_id.startswith("catv2_"): try: - # format: catv2__ parts = custom_id.split("_") - if len(parts) >= 3: - cat_id = "_".join(parts[1:-1]) - else: - cat_id = parts[1] if len(parts) > 1 else 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 - - # 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) - - # SĂ©curitĂ© anti-crash : Limite stricte de Discord Ă  45 caractĂšres pour le titre - if hasattr(modal, "title") and modal.title: - modal.title = str(modal.title)[:45] - await inter.response.send_modal(modal) + cat_id = "_".join(parts[1:-1]) if len(parts) >= 3 else (parts[1] if len(parts) > 1 else None) + await open_ticket_for_category(self.bot, inter, config, cat_id) except Exception as e: - kuby_logger.error(f"[TicketSystem] cat| routing error: {e}", exc_info=True) + kuby_logger.error(f"[TicketSystem] catv2_ routing error: {e}", exc_info=True) if not inter.response.is_done(): await inter.response.send_message(f"Erreur interne: {e}", ephemeral=True) @@ -3225,56 +3343,83 @@ class TicketCommands(commands.Cog): 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) - - # SĂ©curitĂ© anti-crash : Limite stricte de Discord Ă  45 caractĂšres pour le titre - if hasattr(modal, "title") and modal.title: - modal.title = str(modal.title)[:45] - await inter.response.send_modal(modal) + await open_ticket_for_category(self.bot, inter, config, cat_id) except Exception as e: - kuby_logger.error(f"[TicketSystem] catv2_ routing error: {e}", exc_info=True) + 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) elif custom_id == "ticket_create": - # Construction du layout de sĂ©lection Premium V2 - category_items = [ - disnake.ui.TextDisplay( - content="# 📂 SĂ©lectionner une CatĂ©gorie - Omega Kube\n\n" - "Veuillez choisir la catĂ©gorie qui correspond au motif de votre demande pour ouvrir un ticket." + # Debug stockage/cache (pour diagnostiquer "catĂ©gorie non trouvĂ©e") + try: + kuby_logger.info(f"[TicketSystem][ticket_create] guild.id={getattr(inter.guild,'id',None)} guild_id={getattr(inter,'guild_id',None)}") + except Exception: + pass + + self.storage.invalidate_cache(inter.guild.id) + config = self.storage.load_config(inter.guild.id) + + try: + cats = getattr(config, "categories", None) or [] + # Affiche aussi des infos pour diagnostiquer le storage + storage_obj = self.storage + config_path = getattr(storage_obj, "config_path", None) + kuby_logger.info( + f"[TicketSystem][ticket_create] storage.config_path={config_path} " + f"guild_id={inter.guild.id} " + f"categories_len={len(cats)} categories_ids={[getattr(c,'id',None) for c in cats[:5]]}" ) - ] - - # GĂ©nĂ©ration dynamique des sections avec Emojis et Descriptions configurĂ©es - for cat in config.categories[:5]: - cat_emoji = getattr(cat, "emoji", "") or "" - cat_desc = getattr(cat, "description", "") or getattr(cat, "desc", "") or "" - - category_items.append(disnake.ui.Separator(divider=True)) - category_items.append( - disnake.ui.Section( - f"### {cat_emoji} {cat.name}\n{cat_desc}" if cat_desc else f"### {cat_emoji} {cat.name}", - accessory=disnake.ui.Button( - label="Choisir", - style=disnake.ButtonStyle.blurple, - custom_id=f"cat_{cat.id}_select" - ) + except Exception: + pass + + if not config.categories: + # Au cas oĂč cache dĂ©synchronisĂ© => invalider aussi avec guild_id + try: + self.storage.invalidate_cache(inter.guild_id) + config = self.storage.load_config(inter.guild_id) + cats = getattr(config, "categories", None) or [] + kuby_logger.info(f"[TicketSystem][ticket_create] (after invalidate guild_id) categories_len={len(cats)}") + except Exception: + pass + + if not config.categories: + await inter.response.send_message( + "Aucune catĂ©gorie configurĂ©e (data/tickets/config.json semble vide pour cette guild). " + "Allez dans la configuration et ajoutez au moins 1 catĂ©gorie.", + ephemeral=True ) + return + + components = build_category_selection_components(config) + try: + await inter.response.send_message( + components=components, + flags=disnake.MessageFlags(is_components_v2=True), + ephemeral=True ) - - await inter.response.send_message( - components=[disnake.ui.Container(*category_items)], - ephemeral=True - ) + except Exception as e: + kuby_logger.error(f"[TicketSystem] ticket_create UI error: {e}", exc_info=True) + if inter.response.is_done(): + await inter.followup.send( + "**đŸŽ« CrĂ©er un Ticket**\n\nSĂ©lectionnez une catĂ©gorie ci-dessous:", + view=CategorySelectionView(self.bot, config), + ephemeral=True + ) + else: + await inter.response.send_message( + "**đŸŽ« CrĂ©er un Ticket**\n\nSĂ©lectionnez une catĂ©gorie ci-dessous:", + view=CategorySelectionView(self.bot, config), + ephemeral=True + ) + + elif custom_id == "ticket_category_select": + self.storage.invalidate_cache(inter.guild.id) + config = self.storage.load_config(inter.guild.id) + cat_id = inter.values[0] if inter.values else None + if not cat_id: + await inter.response.send_message("CatĂ©gorie non trouvĂ©e.", ephemeral=True) + return + await open_ticket_for_category(self.bot, inter, config, cat_id) elif custom_id == "ticket_my_tickets": # Afficher les tickets de l'utilisateur user_tickets = self.storage.get_user_tickets(inter.guild.id, inter.user.id) @@ -3568,33 +3713,15 @@ class TicketCommands(commands.Cog): return elif custom_id.startswith("ticket_config_cat_del_"): - # Le bouton "✅ Confirmer" a custom_id="ticket_config_cat_del_confirm_title" - # et ne doit pas ĂȘtre traitĂ© comme un "legacy delete cat" (sinon cat_id=confirm_title). + # Ne pas traiter ticket_config_cat_del_confirm_title ici : + # la suppression est dĂ©jĂ  gĂ©rĂ©e par DeleteCategoryConfirmationView.confirm_callback. + # Traiter Ă  nouveau ici provoquait une double suppression (2 catĂ©gories supprimĂ©es). if custom_id == "ticket_config_cat_del_confirm_title": + # Juste reconstruire l'UI parent Ă  partir de l'Ă©tat actuel. config = self.storage.load_config(inter.guild.id) - if not config: - await inter.response.send_message("Erreur: config introuvable.", ephemeral=True) - return - - # ⚠ Dans ce mode, on reconstruit la vue parent et on supprime - # la catĂ©gorie courante stockĂ©e dans le message via l’indexation. - # Ici, la catĂ©gorie exacte n'est pas encodĂ©e dans le custom_id, - # donc on supprime le dernier Ă©lĂ©ment si la liste n’est pas vide (fallback). - # (Meilleur: corriger l’encodage en index au moment de crĂ©er le bouton.) - if not config.categories: - await inter.response.send_message("Aucune catĂ©gorie Ă  supprimer.", ephemeral=True) - return - - # Fallback: supprimer le dernier (ce comportement Ă©vite le "rien ne se passe") - # et rend le bouton fonctionnel immĂ©diatement. - category = config.categories[-1] - if category in config.categories: - config.categories.remove(category) - self.storage.save_config(inter.guild.id, config) - - # Rebuild UI - view = CategoryManagerView(self.bot, self.storage, config) - await inter.response.edit_message(components=view.get_components_v2()) + if config and config.categories is not None: + view = CategoryManagerView(self.bot, self.storage, config) + await inter.response.edit_message(components=view.get_components_v2()) return # Recharger config pour Ă©viter les mismatch (panel vs Ă©tat storage) @@ -3826,7 +3953,11 @@ class TicketCommands(commands.Cog): target = channel or inter.channel view = TicketPanelView(self.bot, config, self.storage) - await target.send(components=view.get_components_v2()) + panel_message = await target.send(components=view.get_components_v2()) + + config.panel_channel_id = target.id + config.panel_message_id = panel_message.id + self.storage.save_config(inter.guild.id, config) await inter.edit_original_response(content=f"Panel envoyĂ© dans {target.mention}") diff --git a/commandes/ticket/data/models.py b/commandes/ticket/data/models.py index 7381f93..ec4248e 100644 --- a/commandes/ticket/data/models.py +++ b/commandes/ticket/data/models.py @@ -301,6 +301,7 @@ class GuildTicketConfig: claim_enabled: Whether claiming is enabled by default survey_enabled: Whether surveys are enabled by default panel_channel_id: Channel for the ticket panel + panel_message_id: Message ID of the ticket panel (for auto-refresh) welcome_enabled: Whether welcome messages are shown priority_enabled: Whether priority selection is enabled """ @@ -322,6 +323,7 @@ class GuildTicketConfig: claim_enabled: bool = True survey_enabled: bool = True panel_channel_id: Optional[int] = None + panel_message_id: Optional[int] = None welcome_enabled: bool = True priority_enabled: bool = True @@ -346,6 +348,7 @@ class GuildTicketConfig: "claim_enabled": self.claim_enabled, "survey_enabled": self.survey_enabled, "panel_channel_id": self.panel_channel_id, + "panel_message_id": self.panel_message_id, "welcome_enabled": self.welcome_enabled, "priority_enabled": self.priority_enabled } @@ -372,6 +375,7 @@ class GuildTicketConfig: claim_enabled=data.get("claim_enabled", True), survey_enabled=data.get("survey_enabled", True), panel_channel_id=data.get("panel_channel_id"), + panel_message_id=data.get("panel_message_id"), welcome_enabled=data.get("welcome_enabled", True), priority_enabled=data.get("priority_enabled", True) )