from app import db from app.utils.time_utils import now_eastern # ── Event type constants ─────────────────────────────────────────────────────── # These are the canonical keys used across the preference system. # Every call to notify() should pass one of these as event_type. EVENT_ISSUE_ASSIGNED = 'issue_assigned' EVENT_ISSUE_STATUS = 'issue_status' EVENT_ISSUE_COMMENT = 'issue_comment' EVENT_ISSUE_FOLLOW = 'issue_follow_update' EVENT_INSPECTION_DONE = 'inspection_completed' EVENT_SLA_ALERT = 'sla_alert' # Fired when an issue is flagged during an inspection (web or mobile). # Listed here so users can configure email preferences for this event. EVENT_ISSUE_FLAGGED = 'issue_flagged' # ── Customer portal events ───────────────────────────────────────────────── # Fired when an inspection completes or an issue is created/updated at a # facility the customer is assigned to. Separate constants allow customers # to manage these preferences independently from internal staff events. EVENT_CUSTOMER_INSPECTION_DONE = 'customer_inspection_completed' EVENT_CUSTOMER_ISSUE_UPDATED = 'customer_issue_updated' # Fired by the score-trend cron when a facility's rolling avg drops by # more than the configured threshold vs. the prior period. EVENT_SCORE_ALERT = 'score_alert' EVENT_ADMIN_BROADCAST = 'admin_broadcast' # bulk messages sent by admin to all apps # Fired by the scheduled-inspection reminder cron (advance/due to the inspector, # overdue to admin/director). Phase 36. EVENT_SCHEDULED_INSPECTION = 'scheduled_inspection' # Fired when someone asks for a follow-up re-inspection of a completed # inspection. Raised by admin/director from the inspection page and — since # phase46 — by CUSTOMERS for their own facilities. Phase 46. EVENT_FOLLOWUP_REQUESTED = 'followup_requested' ALL_EVENT_TYPES = { EVENT_ISSUE_ASSIGNED: 'Issue assigned to me', EVENT_ISSUE_STATUS: 'Issue status changed', EVENT_ISSUE_COMMENT: 'New comment on issue', EVENT_ISSUE_FOLLOW: 'Updates on followed issues', EVENT_ISSUE_FLAGGED: 'Issue flagged (from inspection)', EVENT_INSPECTION_DONE: 'Inspection completed', EVENT_SLA_ALERT: 'SLA at-risk / breached alerts', EVENT_ADMIN_BROADCAST: 'Admin broadcast (system announcements)', # Customer-facing — only relevant for customer role accounts EVENT_CUSTOMER_INSPECTION_DONE: 'Inspection completed at my facility (portal)', EVENT_CUSTOMER_ISSUE_UPDATED: 'Issue created or updated at my facility (portal)', # Score trend alert — admin/director management use EVENT_SCORE_ALERT: 'Facility score trend alert (significant drop detected)', # Scheduled inspection reminders (due/advance/overdue) EVENT_SCHEDULED_INSPECTION: 'Scheduled inspection reminders (due / overdue)', EVENT_FOLLOWUP_REQUESTED: 'Follow-up re-inspection requested', } class Notification(db.Model): """Stores in-app notifications for users. Each notification is tied to a single recipient and optionally linked to either an Issue or an Inspection so the UI can build a direct link. """ __tablename__ = 'notifications' id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False, index=True) title = db.Column(db.String(255), nullable=False) body = db.Column(db.Text, nullable=False) link = db.Column(db.String(512)) is_read = db.Column(db.Boolean, default=False, nullable=False) created_at = db.Column(db.DateTime, default=now_eastern, nullable=False) # Optional FK references — only one will be populated at a time issue_id = db.Column(db.Integer, db.ForeignKey('issues.id', ondelete='CASCADE'), nullable=True) inspection_id = db.Column(db.Integer, db.ForeignKey('inspections.id', ondelete='CASCADE'), nullable=True) # Event type — stored for mobile API polling so the iPad can categorise alerts. # Added phase17; NULL for notifications created before the migration. event_type = db.Column(db.String(50), nullable=True) # Digest tracking: set to True when created, cleared after digest email sent digest_pending = db.Column(db.Boolean, default=False, nullable=False, index=True) recipient = db.relationship('User', foreign_keys=[user_id], backref='notifications') def __repr__(self): return f'' class NotificationPreference(db.Model): """Per-user, per-event notification preferences. One row per (user_id, event_type) combination. If no row exists for a user+event, defaults apply (email on, no digest). """ __tablename__ = 'notification_preferences' id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), nullable=False, index=True) event_type = db.Column(db.String(50), nullable=False) email_enabled = db.Column(db.Boolean, default=True, nullable=False) digest_mode = db.Column(db.Boolean, default=False, nullable=False) # digest_frequency: 'hourly' or 'daily' — only relevant when digest_mode is True digest_frequency = db.Column(db.String(10), default='daily', nullable=False) __table_args__ = ( db.UniqueConstraint('user_id', 'event_type', name='uq_notif_pref_user_event'), ) user = db.relationship('User', foreign_keys=[user_id], backref=db.backref('notification_preferences', lazy='dynamic')) def __repr__(self): return (f'')