From 55a02b644145de2f4ed8de8cd1afce8fa3f52d16 Mon Sep 17 00:00:00 2001 From: Matthew Meszaros Date: Mon, 8 Jun 2026 15:05:09 +0200 Subject: [PATCH] feat: add notification client APIs Adds typed frontend client calls, hooks, and models for notification preferences, feed reads, and read-state mutations. --- .../client/app/notifications/notifications.ts | 48 +++++++++++++++++ .../app/notifications/useNotifications.ts | 52 +++++++++++++++++++ .../models/app/notifications/Notification.ts | 34 ++++++++++++ 3 files changed, 134 insertions(+) create mode 100644 web/src/lib/api/client/app/notifications/notifications.ts create mode 100644 web/src/lib/api/hooks/app/notifications/useNotifications.ts create mode 100644 web/src/lib/api/models/app/notifications/Notification.ts diff --git a/web/src/lib/api/client/app/notifications/notifications.ts b/web/src/lib/api/client/app/notifications/notifications.ts new file mode 100644 index 00000000..ee896add --- /dev/null +++ b/web/src/lib/api/client/app/notifications/notifications.ts @@ -0,0 +1,48 @@ +import type { AppNotification, NotificationPreferences } from "@/lib/api/models/app/notifications/Notification"; +import Request from "../../Request"; + +export async function getNotificationPreferences(): Promise { + const res = await Request<{ preferences: NotificationPreferences }>({ + method: "GET", + url: "/auth/me/notification-preferences", + authorization: true, + }); + return res.preferences; +} + +export async function updateNotificationPreferences(preferences: NotificationPreferences): Promise { + const res = await Request<{ preferences: NotificationPreferences }>({ + method: "PUT", + url: "/auth/me/notification-preferences", + data: { preferences }, + authorization: true, + }); + return res.preferences; +} + +export async function listNotifications(unreadOnly = false, limit = 50): Promise<{ notifications: AppNotification[]; unread: number }> { + const params = new URLSearchParams(); + if (unreadOnly) params.set("unread", "1"); + params.set("limit", String(limit)); + return await Request<{ notifications: AppNotification[]; unread: number }>({ + method: "GET", + url: `/auth/me/notifications?${params.toString()}`, + authorization: true, + }); +} + +export async function markNotificationRead(id: string): Promise { + await Request({ + method: "POST", + url: `/auth/me/notifications/${id}/read`, + authorization: true, + }); +} + +export async function markAllNotificationsRead(): Promise { + await Request({ + method: "PUT", + url: "/auth/me/notifications", + authorization: true, + }); +} diff --git a/web/src/lib/api/hooks/app/notifications/useNotifications.ts b/web/src/lib/api/hooks/app/notifications/useNotifications.ts new file mode 100644 index 00000000..48dd68c9 --- /dev/null +++ b/web/src/lib/api/hooks/app/notifications/useNotifications.ts @@ -0,0 +1,52 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { + getNotificationPreferences, + updateNotificationPreferences, + listNotifications, + markNotificationRead, + markAllNotificationsRead, +} from "@/lib/api/client/app/notifications/notifications"; +import type { NotificationPreferences } from "@/lib/api/models/app/notifications/Notification"; + +const PREFS_KEY = ["notifications", "preferences"]; +const FEED_KEY = ["notifications", "feed"]; + +export function useNotificationPreferences() { + return useQuery({ + queryKey: PREFS_KEY, + queryFn: getNotificationPreferences, + staleTime: 60_000, + }); +} + +export function useUpdateNotificationPreferences() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (prefs: NotificationPreferences) => updateNotificationPreferences(prefs), + onSuccess: () => qc.invalidateQueries({ queryKey: PREFS_KEY }), + }); +} + +export function useNotifications() { + return useQuery({ + queryKey: FEED_KEY, + queryFn: () => listNotifications(false, 50), + staleTime: 15_000, + }); +} + +export function useMarkNotificationRead() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (id: string) => markNotificationRead(id), + onSuccess: () => qc.invalidateQueries({ queryKey: FEED_KEY }), + }); +} + +export function useMarkAllNotificationsRead() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: () => markAllNotificationsRead(), + onSuccess: () => qc.invalidateQueries({ queryKey: FEED_KEY }), + }); +} diff --git a/web/src/lib/api/models/app/notifications/Notification.ts b/web/src/lib/api/models/app/notifications/Notification.ts new file mode 100644 index 00000000..c09838cb --- /dev/null +++ b/web/src/lib/api/models/app/notifications/Notification.ts @@ -0,0 +1,34 @@ +// In-app notification feed + per-user preferences (mirrors the Go models). + +export interface ChannelPrefs { + in_app: boolean; + email: boolean; // reserved; not delivered yet +} + +export interface CategoryPref { + enabled: boolean; + channels: ChannelPrefs; +} + +export interface NotificationPreferences { + inbound_reply: CategoryPref; + inbound_out_of_office: CategoryPref; + health_bounce: CategoryPref; + health_complaint: CategoryPref; + health_worker_downtime: CategoryPref; +} + +export type NotificationCategoryKey = keyof NotificationPreferences; + +export interface AppNotification { + id: string; + user_id: string; + organization_id?: string | null; + category: string; + title: string; + body?: string; + link?: string; + metadata?: Record; + read_at?: string | null; + created_at: string; +}