feat(notifications): TODO 7 — draft decision toast + email + bell badge
This commit is contained in:
@@ -687,3 +687,35 @@ export type ChangelogDiff = {
|
||||
after: unknown | null
|
||||
summary: ChangelogDiffSummary
|
||||
}
|
||||
|
||||
// === Notifications (TODO 7: draft decision toast + email) ===
|
||||
|
||||
/**
|
||||
* Single notification row для current user. Backend serializes
|
||||
* {@code notification_log} entry joined с {@code notification_read_state}
|
||||
* (per-user unread tracking).
|
||||
*
|
||||
* <p>{@code payload} — JSON payload эвента (e.g. RecordDraftApproved).
|
||||
* {@code payloadPreview} — server-pre-rendered summary string чтобы UI
|
||||
* не парсил event payload (event_type → user-facing text mapping живёт
|
||||
* backend'е).
|
||||
*/
|
||||
export type NotificationItem = {
|
||||
id: string
|
||||
eventType: string
|
||||
channel: 'EMAIL' | 'EXPRESS' | 'TELEGRAM' | 'IN_APP'
|
||||
sentAt: string
|
||||
read: boolean
|
||||
payloadPreview: string
|
||||
/** Optional deep-link (e.g. /reviews/{draftId} для draft decision). */
|
||||
href?: string | null
|
||||
}
|
||||
|
||||
export type NotificationsResponse = {
|
||||
items: NotificationItem[]
|
||||
page: number
|
||||
size: number
|
||||
totalElements: number
|
||||
/** Unread count for badge — pre-filtered server-side. */
|
||||
totalUnread: number
|
||||
}
|
||||
|
||||
@@ -664,3 +664,39 @@ export const useRetryWebhookDelivery = () => {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// === Notifications mutations (TODO 7) ===
|
||||
|
||||
/**
|
||||
* Mark одно notification как read. Optimistic update — UI обновляется
|
||||
* мгновенно, query invalidates после server confirm. Если backend reject'нет,
|
||||
* onError refetch'нет original state.
|
||||
*/
|
||||
export const useMarkNotificationRead = () => {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: async (notificationId: string): Promise<void> => {
|
||||
await apiClient.post(
|
||||
`/me/notifications/${encodeURIComponent(notificationId)}/read`,
|
||||
)
|
||||
},
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['notifications', 'me'] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark все unread notifications as read. Drawer footer button "Mark all read".
|
||||
*/
|
||||
export const useMarkAllNotificationsRead = () => {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: async (): Promise<void> => {
|
||||
await apiClient.post('/me/notifications/read-all')
|
||||
},
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['notifications', 'me'] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
type CascadePlan,
|
||||
type ChangelogDiff,
|
||||
type ChangelogResponse,
|
||||
type NotificationsResponse,
|
||||
type SchemaDraft,
|
||||
type SchemaReviewQueuePage,
|
||||
type DictionaryDefinition,
|
||||
@@ -804,3 +805,36 @@ export const serverVersionQuery = queryOptions({
|
||||
})
|
||||
|
||||
export const useServerVersion = () => useQuery(serverVersionQuery)
|
||||
|
||||
// === Notifications (TODO 7: draft decision toast + email) ===
|
||||
|
||||
/**
|
||||
* Current user's notifications feed. Backend filters by recipient = sub claim;
|
||||
* returns paginated rows + `totalUnread` для bell badge counter.
|
||||
*
|
||||
* <p>Poll каждые 30s — same cadence как /reviews queue. {@code staleTime} 5s
|
||||
* чтобы query cache не моргал когда user открывает/закрывает drawer.
|
||||
* {@code refetchIntervalInBackground} = true чтобы tab без focus всё равно
|
||||
* decremented badge когда mark-as-read fires.
|
||||
*
|
||||
* <p>Caveat: `unreadOnly=true` gives a tight badge query (1 unread → badge "1"),
|
||||
* full feed query (default) used drawer открыт. Splitting queries keeps badge
|
||||
* fetch cheap when drawer закрыт.
|
||||
*/
|
||||
export const myNotificationsQuery = (unreadOnly = false, limit = 20) =>
|
||||
queryOptions({
|
||||
queryKey: ['notifications', 'me', { unreadOnly, limit }] as const,
|
||||
queryFn: async (): Promise<NotificationsResponse> => {
|
||||
const { data } = await apiClient.get<NotificationsResponse>(
|
||||
'/me/notifications',
|
||||
{ params: { unreadOnly, limit } },
|
||||
)
|
||||
return data
|
||||
},
|
||||
refetchInterval: 30_000,
|
||||
refetchIntervalInBackground: true,
|
||||
staleTime: 5_000,
|
||||
})
|
||||
|
||||
export const useMyNotifications = (unreadOnly = false, limit = 20) =>
|
||||
useQuery(myNotificationsQuery(unreadOnly, limit))
|
||||
|
||||
Reference in New Issue
Block a user