- Ajout catégorie recrutement avec questions personnalisées - Modal dynamique affichant les questions lors de la création - Envoi des réponses dans un salon staff configurable - Fermeture avec boutons Accepter/Refuser et envoi DM automatique au candidat - Mise à jour des modèles TicketCategory et TicketData Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
385 lines
15 KiB
Python
385 lines
15 KiB
Python
"""
|
|
Ticket Data Models
|
|
==================
|
|
Data models for the modular ticket system.
|
|
"""
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime
|
|
from typing import Optional, List, Dict, Any
|
|
from enum import Enum
|
|
|
|
|
|
class TicketStatus(Enum):
|
|
"""Ticket status enumeration"""
|
|
OPEN = "open"
|
|
CLAIMED = "claimed"
|
|
CLOSED = "closed"
|
|
ARCHIVED = "archived"
|
|
|
|
|
|
class Priority(Enum):
|
|
"""Ticket priority levels"""
|
|
LOW = "low"
|
|
NORMAL = "normal"
|
|
HIGH = "high"
|
|
URGENT = "urgent"
|
|
|
|
|
|
class TicketCategory:
|
|
"""
|
|
Represents a ticket category with customizable settings.
|
|
|
|
Attributes:
|
|
id: Unique identifier for the category
|
|
name: Display name
|
|
description: Description shown in panel
|
|
emoji: Emoji icon for the category
|
|
color: Embed color (hex)
|
|
staff_roles: List of role IDs who can access this category
|
|
welcome_message: Custom welcome message for this category
|
|
require_reason: Whether to require a reason when creating
|
|
max_tickets_per_user: Maximum tickets per user in this category
|
|
auto_close_days: Days before auto-closing inactive tickets
|
|
priority_enabled: Whether priority selection is enabled
|
|
allow_claims: Whether staff can claim tickets
|
|
survey_enabled: Whether satisfaction survey is enabled
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
id: str,
|
|
name: str,
|
|
description: str = "",
|
|
emoji: str = "🎫",
|
|
color: str = "#3498db",
|
|
staff_roles: List[str] = None,
|
|
welcome_message: str = None,
|
|
require_reason: bool = False,
|
|
max_tickets_per_user: int = 5,
|
|
auto_close_days: int = 7,
|
|
priority_enabled: bool = True,
|
|
allow_claims: bool = True,
|
|
survey_enabled: bool = True,
|
|
discord_category_id: Optional[int] = None,
|
|
is_recruitment: bool = False,
|
|
questions: List[str] = None,
|
|
responses_channel_id: Optional[int] = None
|
|
):
|
|
self.id = id
|
|
self.name = name
|
|
self.description = description
|
|
self.emoji = emoji
|
|
self.color = color
|
|
self.staff_roles = staff_roles or []
|
|
self.welcome_message = welcome_message or "Merci d'avoir créé un ticket. Notre équipe va vous répondre sous peu."
|
|
self.require_reason = require_reason
|
|
self.max_tickets_per_user = max_tickets_per_user
|
|
self.auto_close_days = auto_close_days
|
|
self.priority_enabled = priority_enabled
|
|
self.allow_claims = allow_claims
|
|
self.survey_enabled = survey_enabled
|
|
self.discord_category_id = discord_category_id
|
|
self.is_recruitment = is_recruitment
|
|
self.questions = questions or []
|
|
self.responses_channel_id = responses_channel_id
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
"""Convert category to dictionary for storage"""
|
|
return {
|
|
"id": self.id,
|
|
"name": self.name,
|
|
"description": self.description,
|
|
"emoji": self.emoji,
|
|
"color": self.color,
|
|
"staff_roles": self.staff_roles,
|
|
"welcome_message": self.welcome_message,
|
|
"require_reason": self.require_reason,
|
|
"max_tickets_per_user": self.max_tickets_per_user,
|
|
"auto_close_days": self.auto_close_days,
|
|
"priority_enabled": self.priority_enabled,
|
|
"allow_claims": self.allow_claims,
|
|
"survey_enabled": self.survey_enabled,
|
|
"discord_category_id": self.discord_category_id,
|
|
"is_recruitment": self.is_recruitment,
|
|
"questions": self.questions,
|
|
"responses_channel_id": self.responses_channel_id
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Dict[str, Any]) -> 'TicketCategory':
|
|
"""Create category from dictionary"""
|
|
return cls(
|
|
id=data.get("id", ""),
|
|
name=data.get("name", ""),
|
|
description=data.get("description", ""),
|
|
emoji=data.get("emoji", "🎫"),
|
|
color=data.get("color", "#3498db"),
|
|
staff_roles=data.get("staff_roles", []),
|
|
welcome_message=data.get("welcome_message"),
|
|
require_reason=data.get("require_reason", False),
|
|
max_tickets_per_user=data.get("max_tickets_per_user", 5),
|
|
auto_close_days=data.get("auto_close_days", 7),
|
|
priority_enabled=data.get("priority_enabled", True),
|
|
allow_claims=data.get("allow_claims", True),
|
|
survey_enabled=data.get("survey_enabled", True),
|
|
discord_category_id=data.get("discord_category_id"),
|
|
is_recruitment=data.get("is_recruitment", False),
|
|
questions=data.get("questions", []),
|
|
responses_channel_id=data.get("responses_channel_id")
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class TicketMessage:
|
|
"""Represents a message in a ticket"""
|
|
id: str
|
|
author_id: int
|
|
author_name: str
|
|
content: str
|
|
timestamp: datetime
|
|
attachments: List[str] = field(default_factory=list)
|
|
is_staff: bool = False
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
return {
|
|
"id": self.id,
|
|
"author_id": self.author_id,
|
|
"author_name": self.author_name,
|
|
"content": self.content,
|
|
"timestamp": self.timestamp.isoformat(),
|
|
"attachments": self.attachments,
|
|
"is_staff": self.is_staff
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Dict[str, Any]) -> 'TicketMessage':
|
|
return cls(
|
|
id=data.get("id", ""),
|
|
author_id=data.get("author_id", 0),
|
|
author_name=data.get("author_name", ""),
|
|
content=data.get("content", ""),
|
|
timestamp=datetime.fromisoformat(data.get("timestamp", datetime.now().isoformat())),
|
|
attachments=data.get("attachments", []),
|
|
is_staff=data.get("is_staff", False)
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class TicketData:
|
|
"""
|
|
Main ticket data structure.
|
|
|
|
Attributes:
|
|
channel_id: Discord channel ID
|
|
guild_id: Discord guild ID
|
|
user_id: User who created the ticket
|
|
category_id: Ticket category
|
|
status: Current ticket status
|
|
priority: Ticket priority level
|
|
created_at: Creation timestamp
|
|
closed_at: Closing timestamp
|
|
claimed_by: Staff member who claimed the ticket
|
|
messages: List of messages in the ticket
|
|
reason: Reason for creating the ticket
|
|
transcript_path: Path to generated transcript
|
|
rating: Satisfaction rating (1-5)
|
|
feedback: User feedback text
|
|
"""
|
|
|
|
channel_id: int
|
|
guild_id: int
|
|
user_id: int
|
|
category_id: str
|
|
status: TicketStatus = TicketStatus.OPEN
|
|
priority: Priority = Priority.NORMAL
|
|
created_at: datetime = field(default_factory=datetime.now)
|
|
closed_at: Optional[datetime] = None
|
|
claimed_by: Optional[int] = None
|
|
messages: List[TicketMessage] = field(default_factory=list)
|
|
reason: str = ""
|
|
transcript_path: Optional[str] = None
|
|
rating: Optional[int] = None
|
|
feedback: Optional[str] = None
|
|
recruitment_responses: Dict[str, str] = field(default_factory=dict)
|
|
|
|
@property
|
|
def is_open(self) -> bool:
|
|
return self.status in [TicketStatus.OPEN, TicketStatus.CLAIMED]
|
|
|
|
@property
|
|
def duration_minutes(self) -> Optional[float]:
|
|
"""Get ticket duration in minutes"""
|
|
end = self.closed_at or datetime.now()
|
|
return (end - self.created_at).total_seconds() / 60
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
return {
|
|
"channel_id": self.channel_id,
|
|
"guild_id": self.guild_id,
|
|
"user_id": self.user_id,
|
|
"category_id": self.category_id,
|
|
"status": self.status.value,
|
|
"priority": self.priority.value,
|
|
"created_at": self.created_at.isoformat(),
|
|
"closed_at": self.closed_at.isoformat() if self.closed_at else None,
|
|
"claimed_by": self.claimed_by,
|
|
"messages": [m.to_dict() for m in self.messages],
|
|
"reason": self.reason,
|
|
"transcript_path": self.transcript_path,
|
|
"rating": self.rating,
|
|
"feedback": self.feedback,
|
|
"recruitment_responses": self.recruitment_responses
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Dict[str, Any]) -> 'TicketData':
|
|
messages = [TicketMessage.from_dict(m) for m in data.get("messages", [])]
|
|
return cls(
|
|
channel_id=data.get("channel_id", 0),
|
|
guild_id=data.get("guild_id", 0),
|
|
user_id=data.get("user_id", 0),
|
|
category_id=data.get("category_id", ""),
|
|
status=TicketStatus(data.get("status", "open")),
|
|
priority=Priority(data.get("priority", "normal")),
|
|
created_at=datetime.fromisoformat(data.get("created_at", datetime.now().isoformat())),
|
|
closed_at=datetime.fromisoformat(data["closed_at"]) if data.get("closed_at") else None,
|
|
claimed_by=data.get("claimed_by"),
|
|
messages=messages,
|
|
reason=data.get("reason", ""),
|
|
transcript_path=data.get("transcript_path"),
|
|
rating=data.get("rating"),
|
|
feedback=data.get("feedback"),
|
|
recruitment_responses=data.get("recruitment_responses", {})
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class TicketStats:
|
|
"""Statistics for a ticket system"""
|
|
total_tickets: int = 0
|
|
open_tickets: int = 0
|
|
closed_tickets: int = 0
|
|
claimed_tickets: int = 0
|
|
average_duration_minutes: float = 0
|
|
average_rating: float = 0
|
|
tickets_by_category: Dict[str, int] = field(default_factory=dict)
|
|
tickets_by_priority: Dict[str, int] = field(default_factory=dict)
|
|
tickets_by_day: Dict[str, int] = field(default_factory=dict)
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
return {
|
|
"total_tickets": self.total_tickets,
|
|
"open_tickets": self.open_tickets,
|
|
"closed_tickets": self.closed_tickets,
|
|
"claimed_tickets": self.claimed_tickets,
|
|
"average_duration_minutes": self.average_duration_minutes,
|
|
"average_rating": self.average_rating,
|
|
"tickets_by_category": self.tickets_by_category,
|
|
"tickets_by_priority": self.tickets_by_priority,
|
|
"tickets_by_day": self.tickets_by_day
|
|
}
|
|
|
|
|
|
@dataclass
|
|
class GuildTicketConfig:
|
|
"""
|
|
Guild-wide ticket configuration.
|
|
|
|
Attributes:
|
|
guild_id: Discord guild ID
|
|
enabled: Whether the ticket system is enabled
|
|
categories: List of ticket categories
|
|
default_category: Default category ID
|
|
log_channel_id: Channel for ticket logs
|
|
archive_channel_id: Channel for archived transcripts
|
|
staff_roles: Global staff roles (can manage tickets, access analytics, etc.)
|
|
config_roles: Roles that can access the configuration panel (/ticketconfig)
|
|
max_tickets_per_user: Global max tickets per user
|
|
auto_close_enabled: Whether auto-close is enabled
|
|
auto_close_days: Days before auto-close
|
|
transcript_enabled: Whether transcripts are generated
|
|
claim_enabled: Whether claiming is enabled by default
|
|
survey_enabled: Whether surveys are enabled by default
|
|
panel_channel_id: Channel for the ticket panel
|
|
welcome_enabled: Whether welcome messages are shown
|
|
priority_enabled: Whether priority selection is enabled
|
|
"""
|
|
|
|
guild_id: int
|
|
enabled: bool = True
|
|
categories: List[TicketCategory] = field(default_factory=list)
|
|
default_category: Optional[str] = None
|
|
log_channel_id: Optional[int] = None
|
|
archive_channel_id: Optional[int] = None
|
|
staff_roles: List[str] = field(default_factory=list)
|
|
staff_users: List[int] = field(default_factory=list)
|
|
config_roles: List[str] = field(default_factory=list)
|
|
config_users: List[int] = field(default_factory=list)
|
|
max_tickets_per_user: int = 5
|
|
auto_close_enabled: bool = False
|
|
auto_close_days: int = 7
|
|
transcript_enabled: bool = True
|
|
claim_enabled: bool = True
|
|
survey_enabled: bool = True
|
|
panel_channel_id: Optional[int] = None
|
|
welcome_enabled: bool = True
|
|
priority_enabled: bool = True
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
return {
|
|
"guild_id": self.guild_id,
|
|
"enabled": self.enabled,
|
|
"categories": [c.to_dict() for c in self.categories],
|
|
"default_category": self.default_category,
|
|
"log_channel_id": self.log_channel_id,
|
|
"archive_channel_id": self.archive_channel_id,
|
|
"log_channel_id": self.log_channel_id,
|
|
"archive_channel_id": self.archive_channel_id,
|
|
"staff_roles": self.staff_roles,
|
|
"staff_users": self.staff_users,
|
|
"config_roles": self.config_roles,
|
|
"config_users": self.config_users,
|
|
"max_tickets_per_user": self.max_tickets_per_user,
|
|
"auto_close_enabled": self.auto_close_enabled,
|
|
"auto_close_days": self.auto_close_days,
|
|
"transcript_enabled": self.transcript_enabled,
|
|
"claim_enabled": self.claim_enabled,
|
|
"survey_enabled": self.survey_enabled,
|
|
"panel_channel_id": self.panel_channel_id,
|
|
"welcome_enabled": self.welcome_enabled,
|
|
"priority_enabled": self.priority_enabled
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Dict[str, Any]) -> 'GuildTicketConfig':
|
|
categories = [TicketCategory.from_dict(c) for c in data.get("categories", [])]
|
|
return cls(
|
|
guild_id=data.get("guild_id", 0),
|
|
enabled=data.get("enabled", True),
|
|
categories=categories,
|
|
default_category=data.get("default_category"),
|
|
log_channel_id=data.get("log_channel_id"),
|
|
archive_channel_id=data.get("archive_channel_id"),
|
|
|
|
staff_roles=data.get("staff_roles", []),
|
|
staff_users=data.get("staff_users", []),
|
|
config_roles=data.get("config_roles", []),
|
|
config_users=data.get("config_users", []),
|
|
max_tickets_per_user=data.get("max_tickets_per_user", 5),
|
|
auto_close_enabled=data.get("auto_close_enabled", False),
|
|
auto_close_days=data.get("auto_close_days", 7),
|
|
transcript_enabled=data.get("transcript_enabled", True),
|
|
claim_enabled=data.get("claim_enabled", True),
|
|
survey_enabled=data.get("survey_enabled", True),
|
|
panel_channel_id=data.get("panel_channel_id"),
|
|
welcome_enabled=data.get("welcome_enabled", True),
|
|
priority_enabled=data.get("priority_enabled", True)
|
|
)
|
|
|
|
def get_category(self, category_id: str) -> Optional[TicketCategory]:
|
|
"""Get a category by ID"""
|
|
for cat in self.categories:
|
|
if cat.id == category_id:
|
|
return cat
|
|
return None
|
|
|