- number-поля теперь рендерятся как text + inputMode=numeric, чтобы браузер не округлял значения через input type=number - пробелы при вставке в number-поля удаляются - бэкенд нормализует значения number-полей в строку перед сохранением - добавлен хелпер normalizeFieldValueForStorage Closes: искажение расчётного счёта и других длинных числовых полей
193 lines
7.0 KiB
TypeScript
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,
|
|
})));
|
|
}
|