- number-поля теперь рендерятся как text + inputMode=numeric, чтобы браузер не округлял значения через input type=number - пробелы при вставке в number-поля удаляются - бэкенд нормализует значения number-полей в строку перед сохранением - добавлен хелпер normalizeFieldValueForStorage Closes: искажение расчётного счёта и других длинных числовых полей
385 lines
15 KiB
TypeScript
385 lines
15 KiB
TypeScript
import { users, userNotifications, bookmarkFolders, bookmarks, type User, type UserNotification, type BookmarkFolder, type Bookmark, type InsertUserNotification, type InsertBookmarkFolder, type InsertBookmark } from "@shared/schema";
|
||
import { db } from "../db";
|
||
import { eq, and, desc, isNull, sql } from "drizzle-orm";
|
||
import crypto from "crypto";
|
||
import { TasksStorage } from "./tasks.storage";
|
||
export class SocialStorage extends TasksStorage {
|
||
// Bookmark Folders
|
||
async getBookmarkFolder(id: number, organizationId: number, userId: number): Promise<BookmarkFolder | undefined> {
|
||
const [folder] = await db
|
||
.select()
|
||
.from(bookmarkFolders)
|
||
.where(and(eq(bookmarkFolders.id, id), eq(bookmarkFolders.organizationId, organizationId), eq(bookmarkFolders.userId, userId)));
|
||
return folder || undefined;
|
||
}
|
||
|
||
async getBookmarkFoldersByOrganization(organizationId: number, userId: number): Promise<BookmarkFolder[]> {
|
||
return await db
|
||
.select()
|
||
.from(bookmarkFolders)
|
||
.where(and(eq(bookmarkFolders.organizationId, organizationId), eq(bookmarkFolders.userId, userId)))
|
||
.orderBy(bookmarkFolders.position);
|
||
}
|
||
|
||
async getBookmarkFoldersByParent(parentId: number | null, organizationId: number, userId: number): Promise<BookmarkFolder[]> {
|
||
const whereCondition = parentId
|
||
? and(eq(bookmarkFolders.parentId, parentId), eq(bookmarkFolders.organizationId, organizationId), eq(bookmarkFolders.userId, userId))
|
||
: and(isNull(bookmarkFolders.parentId), eq(bookmarkFolders.organizationId, organizationId), eq(bookmarkFolders.userId, userId));
|
||
|
||
return await db
|
||
.select()
|
||
.from(bookmarkFolders)
|
||
.where(whereCondition)
|
||
.orderBy(bookmarkFolders.position);
|
||
}
|
||
|
||
async createBookmarkFolder(insertFolder: InsertBookmarkFolder): Promise<BookmarkFolder> {
|
||
const whereCondition = insertFolder.parentId != null
|
||
? and(eq(bookmarkFolders.parentId, insertFolder.parentId), eq(bookmarkFolders.organizationId, insertFolder.organizationId), eq(bookmarkFolders.userId, insertFolder.userId))
|
||
: and(isNull(bookmarkFolders.parentId), eq(bookmarkFolders.organizationId, insertFolder.organizationId), eq(bookmarkFolders.userId, insertFolder.userId));
|
||
|
||
const [maxRow] = await db
|
||
.select({ maxPos: sql<number>`coalesce(max(${bookmarkFolders.position}), -1)` })
|
||
.from(bookmarkFolders)
|
||
.where(whereCondition);
|
||
|
||
const nextPosition = (maxRow?.maxPos ?? -1) + 1;
|
||
|
||
const [folder] = await db
|
||
.insert(bookmarkFolders)
|
||
.values({ ...insertFolder, position: nextPosition })
|
||
.returning();
|
||
return folder;
|
||
}
|
||
|
||
async updateBookmarkFolder(id: number, organizationId: number, updates: Partial<BookmarkFolder>, userId: number): Promise<BookmarkFolder> {
|
||
const [folder] = await db
|
||
.update(bookmarkFolders)
|
||
.set({ ...updates, updatedAt: new Date() })
|
||
.where(and(eq(bookmarkFolders.id, id), eq(bookmarkFolders.organizationId, organizationId), eq(bookmarkFolders.userId, userId)))
|
||
.returning();
|
||
return folder;
|
||
}
|
||
|
||
async deleteBookmarkFolder(id: number, organizationId: number, userId: number): Promise<void> {
|
||
await db.transaction(async (tx) => {
|
||
const deleted = await tx
|
||
.delete(bookmarkFolders)
|
||
.where(and(eq(bookmarkFolders.id, id), eq(bookmarkFolders.organizationId, organizationId), eq(bookmarkFolders.userId, userId)))
|
||
.returning({ parentId: bookmarkFolders.parentId });
|
||
|
||
if (deleted.length === 0) return;
|
||
|
||
const parentId = deleted[0].parentId;
|
||
const whereCondition = parentId != null
|
||
? and(eq(bookmarkFolders.parentId, parentId), eq(bookmarkFolders.organizationId, organizationId), eq(bookmarkFolders.userId, userId))
|
||
: and(isNull(bookmarkFolders.parentId), eq(bookmarkFolders.organizationId, organizationId), eq(bookmarkFolders.userId, userId));
|
||
|
||
const remaining = await tx
|
||
.select({ id: bookmarkFolders.id, position: bookmarkFolders.position })
|
||
.from(bookmarkFolders)
|
||
.where(whereCondition)
|
||
.orderBy(bookmarkFolders.position, bookmarkFolders.id);
|
||
|
||
for (let idx = 0; idx < remaining.length; idx++) {
|
||
if (remaining[idx].position !== idx) {
|
||
await tx
|
||
.update(bookmarkFolders)
|
||
.set({ position: idx, updatedAt: new Date() })
|
||
.where(and(eq(bookmarkFolders.id, remaining[idx].id), eq(bookmarkFolders.organizationId, organizationId), eq(bookmarkFolders.userId, userId)));
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// Bookmarks
|
||
async getBookmark(id: number, organizationId: number, userId: number): Promise<Bookmark | undefined> {
|
||
const [bookmark] = await db
|
||
.select()
|
||
.from(bookmarks)
|
||
.where(and(eq(bookmarks.id, id), eq(bookmarks.organizationId, organizationId), eq(bookmarks.userId, userId)));
|
||
return bookmark || undefined;
|
||
}
|
||
|
||
async getBookmarksByFolder(folderId: number | null, organizationId: number, userId: number): Promise<Bookmark[]> {
|
||
const whereCondition = folderId
|
||
? and(eq(bookmarks.folderId, folderId), eq(bookmarks.organizationId, organizationId), eq(bookmarks.userId, userId))
|
||
: and(isNull(bookmarks.folderId), eq(bookmarks.organizationId, organizationId), eq(bookmarks.userId, userId));
|
||
|
||
return await db
|
||
.select()
|
||
.from(bookmarks)
|
||
.where(whereCondition)
|
||
.orderBy(bookmarks.position, bookmarks.id);
|
||
}
|
||
|
||
async getBookmarksByOrganization(organizationId: number, userId: number): Promise<Bookmark[]> {
|
||
return await db
|
||
.select()
|
||
.from(bookmarks)
|
||
.where(and(eq(bookmarks.organizationId, organizationId), eq(bookmarks.userId, userId)))
|
||
.orderBy(bookmarks.position, bookmarks.id);
|
||
}
|
||
|
||
async createBookmark(insertBookmark: InsertBookmark): Promise<Bookmark> {
|
||
const whereCondition = insertBookmark.folderId != null
|
||
? and(eq(bookmarks.folderId, insertBookmark.folderId), eq(bookmarks.organizationId, insertBookmark.organizationId), eq(bookmarks.userId, insertBookmark.userId))
|
||
: and(isNull(bookmarks.folderId), eq(bookmarks.organizationId, insertBookmark.organizationId), eq(bookmarks.userId, insertBookmark.userId));
|
||
|
||
const [maxRow] = await db
|
||
.select({ maxPos: sql<number>`coalesce(max(${bookmarks.position}), -1)` })
|
||
.from(bookmarks)
|
||
.where(whereCondition);
|
||
|
||
const nextPosition = (maxRow?.maxPos ?? -1) + 1;
|
||
|
||
const [bookmark] = await db
|
||
.insert(bookmarks)
|
||
.values({ ...insertBookmark, position: nextPosition })
|
||
.returning();
|
||
return bookmark;
|
||
}
|
||
|
||
async updateBookmark(id: number, organizationId: number, updates: Partial<Bookmark>, userId: number): Promise<Bookmark> {
|
||
const [bookmark] = await db
|
||
.update(bookmarks)
|
||
.set({ ...updates, updatedAt: new Date() })
|
||
.where(and(eq(bookmarks.id, id), eq(bookmarks.organizationId, organizationId), eq(bookmarks.userId, userId)))
|
||
.returning();
|
||
return bookmark;
|
||
}
|
||
|
||
async deleteBookmark(id: number, organizationId: number, userId: number): Promise<void> {
|
||
await db.transaction(async (tx) => {
|
||
const deleted = await tx
|
||
.delete(bookmarks)
|
||
.where(and(eq(bookmarks.id, id), eq(bookmarks.organizationId, organizationId), eq(bookmarks.userId, userId)))
|
||
.returning({ folderId: bookmarks.folderId });
|
||
|
||
if (deleted.length === 0) return;
|
||
|
||
const folderId = deleted[0].folderId;
|
||
const whereCondition = folderId != null
|
||
? and(eq(bookmarks.folderId, folderId), eq(bookmarks.organizationId, organizationId), eq(bookmarks.userId, userId))
|
||
: and(isNull(bookmarks.folderId), eq(bookmarks.organizationId, organizationId), eq(bookmarks.userId, userId));
|
||
|
||
const remaining = await tx
|
||
.select({ id: bookmarks.id, position: bookmarks.position })
|
||
.from(bookmarks)
|
||
.where(whereCondition)
|
||
.orderBy(bookmarks.position, bookmarks.id);
|
||
|
||
for (let idx = 0; idx < remaining.length; idx++) {
|
||
if (remaining[idx].position !== idx) {
|
||
await tx
|
||
.update(bookmarks)
|
||
.set({ position: idx, updatedAt: new Date() })
|
||
.where(and(eq(bookmarks.id, remaining[idx].id), eq(bookmarks.organizationId, organizationId), eq(bookmarks.userId, userId)));
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
async normalizeBookmarkPositions(organizationId: number, userId: number): Promise<void> {
|
||
const allBookmarks = await db
|
||
.select()
|
||
.from(bookmarks)
|
||
.where(and(eq(bookmarks.organizationId, organizationId), eq(bookmarks.userId, userId)))
|
||
.orderBy(bookmarks.folderId, bookmarks.position, bookmarks.id);
|
||
|
||
const groups = new Map<string, typeof allBookmarks>();
|
||
for (const bm of allBookmarks) {
|
||
const key = bm.folderId == null ? 'null' : String(bm.folderId);
|
||
if (!groups.has(key)) groups.set(key, []);
|
||
groups.get(key)!.push(bm);
|
||
}
|
||
|
||
const updates: { id: number; position: number }[] = [];
|
||
for (const group of groups.values()) {
|
||
group.forEach((bm, idx) => {
|
||
if (bm.position !== idx) {
|
||
updates.push({ id: bm.id, position: idx });
|
||
}
|
||
});
|
||
}
|
||
|
||
if (updates.length > 0) {
|
||
await this.batchUpdateBookmarkPositions(updates, organizationId, userId);
|
||
}
|
||
}
|
||
|
||
async batchUpdateBookmarkPositions(
|
||
items: { id: number; position: number }[],
|
||
organizationId: number,
|
||
userId: number
|
||
): Promise<void> {
|
||
await db.transaction(async (tx) => {
|
||
for (const item of items) {
|
||
await tx
|
||
.update(bookmarks)
|
||
.set({ position: item.position, updatedAt: new Date() })
|
||
.where(and(eq(bookmarks.id, item.id), eq(bookmarks.organizationId, organizationId), eq(bookmarks.userId, userId)));
|
||
}
|
||
});
|
||
}
|
||
|
||
async batchUpdateBookmarkFolderPositions(
|
||
items: { id: number; position: number }[],
|
||
organizationId: number,
|
||
userId: number
|
||
): Promise<void> {
|
||
await db.transaction(async (tx) => {
|
||
for (const item of items) {
|
||
await tx
|
||
.update(bookmarkFolders)
|
||
.set({ position: item.position, updatedAt: new Date() })
|
||
.where(and(eq(bookmarkFolders.id, item.id), eq(bookmarkFolders.organizationId, organizationId), eq(bookmarkFolders.userId, userId)));
|
||
}
|
||
});
|
||
}
|
||
|
||
async normalizeBookmarkFolderPositions(organizationId: number, userId: number): Promise<void> {
|
||
const allFolders = await db
|
||
.select()
|
||
.from(bookmarkFolders)
|
||
.where(and(eq(bookmarkFolders.organizationId, organizationId), eq(bookmarkFolders.userId, userId)))
|
||
.orderBy(bookmarkFolders.parentId, bookmarkFolders.position, bookmarkFolders.id);
|
||
|
||
const groups = new Map<string, typeof allFolders>();
|
||
for (const folder of allFolders) {
|
||
const key = folder.parentId == null ? 'null' : String(folder.parentId);
|
||
if (!groups.has(key)) groups.set(key, []);
|
||
groups.get(key)!.push(folder);
|
||
}
|
||
|
||
const updates: { id: number; position: number }[] = [];
|
||
for (const group of groups.values()) {
|
||
group.forEach((folder, idx) => {
|
||
if (folder.position !== idx) {
|
||
updates.push({ id: folder.id, position: idx });
|
||
}
|
||
});
|
||
}
|
||
|
||
if (updates.length > 0) {
|
||
await this.batchUpdateBookmarkFolderPositions(updates, organizationId, userId);
|
||
}
|
||
}
|
||
|
||
// User Notifications
|
||
async getUserNotifications(userId: number, organizationId: number, isRead?: boolean): Promise<UserNotification[]> {
|
||
const conditions = [
|
||
eq(userNotifications.userId, userId),
|
||
eq(userNotifications.organizationId, organizationId)
|
||
];
|
||
|
||
if (isRead !== undefined) {
|
||
conditions.push(eq(userNotifications.isRead, isRead));
|
||
}
|
||
|
||
const result = await db.query.userNotifications.findMany({
|
||
where: and(...conditions),
|
||
orderBy: [desc(userNotifications.createdAt)],
|
||
with: {
|
||
task: true,
|
||
taskMessage: {
|
||
with: {
|
||
author: true
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
return result as UserNotification[];
|
||
}
|
||
|
||
async createUserNotification(insertNotification: InsertUserNotification): Promise<UserNotification> {
|
||
const [notification] = await db
|
||
.insert(userNotifications)
|
||
.values(insertNotification)
|
||
.returning();
|
||
return notification;
|
||
}
|
||
|
||
async markNotificationAsRead(id: number, userId: number, organizationId: number): Promise<UserNotification> {
|
||
const [notification] = await db
|
||
.update(userNotifications)
|
||
.set({ isRead: true })
|
||
.where(and(
|
||
eq(userNotifications.id, id),
|
||
eq(userNotifications.userId, userId),
|
||
eq(userNotifications.organizationId, organizationId)
|
||
))
|
||
.returning();
|
||
return notification;
|
||
}
|
||
|
||
async markAllNotificationsAsRead(userId: number, organizationId: number): Promise<void> {
|
||
await db
|
||
.update(userNotifications)
|
||
.set({ isRead: true })
|
||
.where(and(
|
||
eq(userNotifications.userId, userId),
|
||
eq(userNotifications.organizationId, organizationId)
|
||
));
|
||
}
|
||
|
||
async markTaskNotificationsAsRead(taskId: number, userId: number, organizationId: number): Promise<void> {
|
||
await db
|
||
.update(userNotifications)
|
||
.set({ isRead: true })
|
||
.where(and(
|
||
eq(userNotifications.taskId, taskId),
|
||
eq(userNotifications.userId, userId),
|
||
eq(userNotifications.organizationId, organizationId),
|
||
eq(userNotifications.isRead, false)
|
||
));
|
||
}
|
||
|
||
async deleteNotification(id: number, userId: number, organizationId: number): Promise<void> {
|
||
await db
|
||
.delete(userNotifications)
|
||
.where(and(
|
||
eq(userNotifications.id, id),
|
||
eq(userNotifications.userId, userId),
|
||
eq(userNotifications.organizationId, organizationId)
|
||
));
|
||
}
|
||
|
||
// User Search
|
||
async searchUsers(query: string, organizationId: number, limit = 10): Promise<User[]> {
|
||
// Поддержка похожих символов: і/и для корректного поиска
|
||
const normalizedQuery = query
|
||
.replace(/і/g, 'и')
|
||
.replace(/ї/g, 'и')
|
||
.trim();
|
||
|
||
const result = await db
|
||
.select()
|
||
.from(users)
|
||
.where(and(
|
||
eq(users.organizationId, organizationId),
|
||
sql`(
|
||
${users.firstName} || ' ' || ${users.middleName} || ' ' || ${users.lastName} ILIKE ${'%' + query + '%'}
|
||
OR
|
||
${users.firstName} || ' ' || ${users.middleName} || ' ' || ${users.lastName} ILIKE ${'%' + normalizedQuery + '%'}
|
||
OR
|
||
${users.firstName} ILIKE ${'%' + query + '%'}
|
||
OR
|
||
${users.middleName} ILIKE ${'%' + query + '%'}
|
||
OR
|
||
${users.lastName} ILIKE ${'%' + query + '%'}
|
||
)`
|
||
))
|
||
.limit(limit)
|
||
.orderBy(users.firstName, users.middleName, users.lastName);
|
||
|
||
return result;
|
||
}
|
||
|
||
// =====================================================
|
||
// BOTS
|
||
// =====================================================
|
||
|
||
}
|