from datetime import datetime from flask_sqlalchemy import SQLAlchemy from sqlalchemy.dialects.mysql import CHAR, ENUM, JSON, BIGINT from sqlalchemy import text import uuid db = SQLAlchemy() def generate_uuid(): return str(uuid.uuid4()) class TodoItem(db.Model): __tablename__ = 'todo_item' id = db.Column(CHAR(36), primary_key=True, default=generate_uuid) title = db.Column(db.String(200), nullable=False) description = db.Column(db.Text) status = db.Column(ENUM('NEW', 'DOING', 'BLOCKED', 'DONE'), default='NEW') priority = db.Column(ENUM('LOW', 'MEDIUM', 'HIGH', 'URGENT'), default='MEDIUM') due_date = db.Column(db.Date) created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) completed_at = db.Column(db.DateTime) creator_ad = db.Column(db.String(128), nullable=False) creator_display_name = db.Column(db.String(128)) creator_email = db.Column(db.String(256)) starred = db.Column(db.Boolean, default=False) # Relationships responsible_users = db.relationship('TodoItemResponsible', back_populates='todo', cascade='all, delete-orphan') followers = db.relationship('TodoItemFollower', back_populates='todo', cascade='all, delete-orphan') mail_logs = db.relationship('TodoMailLog', back_populates='todo', cascade='all, delete-orphan') audit_logs = db.relationship('TodoAuditLog', back_populates='todo') fire_email_logs = db.relationship('TodoFireEmailLog', back_populates='todo', cascade='all, delete-orphan') def to_dict(self, include_user_details=True): result = { 'id': self.id, 'title': self.title, 'description': self.description, 'status': self.status, 'priority': self.priority, 'due_date': self.due_date.isoformat() if self.due_date else None, 'created_at': self.created_at.isoformat(), 'completed_at': self.completed_at.isoformat() if self.completed_at else None, 'creator_ad': self.creator_ad, 'creator_display_name': self.creator_display_name, 'creator_email': self.creator_email, 'starred': self.starred, 'responsible_users': [r.ad_account for r in self.responsible_users], 'followers': [f.ad_account for f in self.followers] } # 如果需要包含用戶詳細信息,則添加 display names if include_user_details: from utils.ldap_utils import validate_ad_accounts # 獲取所有相關用戶的 display names all_users = set([self.creator_ad] + [r.ad_account for r in self.responsible_users] + [f.ad_account for f in self.followers]) user_details = validate_ad_accounts(list(all_users)) # 添加用戶詳細信息 result['responsible_users_details'] = [] for r in self.responsible_users: user_info = user_details.get(r.ad_account, {}) result['responsible_users_details'].append({ 'ad_account': r.ad_account, 'display_name': user_info.get('display_name', r.ad_account), 'email': user_info.get('email', '') }) result['followers_details'] = [] for f in self.followers: user_info = user_details.get(f.ad_account, {}) result['followers_details'].append({ 'ad_account': f.ad_account, 'display_name': user_info.get('display_name', f.ad_account), 'email': user_info.get('email', '') }) return result def can_edit(self, user_ad): """Check if user can edit this todo""" if self.creator_ad == user_ad: return True return any(r.ad_account == user_ad for r in self.responsible_users) def can_view(self, user_ad): """Check if user can view this todo""" if self.can_edit(user_ad): return True return any(f.ad_account == user_ad for f in self.followers) class TodoItemResponsible(db.Model): __tablename__ = 'todo_item_responsible' todo_id = db.Column(CHAR(36), db.ForeignKey('todo_item.id', ondelete='CASCADE'), primary_key=True) ad_account = db.Column(db.String(128), primary_key=True) added_by = db.Column(db.String(128)) added_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) # Relationships todo = db.relationship('TodoItem', back_populates='responsible_users') class TodoItemFollower(db.Model): __tablename__ = 'todo_item_follower' todo_id = db.Column(CHAR(36), db.ForeignKey('todo_item.id', ondelete='CASCADE'), primary_key=True) ad_account = db.Column(db.String(128), primary_key=True) added_by = db.Column(db.String(128)) added_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) # Relationships todo = db.relationship('TodoItem', back_populates='followers') class TodoMailLog(db.Model): __tablename__ = 'todo_mail_log' id = db.Column(BIGINT, primary_key=True, autoincrement=True) todo_id = db.Column(CHAR(36), db.ForeignKey('todo_item.id', ondelete='CASCADE')) type = db.Column(ENUM('SCHEDULED', 'FIRE'), nullable=False) triggered_by_ad = db.Column(db.String(128)) recipients = db.Column(db.Text) subject = db.Column(db.String(255)) status = db.Column(ENUM('QUEUED', 'SENT', 'FAILED'), default='QUEUED') provider_msg_id = db.Column(db.String(128)) error_text = db.Column(db.Text) created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) sent_at = db.Column(db.DateTime) # Relationships todo = db.relationship('TodoItem', back_populates='mail_logs') class TodoAuditLog(db.Model): __tablename__ = 'todo_audit_log' id = db.Column(BIGINT, primary_key=True, autoincrement=True) actor_ad = db.Column(db.String(128), nullable=False) todo_id = db.Column(CHAR(36), db.ForeignKey('todo_item.id', ondelete='SET NULL')) action = db.Column(ENUM('CREATE', 'UPDATE', 'DELETE', 'COMPLETE', 'IMPORT', 'MAIL_SENT', 'MAIL_FAIL', 'FIRE_EMAIL', 'DIGEST_EMAIL', 'BULK_REMINDER'), nullable=False) detail = db.Column(JSON) created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) # Relationships todo = db.relationship('TodoItem', back_populates='audit_logs') class TodoUserPref(db.Model): __tablename__ = 'todo_user_pref' ad_account = db.Column(db.String(128), primary_key=True) email = db.Column(db.String(256)) display_name = db.Column(db.String(128)) theme = db.Column(ENUM('light', 'dark', 'auto'), default='auto') language = db.Column(db.String(10), default='zh-TW') timezone = db.Column(db.String(50), default='Asia/Taipei') notification_enabled = db.Column(db.Boolean, default=True) email_reminder_enabled = db.Column(db.Boolean, default=True) weekly_summary_enabled = db.Column(db.Boolean, default=True) monthly_summary_enabled = db.Column(db.Boolean, default=False) # 彈性的到期提醒天數設定 (JSON陣列,如 [1, 3, 5] 表示前1天、前3天、前5天提醒) reminder_days_before = db.Column(JSON, default=lambda: [1, 3]) # 摘要郵件時間設定 (時:分格式,如 "09:00") daily_summary_time = db.Column(db.String(5), default='09:00') weekly_summary_time = db.Column(db.String(5), default='09:00') monthly_summary_time = db.Column(db.String(5), default='09:00') # 摘要郵件週幾發送 (0=週日, 1=週一, ..., 6=週六) weekly_summary_day = db.Column(db.Integer, default=1) # 預設週一 monthly_summary_day = db.Column(db.Integer, default=1) # 預設每月1日 # Fire email 配額控制 fire_email_today_count = db.Column(db.Integer, default=0) fire_email_last_reset = db.Column(db.Date) created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) updated_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow) def to_dict(self): return { 'ad_account': self.ad_account, 'email': self.email, 'display_name': self.display_name, 'theme': self.theme, 'language': self.language, 'timezone': self.timezone, 'notification_enabled': self.notification_enabled, 'email_reminder_enabled': self.email_reminder_enabled, 'weekly_summary_enabled': self.weekly_summary_enabled, 'monthly_summary_enabled': self.monthly_summary_enabled, 'reminder_days_before': self.reminder_days_before or [1, 3], 'daily_summary_time': self.daily_summary_time, 'weekly_summary_time': self.weekly_summary_time, 'monthly_summary_time': self.monthly_summary_time, 'weekly_summary_day': self.weekly_summary_day, 'monthly_summary_day': self.monthly_summary_day, } class TodoImportJob(db.Model): __tablename__ = 'todo_import_job' id = db.Column(CHAR(36), primary_key=True, default=generate_uuid) actor_ad = db.Column(db.String(128), nullable=False) filename = db.Column(db.String(255)) total_rows = db.Column(db.Integer, default=0) success_rows = db.Column(db.Integer, default=0) failed_rows = db.Column(db.Integer, default=0) status = db.Column(ENUM('PENDING', 'PROCESSING', 'COMPLETED', 'FAILED'), default='PENDING') error_file_path = db.Column(db.String(500)) error_details = db.Column(JSON) created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) completed_at = db.Column(db.DateTime) def to_dict(self): return { 'id': self.id, 'actor_ad': self.actor_ad, 'filename': self.filename, 'total_rows': self.total_rows, 'success_rows': self.success_rows, 'failed_rows': self.failed_rows, 'status': self.status, 'error_file_path': self.error_file_path, 'error_details': self.error_details, 'created_at': self.created_at.isoformat(), 'completed_at': self.completed_at.isoformat() if self.completed_at else None } class TodoFireEmailLog(db.Model): __tablename__ = 'todo_fire_email_log' id = db.Column(BIGINT, primary_key=True, autoincrement=True) todo_id = db.Column(CHAR(36), db.ForeignKey('todo_item.id', ondelete='CASCADE'), nullable=False) sender_ad = db.Column(db.String(128), nullable=False) sent_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) # Relationships todo = db.relationship('TodoItem', back_populates='fire_email_logs')