498 lines
19 KiB
Python
498 lines
19 KiB
Python
|
|
"""
|
||
|
|
Staff Analytics Command for Ticket System
|
||
|
|
==========================================
|
||
|
|
A modular command for staff to view ticket analytics with beautiful graphics.
|
||
|
|
Shows response times, ratings, comments, and other key metrics.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import os
|
||
|
|
import sys
|
||
|
|
import io
|
||
|
|
from datetime import datetime, timedelta
|
||
|
|
from collections import defaultdict
|
||
|
|
from typing import List, Dict, Any, Optional
|
||
|
|
from PIL import Image, ImageDraw, ImageFont
|
||
|
|
|
||
|
|
# Add parent directory to path for imports
|
||
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||
|
|
|
||
|
|
import discord
|
||
|
|
from discord.ext import commands
|
||
|
|
from discord import app_commands
|
||
|
|
|
||
|
|
from .data.storage import get_storage
|
||
|
|
from .data.models import TicketData, TicketStatus, GuildTicketConfig
|
||
|
|
|
||
|
|
|
||
|
|
class TicketAnalytics(commands.Cog):
|
||
|
|
"""Analytics command for staff to view ticket statistics and ratings"""
|
||
|
|
|
||
|
|
def __init__(self, bot):
|
||
|
|
self.bot = bot
|
||
|
|
self.storage = get_storage()
|
||
|
|
|
||
|
|
def _is_staff(self, interaction: discord.Interaction) -> bool:
|
||
|
|
"""Check if user is staff"""
|
||
|
|
guild = interaction.guild
|
||
|
|
user = interaction.user
|
||
|
|
config = self.storage.load_config(guild.id)
|
||
|
|
|
||
|
|
for role_id in config.staff_roles:
|
||
|
|
role = guild.get_role(int(role_id))
|
||
|
|
if role and role in user.roles:
|
||
|
|
return True
|
||
|
|
|
||
|
|
return user.guild_permissions.administrator
|
||
|
|
|
||
|
|
def _calculate_comprehensive_stats(self, guild_id: int) -> Dict[str, Any]:
|
||
|
|
"""Calculate comprehensive statistics for analytics"""
|
||
|
|
tickets = self.storage.get_guild_tickets(guild_id)
|
||
|
|
|
||
|
|
stats = {
|
||
|
|
'total_tickets': len(tickets),
|
||
|
|
'open_tickets': 0,
|
||
|
|
'closed_tickets': 0,
|
||
|
|
'claimed_tickets': 0,
|
||
|
|
'ratings': [],
|
||
|
|
'response_times': [],
|
||
|
|
'feedbacks': [],
|
||
|
|
'tickets_by_day': defaultdict(int),
|
||
|
|
'tickets_by_category': defaultdict(int),
|
||
|
|
'tickets_by_priority': defaultdict(int),
|
||
|
|
'average_rating': 0,
|
||
|
|
'average_response_time': 0,
|
||
|
|
'total_response_time': 0,
|
||
|
|
'response_time_count': 0
|
||
|
|
}
|
||
|
|
|
||
|
|
for ticket in tickets:
|
||
|
|
# Basic counts
|
||
|
|
if ticket.status == TicketStatus.OPEN:
|
||
|
|
stats['open_tickets'] += 1
|
||
|
|
elif ticket.status == TicketStatus.CLAIMED:
|
||
|
|
stats['claimed_tickets'] += 1
|
||
|
|
stats['open_tickets'] += 1
|
||
|
|
elif ticket.status == TicketStatus.CLOSED:
|
||
|
|
stats['closed_tickets'] += 1
|
||
|
|
|
||
|
|
# Ratings and feedback
|
||
|
|
if ticket.rating:
|
||
|
|
stats['ratings'].append(ticket.rating)
|
||
|
|
if ticket.feedback:
|
||
|
|
stats['feedbacks'].append({
|
||
|
|
'rating': ticket.rating,
|
||
|
|
'feedback': ticket.feedback,
|
||
|
|
'user_id': ticket.user_id,
|
||
|
|
'ticket_id': ticket.channel_id
|
||
|
|
})
|
||
|
|
|
||
|
|
# Response time calculation
|
||
|
|
if ticket.claimed_by and ticket.created_at:
|
||
|
|
claimed_time = None
|
||
|
|
for msg in ticket.messages:
|
||
|
|
if msg.author_id == ticket.claimed_by and msg.is_staff:
|
||
|
|
claimed_time = msg.timestamp
|
||
|
|
break
|
||
|
|
|
||
|
|
if claimed_time:
|
||
|
|
response_time = (claimed_time - ticket.created_at).total_seconds() / 60 # minutes
|
||
|
|
stats['response_times'].append(response_time)
|
||
|
|
stats['total_response_time'] += response_time
|
||
|
|
stats['response_time_count'] += 1
|
||
|
|
|
||
|
|
# Daily stats
|
||
|
|
day_key = ticket.created_at.strftime('%Y-%m-%d')
|
||
|
|
stats['tickets_by_day'][day_key] += 1
|
||
|
|
|
||
|
|
# Category stats
|
||
|
|
stats['tickets_by_category'][ticket.category_id] += 1
|
||
|
|
|
||
|
|
# Priority stats
|
||
|
|
stats['tickets_by_priority'][ticket.priority.value] += 1
|
||
|
|
|
||
|
|
# Calculate averages
|
||
|
|
if stats['ratings']:
|
||
|
|
stats['average_rating'] = sum(stats['ratings']) / len(stats['ratings'])
|
||
|
|
|
||
|
|
if stats['response_time_count'] > 0:
|
||
|
|
stats['average_response_time'] = stats['total_response_time'] / stats['response_time_count']
|
||
|
|
|
||
|
|
return stats
|
||
|
|
|
||
|
|
def _create_rating_chart(self, ratings: List[int]) -> io.BytesIO:
|
||
|
|
"""Create a beautiful rating distribution chart using Pillow"""
|
||
|
|
# Image dimensions
|
||
|
|
width, height = 800, 500
|
||
|
|
img = Image.new('RGB', (width, height), '#2F3136')
|
||
|
|
draw = ImageDraw.Draw(img)
|
||
|
|
|
||
|
|
# Try to load a font, fallback to default if not available
|
||
|
|
try:
|
||
|
|
font_title = ImageFont.truetype("arial.ttf", 24)
|
||
|
|
font_label = ImageFont.truetype("arial.ttf", 16)
|
||
|
|
font_value = ImageFont.truetype("arial.ttf", 14)
|
||
|
|
except:
|
||
|
|
font_title = ImageFont.load_default()
|
||
|
|
font_label = ImageFont.load_default()
|
||
|
|
font_value = ImageFont.load_default()
|
||
|
|
|
||
|
|
# Count ratings
|
||
|
|
rating_counts = defaultdict(int)
|
||
|
|
for rating in ratings:
|
||
|
|
rating_counts[rating] += 1
|
||
|
|
|
||
|
|
values = [rating_counts[i] for i in range(1, 6)]
|
||
|
|
max_value = max(values) if values else 1
|
||
|
|
|
||
|
|
# Chart dimensions
|
||
|
|
chart_left = 100
|
||
|
|
chart_right = width - 50
|
||
|
|
chart_top = 80
|
||
|
|
chart_bottom = height - 100
|
||
|
|
chart_width = chart_right - chart_left
|
||
|
|
chart_height = chart_bottom - chart_top
|
||
|
|
|
||
|
|
# Draw title
|
||
|
|
title = "Distribution des Notes Clients"
|
||
|
|
draw.text((width//2, 30), title, fill='white', font=font_title, anchor='mm')
|
||
|
|
|
||
|
|
# Draw bars
|
||
|
|
bar_width = chart_width // 6
|
||
|
|
for i, value in enumerate(values):
|
||
|
|
if max_value > 0:
|
||
|
|
bar_height = int((value / max_value) * chart_height)
|
||
|
|
else:
|
||
|
|
bar_height = 0
|
||
|
|
|
||
|
|
x1 = chart_left + i * bar_width + 10
|
||
|
|
y1 = chart_bottom - bar_height
|
||
|
|
x2 = x1 + bar_width - 20
|
||
|
|
y2 = chart_bottom
|
||
|
|
|
||
|
|
# Draw bar
|
||
|
|
draw.rectangle([x1, y1, x2, y2], fill='#5865F2', outline='#FFFFFF', width=2)
|
||
|
|
|
||
|
|
# Draw value on top of bar
|
||
|
|
if value > 0:
|
||
|
|
draw.text((x1 + (x2-x1)//2, y1 - 10), str(value), fill='white', font=font_value, anchor='mm')
|
||
|
|
|
||
|
|
# Draw x-axis labels
|
||
|
|
labels = ['1 ⭐', '2 ⭐', '3 ⭐', '4 ⭐', '5 ⭐']
|
||
|
|
for i, label in enumerate(labels):
|
||
|
|
x = chart_left + i * bar_width + bar_width//2
|
||
|
|
draw.text((x, chart_bottom + 20), label, fill='white', font=font_label, anchor='mm')
|
||
|
|
|
||
|
|
# Draw y-axis label
|
||
|
|
draw.text((30, height//2), 'Nombre de Tickets', fill='white', font=font_label, anchor='mm')
|
||
|
|
|
||
|
|
# Draw grid lines
|
||
|
|
for i in range(0, 6):
|
||
|
|
y = chart_bottom - (i * chart_height // 5)
|
||
|
|
draw.line([chart_left, y, chart_right, y], fill='#FFFFFF', width=1)
|
||
|
|
|
||
|
|
# Save to buffer
|
||
|
|
buf = io.BytesIO()
|
||
|
|
img.save(buf, format='PNG')
|
||
|
|
buf.seek(0)
|
||
|
|
return buf
|
||
|
|
|
||
|
|
def _create_response_time_chart(self, response_times: List[float]) -> io.BytesIO:
|
||
|
|
"""Create response time distribution chart using Pillow"""
|
||
|
|
# Image dimensions
|
||
|
|
width, height = 800, 500
|
||
|
|
img = Image.new('RGB', (width, height), '#2F3136')
|
||
|
|
draw = ImageDraw.Draw(img)
|
||
|
|
|
||
|
|
# Try to load a font, fallback to default if not available
|
||
|
|
try:
|
||
|
|
font_title = ImageFont.truetype("arial.ttf", 20)
|
||
|
|
font_label = ImageFont.truetype("arial.ttf", 14)
|
||
|
|
font_value = ImageFont.truetype("arial.ttf", 12)
|
||
|
|
except:
|
||
|
|
font_title = ImageFont.load_default()
|
||
|
|
font_label = ImageFont.load_default()
|
||
|
|
font_value = ImageFont.load_default()
|
||
|
|
|
||
|
|
# Create histogram bins
|
||
|
|
bins = [0, 5, 15, 30, 60, 120, 240, float('inf')]
|
||
|
|
labels = ['< 5min', '5-15min', '15-30min', '30-60min', '1-2h', '2-4h', '> 4h']
|
||
|
|
|
||
|
|
hist_values = []
|
||
|
|
for i in range(len(bins) - 1):
|
||
|
|
count = sum(1 for rt in response_times if bins[i] <= rt < bins[i+1])
|
||
|
|
hist_values.append(count)
|
||
|
|
|
||
|
|
# Last bin for > 4h
|
||
|
|
hist_values.append(sum(1 for rt in response_times if rt >= bins[-2]))
|
||
|
|
|
||
|
|
max_value = max(hist_values) if hist_values else 1
|
||
|
|
|
||
|
|
# Chart dimensions
|
||
|
|
chart_left = 120
|
||
|
|
chart_right = width - 50
|
||
|
|
chart_top = 80
|
||
|
|
chart_bottom = height - 120
|
||
|
|
chart_width = chart_right - chart_left
|
||
|
|
chart_height = chart_bottom - chart_top
|
||
|
|
|
||
|
|
# Draw title
|
||
|
|
title = "Temps de Réponse Moyen"
|
||
|
|
draw.text((width//2, 30), title, fill='white', font=font_title, anchor='mm')
|
||
|
|
|
||
|
|
# Draw bars
|
||
|
|
bar_width = chart_width // len(labels)
|
||
|
|
for i, value in enumerate(hist_values):
|
||
|
|
if max_value > 0:
|
||
|
|
bar_height = int((value / max_value) * chart_height)
|
||
|
|
else:
|
||
|
|
bar_height = 0
|
||
|
|
|
||
|
|
x1 = chart_left + i * bar_width + 5
|
||
|
|
y1 = chart_bottom - bar_height
|
||
|
|
x2 = x1 + bar_width - 10
|
||
|
|
y2 = chart_bottom
|
||
|
|
|
||
|
|
# Draw bar
|
||
|
|
draw.rectangle([x1, y1, x2, y2], fill='#57F287', outline='#FFFFFF', width=2)
|
||
|
|
|
||
|
|
# Draw value on top of bar
|
||
|
|
if value > 0:
|
||
|
|
draw.text((x1 + (x2-x1)//2, y1 - 10), str(value), fill='white', font=font_value, anchor='mm')
|
||
|
|
|
||
|
|
# Draw x-axis labels
|
||
|
|
for i, label in enumerate(labels):
|
||
|
|
x = chart_left + i * bar_width + bar_width//2
|
||
|
|
# Rotate text for better fit
|
||
|
|
draw.text((x, chart_bottom + 30), label, fill='white', font=font_label, anchor='mm')
|
||
|
|
|
||
|
|
# Draw y-axis label
|
||
|
|
draw.text((40, height//2), 'Nombre de Tickets', fill='white', font=font_label, anchor='mm')
|
||
|
|
|
||
|
|
# Draw grid lines
|
||
|
|
for i in range(0, 6):
|
||
|
|
y = chart_bottom - (i * chart_height // 5)
|
||
|
|
draw.line([chart_left, y, chart_right, y], fill='#FFFFFF', width=1)
|
||
|
|
|
||
|
|
# Save to buffer
|
||
|
|
buf = io.BytesIO()
|
||
|
|
img.save(buf, format='PNG')
|
||
|
|
buf.seek(0)
|
||
|
|
return buf
|
||
|
|
|
||
|
|
def _create_trend_chart(self, tickets_by_day: Dict[str, int]) -> io.BytesIO:
|
||
|
|
"""Create ticket trend chart over time using Pillow"""
|
||
|
|
# Image dimensions
|
||
|
|
width, height = 900, 500
|
||
|
|
img = Image.new('RGB', (width, height), '#2F3136')
|
||
|
|
draw = ImageDraw.Draw(img)
|
||
|
|
|
||
|
|
# Try to load a font, fallback to default if not available
|
||
|
|
try:
|
||
|
|
font_title = ImageFont.truetype("arial.ttf", 20)
|
||
|
|
font_label = ImageFont.truetype("arial.ttf", 14)
|
||
|
|
font_value = ImageFont.truetype("arial.ttf", 12)
|
||
|
|
except:
|
||
|
|
font_title = ImageFont.load_default()
|
||
|
|
font_label = ImageFont.load_default()
|
||
|
|
font_value = ImageFont.load_default()
|
||
|
|
|
||
|
|
# Sort dates
|
||
|
|
sorted_dates = sorted(tickets_by_day.keys())
|
||
|
|
values = [tickets_by_day[date] for date in sorted_dates]
|
||
|
|
|
||
|
|
if not values:
|
||
|
|
# Return empty chart if no data
|
||
|
|
buf = io.BytesIO()
|
||
|
|
img.save(buf, format='PNG')
|
||
|
|
buf.seek(0)
|
||
|
|
return buf
|
||
|
|
|
||
|
|
max_value = max(values) if values else 1
|
||
|
|
|
||
|
|
# Chart dimensions
|
||
|
|
chart_left = 100
|
||
|
|
chart_right = width - 50
|
||
|
|
chart_top = 80
|
||
|
|
chart_bottom = height - 100
|
||
|
|
chart_width = chart_right - chart_left
|
||
|
|
chart_height = chart_bottom - chart_top
|
||
|
|
|
||
|
|
# Draw title
|
||
|
|
title = "Évolution des Tickets sur les Derniers Jours"
|
||
|
|
draw.text((width//2, 30), title, fill='white', font=font_title, anchor='mm')
|
||
|
|
|
||
|
|
# Calculate points for line chart
|
||
|
|
points = []
|
||
|
|
for i, (date_str, value) in enumerate(zip(sorted_dates, values)):
|
||
|
|
x = chart_left + (i * chart_width) // max(1, len(values) - 1)
|
||
|
|
if max_value > 0:
|
||
|
|
y = chart_bottom - int((value / max_value) * chart_height)
|
||
|
|
else:
|
||
|
|
y = chart_bottom
|
||
|
|
points.append((x, y))
|
||
|
|
|
||
|
|
# Draw line
|
||
|
|
if len(points) > 1:
|
||
|
|
draw.line(points, fill='#FEE75C', width=3, joint='curve')
|
||
|
|
|
||
|
|
# Draw points and values
|
||
|
|
for i, ((x, y), value, date_str) in enumerate(zip(points, values, sorted_dates)):
|
||
|
|
# Draw point
|
||
|
|
draw.ellipse([x-4, y-4, x+4, y+4], fill='#FEE75C', outline='#FFFFFF', width=2)
|
||
|
|
|
||
|
|
# Draw value above point
|
||
|
|
draw.text((x, y-15), str(value), fill='white', font=font_value, anchor='mm')
|
||
|
|
|
||
|
|
# Draw date label (every few points to avoid crowding)
|
||
|
|
if i % max(1, len(points)//7) == 0 or i == len(points)-1:
|
||
|
|
date_obj = datetime.strptime(date_str, '%Y-%m-%d')
|
||
|
|
date_label = date_obj.strftime('%d/%m')
|
||
|
|
draw.text((x, chart_bottom + 20), date_label, fill='white', font=font_label, anchor='mm')
|
||
|
|
|
||
|
|
# Draw y-axis label
|
||
|
|
draw.text((40, height//2), 'Nombre de Tickets', fill='white', font=font_label, anchor='mm')
|
||
|
|
|
||
|
|
# Draw grid lines
|
||
|
|
for i in range(0, 6):
|
||
|
|
y = chart_bottom - (i * chart_height // 5)
|
||
|
|
draw.line([chart_left, y, chart_right, y], fill='#FFFFFF', width=1)
|
||
|
|
|
||
|
|
# Save to buffer
|
||
|
|
buf = io.BytesIO()
|
||
|
|
img.save(buf, format='PNG')
|
||
|
|
buf.seek(0)
|
||
|
|
return buf
|
||
|
|
|
||
|
|
@app_commands.command(name="ticket_analytics", description="Affiche les statistiques détaillées des tickets (Staff uniquement)")
|
||
|
|
@app_commands.describe(period="Période d'analyse (7d, 30d, 90d, all)")
|
||
|
|
async def ticket_analytics(self, interaction: discord.Interaction, period: str = "30d"):
|
||
|
|
"""Display comprehensive ticket analytics for staff"""
|
||
|
|
await interaction.response.defer(ephemeral=True)
|
||
|
|
|
||
|
|
# Check permissions
|
||
|
|
if not self._is_staff(interaction):
|
||
|
|
await interaction.followup.send("❌ Cette commande est réservée au staff.", ephemeral=True)
|
||
|
|
return
|
||
|
|
|
||
|
|
# Parse period
|
||
|
|
days = 30
|
||
|
|
if period == "7d":
|
||
|
|
days = 7
|
||
|
|
elif period == "30d":
|
||
|
|
days = 30
|
||
|
|
elif period == "90d":
|
||
|
|
days = 90
|
||
|
|
elif period == "all":
|
||
|
|
days = None
|
||
|
|
|
||
|
|
# Calculate stats
|
||
|
|
stats = self._calculate_comprehensive_stats(interaction.guild_id)
|
||
|
|
|
||
|
|
# Filter by period if specified
|
||
|
|
if days:
|
||
|
|
cutoff_date = datetime.now() - timedelta(days=days)
|
||
|
|
tickets = self.storage.get_guild_tickets(interaction.guild_id)
|
||
|
|
filtered_tickets = [t for t in tickets if t.created_at >= cutoff_date]
|
||
|
|
|
||
|
|
# Recalculate stats for filtered period
|
||
|
|
stats = self._calculate_comprehensive_stats(interaction.guild_id)
|
||
|
|
# Filter the data
|
||
|
|
stats['tickets_by_day'] = {k: v for k, v in stats['tickets_by_day'].items()
|
||
|
|
if datetime.strptime(k, '%Y-%m-%d') >= cutoff_date}
|
||
|
|
stats['ratings'] = [r for r in stats['ratings'] if any(
|
||
|
|
t.created_at >= cutoff_date and t.rating == r for t in filtered_tickets)]
|
||
|
|
stats['response_times'] = [rt for rt in stats['response_times'] if any(
|
||
|
|
t.created_at >= cutoff_date for t in filtered_tickets if hasattr(t, 'response_time') and t.response_time == rt)]
|
||
|
|
|
||
|
|
# Create main embed
|
||
|
|
embed = discord.Embed(
|
||
|
|
title="📊 Analytics des Tickets",
|
||
|
|
description=f"Statistiques pour la période: **{period}**",
|
||
|
|
color=discord.Color.blue(),
|
||
|
|
timestamp=datetime.now()
|
||
|
|
)
|
||
|
|
|
||
|
|
# Basic stats
|
||
|
|
embed.add_field(
|
||
|
|
name="📈 Statistiques Générales",
|
||
|
|
value=f"**Total:** {stats['total_tickets']}\n"
|
||
|
|
f"**Ouverts:** {stats['open_tickets']}\n"
|
||
|
|
f"**Fermés:** {stats['closed_tickets']}\n"
|
||
|
|
f"**Réclamés:** {stats['claimed_tickets']}",
|
||
|
|
inline=True
|
||
|
|
)
|
||
|
|
|
||
|
|
# Performance metrics
|
||
|
|
avg_rating = f"{stats['average_rating']:.1f}⭐" if stats['ratings'] else "N/A"
|
||
|
|
avg_response = f"{stats['average_response_time']:.1f}min" if stats['response_times'] else "N/A"
|
||
|
|
|
||
|
|
embed.add_field(
|
||
|
|
name="⚡ Performance",
|
||
|
|
value=f"**Note moyenne:** {avg_rating}\n"
|
||
|
|
f"**Temps réponse moyen:** {avg_response}\n"
|
||
|
|
f"**Feedbacks:** {len(stats['feedbacks'])}",
|
||
|
|
inline=True
|
||
|
|
)
|
||
|
|
|
||
|
|
# Send main embed
|
||
|
|
await interaction.followup.send(embed=embed, ephemeral=True)
|
||
|
|
|
||
|
|
# Create and send charts
|
||
|
|
try:
|
||
|
|
# Rating chart
|
||
|
|
if stats['ratings']:
|
||
|
|
rating_chart = self._create_rating_chart(stats['ratings'])
|
||
|
|
await interaction.followup.send(
|
||
|
|
"📊 **Distribution des Notes Clients**",
|
||
|
|
file=discord.File(rating_chart, 'rating_distribution.png'),
|
||
|
|
ephemeral=True
|
||
|
|
)
|
||
|
|
|
||
|
|
# Response time chart
|
||
|
|
if stats['response_times']:
|
||
|
|
response_chart = self._create_response_time_chart(stats['response_times'])
|
||
|
|
await interaction.followup.send(
|
||
|
|
"⏱️ **Temps de Réponse**",
|
||
|
|
file=discord.File(response_chart, 'response_times.png'),
|
||
|
|
ephemeral=True
|
||
|
|
)
|
||
|
|
|
||
|
|
# Trend chart
|
||
|
|
if stats['tickets_by_day']:
|
||
|
|
trend_chart = self._create_trend_chart(stats['tickets_by_day'])
|
||
|
|
await interaction.followup.send(
|
||
|
|
"📈 **Évolution des Tickets**",
|
||
|
|
file=discord.File(trend_chart, 'ticket_trends.png'),
|
||
|
|
ephemeral=True
|
||
|
|
)
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
await interaction.followup.send(f"❌ Erreur lors de la génération des graphiques: {e}", ephemeral=True)
|
||
|
|
|
||
|
|
# Send feedback summary if available
|
||
|
|
if stats['feedbacks']:
|
||
|
|
feedback_embed = discord.Embed(
|
||
|
|
title="💬 Commentaires Clients",
|
||
|
|
description=f"Derniers commentaires ({min(5, len(stats['feedbacks']))} affichés)",
|
||
|
|
color=discord.Color.green()
|
||
|
|
)
|
||
|
|
|
||
|
|
# Sort by rating (lowest first to show areas for improvement)
|
||
|
|
sorted_feedbacks = sorted(stats['feedbacks'], key=lambda x: x['rating'] or 0)
|
||
|
|
|
||
|
|
for i, fb in enumerate(sorted_feedbacks[:5]):
|
||
|
|
rating_text = f"{fb['rating']}⭐" if fb['rating'] else "N/A"
|
||
|
|
feedback_text = fb['feedback'][:200] + "..." if len(fb['feedback']) > 200 else fb['feedback']
|
||
|
|
feedback_embed.add_field(
|
||
|
|
name=f"Ticket #{fb['ticket_id']} - {rating_text}",
|
||
|
|
value=feedback_text,
|
||
|
|
inline=False
|
||
|
|
)
|
||
|
|
|
||
|
|
await interaction.followup.send(embed=feedback_embed, ephemeral=True)
|
||
|
|
|
||
|
|
|
||
|
|
async def setup(bot):
|
||
|
|
"""Setup function for the analytics cog"""
|
||
|
|
await bot.add_cog(TicketAnalytics(bot))
|