Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 112 additions & 6 deletions backend/app/api/routes/reminders.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""Reminder management routes."""

import uuid
from datetime import datetime, timezone
from datetime import datetime, timedelta, timezone
from typing import Any

from fastapi import APIRouter, HTTPException
Expand All @@ -15,6 +14,8 @@
ReminderPublic,
RemindersPublic,
ReminderUpdate,
ReminderWithContactPublic,
RemindersWithContactPublic,
)

router = APIRouter(prefix="/reminders", tags=["reminders"])
Expand Down Expand Up @@ -59,6 +60,98 @@ def list_reminders(
)


@router.get("/due", response_model=RemindersWithContactPublic)
def list_due_reminders(
session: SessionDep,
current_user: CurrentUser,
skip: int = 0,
limit: int = 100,
) -> Any:
"""List reminders due now or overdue for the current user.

Filters for reminders where:
- remind_at <= now (due or overdue)
- snoozed_until is NULL or snoozed_until <= now (not snoozed)
- is_active is True
- owned by current user or tied to visible contacts

Also joins with Contact to include contact_name.
"""
now = datetime.now(timezone.utc)

statement = (
select(Reminder, Contact.first_name, Contact.last_name)
.outerjoin(Contact, Reminder.contact_id == Contact.id)
.where(
Reminder.remind_at <= now,
or_(
Reminder.snoozed_until.is_(None),
Reminder.snoozed_until <= now,
),
Reminder.is_active == True,
or_(
Reminder.owner_id == current_user.id,
Reminder.contact_id.in_(visible_contact_ids(current_user)),
),
)
)

count_statement = select(func.count()).select_from(
select(Reminder.id)
.outerjoin(Contact, Reminder.contact_id == Contact.id)
.where(
Reminder.remind_at <= now,
or_(
Reminder.snoozed_until.is_(None),
Reminder.snoozed_until <= now,
),
Reminder.is_active == True,
or_(
Reminder.owner_id == current_user.id,
Reminder.contact_id.in_(visible_contact_ids(current_user)),
),
)
.subquery()
)
count = session.exec(count_statement).one()

statement = statement.order_by(Reminder.remind_at.asc()).offset(skip).limit(limit)
results = session.exec(statement).all()

reminders_with_contact = []
for row in results:
reminder = row[0]
first_name = row[1]
last_name = row[2]
reminder_data = ReminderWithContactPublic.model_validate(reminder)
if first_name:
reminder_data.contact_name = f"{first_name} {last_name or ''}".strip()
reminders_with_contact.append(reminder_data)

return RemindersWithContactPublic(
data=reminders_with_contact,
count=count,
)


@router.post("/{reminder_id}/dismiss")
def dismiss_reminder(
session: SessionDep,
current_user: CurrentUser,
reminder_id: uuid.UUID,
) -> Any:
"""Dismiss a reminder by setting snoozed_until to now (soft-clear from badge)."""
reminder = session.get(Reminder, reminder_id)
if reminder is None or not _reminder_accessible(current_user, reminder, session):
raise HTTPException(status_code=404, detail="Reminder not found")

reminder.snoozed_until = datetime.now(timezone.utc)
session.add(reminder)
session.commit()
session.refresh(reminder)
return ReminderPublic.model_validate(reminder)


@router.post("/", response_model=ReminderPublic)
def create_reminder_route(
*,
Expand Down Expand Up @@ -104,16 +197,29 @@ def snooze_reminder(
session: SessionDep,
current_user: CurrentUser,
reminder_id: uuid.UUID,
minutes: int = 30,
minutes: int | None = None,
snooze_until: datetime | None = None,
) -> Any:
"""Snooze a reminder."""
"""Snooze a reminder.

Provide either:
- minutes: snooze for this many minutes from now
- snooze_until: snooze until this specific datetime
- neither: defaults to 1 hour
"""
reminder = session.get(Reminder, reminder_id)
if reminder is None or not _reminder_accessible(current_user, reminder, session):
raise HTTPException(status_code=404, detail="Reminder not found")

from datetime import timedelta
now = datetime.now(timezone.utc)
if snooze_until is not None:
reminder.snoozed_until = snooze_until
elif minutes is not None:
reminder.snoozed_until = now + timedelta(minutes=minutes)
else:
# Default: snooze for 1 hour
reminder.snoozed_until = now + timedelta(hours=1)

reminder.snoozed_until = datetime.now(timezone.utc) + timedelta(minutes=minutes)
session.add(reminder)
session.commit()
session.refresh(reminder)
Expand Down
10 changes: 10 additions & 0 deletions backend/app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1252,8 +1252,18 @@ class ReminderPublic(ReminderBase):
created_at: datetime


class ReminderWithContactPublic(ReminderPublic):
"""Reminder with optional contact name for list views."""
contact_name: str | None = None


class RemindersPublic(SQLModel):
data: list[ReminderPublic]


class RemindersWithContactPublic(SQLModel):
"""Response wrapper for reminders that include contact name."""
data: list[ReminderWithContactPublic]
count: int


Expand Down
21 changes: 21 additions & 0 deletions front
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Types for reminders with contact information
// These extend the auto-generated types with contact_name field

export interface ReminderWithContactPublic {
title: string;
description: string | null;
remind_at: string;
frequency: string;
is_active: boolean;
id: string;
contact_id: string | null;
last_sent_at: string | null;
snoozed_until: string | null;
created_at: string;
contact_name: string | null;
}

export interface RemindersWithContactPublic {
data: ReminderWithContactPublic[];
count: number;
}
72 changes: 72 additions & 0 deletions frontend/src/client/sdk.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2036,8 +2036,80 @@ export class RemindersService {
422: 'Validation Error'
}
});
},

/**
* List Due Reminders
* List reminders due now or overdue for the current user.
* @param data The data for the request.
* @param data.skip
* @param data.limit
* @returns RemindersPublic Successful Response
* @throws ApiError
*/
public static listDueReminders(data: RemindersListDueRemindersData = {}): CancelablePromise<RemindersListDueRemindersResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/api/v1/reminders/due',
query: {
skip: data.skip,
limit: data.limit,
},
errors: {
422: 'Validation Error'
}
});
}

/**
* Dismiss Reminder
* Dismiss a reminder by setting snoozed_until to now (soft-clear from badge).
* @param data The data for the request.
* @param data.reminderId
* @returns ReminderPublic Successful Response
* @throws ApiError
*/
public static dismissReminder(data: RemindersDismissReminderData): CancelablePromise<RemindersDismissReminderResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/api/v1/reminders/{reminder_id}/dismiss',
path: {
reminder_id: data.reminderId
},
errors: {
422: 'Validation Error'
}
});
}

/**
* Snooze Reminder
* Snooze a reminder.
* @param data The data for the request.
* @param data.reminderId
* @param data.minutes
* @param data.snoozeUntil
* @returns ReminderPublic Successful Response
* @throws ApiError
*/
public static snoozeReminder(data: RemindersSnoozeReminderData): CancelablePromise<RemindersSnoozeReminderResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/api/v1/reminders/{reminder_id}/snooze',
path: {
reminder_id: data.reminderId
},
query: {
minutes: data.minutes,
snooze_until: data.snoozeUntil,
},
errors: {
422: 'Validation Error'
}
});
}
}
}

export class TagsService {
/**
Expand Down
25 changes: 25 additions & 0 deletions frontend/src/client/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2042,11 +2042,36 @@ export type RemindersDeleteReminderData = {

export type RemindersDeleteReminderResponse = (unknown);

export type RemindersListDueRemindersData = {
skip?: number;
limit?: number;
};


export type RemindersListDueRemindersResponse = (RemindersPublic);


export type RemindersDismissReminderData = {
reminderId: string;
};


export type RemindersDismissReminderResponse = (ReminderPublic);


export type RemindersSnoozeReminderData = {
minutes?: number;
reminderId: string;
};

export type RemindersSnoozeReminderData = {
minutes?: number;
snoozeUntil?: string;
reminderId: string;
};

export type RemindersSnoozeReminderResponse = (ReminderPublic);

export type RemindersSnoozeReminderResponse = (unknown);

export type TagsListTagsData = {
Expand Down
1 change: 1 addition & 0 deletions openapi.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
service "backend" is not running
Loading