stadtwerke/innungsapp/apps/admin/server/routers/messages.ts

185 lines
6.2 KiB
TypeScript

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