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 { 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 { 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 { 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 { 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`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, userId: number): Promise { 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 { 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 { 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 { 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 { 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 { 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`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, userId: number): Promise { 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 { 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 { 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(); 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 { 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 { 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 { 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(); 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 { 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 { const [notification] = await db .insert(userNotifications) .values(insertNotification) .returning(); return notification; } async markNotificationAsRead(id: number, userId: number, organizationId: number): Promise { 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 { 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 { 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 { 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 { // Поддержка похожих символов: і/и для корректного поиска 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 // ===================================================== }