Kuby/commandes/ticket/views/admin.py

522 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

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

"""
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
)