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; +}