import { db } from "../db"; import { messageReactions, polls, pollVotes, taskMessages, tasks, conversationMessages, conversationMembers, users, } from "@shared/schema"; import { eq, and, inArray } from "drizzle-orm"; import { eventBus } from "./shared"; // ── Types ────────────────────────────────────────────────────────────────────── export interface ReactionGroup { emoji: string; count: number; userIds: number[]; users: { id: number; firstName: string; middleName: string; lastName: string }[]; hasReacted: boolean; } export interface PollData { id: number; question: string; options: string[]; isMultiple: boolean; isClosed: boolean; createdBy: number; totalVotes: number; myVote: number[] | null; optionVoteCounts: number[]; optionVoters: { id: number; firstName: string; middleName: string; lastName: string }[][]; } export interface ReactionRow { id: number; taskMessageId: number | null; convMessageId: number | null; userId: number; emoji: string; firstName: string; middleName: string | null; lastName: string; } export interface PollVoteRow { userId: number; optionIndexes: number[]; firstName: string; middleName: string; lastName: string; } // ── Auth helpers ─────────────────────────────────────────────────────────────── export async function getTaskMsgContext(msgId: number, orgId: number): Promise<{ taskId: number; formId: number } | null> { const [row] = await db .select({ taskId: taskMessages.taskId, formId: taskMessages.formId }) .from(taskMessages) .innerJoin(tasks, eq(taskMessages.taskId, tasks.id)) .where(and(eq(taskMessages.id, msgId), eq(tasks.organizationId, orgId))); return row ?? null; } export async function getConvMsgContext( msgId: number, orgId: number, userId: number ): Promise<{ conversationId: number } | null> { const [row] = await db .select({ conversationId: conversationMessages.conversationId }) .from(conversationMessages) .where(eq(conversationMessages.id, msgId)); if (!row) return null; const [member] = await db .select({ id: conversationMembers.id }) .from(conversationMembers) .where(and( eq(conversationMembers.conversationId, row.conversationId), eq(conversationMembers.userId, userId), )); return member ? row : null; } // ── Reaction helpers ─────────────────────────────────────────────────────────── export async function buildReactionGroups( rows: ReactionRow[], msgIdFn: (r: ReactionRow) => number | null, currentUserId: number, ): Promise> { const byMsg: Record> = {}; for (const r of rows) { const msgId = msgIdFn(r); if (msgId == null) continue; if (!byMsg[msgId]) byMsg[msgId] = {}; if (!byMsg[msgId][r.emoji]) { byMsg[msgId][r.emoji] = { emoji: r.emoji, count: 0, userIds: [], users: [], hasReacted: false, }; } const grp = byMsg[msgId][r.emoji]; grp.count++; grp.userIds.push(r.userId); grp.users.push({ id: r.userId, firstName: r.firstName, middleName: r.middleName ?? '', lastName: r.lastName }); if (r.userId === currentUserId) grp.hasReacted = true; } const result: Record = {}; for (const [msgId, emojiMap] of Object.entries(byMsg)) { result[Number(msgId)] = Object.values(emojiMap); } return result; } export async function getReactionsForTaskMsgs( taskMsgIds: number[], currentUserId: number, ): Promise> { if (!taskMsgIds.length) return {}; const rows = await db .select({ id: messageReactions.id, taskMessageId: messageReactions.taskMessageId, convMessageId: messageReactions.convMessageId, userId: messageReactions.userId, emoji: messageReactions.emoji, firstName: users.firstName, middleName: users.middleName, lastName: users.lastName, }) .from(messageReactions) .innerJoin(users, eq(messageReactions.userId, users.id)) .where(inArray(messageReactions.taskMessageId, taskMsgIds)); return buildReactionGroups(rows, r => r.taskMessageId, currentUserId); } export async function getReactionsForConvMsgs( convMsgIds: number[], currentUserId: number, ): Promise> { if (!convMsgIds.length) return {}; const rows = await db .select({ id: messageReactions.id, taskMessageId: messageReactions.taskMessageId, convMessageId: messageReactions.convMessageId, userId: messageReactions.userId, emoji: messageReactions.emoji, firstName: users.firstName, middleName: users.middleName, lastName: users.lastName, }) .from(messageReactions) .innerJoin(users, eq(messageReactions.userId, users.id)) .where(inArray(messageReactions.convMessageId, convMsgIds)); return buildReactionGroups(rows, r => r.convMessageId, currentUserId); } export async function getReactionGroupsForMsg( taskMessageId: number | null, convMessageId: number | null, currentUserId: number, ): Promise { const condition = taskMessageId != null ? eq(messageReactions.taskMessageId, taskMessageId) : eq(messageReactions.convMessageId, convMessageId!); const rows = await db .select({ id: messageReactions.id, taskMessageId: messageReactions.taskMessageId, convMessageId: messageReactions.convMessageId, userId: messageReactions.userId, emoji: messageReactions.emoji, firstName: users.firstName, middleName: users.middleName, lastName: users.lastName, }) .from(messageReactions) .innerJoin(users, eq(messageReactions.userId, users.id)) .where(condition); const map: Record = {}; for (const r of rows) { if (!map[r.emoji]) map[r.emoji] = { emoji: r.emoji, count: 0, userIds: [], users: [], hasReacted: false }; const g = map[r.emoji]; g.count++; g.userIds.push(r.userId); g.users.push({ id: r.userId, firstName: r.firstName, middleName: r.middleName ?? '', lastName: r.lastName }); if (r.userId === currentUserId) g.hasReacted = true; } return Object.values(map); } // ── Poll helpers ─────────────────────────────────────────────────────────────── export async function getPollDataById(pollId: number, currentUserId: number): Promise { const [p] = await db.select().from(polls).where(eq(polls.id, pollId)); if (!p) return null; const voteRows = await db .select({ userId: pollVotes.userId, optionIndexes: pollVotes.optionIndexes, firstName: users.firstName, middleName: users.middleName, lastName: users.lastName, }) .from(pollVotes) .innerJoin(users, eq(pollVotes.userId, users.id)) .where(eq(pollVotes.pollId, pollId)); const optCount = (p.options as string[]).length; const voteCounts = new Array(optCount).fill(0); const voters: { id: number; firstName: string; middleName: string; lastName: string }[][] = Array.from({ length: optCount }, () => []); let myVote: number[] | null = null; for (const v of voteRows) { if (v.userId === currentUserId) myVote = v.optionIndexes; for (const idx of v.optionIndexes) { if (idx >= 0 && idx < optCount) { voteCounts[idx]++; voters[idx].push({ id: v.userId, firstName: v.firstName, middleName: v.middleName ?? '', lastName: v.lastName }); } } } return { id: p.id, question: p.question, options: p.options as string[], isMultiple: p.isMultiple ?? false, isClosed: p.isClosed ?? false, createdBy: p.createdBy, totalVotes: new Set(voteRows.map(v => v.userId)).size, myVote, optionVoteCounts: voteCounts, optionVoters: voters, }; } // ── SSE emitters ─────────────────────────────────────────────────────────────── export function emitTaskReaction(orgId: number, data: object) { eventBus.publishEvent({ type: 'reaction_updated', data, organizationId: orgId }); } export function emitTaskPoll(orgId: number, data: object) { eventBus.publishEvent({ type: 'poll_updated', data, organizationId: orgId }); } export async function emitConvReaction(convId: number, orgId: number, data: object) { const members = await db .select({ userId: conversationMembers.userId }) .from(conversationMembers) .where(eq(conversationMembers.conversationId, convId)); for (const m of members) { eventBus.publishEvent({ type: 'reaction_updated', data, organizationId: orgId, userId: m.userId }); } } export async function emitConvPoll(convId: number, orgId: number, data: object) { const members = await db .select({ userId: conversationMembers.userId }) .from(conversationMembers) .where(eq(conversationMembers.conversationId, convId)); for (const m of members) { eventBus.publishEvent({ type: 'poll_updated', data, organizationId: orgId, userId: m.userId }); } }