Files
iistwin/server/routes/messenger.helpers.ts
Ильяс Султанов 1f5ecb6da4 fix(number-fields): избегаем потери точности длинных чисел
- number-поля теперь рендерятся как text + inputMode=numeric,
  чтобы браузер не округлял значения через input type=number
- пробелы при вставке в number-поля удаляются
- бэкенд нормализует значения number-полей в строку перед сохранением
- добавлен хелпер normalizeFieldValueForStorage

Closes: искажение расчётного счёта и других длинных числовых полей
2026-07-07 21:03:40 +03:00

193 lines
7.0 KiB
TypeScript

import { db } from "../db";
import { conversations, conversationMembers, conversationMessages, users, bots } from "@shared/schema";
import { eq, and, desc, sql } from "drizzle-orm";
import { formatUserName } from "../utils/formatUserName";
export async function isMember(conversationId: number, userId: number): Promise<boolean> {
const [row] = await db
.select({ id: conversationMembers.id })
.from(conversationMembers)
.where(and(
eq(conversationMembers.conversationId, conversationId),
eq(conversationMembers.userId, userId),
));
return !!row;
}
export async function getConversationWithMeta(convId: number, requestingUserId: number) {
const [conv] = await db
.select()
.from(conversations)
.where(eq(conversations.id, convId));
if (!conv) return null;
const members = await db
.select({
id: users.id,
firstName: users.firstName,
middleName: users.middleName,
lastName: users.lastName,
position: users.position,
lastReadAt: conversationMembers.lastReadAt,
externalOrgId: conversationMembers.externalOrgId,
organizationId: users.organizationId,
})
.from(conversationMembers)
.innerJoin(users, eq(conversationMembers.userId, users.id))
.where(eq(conversationMembers.conversationId, convId));
const member = await db
.select({ lastReadAt: conversationMembers.lastReadAt, reactionUnreadCount: conversationMembers.reactionUnreadCount, mutedAt: conversationMembers.mutedAt })
.from(conversationMembers)
.where(and(
eq(conversationMembers.conversationId, convId),
eq(conversationMembers.userId, requestingUserId),
));
const lastReadAt = member[0]?.lastReadAt ?? null;
const reactionUnreadCount = member[0]?.reactionUnreadCount ?? 0;
const isMuted = member[0]?.mutedAt !== null && member[0]?.mutedAt !== undefined;
let unreadCount = 0;
if (lastReadAt) {
const [row] = await db
.select({ count: sql<number>`count(*)::int` })
.from(conversationMessages)
.where(and(
eq(conversationMessages.conversationId, convId),
eq(conversationMessages.isDeleted, false),
sql`${conversationMessages.createdAt} > ${lastReadAt}`,
sql`(${conversationMessages.authorId} IS NULL OR ${conversationMessages.authorId} != ${requestingUserId})`,
));
unreadCount = (row?.count ?? 0) + reactionUnreadCount;
} else {
const [row] = await db
.select({ count: sql<number>`count(*)::int` })
.from(conversationMessages)
.where(and(
eq(conversationMessages.conversationId, convId),
eq(conversationMessages.isDeleted, false),
sql`(${conversationMessages.authorId} IS NULL OR ${conversationMessages.authorId} != ${requestingUserId})`,
));
unreadCount = (row?.count ?? 0) + reactionUnreadCount;
}
const [lastMsg] = await db
.select({
id: conversationMessages.id,
message: conversationMessages.message,
createdAt: conversationMessages.createdAt,
authorId: conversationMessages.authorId,
isDeleted: conversationMessages.isDeleted,
})
.from(conversationMessages)
.where(eq(conversationMessages.conversationId, convId))
.orderBy(desc(conversationMessages.createdAt))
.limit(1);
let displayName = conv.name ?? null;
if (conv.type === "direct" && !displayName) {
const other = members.find(m => m.id !== requestingUserId);
displayName = other ? formatUserName(other) : "Диалог";
} else if (conv.type === "bot_direct" && conv.botId) {
const [bot] = await db
.select({ name: bots.name })
.from(bots)
.where(eq(bots.id, conv.botId));
displayName = bot?.name ?? "Бот";
}
const isExternal = members.some(m => m.externalOrgId !== null && m.externalOrgId !== undefined);
// Load bot info for bot_direct conversations
let botInfo: { id: number; name: string; avatarUrl: string | null } | null = null;
if (conv.type === "bot_direct" && conv.botId) {
const [bot] = await db
.select({ id: bots.id, name: bots.name, avatarUrl: bots.avatarUrl })
.from(bots)
.where(eq(bots.id, conv.botId));
botInfo = bot ?? null;
}
return { ...conv, displayName, members, unreadCount, lastMessage: lastMsg ?? null, isExternal, isMuted, bot: botInfo };
}
export async function enrichMessage(msgId: number) {
const [msg] = await db
.select({
id: conversationMessages.id,
conversationId: conversationMessages.conversationId,
message: conversationMessages.message,
replyToId: conversationMessages.replyToId,
mentionedUserIds: conversationMessages.mentionedUserIds,
attachments: conversationMessages.attachments,
createdAt: conversationMessages.createdAt,
updatedAt: conversationMessages.updatedAt,
isDeleted: conversationMessages.isDeleted,
authorId: conversationMessages.authorId,
botId: conversationMessages.botId,
authorFirstName: users.firstName,
authorMiddleName: users.middleName,
authorLastName: users.lastName,
})
.from(conversationMessages)
.leftJoin(users, eq(conversationMessages.authorId, users.id))
.where(eq(conversationMessages.id, msgId));
if (!msg) return null;
let replyTo: { id: number; message: string; author: { id: number; firstName: string; middleName: string; lastName: string } } | null = null;
if (msg.replyToId) {
const [r] = await db
.select({
id: conversationMessages.id,
message: conversationMessages.message,
authorId: conversationMessages.authorId,
authorFirstName: users.firstName,
authorMiddleName: users.middleName,
authorLastName: users.lastName,
})
.from(conversationMessages)
.leftJoin(users, eq(conversationMessages.authorId, users.id))
.where(and(
eq(conversationMessages.id, msg.replyToId),
eq(conversationMessages.conversationId, msg.conversationId),
));
if (r && r.authorId) {
replyTo = {
id: r.id,
message: r.message,
author: { id: r.authorId, firstName: r.authorFirstName ?? '', middleName: r.authorMiddleName ?? '', lastName: r.authorLastName ?? '' },
};
}
}
// Load bot info if this is a bot message
let botInfo: { id: number; name: string; avatarUrl: string | null } | null = null;
if (msg.botId) {
const [bot] = await db
.select({ id: bots.id, name: bots.name, avatarUrl: bots.avatarUrl })
.from(bots)
.where(eq(bots.id, msg.botId));
botInfo = bot ?? null;
}
return {
...msg,
author: msg.authorId
? { id: msg.authorId, firstName: msg.authorFirstName ?? '', middleName: msg.authorMiddleName ?? '', lastName: msg.authorLastName ?? '' }
: null,
bot: botInfo,
replyTo,
};
}
export async function getMembersForSSE(convId: number, conversationOrgId: number) {
return db
.select({ userId: conversationMembers.userId, externalOrgId: conversationMembers.externalOrgId })
.from(conversationMembers)
.where(eq(conversationMembers.conversationId, convId))
.then(rows => rows.map(r => ({
userId: r.userId,
orgId: r.externalOrgId ?? conversationOrgId,
})));
}