from datetime import datetime from flask_login import UserMixin from werkzeug.security import generate_password_hash, check_password_hash from app import db # ─── Enumerations ──────────────────────────────────────────────────────────── class UserRole: EMPLOYEE = 'employee' IT_STAFF = 'it_staff' ADMIN = 'admin' class TicketStatus: OPEN = 'open' IN_PROGRESS = 'in_progress' PENDING = 'pending' RESOLVED = 'resolved' CLOSED = 'closed' class TicketPriority: LOW = 'low' MEDIUM = 'medium' HIGH = 'high' CRITICAL = 'critical' class TicketCategory: HARDWARE = 'hardware' SOFTWARE = 'software' NETWORK = 'network' ACCESS = 'access' EMAIL = 'email' PRINTER = 'printer' PHONE = 'phone' SECURITY = 'security' OTHER = 'other' class NotificationType: TICKET_CREATED = 'ticket_created' TICKET_UPDATED = 'ticket_updated' TICKET_ASSIGNED = 'ticket_assigned' COMMENT_ADDED = 'comment_added' STATUS_CHANGED = 'status_changed' TICKET_RESOLVED = 'ticket_resolved' TICKET_CLOSED = 'ticket_closed' MENTION = 'mention' # ─── Models ────────────────────────────────────────────────────────────────── class User(UserMixin, db.Model): __tablename__ = 'users' id = db.Column(db.Integer, primary_key=True) email = db.Column(db.String(120), unique=True, nullable=False, index=True) username = db.Column(db.String(64), unique=True, nullable=False, index=True) full_name = db.Column(db.String(128), nullable=False) password_hash= db.Column(db.String(256), nullable=False) role = db.Column(db.String(20), default=UserRole.EMPLOYEE, nullable=False) department = db.Column(db.String(100)) phone = db.Column(db.String(20)) avatar_url = db.Column(db.String(256)) is_active = db.Column(db.Boolean, default=True, nullable=False) email_notif = db.Column(db.Boolean, default=True, nullable=False) web_notif = db.Column(db.Boolean, default=True, nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow) last_login = db.Column(db.DateTime) # Relationships created_tickets = db.relationship('Ticket', foreign_keys='Ticket.created_by_id', backref='creator', lazy='dynamic') assigned_tickets = db.relationship('Ticket', foreign_keys='Ticket.assigned_to_id', backref='assignee', lazy='dynamic') comments = db.relationship('Comment', backref='author', lazy='dynamic') notifications = db.relationship('Notification', backref='recipient', lazy='dynamic') activity_logs = db.relationship('ActivityLog', backref='user', lazy='dynamic') def set_password(self, password): self.password_hash = generate_password_hash(password) def check_password(self, password): return check_password_hash(self.password_hash, password) @property def is_it_staff(self): return self.role in (UserRole.IT_STAFF, UserRole.ADMIN) @property def is_admin(self): return self.role == UserRole.ADMIN def __repr__(self): return f'' class Ticket(db.Model): __tablename__ = 'tickets' id = db.Column(db.Integer, primary_key=True) ticket_number = db.Column(db.String(20), unique=True, nullable=False, index=True) title = db.Column(db.String(200), nullable=False) description = db.Column(db.Text, nullable=False) category = db.Column(db.String(50), default=TicketCategory.OTHER, nullable=False) priority = db.Column(db.String(20), default=TicketPriority.MEDIUM, nullable=False) status = db.Column(db.String(20), default=TicketStatus.OPEN, nullable=False, index=True) location = db.Column(db.String(100)) asset_tag = db.Column(db.String(50)) created_by_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) assigned_to_id = db.Column(db.Integer, db.ForeignKey('users.id')) created_at = db.Column(db.DateTime, default=datetime.utcnow, index=True) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) resolved_at = db.Column(db.DateTime) closed_at = db.Column(db.DateTime) due_date = db.Column(db.DateTime) ai_generated = db.Column(db.Boolean, default=False) internal_notes = db.Column(db.Text) resolution_notes = db.Column(db.Text) # Relationships comments = db.relationship('Comment', backref='ticket', lazy='dynamic', cascade='all, delete-orphan') attachments = db.relationship('Attachment', backref='ticket', lazy='dynamic', cascade='all, delete-orphan') notifications= db.relationship('Notification', backref='ticket', lazy='dynamic', cascade='all, delete-orphan') history = db.relationship('TicketHistory', backref='ticket', lazy='dynamic', cascade='all, delete-orphan') def generate_ticket_number(self): """Generate unique ticket number like TKT-20240101-0001""" date_str = datetime.utcnow().strftime('%Y%m%d') last = Ticket.query.filter( Ticket.ticket_number.like(f'TKT-{date_str}-%') ).order_by(Ticket.id.desc()).first() if last: seq = int(last.ticket_number.split('-')[-1]) + 1 else: seq = 1 return f'TKT-{date_str}-{seq:04d}' @property def priority_color(self): return { TicketPriority.LOW: 'success', TicketPriority.MEDIUM: 'warning', TicketPriority.HIGH: 'danger', TicketPriority.CRITICAL: 'dark', }.get(self.priority, 'secondary') @property def status_color(self): return { TicketStatus.OPEN: 'primary', TicketStatus.IN_PROGRESS: 'info', TicketStatus.PENDING: 'warning', TicketStatus.RESOLVED: 'success', TicketStatus.CLOSED: 'secondary', }.get(self.status, 'secondary') def __repr__(self): return f'' class Comment(db.Model): __tablename__ = 'comments' id = db.Column(db.Integer, primary_key=True) ticket_id = db.Column(db.Integer, db.ForeignKey('tickets.id'), nullable=False) author_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) body = db.Column(db.Text, nullable=False) is_internal = db.Column(db.Boolean, default=False) # IT-only internal note created_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) attachments = db.relationship('Attachment', backref='comment', lazy='dynamic') def __repr__(self): return f'' class Attachment(db.Model): __tablename__ = 'attachments' id = db.Column(db.Integer, primary_key=True) ticket_id = db.Column(db.Integer, db.ForeignKey('tickets.id')) comment_id = db.Column(db.Integer, db.ForeignKey('comments.id')) filename = db.Column(db.String(256), nullable=False) stored_name = db.Column(db.String(256), nullable=False) file_size = db.Column(db.Integer) mime_type = db.Column(db.String(100)) uploaded_by = db.Column(db.Integer, db.ForeignKey('users.id')) uploaded_at = db.Column(db.DateTime, default=datetime.utcnow) uploader = db.relationship('User', foreign_keys=[uploaded_by]) def __repr__(self): return f'' class Notification(db.Model): __tablename__ = 'notifications' id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) ticket_id = db.Column(db.Integer, db.ForeignKey('tickets.id')) type = db.Column(db.String(50), nullable=False) title = db.Column(db.String(200), nullable=False) message = db.Column(db.Text) is_read = db.Column(db.Boolean, default=False, index=True) created_at = db.Column(db.DateTime, default=datetime.utcnow, index=True) link = db.Column(db.String(256)) def __repr__(self): return f'' class TicketHistory(db.Model): __tablename__ = 'ticket_history' id = db.Column(db.Integer, primary_key=True) ticket_id = db.Column(db.Integer, db.ForeignKey('tickets.id'), nullable=False) changed_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) field_name = db.Column(db.String(50), nullable=False) old_value = db.Column(db.String(200)) new_value = db.Column(db.String(200)) changed_at = db.Column(db.DateTime, default=datetime.utcnow) changer = db.relationship('User', foreign_keys=[changed_by]) def __repr__(self): return f'' class ActivityLog(db.Model): __tablename__ = 'activity_logs' id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('users.id')) action = db.Column(db.String(100), nullable=False) entity_type = db.Column(db.String(50)) entity_id = db.Column(db.Integer) details = db.Column(db.Text) ip_address = db.Column(db.String(45)) created_at = db.Column(db.DateTime, default=datetime.utcnow, index=True) def __repr__(self): return f'' class KnowledgeBase(db.Model): __tablename__ = 'knowledge_base' id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(200), nullable=False) body = db.Column(db.Text, nullable=False) category = db.Column(db.String(50)) tags = db.Column(db.String(256)) author_id = db.Column(db.Integer, db.ForeignKey('users.id')) is_published= db.Column(db.Boolean, default=True) view_count = db.Column(db.Integer, default=0) created_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) author = db.relationship('User', foreign_keys=[author_id]) def __repr__(self): return f''