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

16
bot.py
View file

@ -1,4 +1,6 @@
import discord import discord
from commandes.ticket.utils.permissions import can_access_config
from commandes.ticket.data.storage import get_storage
from discord.ext import commands from discord.ext import commands
import os import os
import asyncio import asyncio
@ -78,6 +80,19 @@ class MyBot(commands.Bot):
if await self.is_owner(ctx.author): if await self.is_owner(ctx.author):
return True 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 # 2. Récupération du Cog
whitelist_cog = self.get_cog("WhitelistMonitor") whitelist_cog = self.get_cog("WhitelistMonitor")
if not whitelist_cog: if not whitelist_cog:
@ -190,4 +205,3 @@ class MyBot(commands.Bot):
kuby_logger.error(f"Command error in {ctx.command}: {str(error)}", exc_info=True) kuby_logger.error(f"Command error in {ctx.command}: {str(error)}", exc_info=True)
bot = MyBot() bot = MyBot()
bot.run(os.getenv("DISCORD_TOKEN"))

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 os
import sys import sys
import io import io
import asyncio
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
from collections import defaultdict 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 import discord
from discord.ext import commands, tasks from discord.ext import commands, tasks
import traceback
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
from .data.storage import get_storage from .data.storage import get_storage
@ -34,6 +36,8 @@ from .data.models import (
) )
from .utils.formatters import TicketEmbedFormatter from .utils.formatters import TicketEmbedFormatter
from .staff_analytics import StaffAnalytics, StaffStatsView 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): class TicketPanelView(discord.ui.View):
@ -93,6 +97,9 @@ class TicketPanelView(discord.ui.View):
async def _create_callback(self, interaction: discord.Interaction): async def _create_callback(self, interaction: discord.Interaction):
"""Affiche les catégories pour créer un ticket""" """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): 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) await interaction.response.send_message("❌ Vous n'êtes pas autorisé à utiliser cette fonctionnalité. Veuillez demander à être ajouté à la whitelist.", ephemeral=True)
return return
@ -125,6 +132,9 @@ class TicketPanelView(discord.ui.View):
async def _my_tickets_callback(self, interaction: discord.Interaction): async def _my_tickets_callback(self, interaction: discord.Interaction):
"""Affiche les tickets de l'utilisateur""" """Affiche les tickets de l'utilisateur"""
# Reload config
self.config = self.storage.load_config(interaction.guild_id)
if not self._is_whitelisted(interaction): 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) await interaction.response.send_message("❌ Vous n'êtes pas autorisé à utiliser cette fonctionnalité. Veuillez demander à être ajouté à la whitelist.", ephemeral=True)
return return
@ -155,6 +165,9 @@ class TicketPanelView(discord.ui.View):
async def _analytics_callback(self, interaction: discord.Interaction): async def _analytics_callback(self, interaction: discord.Interaction):
"""Affiche les analytics (staff uniquement)""" """Affiche les analytics (staff uniquement)"""
# Reload config
self.config = self.storage.load_config(interaction.guild_id)
if not self._is_staff(interaction): if not self._is_staff(interaction):
await interaction.response.send_message("❌ Cette fonctionnalité est réservée au staff.", ephemeral=True) await interaction.response.send_message("❌ Cette fonctionnalité est réservée au staff.", ephemeral=True)
return return
@ -233,6 +246,9 @@ class TicketPanelView(discord.ui.View):
async def _config_callback(self, interaction: discord.Interaction): async def _config_callback(self, interaction: discord.Interaction):
"""Ouvre le panel de configuration (admin ou rôles config uniquement)""" """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): 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) await interaction.response.send_message("❌ Vous n'êtes pas autorisé à utiliser cette fonctionnalité. Veuillez demander à être ajouté à la whitelist.", ephemeral=True)
return return
@ -253,53 +269,13 @@ class TicketPanelView(discord.ui.View):
def _is_staff(self, interaction: discord.Interaction) -> bool: def _is_staff(self, interaction: discord.Interaction) -> bool:
"""Vérifie si l'utilisateur est staff""" return is_staff(interaction, self.config)
# 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
def _can_access_config(self, interaction: discord.Interaction) -> bool: def _can_access_config(self, interaction: discord.Interaction) -> bool:
"""Vérifie si l'utilisateur peut accéder à la configuration""" return can_access_config(interaction, self.config)
# 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: def _is_whitelisted(self, interaction: discord.Interaction) -> bool:
"""Vérifie si l'utilisateur est whitelisté ou a les permissions staff/config""" return is_whitelisted(interaction, self.config, self.bot)
# 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 _calculate_stats(self, guild_id: int) -> TicketStats: def _calculate_stats(self, guild_id: int) -> TicketStats:
"""Calcule les statistiques""" """Calcule les statistiques"""
@ -340,121 +316,46 @@ class TicketPanelView(discord.ui.View):
return stats 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): class CategorySelectionView(discord.ui.View):
"""Sélection de catégorie pour créer un ticket""" """Sélection de catégorie pour créer un ticket"""
def __init__(self, bot, config: GuildTicketConfig): def __init__(self, bot, config: GuildTicketConfig):
super().__init__(timeout=None) super().__init__(timeout=300) # 5 minutes timeout for ephemeral menu
self.bot = bot self.bot = bot
self.config = config self.config = config
self.id_gen = int(datetime.now().timestamp())
# Ajouter boutons de catégories # Ajouter boutons de catégories
for category in config.categories[:5]: for category in config.categories[:5]:
btn = discord.ui.Button( self.add_item(CategoryButton(category, self))
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)
def _is_staff(self, interaction: discord.Interaction) -> bool: def _is_staff(self, interaction: discord.Interaction) -> bool:
"""Vérifie si l'utilisateur est staff""" return is_staff(interaction, self.config)
# 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: def _can_access_config(self, interaction: discord.Interaction) -> bool:
"""Vérifie si l'utilisateur peut accéder à la configuration""" return can_access_config(interaction, self.config)
# 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: def _is_whitelisted(self, interaction: discord.Interaction) -> bool:
"""Vérifie si l'utilisateur est whitelisté ou a les permissions staff/config""" return is_whitelisted(interaction, self.config, self.bot)
# 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
async def _on_category_select(self, interaction: discord.Interaction, category_id: str): async def _on_category_select(self, interaction: discord.Interaction, category_id: str):
"""Handle category selection""" """Handle category selection"""
try:
if not self._is_whitelisted(interaction): 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) await interaction.response.send_message("❌ Vous n'êtes pas autorisé à utiliser cette fonctionnalité. Veuillez demander à être ajouté à la whitelist.", ephemeral=True)
return return
@ -479,6 +380,13 @@ class CategorySelectionView(discord.ui.View):
# Show priority modal # Show priority modal
modal = TicketPriorityModal(self.bot, self.config, category) modal = TicketPriorityModal(self.bot, self.config, category)
await interaction.response.send_modal(modal) 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): class TicketPriorityModal(discord.ui.Modal):
@ -598,7 +506,18 @@ class TicketPriorityModal(discord.ui.Modal):
# Add management buttons # Add management buttons
view = TicketManagementView(self.bot, storage, self.config, ticket_data) 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 # Log
if self.config.log_channel_id: if self.config.log_channel_id:
@ -667,9 +586,38 @@ class TicketManagementView(discord.ui.View):
self.add_item(reopen_btn) self.add_item(reopen_btn)
async def _close_callback(self, interaction: discord.Interaction): async def _close_callback(self, interaction: discord.Interaction):
"""Ferme le ticket""" """Demande confirmation avant de fermer le ticket"""
modal = CloseTicketModal(self.bot, self.storage, self.config, self.ticket) # Vérifier si l'utilisateur peut fermer le ticket
await interaction.response.send_modal(modal) 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): async def _transcript_callback(self, interaction: discord.Interaction):
"""Génère le transcript""" """Génère le transcript"""
@ -778,6 +726,215 @@ class TicketManagementView(discord.ui.View):
await interaction.channel.send(embed=embed) await interaction.channel.send(embed=embed)
await interaction.response.send_message("Ticket rouvert!", ephemeral=True) 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
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
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: def _is_staff(self, interaction: discord.Interaction) -> bool:
guild = interaction.guild guild = interaction.guild
user = interaction.user user = interaction.user
@ -1494,14 +1651,23 @@ class ConfigPanelView(discord.ui.View):
add_cat_btn.callback = self._add_cat_callback add_cat_btn.callback = self._add_cat_callback
self.add_item(add_cat_btn) self.add_item(add_cat_btn)
# Staff roles # Staff permissions
roles_btn = discord.ui.Button( staff_btn = discord.ui.Button(
style=discord.ButtonStyle.secondary, style=discord.ButtonStyle.secondary,
label="Rôles Staff", label="Permissions Staff",
custom_id="config_roles" custom_id="config_staff_perms"
) )
roles_btn.callback = self._roles_callback staff_btn.callback = self._staff_perms_callback
self.add_item(roles_btn) 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 channel
log_btn = discord.ui.Button( log_btn = discord.ui.Button(
@ -1529,24 +1695,47 @@ class ConfigPanelView(discord.ui.View):
modal = AddCategoryModal(self.bot, self.storage, self.config) modal = AddCategoryModal(self.bot, self.storage, self.config)
await interaction.response.send_modal(modal) await interaction.response.send_modal(modal)
async def _roles_callback(self, interaction: discord.Interaction): async def _staff_perms_callback(self, interaction: discord.Interaction):
view = StaffRolesView(self.bot, self.storage, self.config, interaction.guild) view = PermissionConfigView(self.bot, self.storage, self.config, interaction.guild, "staff")
embed = discord.Embed( embed = discord.Embed(
title="Rôles Staff", title="Permissions Staff",
description="Sélectionnez les rôles qui peuvent gérer les tickets:", description="Configurez qui peut gérer les tickets (répondre, fermer, etc.)",
color=discord.Color.blue() 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 = [] role_mentions = []
for role_id in self.config.staff_roles: for role_id in roles:
role = interaction.guild.get_role(int(role_id)) role = guild.get_role(int(role_id))
if role: if role:
role_mentions.append(role.mention) role_mentions.append(role.mention)
embed.add_field(name="Actuels", value=", ".join(role_mentions) if role_mentions else "Aucun", inline=False) 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}>")
await interaction.response.send_message(embed=embed, view=view, ephemeral=True) 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): async def _log_channel_callback(self, interaction: discord.Interaction):
modal = SetChannelModal(self.bot, self.storage, self.config, "log") 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) 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): class PermissionConfigView(discord.ui.View):
"""Sélection des rôles staff (alternative avec select menu)""" """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) super().__init__(timeout=None)
self.bot = bot self.bot = bot
self.storage = storage self.storage = storage
self.config = config self.config = config
self.guild = guild self.guild = guild
self.config_type = config_type # "staff" or "config"
# Le select sera populated dans le callback # Add components
self._build_select(guild) self._add_role_select()
self._add_user_select()
def _build_select(self, guild: discord.Guild): def _add_role_select(self):
if not guild: select = discord.ui.RoleSelect(
return placeholder=f"Sélectionnez les rôles {self.config_type}",
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
)
)
if options:
select = discord.ui.Select(
placeholder="Sélectionnez les rôles staff...",
options=options[:25],
min_values=0, min_values=0,
max_values=min(25, len(options)) max_values=25,
row=0
) )
select.callback = self._select_callback select.callback = self._role_callback
self.add_item(select) self.add_item(select)
async def _select_callback(self, interaction: discord.Interaction): def _add_user_select(self):
selected = list(interaction.data.get("values", [])) select = discord.ui.UserSelect(
self.config.staff_roles = selected 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
# 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.
# 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) self.storage.save_config(interaction.guild_id, self.config)
role_mentions = [] await interaction.response.send_message(
for role_id in selected: f"✅ **{added_count}** rôle(s) ajouté(s) à la configuration {self.config_type}.",
role = interaction.guild.get_role(int(role_id)) ephemeral=True
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()
) )
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): class TicketCommands(commands.Cog):
@ -1808,7 +2165,7 @@ class TicketCommands(commands.Cog):
self.bot = bot self.bot = bot
self.storage = get_storage() self.storage = get_storage()
def cog_unload(self): async def cog_unload(self):
pass pass
async def cog_load(self): async def cog_load(self):
@ -1892,23 +2249,16 @@ class TicketCommands(commands.Cog):
"""Ouvre le panel de configuration complet du système de tickets""" """Ouvre le panel de configuration complet du système de tickets"""
await ctx.defer(ephemeral=True) await ctx.defer(ephemeral=True)
# Reload config
config = self.storage.load_config(ctx.guild.id) config = self.storage.load_config(ctx.guild.id)
# Vérifier si l'utilisateur peut accéder à la configuration # Vérifier permissions (admin ou config roles/users)
can_access = ctx.author.guild_permissions.administrator if not can_access_config(ctx, config):
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:
await ctx.send("❌ Cette commande est réservée aux administrateurs ou aux rôles configurés.", ephemeral=True) await ctx.send("❌ Cette commande est réservée aux administrateurs ou aux rôles configurés.", ephemeral=True)
return return
embed = discord.Embed( embed = discord.Embed(
title="⚙️ Configuration du Système de Tickets", title="⚙️ Configuration du Système de Tickets",
description="Configurez complètement votre système de tickets depuis cette interface.\n\n" 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 log_channel_id: Optional[int] = None
archive_channel_id: Optional[int] = None archive_channel_id: Optional[int] = None
staff_roles: List[str] = field(default_factory=list) 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_roles: List[str] = field(default_factory=list)
config_users: List[int] = field(default_factory=list)
max_tickets_per_user: int = 5 max_tickets_per_user: int = 5
auto_close_enabled: bool = False auto_close_enabled: bool = False
auto_close_days: int = 7 auto_close_days: int = 7
@ -312,8 +314,12 @@ class GuildTicketConfig:
"default_category": self.default_category, "default_category": self.default_category,
"log_channel_id": self.log_channel_id, "log_channel_id": self.log_channel_id,
"archive_channel_id": self.archive_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_roles": self.staff_roles,
"staff_users": self.staff_users,
"config_roles": self.config_roles, "config_roles": self.config_roles,
"config_users": self.config_users,
"max_tickets_per_user": self.max_tickets_per_user, "max_tickets_per_user": self.max_tickets_per_user,
"auto_close_enabled": self.auto_close_enabled, "auto_close_enabled": self.auto_close_enabled,
"auto_close_days": self.auto_close_days, "auto_close_days": self.auto_close_days,
@ -335,8 +341,11 @@ class GuildTicketConfig:
default_category=data.get("default_category"), default_category=data.get("default_category"),
log_channel_id=data.get("log_channel_id"), log_channel_id=data.get("log_channel_id"),
archive_channel_id=data.get("archive_channel_id"), archive_channel_id=data.get("archive_channel_id"),
staff_roles=data.get("staff_roles", []), staff_roles=data.get("staff_roles", []),
staff_users=data.get("staff_users", []),
config_roles=data.get("config_roles", []), config_roles=data.get("config_roles", []),
config_users=data.get("config_users", []),
max_tickets_per_user=data.get("max_tickets_per_user", 5), max_tickets_per_user=data.get("max_tickets_per_user", 5),
auto_close_enabled=data.get("auto_close_enabled", False), auto_close_enabled=data.get("auto_close_enabled", False),
auto_close_days=data.get("auto_close_days", 7), 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]]: async def get_staff_list(self, guild_id: int) -> List[Dict[str, Any]]:
"""Get list of staff members with their stats""" """Get list of staff members with their stats"""
config = self.storage.load_config(guild_id) 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) tickets = self.storage.get_guild_tickets(guild_id)
staff_stats = defaultdict(lambda: { 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>