Fix du système de permissions

This commit is contained in:
Mathis 2026-01-14 21:32:20 +01:00
parent dfec6b2418
commit c330b9a1cc
48 changed files with 4229 additions and 243 deletions

18
bot.py
View file

@ -1,4 +1,6 @@
import discord
from commandes.ticket.utils.permissions import can_access_config
from commandes.ticket.data.storage import get_storage
from discord.ext import commands
import os
import asyncio
@ -78,6 +80,19 @@ class MyBot(commands.Bot):
if await self.is_owner(ctx.author):
return True
# 1b. Bypass pour les commandes de config tickets
# Si la commande est une commande ticket et l'utilisateur a les perms ticket
if ctx.command and (ctx.command.name in ["ticketconfig", "ticketsetup", "configticket"]):
try:
storage = get_storage()
config = storage.load_config(ctx.guild.id)
if can_access_config(ctx, config):
return True
except Exception as e:
kuby_logger.error(f"Error checking ticket permissions in global whitelist: {e}")
# Fallthrough to normal whitelist check if error
# 2. Récupération du Cog
whitelist_cog = self.get_cog("WhitelistMonitor")
if not whitelist_cog:
@ -189,5 +204,4 @@ class MyBot(commands.Bot):
return # Déjà géré dans le whitelist_check
kuby_logger.error(f"Command error in {ctx.command}: {str(error)}", exc_info=True)
bot = MyBot()
bot.run(os.getenv("DISCORD_TOKEN"))
bot = MyBot()

View file

@ -0,0 +1,67 @@
# Ticket Close Confirmation and Delay - Implementation Plan
## Objective
Add confirmation dialog and waiting delay before ticket closure.
## Changes to make
### 1. Create `CloseConfirmationView` class
- Yes/No confirmation buttons
- Cancel option
- Shows before closing
### 2. Create `CloseDelayView` class
- Countdown display (5 seconds)
- Progress steps:
- 📄 Génération du transcript...
- 📝 Sauvegarde des données...
- 🗑️ Suppression du canal...
- Cancel option during delay
### 3. Modify `TicketManagementView._close_callback`
- Show confirmation view first
- After Yes, show delay view with countdown
- After delay completes, proceed with closing
## Files to modify
- `commandes/ticket/__init__.py`
## Status
- [x] Add CloseConfirmationView class ✅
- [x] Add CloseDelayView class ✅
- [x] Modify TicketManagementView._close_callback ✅
- [x] Add cancel button to CloseDelayView ✅
- [x] Test the flow ✅
## Implementation Details
### CloseConfirmationView
```python
class CloseConfirmationView(discord.ui.View):
"""Confirmation dialog before closing ticket"""
def __init__(self, bot, storage, config, ticket):
super().__init__(timeout=60)
# Yes button (green)
# No button (red) - cancels and removes message
```
### CloseDelayView
```python
class CloseDelayView(discord.ui.View):
"""Countdown and progress before ticket deletion"""
def __init__(self, bot, storage, config, ticket, close_reason):
super().__init__(timeout=None)
# Disable all buttons during countdown
# Update embed with countdown
# After delay, call actual close function
```
## Flow
1. User clicks "Fermer"
2. Show confirmation embed with Yes/No buttons
3. If Yes → Show delay view with countdown
4. After delay → Close ticket and delete channel
5. If No or timeout → Remove confirmation message

View file

@ -0,0 +1,23 @@
# TODO - Correction du système de tickets pour les rôles staff/config
## Problème
Les utilisateurs avec les rôles staff et config ne peuvent pas interagir avec le système de tickets.
## Cause
La méthode `_is_whitelisted` retourne `False` quand le cog WhitelistMonitor n'est pas disponible, les utilisateurs bloquant même staff/config.
## Plan de correction
### Étape 1: Corriger TicketPanelView._is_whitelisted()
- [ ] Modifier pour que les rôles staff/config soient toujours autorisés
### Étape 2: Corriger CategorySelectionView._is_whitelisted()
- [ ] Modifier pour que les rôles staff/config soient toujours autorisés
### Étape 3: Vérifier les autres vues
- [ ] Vérifier TicketManagementView._is_staff()
## État d'avancement
- [x] TicketPanelView._is_whitelisted() corrigé
- [x] CategorySelectionView._is_whitelisted() corrigé
- [x] Tests de vérification

View file

@ -12,6 +12,7 @@ Fonctionnalités:
import os
import sys
import io
import asyncio
from datetime import datetime, timedelta
from typing import List, Dict, Any, Optional
from collections import defaultdict
@ -21,6 +22,7 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspa
import discord
from discord.ext import commands, tasks
import traceback
from PIL import Image, ImageDraw, ImageFont
from .data.storage import get_storage
@ -34,6 +36,8 @@ from .data.models import (
)
from .utils.formatters import TicketEmbedFormatter
from .staff_analytics import StaffAnalytics, StaffStatsView
from .utils.permissions import is_staff, is_whitelisted, can_access_config
from src.logger import kuby_logger
class TicketPanelView(discord.ui.View):
@ -93,6 +97,9 @@ class TicketPanelView(discord.ui.View):
async def _create_callback(self, interaction: discord.Interaction):
"""Affiche les catégories pour créer un ticket"""
# Reload config to ensure we have latest permissions/categories
self.config = self.storage.load_config(interaction.guild_id)
if not self._is_whitelisted(interaction):
await interaction.response.send_message("❌ Vous n'êtes pas autorisé à utiliser cette fonctionnalité. Veuillez demander à être ajouté à la whitelist.", ephemeral=True)
return
@ -125,6 +132,9 @@ class TicketPanelView(discord.ui.View):
async def _my_tickets_callback(self, interaction: discord.Interaction):
"""Affiche les tickets de l'utilisateur"""
# Reload config
self.config = self.storage.load_config(interaction.guild_id)
if not self._is_whitelisted(interaction):
await interaction.response.send_message("❌ Vous n'êtes pas autorisé à utiliser cette fonctionnalité. Veuillez demander à être ajouté à la whitelist.", ephemeral=True)
return
@ -155,6 +165,9 @@ class TicketPanelView(discord.ui.View):
async def _analytics_callback(self, interaction: discord.Interaction):
"""Affiche les analytics (staff uniquement)"""
# Reload config
self.config = self.storage.load_config(interaction.guild_id)
if not self._is_staff(interaction):
await interaction.response.send_message("❌ Cette fonctionnalité est réservée au staff.", ephemeral=True)
return
@ -233,6 +246,9 @@ class TicketPanelView(discord.ui.View):
async def _config_callback(self, interaction: discord.Interaction):
"""Ouvre le panel de configuration (admin ou rôles config uniquement)"""
# Reload config
self.config = self.storage.load_config(interaction.guild_id)
if not self._is_whitelisted(interaction):
await interaction.response.send_message("❌ Vous n'êtes pas autorisé à utiliser cette fonctionnalité. Veuillez demander à être ajouté à la whitelist.", ephemeral=True)
return
@ -253,54 +269,14 @@ class TicketPanelView(discord.ui.View):
def _is_staff(self, interaction: discord.Interaction) -> bool:
"""Vérifie si l'utilisateur est staff"""
# Admin peut toujours accéder
if interaction.user.guild_permissions.administrator:
return True
guild = interaction.guild
user = interaction.user
for role_id in self.config.staff_roles:
role = guild.get_role(int(role_id))
if role and role in user.roles:
return True
return False
def _is_admin(self, interaction: discord.Interaction) -> bool:
"""Vérifie si l'utilisateur est administrateur"""
return interaction.user.guild_permissions.administrator
return is_staff(interaction, self.config)
def _can_access_config(self, interaction: discord.Interaction) -> bool:
"""Vérifie si l'utilisateur peut accéder à la configuration"""
# Admin peut toujours accéder
if interaction.user.guild_permissions.administrator:
return True
# Vérifier les rôles staff
guild = interaction.guild
user = interaction.user
for role_id in self.config.staff_roles:
role = guild.get_role(int(role_id))
if role and role in user.roles:
return True
return False
return can_access_config(interaction, self.config)
def _is_whitelisted(self, interaction: discord.Interaction) -> bool:
"""Vérifie si l'utilisateur est whitelisté ou a les permissions staff/config"""
# Staff members and config roles can always access
if self._is_staff(interaction) or self._can_access_config(interaction):
return True
return is_whitelisted(interaction, self.config, self.bot)
# Get whitelist monitor cog
whitelist_cog = self.bot.get_cog("WhitelistMonitor")
if not whitelist_cog:
return False # Deny if whitelist system not available
return whitelist_cog.is_whitelisted(interaction.guild.id, interaction.user.id)
def _calculate_stats(self, guild_id: int) -> TicketStats:
"""Calcule les statistiques"""
storage = get_storage()
@ -340,145 +316,77 @@ class TicketPanelView(discord.ui.View):
return stats
class CategoryButton(discord.ui.Button):
def __init__(self, category, view_instance):
super().__init__(
style=discord.ButtonStyle.blurple,
label=f"{category.emoji} {category.name}",
custom_id=f"cat_{category.id}_{view_instance.id_gen}"
)
self.category = category
self.view_instance = view_instance
# We need unique custom_ids even for ephemeral to avoid any weird collisions/caching
async def callback(self, interaction: discord.Interaction):
await self.view_instance._on_category_select(interaction, self.category.id)
class CategorySelectionView(discord.ui.View):
"""Sélection de catégorie pour créer un ticket"""
def __init__(self, bot, config: GuildTicketConfig):
super().__init__(timeout=None)
super().__init__(timeout=300) # 5 minutes timeout for ephemeral menu
self.bot = bot
self.config = config
self.id_gen = int(datetime.now().timestamp())
# Ajouter boutons de catégories
for category in config.categories[:5]:
btn = discord.ui.Button(
style=discord.ButtonStyle.blurple,
label=f"{category.emoji} {category.name}",
custom_id=f"cat_{category.id}"
)
btn.callback = self._category_callback(category.id)
self.add_item(btn)
self.add_item(CategoryButton(category, self))
def _is_staff(self, interaction: discord.Interaction) -> bool:
"""Vérifie si l'utilisateur est staff"""
# Admin peut toujours accéder
if interaction.user.guild_permissions.administrator:
return True
guild = interaction.guild
user = interaction.user
for role_id in self.config.staff_roles:
role = guild.get_role(int(role_id))
if role and role in user.roles:
return True
return False
return is_staff(interaction, self.config)
def _can_access_config(self, interaction: discord.Interaction) -> bool:
"""Vérifie si l'utilisateur peut accéder à la configuration"""
# Admin peut toujours accéder
if interaction.user.guild_permissions.administrator:
return True
# Vérifier les rôles staff
guild = interaction.guild
user = interaction.user
for role_id in self.config.staff_roles:
role = guild.get_role(int(role_id))
if role and role in user.roles:
return True
return False
return can_access_config(interaction, self.config)
def _is_whitelisted(self, interaction: discord.Interaction) -> bool:
"""Vérifie si l'utilisateur est whitelisté ou a les permissions staff/config"""
# Staff members and config roles can always access
if self._is_staff(interaction) or self._can_access_config(interaction):
return True
# Get whitelist monitor cog
whitelist_cog = self.bot.get_cog("WhitelistMonitor")
if not whitelist_cog:
return False # Deny if whitelist system not available
return whitelist_cog.is_whitelisted(interaction.guild.id, interaction.user.id)
def _is_staff(self, interaction: discord.Interaction) -> bool:
"""Vérifie si l'utilisateur est staff"""
# Admin peut toujours accéder
if interaction.user.guild_permissions.administrator:
return True
guild = interaction.guild
user = interaction.user
for role_id in self.config.staff_roles:
role = guild.get_role(int(role_id))
if role and role in user.roles:
return True
return False
def _can_access_config(self, interaction: discord.Interaction) -> bool:
"""Vérifie si l'utilisateur peut accéder à la configuration"""
# Admin peut toujours accéder
if interaction.user.guild_permissions.administrator:
return True
# Vérifier les rôles staff
guild = interaction.guild
user = interaction.user
for role_id in self.config.staff_roles:
role = guild.get_role(int(role_id))
if role and role in user.roles:
return True
return False
def _is_whitelisted(self, interaction: discord.Interaction) -> bool:
"""Vérifie si l'utilisateur est whitelisté ou a les permissions staff/config"""
# Staff members and config roles can always access
if self._is_staff(interaction) or self._can_access_config(interaction):
return True
# Get whitelist monitor cog
whitelist_cog = self.bot.get_cog("WhitelistMonitor")
if not whitelist_cog:
return False # Deny if whitelist system not available
return whitelist_cog.is_whitelisted(interaction.guild.id, interaction.user.id)
def _category_callback(self, category_id: str):
async def callback(interaction: discord.Interaction):
await self._on_category_select(interaction, category_id)
return callback
return is_whitelisted(interaction, self.config, self.bot)
async def _on_category_select(self, interaction: discord.Interaction, category_id: str):
"""Handle category selection"""
if not self._is_whitelisted(interaction):
await interaction.response.send_message("❌ Vous n'êtes pas autorisé à utiliser cette fonctionnalité. Veuillez demander à être ajouté à la whitelist.", ephemeral=True)
return
try:
if not self._is_whitelisted(interaction):
await interaction.response.send_message("❌ Vous n'êtes pas autorisé à utiliser cette fonctionnalité. Veuillez demander à être ajouté à la whitelist.", ephemeral=True)
return
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)
if len(user_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
# Show priority modal
modal = TicketPriorityModal(self.bot, self.config, category)
await interaction.response.send_modal(modal)
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)
if len(user_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
# Show priority modal
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)
try:
if not interaction.response.is_done():
await interaction.response.send_message(f"❌ Erreur interne: {e}", ephemeral=True)
except:
pass
class TicketPriorityModal(discord.ui.Modal):
@ -598,7 +506,18 @@ class TicketPriorityModal(discord.ui.Modal):
# Add management buttons
view = TicketManagementView(self.bot, storage, self.config, ticket_data)
await ticket_channel.send(content=user.mention, embed=embed, view=view)
# Prepare mentions
mentions = [user.mention]
for role_id in self.config.staff_roles:
mentions.append(f"<@&{role_id}>")
for user_id in self.config.staff_users:
mentions.append(f"<@{user_id}>")
mention_content = " ".join(mentions)
# Add management buttons
view = TicketManagementView(self.bot, storage, self.config, ticket_data)
await ticket_channel.send(content=mention_content, embed=embed, view=view)
# Log
if self.config.log_channel_id:
@ -667,9 +586,38 @@ class TicketManagementView(discord.ui.View):
self.add_item(reopen_btn)
async def _close_callback(self, interaction: discord.Interaction):
"""Ferme le ticket"""
modal = CloseTicketModal(self.bot, self.storage, self.config, self.ticket)
await interaction.response.send_modal(modal)
"""Demande confirmation avant de fermer le ticket"""
# Vérifier si l'utilisateur peut fermer le ticket
can_close = (
interaction.user.id == self.ticket.user_id or
self._is_staff(interaction)
)
if not can_close:
await interaction.response.send_message("Vous ne pouvez pas fermer ce ticket.", ephemeral=True)
return
# Créer l'embed de confirmation
embed = discord.Embed(
title="🔒 Confirmer la Fermeture",
description="Êtes-vous sûr de vouloir fermer ce ticket?\n\n"
"Cette action est irréversible et le salon sera supprimé.",
color=discord.Color.orange()
)
# Ajouter les informations du ticket
category = self.config.get_category(self.ticket.category_id)
duration = self.ticket.duration_minutes
embed.add_field(name="📁 Catégorie", value=category.name if category else "N/A", inline=True)
embed.add_field(name="⏱️ Durée", value=f"{duration:.1f} min" if duration else "N/A", inline=True)
embed.add_field(name="📊 Statut", value=self.ticket.status.value.capitalize(), inline=True)
embed.set_footer(text="Cliquez sur Confirmer pour fermer ou Annuler pour annuler l'opération")
# Afficher la vue de confirmation
view = CloseConfirmationView(self.bot, self.storage, self.config, self.ticket)
await interaction.response.send_message(embed=embed, view=view, ephemeral=True)
async def _transcript_callback(self, interaction: discord.Interaction):
"""Génère le transcript"""
@ -778,6 +726,69 @@ class TicketManagementView(discord.ui.View):
await interaction.channel.send(embed=embed)
await interaction.response.send_message("Ticket rouvert!", ephemeral=True)
def _is_staff(self, interaction: discord.Interaction) -> bool:
return is_staff(interaction, self.config)
def _is_staff_member(self, member) -> bool:
# Create a fake interaction-like check or manual check
if member.guild_permissions.administrator:
return True
if member.id in self.config.staff_users:
return True
for role_id in self.config.staff_roles:
role = member.guild.get_role(int(role_id))
if role and role in member.roles:
return True
return False
class CloseConfirmationView(discord.ui.View):
"""Demande de confirmation avant la fermeture du ticket"""
def __init__(self, bot, storage, config: GuildTicketConfig, ticket: TicketData):
super().__init__(timeout=60)
self.bot = bot
self.storage = storage
self.config = config
self.ticket = ticket
@discord.ui.button(label="✅ Confirmer", style=discord.ButtonStyle.green, custom_id="close_confirm")
async def confirm_callback(self, interaction: discord.Interaction, button: discord.ui.Button):
"""L'utilisateur confirme la fermeture"""
# Vérifier les permissions
can_close = (
interaction.user.id == self.ticket.user_id or
self._is_staff(interaction)
)
if not can_close:
await interaction.response.send_message("Vous ne pouvez pas fermer ce ticket.", ephemeral=True)
return
# Show the delay view with countdown
embed = discord.Embed(
title="⏳ Fermeture du Ticket",
description="Le ticket va être fermé dans quelques secondes...\n\n"
"📄 Génération du transcript en cours...\n"
"📝 Envoi aux logs...\n"
"🗑️ Suppression du canal...",
color=discord.Color.orange()
)
view = CloseDelayView(self.bot, self.storage, self.config, self.ticket)
await interaction.response.send_message(embed=embed, view=view, ephemeral=True)
# Start the countdown
await view.start_countdown(interaction)
@discord.ui.button(label="❌ Annuler", style=discord.ButtonStyle.red, custom_id="close_cancel")
async def cancel_callback(self, interaction: discord.Interaction, button: discord.ui.Button):
"""L'utilisateur annule la fermeture"""
await interaction.response.defer()
await interaction.delete_original_response()
def _is_staff(self, interaction: discord.Interaction) -> bool:
guild = interaction.guild
user = interaction.user
@ -786,7 +797,153 @@ class TicketManagementView(discord.ui.View):
if role and role in user.roles:
return True
return user.guild_permissions.administrator
class CloseDelayView(discord.ui.View):
"""Affiche le délai d'attente avant suppression du ticket"""
def __init__(self, bot, storage, config: GuildTicketConfig, ticket: TicketData, close_reason: str = ""):
super().__init__(timeout=300)
self.bot = bot
self.storage = storage
self.config = config
self.ticket = ticket
self.close_reason = close_reason
self.category = config.get_category(ticket.category_id)
# Ajouter le bouton Annuler
cancel_btn = discord.ui.Button(
label="❌ Annuler",
style=discord.ButtonStyle.red,
custom_id="delay_cancel"
)
cancel_btn.callback = self._cancel_callback
self.add_item(cancel_btn)
async def start_countdown(self, interaction: discord.Interaction):
"""Démarre le compte à rebours"""
# Désactiver le bouton Annuler
for item in self.children:
item.disabled = True
await interaction.edit_original_response(view=self)
# Étapes du processus
steps = [
("⏳ Préparation...", 1),
("📄 Génération du transcript...", 1),
("📝 Sauvegarde des données...", 1),
("🗑️ Suppression du canal...", 1)
]
for step_text, delay in steps:
embed = discord.Embed(
title="🔒 Fermeture du Ticket",
description=f"**{step_text}**\n\n"
f"Merci pour votre confiance!",
color=discord.Color.orange()
)
embed.set_footer(text=f"Ticket #{self.ticket.channel_id}")
await interaction.edit_original_response(embed=embed)
await asyncio.sleep(delay)
# Procéder à la fermeture
await self._close_ticket(interaction)
async def _close_ticket(self, interaction: discord.Interaction):
"""Effectue la fermeture réelle du ticket"""
# Collect messages for transcript
transcript_file = None
try:
messages = []
async for msg in interaction.channel.history(limit=1000, oldest_first=True):
if msg.author.bot:
continue
messages.append({
"id": str(msg.id),
"author_id": msg.author.id,
"author_name": str(msg.author),
"content": msg.content,
"timestamp": msg.created_at.isoformat(),
"is_staff": self._is_staff_member(msg.author)
})
from .utils.transcript import get_transcript_generator
transcript_gen = get_transcript_generator()
transcript_file = transcript_gen.save_transcript(self.ticket, messages, format="html")
except Exception as e:
print(f"Error generating transcript: {e}")
# Close ticket
self.ticket.status = TicketStatus.CLOSED
self.ticket.closed_at = datetime.now()
if self.close_reason:
self.ticket.feedback = self.close_reason
self.storage.save_ticket(self.ticket)
duration = self.ticket.duration_minutes
# Send transcript to log channel
if self.config.log_channel_id and transcript_file and os.path.exists(transcript_file):
try:
log_channel = interaction.guild.get_channel(self.config.log_channel_id)
if log_channel:
log_embed = discord.Embed(
title="📄 Ticket Fermé - Transcript",
description=f"Ticket #{self.ticket.channel_id} fermé par {interaction.user.mention}",
color=discord.Color.red(),
timestamp=datetime.now()
)
ticket_creator = interaction.guild.get_member(self.ticket.user_id)
creator_mention = ticket_creator.mention if ticket_creator else f"Utilisateur {self.ticket.user_id}"
log_embed.add_field(name="Créateur", value=creator_mention, inline=True)
log_embed.add_field(name="Catégorie", value=self.category.name if self.category else "N/A", inline=True)
log_embed.add_field(name="Durée", value=f"{duration:.1f} min" if duration else "N/A", inline=True)
if self.close_reason:
log_embed.add_field(name="Raison", value=self.close_reason, inline=False)
file = discord.File(transcript_file, filename=os.path.basename(transcript_file))
await log_channel.send(embed=log_embed, file=file)
except Exception as e:
print(f"Error sending transcript to log channel: {e}")
# Delete the ticket channel
try:
await interaction.channel.delete(reason=f"Ticket fermé par {interaction.user}")
except Exception as e:
print(f"Error deleting channel: {e}")
embed = discord.Embed(
title="❌ Erreur",
description="Le ticket a été fermé mais la suppression du salon a échoué.",
color=discord.Color.red()
)
await interaction.channel.send(embed=embed)
async def _cancel_callback(self, interaction: discord.Interaction):
"""Annule la fermeture du ticket pendant le délai"""
await interaction.response.defer()
await interaction.delete_original_response()
# Envoyer un message de confirmation d'annulation
embed = discord.Embed(
title="❌ Fermeture Annulée",
description="La fermeture du ticket a été annulée.",
color=discord.Color.red()
)
await interaction.channel.send(embed=embed, delete_after=5)
def _is_staff(self, interaction: discord.Interaction) -> bool:
guild = interaction.guild
user = interaction.user
for role_id in self.config.staff_roles:
role = guild.get_role(int(role_id))
if role and role in user.roles:
return True
return user.guild_permissions.administrator
def _is_staff_member(self, member) -> bool:
guild = member.guild
for role_id in self.config.staff_roles:
@ -1494,14 +1651,23 @@ class ConfigPanelView(discord.ui.View):
add_cat_btn.callback = self._add_cat_callback
self.add_item(add_cat_btn)
# Staff roles
roles_btn = discord.ui.Button(
# Staff permissions
staff_btn = discord.ui.Button(
style=discord.ButtonStyle.secondary,
label="Rôles Staff",
custom_id="config_roles"
label="Permissions Staff",
custom_id="config_staff_perms"
)
roles_btn.callback = self._roles_callback
self.add_item(roles_btn)
staff_btn.callback = self._staff_perms_callback
self.add_item(staff_btn)
# Config permissions
config_perms_btn = discord.ui.Button(
style=discord.ButtonStyle.secondary,
label="Permissions Config",
custom_id="config_config_perms"
)
config_perms_btn.callback = self._config_perms_callback
self.add_item(config_perms_btn)
# Log channel
log_btn = discord.ui.Button(
@ -1529,24 +1695,47 @@ class ConfigPanelView(discord.ui.View):
modal = AddCategoryModal(self.bot, self.storage, self.config)
await interaction.response.send_modal(modal)
async def _roles_callback(self, interaction: discord.Interaction):
view = StaffRolesView(self.bot, self.storage, self.config, interaction.guild)
async def _staff_perms_callback(self, interaction: discord.Interaction):
view = PermissionConfigView(self.bot, self.storage, self.config, interaction.guild, "staff")
embed = discord.Embed(
title="Rôles Staff",
description="Sélectionnez les rôles qui peuvent gérer les tickets:",
title="Permissions Staff",
description="Configurez qui peut gérer les tickets (répondre, fermer, etc.)",
color=discord.Color.blue()
)
self._add_current_perms_fields(embed, self.config.staff_roles, self.config.staff_users, interaction.guild)
await interaction.response.send_message(embed=embed, view=view, ephemeral=True)
async def _config_perms_callback(self, interaction: discord.Interaction):
view = PermissionConfigView(self.bot, self.storage, self.config, interaction.guild, "config")
embed = discord.Embed(
title="Permissions Configuration",
description="Configurez qui peut modifier les paramètres du bot (/ticketconfig)",
color=discord.Color.orange()
)
self._add_current_perms_fields(embed, self.config.config_roles, self.config.config_users, interaction.guild)
await interaction.response.send_message(embed=embed, view=view, ephemeral=True)
def _add_current_perms_fields(self, embed, roles, users, guild):
role_mentions = []
for role_id in self.config.staff_roles:
role = interaction.guild.get_role(int(role_id))
for role_id in roles:
role = guild.get_role(int(role_id))
if role:
role_mentions.append(role.mention)
embed.add_field(name="Actuels", value=", ".join(role_mentions) if role_mentions else "Aucun", inline=False)
await interaction.response.send_message(embed=embed, view=view, ephemeral=True)
user_mentions = []
for user_id in users:
user = guild.get_member(user_id)
if user:
user_mentions.append(user.mention)
else:
user_mentions.append(f"<@{user_id}>")
embed.add_field(name="Rôles", value=", ".join(role_mentions) if role_mentions else "Aucun", inline=False)
embed.add_field(name="Utilisateurs", value=", ".join(user_mentions) if user_mentions else "Aucun", inline=False)
async def _log_channel_callback(self, interaction: discord.Interaction):
modal = SetChannelModal(self.bot, self.storage, self.config, "log")
@ -1741,64 +1930,232 @@ class StaffRolesModal(discord.ui.Modal):
await interaction.response.send_message("Une erreur inattendue s'est produite lors de la configuration des rôles.", ephemeral=True)
class StaffRolesView(discord.ui.View):
"""Sélection des rôles staff (alternative avec select menu)"""
class PermissionConfigView(discord.ui.View):
"""View to configure permissions (Staff/Config Roles & Users)"""
def __init__(self, bot, storage, config: GuildTicketConfig, guild: discord.Guild):
def __init__(self, bot, storage, config: GuildTicketConfig, guild: discord.Guild, config_type: str = "staff"):
super().__init__(timeout=None)
self.bot = bot
self.storage = storage
self.config = config
self.guild = guild
# Le select sera populated dans le callback
self._build_select(guild)
self.config_type = config_type # "staff" or "config"
# Add components
self._add_role_select()
self._add_user_select()
def _build_select(self, guild: discord.Guild):
if not guild:
return
def _add_role_select(self):
select = discord.ui.RoleSelect(
placeholder=f"Sélectionnez les rôles {self.config_type}",
min_values=0,
max_values=25,
row=0
)
select.callback = self._role_callback
self.add_item(select)
def _add_user_select(self):
select = discord.ui.UserSelect(
placeholder=f"Sélectionnez les utilisateurs {self.config_type}",
min_values=0,
max_values=25,
row=1
)
select.callback = self._user_callback
self.add_item(select)
async def _role_callback(self, interaction: discord.Interaction):
# Determine which list to update
if self.config_type == "staff":
target_list = self.config.staff_roles
else:
target_list = self.config.config_roles
options = []
for role in guild.roles:
if role.id != guild.id: # Exclude @everyone
options.append(
discord.SelectOption(
label=role.name,
value=str(role.id),
default=str(role.id) in self.config.staff_roles
)
)
# Get new selected IDs
# Note: RoleSelect/UserSelect returns the objects, not IDs directly in values sometimes depending on lib version,
# but interaction.data['values'] is reliable for IDs if we want raw.
# However, interaction.values which is bound to callback by library usually contains the objects.
# Let's use the view item's values.
if options:
select = discord.ui.Select(
placeholder="Sélectionnez les rôles staff...",
options=options[:25],
min_values=0,
max_values=min(25, len(options))
)
select.callback = self._select_callback
self.add_item(select)
async def _select_callback(self, interaction: discord.Interaction):
selected = list(interaction.data.get("values", []))
self.config.staff_roles = selected
# Actually standard lib behavior: interaction.data['values'] contains IDs.
# But let's use the argument 'interaction' and the component.
# Better: use the values from the specific component that triggered this
# But since we have separate callbacks we can just read interaction.data
selected_ids = [str(r.id) for r in interaction.data.get('values', [])] # But wait, select menu interactions return objects in the view item's values property if mapped? NO, interaction.values is for string selects.
# For Entity Select Menus (Role/User), interaction.data['values'] contains IDs.
# BUT, discord.py 2.0 uses select.values for string selects.
# For RoleSelect, values are the list of Role objects.
# Let's try to trust the interaction object's resolution.
# Wait, standard discord.py RoleSelect callback receives interaction.
# The select object itself will have 'values' populated with Role objects after processing.
# But we need to reference the select item.
# Since we added it dynamically, we can't easily reference 'self.role_select'.
# Workaround: Re-read from interaction.data['values'] which are IDs.
values = interaction.data.get('values', [])
# WE NEED TO MERGE with existing? No, select menu usually replaces selection if it shows current selection.
# BUT Discord UI Selects don't show "current selection" natively pre-filled unless we set defaults.
# AND we can only set defaults for StringSelect, NOT for RoleSelect/UserSelect (API Limitation).
# So RoleSelect/UserSelect always start empty.
# This implies we can only ADD/REMOVE or we have to handle it as "Add these", "Remove these"?
# OR we treat valid input as "Append these to existing" or "Replace existing"?
# Given we can't show pre-selected items, "Replace" is dangerous because user can't see what was there.
# "Append" is safer but how to remove?
# Modification: Make it "Toggle" or just "Add/Remove" via specific actions?
# A common pattern is: The select menu creates a list. We Add them.
# But how to remove?
# Maybe we should stick to STRING SELECT for removal if we want to show lists.
# But the user issue is "Can't see all roles".
# Better approach for Entity Selects:
# They are usually used for "Add these items".
# To remove, we might need a separate "Remove" workflow or a StringSelect of *current* items to remove.
# Let's implement: "Add selected roles/users to permission".
# And provide a way to "Clear/Remove" via a StringSelect of *existing* configured items.
# So:
# 1. RoleSelect (Add)
# 2. UserSelect (Add)
# 3. StringSelect (Remove) - Populated with currently configured items.
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)
role_mentions = []
for role_id in selected:
role = interaction.guild.get_role(int(role_id))
if role:
role_mentions.append(role.mention)
embed = discord.Embed(
title="✅ Rôles Staff Mis à Jour",
description=f"**{len(selected)}** rôle(s) configuré(s).",
color=discord.Color.green()
await interaction.response.send_message(
f"✅ **{added_count}** rôle(s) ajouté(s) à la configuration {self.config_type}.",
ephemeral=True
)
embed.add_field(name="Rôles", value=", ".join(role_mentions) if role_mentions else "Aucun", inline=False)
await interaction.response.send_message(embed=embed, ephemeral=True)
# Refresh view to update Remove dropdown if we add it
await self._refresh_view(interaction)
async def _user_callback(self, interaction: discord.Interaction):
if self.config_type == "staff":
target_list = self.config.staff_users
else:
target_list = self.config.config_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)
await interaction.response.send_message(
f"✅ **{added_count}** utilisateur(s) ajouté(s) à la configuration {self.config_type}.",
ephemeral=True
)
await self._refresh_view(interaction)
async def _refresh_view(self, interaction):
# Re-render the view?
# We can't easily edit the ephemeral message with a new view from here without 'response.edit_message'
# But we already responded to interaction.
# So we can followup edit?
# Actually we just want to update the "Remove" select menu.
# Let's add the Remove Select Menu.
self.clear_items()
self._add_role_select()
self._add_user_select()
self._add_remove_select()
await interaction.message.edit(view=self)
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:
roles = self.config.config_roles
users = self.config.config_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
role = self.guild.get_role(int(r_id))
name = role.name if role else f"Role {r_id}"
options.append(discord.SelectOption(
label=f"Role: {name[:90]}",
value=f"r_{r_id}",
emoji="🛡️",
description="Cliquez pour retirer"
))
count += 1
for u_id in users:
if count >= 25: break
user = self.guild.get_member(u_id)
name = user.display_name if user else f"User {u_id}"
options.append(discord.SelectOption(
label=f"User: {name[:90]}",
value=f"u_{u_id}",
emoji="👤",
description="Cliquez pour retirer"
))
count += 1
if options:
select = discord.ui.Select(
placeholder="Sélectionnez pour RETIRER des permissions...",
options=options,
min_values=1,
max_values=min(len(options), 25),
row=2,
custom_id="remove_select"
)
select.callback = self._remove_callback
self.add_item(select)
async def _remove_callback(self, interaction: discord.Interaction):
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 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
if id_val in target_list:
target_list.remove(id_val)
removed_count += 1
self.storage.save_config(interaction.guild_id, self.config)
await interaction.response.send_message(
f"🗑️ **{removed_count}** permission(s) retirée(s).",
ephemeral=True
)
await self._refresh_view(interaction)
class TicketCommands(commands.Cog):
@ -1808,7 +2165,7 @@ class TicketCommands(commands.Cog):
self.bot = bot
self.storage = get_storage()
def cog_unload(self):
async def cog_unload(self):
pass
async def cog_load(self):
@ -1892,23 +2249,16 @@ class TicketCommands(commands.Cog):
"""Ouvre le panel de configuration complet du système de tickets"""
await ctx.defer(ephemeral=True)
# Reload config
config = self.storage.load_config(ctx.guild.id)
# Vérifier si l'utilisateur peut accéder à la configuration
can_access = ctx.author.guild_permissions.administrator
if not can_access:
# Vérifier les rôles staff
for role_id in config.staff_roles:
role = ctx.guild.get_role(int(role_id))
if role and role in ctx.author.roles:
can_access = True
break
if not can_access:
# Vérifier permissions (admin ou config roles/users)
if not can_access_config(ctx, config):
await ctx.send("❌ Cette commande est réservée aux administrateurs ou aux rôles configurés.", ephemeral=True)
return
embed = discord.Embed(
title="⚙️ Configuration du Système de Tickets",
description="Configurez complètement votre système de tickets depuis cette interface.\n\n"

View file

@ -293,7 +293,9 @@ class GuildTicketConfig:
log_channel_id: Optional[int] = None
archive_channel_id: Optional[int] = None
staff_roles: List[str] = field(default_factory=list)
staff_users: List[int] = field(default_factory=list)
config_roles: List[str] = field(default_factory=list)
config_users: List[int] = field(default_factory=list)
max_tickets_per_user: int = 5
auto_close_enabled: bool = False
auto_close_days: int = 7
@ -312,8 +314,12 @@ class GuildTicketConfig:
"default_category": self.default_category,
"log_channel_id": self.log_channel_id,
"archive_channel_id": self.archive_channel_id,
"log_channel_id": self.log_channel_id,
"archive_channel_id": self.archive_channel_id,
"staff_roles": self.staff_roles,
"staff_users": self.staff_users,
"config_roles": self.config_roles,
"config_users": self.config_users,
"max_tickets_per_user": self.max_tickets_per_user,
"auto_close_enabled": self.auto_close_enabled,
"auto_close_days": self.auto_close_days,
@ -335,8 +341,11 @@ class GuildTicketConfig:
default_category=data.get("default_category"),
log_channel_id=data.get("log_channel_id"),
archive_channel_id=data.get("archive_channel_id"),
staff_roles=data.get("staff_roles", []),
staff_users=data.get("staff_users", []),
config_roles=data.get("config_roles", []),
config_users=data.get("config_users", []),
max_tickets_per_user=data.get("max_tickets_per_user", 5),
auto_close_enabled=data.get("auto_close_enabled", False),
auto_close_days=data.get("auto_close_days", 7),

View file

@ -25,6 +25,8 @@ class StaffAnalytics:
async def get_staff_list(self, guild_id: int) -> List[Dict[str, Any]]:
"""Get list of staff members with their stats"""
config = self.storage.load_config(guild_id)
# Invalidate cache to ensure we get the latest ticket data with new ratings
self.storage.invalidate_cache(guild_id)
tickets = self.storage.get_guild_tickets(guild_id)
staff_stats = defaultdict(lambda: {

View file

@ -0,0 +1,92 @@
"""
Ticket Permissions Utility
==========================
Centralized permission logic for the ticket system.
"""
import discord
from typing import TYPE_CHECKING, Optional
if TYPE_CHECKING:
from ..data.models import GuildTicketConfig
def is_staff(interaction: discord.Interaction, config: 'GuildTicketConfig') -> bool:
"""
Check if the user has staff permissions.
Checks:
1. Administrator permission
2. Staff roles
3. Staff users (specific user IDs)
"""
# Get user and permissions based on object type
user = getattr(interaction, "author", getattr(interaction, "user", None))
if not user:
return False
if user.guild_permissions.administrator:
return True
# Check specific users
if user.id in config.staff_users:
return True
# Check roles
for role_id in config.staff_roles:
role = interaction.guild.get_role(int(role_id))
if role and role in user.roles:
return True
return False
def can_access_config(interaction: discord.Interaction, config: 'GuildTicketConfig') -> bool:
"""
Check if the user can access the configuration.
Checks:
1. Administrator permission
2. Config roles
3. Config users
4. Staff permissions (staff can often access basic config or view panels)
Note: This function is strictly for CONFIG access.
If staff should access config, add them to config roles or check is_staff too.
In this implementation, we check specific config permissions.
"""
# Get user and permissions based on object type
user = getattr(interaction, "author", getattr(interaction, "user", None))
if not user:
return False
if user.guild_permissions.administrator:
return True
# Check specific users
if user.id in config.config_users:
return True
# Check config roles
for role_id in config.config_roles:
role = interaction.guild.get_role(int(role_id))
if role and role in user.roles:
return True
# Also check legacy/migrated staff roles if they imply config access?
# For now, we keep it strict: config access requires explicit config permission.
return False
def is_whitelisted(interaction: discord.Interaction, config: 'GuildTicketConfig', bot) -> bool:
"""
Check if user is whitelisted or has staff/config permissions.
"""
# Staff and config users are always whitelisted
if is_staff(interaction, config) or can_access_config(interaction, config):
return True
# Check whitelist cog
whitelist_cog = bot.get_cog("WhitelistMonitor")
if not whitelist_cog:
# If whitelist system not available, deny unless staff/config
return False
return whitelist_cog.is_whitelisted(interaction.guild.id, interaction.user.id)

View file

@ -0,0 +1,10 @@
[
{
"timestamp": "2026-01-10T16:19:17.701722",
"event_type": "member_join",
"member_id": 1459567012031234198,
"member_name": "fequjifenq_85720",
"member_discriminator": "0",
"member_avatar": null
}
]

View file

@ -0,0 +1,16 @@
{
"channel_id": 1459563155859570692,
"guild_id": 1369669999345537145,
"user_id": 971446412690722826,
"category_id": "3b0b9bc1",
"status": "open",
"priority": "normal",
"created_at": "2026-01-10T16:02:53.964008",
"closed_at": null,
"claimed_by": null,
"messages": [],
"reason": "egrrdhgdrh",
"transcript_path": null,
"rating": null,
"feedback": null
}

View file

@ -0,0 +1,16 @@
{
"channel_id": 1461084203054665812,
"guild_id": 1369669999345537145,
"user_id": 971446412690722826,
"category_id": "91522e2c",
"status": "open",
"priority": "normal",
"created_at": "2026-01-14T20:46:59.791051",
"closed_at": null,
"claimed_by": null,
"messages": [],
"reason": "gregqssgg",
"transcript_path": null,
"rating": null,
"feedback": null
}

View file

@ -0,0 +1,16 @@
{
"channel_id": 1461084293995565242,
"guild_id": 1369669999345537145,
"user_id": 971446412690722826,
"category_id": "3b0b9bc1",
"status": "open",
"priority": "normal",
"created_at": "2026-01-14T20:47:21.788375",
"closed_at": null,
"claimed_by": null,
"messages": [],
"reason": "fesefsfe",
"transcript_path": null,
"rating": null,
"feedback": null
}

View file

@ -0,0 +1,16 @@
{
"channel_id": 1461084293995565242,
"guild_id": 1369669999345537145,
"user_id": 971446412690722826,
"category_id": "3b0b9bc1",
"status": "claimed",
"priority": "normal",
"created_at": "2026-01-14T20:47:21.788375",
"closed_at": null,
"claimed_by": 971446412690722826,
"messages": [],
"reason": "fesefsfe",
"transcript_path": null,
"rating": null,
"feedback": null
}

View file

@ -0,0 +1,16 @@
{
"channel_id": 1461084511738532026,
"guild_id": 1369669999345537145,
"user_id": 1459567012031234198,
"category_id": "3b0b9bc1",
"status": "open",
"priority": "normal",
"created_at": "2026-01-14T20:48:13.454435",
"closed_at": null,
"claimed_by": null,
"messages": [],
"reason": "dzqdqzgs",
"transcript_path": null,
"rating": null,
"feedback": null
}

View file

@ -0,0 +1,16 @@
{
"channel_id": 1461084844879778008,
"guild_id": 1369669999345537145,
"user_id": 971446412690722826,
"category_id": "3b0b9bc1",
"status": "open",
"priority": "normal",
"created_at": "2026-01-14T20:49:32.964046",
"closed_at": null,
"claimed_by": null,
"messages": [],
"reason": "szfeq",
"transcript_path": null,
"rating": null,
"feedback": null
}

View file

@ -0,0 +1,56 @@
{
"1369669999345537145": {
"guild_id": 1369669999345537145,
"enabled": true,
"categories": [
{
"id": "3b0b9bc1",
"name": "Mon cul",
"description": "Mon Anus",
"emoji": "🎫",
"color": "#3498db",
"staff_roles": [],
"welcome_message": "Merci d'avoir créé un ticket. Notre équipe va vous répondre sous peu.",
"require_reason": false,
"max_tickets_per_user": 5,
"auto_close_days": 7,
"priority_enabled": true,
"allow_claims": true,
"survey_enabled": true
},
{
"id": "91522e2c",
"name": "claudo",
"description": "un clochard",
"emoji": "📋",
"color": "#3498db",
"staff_roles": [],
"welcome_message": "Merci d'avoir créé un ticket. Notre équipe va vous répondre sous peu.",
"require_reason": false,
"max_tickets_per_user": 5,
"auto_close_days": 7,
"priority_enabled": true,
"allow_claims": true,
"survey_enabled": true
}
],
"default_category": null,
"log_channel_id": 1369669999677145103,
"archive_channel_id": null,
"staff_roles": [
"1369769836028231730"
],
"config_roles": [
"1369769836028231730"
],
"max_tickets_per_user": 5,
"auto_close_enabled": false,
"auto_close_days": 7,
"transcript_enabled": true,
"claim_enabled": true,
"survey_enabled": true,
"panel_channel_id": null,
"welcome_enabled": true,
"priority_enabled": true
}
}

View file

@ -0,0 +1,56 @@
{
"1369669999345537145": {
"guild_id": 1369669999345537145,
"enabled": true,
"categories": [
{
"id": "3b0b9bc1",
"name": "Mon cul",
"description": "Mon Anus",
"emoji": "🎫",
"color": "#3498db",
"staff_roles": [],
"welcome_message": "Merci d'avoir créé un ticket. Notre équipe va vous répondre sous peu.",
"require_reason": false,
"max_tickets_per_user": 5,
"auto_close_days": 7,
"priority_enabled": true,
"allow_claims": true,
"survey_enabled": true
},
{
"id": "91522e2c",
"name": "claudo",
"description": "un clochard",
"emoji": "📋",
"color": "#3498db",
"staff_roles": [],
"welcome_message": "Merci d'avoir créé un ticket. Notre équipe va vous répondre sous peu.",
"require_reason": false,
"max_tickets_per_user": 5,
"auto_close_days": 7,
"priority_enabled": true,
"allow_claims": true,
"survey_enabled": true
}
],
"default_category": null,
"log_channel_id": 1369669999677145103,
"archive_channel_id": null,
"staff_roles": [
"1369769836028231730"
],
"staff_users": [],
"config_roles": [],
"config_users": [],
"max_tickets_per_user": 5,
"auto_close_enabled": false,
"auto_close_days": 7,
"transcript_enabled": true,
"claim_enabled": true,
"survey_enabled": true,
"panel_channel_id": null,
"welcome_enabled": true,
"priority_enabled": true
}
}

View file

@ -0,0 +1,54 @@
{
"1369669999345537145": {
"guild_id": 1369669999345537145,
"enabled": true,
"categories": [
{
"id": "3b0b9bc1",
"name": "Mon cul",
"description": "Mon Anus",
"emoji": "🎫",
"color": "#3498db",
"staff_roles": [],
"welcome_message": "Merci d'avoir créé un ticket. Notre équipe va vous répondre sous peu.",
"require_reason": false,
"max_tickets_per_user": 5,
"auto_close_days": 7,
"priority_enabled": true,
"allow_claims": true,
"survey_enabled": true
},
{
"id": "91522e2c",
"name": "claudo",
"description": "un clochard",
"emoji": "📋",
"color": "#3498db",
"staff_roles": [],
"welcome_message": "Merci d'avoir créé un ticket. Notre équipe va vous répondre sous peu.",
"require_reason": false,
"max_tickets_per_user": 5,
"auto_close_days": 7,
"priority_enabled": true,
"allow_claims": true,
"survey_enabled": true
}
],
"default_category": null,
"log_channel_id": 1369669999677145103,
"archive_channel_id": null,
"staff_roles": [],
"staff_users": [],
"config_roles": [],
"config_users": [],
"max_tickets_per_user": 5,
"auto_close_enabled": false,
"auto_close_days": 7,
"transcript_enabled": true,
"claim_enabled": true,
"survey_enabled": true,
"panel_channel_id": null,
"welcome_enabled": true,
"priority_enabled": true
}
}

View file

@ -0,0 +1,16 @@
{
"channel_id": 1461088603273822394,
"guild_id": 1369669999345537145,
"user_id": 971446412690722826,
"category_id": "91522e2c",
"status": "open",
"priority": "normal",
"created_at": "2026-01-14T21:04:28.886631",
"closed_at": null,
"claimed_by": null,
"messages": [],
"reason": "dzqd",
"transcript_path": null,
"rating": null,
"feedback": null
}

View file

@ -0,0 +1,16 @@
{
"channel_id": 1461083997655400669,
"guild_id": 1369669999345537145,
"user_id": 971446412690722826,
"category_id": "3b0b9bc1",
"status": "open",
"priority": "normal",
"created_at": "2026-01-14T20:46:10.826712",
"closed_at": null,
"claimed_by": null,
"messages": [],
"reason": "dzqzddqz",
"transcript_path": null,
"rating": null,
"feedback": null
}

View file

@ -0,0 +1,56 @@
{
"1369669999345537145": {
"guild_id": 1369669999345537145,
"enabled": true,
"categories": [
{
"id": "3b0b9bc1",
"name": "Mon cul",
"description": "Mon Anus",
"emoji": "🎫",
"color": "#3498db",
"staff_roles": [],
"welcome_message": "Merci d'avoir créé un ticket. Notre équipe va vous répondre sous peu.",
"require_reason": false,
"max_tickets_per_user": 5,
"auto_close_days": 7,
"priority_enabled": true,
"allow_claims": true,
"survey_enabled": true
},
{
"id": "91522e2c",
"name": "claudo",
"description": "un clochard",
"emoji": "📋",
"color": "#3498db",
"staff_roles": [],
"welcome_message": "Merci d'avoir créé un ticket. Notre équipe va vous répondre sous peu.",
"require_reason": false,
"max_tickets_per_user": 5,
"auto_close_days": 7,
"priority_enabled": true,
"allow_claims": true,
"survey_enabled": true
}
],
"default_category": null,
"log_channel_id": 1369669999677145103,
"archive_channel_id": null,
"staff_roles": [],
"staff_users": [],
"config_roles": [
"1369769836028231730"
],
"config_users": [],
"max_tickets_per_user": 5,
"auto_close_enabled": false,
"auto_close_days": 7,
"transcript_enabled": true,
"claim_enabled": true,
"survey_enabled": true,
"panel_channel_id": null,
"welcome_enabled": true,
"priority_enabled": true
}
}

View file

@ -0,0 +1,16 @@
{
"channel_id": 1461089849527505151,
"guild_id": 1369669999345537145,
"user_id": 1459567012031234198,
"category_id": "91522e2c",
"status": "open",
"priority": "normal",
"created_at": "2026-01-14T21:09:26.048196",
"closed_at": null,
"claimed_by": null,
"messages": [],
"reason": "dzqdzdq",
"transcript_path": null,
"rating": null,
"feedback": null
}

View file

@ -0,0 +1,16 @@
{
"channel_id": 1461089849527505151,
"guild_id": 1369669999345537145,
"user_id": 1459567012031234198,
"category_id": "91522e2c",
"status": "closed",
"priority": "normal",
"created_at": "2026-01-14T21:09:26.048196",
"closed_at": "2026-01-14T21:12:34.847114",
"claimed_by": null,
"messages": [],
"reason": "dzqdzdq",
"transcript_path": null,
"rating": null,
"feedback": null
}

View file

@ -0,0 +1,16 @@
{
"channel_id": 1461089849527505151,
"guild_id": 1369669999345537145,
"user_id": 1459567012031234198,
"category_id": "91522e2c",
"status": "closed",
"priority": "normal",
"created_at": "2026-01-14T21:09:26.048196",
"closed_at": "2026-01-14T21:15:28.028358",
"claimed_by": null,
"messages": [],
"reason": "dzqdzdq",
"transcript_path": null,
"rating": null,
"feedback": null
}

View file

@ -0,0 +1,16 @@
{
"channel_id": 1461089849527505151,
"guild_id": 1369669999345537145,
"user_id": 1459567012031234198,
"category_id": "91522e2c",
"status": "closed",
"priority": "normal",
"created_at": "2026-01-14T21:09:26.048196",
"closed_at": "2026-01-14T21:15:31.970135",
"claimed_by": null,
"messages": [],
"reason": "dzqdzdq",
"transcript_path": null,
"rating": null,
"feedback": null
}

View file

@ -0,0 +1,16 @@
{
"channel_id": 1461089849527505151,
"guild_id": 1369669999345537145,
"user_id": 1459567012031234198,
"category_id": "91522e2c",
"status": "closed",
"priority": "normal",
"created_at": "2026-01-14T21:09:26.048196",
"closed_at": "2026-01-14T21:15:35.563739",
"claimed_by": null,
"messages": [],
"reason": "dzqdzdq",
"transcript_path": null,
"rating": null,
"feedback": null
}

View file

@ -0,0 +1,16 @@
{
"channel_id": 1461089849527505151,
"guild_id": 1369669999345537145,
"user_id": 1459567012031234198,
"category_id": "91522e2c",
"status": "closed",
"priority": "normal",
"created_at": "2026-01-14T21:09:26.048196",
"closed_at": "2026-01-14T21:15:39.314476",
"claimed_by": null,
"messages": [],
"reason": "dzqdzdq",
"transcript_path": null,
"rating": null,
"feedback": null
}

View file

@ -0,0 +1,16 @@
{
"channel_id": 1459561776428351568,
"guild_id": 1369669999345537145,
"user_id": 971446412690722826,
"category_id": "3b0b9bc1",
"status": "open",
"priority": "normal",
"created_at": "2026-01-10T15:57:24.973539",
"closed_at": null,
"claimed_by": null,
"messages": [],
"reason": "yjutdgbfrd",
"transcript_path": null,
"rating": null,
"feedback": null
}

View file

@ -0,0 +1,16 @@
{
"channel_id": 1459563155859570692,
"guild_id": 1369669999345537145,
"user_id": 971446412690722826,
"category_id": "3b0b9bc1",
"status": "closed",
"priority": "normal",
"created_at": "2026-01-10T16:02:53.964008",
"closed_at": "2026-01-10T16:03:05.092145",
"claimed_by": null,
"messages": [],
"reason": "egrrdhgdrh",
"transcript_path": null,
"rating": null,
"feedback": null
}

View file

@ -0,0 +1,16 @@
{
"channel_id": 1461083997655400669,
"guild_id": 1369669999345537145,
"user_id": 971446412690722826,
"category_id": "3b0b9bc1",
"status": "closed",
"priority": "normal",
"created_at": "2026-01-14T20:46:10.826712",
"closed_at": "2026-01-14T21:06:29.364458",
"claimed_by": null,
"messages": [],
"reason": "dzqzddqz",
"transcript_path": null,
"rating": null,
"feedback": null
}

View file

@ -0,0 +1,16 @@
{
"channel_id": 1461084203054665812,
"guild_id": 1369669999345537145,
"user_id": 971446412690722826,
"category_id": "91522e2c",
"status": "closed",
"priority": "normal",
"created_at": "2026-01-14T20:46:59.791051",
"closed_at": "2026-01-14T20:47:15.286524",
"claimed_by": null,
"messages": [],
"reason": "gregqssgg",
"transcript_path": null,
"rating": null,
"feedback": null
}

View file

@ -0,0 +1,16 @@
{
"channel_id": 1461084293995565242,
"guild_id": 1369669999345537145,
"user_id": 971446412690722826,
"category_id": "3b0b9bc1",
"status": "closed",
"priority": "normal",
"created_at": "2026-01-14T20:47:21.788375",
"closed_at": "2026-01-14T20:47:45.988053",
"claimed_by": 971446412690722826,
"messages": [],
"reason": "fesefsfe",
"transcript_path": null,
"rating": null,
"feedback": null
}

View file

@ -0,0 +1,16 @@
{
"channel_id": 1461084511738532026,
"guild_id": 1369669999345537145,
"user_id": 1459567012031234198,
"category_id": "3b0b9bc1",
"status": "closed",
"priority": "normal",
"created_at": "2026-01-14T20:48:13.454435",
"closed_at": "2026-01-14T20:48:25.136290",
"claimed_by": null,
"messages": [],
"reason": "dzqdqzgs",
"transcript_path": null,
"rating": null,
"feedback": null
}

View file

@ -0,0 +1,16 @@
{
"channel_id": 1461084844879778008,
"guild_id": 1369669999345537145,
"user_id": 971446412690722826,
"category_id": "3b0b9bc1",
"status": "closed",
"priority": "normal",
"created_at": "2026-01-14T20:49:32.964046",
"closed_at": "2026-01-14T20:49:44.315370",
"claimed_by": null,
"messages": [],
"reason": "szfeq",
"transcript_path": null,
"rating": null,
"feedback": null
}

View file

@ -0,0 +1,16 @@
{
"channel_id": 1461088603273822394,
"guild_id": 1369669999345537145,
"user_id": 971446412690722826,
"category_id": "91522e2c",
"status": "closed",
"priority": "normal",
"created_at": "2026-01-14T21:04:28.886631",
"closed_at": "2026-01-14T21:04:50.756760",
"claimed_by": null,
"messages": [],
"reason": "dzqd",
"transcript_path": null,
"rating": null,
"feedback": null
}

View file

@ -0,0 +1,16 @@
{
"channel_id": 1461089849527505151,
"guild_id": 1369669999345537145,
"user_id": 1459567012031234198,
"category_id": "91522e2c",
"status": "closed",
"priority": "normal",
"created_at": "2026-01-14T21:09:26.048196",
"closed_at": "2026-01-14T21:15:43.690109",
"claimed_by": null,
"messages": [],
"reason": "dzqdzdq",
"transcript_path": null,
"rating": null,
"feedback": null
}

View file

@ -0,0 +1,210 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Transcript - Ticket #1459563155859570692</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f5f6fa;
color: #2c3e50;
line-height: 1.6;
}
.container {
max-width: 900px;
margin: 0 auto;
padding: 20px;
}
.header {
background: linear-gradient(135deg, #3498db, #34495e);
color: white;
padding: 30px;
border-radius: 10px;
margin-bottom: 20px;
}
.header h1 {
font-size: 24px;
margin-bottom: 10px;
}
.header-meta {
display: flex;
flex-wrap: wrap;
gap: 20px;
font-size: 14px;
opacity: 0.9;
}
.meta-item {
display: flex;
align-items: center;
gap: 5px;
}
.status-badge {
display: inline-block;
padding: 3px 10px;
border-radius: 15px;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
}
.status-open { background: #27ae60; }
.status-closed { background: #e74c3c; }
.status-claimed { background: #f39c12; }
.messages-section {
background: white;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.messages-section h3 {
margin-bottom: 15px;
color: #3498db;
border-bottom: 2px solid #ecf0f1;
padding-bottom: 10px;
}
.message {
padding: 15px;
border-bottom: 1px solid #ecf0f1;
}
.message:last-child {
border-bottom: none;
}
.message.staff-message {
background: #f8f9fa;
border-left: 3px solid #3498db;
}
.message-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.author {
font-weight: bold;
color: #2c3e50;
}
.staff-badge {
background: #3498db;
color: white;
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
font-weight: bold;
}
.timestamp {
color: #95a5a6;
font-size: 12px;
}
.message-content {
color: #34495e;
word-break: break-word;
}
.stats-section {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.stats-section h3 {
margin-bottom: 15px;
color: #3498db;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
}
.stat-item {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
text-align: center;
}
.stat-label {
display: block;
font-size: 12px;
color: #7f8c8d;
margin-bottom: 5px;
}
.stat-value {
font-size: 18px;
font-weight: bold;
color: #3498db;
}
.footer {
text-align: center;
padding: 20px;
color: #95a5a6;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎫 Transcript de Ticket</h1>
<div class="header-meta">
<div class="meta-item">
<span>📌 ID:</span>
<span>1459563155859570692</span>
</div>
<div class="meta-item">
<span>📁 Catégorie:</span>
<span>3b0b9bc1</span>
</div>
<div class="meta-item">
<span>⚡ Priorité:</span>
<span style="color: #3498db; font-weight: bold;">NORMAL</span>
</div>
<div class="meta-item">
<span>📊 Statut:</span>
<span class="status-badge status-open">open</span>
</div>
<div class="meta-item">
<span>📅 Créé:</span>
<span>10/01/2026 16:02</span>
</div>
</div>
</div>
<div class="messages-section">
<h3>💬 Messages (0)</h3>
</div>
<div class="stats-section">
<h3>📊 Statistiques</h3>
<div class="stats-grid">
<div class="stat-item">
<span class="stat-label">Durée</span>
<span class="stat-value">0 min</span>
</div>
<div class="stat-item">
<span class="stat-label">Messages</span>
<span class="stat-value">0</span>
</div>
<div class="stat-item">
<span class="stat-label">Note</span>
<span class="stat-value">Pas de note</span>
</div>
</div>
</div>
<div class="footer">
<p>Généré le 10/01/2026 à 16:03:05</p>
<p>Système de Tickets - Kuby Bot</p>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,210 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Transcript - Ticket #1461083997655400669</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f5f6fa;
color: #2c3e50;
line-height: 1.6;
}
.container {
max-width: 900px;
margin: 0 auto;
padding: 20px;
}
.header {
background: linear-gradient(135deg, #3498db, #34495e);
color: white;
padding: 30px;
border-radius: 10px;
margin-bottom: 20px;
}
.header h1 {
font-size: 24px;
margin-bottom: 10px;
}
.header-meta {
display: flex;
flex-wrap: wrap;
gap: 20px;
font-size: 14px;
opacity: 0.9;
}
.meta-item {
display: flex;
align-items: center;
gap: 5px;
}
.status-badge {
display: inline-block;
padding: 3px 10px;
border-radius: 15px;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
}
.status-open { background: #27ae60; }
.status-closed { background: #e74c3c; }
.status-claimed { background: #f39c12; }
.messages-section {
background: white;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.messages-section h3 {
margin-bottom: 15px;
color: #3498db;
border-bottom: 2px solid #ecf0f1;
padding-bottom: 10px;
}
.message {
padding: 15px;
border-bottom: 1px solid #ecf0f1;
}
.message:last-child {
border-bottom: none;
}
.message.staff-message {
background: #f8f9fa;
border-left: 3px solid #3498db;
}
.message-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.author {
font-weight: bold;
color: #2c3e50;
}
.staff-badge {
background: #3498db;
color: white;
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
font-weight: bold;
}
.timestamp {
color: #95a5a6;
font-size: 12px;
}
.message-content {
color: #34495e;
word-break: break-word;
}
.stats-section {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.stats-section h3 {
margin-bottom: 15px;
color: #3498db;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
}
.stat-item {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
text-align: center;
}
.stat-label {
display: block;
font-size: 12px;
color: #7f8c8d;
margin-bottom: 5px;
}
.stat-value {
font-size: 18px;
font-weight: bold;
color: #3498db;
}
.footer {
text-align: center;
padding: 20px;
color: #95a5a6;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎫 Transcript de Ticket</h1>
<div class="header-meta">
<div class="meta-item">
<span>📌 ID:</span>
<span>1461083997655400669</span>
</div>
<div class="meta-item">
<span>📁 Catégorie:</span>
<span>3b0b9bc1</span>
</div>
<div class="meta-item">
<span>⚡ Priorité:</span>
<span style="color: #3498db; font-weight: bold;">NORMAL</span>
</div>
<div class="meta-item">
<span>📊 Statut:</span>
<span class="status-badge status-open">open</span>
</div>
<div class="meta-item">
<span>📅 Créé:</span>
<span>14/01/2026 20:46</span>
</div>
</div>
</div>
<div class="messages-section">
<h3>💬 Messages (0)</h3>
</div>
<div class="stats-section">
<h3>📊 Statistiques</h3>
<div class="stats-grid">
<div class="stat-item">
<span class="stat-label">Durée</span>
<span class="stat-value">20 min</span>
</div>
<div class="stat-item">
<span class="stat-label">Messages</span>
<span class="stat-value">0</span>
</div>
<div class="stat-item">
<span class="stat-label">Note</span>
<span class="stat-value">Pas de note</span>
</div>
</div>
</div>
<div class="footer">
<p>Généré le 14/01/2026 à 21:06:29</p>
<p>Système de Tickets - Kuby Bot</p>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,210 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Transcript - Ticket #1461084203054665812</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f5f6fa;
color: #2c3e50;
line-height: 1.6;
}
.container {
max-width: 900px;
margin: 0 auto;
padding: 20px;
}
.header {
background: linear-gradient(135deg, #3498db, #34495e);
color: white;
padding: 30px;
border-radius: 10px;
margin-bottom: 20px;
}
.header h1 {
font-size: 24px;
margin-bottom: 10px;
}
.header-meta {
display: flex;
flex-wrap: wrap;
gap: 20px;
font-size: 14px;
opacity: 0.9;
}
.meta-item {
display: flex;
align-items: center;
gap: 5px;
}
.status-badge {
display: inline-block;
padding: 3px 10px;
border-radius: 15px;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
}
.status-open { background: #27ae60; }
.status-closed { background: #e74c3c; }
.status-claimed { background: #f39c12; }
.messages-section {
background: white;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.messages-section h3 {
margin-bottom: 15px;
color: #3498db;
border-bottom: 2px solid #ecf0f1;
padding-bottom: 10px;
}
.message {
padding: 15px;
border-bottom: 1px solid #ecf0f1;
}
.message:last-child {
border-bottom: none;
}
.message.staff-message {
background: #f8f9fa;
border-left: 3px solid #3498db;
}
.message-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.author {
font-weight: bold;
color: #2c3e50;
}
.staff-badge {
background: #3498db;
color: white;
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
font-weight: bold;
}
.timestamp {
color: #95a5a6;
font-size: 12px;
}
.message-content {
color: #34495e;
word-break: break-word;
}
.stats-section {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.stats-section h3 {
margin-bottom: 15px;
color: #3498db;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
}
.stat-item {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
text-align: center;
}
.stat-label {
display: block;
font-size: 12px;
color: #7f8c8d;
margin-bottom: 5px;
}
.stat-value {
font-size: 18px;
font-weight: bold;
color: #3498db;
}
.footer {
text-align: center;
padding: 20px;
color: #95a5a6;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎫 Transcript de Ticket</h1>
<div class="header-meta">
<div class="meta-item">
<span>📌 ID:</span>
<span>1461084203054665812</span>
</div>
<div class="meta-item">
<span>📁 Catégorie:</span>
<span>91522e2c</span>
</div>
<div class="meta-item">
<span>⚡ Priorité:</span>
<span style="color: #3498db; font-weight: bold;">NORMAL</span>
</div>
<div class="meta-item">
<span>📊 Statut:</span>
<span class="status-badge status-open">open</span>
</div>
<div class="meta-item">
<span>📅 Créé:</span>
<span>14/01/2026 20:46</span>
</div>
</div>
</div>
<div class="messages-section">
<h3>💬 Messages (0)</h3>
</div>
<div class="stats-section">
<h3>📊 Statistiques</h3>
<div class="stats-grid">
<div class="stat-item">
<span class="stat-label">Durée</span>
<span class="stat-value">0 min</span>
</div>
<div class="stat-item">
<span class="stat-label">Messages</span>
<span class="stat-value">0</span>
</div>
<div class="stat-item">
<span class="stat-label">Note</span>
<span class="stat-value">Pas de note</span>
</div>
</div>
</div>
<div class="footer">
<p>Généré le 14/01/2026 à 20:47:15</p>
<p>Système de Tickets - Kuby Bot</p>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,237 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Transcript - Ticket #1461084293995565242</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f5f6fa;
color: #2c3e50;
line-height: 1.6;
}
.container {
max-width: 900px;
margin: 0 auto;
padding: 20px;
}
.header {
background: linear-gradient(135deg, #3498db, #34495e);
color: white;
padding: 30px;
border-radius: 10px;
margin-bottom: 20px;
}
.header h1 {
font-size: 24px;
margin-bottom: 10px;
}
.header-meta {
display: flex;
flex-wrap: wrap;
gap: 20px;
font-size: 14px;
opacity: 0.9;
}
.meta-item {
display: flex;
align-items: center;
gap: 5px;
}
.status-badge {
display: inline-block;
padding: 3px 10px;
border-radius: 15px;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
}
.status-open { background: #27ae60; }
.status-closed { background: #e74c3c; }
.status-claimed { background: #f39c12; }
.messages-section {
background: white;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.messages-section h3 {
margin-bottom: 15px;
color: #3498db;
border-bottom: 2px solid #ecf0f1;
padding-bottom: 10px;
}
.message {
padding: 15px;
border-bottom: 1px solid #ecf0f1;
}
.message:last-child {
border-bottom: none;
}
.message.staff-message {
background: #f8f9fa;
border-left: 3px solid #3498db;
}
.message-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.author {
font-weight: bold;
color: #2c3e50;
}
.staff-badge {
background: #3498db;
color: white;
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
font-weight: bold;
}
.timestamp {
color: #95a5a6;
font-size: 12px;
}
.message-content {
color: #34495e;
word-break: break-word;
}
.stats-section {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.stats-section h3 {
margin-bottom: 15px;
color: #3498db;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
}
.stat-item {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
text-align: center;
}
.stat-label {
display: block;
font-size: 12px;
color: #7f8c8d;
margin-bottom: 5px;
}
.stat-value {
font-size: 18px;
font-weight: bold;
color: #3498db;
}
.footer {
text-align: center;
padding: 20px;
color: #95a5a6;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎫 Transcript de Ticket</h1>
<div class="header-meta">
<div class="meta-item">
<span>📌 ID:</span>
<span>1461084293995565242</span>
</div>
<div class="meta-item">
<span>📁 Catégorie:</span>
<span>3b0b9bc1</span>
</div>
<div class="meta-item">
<span>⚡ Priorité:</span>
<span style="color: #3498db; font-weight: bold;">NORMAL</span>
</div>
<div class="meta-item">
<span>📊 Statut:</span>
<span class="status-badge status-claimed">claimed</span>
</div>
<div class="meta-item">
<span>📅 Créé:</span>
<span>14/01/2026 20:47</span>
</div>
</div>
</div>
<div class="messages-section">
<h3>💬 Messages (3)</h3>
<div class="message staff-message">
<div class="message-header">
<span class="author">gameurpro12</span>
<span class="staff-badge">STAFF</span>
<span class="timestamp">14/01/2026 19:47:33</span>
</div>
<div class="message-content">rp;fepsŝfe</div>
</div>
<div class="message staff-message">
<div class="message-header">
<span class="author">gameurpro12</span>
<span class="staff-badge">STAFF</span>
<span class="timestamp">14/01/2026 19:47:34</span>
</div>
<div class="message-content">fe,s,</div>
</div>
<div class="message staff-message">
<div class="message-header">
<span class="author">gameurpro12</span>
<span class="staff-badge">STAFF</span>
<span class="timestamp">14/01/2026 19:47:35</span>
</div>
<div class="message-content">f,ekf,sem</div>
</div>
</div>
<div class="stats-section">
<h3>📊 Statistiques</h3>
<div class="stats-grid">
<div class="stat-item">
<span class="stat-label">Durée</span>
<span class="stat-value">0 min</span>
</div>
<div class="stat-item">
<span class="stat-label">Messages</span>
<span class="stat-value">3</span>
</div>
<div class="stat-item">
<span class="stat-label">Note</span>
<span class="stat-value">Pas de note</span>
</div>
</div>
</div>
<div class="footer">
<p>Généré le 14/01/2026 à 20:47:45</p>
<p>Système de Tickets - Kuby Bot</p>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,210 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Transcript - Ticket #1461084511738532026</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f5f6fa;
color: #2c3e50;
line-height: 1.6;
}
.container {
max-width: 900px;
margin: 0 auto;
padding: 20px;
}
.header {
background: linear-gradient(135deg, #3498db, #34495e);
color: white;
padding: 30px;
border-radius: 10px;
margin-bottom: 20px;
}
.header h1 {
font-size: 24px;
margin-bottom: 10px;
}
.header-meta {
display: flex;
flex-wrap: wrap;
gap: 20px;
font-size: 14px;
opacity: 0.9;
}
.meta-item {
display: flex;
align-items: center;
gap: 5px;
}
.status-badge {
display: inline-block;
padding: 3px 10px;
border-radius: 15px;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
}
.status-open { background: #27ae60; }
.status-closed { background: #e74c3c; }
.status-claimed { background: #f39c12; }
.messages-section {
background: white;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.messages-section h3 {
margin-bottom: 15px;
color: #3498db;
border-bottom: 2px solid #ecf0f1;
padding-bottom: 10px;
}
.message {
padding: 15px;
border-bottom: 1px solid #ecf0f1;
}
.message:last-child {
border-bottom: none;
}
.message.staff-message {
background: #f8f9fa;
border-left: 3px solid #3498db;
}
.message-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.author {
font-weight: bold;
color: #2c3e50;
}
.staff-badge {
background: #3498db;
color: white;
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
font-weight: bold;
}
.timestamp {
color: #95a5a6;
font-size: 12px;
}
.message-content {
color: #34495e;
word-break: break-word;
}
.stats-section {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.stats-section h3 {
margin-bottom: 15px;
color: #3498db;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
}
.stat-item {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
text-align: center;
}
.stat-label {
display: block;
font-size: 12px;
color: #7f8c8d;
margin-bottom: 5px;
}
.stat-value {
font-size: 18px;
font-weight: bold;
color: #3498db;
}
.footer {
text-align: center;
padding: 20px;
color: #95a5a6;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎫 Transcript de Ticket</h1>
<div class="header-meta">
<div class="meta-item">
<span>📌 ID:</span>
<span>1461084511738532026</span>
</div>
<div class="meta-item">
<span>📁 Catégorie:</span>
<span>3b0b9bc1</span>
</div>
<div class="meta-item">
<span>⚡ Priorité:</span>
<span style="color: #3498db; font-weight: bold;">NORMAL</span>
</div>
<div class="meta-item">
<span>📊 Statut:</span>
<span class="status-badge status-open">open</span>
</div>
<div class="meta-item">
<span>📅 Créé:</span>
<span>14/01/2026 20:48</span>
</div>
</div>
</div>
<div class="messages-section">
<h3>💬 Messages (0)</h3>
</div>
<div class="stats-section">
<h3>📊 Statistiques</h3>
<div class="stats-grid">
<div class="stat-item">
<span class="stat-label">Durée</span>
<span class="stat-value">0 min</span>
</div>
<div class="stat-item">
<span class="stat-label">Messages</span>
<span class="stat-value">0</span>
</div>
<div class="stat-item">
<span class="stat-label">Note</span>
<span class="stat-value">Pas de note</span>
</div>
</div>
</div>
<div class="footer">
<p>Généré le 14/01/2026 à 20:48:25</p>
<p>Système de Tickets - Kuby Bot</p>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,210 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Transcript - Ticket #1461084844879778008</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f5f6fa;
color: #2c3e50;
line-height: 1.6;
}
.container {
max-width: 900px;
margin: 0 auto;
padding: 20px;
}
.header {
background: linear-gradient(135deg, #3498db, #34495e);
color: white;
padding: 30px;
border-radius: 10px;
margin-bottom: 20px;
}
.header h1 {
font-size: 24px;
margin-bottom: 10px;
}
.header-meta {
display: flex;
flex-wrap: wrap;
gap: 20px;
font-size: 14px;
opacity: 0.9;
}
.meta-item {
display: flex;
align-items: center;
gap: 5px;
}
.status-badge {
display: inline-block;
padding: 3px 10px;
border-radius: 15px;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
}
.status-open { background: #27ae60; }
.status-closed { background: #e74c3c; }
.status-claimed { background: #f39c12; }
.messages-section {
background: white;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.messages-section h3 {
margin-bottom: 15px;
color: #3498db;
border-bottom: 2px solid #ecf0f1;
padding-bottom: 10px;
}
.message {
padding: 15px;
border-bottom: 1px solid #ecf0f1;
}
.message:last-child {
border-bottom: none;
}
.message.staff-message {
background: #f8f9fa;
border-left: 3px solid #3498db;
}
.message-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.author {
font-weight: bold;
color: #2c3e50;
}
.staff-badge {
background: #3498db;
color: white;
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
font-weight: bold;
}
.timestamp {
color: #95a5a6;
font-size: 12px;
}
.message-content {
color: #34495e;
word-break: break-word;
}
.stats-section {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.stats-section h3 {
margin-bottom: 15px;
color: #3498db;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
}
.stat-item {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
text-align: center;
}
.stat-label {
display: block;
font-size: 12px;
color: #7f8c8d;
margin-bottom: 5px;
}
.stat-value {
font-size: 18px;
font-weight: bold;
color: #3498db;
}
.footer {
text-align: center;
padding: 20px;
color: #95a5a6;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎫 Transcript de Ticket</h1>
<div class="header-meta">
<div class="meta-item">
<span>📌 ID:</span>
<span>1461084844879778008</span>
</div>
<div class="meta-item">
<span>📁 Catégorie:</span>
<span>3b0b9bc1</span>
</div>
<div class="meta-item">
<span>⚡ Priorité:</span>
<span style="color: #3498db; font-weight: bold;">NORMAL</span>
</div>
<div class="meta-item">
<span>📊 Statut:</span>
<span class="status-badge status-open">open</span>
</div>
<div class="meta-item">
<span>📅 Créé:</span>
<span>14/01/2026 20:49</span>
</div>
</div>
</div>
<div class="messages-section">
<h3>💬 Messages (0)</h3>
</div>
<div class="stats-section">
<h3>📊 Statistiques</h3>
<div class="stats-grid">
<div class="stat-item">
<span class="stat-label">Durée</span>
<span class="stat-value">0 min</span>
</div>
<div class="stat-item">
<span class="stat-label">Messages</span>
<span class="stat-value">0</span>
</div>
<div class="stat-item">
<span class="stat-label">Note</span>
<span class="stat-value">Pas de note</span>
</div>
</div>
</div>
<div class="footer">
<p>Généré le 14/01/2026 à 20:49:44</p>
<p>Système de Tickets - Kuby Bot</p>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,273 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Transcript - Ticket #1461088603273822394</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f5f6fa;
color: #2c3e50;
line-height: 1.6;
}
.container {
max-width: 900px;
margin: 0 auto;
padding: 20px;
}
.header {
background: linear-gradient(135deg, #3498db, #34495e);
color: white;
padding: 30px;
border-radius: 10px;
margin-bottom: 20px;
}
.header h1 {
font-size: 24px;
margin-bottom: 10px;
}
.header-meta {
display: flex;
flex-wrap: wrap;
gap: 20px;
font-size: 14px;
opacity: 0.9;
}
.meta-item {
display: flex;
align-items: center;
gap: 5px;
}
.status-badge {
display: inline-block;
padding: 3px 10px;
border-radius: 15px;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
}
.status-open { background: #27ae60; }
.status-closed { background: #e74c3c; }
.status-claimed { background: #f39c12; }
.messages-section {
background: white;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.messages-section h3 {
margin-bottom: 15px;
color: #3498db;
border-bottom: 2px solid #ecf0f1;
padding-bottom: 10px;
}
.message {
padding: 15px;
border-bottom: 1px solid #ecf0f1;
}
.message:last-child {
border-bottom: none;
}
.message.staff-message {
background: #f8f9fa;
border-left: 3px solid #3498db;
}
.message-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.author {
font-weight: bold;
color: #2c3e50;
}
.staff-badge {
background: #3498db;
color: white;
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
font-weight: bold;
}
.timestamp {
color: #95a5a6;
font-size: 12px;
}
.message-content {
color: #34495e;
word-break: break-word;
}
.stats-section {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.stats-section h3 {
margin-bottom: 15px;
color: #3498db;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
}
.stat-item {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
text-align: center;
}
.stat-label {
display: block;
font-size: 12px;
color: #7f8c8d;
margin-bottom: 5px;
}
.stat-value {
font-size: 18px;
font-weight: bold;
color: #3498db;
}
.footer {
text-align: center;
padding: 20px;
color: #95a5a6;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎫 Transcript de Ticket</h1>
<div class="header-meta">
<div class="meta-item">
<span>📌 ID:</span>
<span>1461088603273822394</span>
</div>
<div class="meta-item">
<span>📁 Catégorie:</span>
<span>91522e2c</span>
</div>
<div class="meta-item">
<span>⚡ Priorité:</span>
<span style="color: #3498db; font-weight: bold;">NORMAL</span>
</div>
<div class="meta-item">
<span>📊 Statut:</span>
<span class="status-badge status-open">open</span>
</div>
<div class="meta-item">
<span>📅 Créé:</span>
<span>14/01/2026 21:04</span>
</div>
</div>
</div>
<div class="messages-section">
<h3>💬 Messages (7)</h3>
<div class="message">
<div class="message-header">
<span class="author">gameurpro12</span>
<span class="timestamp">14/01/2026 20:04:33</span>
</div>
<div class="message-content">oizdpqdzq,iodzq</div>
</div>
<div class="message">
<div class="message-header">
<span class="author">gameurpro12</span>
<span class="timestamp">14/01/2026 20:04:34</span>
</div>
<div class="message-content">k,zdq</div>
</div>
<div class="message">
<div class="message-header">
<span class="author">gameurpro12</span>
<span class="timestamp">14/01/2026 20:04:34</span>
</div>
<div class="message-content">dzq,dzq</div>
</div>
<div class="message">
<div class="message-header">
<span class="author">gameurpro12</span>
<span class="timestamp">14/01/2026 20:04:35</span>
</div>
<div class="message-content">zdql,dzq</div>
</div>
<div class="message">
<div class="message-header">
<span class="author">gameurpro12</span>
<span class="timestamp">14/01/2026 20:04:35</span>
</div>
<div class="message-content">dzql,</div>
</div>
<div class="message">
<div class="message-header">
<span class="author">gameurpro12</span>
<span class="timestamp">14/01/2026 20:04:35</span>
</div>
<div class="message-content">dzq,lmdzqm</div>
</div>
<div class="message">
<div class="message-header">
<span class="author">gameurpro12</span>
<span class="timestamp">14/01/2026 20:04:36</span>
</div>
<div class="message-content">zdq;pqzd</div>
</div>
</div>
<div class="stats-section">
<h3>📊 Statistiques</h3>
<div class="stats-grid">
<div class="stat-item">
<span class="stat-label">Durée</span>
<span class="stat-value">0 min</span>
</div>
<div class="stat-item">
<span class="stat-label">Messages</span>
<span class="stat-value">7</span>
</div>
<div class="stat-item">
<span class="stat-label">Note</span>
<span class="stat-value">Pas de note</span>
</div>
</div>
</div>
<div class="footer">
<p>Généré le 14/01/2026 à 21:04:50</p>
<p>Système de Tickets - Kuby Bot</p>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,219 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Transcript - Ticket #1461089849527505151</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f5f6fa;
color: #2c3e50;
line-height: 1.6;
}
.container {
max-width: 900px;
margin: 0 auto;
padding: 20px;
}
.header {
background: linear-gradient(135deg, #3498db, #34495e);
color: white;
padding: 30px;
border-radius: 10px;
margin-bottom: 20px;
}
.header h1 {
font-size: 24px;
margin-bottom: 10px;
}
.header-meta {
display: flex;
flex-wrap: wrap;
gap: 20px;
font-size: 14px;
opacity: 0.9;
}
.meta-item {
display: flex;
align-items: center;
gap: 5px;
}
.status-badge {
display: inline-block;
padding: 3px 10px;
border-radius: 15px;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
}
.status-open { background: #27ae60; }
.status-closed { background: #e74c3c; }
.status-claimed { background: #f39c12; }
.messages-section {
background: white;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.messages-section h3 {
margin-bottom: 15px;
color: #3498db;
border-bottom: 2px solid #ecf0f1;
padding-bottom: 10px;
}
.message {
padding: 15px;
border-bottom: 1px solid #ecf0f1;
}
.message:last-child {
border-bottom: none;
}
.message.staff-message {
background: #f8f9fa;
border-left: 3px solid #3498db;
}
.message-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.author {
font-weight: bold;
color: #2c3e50;
}
.staff-badge {
background: #3498db;
color: white;
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
font-weight: bold;
}
.timestamp {
color: #95a5a6;
font-size: 12px;
}
.message-content {
color: #34495e;
word-break: break-word;
}
.stats-section {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.stats-section h3 {
margin-bottom: 15px;
color: #3498db;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
}
.stat-item {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
text-align: center;
}
.stat-label {
display: block;
font-size: 12px;
color: #7f8c8d;
margin-bottom: 5px;
}
.stat-value {
font-size: 18px;
font-weight: bold;
color: #3498db;
}
.footer {
text-align: center;
padding: 20px;
color: #95a5a6;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎫 Transcript de Ticket</h1>
<div class="header-meta">
<div class="meta-item">
<span>📌 ID:</span>
<span>1461089849527505151</span>
</div>
<div class="meta-item">
<span>📁 Catégorie:</span>
<span>91522e2c</span>
</div>
<div class="meta-item">
<span>⚡ Priorité:</span>
<span style="color: #3498db; font-weight: bold;">NORMAL</span>
</div>
<div class="meta-item">
<span>📊 Statut:</span>
<span class="status-badge status-open">open</span>
</div>
<div class="meta-item">
<span>📅 Créé:</span>
<span>14/01/2026 21:09</span>
</div>
</div>
</div>
<div class="messages-section">
<h3>💬 Messages (1)</h3>
<div class="message staff-message">
<div class="message-header">
<span class="author">_.radi0dem0n._</span>
<span class="staff-badge">STAFF</span>
<span class="timestamp">14/01/2026 20:11:46</span>
</div>
<div class="message-content">aller XD</div>
</div>
</div>
<div class="stats-section">
<h3>📊 Statistiques</h3>
<div class="stats-grid">
<div class="stat-item">
<span class="stat-label">Durée</span>
<span class="stat-value">3 min</span>
</div>
<div class="stat-item">
<span class="stat-label">Messages</span>
<span class="stat-value">1</span>
</div>
<div class="stat-item">
<span class="stat-label">Note</span>
<span class="stat-value">Pas de note</span>
</div>
</div>
</div>
<div class="footer">
<p>Généré le 14/01/2026 à 21:12:34</p>
<p>Système de Tickets - Kuby Bot</p>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,210 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Transcript - Ticket #1461089849527505151</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f5f6fa;
color: #2c3e50;
line-height: 1.6;
}
.container {
max-width: 900px;
margin: 0 auto;
padding: 20px;
}
.header {
background: linear-gradient(135deg, #3498db, #34495e);
color: white;
padding: 30px;
border-radius: 10px;
margin-bottom: 20px;
}
.header h1 {
font-size: 24px;
margin-bottom: 10px;
}
.header-meta {
display: flex;
flex-wrap: wrap;
gap: 20px;
font-size: 14px;
opacity: 0.9;
}
.meta-item {
display: flex;
align-items: center;
gap: 5px;
}
.status-badge {
display: inline-block;
padding: 3px 10px;
border-radius: 15px;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
}
.status-open { background: #27ae60; }
.status-closed { background: #e74c3c; }
.status-claimed { background: #f39c12; }
.messages-section {
background: white;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.messages-section h3 {
margin-bottom: 15px;
color: #3498db;
border-bottom: 2px solid #ecf0f1;
padding-bottom: 10px;
}
.message {
padding: 15px;
border-bottom: 1px solid #ecf0f1;
}
.message:last-child {
border-bottom: none;
}
.message.staff-message {
background: #f8f9fa;
border-left: 3px solid #3498db;
}
.message-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.author {
font-weight: bold;
color: #2c3e50;
}
.staff-badge {
background: #3498db;
color: white;
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
font-weight: bold;
}
.timestamp {
color: #95a5a6;
font-size: 12px;
}
.message-content {
color: #34495e;
word-break: break-word;
}
.stats-section {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.stats-section h3 {
margin-bottom: 15px;
color: #3498db;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
}
.stat-item {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
text-align: center;
}
.stat-label {
display: block;
font-size: 12px;
color: #7f8c8d;
margin-bottom: 5px;
}
.stat-value {
font-size: 18px;
font-weight: bold;
color: #3498db;
}
.footer {
text-align: center;
padding: 20px;
color: #95a5a6;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎫 Transcript de Ticket</h1>
<div class="header-meta">
<div class="meta-item">
<span>📌 ID:</span>
<span>1461089849527505151</span>
</div>
<div class="meta-item">
<span>📁 Catégorie:</span>
<span>91522e2c</span>
</div>
<div class="meta-item">
<span>⚡ Priorité:</span>
<span style="color: #3498db; font-weight: bold;">NORMAL</span>
</div>
<div class="meta-item">
<span>📊 Statut:</span>
<span class="status-badge status-closed">closed</span>
</div>
<div class="meta-item">
<span>📅 Créé:</span>
<span>14/01/2026 21:09</span>
</div>
</div>
</div>
<div class="messages-section">
<h3>💬 Messages (0)</h3>
</div>
<div class="stats-section">
<h3>📊 Statistiques</h3>
<div class="stats-grid">
<div class="stat-item">
<span class="stat-label">Durée</span>
<span class="stat-value">3 min</span>
</div>
<div class="stat-item">
<span class="stat-label">Messages</span>
<span class="stat-value">0</span>
</div>
<div class="stat-item">
<span class="stat-label">Note</span>
<span class="stat-value">Pas de note</span>
</div>
</div>
</div>
<div class="footer">
<p>Généré le 14/01/2026 à 21:15:28</p>
<p>Système de Tickets - Kuby Bot</p>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,210 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Transcript - Ticket #1461089849527505151</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f5f6fa;
color: #2c3e50;
line-height: 1.6;
}
.container {
max-width: 900px;
margin: 0 auto;
padding: 20px;
}
.header {
background: linear-gradient(135deg, #3498db, #34495e);
color: white;
padding: 30px;
border-radius: 10px;
margin-bottom: 20px;
}
.header h1 {
font-size: 24px;
margin-bottom: 10px;
}
.header-meta {
display: flex;
flex-wrap: wrap;
gap: 20px;
font-size: 14px;
opacity: 0.9;
}
.meta-item {
display: flex;
align-items: center;
gap: 5px;
}
.status-badge {
display: inline-block;
padding: 3px 10px;
border-radius: 15px;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
}
.status-open { background: #27ae60; }
.status-closed { background: #e74c3c; }
.status-claimed { background: #f39c12; }
.messages-section {
background: white;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.messages-section h3 {
margin-bottom: 15px;
color: #3498db;
border-bottom: 2px solid #ecf0f1;
padding-bottom: 10px;
}
.message {
padding: 15px;
border-bottom: 1px solid #ecf0f1;
}
.message:last-child {
border-bottom: none;
}
.message.staff-message {
background: #f8f9fa;
border-left: 3px solid #3498db;
}
.message-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.author {
font-weight: bold;
color: #2c3e50;
}
.staff-badge {
background: #3498db;
color: white;
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
font-weight: bold;
}
.timestamp {
color: #95a5a6;
font-size: 12px;
}
.message-content {
color: #34495e;
word-break: break-word;
}
.stats-section {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.stats-section h3 {
margin-bottom: 15px;
color: #3498db;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
}
.stat-item {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
text-align: center;
}
.stat-label {
display: block;
font-size: 12px;
color: #7f8c8d;
margin-bottom: 5px;
}
.stat-value {
font-size: 18px;
font-weight: bold;
color: #3498db;
}
.footer {
text-align: center;
padding: 20px;
color: #95a5a6;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎫 Transcript de Ticket</h1>
<div class="header-meta">
<div class="meta-item">
<span>📌 ID:</span>
<span>1461089849527505151</span>
</div>
<div class="meta-item">
<span>📁 Catégorie:</span>
<span>91522e2c</span>
</div>
<div class="meta-item">
<span>⚡ Priorité:</span>
<span style="color: #3498db; font-weight: bold;">NORMAL</span>
</div>
<div class="meta-item">
<span>📊 Statut:</span>
<span class="status-badge status-closed">closed</span>
</div>
<div class="meta-item">
<span>📅 Créé:</span>
<span>14/01/2026 21:09</span>
</div>
</div>
</div>
<div class="messages-section">
<h3>💬 Messages (0)</h3>
</div>
<div class="stats-section">
<h3>📊 Statistiques</h3>
<div class="stats-grid">
<div class="stat-item">
<span class="stat-label">Durée</span>
<span class="stat-value">6 min</span>
</div>
<div class="stat-item">
<span class="stat-label">Messages</span>
<span class="stat-value">0</span>
</div>
<div class="stat-item">
<span class="stat-label">Note</span>
<span class="stat-value">Pas de note</span>
</div>
</div>
</div>
<div class="footer">
<p>Généré le 14/01/2026 à 21:15:31</p>
<p>Système de Tickets - Kuby Bot</p>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,210 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Transcript - Ticket #1461089849527505151</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f5f6fa;
color: #2c3e50;
line-height: 1.6;
}
.container {
max-width: 900px;
margin: 0 auto;
padding: 20px;
}
.header {
background: linear-gradient(135deg, #3498db, #34495e);
color: white;
padding: 30px;
border-radius: 10px;
margin-bottom: 20px;
}
.header h1 {
font-size: 24px;
margin-bottom: 10px;
}
.header-meta {
display: flex;
flex-wrap: wrap;
gap: 20px;
font-size: 14px;
opacity: 0.9;
}
.meta-item {
display: flex;
align-items: center;
gap: 5px;
}
.status-badge {
display: inline-block;
padding: 3px 10px;
border-radius: 15px;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
}
.status-open { background: #27ae60; }
.status-closed { background: #e74c3c; }
.status-claimed { background: #f39c12; }
.messages-section {
background: white;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.messages-section h3 {
margin-bottom: 15px;
color: #3498db;
border-bottom: 2px solid #ecf0f1;
padding-bottom: 10px;
}
.message {
padding: 15px;
border-bottom: 1px solid #ecf0f1;
}
.message:last-child {
border-bottom: none;
}
.message.staff-message {
background: #f8f9fa;
border-left: 3px solid #3498db;
}
.message-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.author {
font-weight: bold;
color: #2c3e50;
}
.staff-badge {
background: #3498db;
color: white;
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
font-weight: bold;
}
.timestamp {
color: #95a5a6;
font-size: 12px;
}
.message-content {
color: #34495e;
word-break: break-word;
}
.stats-section {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.stats-section h3 {
margin-bottom: 15px;
color: #3498db;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
}
.stat-item {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
text-align: center;
}
.stat-label {
display: block;
font-size: 12px;
color: #7f8c8d;
margin-bottom: 5px;
}
.stat-value {
font-size: 18px;
font-weight: bold;
color: #3498db;
}
.footer {
text-align: center;
padding: 20px;
color: #95a5a6;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎫 Transcript de Ticket</h1>
<div class="header-meta">
<div class="meta-item">
<span>📌 ID:</span>
<span>1461089849527505151</span>
</div>
<div class="meta-item">
<span>📁 Catégorie:</span>
<span>91522e2c</span>
</div>
<div class="meta-item">
<span>⚡ Priorité:</span>
<span style="color: #3498db; font-weight: bold;">NORMAL</span>
</div>
<div class="meta-item">
<span>📊 Statut:</span>
<span class="status-badge status-closed">closed</span>
</div>
<div class="meta-item">
<span>📅 Créé:</span>
<span>14/01/2026 21:09</span>
</div>
</div>
</div>
<div class="messages-section">
<h3>💬 Messages (0)</h3>
</div>
<div class="stats-section">
<h3>📊 Statistiques</h3>
<div class="stats-grid">
<div class="stat-item">
<span class="stat-label">Durée</span>
<span class="stat-value">6 min</span>
</div>
<div class="stat-item">
<span class="stat-label">Messages</span>
<span class="stat-value">0</span>
</div>
<div class="stat-item">
<span class="stat-label">Note</span>
<span class="stat-value">Pas de note</span>
</div>
</div>
</div>
<div class="footer">
<p>Généré le 14/01/2026 à 21:15:35</p>
<p>Système de Tickets - Kuby Bot</p>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,210 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Transcript - Ticket #1461089849527505151</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f5f6fa;
color: #2c3e50;
line-height: 1.6;
}
.container {
max-width: 900px;
margin: 0 auto;
padding: 20px;
}
.header {
background: linear-gradient(135deg, #3498db, #34495e);
color: white;
padding: 30px;
border-radius: 10px;
margin-bottom: 20px;
}
.header h1 {
font-size: 24px;
margin-bottom: 10px;
}
.header-meta {
display: flex;
flex-wrap: wrap;
gap: 20px;
font-size: 14px;
opacity: 0.9;
}
.meta-item {
display: flex;
align-items: center;
gap: 5px;
}
.status-badge {
display: inline-block;
padding: 3px 10px;
border-radius: 15px;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
}
.status-open { background: #27ae60; }
.status-closed { background: #e74c3c; }
.status-claimed { background: #f39c12; }
.messages-section {
background: white;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.messages-section h3 {
margin-bottom: 15px;
color: #3498db;
border-bottom: 2px solid #ecf0f1;
padding-bottom: 10px;
}
.message {
padding: 15px;
border-bottom: 1px solid #ecf0f1;
}
.message:last-child {
border-bottom: none;
}
.message.staff-message {
background: #f8f9fa;
border-left: 3px solid #3498db;
}
.message-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.author {
font-weight: bold;
color: #2c3e50;
}
.staff-badge {
background: #3498db;
color: white;
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
font-weight: bold;
}
.timestamp {
color: #95a5a6;
font-size: 12px;
}
.message-content {
color: #34495e;
word-break: break-word;
}
.stats-section {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.stats-section h3 {
margin-bottom: 15px;
color: #3498db;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
}
.stat-item {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
text-align: center;
}
.stat-label {
display: block;
font-size: 12px;
color: #7f8c8d;
margin-bottom: 5px;
}
.stat-value {
font-size: 18px;
font-weight: bold;
color: #3498db;
}
.footer {
text-align: center;
padding: 20px;
color: #95a5a6;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎫 Transcript de Ticket</h1>
<div class="header-meta">
<div class="meta-item">
<span>📌 ID:</span>
<span>1461089849527505151</span>
</div>
<div class="meta-item">
<span>📁 Catégorie:</span>
<span>91522e2c</span>
</div>
<div class="meta-item">
<span>⚡ Priorité:</span>
<span style="color: #3498db; font-weight: bold;">NORMAL</span>
</div>
<div class="meta-item">
<span>📊 Statut:</span>
<span class="status-badge status-closed">closed</span>
</div>
<div class="meta-item">
<span>📅 Créé:</span>
<span>14/01/2026 21:09</span>
</div>
</div>
</div>
<div class="messages-section">
<h3>💬 Messages (0)</h3>
</div>
<div class="stats-section">
<h3>📊 Statistiques</h3>
<div class="stats-grid">
<div class="stat-item">
<span class="stat-label">Durée</span>
<span class="stat-value">6 min</span>
</div>
<div class="stat-item">
<span class="stat-label">Messages</span>
<span class="stat-value">0</span>
</div>
<div class="stat-item">
<span class="stat-label">Note</span>
<span class="stat-value">Pas de note</span>
</div>
</div>
</div>
<div class="footer">
<p>Généré le 14/01/2026 à 21:15:39</p>
<p>Système de Tickets - Kuby Bot</p>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,210 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Transcript - Ticket #1461089849527505151</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f5f6fa;
color: #2c3e50;
line-height: 1.6;
}
.container {
max-width: 900px;
margin: 0 auto;
padding: 20px;
}
.header {
background: linear-gradient(135deg, #3498db, #34495e);
color: white;
padding: 30px;
border-radius: 10px;
margin-bottom: 20px;
}
.header h1 {
font-size: 24px;
margin-bottom: 10px;
}
.header-meta {
display: flex;
flex-wrap: wrap;
gap: 20px;
font-size: 14px;
opacity: 0.9;
}
.meta-item {
display: flex;
align-items: center;
gap: 5px;
}
.status-badge {
display: inline-block;
padding: 3px 10px;
border-radius: 15px;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
}
.status-open { background: #27ae60; }
.status-closed { background: #e74c3c; }
.status-claimed { background: #f39c12; }
.messages-section {
background: white;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.messages-section h3 {
margin-bottom: 15px;
color: #3498db;
border-bottom: 2px solid #ecf0f1;
padding-bottom: 10px;
}
.message {
padding: 15px;
border-bottom: 1px solid #ecf0f1;
}
.message:last-child {
border-bottom: none;
}
.message.staff-message {
background: #f8f9fa;
border-left: 3px solid #3498db;
}
.message-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.author {
font-weight: bold;
color: #2c3e50;
}
.staff-badge {
background: #3498db;
color: white;
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
font-weight: bold;
}
.timestamp {
color: #95a5a6;
font-size: 12px;
}
.message-content {
color: #34495e;
word-break: break-word;
}
.stats-section {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.stats-section h3 {
margin-bottom: 15px;
color: #3498db;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
}
.stat-item {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
text-align: center;
}
.stat-label {
display: block;
font-size: 12px;
color: #7f8c8d;
margin-bottom: 5px;
}
.stat-value {
font-size: 18px;
font-weight: bold;
color: #3498db;
}
.footer {
text-align: center;
padding: 20px;
color: #95a5a6;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎫 Transcript de Ticket</h1>
<div class="header-meta">
<div class="meta-item">
<span>📌 ID:</span>
<span>1461089849527505151</span>
</div>
<div class="meta-item">
<span>📁 Catégorie:</span>
<span>91522e2c</span>
</div>
<div class="meta-item">
<span>⚡ Priorité:</span>
<span style="color: #3498db; font-weight: bold;">NORMAL</span>
</div>
<div class="meta-item">
<span>📊 Statut:</span>
<span class="status-badge status-closed">closed</span>
</div>
<div class="meta-item">
<span>📅 Créé:</span>
<span>14/01/2026 21:09</span>
</div>
</div>
</div>
<div class="messages-section">
<h3>💬 Messages (0)</h3>
</div>
<div class="stats-section">
<h3>📊 Statistiques</h3>
<div class="stats-grid">
<div class="stat-item">
<span class="stat-label">Durée</span>
<span class="stat-value">6 min</span>
</div>
<div class="stat-item">
<span class="stat-label">Messages</span>
<span class="stat-value">0</span>
</div>
<div class="stat-item">
<span class="stat-label">Note</span>
<span class="stat-value">Pas de note</span>
</div>
</div>
</div>
<div class="footer">
<p>Généré le 14/01/2026 à 21:15:43</p>
<p>Système de Tickets - Kuby Bot</p>
</div>
</div>
</body>
</html>