from .extensions import db from datetime import datetime, date, timezone class Project(db.Model): __tablename__ = 'projects' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(200), nullable=False) color = db.Column(db.String(7), nullable=False, default='#C9A84C') description = db.Column(db.Text) drive_url = db.Column(db.String(500)) archived_at = db.Column(db.DateTime, nullable=True) created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc)) deliverables = db.relationship( 'Deliverable', backref='project', cascade='all, delete-orphan', lazy=True, ) def to_dict(self, include_deliverables=True): data = { 'id': self.id, 'name': self.name, 'color': self.color, 'description': self.description, 'drive_url': self.drive_url, 'archived_at': self.archived_at.isoformat() if self.archived_at else None, 'created_at': self.created_at.isoformat() if self.created_at else None, } if include_deliverables: data['deliverables'] = [ d.to_dict() for d in sorted(self.deliverables, key=lambda x: x.due_date) ] return data class Deliverable(db.Model): __tablename__ = 'deliverables' id = db.Column(db.Integer, primary_key=True) project_id = db.Column( db.Integer, db.ForeignKey('projects.id', ondelete='CASCADE'), nullable=False, ) title = db.Column(db.String(300), nullable=False) due_date = db.Column(db.Date, nullable=False) status = db.Column(db.String(20), nullable=False, default='upcoming') created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc)) def effective_status(self): """ Returns 'overdue' if the due date has passed and the deliverable has not been marked completed. Completed deliverables are never auto-downgraded. """ if self.status != 'completed' and self.due_date < date.today(): return 'overdue' return self.status def to_dict(self): return { 'id': self.id, 'project_id': self.project_id, 'title': self.title, 'due_date': self.due_date.isoformat() if self.due_date else None, 'status': self.effective_status(), 'created_at': self.created_at.isoformat() if self.created_at else None, }