/** * 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 })), } }), })