bmc_hub/app/modules/sag/backend/reminders.py

740 lines
29 KiB
Python
Raw Normal View History

"""
Reminder API Endpoints for Sag Module
CRUD operations, user preferences, snooze/dismiss functionality
"""
import logging
from typing import List, Optional
import json
from datetime import datetime, timedelta
from fastapi import APIRouter, HTTPException, status, Depends, Request
from pydantic import BaseModel, Field
from app.core.database import execute_query, execute_insert
from app.core.config import settings
from app.core.auth_dependencies import require_any_permission
from app.services.reminder_notification_service import reminder_notification_service
logger = logging.getLogger(__name__)
router = APIRouter()
case_read_access = require_any_permission("cases.view", "tickets.view")
case_edit_access = require_any_permission("cases.edit", "tickets.edit")
def _ensure_task_list_rules():
execute_query("""CREATE TABLE IF NOT EXISTS reminder_task_list_rules (
id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL, title TEXT NOT NULL,
times_json JSONB NOT NULL DEFAULT '[]'::jsonb, include_groups BOOLEAN NOT NULL DEFAULT true,
notify_mattermost BOOLEAN NOT NULL DEFAULT true, is_active BOOLEAN NOT NULL DEFAULT true, last_sent_at TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP)""", fetch=False)
execute_query("ALTER TABLE reminder_task_list_rules ADD COLUMN IF NOT EXISTS last_sent_at TIMESTAMP", fetch=False)
def _task_list(user_id: int, include_groups: bool):
group_filter = "OR s.assigned_group_id IN (SELECT group_id FROM user_groups WHERE user_id=%s)" if include_groups else ""
params = (user_id, user_id) if include_groups else (user_id,)
return execute_query(f"""SELECT s.id, s.titel, s.status, s.priority, s.deadline, c.name AS customer_name
FROM sag_sager s LEFT JOIN customers c ON c.id=s.customer_id
WHERE s.deleted_at IS NULL AND LOWER(COALESCE(s.status,'')) NOT IN ('lukket','løst','closed','resolved','udsat','deferred')
AND (s.ansvarlig_bruger_id=%s {group_filter})
ORDER BY s.deadline NULLS LAST, s.updated_at DESC LIMIT 50""", params) or []
async def _send_task_list(rule, user_id: int):
tasks = _task_list(user_id, bool(rule.get('include_groups')))
if not tasks: return {'sent': False, 'message': 'Ingen åbne sager at sende'}
base_url = str(settings.HUB_BASE_URL or "https://hub.bmcnetworks.dk").rstrip('/')
def cell(value, fallback=''):
value = str(value or fallback).replace('|', '\\|').replace('\n', ' ')
return value[:120]
lines = [
'| Sag | Kunde | Status | Prioritet | Deadline |',
'| :-- | :-- | :-- | :-- | :-- |',
]
for row in tasks:
title = cell(row.get('titel'), 'Uden titel')
link = f"[#{row['id']} · {title}]({base_url}/sag/{row['id']}/v3)"
deadline = row.get('deadline')
if hasattr(deadline, 'strftime'):
deadline = deadline.strftime('%d.%m.%Y')
lines.append(
f"| {link} | {cell(row.get('customer_name'))} | {cell(row.get('status'))} "
f"| {cell(row.get('priority'))} | {cell(deadline)} |"
)
result = await reminder_notification_service.send_reminder(
reminder_id=0, sag_id=int(tasks[0]['id']), case_title='Opgaveliste', customer_name=None,
reminder_title=rule['title'], reminder_message='\n'.join(lines), recipient_user_ids=[user_id],
recipient_emails=[], priority='normal', notify_mattermost=bool(rule.get('notify_mattermost')),
notify_email=False, notify_frontend=True, override_user_preferences=True)
return {'sent': bool(result.get('success')), 'count': len(tasks), 'result': result}
async def process_task_list_rules():
_ensure_task_list_rules()
now = datetime.now()
window_start = now - timedelta(minutes=5)
for rule in execute_query("SELECT * FROM reminder_task_list_rules WHERE is_active=true") or []:
times = rule.get('times_json') or []
if isinstance(times, str): times = json.loads(times)
for scheduled_time in times:
try:
hour, minute = (int(value) for value in scheduled_time.split(':', 1))
scheduled_at = now.replace(hour=hour, minute=minute, second=0, microsecond=0)
except (AttributeError, TypeError, ValueError):
logger.warning("Ignoring invalid task-list schedule %r for rule %s", scheduled_time, rule['id'])
continue
last_sent = rule.get('last_sent_at')
if window_start < scheduled_at <= now and (not last_sent or last_sent < scheduled_at):
await _send_task_list(rule, int(rule['user_id']))
execute_query('UPDATE reminder_task_list_rules SET last_sent_at=CURRENT_TIMESTAMP,updated_at=CURRENT_TIMESTAMP WHERE id=%s',(rule['id'],),fetch=False)
break
@router.get('/api/v1/reminder-task-rules')
async def list_task_rules(request: Request):
_ensure_task_list_rules(); user_id=_get_user_id_from_request(request)
return execute_query('SELECT * FROM reminder_task_list_rules WHERE user_id=%s ORDER BY id DESC',(user_id,)) or []
@router.post('/api/v1/reminder-task-rules')
async def save_task_rule(request: Request, data: dict):
_ensure_task_list_rules(); user_id=_get_user_id_from_request(request)
title=str(data.get('title') or 'Min opgaveliste').strip(); times=[t for t in (data.get('times') or []) if isinstance(t,str)]
if not times: raise HTTPException(status_code=400, detail='Vælg mindst ét tidspunkt')
rows=execute_query("""INSERT INTO reminder_task_list_rules(user_id,title,times_json,include_groups,notify_mattermost)
VALUES(%s,%s,%s::jsonb,%s,%s) RETURNING *""",(user_id,title,json.dumps(times),bool(data.get('include_groups',True)),bool(data.get('notify_mattermost',True))))
return rows[0]
@router.post('/api/v1/reminder-task-rules/{rule_id}/send-now')
async def send_task_rule_now(rule_id:int, request:Request):
_ensure_task_list_rules(); user_id=_get_user_id_from_request(request)
rule=execute_query("SELECT * FROM reminder_task_list_rules WHERE id=%s AND user_id=%s",(rule_id,user_id))
if not rule: raise HTTPException(status_code=404, detail='Reglen findes ikke')
return await _send_task_list(rule[0],user_id)
# ============================================================================
# Helper Functions
# ============================================================================
def _get_user_id_from_request(request: Request) -> int:
"""Extract user_id from request query params or raise 401"""
user_id = getattr(request.state, 'user_id', None)
if user_id is not None:
try:
return int(user_id)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid user_id format")
user_id = request.query_params.get('user_id')
if user_id:
try:
return int(user_id)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid user_id format")
raise HTTPException(status_code=401, detail="User not authenticated - provide user_id query parameter")
# ============================================================================
# Pydantic Schemas
# ============================================================================
class UserNotificationPreferences(BaseModel):
"""User notification preferences"""
notify_mattermost: bool = True
notify_email: bool = True
notify_frontend: bool = True
email_override: Optional[str] = None
mattermost_username: Optional[str] = None
quiet_hours_enabled: bool = False
quiet_hours_start: Optional[str] = None # HH:MM format
quiet_hours_end: Optional[str] = None # HH:MM format
class ReminderCreate(BaseModel):
"""Create reminder request"""
title: str = Field(..., min_length=3, max_length=255)
message: Optional[str] = None
priority: str = Field(default="normal", pattern="^(low|normal|high|urgent)$")
event_type: str = Field(default="reminder", pattern="^(reminder|meeting|technician_visit|obs|deadline)$")
trigger_type: str = Field(pattern="^(status_change|deadline_approaching|time_based)$")
trigger_config: dict # JSON config for trigger
recipient_user_ids: List[int] = []
recipient_emails: List[str] = []
notify_mattermost: Optional[bool] = None
notify_email: Optional[bool] = None
notify_frontend: Optional[bool] = None
override_user_preferences: bool = False
recurrence_type: str = Field(default="once", pattern="^(once|daily|weekly|monthly)$")
recurrence_day_of_week: Optional[int] = None # 0-6 for weekly
recurrence_day_of_month: Optional[int] = None # 1-31 for monthly
scheduled_at: Optional[datetime] = None
class ReminderUpdate(BaseModel):
"""Update reminder request"""
title: Optional[str] = None
message: Optional[str] = None
priority: Optional[str] = None
event_type: Optional[str] = Field(default=None, pattern="^(reminder|meeting|technician_visit|obs|deadline)$")
notify_mattermost: Optional[bool] = None
notify_email: Optional[bool] = None
notify_frontend: Optional[bool] = None
override_user_preferences: Optional[bool] = None
is_active: Optional[bool] = None
class ReminderResponse(BaseModel):
"""Reminder response"""
id: int
sag_id: int
title: str
message: Optional[str]
priority: str
event_type: Optional[str]
trigger_type: str
recurrence_type: str
is_active: bool
next_check_at: Optional[datetime]
last_sent_at: Optional[datetime]
created_at: datetime
class ReminderProfileResponse(BaseModel):
"""Reminder response for profile list"""
id: int
sag_id: int
title: str
message: Optional[str]
priority: str
event_type: Optional[str]
trigger_type: str
recurrence_type: str
is_active: bool
next_check_at: Optional[datetime]
last_sent_at: Optional[datetime]
created_at: datetime
case_title: Optional[str]
customer_name: Optional[str]
class ReminderLogResponse(BaseModel):
"""Reminder execution log"""
id: int
reminder_id: Optional[int]
sag_id: int
status: str
triggered_at: datetime
channels_used: List[str]
class SnoozeRequest(BaseModel):
"""Snooze reminder request"""
duration_minutes: int = Field(..., ge=15, le=1440) # 15 min to 24 hours
class DismissRequest(BaseModel):
"""Dismiss reminder request"""
reason: Optional[str] = None
# ============================================================================
# User Preferences Endpoints
# ============================================================================
@router.get("/api/v1/users/me/notification-preferences", response_model=UserNotificationPreferences)
async def get_user_notification_preferences(request: Request):
"""Get current user's notification preferences"""
user_id = _get_user_id_from_request(request)
query = """
SELECT
notify_mattermost, notify_email, notify_frontend,
email_override, mattermost_username,
quiet_hours_enabled, quiet_hours_start, quiet_hours_end
FROM user_notification_preferences
WHERE user_id = %s
"""
result = execute_query(query, (user_id,))
if result:
r = result[0]
return UserNotificationPreferences(
notify_mattermost=r.get('notify_mattermost', True),
notify_email=r.get('notify_email', True),
notify_frontend=r.get('notify_frontend', True),
email_override=r.get('email_override'),
mattermost_username=r.get('mattermost_username'),
quiet_hours_enabled=r.get('quiet_hours_enabled', False),
quiet_hours_start=r.get('quiet_hours_start'),
quiet_hours_end=r.get('quiet_hours_end')
)
# Return defaults
return UserNotificationPreferences()
@router.patch("/api/v1/users/me/notification-preferences")
async def update_user_notification_preferences(
request: Request,
preferences: UserNotificationPreferences
):
"""Update user notification preferences"""
user_id = _get_user_id_from_request(request)
# Check if preferences exist
check_query = "SELECT id FROM user_notification_preferences WHERE user_id = %s"
exists = execute_query(check_query, (user_id,))
try:
if exists:
# Update existing
query = """
UPDATE user_notification_preferences
SET notify_mattermost = %s,
notify_email = %s,
notify_frontend = %s,
email_override = %s,
mattermost_username = %s,
quiet_hours_enabled = %s,
quiet_hours_start = %s,
quiet_hours_end = %s,
updated_at = CURRENT_TIMESTAMP
WHERE user_id = %s
RETURNING id
"""
execute_insert(query, (
preferences.notify_mattermost,
preferences.notify_email,
preferences.notify_frontend,
preferences.email_override,
preferences.mattermost_username,
preferences.quiet_hours_enabled,
preferences.quiet_hours_start,
preferences.quiet_hours_end,
user_id
))
else:
# Create new
query = """
INSERT INTO user_notification_preferences (
user_id, notify_mattermost, notify_email, notify_frontend,
email_override, mattermost_username,
quiet_hours_enabled, quiet_hours_start, quiet_hours_end
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
RETURNING id
"""
execute_insert(query, (
user_id,
preferences.notify_mattermost,
preferences.notify_email,
preferences.notify_frontend,
preferences.email_override,
preferences.mattermost_username,
preferences.quiet_hours_enabled,
preferences.quiet_hours_start,
preferences.quiet_hours_end
))
logger.info(f"✅ Updated notification preferences for user {user_id}")
return {"success": True, "message": "Preferences updated"}
except Exception as e:
logger.error(f"❌ Error updating preferences: {e}")
raise HTTPException(status_code=500, detail=str(e))
# ============================================================================
# Reminder CRUD Endpoints
# ============================================================================
@router.get(
"/api/v1/sag/{sag_id}/reminders",
response_model=List[ReminderResponse],
dependencies=[Depends(case_read_access)],
)
async def list_sag_reminders(sag_id: int):
"""List all reminders for a case"""
query = """
SELECT id, sag_id, title, message, priority, event_type, trigger_type,
recurrence_type, is_active, next_check_at, last_sent_at, created_at
FROM sag_reminders
WHERE sag_id = %s AND deleted_at IS NULL
ORDER BY created_at DESC
"""
results = execute_query(query, (sag_id,))
return [
ReminderResponse(
id=r['id'],
sag_id=r['sag_id'],
title=r['title'],
message=r['message'],
priority=r['priority'],
event_type=r.get('event_type'),
trigger_type=r['trigger_type'],
recurrence_type=r['recurrence_type'],
is_active=r['is_active'],
next_check_at=r['next_check_at'],
last_sent_at=r['last_sent_at'],
created_at=r['created_at']
)
for r in results
]
@router.get("/api/v1/reminders/my", response_model=List[ReminderProfileResponse])
async def list_my_reminders(request: Request):
"""List reminders for the authenticated user"""
user_id = _get_user_id_from_request(request)
query = """
SELECT r.id, r.sag_id, r.title, r.message, r.priority, r.event_type, r.trigger_type,
r.recurrence_type, r.is_active, r.next_check_at, r.last_sent_at, r.created_at,
s.titel as case_title, c.name as customer_name
FROM sag_reminders r
LEFT JOIN sag_sager s ON s.id = r.sag_id
LEFT JOIN customers c ON c.id = s.customer_id
WHERE r.deleted_at IS NULL
AND %s = ANY(r.recipient_user_ids)
ORDER BY r.created_at DESC
"""
results = execute_query(query, (user_id,))
return [
ReminderProfileResponse(
id=r['id'],
sag_id=r['sag_id'],
title=r['title'],
message=r['message'],
priority=r['priority'],
event_type=r.get('event_type'),
trigger_type=r['trigger_type'],
recurrence_type=r['recurrence_type'],
is_active=r['is_active'],
next_check_at=r['next_check_at'],
last_sent_at=r['last_sent_at'],
created_at=r['created_at'],
case_title=r.get('case_title'),
customer_name=r.get('customer_name')
)
for r in results
]
@router.post(
"/api/v1/sag/{sag_id}/reminders",
response_model=ReminderResponse,
dependencies=[Depends(case_edit_access)],
)
async def create_sag_reminder(sag_id: int, request: Request, reminder: ReminderCreate):
"""Create a new reminder for a case"""
user_id = _get_user_id_from_request(request)
# Verify case exists
case_query = "SELECT id FROM sag_sager WHERE id = %s"
case = execute_query(case_query, (sag_id,))
if not case:
raise HTTPException(status_code=404, detail=f"Case #{sag_id} not found")
# Calculate next_check_at based on trigger type
next_check_at = None
if reminder.trigger_type == 'time_based' and reminder.scheduled_at:
next_check_at = reminder.scheduled_at
elif reminder.trigger_type == 'deadline_approaching':
next_check_at = datetime.now() + timedelta(days=1) # Check daily
try:
import json
query = """
INSERT INTO sag_reminders (
sag_id, title, message, priority, event_type, trigger_type, trigger_config,
recipient_user_ids, recipient_emails,
notify_mattermost, notify_email, notify_frontend,
override_user_preferences,
recurrence_type, recurrence_day_of_week, recurrence_day_of_month,
scheduled_at, next_check_at,
is_active, created_by_user_id
)
VALUES (
%s, %s, %s, %s, %s, %s, %s,
%s, %s,
%s, %s, %s,
%s,
%s, %s, %s,
%s, %s,
true, %s
)
RETURNING id, sag_id, title, message, priority, event_type, trigger_type,
recurrence_type, is_active, next_check_at, last_sent_at, created_at
"""
result = execute_insert(query, (
sag_id, reminder.title, reminder.message, reminder.priority, reminder.event_type,
reminder.trigger_type, json.dumps(reminder.trigger_config),
reminder.recipient_user_ids, reminder.recipient_emails,
reminder.notify_mattermost, reminder.notify_email, reminder.notify_frontend,
reminder.override_user_preferences,
reminder.recurrence_type, reminder.recurrence_day_of_week, reminder.recurrence_day_of_month,
reminder.scheduled_at, next_check_at,
user_id
))
if not result:
raise HTTPException(status_code=500, detail="Failed to create reminder")
raw_row = result[0] if isinstance(result, list) else result
if isinstance(raw_row, dict):
r = raw_row
else:
reminder_id = int(raw_row)
fetch_query = """
SELECT id, sag_id, title, message, priority, event_type, trigger_type,
recurrence_type, is_active, next_check_at, last_sent_at, created_at
FROM sag_reminders
WHERE id = %s
"""
fetched = execute_query(fetch_query, (reminder_id,))
if not fetched:
raise HTTPException(status_code=500, detail="Failed to fetch reminder after creation")
r = fetched[0]
logger.info(f"✅ Reminder created for case #{sag_id} by user {user_id}")
return ReminderResponse(
id=r['id'],
sag_id=r['sag_id'],
title=r['title'],
message=r['message'],
priority=r['priority'],
event_type=r.get('event_type'),
trigger_type=r['trigger_type'],
recurrence_type=r['recurrence_type'],
is_active=r['is_active'],
next_check_at=r['next_check_at'],
last_sent_at=r['last_sent_at'],
created_at=r['created_at']
)
except Exception as e:
logger.error(f"❌ Error creating reminder: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.patch("/api/v1/sag/reminders/{reminder_id}", dependencies=[Depends(case_edit_access)])
async def update_sag_reminder(reminder_id: int, update: ReminderUpdate, request: Request):
"""Update a reminder"""
user_id = _get_user_id_from_request(request)
# Build update query dynamically
updates = []
params = []
if update.title is not None:
updates.append("title = %s")
params.append(update.title)
if update.message is not None:
updates.append("message = %s")
params.append(update.message)
if update.priority is not None:
updates.append("priority = %s")
params.append(update.priority)
if update.event_type is not None:
updates.append("event_type = %s")
params.append(update.event_type)
if update.notify_mattermost is not None:
updates.append("notify_mattermost = %s")
params.append(update.notify_mattermost)
if update.notify_email is not None:
updates.append("notify_email = %s")
params.append(update.notify_email)
if update.notify_frontend is not None:
updates.append("notify_frontend = %s")
params.append(update.notify_frontend)
if update.override_user_preferences is not None:
updates.append("override_user_preferences = %s")
params.append(update.override_user_preferences)
if update.is_active is not None:
updates.append("is_active = %s")
params.append(update.is_active)
if not updates:
raise HTTPException(status_code=400, detail="No fields to update")
updates.append("updated_at = CURRENT_TIMESTAMP")
params.append(reminder_id)
try:
query = f"""
UPDATE sag_reminders
SET {', '.join(updates)}
WHERE id = %s AND created_by_user_id = %s AND deleted_at IS NULL
RETURNING id
"""
params.append(user_id)
result = execute_insert(query, tuple(params))
if not result:
raise HTTPException(status_code=404, detail="Reminder not found")
logger.info(f"✅ Reminder {reminder_id} updated")
return {"success": True, "message": "Reminder updated"}
except HTTPException:
raise
except Exception as e:
logger.error(f"❌ Error updating reminder: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/api/v1/sag/reminders/{reminder_id}", dependencies=[Depends(case_edit_access)])
async def delete_sag_reminder(reminder_id: int, request: Request):
"""Soft-delete a reminder"""
user_id = _get_user_id_from_request(request)
try:
query = """
UPDATE sag_reminders
SET deleted_at = CURRENT_TIMESTAMP, is_active = false
WHERE id = %s AND created_by_user_id = %s AND deleted_at IS NULL
RETURNING id
"""
result = execute_insert(query, (reminder_id, user_id))
if not result:
raise HTTPException(status_code=404, detail="Reminder not found")
logger.info(f"✅ Reminder {reminder_id} deleted")
return {"success": True, "message": "Reminder deleted"}
except HTTPException:
raise
except Exception as e:
logger.error(f"❌ Error deleting reminder: {e}")
raise HTTPException(status_code=500, detail=str(e))
# ============================================================================
# Reminder Interaction Endpoints
# ============================================================================
@router.post("/api/v1/sag/reminders/{reminder_id}/snooze")
async def snooze_reminder(reminder_id: int, request: Request, snooze_request: SnoozeRequest):
"""Snooze a reminder for specified minutes"""
user_id = _get_user_id_from_request(request)
snooze_until = datetime.now() + timedelta(minutes=snooze_request.duration_minutes)
try:
query = """
INSERT INTO sag_reminder_logs (
reminder_id, sag_id, user_id, status, snoozed_until, snoozed_by_user_id, triggered_at
)
SELECT id, sag_id, %s, 'snoozed', %s, %s, CURRENT_TIMESTAMP
FROM sag_reminders
WHERE id = %s
RETURNING id
"""
result = execute_insert(query, (user_id, snooze_until, user_id, reminder_id))
if not result:
raise HTTPException(status_code=404, detail="Reminder not found")
logger.info(f"✅ Reminder {reminder_id} snoozed until {snooze_until}")
return {
"success": True,
"message": f"Reminder snoozed for {snooze_request.duration_minutes} minutes",
"snoozed_until": snooze_until
}
except Exception as e:
logger.error(f"❌ Error snoozing reminder: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/api/v1/sag/reminders/{reminder_id}/dismiss")
async def dismiss_reminder(reminder_id: int, request: Request, dismiss_request: DismissRequest):
"""Dismiss a reminder"""
user_id = _get_user_id_from_request(request)
try:
query = """
INSERT INTO sag_reminder_logs (
reminder_id, sag_id, user_id, status, dismissed_at, dismissed_by_user_id, triggered_at
)
SELECT id, sag_id, %s, 'dismissed', CURRENT_TIMESTAMP, %s, CURRENT_TIMESTAMP
FROM sag_reminders
WHERE id = %s
RETURNING id
"""
result = execute_insert(query, (user_id, user_id, reminder_id))
if not result:
raise HTTPException(status_code=404, detail="Reminder not found")
logger.info(f"✅ Reminder {reminder_id} dismissed by user {user_id}")
return {"success": True, "message": "Reminder dismissed"}
except Exception as e:
logger.error(f"❌ Error dismissing reminder: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/api/v1/reminders/pending/me")
async def get_pending_reminders(request: Request):
"""Get pending reminders for current user (for frontend polling)"""
user_id = _get_user_id_from_request(request)
query = """
SELECT
r.id, r.sag_id, r.title, r.message, r.priority,
s.titel as case_title, c.name as customer_name,
l.id as log_id, l.snoozed_until, l.status as log_status
FROM sag_reminders r
JOIN sag_sager s ON r.sag_id = s.id
JOIN customers c ON s.customer_id = c.id
LEFT JOIN LATERAL (
SELECT id, snoozed_until, status, triggered_at
FROM sag_reminder_logs
WHERE reminder_id = r.id AND user_id = %s
ORDER BY triggered_at DESC
LIMIT 1
) l ON true
WHERE r.is_active = true
AND r.deleted_at IS NULL
AND r.next_check_at <= CURRENT_TIMESTAMP
AND %s = ANY(r.recipient_user_ids)
AND (l.snoozed_until IS NULL OR l.snoozed_until < CURRENT_TIMESTAMP)
AND (l.status IS NULL OR l.status != 'dismissed')
ORDER BY r.priority DESC, r.next_check_at ASC
LIMIT 5
"""
try:
results = execute_query(query, (user_id, user_id))
return [{
'id': r['id'],
'sag_id': r['sag_id'],
'title': r['title'],
'message': r['message'],
'priority': r['priority'],
'case_title': r['case_title'],
'customer_name': r['customer_name']
} for r in results]
except Exception as e:
logger.error(f"❌ Error fetching pending reminders: {e}")
raise HTTPException(status_code=500, detail=str(e))