""" 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 ): 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 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 } @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") ) @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 @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 } @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") ) @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