import { z } from 'zod' import { router, memberProcedure } from '../trpc' import { TRPCError } from '@trpc/server' export const messagesRouter = router({ // List all conversations for the current member getConversations: memberProcedure.query(async ({ ctx }) => { const member = await ctx.prisma.member.findFirst({ where: { userId: ctx.session.user.id, orgId: ctx.orgId }, }) if (!member) throw new TRPCError({ code: 'NOT_FOUND' }) const convMembers = await ctx.prisma.conversationMember.findMany({ where: { memberId: member.id }, include: { conversation: { include: { members: { include: { member: { select: { id: true, name: true, betrieb: true, avatarUrl: true } } }, }, messages: { orderBy: { createdAt: 'desc' }, take: 1, select: { body: true, createdAt: true, senderId: true }, }, }, }, }, orderBy: { conversation: { updatedAt: 'desc' } }, }) return convMembers.map((cm) => { const other = cm.conversation.members.find((m) => m.memberId !== member.id)?.member const lastMsg = cm.conversation.messages[0] ?? null const unread = lastMsg && (!cm.lastReadAt || lastMsg.createdAt > cm.lastReadAt) && lastMsg.senderId !== member.id return { conversationId: cm.conversationId, other, lastMessage: lastMsg, hasUnread: !!unread, updatedAt: cm.conversation.updatedAt, } }) }), // Get or create a 1-on-1 conversation between current member and another getOrCreate: memberProcedure .input(z.object({ otherMemberId: z.string() })) .mutation(async ({ ctx, input }) => { const member = await ctx.prisma.member.findFirst({ where: { userId: ctx.session.user.id, orgId: ctx.orgId }, }) if (!member) throw new TRPCError({ code: 'NOT_FOUND' }) const other = await ctx.prisma.member.findFirst({ where: { id: input.otherMemberId, orgId: ctx.orgId }, }) if (!other) throw new TRPCError({ code: 'NOT_FOUND', message: 'Mitglied nicht gefunden' }) // Find existing conversation between the two members in this org const existing = await ctx.prisma.conversation.findFirst({ where: { orgId: ctx.orgId, members: { every: { memberId: { in: [member.id, other.id] } } }, AND: [ { members: { some: { memberId: member.id } } }, { members: { some: { memberId: other.id } } }, ], }, }) if (existing) return { conversationId: existing.id } const conv = await ctx.prisma.conversation.create({ data: { orgId: ctx.orgId, members: { create: [{ memberId: member.id }, { memberId: other.id }], }, }, }) return { conversationId: conv.id } }), // Get messages for a conversation getMessages: memberProcedure .input(z.object({ conversationId: z.string(), cursor: z.string().optional() })) .query(async ({ ctx, input }) => { const member = await ctx.prisma.member.findFirst({ where: { userId: ctx.session.user.id, orgId: ctx.orgId }, }) if (!member) throw new TRPCError({ code: 'NOT_FOUND' }) // Verify membership in conversation const cm = await ctx.prisma.conversationMember.findUnique({ where: { conversationId_memberId: { conversationId: input.conversationId, memberId: member.id } }, }) if (!cm) throw new TRPCError({ code: 'FORBIDDEN' }) const messages = await ctx.prisma.message.findMany({ where: { conversationId: input.conversationId }, include: { sender: { select: { id: true, name: true, avatarUrl: true } } }, orderBy: { createdAt: 'desc' }, take: 40, ...(input.cursor ? { skip: 1, cursor: { id: input.cursor } } : {}), }) // Mark as read await ctx.prisma.conversationMember.update({ where: { conversationId_memberId: { conversationId: input.conversationId, memberId: member.id } }, data: { lastReadAt: new Date() }, }) return { messages: messages.reverse(), nextCursor: messages.length === 40 ? messages[0]?.id : undefined, } }), // Send a message sendMessage: memberProcedure .input(z.object({ conversationId: z.string(), body: z.string().min(1).max(2000) })) .mutation(async ({ ctx, input }) => { const member = await ctx.prisma.member.findFirst({ where: { userId: ctx.session.user.id, orgId: ctx.orgId }, }) if (!member) throw new TRPCError({ code: 'NOT_FOUND' }) const cm = await ctx.prisma.conversationMember.findUnique({ where: { conversationId_memberId: { conversationId: input.conversationId, memberId: member.id } }, }) if (!cm) throw new TRPCError({ code: 'FORBIDDEN' }) const message = await ctx.prisma.message.create({ data: { conversationId: input.conversationId, senderId: member.id, body: input.body.trim(), }, include: { sender: { select: { id: true, name: true, avatarUrl: true } } }, }) // Update conversation updatedAt so it sorts to top await ctx.prisma.conversation.update({ where: { id: input.conversationId }, data: { updatedAt: new Date() }, }) return message }), // Count total unread conversations unreadCount: memberProcedure.query(async ({ ctx }) => { const member = await ctx.prisma.member.findFirst({ where: { userId: ctx.session.user.id, orgId: ctx.orgId }, }) if (!member) return { count: 0 } const convMembers = await ctx.prisma.conversationMember.findMany({ where: { memberId: member.id }, include: { conversation: { include: { messages: { orderBy: { createdAt: 'desc' }, take: 1, select: { createdAt: true, senderId: true }, }, }, }, }, }) const count = convMembers.filter((cm) => { const last = cm.conversation.messages[0] return last && last.senderId !== member.id && (!cm.lastReadAt || last.createdAt > cm.lastReadAt) }).length return { count } }), })