Première version non terminer du nouveau système de ticket § ajout du système de boutons persistant (non tester)
This commit is contained in:
parent
10173132a2
commit
93e2f9bb44
15 changed files with 3788 additions and 209 deletions
522
commandes/ticket/views/admin.py
Normal file
522
commandes/ticket/views/admin.py
Normal file
|
|
@ -0,0 +1,522 @@
|
|||
"""
|
||||
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
|
||||
)
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue