From ff96cf1d4ee760355278dd268b7ce8959834a4bd Mon Sep 17 00:00:00 2001 From: Will Pike <6687499+pike00@users.noreply.github.com> Date: Sun, 3 May 2026 04:29:05 -0500 Subject: [PATCH 1/2] WIP: dirac autorun for reminders-bell-badge (error) --- backend/app/api/routes/reminders.py | 84 +- frontend/src/client/sdk.gen.ts | 72 + frontend/src/client/types.gen.ts | 25 + openapi.json | 1 + types_backup.ts | 1961 +++++++++++++++++++++++++++ 5 files changed, 2137 insertions(+), 6 deletions(-) create mode 100644 openapi.json create mode 100644 types_backup.ts diff --git a/backend/app/api/routes/reminders.py b/backend/app/api/routes/reminders.py index dcb43ebe..bca4703e 100644 --- a/backend/app/api/routes/reminders.py +++ b/backend/app/api/routes/reminders.py @@ -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 @@ -59,6 +58,66 @@ def list_reminders( ) +@router.get("/due", response_model=RemindersPublic) +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 + """ + now = datetime.now(timezone.utc) + + statement = select(Reminder).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(statement.subquery()) + count = session.exec(count_statement).one() + + statement = statement.order_by(Reminder.remind_at.asc()).offset(skip).limit(limit) + reminders = session.exec(statement).all() + + return RemindersPublic( + data=[ReminderPublic.model_validate(r) for r in reminders], + 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( *, @@ -104,16 +163,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) diff --git a/frontend/src/client/sdk.gen.ts b/frontend/src/client/sdk.gen.ts index 77d74440..ed012673 100644 --- a/frontend/src/client/sdk.gen.ts +++ b/frontend/src/client/sdk.gen.ts @@ -1817,8 +1817,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 { + 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 { + 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 { + 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 { /** diff --git a/frontend/src/client/types.gen.ts b/frontend/src/client/types.gen.ts index 1c57c675..cfdd43a8 100644 --- a/frontend/src/client/types.gen.ts +++ b/frontend/src/client/types.gen.ts @@ -1789,11 +1789,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 = { diff --git a/openapi.json b/openapi.json new file mode 100644 index 00000000..d9e57abf --- /dev/null +++ b/openapi.json @@ -0,0 +1 @@ +service "backend" is not running diff --git a/types_backup.ts b/types_backup.ts new file mode 100644 index 00000000..cfdd43a8 --- /dev/null +++ b/types_backup.ts @@ -0,0 +1,1961 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type _MentionSourceContact = { + id: string; + first_name: string; + last_name?: (string | null); + avatar_url?: (string | null); +}; + +export type _ShareIn = { + tag_id: string; + grantee_id: string; +}; + +export type ActivityLogPublic = { + id: string; + owner_id: string; + actor_id: (string | null); + entity_type: string; + entity_id: string; + action: string; + changes_json: ({ + [key: string]: unknown; +} | null); + occurred_at: string; +}; + +export type ActivityLogsPublic = { + data: Array; + count: number; +}; + +export type AddressCreate = { + /** + * Label like "home", "work", "other". + */ + label?: string; + /** + * Street line 1. + */ + street?: (string | null); + /** + * Apartment, suite, floor, etc. + */ + extended?: (string | null); + /** + * City. + */ + city?: (string | null); + /** + * State, province, or region. + */ + region?: (string | null); + /** + * ZIP or postal code. + */ + postal_code?: (string | null); + /** + * Country. + */ + country?: (string | null); + /** + * Geocoded latitude; used for map visualization. + */ + latitude?: (number | null); + /** + * Geocoded longitude; used for map visualization. + */ + longitude?: (number | null); + contact_id: string; +}; + +export type AddressPublic = { + /** + * Label like "home", "work", "other". + */ + label?: string; + /** + * Street line 1. + */ + street?: (string | null); + /** + * Apartment, suite, floor, etc. + */ + extended?: (string | null); + /** + * City. + */ + city?: (string | null); + /** + * State, province, or region. + */ + region?: (string | null); + /** + * ZIP or postal code. + */ + postal_code?: (string | null); + /** + * Country. + */ + country?: (string | null); + /** + * Geocoded latitude; used for map visualization. + */ + latitude?: (number | null); + /** + * Geocoded longitude; used for map visualization. + */ + longitude?: (number | null); + id: string; + contact_id: string; +}; + +export type AddressUpdate = { + label?: (string | null); + street?: (string | null); + extended?: (string | null); + city?: (string | null); + region?: (string | null); + postal_code?: (string | null); + country?: (string | null); + latitude?: (number | null); + longitude?: (number | null); +}; + +export type Body_import_export_import_vcard = { + file: (Blob | File); +}; + +export type Body_login_login_access_token = { + grant_type?: (string | null); + username: string; + password: string; + scope?: string; + client_id?: (string | null); + client_secret?: (string | null); +}; + +export type CalendarEntry = { + contact_id: string; + name: string; + type: string; + age: (number | null); +}; + +export type CalendarMonthResponse = { + month: string; + days: { + [key: string]: Array; + }; +}; + +export type ContactCreate = { + /** + * Given name; required. + */ + first_name: string; + /** + * Family name. + */ + last_name?: (string | null); + /** + * Middle name or initial. + */ + middle_name?: (string | null); + /** + * Honorific like Dr., Mr., Ms. + */ + prefix?: (string | null); + /** + * Suffix like Jr., PhD. + */ + suffix?: (string | null); + /** + * Preferred or informal name. + */ + nickname?: (string | null); + /** + * Organization name. + */ + company?: (string | null); + /** + * Department within the company. + */ + department?: (string | null); + /** + * Job title. + */ + title?: (string | null); + /** + * Date of birth; used for milestone and birthday reminders. + */ + birthday?: (string | null); + /** + * Short story of how the introduction happened. + */ + how_we_met?: (string | null); + /** + * Pinned to the top of contact lists. + */ + is_favorite?: boolean; + /** + * Soft-deleted; excluded from default lists. + */ + is_archived?: boolean; + /** + * Marks the contact as deceased. + */ + is_deceased?: boolean; + /** + * Date the contact passed away. + */ + deceased_at?: (string | null); + /** + * Target days between interactions; drives losing-touch cadence. + */ + contact_frequency_days?: (number | null); + /** + * Kanban stage like Active, Dormant, Lost. + */ + stage?: (string | null); + tag_ids?: (Array<(string)> | null); + group_ids?: (Array<(string)> | null); +}; + +export type ContactFieldCreate = { + /** + * Kind of contact info (email or phone). + */ + field_type: ContactFieldType; + /** + * Label like "home", "work", "cell", "twitter". + */ + label: string; + /** + * The actual email address, phone number, etc. + */ + value: string; + /** + * Marks the primary entry for this field_type on the contact. + */ + is_primary?: boolean; + /** + * Display order within the same field_type. + */ + sort_order?: number; + contact_id: string; +}; + +export type ContactFieldPublic = { + /** + * Kind of contact info (email or phone). + */ + field_type: ContactFieldType; + /** + * Label like "home", "work", "cell", "twitter". + */ + label: string; + /** + * The actual email address, phone number, etc. + */ + value: string; + /** + * Marks the primary entry for this field_type on the contact. + */ + is_primary?: boolean; + /** + * Display order within the same field_type. + */ + sort_order?: number; + id: string; + contact_id: string; +}; + +export type ContactFieldType = 'email' | 'phone'; + +export type ContactFieldUpdate = { + field_type?: (ContactFieldType | null); + label?: (string | null); + value?: (string | null); + is_primary?: (boolean | null); + sort_order?: (number | null); +}; + +export type ContactPublic = { + /** + * Given name; required. + */ + first_name: string; + /** + * Family name. + */ + last_name?: (string | null); + /** + * Middle name or initial. + */ + middle_name?: (string | null); + /** + * Honorific like Dr., Mr., Ms. + */ + prefix?: (string | null); + /** + * Suffix like Jr., PhD. + */ + suffix?: (string | null); + /** + * Preferred or informal name. + */ + nickname?: (string | null); + /** + * Organization name. + */ + company?: (string | null); + /** + * Department within the company. + */ + department?: (string | null); + /** + * Job title. + */ + title?: (string | null); + /** + * Date of birth; used for milestone and birthday reminders. + */ + birthday?: (string | null); + /** + * Short story of how the introduction happened. + */ + how_we_met?: (string | null); + /** + * Pinned to the top of contact lists. + */ + is_favorite?: boolean; + /** + * Soft-deleted; excluded from default lists. + */ + is_archived?: boolean; + /** + * Marks the contact as deceased. + */ + is_deceased?: boolean; + /** + * Date the contact passed away. + */ + deceased_at?: (string | null); + /** + * Target days between interactions; drives losing-touch cadence. + */ + contact_frequency_days?: (number | null); + /** + * Kanban stage like Active, Dormant, Lost. + */ + stage?: (string | null); + id: string; + avatar_url: (string | null); + last_contacted_at: (string | null); + created_at: string; + updated_at: string; + deleted_at?: (string | null); + tags?: Array; + groups?: Array; +}; + +export type ContactsPublic = { + data: Array; + count: number; +}; + +export type ContactUpdate = { + first_name?: (string | null); + last_name?: (string | null); + middle_name?: (string | null); + prefix?: (string | null); + suffix?: (string | null); + nickname?: (string | null); + company?: (string | null); + department?: (string | null); + title?: (string | null); + birthday?: (string | null); + how_we_met?: (string | null); + is_favorite?: (boolean | null); + is_archived?: (boolean | null); + is_deceased?: (boolean | null); + deceased_at?: (string | null); + contact_frequency_days?: (number | null); + stage?: (string | null); + tag_ids?: (Array<(string)> | null); + group_ids?: (Array<(string)> | null); +}; + +export type CustomFieldDefinitionCreate = { + /** + * Custom field name shown in the UI. + */ + name: string; + /** + * Field type: text, number, date, boolean, or select. + */ + field_type?: string; + /** + * Help text displayed alongside the field in the UI. + */ + description?: (string | null); + /** + * Comma-separated options for field_type="select". + */ + options?: (string | null); + /** + * Icon slug for the UI (e.g. "heart", "book"). + */ + icon?: (string | null); +}; + +export type CustomFieldDefinitionPublic = { + /** + * Custom field name shown in the UI. + */ + name: string; + /** + * Field type: text, number, date, boolean, or select. + */ + field_type?: string; + /** + * Help text displayed alongside the field in the UI. + */ + description?: (string | null); + /** + * Comma-separated options for field_type="select". + */ + options?: (string | null); + /** + * Icon slug for the UI (e.g. "heart", "book"). + */ + icon?: (string | null); + id: string; + created_at: string; +}; + +export type CustomFieldDefinitionUpdate = { + name?: (string | null); + field_type?: (string | null); + description?: (string | null); + options?: (string | null); + icon?: (string | null); +}; + +export type CustomFieldValueCreate = { + /** + * Value as a string; coerced from the declared field_type. + */ + value: string; + contact_id: string; + field_definition_id: string; +}; + +export type CustomFieldValuePublic = { + /** + * Value as a string; coerced from the declared field_type. + */ + value: string; + id: string; + contact_id: string; + field_definition_id: string; + field_name?: (string | null); +}; + +export type CustomFieldValueUpdate = { + value?: (string | null); +}; + +export type DebtCreate = { + /** + * Who owes whom: i_owe (you owe them) or they_owe (they owe you). + */ + direction: DebtDirection; + /** + * Amount owed; must be greater than zero. + */ + amount: number; + /** + * ISO 4217 currency code. + */ + currency?: string; + /** + * What the debt is for. + */ + reason?: (string | null); + /** + * Marked paid off. + */ + is_settled?: boolean; + /** + * Date the debt was settled. + */ + settled_at?: (string | null); + contact_id: string; +}; + +export type DebtDirection = 'i_owe' | 'they_owe'; + +export type DebtPublic = { + /** + * Who owes whom: i_owe (you owe them) or they_owe (they owe you). + */ + direction: DebtDirection; + /** + * Amount owed; must be greater than zero. + */ + amount: number; + /** + * ISO 4217 currency code. + */ + currency?: string; + /** + * What the debt is for. + */ + reason?: (string | null); + /** + * Marked paid off. + */ + is_settled?: boolean; + /** + * Date the debt was settled. + */ + settled_at?: (string | null); + id: string; + contact_id: string; + created_at: string; +}; + +export type DebtsPublic = { + data: Array; + count: number; +}; + +export type DebtUpdate = { + direction?: (DebtDirection | null); + amount?: (number | null); + currency?: (string | null); + reason?: (string | null); + is_settled?: (boolean | null); + settled_at?: (string | null); +}; + +export type GiftCreate = { + /** + * Gift name. + */ + name: string; + /** + * Details about the gift. + */ + description?: (string | null); + /** + * Lifecycle: idea, given, or received. + */ + status?: GiftStatus; + /** + * Occasion like Birthday, Christmas, Housewarming. + */ + occasion?: (string | null); + /** + * When the gift was given or received. + */ + gift_date?: (string | null); + /** + * Monetary cost or value. + */ + value_amount?: (number | null); + /** + * ISO 4217 currency code. + */ + value_currency?: string; + /** + * Link to the product page (e.g. Amazon). + */ + url?: (string | null); + contact_id: string; +}; + +export type GiftPublic = { + /** + * Gift name. + */ + name: string; + /** + * Details about the gift. + */ + description?: (string | null); + /** + * Lifecycle: idea, given, or received. + */ + status?: GiftStatus; + /** + * Occasion like Birthday, Christmas, Housewarming. + */ + occasion?: (string | null); + /** + * When the gift was given or received. + */ + gift_date?: (string | null); + /** + * Monetary cost or value. + */ + value_amount?: (number | null); + /** + * ISO 4217 currency code. + */ + value_currency?: string; + /** + * Link to the product page (e.g. Amazon). + */ + url?: (string | null); + id: string; + contact_id: string; + created_at: string; +}; + +export type GiftsPublic = { + data: Array; + count: number; +}; + +export type GiftStatus = 'idea' | 'given' | 'received'; + +export type GiftUpdate = { + name?: (string | null); + description?: (string | null); + status?: (GiftStatus | null); + occasion?: (string | null); + gift_date?: (string | null); + value_amount?: (number | null); + value_currency?: (string | null); + url?: (string | null); +}; + +export type GroupCreate = { + /** + * Group name, 1-255 chars. + */ + name: string; + /** + * Optional group description. + */ + description?: (string | null); +}; + +export type GroupPublic = { + /** + * Group name, 1-255 chars. + */ + name: string; + /** + * Optional group description. + */ + description?: (string | null); + id: string; + created_at: string; +}; + +export type GroupsPublic = { + data: Array; + count: number; +}; + +export type GroupUpdate = { + name?: (string | null); + description?: (string | null); +}; + +export type HTTPValidationError = { + detail?: Array; +}; + +export type InteractionAttendeeSummary = { + id: string; + first_name: string; + last_name?: (string | null); + avatar_url?: (string | null); +}; + +export type InteractionChannel = 'call' | 'in_person' | 'text' | 'email' | 'video' | 'social' | 'other'; + +export type InteractionCreate = { + /** + * How the interaction happened (call, in_person, text, etc.). + */ + channel: InteractionChannel; + /** + * When the interaction actually took place. + */ + occurred_at: string; + /** + * Conversation summary, action items, etc. + */ + notes?: (string | null); + /** + * Emoji or keyword capturing the tone. + */ + mood?: (string | null); + /** + * Length of the interaction in minutes. + */ + duration_minutes?: (number | null); + /** + * Contacts that attended; must have at least one. + */ + attendee_ids: Array<(string)>; +}; + +export type InteractionPublic = { + /** + * How the interaction happened (call, in_person, text, etc.). + */ + channel: InteractionChannel; + /** + * When the interaction actually took place. + */ + occurred_at: string; + /** + * Conversation summary, action items, etc. + */ + notes?: (string | null); + /** + * Emoji or keyword capturing the tone. + */ + mood?: (string | null); + /** + * Length of the interaction in minutes. + */ + duration_minutes?: (number | null); + id: string; + attendees?: Array; + created_at: string; +}; + +export type InteractionsPublic = { + data: Array; + count: number; +}; + +export type InteractionUpdate = { + channel?: (InteractionChannel | null); + occurred_at?: (string | null); + notes?: (string | null); + mood?: (string | null); + duration_minutes?: (number | null); + /** + * Replace the attendee set; must have at least one if provided. + */ + attendee_ids?: (Array<(string)> | null); +}; + +export type JournalEntriesPublic = { + data: Array; + count: number; +}; + +export type JournalEntryCreate = { + /** + * Entry body, 1-50000 chars. + */ + body: string; + /** + * Emoji or keyword capturing the mood. + */ + mood?: (string | null); + /** + * Date the entry is about (may differ from created_at). + */ + entry_date: string; +}; + +export type JournalEntryPublic = { + /** + * Entry body, 1-50000 chars. + */ + body: string; + /** + * Emoji or keyword capturing the mood. + */ + mood?: (string | null); + /** + * Date the entry is about (may differ from created_at). + */ + entry_date: string; + id: string; + created_at: string; + updated_at: string; +}; + +export type JournalEntryUpdate = { + body?: (string | null); + mood?: (string | null); + entry_date?: (string | null); +}; + +export type LifeEventCreate = { + /** + * Kind of milestone: job_change, move, wedding, baby, graduation, birthday, anniversary, etc. + */ + event_type: string; + /** + * Event title. + */ + title: string; + /** + * Extra details about the event. + */ + description?: (string | null); + /** + * Date the event happened. + */ + occurred_at: string; + /** + * If true, auto-create a yearly recurring reminder on this date. + */ + create_annual_reminder?: boolean; + contact_id: string; +}; + +export type LifeEventPublic = { + /** + * Kind of milestone: job_change, move, wedding, baby, graduation, birthday, anniversary, etc. + */ + event_type: string; + /** + * Event title. + */ + title: string; + /** + * Extra details about the event. + */ + description?: (string | null); + /** + * Date the event happened. + */ + occurred_at: string; + /** + * If true, auto-create a yearly recurring reminder on this date. + */ + create_annual_reminder?: boolean; + id: string; + contact_id: string; + created_at: string; +}; + +export type LifeEventsPublic = { + data: Array; + count: number; +}; + +export type LifeEventUpdate = { + event_type?: (string | null); + title?: (string | null); + description?: (string | null); + occurred_at?: (string | null); + create_annual_reminder?: (boolean | null); +}; + +export type MediaCategory = 'movie' | 'tv_show' | 'podcast' | 'musician' | 'book' | 'other'; + +export type MediaRecommendationCreate = { + /** + * Media category: movie, tv_show, podcast, musician, book, or other. + */ + category: MediaCategory; + /** + * Title of the work. + */ + title: string; + /** + * Author, director, artist, or similar creator. + */ + creator?: (string | null); + /** + * Why it was recommended or personal reaction. + */ + note?: (string | null); + /** + * Date the recommendation was made. + */ + recommended_at?: (string | null); + contact_id: string; +}; + +export type MediaRecommendationPublic = { + /** + * Media category: movie, tv_show, podcast, musician, book, or other. + */ + category: MediaCategory; + /** + * Title of the work. + */ + title: string; + /** + * Author, director, artist, or similar creator. + */ + creator?: (string | null); + /** + * Why it was recommended or personal reaction. + */ + note?: (string | null); + /** + * Date the recommendation was made. + */ + recommended_at?: (string | null); + id: string; + contact_id: string; + created_at: string; + updated_at: string; +}; + +export type MediaRecommendationsPublic = { + data: Array; + count: number; +}; + +export type MediaRecommendationUpdate = { + category?: (MediaCategory | null); + title?: (string | null); + creator?: (string | null); + note?: (string | null); + recommended_at?: (string | null); +}; + +export type Message = { + message: string; +}; + +export type NewPassword = { + token: string; + new_password: string; +}; + +export type NoteCreate = { + /** + * Note body, 1-50000 chars. + */ + body: string; + contact_id: string; +}; + +export type NoteMentionPublic = { + note_id: string; + note_body: string; + note_created_at: string; + source_contact: _MentionSourceContact; +}; + +export type NotePublic = { + /** + * Note body, 1-50000 chars. + */ + body: string; + id: string; + contact_id: string; + created_at: string; + updated_at: string; +}; + +export type NotesPublic = { + data: Array; + count: number; +}; + +export type NoteUpdate = { + body?: (string | null); +}; + +export type PetCreate = { + /** + * Pet's name. + */ + name: string; + /** + * Species like dog, cat, bird. + */ + species?: (string | null); + /** + * Breed, if known. + */ + breed?: (string | null); + /** + * Freeform notes (e.g. allergies, birthday). + */ + notes?: (string | null); + contact_id: string; +}; + +export type PetPublic = { + /** + * Pet's name. + */ + name: string; + /** + * Species like dog, cat, bird. + */ + species?: (string | null); + /** + * Breed, if known. + */ + breed?: (string | null); + /** + * Freeform notes (e.g. allergies, birthday). + */ + notes?: (string | null); + id: string; + contact_id: string; +}; + +export type PetUpdate = { + name?: (string | null); + species?: (string | null); + breed?: (string | null); + notes?: (string | null); +}; + +export type RelationshipCreate = { + /** + * Kind of relationship: spouse, child, parent, sibling, friend, colleague, etc. + */ + relationship_type: string; + /** + * Additional context about the relationship. + */ + notes?: (string | null); + contact_id: string; + related_contact_id: string; + /** + * Type for the auto-generated inverse row. If omitted, the server infers it from a known map of symmetric/asymmetric types and returns 422 when it cannot. + */ + inverse_relationship_type?: (string | null); +}; + +export type RelationshipPublic = { + /** + * Kind of relationship: spouse, child, parent, sibling, friend, colleague, etc. + */ + relationship_type: string; + /** + * Additional context about the relationship. + */ + notes?: (string | null); + id: string; + contact_id: string; + related_contact_id: string; + inverse_id?: (string | null); +}; + +export type RelationshipUpdate = { + relationship_type?: (string | null); + notes?: (string | null); +}; + +export type ReminderCreate = { + /** + * Reminder title. + */ + title: string; + /** + * Extra details shown with the reminder. + */ + description?: (string | null); + /** + * When to fire the reminder. + */ + remind_at: string; + /** + * How often the reminder repeats. + */ + frequency?: ReminderFrequency; + /** + * Enable or disable without deleting. + */ + is_active?: boolean; + contact_id?: (string | null); +}; + +export type ReminderFrequency = 'once' | 'daily' | 'weekly' | 'monthly' | 'yearly'; + +export type ReminderPublic = { + /** + * Reminder title. + */ + title: string; + /** + * Extra details shown with the reminder. + */ + description?: (string | null); + /** + * When to fire the reminder. + */ + remind_at: string; + /** + * How often the reminder repeats. + */ + frequency?: ReminderFrequency; + /** + * Enable or disable without deleting. + */ + is_active?: boolean; + id: string; + contact_id: (string | null); + last_sent_at: (string | null); + snoozed_until: (string | null); + created_at: string; +}; + +export type RemindersPublic = { + data: Array; + count: number; +}; + +export type ReminderUpdate = { + title?: (string | null); + description?: (string | null); + remind_at?: (string | null); + frequency?: (ReminderFrequency | null); + is_active?: (boolean | null); +}; + +export type TagCreate = { + /** + * Tag name, 1-100 chars. + */ + name: string; + /** + * Optional hex color like #ff0000 for UI display. + */ + color?: (string | null); +}; + +export type TagPublic = { + /** + * Tag name, 1-100 chars. + */ + name: string; + /** + * Optional hex color like #ff0000 for UI display. + */ + color?: (string | null); + id: string; + created_at: string; +}; + +export type TagSharePublic = { + tag_id: string; + grantee_id: string; + grantee_email: string; + created_at: string; +}; + +export type TagSharesPublic = { + data: Array; + count: number; +}; + +export type TagsPublic = { + data: Array; + count: number; +}; + +export type TagUpdate = { + name?: (string | null); + color?: (string | null); +}; + +export type Token = { + access_token: string; + token_type?: string; +}; + +export type UpdatePassword = { + current_password: string; + new_password: string; +}; + +export type UserCreate = { + /** + * Login email; must be unique. + */ + email: string; + /** + * Whether the account can log in. + */ + is_active?: boolean; + /** + * Grants admin-only endpoints. + */ + is_superuser?: boolean; + /** + * Display name; optional. + */ + full_name?: (string | null); + password: string; +}; + +export type UserPublic = { + /** + * Login email; must be unique. + */ + email: string; + /** + * Whether the account can log in. + */ + is_active?: boolean; + /** + * Grants admin-only endpoints. + */ + is_superuser?: boolean; + /** + * Display name; optional. + */ + full_name?: (string | null); + id: string; + created_at?: (string | null); +}; + +export type UserRegister = { + email: string; + password: string; + full_name?: (string | null); +}; + +export type UsersPublic = { + data: Array; + count: number; +}; + +export type UserUpdate = { + email?: (string | null); + /** + * Whether the account can log in. + */ + is_active?: boolean; + /** + * Grants admin-only endpoints. + */ + is_superuser?: boolean; + /** + * Display name; optional. + */ + full_name?: (string | null); + password?: (string | null); +}; + +export type UserUpdateMe = { + full_name?: (string | null); + email?: (string | null); +}; + +export type ValidationError = { + loc: Array<(string | number)>; + msg: string; + type: string; + input?: unknown; + ctx?: { + [key: string]: unknown; + }; +}; + +export type WebhookEndpointBase = { + /** + * Human-readable endpoint name. + */ + name: string; + /** + * Target URL for outbound webhooks; null for inbound. + */ + url?: (string | null); + /** + * "inbound" or "outbound". + */ + direction: string; + /** + * Comma-separated event types (e.g. contact.created,interaction.logged). + */ + event_types?: (string | null); + /** + * Enable or disable without deleting. + */ + is_active?: boolean; + /** + * HMAC secret for verifying inbound payloads. + */ + secret?: (string | null); +}; + +export type ActivityLogsListActivityLogsData = { + entityId?: (string | null); + entityType?: (string | null); + limit?: number; + offset?: number; + tagId?: (string | null); +}; + +export type ActivityLogsListActivityLogsResponse = (ActivityLogsPublic); + +export type AddressesListAddressesData = { + contactId: string; +}; + +export type AddressesListAddressesResponse = (unknown); + +export type AddressesCreateAddressRouteData = { + requestBody: AddressCreate; +}; + +export type AddressesCreateAddressRouteResponse = (AddressPublic); + +export type AddressesUpdateAddressData = { + addressId: string; + requestBody: AddressUpdate; +}; + +export type AddressesUpdateAddressResponse = (AddressPublic); + +export type AddressesDeleteAddressData = { + addressId: string; +}; + +export type AddressesDeleteAddressResponse = (unknown); + +export type CalendarGetCalendarMonthData = { + yyyyMm: string; +}; + +export type CalendarGetCalendarMonthResponse = (CalendarMonthResponse); + +export type CarddavWellKnownCarddavResponse = (unknown); + +export type ContactFieldsListContactFieldsData = { + contactId: string; + limit?: number; + skip?: number; +}; + +export type ContactFieldsListContactFieldsResponse = (unknown); + +export type ContactFieldsCreateContactFieldRouteData = { + requestBody: ContactFieldCreate; +}; + +export type ContactFieldsCreateContactFieldRouteResponse = (ContactFieldPublic); + +export type ContactFieldsUpdateContactFieldData = { + fieldId: string; + requestBody: ContactFieldUpdate; +}; + +export type ContactFieldsUpdateContactFieldResponse = (ContactFieldPublic); + +export type ContactFieldsDeleteContactFieldData = { + fieldId: string; +}; + +export type ContactFieldsDeleteContactFieldResponse = (unknown); + +export type ContactsListContactsData = { + groupId?: (string | null); + ids?: (Array<(string)> | null); + includeDeleted?: boolean; + isArchived?: (boolean | null); + isFavorite?: (boolean | null); + limit?: number; + onlyDeleted?: boolean; + search?: (string | null); + skip?: number; + stage?: (string | null); + tagId?: (string | null); +}; + +export type ContactsListContactsResponse = (ContactsPublic); + +export type ContactsCreateContactData = { + requestBody: ContactCreate; +}; + +export type ContactsCreateContactResponse = (ContactPublic); + +export type ContactsListLosingTouchData = { + limit?: number; +}; + +export type ContactsListLosingTouchResponse = (ContactsPublic); + +export type ContactsGetContactData = { + contactId: string; +}; + +export type ContactsGetContactResponse = (ContactPublic); + +export type ContactsUpdateContactData = { + contactId: string; + requestBody: ContactUpdate; +}; + +export type ContactsUpdateContactResponse = (ContactPublic); + +export type ContactsDeleteContactData = { + contactId: string; +}; + +export type ContactsDeleteContactResponse = (unknown); + +export type ContactsListContactMentionsData = { + contactId: string; +}; + +export type ContactsListContactMentionsResponse = (Array); + +export type ContactsRestoreContactData = { + contactId: string; +}; + +export type ContactsRestoreContactResponse = (ContactPublic); + +export type CustomFieldsListFieldDefinitionsResponse = (unknown); + +export type CustomFieldsCreateFieldDefinitionData = { + requestBody: CustomFieldDefinitionCreate; +}; + +export type CustomFieldsCreateFieldDefinitionResponse = (CustomFieldDefinitionPublic); + +export type CustomFieldsUpdateFieldDefinitionData = { + defId: string; + requestBody: CustomFieldDefinitionUpdate; +}; + +export type CustomFieldsUpdateFieldDefinitionResponse = (CustomFieldDefinitionPublic); + +export type CustomFieldsDeleteFieldDefinitionData = { + defId: string; +}; + +export type CustomFieldsDeleteFieldDefinitionResponse = (unknown); + +export type CustomFieldsListFieldValuesData = { + contactId: string; +}; + +export type CustomFieldsListFieldValuesResponse = (unknown); + +export type CustomFieldsCreateFieldValueData = { + requestBody: CustomFieldValueCreate; +}; + +export type CustomFieldsCreateFieldValueResponse = (CustomFieldValuePublic); + +export type CustomFieldsUpdateFieldValueData = { + requestBody: CustomFieldValueUpdate; + valueId: string; +}; + +export type CustomFieldsUpdateFieldValueResponse = (CustomFieldValuePublic); + +export type CustomFieldsDeleteFieldValueData = { + valueId: string; +}; + +export type CustomFieldsDeleteFieldValueResponse = (unknown); + +export type DebtsListDebtsData = { + contactId: string; +}; + +export type DebtsListDebtsResponse = (DebtsPublic); + +export type DebtsCreateDebtRouteData = { + requestBody: DebtCreate; +}; + +export type DebtsCreateDebtRouteResponse = (DebtPublic); + +export type DebtsUpdateDebtData = { + debtId: string; + requestBody: DebtUpdate; +}; + +export type DebtsUpdateDebtResponse = (DebtPublic); + +export type DebtsDeleteDebtData = { + debtId: string; +}; + +export type DebtsDeleteDebtResponse = (unknown); + +export type GiftsListGiftsData = { + contactId: string; +}; + +export type GiftsListGiftsResponse = (GiftsPublic); + +export type GiftsCreateGiftRouteData = { + requestBody: GiftCreate; +}; + +export type GiftsCreateGiftRouteResponse = (GiftPublic); + +export type GiftsUpdateGiftData = { + giftId: string; + requestBody: GiftUpdate; +}; + +export type GiftsUpdateGiftResponse = (GiftPublic); + +export type GiftsDeleteGiftData = { + giftId: string; +}; + +export type GiftsDeleteGiftResponse = (unknown); + +export type GroupsListGroupsData = { + limit?: number; + skip?: number; +}; + +export type GroupsListGroupsResponse = (GroupsPublic); + +export type GroupsCreateGroupRouteData = { + requestBody: GroupCreate; +}; + +export type GroupsCreateGroupRouteResponse = (GroupPublic); + +export type GroupsUpdateGroupData = { + groupId: string; + requestBody: GroupUpdate; +}; + +export type GroupsUpdateGroupResponse = (GroupPublic); + +export type GroupsDeleteGroupData = { + groupId: string; +}; + +export type GroupsDeleteGroupResponse = (unknown); + +export type ImportExportImportVcardData = { + formData: Body_import_export_import_vcard; +}; + +export type ImportExportImportVcardResponse = (unknown); + +export type ImportExportExportVcardResponse = (unknown); + +export type ImportExportExportJsonResponse = (unknown); + +export type InteractionsListInteractionsData = { + contactId?: (string | null); + limit?: number; + skip?: number; +}; + +export type InteractionsListInteractionsResponse = (InteractionsPublic); + +export type InteractionsCreateInteractionRouteData = { + requestBody: InteractionCreate; +}; + +export type InteractionsCreateInteractionRouteResponse = (InteractionPublic); + +export type InteractionsUpdateInteractionData = { + interactionId: string; + requestBody: InteractionUpdate; +}; + +export type InteractionsUpdateInteractionResponse = (InteractionPublic); + +export type InteractionsDeleteInteractionData = { + interactionId: string; +}; + +export type InteractionsDeleteInteractionResponse = (unknown); + +export type JournalListJournalEntriesData = { + limit?: number; + skip?: number; +}; + +export type JournalListJournalEntriesResponse = (JournalEntriesPublic); + +export type JournalCreateJournalEntryRouteData = { + requestBody: JournalEntryCreate; +}; + +export type JournalCreateJournalEntryRouteResponse = (JournalEntryPublic); + +export type JournalUpdateJournalEntryData = { + entryId: string; + requestBody: JournalEntryUpdate; +}; + +export type JournalUpdateJournalEntryResponse = (JournalEntryPublic); + +export type JournalDeleteJournalEntryData = { + entryId: string; +}; + +export type JournalDeleteJournalEntryResponse = (unknown); + +export type LifeEventsListLifeEventsData = { + contactId: string; +}; + +export type LifeEventsListLifeEventsResponse = (LifeEventsPublic); + +export type LifeEventsCreateLifeEventRouteData = { + requestBody: LifeEventCreate; +}; + +export type LifeEventsCreateLifeEventRouteResponse = (LifeEventPublic); + +export type LifeEventsUpdateLifeEventData = { + eventId: string; + requestBody: LifeEventUpdate; +}; + +export type LifeEventsUpdateLifeEventResponse = (LifeEventPublic); + +export type LifeEventsDeleteLifeEventData = { + eventId: string; +}; + +export type LifeEventsDeleteLifeEventResponse = (unknown); + +export type LoginLoginAccessTokenData = { + formData: Body_login_login_access_token; +}; + +export type LoginLoginAccessTokenResponse = (Token); + +export type LoginTestTokenResponse = (UserPublic); + +export type LoginRecoverPasswordData = { + email: string; +}; + +export type LoginRecoverPasswordResponse = (Message); + +export type LoginResetPasswordData = { + requestBody: NewPassword; +}; + +export type LoginResetPasswordResponse = (Message); + +export type LoginRecoverPasswordHtmlContentData = { + email: string; +}; + +export type LoginRecoverPasswordHtmlContentResponse = (string); + +export type MediaRecommendationsListMediaRecommendationsData = { + contactId: string; +}; + +export type MediaRecommendationsListMediaRecommendationsResponse = (MediaRecommendationsPublic); + +export type MediaRecommendationsCreateMediaRecommendationRouteData = { + requestBody: MediaRecommendationCreate; +}; + +export type MediaRecommendationsCreateMediaRecommendationRouteResponse = (MediaRecommendationPublic); + +export type MediaRecommendationsUpdateMediaRecommendationData = { + recId: string; + requestBody: MediaRecommendationUpdate; +}; + +export type MediaRecommendationsUpdateMediaRecommendationResponse = (MediaRecommendationPublic); + +export type MediaRecommendationsDeleteMediaRecommendationData = { + recId: string; +}; + +export type MediaRecommendationsDeleteMediaRecommendationResponse = (unknown); + +export type NotesListNotesData = { + contactId: string; + limit?: number; + skip?: number; +}; + +export type NotesListNotesResponse = (NotesPublic); + +export type NotesCreateNoteRouteData = { + requestBody: NoteCreate; +}; + +export type NotesCreateNoteRouteResponse = (NotePublic); + +export type NotesUpdateNoteRouteData = { + noteId: string; + requestBody: NoteUpdate; +}; + +export type NotesUpdateNoteRouteResponse = (NotePublic); + +export type NotesDeleteNoteData = { + noteId: string; +}; + +export type NotesDeleteNoteResponse = (unknown); + +export type PetsListPetsData = { + contactId: string; +}; + +export type PetsListPetsResponse = (unknown); + +export type PetsCreatePetRouteData = { + requestBody: PetCreate; +}; + +export type PetsCreatePetRouteResponse = (PetPublic); + +export type PetsUpdatePetData = { + petId: string; + requestBody: PetUpdate; +}; + +export type PetsUpdatePetResponse = (PetPublic); + +export type PetsDeletePetData = { + petId: string; +}; + +export type PetsDeletePetResponse = (unknown); + +export type RelationshipsLookupInverseData = { + type: string; +}; + +export type RelationshipsLookupInverseResponse = ({ + [key: string]: (string | null); +}); + +export type RelationshipsListRelationshipsData = { + contactId: string; +}; + +export type RelationshipsListRelationshipsResponse = (unknown); + +export type RelationshipsCreateRelationshipRouteData = { + requestBody: RelationshipCreate; +}; + +export type RelationshipsCreateRelationshipRouteResponse = (RelationshipPublic); + +export type RelationshipsUpdateRelationshipData = { + relId: string; + requestBody: RelationshipUpdate; +}; + +export type RelationshipsUpdateRelationshipResponse = (RelationshipPublic); + +export type RelationshipsDeleteRelationshipData = { + relId: string; +}; + +export type RelationshipsDeleteRelationshipResponse = (unknown); + +export type RemindersListRemindersData = { + isActive?: (boolean | null); + limit?: number; + skip?: number; +}; + +export type RemindersListRemindersResponse = (RemindersPublic); + +export type RemindersCreateReminderRouteData = { + requestBody: ReminderCreate; +}; + +export type RemindersCreateReminderRouteResponse = (ReminderPublic); + +export type RemindersUpdateReminderData = { + reminderId: string; + requestBody: ReminderUpdate; +}; + +export type RemindersUpdateReminderResponse = (ReminderPublic); + +export type RemindersDeleteReminderData = { + reminderId: string; +}; + +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 = { + limit?: number; + skip?: number; +}; + +export type TagsListTagsResponse = (TagsPublic); + +export type TagsCreateTagRouteData = { + requestBody: TagCreate; +}; + +export type TagsCreateTagRouteResponse = (TagPublic); + +export type TagsUpdateTagData = { + requestBody: TagUpdate; + tagId: string; +}; + +export type TagsUpdateTagResponse = (TagPublic); + +export type TagsDeleteTagData = { + tagId: string; +}; + +export type TagsDeleteTagResponse = (unknown); + +export type TagSharesCreateTagShareData = { + requestBody: _ShareIn; +}; + +export type TagSharesCreateTagShareResponse = (TagSharePublic); + +export type TagSharesListTagSharesData = { + tagId: string; +}; + +export type TagSharesListTagSharesResponse = (TagSharesPublic); + +export type TagSharesDeleteTagShareData = { + granteeId: string; + tagId: string; +}; + +export type TagSharesDeleteTagShareResponse = ({ + [key: string]: (string); +}); + +export type UsersReadUsersData = { + limit?: number; + skip?: number; +}; + +export type UsersReadUsersResponse = (UsersPublic); + +export type UsersCreateUserData = { + requestBody: UserCreate; +}; + +export type UsersCreateUserResponse = (UserPublic); + +export type UsersReadUserMeResponse = (UserPublic); + +export type UsersDeleteUserMeResponse = (Message); + +export type UsersUpdateUserMeData = { + requestBody: UserUpdateMe; +}; + +export type UsersUpdateUserMeResponse = (UserPublic); + +export type UsersUpdatePasswordMeData = { + requestBody: UpdatePassword; +}; + +export type UsersUpdatePasswordMeResponse = (Message); + +export type UsersRegisterUserData = { + requestBody: UserRegister; +}; + +export type UsersRegisterUserResponse = (UserPublic); + +export type UsersReadUserByIdData = { + userId: string; +}; + +export type UsersReadUserByIdResponse = (UserPublic); + +export type UsersUpdateUserData = { + requestBody: UserUpdate; + userId: string; +}; + +export type UsersUpdateUserResponse = (UserPublic); + +export type UsersDeleteUserData = { + userId: string; +}; + +export type UsersDeleteUserResponse = (Message); + +export type UtilsTestEmailData = { + emailTo: string; +}; + +export type UtilsTestEmailResponse = (Message); + +export type UtilsHealthCheckResponse = (boolean); + +export type WebhooksListWebhooksResponse = (unknown); + +export type WebhooksCreateWebhookData = { + requestBody: WebhookEndpointBase; +}; + +export type WebhooksCreateWebhookResponse = (unknown); + +export type WebhooksUpdateWebhookData = { + requestBody: WebhookEndpointBase; + webhookId: string; +}; + +export type WebhooksUpdateWebhookResponse = (unknown); + +export type WebhooksDeleteWebhookData = { + webhookId: string; +}; + +export type WebhooksDeleteWebhookResponse = (unknown); + +export type WebhooksInboundWebhookData = { + apiKey: string; + requestBody: { + [key: string]: unknown; + }; +}; + +export type WebhooksInboundWebhookResponse = (unknown); \ No newline at end of file From 8d96b7d875debd6e0af7b6318fd124c020fde613 Mon Sep 17 00:00:00 2001 From: Will Pike <6687499+pike00@users.noreply.github.com> Date: Sun, 3 May 2026 19:36:30 -0500 Subject: [PATCH 2/2] WIP: dirac autorun for reminders-bell-badge (error) --- backend/app/api/routes/reminders.py | 68 +++++++++++++++++++++-------- backend/app/models.py | 10 +++++ front | 21 +++++++++ 3 files changed, 82 insertions(+), 17 deletions(-) create mode 100644 front diff --git a/backend/app/api/routes/reminders.py b/backend/app/api/routes/reminders.py index bca4703e..a81914fc 100644 --- a/backend/app/api/routes/reminders.py +++ b/backend/app/api/routes/reminders.py @@ -14,6 +14,8 @@ ReminderPublic, RemindersPublic, ReminderUpdate, + ReminderWithContactPublic, + RemindersWithContactPublic, ) router = APIRouter(prefix="/reminders", tags=["reminders"]) @@ -58,7 +60,7 @@ def list_reminders( ) -@router.get("/due", response_model=RemindersPublic) +@router.get("/due", response_model=RemindersWithContactPublic) def list_due_reminders( session: SessionDep, current_user: CurrentUser, @@ -72,30 +74,62 @@ def list_due_reminders( - 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).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)), - ), + 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(statement.subquery()) + 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) - reminders = session.exec(statement).all() - - return RemindersPublic( - data=[ReminderPublic.model_validate(r) for r in reminders], + 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, ) diff --git a/backend/app/models.py b/backend/app/models.py index 606a1bfb..34d3329d 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -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 diff --git a/front b/front new file mode 100644 index 00000000..b9e2f58f --- /dev/null +++ b/front @@ -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; +}