feat(ticket): update email integration to use new priority constants for ticket classification feat(procurement): add procurement overview page with dynamic data loading and display test(subscriptions): add tests for billing calendar to ensure correct invoice dates feat(reminder): implement automated task lists with user-defined rules for reminders feat(migrations): create tables for managing delefiber product prices and mobile recorder provisioning history test(mobile_recorder): add tests for provisioning mobile recorders to ensure correct asset creation and updates
740 lines
29 KiB
Python
740 lines
29 KiB
Python
"""
|
|
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))
|