""" Ticket Admin Views ================== Admin panel views for bulk management and configuration. """ from datetime import datetime from typing import Optional, List, Dict, Any import discord from discord.ui import View, Button, Select, Modal, TextInput from discord import Interaction, ButtonStyle, Color from ..data.models import ( TicketData, TicketCategory, TicketStatus, TicketStats, GuildTicketConfig ) from ..data.storage import get_storage from ..utils.formatters import TicketEmbedFormatter from ..utils.transcript import get_transcript_generator class AdminPanelView(View): """ Main admin panel for ticket management. """ def __init__(self, bot, config: GuildTicketConfig): super().__init__(timeout=None) self.bot = bot self.config = config # Stats button stats_btn = Button( style=ButtonStyle.blurple, label="📊 Statistiques", custom_id="admin_stats" ) stats_btn.callback = self._stats_callback self.add_item(stats_btn) # Bulk close button bulk_close_btn = Button( style=ButtonStyle.red, label="🔴 Fermeture Massive", custom_id="admin_bulk_close" ) bulk_close_btn.callback = self._bulk_close_callback self.add_item(bulk_close_btn) # Config button config_btn = Button( style=ButtonStyle.secondary, label="⚙️ Configuration", custom_id="admin_config" ) config_btn.callback = self._config_callback self.add_item(config_btn) # Clean old tickets button cleanup_btn = Button( style=ButtonStyle.secondary, label="🧹 Nettoyer", custom_id="admin_cleanup" ) cleanup_btn.callback = self._cleanup_callback self.add_item(cleanup_btn) async def _stats_callback(self, interaction: Interaction): """Show statistics""" await interaction.response.defer(ephemeral=True) storage = get_storage() stats = self._calculate_stats(interaction.guild_id) embed = TicketEmbedFormatter.stats_embed(stats, interaction.guild.name) await interaction.followup.send(embed=embed, ephemeral=True) async def _bulk_close_callback(self, interaction: Interaction): """Show bulk close options""" await interaction.response.defer(ephemeral=True) # Get open tickets storage = get_storage() tickets = storage.get_guild_tickets(interaction.guild_id, TicketStatus.OPEN) if not tickets: await interaction.followup.send("Aucun ticket ouvert à fermer.", ephemeral=True) return # Show selection view view = BulkCloseView(self.bot, self.config, tickets) embed = discord.Embed( title="🔴 Fermeture Massive", description=f"Nombre de tickets ouverts: **{len(tickets)}**\n\n" "Sélectionnez les tickets à fermer:", color=Color.red() ) await interaction.followup.send(embed=embed, view=view, ephemeral=True) async def _config_callback(self, interaction: Interaction): """Show configuration options""" await interaction.response.defer(ephemeral=True) view = ConfigPanelView(self.bot, self.config) embed = discord.Embed( title="⚙️ Configuration des Tickets", color=Color.blue() ) embed.add_field(name="Tickets activés", value="✅" if self.config.enabled else "❌", inline=True) embed.add_field(name="Catégories", value=str(len(self.config.categories)), inline=True) embed.add_field(name="Rôles Staff", value=str(len(self.config.staff_roles)), inline=True) embed.add_field(name="Canal Logs", value=f"<#{self.config.log_channel_id}>" if self.config.log_channel_id else "Non configuré", inline=True) embed.add_field(name="Limite par utilisateur", value=str(self.config.max_tickets_per_user), inline=True) embed.add_field(name="Auto-close", value="✅" if self.config.auto_close_enabled else "❌", inline=True) await interaction.followup.send(embed=embed, view=view, ephemeral=True) async def _cleanup_callback(self, interaction: Interaction): """Clean up old closed tickets""" await interaction.response.defer(ephemeral=True) storage = get_storage() removed = await self._cleanup_closed_tickets(interaction.guild_id) await interaction.followup.send( f"✅ {removed} tickets fermés ont été supprimés.", ephemeral=True ) def _calculate_stats(self, guild_id: int) -> TicketStats: """Calculate ticket statistics""" storage = get_storage() tickets = storage.get_guild_tickets(guild_id) stats = TicketStats() stats.total_tickets = len(tickets) total_duration = 0 closed_count = 0 rating_sum = 0 rating_count = 0 for ticket in tickets: # By status if ticket.status == TicketStatus.OPEN: stats.open_tickets += 1 elif ticket.status == TicketStatus.CLAIMED: stats.claimed_tickets += 1 stats.open_tickets += 1 elif ticket.status == TicketStatus.CLOSED: stats.closed_tickets += 1 closed_count += 1 # By category stats.tickets_by_category[ticket.category_id] = \ stats.tickets_by_category.get(ticket.category_id, 0) + 1 # By priority stats.tickets_by_priority[ticket.priority.value] = \ stats.tickets_by_priority.get(ticket.priority.value, 0) + 1 # By day day = ticket.created_at.strftime("%Y-%m-%d") stats.tickets_by_day[day] = stats.tickets_by_day.get(day, 0) + 1 # Duration if ticket.duration_minutes: total_duration += ticket.duration_minutes # Rating if ticket.rating: rating_sum += ticket.rating rating_count += 1 # Averages if closed_count > 0: stats.average_duration_minutes = total_duration / closed_count if rating_count > 0: stats.average_rating = rating_sum / rating_count return stats async def _cleanup_closed_tickets(self, guild_id: int, days: int = 30) -> int: """Remove closed tickets older than X days""" storage = get_storage() tickets = storage.get_guild_tickets(guild_id, TicketStatus.CLOSED) cutoff = datetime.now().timestamp() - (days * 86400) removed = 0 for ticket in tickets: if ticket.closed_at and ticket.closed_at.timestamp() < cutoff: # Delete channel if it exists channel = self.bot.get_channel(ticket.channel_id) if channel: try: await channel.delete() except Exception: pass # Delete ticket data storage.delete_ticket(ticket.channel_id) removed += 1 return removed class BulkCloseView(View): """View for selecting tickets to bulk close""" def __init__(self, bot, config: GuildTicketConfig, tickets: List[TicketData]): super().__init__(timeout=None) # Persistent view self.bot = bot self.config = config self.tickets = tickets self.selected_tickets = set() # Create select with tickets (max 25 per select) options = [] for ticket in tickets[:25]: channel = self.bot.get_channel(ticket.channel_id) channel_name = channel.name if channel else f"ticket_{ticket.channel_id}" options.append( discord.SelectOption( label=channel_name[:100], value=str(ticket.channel_id), description=f"Priorité: {ticket.priority.value}" ) ) if options: select = Select( placeholder="Sélectionnez les tickets à fermer...", options=options, min_values=1, max_values=len(options) ) select.callback = self._select_callback self.add_item(select) # Confirm button confirm_btn = Button( style=ButtonStyle.red, label="Confirmer la fermeture", custom_id="bulk_confirm" ) confirm_btn.callback = self._confirm_callback self.add_item(confirm_btn) async def _select_callback(self, interaction: Interaction): """Handle ticket selection""" self.selected_tickets = set(int(v) for v in interaction.data["values"]) await interaction.response.send_message( f"*{len(self.selected_tickets)} ticket(s) sélectionné(s)*", ephemeral=True ) async def _confirm_callback(self, interaction: Interaction): """Confirm bulk close""" await interaction.response.defer(ephemeral=True) if not self.selected_tickets: await interaction.followup.send("Aucun ticket sélectionné.", ephemeral=True) return storage = get_storage() closed_count = 0 for channel_id in self.selected_tickets: ticket = storage.load_ticket(channel_id) if ticket: ticket.status = TicketStatus.CLOSED ticket.closed_at = datetime.now() storage.save_ticket(ticket) # Update channel channel = interaction.guild.get_channel(channel_id) if channel: # Update permissions overwrites = channel.overwrites.copy() for target, perms in overwrites.items(): if isinstance(target, discord.Member) and target.id != interaction.guild.me.id: overwrites[target] = discord.PermissionOverwrite( read_messages=True, send_messages=False ) await channel.edit( name=f"closed-{channel.name}", overwrites=overwrites ) await channel.send(f"Fermé par bulk close (Admin: {interaction.user.mention})") closed_count += 1 await interaction.followup.send( f"✅ {closed_count} ticket(s) fermé(s).", ephemeral=True ) class ConfigPanelView(View): """Configuration panel view""" def __init__(self, bot, config: GuildTicketConfig): super().__init__(timeout=None) self.bot = bot self.config = config # Toggle enabled toggle_btn = Button( style=ButtonStyle.green if config.enabled else ButtonStyle.red, label="Activer/Désactiver", custom_id="config_toggle" ) toggle_btn.callback = self._toggle_callback self.add_item(toggle_btn) # Add category add_cat_btn = Button( style=ButtonStyle.blurple, label="➕ Ajouter Catégorie", custom_id="config_add_category" ) add_cat_btn.callback = self._add_category_callback self.add_item(add_cat_btn) # Edit roles roles_btn = Button( style=ButtonStyle.secondary, label="👥 Gérer Rôles Staff", custom_id="config_roles" ) roles_btn.callback = self._roles_callback self.add_item(roles_btn) # Toggle auto-close auto_close_btn = Button( style=ButtonStyle.green if config.auto_close_enabled else ButtonStyle.red, label="Auto-close", custom_id="config_auto_close" ) auto_close_btn.callback = self._auto_close_callback self.add_item(auto_close_btn) async def _toggle_callback(self, interaction: Interaction): """Toggle ticket system""" self.config.enabled = not self.config.enabled storage = get_storage() storage.save_config(interaction.guild_id, self.config) status = "activé" if self.config.enabled else "désactivé" await interaction.response.send_message( f"Le système de tickets est maintenant **{status}**.", ephemeral=True ) async def _add_category_callback(self, interaction: Interaction): """Show add category modal""" modal = AddCategoryModal(self.bot, self.config) await interaction.response.send_modal(modal) async def _roles_callback(self, interaction: Interaction): """Show role management""" view = RolesManagementView(self.bot, self.config) embed = discord.Embed( title="👥 Gestion des Rôles Staff", description="Sélectionnez les rôles qui auront accès aux tickets:", color=Color.blue() ) # Show current roles role_mentions = [] for role_id in self.config.staff_roles: role = interaction.guild.get_role(int(role_id)) if role: role_mentions.append(role.mention) embed.add_field( name="Rôles actuels", value=", ".join(role_mentions) if role_mentions else "Aucun", inline=False ) await interaction.response.send_message(embed=embed, view=view, ephemeral=True) async def _auto_close_callback(self, interaction: Interaction): """Toggle auto-close""" self.config.auto_close_enabled = not self.config.auto_close_enabled storage = get_storage() storage.save_config(interaction.guild_id, self.config) status = "activé" if self.config.auto_close_enabled else "désactivé" await interaction.response.send_message( f"L'auto-close est maintenant **{status}**.", ephemeral=True ) class AddCategoryModal(Modal): """Modal for adding a new category""" def __init__(self, bot, config: GuildTicketConfig): super().__init__(title="➕ Nouvelle Catégorie") self.bot = bot self.config = config self.name = TextInput( label="Nom", placeholder="Ex: Support", required=True ) self.add_item(self.name) self.description = TextInput( label="Description", placeholder="Description de la catégorie...", required=False, style=discord.TextStyle.paragraph ) self.add_item(self.description) self.emoji = TextInput( label="Emoji", placeholder="🎫", required=False ) self.add_item(self.emoji) self.welcome_msg = TextInput( label="Message de bienvenue", placeholder="Message affiché quand un ticket est créé...", required=False, style=discord.TextStyle.paragraph ) self.add_item(self.welcome_msg) async def on_submit(self, interaction: Interaction): # Generate ID import uuid cat_id = str(uuid.uuid4())[:8] category = TicketCategory( id=cat_id, name=self.name.value, description=self.description.value or "", emoji=self.emoji.value or "🎫", welcome_message=self.welcome_msg.value or "Merci d'avoir créé un ticket." ) self.config.categories.append(category) if not self.config.default_category: self.config.default_category = cat_id storage = get_storage() storage.save_config(interaction.guild_id, self.config) await interaction.response.send_message( f"Catégorie **{self.name.value}** créée avec succès!", ephemeral=True ) class RolesManagementView(View): """View for managing staff roles""" def __init__(self, bot, config: GuildTicketConfig): super().__init__(timeout=None) self.bot = bot self.config = config # Create select with guild roles guild = bot.guilds[0] if bot.guilds else None options = [] if guild: for role in guild.roles: if role.id != guild.id: # Skip @everyone selected = str(role.id) in config.staff_roles options.append( discord.SelectOption( label=role.name, value=str(role.id), default=selected ) ) if options: select = Select( placeholder="Sélectionnez les rôles staff...", options=options[:25] ) select.callback = self._select_callback self.add_item(select) async def _select_callback(self, interaction: Interaction): """Handle role selection""" selected_ids = set(interaction.data["values"]) # Update config self.config.staff_roles = list(selected_ids) storage = get_storage() storage.save_config(interaction.guild_id, self.config) await interaction.response.send_message( f"*{len(selected_ids)} rôle(s) mis à jour.*", ephemeral=True )