arcadia-suite-sv/server/productivity/routes.ts

685 lines
21 KiB
TypeScript

import { Router } from "express";
import { db } from "../../db";
import {
workspacePages, pageBlocks, pageLinks, dashboardWidgets,
quickNotes, activityFeed, userFavorites, commandHistory,
insertWorkspacePageSchema, insertPageBlockSchema, insertDashboardWidgetSchema,
insertQuickNoteSchema, insertActivityFeedSchema, insertUserFavoriteSchema,
pcClients, pcProjects, pcTasks, conversations, knowledgeBase
} from "@shared/schema";
import { eq, desc, and, or, ilike, sql } from "drizzle-orm";
const router = Router();
const requireAuth = (req: any, res: any, next: any) => {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
next();
};
// ========== UNIVERSAL SEARCH ==========
router.get("/search", requireAuth, async (req, res) => {
try {
const { q, modules, limit = 20 } = req.query;
const userId = req.user!.id;
const searchQuery = `%${q}%`;
const limitNum = Math.min(parseInt(limit as string) || 20, 50);
const results: any = { pages: [], clients: [], projects: [], tasks: [], conversations: [], knowledge: [] };
const moduleFilter = modules ? (modules as string).split(',') : ['all'];
const searchAll = moduleFilter.includes('all');
if (searchAll || moduleFilter.includes('pages')) {
const pages = await db.select()
.from(workspacePages)
.where(and(
eq(workspacePages.userId, userId),
eq(workspacePages.isArchived, 0),
ilike(workspacePages.title, searchQuery)
))
.limit(limitNum);
results.pages = pages.map(p => ({ ...p, _type: 'page', _module: 'workspace' }));
}
if (searchAll || moduleFilter.includes('clients')) {
const clients = await db.select()
.from(pcClients)
.where(or(
ilike(pcClients.name, searchQuery),
ilike(pcClients.email, searchQuery),
ilike(pcClients.phone, searchQuery)
))
.limit(limitNum);
results.clients = clients.map(c => ({ ...c, _type: 'client', _module: 'compass' }));
}
if (searchAll || moduleFilter.includes('projects')) {
const projects = await db.select()
.from(pcProjects)
.where(or(
ilike(pcProjects.name, searchQuery),
ilike(pcProjects.description, searchQuery)
))
.limit(limitNum);
results.projects = projects.map(p => ({ ...p, _type: 'project', _module: 'compass' }));
}
if (searchAll || moduleFilter.includes('tasks')) {
const tasks = await db.select()
.from(pcTasks)
.where(or(
ilike(pcTasks.title, searchQuery),
ilike(pcTasks.description, searchQuery)
))
.limit(limitNum);
results.tasks = tasks.map(t => ({ ...t, _type: 'task', _module: 'compass' }));
}
if (searchAll || moduleFilter.includes('conversations')) {
const convs = await db.select()
.from(conversations)
.where(and(
eq(conversations.userId, userId),
ilike(conversations.title, searchQuery)
))
.limit(limitNum);
results.conversations = convs.map(c => ({ ...c, _type: 'conversation', _module: 'agent' }));
}
if (searchAll || moduleFilter.includes('knowledge')) {
const knowledge = await db.select()
.from(knowledgeBase)
.where(or(
ilike(knowledgeBase.title, searchQuery),
ilike(knowledgeBase.content, searchQuery)
))
.limit(limitNum);
results.knowledge = knowledge.map(k => ({ ...k, _type: 'knowledge', _module: 'agent' }));
}
const allResults = [
...results.pages,
...results.clients,
...results.projects,
...results.tasks,
...results.conversations,
...results.knowledge
];
res.json({ results: allResults, grouped: results });
} catch (error) {
console.error("Search error:", error);
res.status(500).json({ error: "Search failed" });
}
});
// ========== WORKSPACE PAGES ==========
router.get("/pages", requireAuth, async (req, res) => {
try {
const userId = req.user!.id;
const { parentId, archived } = req.query;
let whereClause = and(
eq(workspacePages.userId, userId),
eq(workspacePages.isArchived, archived === 'true' ? 1 : 0)
);
if (parentId) {
whereClause = and(whereClause, eq(workspacePages.parentId, parseInt(parentId as string)));
} else {
whereClause = and(whereClause, sql`${workspacePages.parentId} IS NULL`);
}
const pages = await db.select()
.from(workspacePages)
.where(whereClause)
.orderBy(workspacePages.orderIndex);
res.json(pages);
} catch (error) {
console.error("Error fetching pages:", error);
res.status(500).json({ error: "Failed to fetch pages" });
}
});
router.get("/pages/favorites", requireAuth, async (req, res) => {
try {
const userId = req.user!.id;
const pages = await db.select()
.from(workspacePages)
.where(and(
eq(workspacePages.userId, userId),
eq(workspacePages.isFavorite, 1),
eq(workspacePages.isArchived, 0)
))
.orderBy(workspacePages.title);
res.json(pages);
} catch (error) {
res.status(500).json({ error: "Failed to fetch favorite pages" });
}
});
router.get("/pages/:id", requireAuth, async (req, res) => {
try {
const userId = req.user!.id;
const [page] = await db.select()
.from(workspacePages)
.where(and(
eq(workspacePages.id, parseInt(req.params.id)),
or(eq(workspacePages.userId, userId), eq(workspacePages.isPublic, 1))
));
if (!page) {
return res.status(404).json({ error: "Page not found" });
}
res.json(page);
} catch (error) {
res.status(500).json({ error: "Failed to fetch page" });
}
});
router.post("/pages", requireAuth, async (req, res) => {
try {
const userId = req.user!.id;
const data = insertWorkspacePageSchema.parse({ ...req.body, userId });
const [page] = await db.insert(workspacePages).values(data).returning();
await db.insert(activityFeed).values({
userId,
actorId: userId,
type: 'created',
module: 'workspace',
entityType: 'page',
entityId: page.id.toString(),
entityTitle: page.title,
description: `Criou a página "${page.title}"`
});
res.status(201).json(page);
} catch (error) {
console.error("Error creating page:", error);
res.status(500).json({ error: "Failed to create page" });
}
});
router.patch("/pages/:id", requireAuth, async (req, res) => {
try {
const userId = req.user!.id;
const pageId = parseInt(req.params.id);
const [existing] = await db.select()
.from(workspacePages)
.where(and(eq(workspacePages.id, pageId), eq(workspacePages.userId, userId)));
if (!existing) {
return res.status(404).json({ error: "Page not found" });
}
const updateData = { ...req.body, updatedAt: new Date() };
delete updateData.id;
delete updateData.userId;
delete updateData.createdAt;
const [updated] = await db.update(workspacePages)
.set(updateData)
.where(eq(workspacePages.id, pageId))
.returning();
res.json(updated);
} catch (error) {
console.error("Error updating page:", error);
res.status(500).json({ error: "Failed to update page" });
}
});
router.delete("/pages/:id", requireAuth, async (req, res) => {
try {
const userId = req.user!.id;
const pageId = parseInt(req.params.id);
const [deleted] = await db.delete(workspacePages)
.where(and(eq(workspacePages.id, pageId), eq(workspacePages.userId, userId)))
.returning();
if (!deleted) {
return res.status(404).json({ error: "Page not found" });
}
res.status(204).send();
} catch (error) {
res.status(500).json({ error: "Failed to delete page" });
}
});
// ========== PAGE BLOCKS ==========
router.get("/pages/:pageId/blocks", requireAuth, async (req, res) => {
try {
const pageId = parseInt(req.params.pageId);
const blocks = await db.select()
.from(pageBlocks)
.where(eq(pageBlocks.pageId, pageId))
.orderBy(pageBlocks.orderIndex);
res.json(blocks);
} catch (error) {
res.status(500).json({ error: "Failed to fetch blocks" });
}
});
router.post("/pages/:pageId/blocks", requireAuth, async (req, res) => {
try {
const pageId = parseInt(req.params.pageId);
const data = insertPageBlockSchema.parse({ ...req.body, pageId });
const [block] = await db.insert(pageBlocks).values(data).returning();
res.status(201).json(block);
} catch (error) {
console.error("Error creating block:", error);
res.status(500).json({ error: "Failed to create block" });
}
});
router.patch("/blocks/:id", requireAuth, async (req, res) => {
try {
const blockId = parseInt(req.params.id);
const updateData = { ...req.body, updatedAt: new Date() };
delete updateData.id;
delete updateData.pageId;
delete updateData.createdAt;
const [updated] = await db.update(pageBlocks)
.set(updateData)
.where(eq(pageBlocks.id, blockId))
.returning();
if (!updated) {
return res.status(404).json({ error: "Block not found" });
}
res.json(updated);
} catch (error) {
res.status(500).json({ error: "Failed to update block" });
}
});
router.delete("/blocks/:id", requireAuth, async (req, res) => {
try {
const blockId = parseInt(req.params.id);
const [deleted] = await db.delete(pageBlocks)
.where(eq(pageBlocks.id, blockId))
.returning();
if (!deleted) {
return res.status(404).json({ error: "Block not found" });
}
res.status(204).send();
} catch (error) {
res.status(500).json({ error: "Failed to delete block" });
}
});
router.post("/pages/:pageId/blocks/reorder", requireAuth, async (req, res) => {
try {
const { blocks } = req.body;
for (const { id, orderIndex } of blocks) {
await db.update(pageBlocks)
.set({ orderIndex, updatedAt: new Date() })
.where(eq(pageBlocks.id, id));
}
res.json({ success: true });
} catch (error) {
res.status(500).json({ error: "Failed to reorder blocks" });
}
});
// ========== DASHBOARD WIDGETS ==========
router.get("/widgets", requireAuth, async (req, res) => {
try {
const userId = req.user!.id;
const widgets = await db.select()
.from(dashboardWidgets)
.where(eq(dashboardWidgets.userId, userId));
res.json(widgets);
} catch (error) {
res.status(500).json({ error: "Failed to fetch widgets" });
}
});
router.post("/widgets", requireAuth, async (req, res) => {
try {
const userId = req.user!.id;
const data = insertDashboardWidgetSchema.parse({ ...req.body, userId });
const [widget] = await db.insert(dashboardWidgets).values(data).returning();
res.status(201).json(widget);
} catch (error) {
res.status(500).json({ error: "Failed to create widget" });
}
});
router.patch("/widgets/:id", requireAuth, async (req, res) => {
try {
const userId = req.user!.id;
const widgetId = parseInt(req.params.id);
const updateData = { ...req.body };
delete updateData.id;
delete updateData.userId;
delete updateData.createdAt;
const [updated] = await db.update(dashboardWidgets)
.set(updateData)
.where(and(eq(dashboardWidgets.id, widgetId), eq(dashboardWidgets.userId, userId)))
.returning();
if (!updated) {
return res.status(404).json({ error: "Widget not found" });
}
res.json(updated);
} catch (error) {
res.status(500).json({ error: "Failed to update widget" });
}
});
router.delete("/widgets/:id", requireAuth, async (req, res) => {
try {
const userId = req.user!.id;
const widgetId = parseInt(req.params.id);
const [deleted] = await db.delete(dashboardWidgets)
.where(and(eq(dashboardWidgets.id, widgetId), eq(dashboardWidgets.userId, userId)))
.returning();
if (!deleted) {
return res.status(404).json({ error: "Widget not found" });
}
res.status(204).send();
} catch (error) {
res.status(500).json({ error: "Failed to delete widget" });
}
});
router.post("/widgets/defaults", requireAuth, async (req, res) => {
try {
const userId = req.user!.id;
const existingWidgets = await db.select().from(dashboardWidgets).where(eq(dashboardWidgets.userId, userId));
if (existingWidgets.length > 0) {
return res.json({ message: "Widgets already exist", widgets: existingWidgets });
}
const defaultWidgets = [
{ userId, type: 'tasks', title: 'Tarefas Pendentes', position: JSON.stringify({ x: 0, y: 0, w: 2, h: 2 }), isVisible: 1 },
{ userId, type: 'recent_activity', title: 'Atividades Recentes', position: JSON.stringify({ x: 2, y: 0, w: 2, h: 2 }), isVisible: 1 },
{ userId, type: 'quick_notes', title: 'Notas Rápidas', position: JSON.stringify({ x: 0, y: 2, w: 2, h: 1 }), isVisible: 1 },
{ userId, type: 'favorites', title: 'Favoritos', position: JSON.stringify({ x: 2, y: 2, w: 2, h: 1 }), isVisible: 1 },
];
const widgets = await db.insert(dashboardWidgets).values(defaultWidgets).returning();
res.status(201).json(widgets);
} catch (error) {
res.status(500).json({ error: "Failed to create default widgets" });
}
});
// ========== QUICK NOTES ==========
router.get("/notes", requireAuth, async (req, res) => {
try {
const userId = req.user!.id;
const notes = await db.select()
.from(quickNotes)
.where(eq(quickNotes.userId, userId))
.orderBy(desc(quickNotes.isPinned), desc(quickNotes.updatedAt));
res.json(notes);
} catch (error) {
res.status(500).json({ error: "Failed to fetch notes" });
}
});
router.post("/notes", requireAuth, async (req, res) => {
try {
const userId = req.user!.id;
const data = insertQuickNoteSchema.parse({ ...req.body, userId });
const [note] = await db.insert(quickNotes).values(data).returning();
res.status(201).json(note);
} catch (error) {
res.status(500).json({ error: "Failed to create note" });
}
});
router.patch("/notes/:id", requireAuth, async (req, res) => {
try {
const userId = req.user!.id;
const noteId = parseInt(req.params.id);
const updateData = { ...req.body, updatedAt: new Date() };
delete updateData.id;
delete updateData.userId;
delete updateData.createdAt;
const [updated] = await db.update(quickNotes)
.set(updateData)
.where(and(eq(quickNotes.id, noteId), eq(quickNotes.userId, userId)))
.returning();
if (!updated) {
return res.status(404).json({ error: "Note not found" });
}
res.json(updated);
} catch (error) {
res.status(500).json({ error: "Failed to update note" });
}
});
router.delete("/notes/:id", requireAuth, async (req, res) => {
try {
const userId = req.user!.id;
const noteId = parseInt(req.params.id);
const [deleted] = await db.delete(quickNotes)
.where(and(eq(quickNotes.id, noteId), eq(quickNotes.userId, userId)))
.returning();
if (!deleted) {
return res.status(404).json({ error: "Note not found" });
}
res.status(204).send();
} catch (error) {
res.status(500).json({ error: "Failed to delete note" });
}
});
// ========== ACTIVITY FEED / INBOX ==========
router.get("/activity", requireAuth, async (req, res) => {
try {
const userId = req.user!.id;
const { unreadOnly, module, limit = 50 } = req.query;
const limitNum = Math.min(parseInt(limit as string) || 50, 100);
let whereClause: any = eq(activityFeed.userId, userId);
if (unreadOnly === 'true') {
whereClause = and(whereClause, eq(activityFeed.isRead, 0))!;
}
if (module) {
whereClause = and(whereClause, eq(activityFeed.module, module as string))!;
}
const activities = await db.select()
.from(activityFeed)
.where(whereClause as any)
.orderBy(desc(activityFeed.createdAt))
.limit(limitNum);
res.json(activities);
} catch (error) {
res.status(500).json({ error: "Failed to fetch activities" });
}
});
router.get("/activity/unread-count", requireAuth, async (req, res) => {
try {
const userId = req.user!.id;
const result = await db.select({ count: sql<number>`count(*)` })
.from(activityFeed)
.where(and(eq(activityFeed.userId, userId), eq(activityFeed.isRead, 0)));
res.json({ count: result[0]?.count || 0 });
} catch (error) {
res.status(500).json({ error: "Failed to fetch unread count" });
}
});
router.patch("/activity/:id/read", requireAuth, async (req, res) => {
try {
const userId = req.user!.id;
const activityId = parseInt(req.params.id);
const [updated] = await db.update(activityFeed)
.set({ isRead: 1 })
.where(and(eq(activityFeed.id, activityId), eq(activityFeed.userId, userId)))
.returning();
res.json(updated || { success: true });
} catch (error) {
res.status(500).json({ error: "Failed to mark as read" });
}
});
router.post("/activity/mark-all-read", requireAuth, async (req, res) => {
try {
const userId = req.user!.id;
await db.update(activityFeed)
.set({ isRead: 1 })
.where(and(eq(activityFeed.userId, userId), eq(activityFeed.isRead, 0)));
res.json({ success: true });
} catch (error) {
res.status(500).json({ error: "Failed to mark all as read" });
}
});
// ========== FAVORITES ==========
router.get("/favorites", requireAuth, async (req, res) => {
try {
const userId = req.user!.id;
const favorites = await db.select()
.from(userFavorites)
.where(eq(userFavorites.userId, userId))
.orderBy(userFavorites.orderIndex);
res.json(favorites);
} catch (error) {
res.status(500).json({ error: "Failed to fetch favorites" });
}
});
router.post("/favorites", requireAuth, async (req, res) => {
try {
const userId = req.user!.id;
const data = insertUserFavoriteSchema.parse({ ...req.body, userId });
const [favorite] = await db.insert(userFavorites).values(data).returning();
res.status(201).json(favorite);
} catch (error) {
res.status(500).json({ error: "Failed to add favorite" });
}
});
router.delete("/favorites/:id", requireAuth, async (req, res) => {
try {
const userId = req.user!.id;
const favoriteId = parseInt(req.params.id);
const [deleted] = await db.delete(userFavorites)
.where(and(eq(userFavorites.id, favoriteId), eq(userFavorites.userId, userId)))
.returning();
if (!deleted) {
return res.status(404).json({ error: "Favorite not found" });
}
res.status(204).send();
} catch (error) {
res.status(500).json({ error: "Failed to remove favorite" });
}
});
// ========== COMMAND HISTORY ==========
router.get("/commands/recent", requireAuth, async (req, res) => {
try {
const userId = req.user!.id;
const commands = await db.select()
.from(commandHistory)
.where(eq(commandHistory.userId, userId))
.orderBy(desc(commandHistory.frequency), desc(commandHistory.lastUsedAt))
.limit(20);
res.json(commands);
} catch (error) {
res.status(500).json({ error: "Failed to fetch command history" });
}
});
router.post("/commands/track", requireAuth, async (req, res) => {
try {
const userId = req.user!.id;
const { command } = req.body;
const [existing] = await db.select()
.from(commandHistory)
.where(and(eq(commandHistory.userId, userId), eq(commandHistory.command, command)));
if (existing) {
const [updated] = await db.update(commandHistory)
.set({ frequency: (existing.frequency || 0) + 1, lastUsedAt: new Date() })
.where(eq(commandHistory.id, existing.id))
.returning();
res.json(updated);
} else {
const [created] = await db.insert(commandHistory)
.values({ userId, command })
.returning();
res.status(201).json(created);
}
} catch (error) {
res.status(500).json({ error: "Failed to track command" });
}
});
// ========== PAGE BACKLINKS ==========
router.get("/pages/:id/backlinks", requireAuth, async (req, res) => {
try {
const pageId = parseInt(req.params.id);
const backlinks = await db.select({
id: pageLinks.id,
sourcePageId: pageLinks.sourcePageId,
sourcePageTitle: workspacePages.title,
sourcePageIcon: workspacePages.icon,
createdAt: pageLinks.createdAt
})
.from(pageLinks)
.innerJoin(workspacePages, eq(pageLinks.sourcePageId, workspacePages.id))
.where(eq(pageLinks.targetPageId, pageId));
res.json(backlinks);
} catch (error) {
res.status(500).json({ error: "Failed to fetch backlinks" });
}
});
export default router;