222 lines
5.8 KiB
TypeScript
222 lines
5.8 KiB
TypeScript
/**
|
|
* Notification Router
|
|
*
|
|
* Handles in-app notification CRUD operations for users.
|
|
*/
|
|
|
|
import { z } from 'zod'
|
|
import { router, protectedProcedure, adminProcedure } from '../trpc'
|
|
import {
|
|
markNotificationAsRead,
|
|
markAllNotificationsAsRead,
|
|
getUnreadCount,
|
|
deleteExpiredNotifications,
|
|
deleteOldNotifications,
|
|
NotificationIcons,
|
|
NotificationPriorities,
|
|
} from '../services/in-app-notification'
|
|
|
|
export const notificationRouter = router({
|
|
/**
|
|
* List notifications for the current user
|
|
*/
|
|
list: protectedProcedure
|
|
.input(
|
|
z.object({
|
|
unreadOnly: z.boolean().default(false),
|
|
limit: z.number().int().min(1).max(100).default(50),
|
|
cursor: z.string().optional(), // For infinite scroll pagination
|
|
})
|
|
)
|
|
.query(async ({ ctx, input }) => {
|
|
const { unreadOnly, limit, cursor } = input
|
|
const userId = ctx.user.id
|
|
|
|
const where = {
|
|
userId,
|
|
...(unreadOnly && { isRead: false }),
|
|
// Don't show expired notifications
|
|
OR: [{ expiresAt: null }, { expiresAt: { gt: new Date() } }],
|
|
}
|
|
|
|
const notifications = await ctx.prisma.inAppNotification.findMany({
|
|
where,
|
|
take: limit + 1, // Fetch one extra to check if there are more
|
|
orderBy: { createdAt: 'desc' },
|
|
...(cursor && {
|
|
cursor: { id: cursor },
|
|
skip: 1, // Skip the cursor item
|
|
}),
|
|
})
|
|
|
|
let nextCursor: string | undefined
|
|
if (notifications.length > limit) {
|
|
const nextItem = notifications.pop()
|
|
nextCursor = nextItem?.id
|
|
}
|
|
|
|
return {
|
|
notifications,
|
|
nextCursor,
|
|
}
|
|
}),
|
|
|
|
/**
|
|
* Get unread notification count for the current user
|
|
*/
|
|
getUnreadCount: protectedProcedure.query(async ({ ctx }) => {
|
|
return getUnreadCount(ctx.user.id)
|
|
}),
|
|
|
|
/**
|
|
* Check if there are any urgent unread notifications
|
|
*/
|
|
hasUrgent: protectedProcedure.query(async ({ ctx }) => {
|
|
const count = await ctx.prisma.inAppNotification.count({
|
|
where: {
|
|
userId: ctx.user.id,
|
|
isRead: false,
|
|
priority: 'urgent',
|
|
OR: [{ expiresAt: null }, { expiresAt: { gt: new Date() } }],
|
|
},
|
|
})
|
|
return count > 0
|
|
}),
|
|
|
|
/**
|
|
* Mark a single notification as read
|
|
*/
|
|
markAsRead: protectedProcedure
|
|
.input(z.object({ id: z.string() }))
|
|
.mutation(async ({ ctx, input }) => {
|
|
await markNotificationAsRead(input.id, ctx.user.id)
|
|
return { success: true }
|
|
}),
|
|
|
|
/**
|
|
* Mark all notifications as read for the current user
|
|
*/
|
|
markAllAsRead: protectedProcedure.mutation(async ({ ctx }) => {
|
|
await markAllNotificationsAsRead(ctx.user.id)
|
|
return { success: true }
|
|
}),
|
|
|
|
/**
|
|
* Delete a notification (user can only delete their own)
|
|
*/
|
|
delete: protectedProcedure
|
|
.input(z.object({ id: z.string() }))
|
|
.mutation(async ({ ctx, input }) => {
|
|
await ctx.prisma.inAppNotification.deleteMany({
|
|
where: {
|
|
id: input.id,
|
|
userId: ctx.user.id, // Ensure user can only delete their own
|
|
},
|
|
})
|
|
return { success: true }
|
|
}),
|
|
|
|
/**
|
|
* Get notification email settings (admin only)
|
|
*/
|
|
getEmailSettings: adminProcedure.query(async ({ ctx }) => {
|
|
return ctx.prisma.notificationEmailSetting.findMany({
|
|
orderBy: [{ category: 'asc' }, { label: 'asc' }],
|
|
include: {
|
|
updatedBy: { select: { name: true, email: true } },
|
|
},
|
|
})
|
|
}),
|
|
|
|
/**
|
|
* Update a notification email setting (admin only)
|
|
*/
|
|
updateEmailSetting: adminProcedure
|
|
.input(
|
|
z.object({
|
|
notificationType: z.string(),
|
|
sendEmail: z.boolean(),
|
|
emailSubject: z.string().optional(),
|
|
emailTemplate: z.string().optional(),
|
|
})
|
|
)
|
|
.mutation(async ({ ctx, input }) => {
|
|
const { notificationType, sendEmail, emailSubject, emailTemplate } = input
|
|
|
|
return ctx.prisma.notificationEmailSetting.upsert({
|
|
where: { notificationType },
|
|
update: {
|
|
sendEmail,
|
|
emailSubject,
|
|
emailTemplate,
|
|
updatedById: ctx.user.id,
|
|
},
|
|
create: {
|
|
notificationType,
|
|
category: 'custom',
|
|
label: notificationType,
|
|
sendEmail,
|
|
emailSubject,
|
|
emailTemplate,
|
|
updatedById: ctx.user.id,
|
|
},
|
|
})
|
|
}),
|
|
|
|
/**
|
|
* Delete expired notifications (admin cleanup)
|
|
*/
|
|
deleteExpired: adminProcedure.mutation(async () => {
|
|
const count = await deleteExpiredNotifications()
|
|
return { deletedCount: count }
|
|
}),
|
|
|
|
/**
|
|
* Delete old read notifications (admin cleanup)
|
|
*/
|
|
deleteOld: adminProcedure
|
|
.input(z.object({ olderThanDays: z.number().int().min(1).max(365).default(30) }))
|
|
.mutation(async ({ input }) => {
|
|
const count = await deleteOldNotifications(input.olderThanDays)
|
|
return { deletedCount: count }
|
|
}),
|
|
|
|
/**
|
|
* Get notification icon and priority mappings (for UI)
|
|
*/
|
|
getMappings: protectedProcedure.query(() => {
|
|
return {
|
|
icons: NotificationIcons,
|
|
priorities: NotificationPriorities,
|
|
}
|
|
}),
|
|
|
|
/**
|
|
* Admin: Get notification statistics
|
|
*/
|
|
getStats: adminProcedure.query(async ({ ctx }) => {
|
|
const [total, unread, byType, byPriority] = await Promise.all([
|
|
ctx.prisma.inAppNotification.count(),
|
|
ctx.prisma.inAppNotification.count({ where: { isRead: false } }),
|
|
ctx.prisma.inAppNotification.groupBy({
|
|
by: ['type'],
|
|
_count: true,
|
|
orderBy: { _count: { type: 'desc' } },
|
|
take: 10,
|
|
}),
|
|
ctx.prisma.inAppNotification.groupBy({
|
|
by: ['priority'],
|
|
_count: true,
|
|
}),
|
|
])
|
|
|
|
return {
|
|
total,
|
|
unread,
|
|
readRate: total > 0 ? ((total - unread) / total) * 100 : 0,
|
|
byType: byType.map((t) => ({ type: t.type, count: t._count })),
|
|
byPriority: byPriority.map((p) => ({ priority: p.priority, count: p._count })),
|
|
}
|
|
}),
|
|
})
|