453 lines
16 KiB
Python
453 lines
16 KiB
Python
"""
|
|
Ticket Formatters
|
|
=================
|
|
Beautiful embed and message formatters for the ticket system.
|
|
"""
|
|
from datetime import datetime
|
|
from typing import Optional, List, Dict, Any
|
|
from discord import Embed, Color, User, Member, Role
|
|
|
|
from ..data.models import (
|
|
TicketData,
|
|
TicketCategory,
|
|
TicketStats,
|
|
GuildTicketConfig,
|
|
TicketStatus,
|
|
Priority
|
|
)
|
|
|
|
|
|
class TicketEmbedFormatter:
|
|
"""
|
|
Creates beautiful, consistent embeds for the ticket system.
|
|
|
|
Features:
|
|
- Color-coded by priority
|
|
- Consistent styling
|
|
- Rich information display
|
|
- Translatable text
|
|
"""
|
|
|
|
# Priority colors
|
|
PRIORITY_COLORS = {
|
|
Priority.LOW: Color.green(),
|
|
Priority.NORMAL: Color.blue(),
|
|
Priority.HIGH: Color.orange(),
|
|
Priority.URGENT: Color.red()
|
|
}
|
|
|
|
# Status emojis
|
|
STATUS_EMOJIS = {
|
|
TicketStatus.OPEN: "🟢",
|
|
TicketStatus.CLAIMED: "🟡",
|
|
TicketStatus.CLOSED: "🔴",
|
|
TicketStatus.ARCHIVED: "⚫"
|
|
}
|
|
|
|
# Priority emojis
|
|
PRIORITY_EMOJIS = {
|
|
Priority.LOW: "🟢",
|
|
Priority.NORMAL: "🔵",
|
|
Priority.HIGH: "🟠",
|
|
Priority.URGENT: "🔴"
|
|
}
|
|
|
|
@classmethod
|
|
def get_priority_color(cls, priority: Priority) -> Color:
|
|
"""Get embed color based on priority"""
|
|
return cls.PRIORITY_COLORS.get(priority, Color.blue())
|
|
|
|
@classmethod
|
|
def format_status(cls, status: TicketStatus) -> str:
|
|
"""Get formatted status with emoji"""
|
|
emoji = cls.STATUS_EMOJIS.get(status, "⚪")
|
|
status_names = {
|
|
TicketStatus.OPEN: "Ouvert",
|
|
TicketStatus.CLAIMED: "Réclamé",
|
|
TicketStatus.CLOSED: "Fermé",
|
|
TicketStatus.ARCHIVED: "Archivé"
|
|
}
|
|
return f"{emoji} {status_names.get(status, status.value)}"
|
|
|
|
@classmethod
|
|
def format_priority(cls, priority: Priority) -> str:
|
|
"""Get formatted priority with emoji"""
|
|
emoji = cls.PRIORITY_EMOJIS.get(priority, "⚪")
|
|
priority_names = {
|
|
Priority.LOW: "Basse",
|
|
Priority.NORMAL: "Normale",
|
|
Priority.HIGH: "Haute",
|
|
Priority.URGENT: "Urgente"
|
|
}
|
|
return f"{emoji} {priority_names.get(priority, priority.value)}"
|
|
|
|
# --- Ticket Creation Embeds ---
|
|
|
|
@classmethod
|
|
def ticket_created(
|
|
cls,
|
|
ticket: TicketData,
|
|
user: User,
|
|
category: Optional[TicketCategory] = None,
|
|
guild_name: str = "Serveur"
|
|
) -> Embed:
|
|
"""Embed for when a ticket is created"""
|
|
embed = Embed(
|
|
title="🎫 Nouveau Ticket Créé",
|
|
description=category.welcome_message if category else "Merci d'avoir créé un ticket. Notre équipe va vous répondre sous peu.",
|
|
color=cls.get_priority_color(ticket.priority),
|
|
timestamp=datetime.now()
|
|
)
|
|
|
|
embed.set_author(name=user.display_name, icon_url=user.display_avatar.url)
|
|
|
|
# Add fields
|
|
embed.add_field(name="📌 Ticket ID", value=f"`{ticket.channel_id}`", inline=True)
|
|
embed.add_field(name="👤 Utilisateur", value=user.mention, inline=True)
|
|
embed.add_field(name="📁 Catégorie", value=category.name if category else ticket.category_id, inline=True)
|
|
embed.add_field(name="⚡ Priorité", value=cls.format_priority(ticket.priority), inline=True)
|
|
|
|
if ticket.reason:
|
|
embed.add_field(name="📝 Raison", value=ticket.reason, inline=False)
|
|
|
|
embed.set_footer(text=f"ID: {ticket.channel_id} | {guild_name}")
|
|
|
|
return embed
|
|
|
|
@classmethod
|
|
def ticket_closed(
|
|
cls,
|
|
ticket: TicketData,
|
|
closed_by: User,
|
|
duration_minutes: float,
|
|
reason: Optional[str] = None
|
|
) -> Embed:
|
|
"""Embed for when a ticket is closed"""
|
|
embed = Embed(
|
|
title="🔴 Ticket Fermé",
|
|
description=f"Ce ticket a été fermé par {closed_by.mention}",
|
|
color=Color.red(),
|
|
timestamp=datetime.now()
|
|
)
|
|
|
|
embed.add_field(name="👤 Fermé par", value=closed_by.mention, inline=True)
|
|
embed.add_field(name="⏱️ Durée", value=cls._format_duration(duration_minutes), inline=True)
|
|
embed.add_field(name="📊 Statut", value=cls.format_status(ticket.status), inline=True)
|
|
|
|
if reason:
|
|
embed.add_field(name="📝 Raison de fermeture", value=reason, inline=False)
|
|
|
|
return embed
|
|
|
|
@classmethod
|
|
def ticket_reopened(cls, ticket: TicketData, reopened_by: User) -> Embed:
|
|
"""Embed for when a ticket is reopened"""
|
|
embed = Embed(
|
|
title="🟢 Ticket Rouvert",
|
|
description=f"Ce ticket a été rouvert par {reopened_by.mention}",
|
|
color=Color.green(),
|
|
timestamp=datetime.now()
|
|
)
|
|
|
|
embed.add_field(name="👤 Rouvert par", value=reopened_by.mention, inline=True)
|
|
embed.add_field(name="📊 Statut", value=cls.format_status(ticket.status), inline=True)
|
|
|
|
return embed
|
|
|
|
@classmethod
|
|
def ticket_claimed(cls, ticket: TicketData, claimed_by: User) -> Embed:
|
|
"""Embed for when a ticket is claimed"""
|
|
embed = Embed(
|
|
title="🟡 Ticket Réclamé",
|
|
description=f"Ce ticket a été réclamé par {claimed_by.mention}",
|
|
color=Color.gold(),
|
|
timestamp=datetime.now()
|
|
)
|
|
|
|
embed.add_field(name="👤 Réclamé par", value=claimed_by.mention, inline=True)
|
|
embed.add_field(name="📊 Statut", value=cls.format_status(ticket.status), inline=True)
|
|
|
|
return embed
|
|
|
|
@classmethod
|
|
def ticket_unclaimed(cls, ticket: TicketData, unclaimed_by: User) -> Embed:
|
|
"""Embed for when a ticket claim is removed"""
|
|
embed = Embed(
|
|
title="🟠 Ticket Non Réclamé",
|
|
description=f"Le ticket n'est plus réclamé par {unclaimed_by.mention}",
|
|
color=Color.orange(),
|
|
timestamp=datetime.now()
|
|
)
|
|
|
|
return embed
|
|
|
|
# --- Panel Embeds ---
|
|
|
|
@classmethod
|
|
def panel_embed(
|
|
cls,
|
|
config: GuildTicketConfig,
|
|
guild_name: str
|
|
) -> Embed:
|
|
"""Embed for the ticket creation panel"""
|
|
embed = Embed(
|
|
title="🎫 Support - Créer un Ticket",
|
|
description="Cliquez sur le bouton ci-dessous pour créer un ticket. Notre équipe vous répondra dans les plus brefs délais.",
|
|
color=Color.blue(),
|
|
timestamp=datetime.now()
|
|
)
|
|
|
|
# Add categories
|
|
if config.categories:
|
|
categories_text = "\n".join(
|
|
f"{cat.emoji} **{cat.name}**\n{cat.description}"
|
|
for cat in config.categories
|
|
)
|
|
embed.add_field(name="📁 Catégories disponibles", value=categories_text, inline=False)
|
|
|
|
embed.set_footer(text=f"ID: {config.guild_id} | {guild_name}")
|
|
|
|
return embed
|
|
|
|
@classmethod
|
|
def category_select_embed(cls, categories: List[TicketCategory]) -> Embed:
|
|
"""Embed shown when selecting a category"""
|
|
embed = Embed(
|
|
title="📁 Choisir une catégorie",
|
|
description="Sélectionnez la catégorie qui correspond le mieux à votre demande:",
|
|
color=Color.blue(),
|
|
timestamp=datetime.now()
|
|
)
|
|
|
|
for cat in categories:
|
|
embed.add_field(
|
|
name=f"{cat.emoji} {cat.name}",
|
|
value=cat.description or "Aucune description",
|
|
inline=False
|
|
)
|
|
|
|
return embed
|
|
|
|
# --- Admin Embeds ---
|
|
|
|
@classmethod
|
|
def ticket_list_embed(
|
|
cls,
|
|
tickets: List[TicketData],
|
|
user: Optional[User] = None
|
|
) -> Embed:
|
|
"""Embed showing a list of tickets"""
|
|
embed = Embed(
|
|
title="📋 Liste des Tickets",
|
|
description=f"Nombre de tickets: **{len(tickets)}**",
|
|
color=Color.blue()
|
|
)
|
|
|
|
if user:
|
|
embed.set_author(name=f"Tickets de {user.display_name}", icon_url=user.display_avatar.url)
|
|
|
|
# Group by status
|
|
by_status = {}
|
|
for ticket in tickets:
|
|
status = cls.format_status(ticket.status)
|
|
by_status.setdefault(status, []).append(ticket)
|
|
|
|
for status, status_tickets in by_status.items():
|
|
ticket_list = "\n".join(
|
|
f"`{t.channel_id}` - {t.category_id} ({cls.format_priority(t.priority)})"
|
|
for t in status_tickets[:10] # Limit to 10 per status
|
|
)
|
|
if len(status_tickets) > 10:
|
|
ticket_list += f"\n... et {len(status_tickets) - 10} autres"
|
|
|
|
embed.add_field(name=status, value=ticket_list or "Aucun", inline=False)
|
|
|
|
return embed
|
|
|
|
@classmethod
|
|
def stats_embed(cls, stats: TicketStats, guild_name: str) -> Embed:
|
|
"""Embed showing ticket statistics"""
|
|
embed = Embed(
|
|
title="📊 Statistiques des Tickets",
|
|
color=Color.blue(),
|
|
timestamp=datetime.now()
|
|
)
|
|
|
|
embed.set_author(name=guild_name)
|
|
|
|
# Main stats
|
|
embed.add_field(name="📝 Total", value=str(stats.total_tickets), inline=True)
|
|
embed.add_field(name="🟢 Ouverts", value=str(stats.open_tickets), inline=True)
|
|
embed.add_field(name="🔴 Fermés", value=str(stats.closed_tickets), inline=True)
|
|
|
|
embed.add_field(name="⏱️ Durée Moyenne", value=cls._format_duration(stats.average_duration_minutes), inline=True)
|
|
embed.add_field(name="⭐ Note Moyenne", value=f"{stats.average_rating:.1f}/5" if stats.average_rating else "N/A", inline=True)
|
|
|
|
# By category
|
|
if stats.tickets_by_category:
|
|
cat_text = "\n".join(f"{cat}: **{count}**" for cat, count in stats.tickets_by_category.items())
|
|
embed.add_field(name="📁 Par Catégorie", value=cat_text, inline=False)
|
|
|
|
# By priority
|
|
if stats.tickets_by_priority:
|
|
priority_text = "\n".join(f"{prio}: **{count}**" for prio, count in stats.tickets_by_priority.items())
|
|
embed.add_field(name="⚡ Par Priorité", value=priority_text, inline=False)
|
|
|
|
return embed
|
|
|
|
@classmethod
|
|
def ticket_info_embed(
|
|
cls,
|
|
ticket: TicketData,
|
|
user: Optional[User] = None,
|
|
category: Optional[TicketCategory] = None
|
|
) -> Embed:
|
|
"""Embed showing detailed ticket information"""
|
|
embed = Embed(
|
|
title=f"🎫 Informations du Ticket",
|
|
color=cls.get_priority_color(ticket.priority),
|
|
timestamp=datetime.now()
|
|
)
|
|
|
|
if user:
|
|
embed.set_author(name=user.display_name, icon_url=user.display_avatar.url)
|
|
|
|
embed.add_field(name="📌 ID", value=f"`{ticket.channel_id}`", inline=True)
|
|
embed.add_field(name="📊 Statut", value=cls.format_status(ticket.status), inline=True)
|
|
embed.add_field(name="⚡ Priorité", value=cls.format_priority(ticket.priority), inline=True)
|
|
embed.add_field(name="📁 Catégorie", value=category.name if category else ticket.category_id, inline=True)
|
|
|
|
embed.add_field(name="📅 Créé le", value=ticket.created_at.strftime("%d/%m/%Y à %H:%M"), inline=True)
|
|
|
|
if ticket.closed_at:
|
|
embed.add_field(name="🔴 Fermé le", value=ticket.closed_at.strftime("%d/%m/%Y à %H:%M"), inline=True)
|
|
|
|
if ticket.claimed_by:
|
|
embed.add_field(name="👤 Réclamé par", value=f"<@{ticket.claimed_by}>", inline=True)
|
|
|
|
if ticket.reason:
|
|
embed.add_field(name="📝 Raison", value=ticket.reason, inline=False)
|
|
|
|
if ticket.duration_minutes:
|
|
embed.add_field(name="⏱️ Durée", value=cls._format_duration(ticket.duration_minutes), inline=True)
|
|
|
|
if ticket.rating:
|
|
stars = "⭐" * ticket.rating
|
|
embed.add_field(name="⭐ Note", value=f"{stars} ({ticket.rating}/5)", inline=True)
|
|
|
|
return embed
|
|
|
|
# --- Survey Embeds ---
|
|
|
|
@classmethod
|
|
def survey_embed(cls) -> Embed:
|
|
"""Embed for satisfaction survey"""
|
|
embed = Embed(
|
|
title="⭐ Évaluation du Support",
|
|
description="Merci d'avoir utilisé notre système de tickets! "
|
|
"Pour améliorer nos services, merci de noter votre expérience.",
|
|
color=Color.gold(),
|
|
timestamp=datetime.now()
|
|
)
|
|
|
|
embed.add_field(name="1 ⭐", value="Très insatisfait", inline=True)
|
|
embed.add_field(name="3 ⭐", value="Neutre", inline=True)
|
|
embed.add_field(name="5 ⭐", value="Très satisfait", inline=True)
|
|
|
|
return embed
|
|
|
|
@classmethod
|
|
def survey_thanks_embed(cls, rating: int) -> Embed:
|
|
"""Embed shown after submitting a survey"""
|
|
stars = "⭐" * rating
|
|
embed = Embed(
|
|
title="Merci pour votre évaluation!",
|
|
description=f"Vous avez attribué: {stars}",
|
|
color=Color.green()
|
|
)
|
|
return embed
|
|
|
|
# --- Helper Methods ---
|
|
|
|
@staticmethod
|
|
def _format_duration(minutes: float) -> str:
|
|
"""Format duration in minutes to human-readable string"""
|
|
if minutes < 1:
|
|
return f"{minutes * 60:.0f}s"
|
|
elif minutes < 60:
|
|
return f"{minutes:.0f}min"
|
|
elif minutes < 1440:
|
|
hours = minutes / 60
|
|
return f"{hours:.1f}h"
|
|
else:
|
|
days = minutes / 1440
|
|
return f"{days:.1f}j"
|
|
|
|
|
|
class TicketMessageFormatter:
|
|
"""Formats regular messages for the ticket system"""
|
|
|
|
@staticmethod
|
|
def welcome_message(
|
|
user: User,
|
|
category: TicketCategory
|
|
) -> str:
|
|
"""Welcome message when entering a ticket"""
|
|
return (
|
|
f"👋 Bienvenue {user.mention} dans votre ticket **{category.name}**!\n\n"
|
|
f"{category.welcome_message}\n\n"
|
|
f"Un membre de notre équipe va vous répondre sous peu."
|
|
)
|
|
|
|
@staticmethod
|
|
def staff_notification(
|
|
claimed_by: User,
|
|
ticket_channel_mention: str
|
|
) -> str:
|
|
"""Notification when a ticket is claimed"""
|
|
return (
|
|
f"🟡 Le ticket {ticket_channel_mention} a été réclamé par {claimed_by.mention}"
|
|
)
|
|
|
|
@staticmethod
|
|
def ticket_closed_notification(
|
|
closed_by: User,
|
|
reason: Optional[str] = None
|
|
) -> str:
|
|
"""Notification when a ticket is closed"""
|
|
msg = f"🔴 Ce ticket a été fermé par {closed_by.mention}"
|
|
if reason:
|
|
msg += f"\n📝 Raison: {reason}"
|
|
return msg
|
|
|
|
@staticmethod
|
|
def auto_close_warning(
|
|
days_remaining: int,
|
|
ticket_channel_mention: str
|
|
) -> str:
|
|
"""Warning before auto-closing a ticket"""
|
|
return (
|
|
f"⚠️ Ce ticket {ticket_channel_mention} sera fermé automatiquement "
|
|
f"dans **{days_remaining} jour(s)** en raison de son inactivity."
|
|
)
|
|
|
|
@staticmethod
|
|
def transcript_saved(filepath: str) -> str:
|
|
"""Message when transcript is saved"""
|
|
return f"📄 Transcript sauvegardé: `{filepath}`"
|
|
|
|
@staticmethod
|
|
def no_permission(action: str) -> str:
|
|
"""Message when user has no permission"""
|
|
return f"❌ Vous n'avez pas la permission de {action}"
|
|
|
|
@staticmethod
|
|
def ticket_limit_reached(limit: int) -> str:
|
|
"""Message when user has reached ticket limit"""
|
|
return f"❌ Vous avez atteint la limite de {limit} tickets ouverts"
|
|
|
|
@staticmethod
|
|
def already_has_ticket(channel_mention: str) -> str:
|
|
"""Message when user already has a ticket"""
|
|
return f"❌ Vous avez déjà un ticket ouvert: {channel_mention}"
|
|
|