"""Notifications plugin models - adapted to existing database schema.""" from datetime import datetime, timezone from shopdb.api import db class NotificationType(db.Model): """ Notification type classification. Matches existing notificationtypes table. """ __tablename__ = 'notificationtypes' notificationtypeid = db.Column(db.Integer, primary_key=True) typename = db.Column(db.String(50), nullable=False) typedescription = db.Column(db.Text) typecolor = db.Column(db.String(20), default='#17a2b8') isactive = db.Column(db.Boolean, default=True) # Auto-expiry rule: when a notification of this type has no explicit end time, # how long it stays up on the shopfloor board. # 'none' -> indefinite (never auto-expires) # 'duration' -> starttime + expirydays days # 'dailytime' -> next expiryhour:expiryminute Eastern (daily reset) expirymode = db.Column(db.String(20), default='none') expirydays = db.Column(db.Integer, nullable=True) expiryhour = db.Column(db.SmallInteger, nullable=True) expiryminute = db.Column(db.SmallInteger, nullable=True, default=0) # Shopfloor display behavior (data-driven; replaces hardcoded per-type logic). # splitperemployee -> one card per listed employee SSO # showemployeephoto -> resolve + show each employee's photo + name # displaystyle -> 'standard' (rows) | 'carousel' (rotating photo card) # | 'grid' (cycling row of tiles) | 'banner' splitperemployee = db.Column(db.Boolean, default=False) showemployeephoto = db.Column(db.Boolean, default=False) displaystyle = db.Column(db.String(20), default='standard') def __repr__(self): return f"" def to_dict(self): return { 'notificationtypeid': self.notificationtypeid, 'typename': self.typename, 'typedescription': self.typedescription, 'typecolor': self.typecolor, 'isactive': self.isactive, 'expirymode': self.expirymode or 'none', 'expirydays': self.expirydays, 'expiryhour': self.expiryhour, 'expiryminute': self.expiryminute if self.expiryminute is not None else 0, 'splitperemployee': bool(self.splitperemployee), 'showemployeephoto': bool(self.showemployeephoto), 'displaystyle': self.displaystyle or 'standard' } class Notification(db.Model): """ Notification/announcement model. Matches existing notifications table schema. """ __tablename__ = 'notifications' notificationid = db.Column(db.Integer, primary_key=True) notificationtypeid = db.Column( db.Integer, db.ForeignKey('notificationtypes.notificationtypeid'), nullable=True ) businessunitid = db.Column(db.Integer, nullable=True, index=True) appid = db.Column(db.Integer, nullable=True) notification = db.Column(db.Text, nullable=False, comment='The message content') starttime = db.Column(db.DateTime, nullable=True) endtime = db.Column(db.DateTime, nullable=True) ticketnumber = db.Column(db.String(50), nullable=True) link = db.Column(db.String(500), nullable=True) isactive = db.Column(db.Boolean, default=True) isshopfloor = db.Column(db.Boolean, default=False) # TEXT (not VARCHAR): recognition/recertification notifications comma-join # every employee's SSO/name into one field, which overflows 100 chars once # ~11 people are listed. employeesso = db.Column(db.Text, nullable=True) employeename = db.Column(db.Text, nullable=True) # Relationships notificationtype = db.relationship('NotificationType', backref='notifications') def __repr__(self): return f"" @property def is_current(self): """Check if notification is currently active based on dates.""" now = datetime.now(timezone.utc).replace(tzinfo=None) if not self.isactive: return False if self.starttime and now < self.starttime: return False if self.endtime and now > self.endtime: return False return True @property def title(self): """Get title - first line or first 100 chars of notification.""" if not self.notification: return '' lines = self.notification.split('\n') return lines[0][:100] if lines else self.notification[:100] def to_dict(self): """Convert to dictionary with related data.""" result = { 'notificationid': self.notificationid, 'notificationtypeid': self.notificationtypeid, 'businessunitid': self.businessunitid, 'appid': self.appid, 'notification': self.notification, 'title': self.title, 'message': self.notification, 'starttime': self.starttime.isoformat() if self.starttime else None, 'endtime': self.endtime.isoformat() if self.endtime else None, 'startdate': self.starttime.isoformat() if self.starttime else None, 'enddate': self.endtime.isoformat() if self.endtime else None, 'ticketnumber': self.ticketnumber, 'link': self.link, 'linkurl': self.link, 'isactive': bool(self.isactive) if self.isactive is not None else True, 'isshopfloor': bool(self.isshopfloor) if self.isshopfloor is not None else False, 'employeesso': self.employeesso, 'employeename': self.employeename, 'iscurrent': self.is_current } # Add type info if self.notificationtype: result['typename'] = self.notificationtype.typename result['typecolor'] = self.notificationtype.typecolor return result def to_calendar_event(self): """Convert to FullCalendar event format.""" # Color is data-driven: types store a hex typecolor. Only the legacy # Bootstrap color-name aliases still need translating; hex passes through. color_aliases = { 'success': '#04b962', 'warning': '#ff8800', 'danger': '#f5365c', 'info': '#14abef', 'primary': '#7934f3', 'secondary': '#94614f', } ntype = self.notificationtype raw_color = ntype.typecolor if ntype else '#14abef' color = color_aliases.get(raw_color, raw_color if raw_color.startswith('#') else '#14abef') show_photo = bool(ntype and getattr(ntype, 'showemployeephoto', False)) # Employee-photo types prefix the card with the person's name/SSO. A # multi-person recognition/training lists many people, so summarize as # "First Person +N" to keep the calendar title short (the detail popup # still shows the full employeename). title = self.title if show_photo: employee_display = self.employeename or self.employeesso if employee_display: people = [p.strip() for p in employee_display.split(',') if p.strip()] if len(people) > 1: employee_display = f"{people[0]} +{len(people) - 1}" title = f"{employee_display}: {title}" return { 'id': self.notificationid, 'title': title, 'start': self.starttime.isoformat() if self.starttime else None, 'end': self.endtime.isoformat() if self.endtime else None, 'allDay': True, 'backgroundColor': color, 'borderColor': color, 'extendedProps': { 'notificationid': self.notificationid, 'message': self.notification, 'typename': ntype.typename if ntype else None, 'typecolor': raw_color, 'showemployeephoto': show_photo, 'displaystyle': (ntype.displaystyle or 'standard') if ntype else 'standard', 'linkurl': self.link, 'ticketnumber': self.ticketnumber, 'employeename': self.employeename, 'employeesso': self.employeesso, } }