615 lines
22 KiB
Python
615 lines
22 KiB
Python
|
|
"""
|
||
|
|
Reminder API Endpoints for Sag Module
|
||
|
|
CRUD operations, user preferences, snooze/dismiss functionality
|
||
|
|
"""
|
||
|
|
|
||
|
|
import logging
|
||
|
|
from typing import List, Optional
|
||
|
|
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.services.reminder_notification_service import reminder_notification_service
|
||
|
|
|
||
|
|
logger = logging.getLogger(__name__)
|
||
|
|
|
||
|
|
router = APIRouter()
|
||
|
|
|
||
|
|
|
||
|
|
# ============================================================================
|
||
|
|
# 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 = False
|
||
|
|
notify_frontend: bool = True
|
||
|
|
email_override: 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)$")
|
||
|
|
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
|
||
|
|
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
|
||
|
|
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
|
||
|
|
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, 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', False),
|
||
|
|
notify_frontend=r.get('notify_frontend', True),
|
||
|
|
email_override=r.get('email_override'),
|
||
|
|
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,
|
||
|
|
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.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, quiet_hours_enabled, quiet_hours_start, quiet_hours_end
|
||
|
|
)
|
||
|
|
VALUES (%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.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])
|
||
|
|
async def list_sag_reminders(sag_id: int):
|
||
|
|
"""List all reminders for a case"""
|
||
|
|
|
||
|
|
query = """
|
||
|
|
SELECT id, sag_id, title, message, priority, 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'],
|
||
|
|
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.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'],
|
||
|
|
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)
|
||
|
|
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, 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,
|
||
|
|
true, %s
|
||
|
|
)
|
||
|
|
RETURNING id, sag_id, title, message, priority, 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.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, 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'],
|
||
|
|
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}")
|
||
|
|
async def update_sag_reminder(reminder_id: int, update: ReminderUpdate):
|
||
|
|
"""Update a reminder"""
|
||
|
|
|
||
|
|
# 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.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
|
||
|
|
RETURNING 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 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}")
|
||
|
|
async def delete_sag_reminder(reminder_id: int):
|
||
|
|
"""Soft-delete a reminder"""
|
||
|
|
|
||
|
|
try:
|
||
|
|
query = """
|
||
|
|
UPDATE sag_reminders
|
||
|
|
SET deleted_at = CURRENT_TIMESTAMP, is_active = false
|
||
|
|
WHERE id = %s
|
||
|
|
RETURNING id
|
||
|
|
"""
|
||
|
|
|
||
|
|
result = execute_insert(query, (reminder_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 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))
|