import { Router, Request, Response, NextFunction } from "express"; import { compassStorage } from "./storage"; import { crmStorage } from "../crm/storage"; import { productionStorage } from "../production/storage"; import { valuationStorage } from "../valuation/storage"; import { insertTenantSchema, insertTenantUserSchema, insertPcClientSchema, insertPcClientContactSchema, insertPcProjectSchema, insertPcProjectMemberSchema, insertPcCanvasBlockSchema, insertPcCanvasQuestionSchema, insertPcCanvasExpectedOutputSchema, insertPcCanvasPdcaLinkSchema, insertPcCanvasSwotLinkSchema, insertPcProcessSchema, insertPcProcessStepSchema, insertPcSwotAnalysisSchema, insertPcSwotItemSchema, updatePcSwotItemSchema, insertPcCrmStageSchema, insertPcCrmLeadSchema, insertPcCrmOpportunitySchema, insertPcCrmActivitySchema, insertPcDeliverableSchema, insertPcTaskSchema, insertPcPdcaCycleSchema, insertPcPdcaActionSchema, insertPcRequirementSchema, insertPcReportTemplateSchema, insertPcReportConfigurationSchema, insertPcGeneratedReportSchema, insertPcErpModuleSchema, insertPcErpRequirementSchema, insertPcErpParameterizationTopicSchema, insertPcErpParameterizationItemSchema } from "@shared/schema"; const router = Router(); // Authentication middleware function requireAuth(req: Request, res: Response, next: NextFunction) { if (!req.isAuthenticated() || !req.user) { return res.status(401).json({ error: "Não autorizado" }); } next(); } // Apply auth to all routes router.use(requireAuth); // Helper to get tenant ID from request header or user's first tenant async function getTenantId(req: Request): Promise { const userId = (req.user as any).id; const headerTenantId = req.headers["x-tenant-id"]; if (headerTenantId) { const tenantId = parseInt(headerTenantId as string); const isMember = await compassStorage.isUserInTenant(userId, tenantId); return isMember ? tenantId : null; } const tenants = await compassStorage.getUserTenants(userId); return tenants.length > 0 ? tenants[0].id : null; } // Validate tenant membership async function validateTenantMembership(userId: string, tenantId: number): Promise { return await compassStorage.isUserInTenant(userId, tenantId); } // Helper to validate project belongs to tenant async function validateProjectAccess(projectId: number, tenantId: number): Promise { const project = await compassStorage.getProject(projectId, tenantId); return !!project; } // Helper to validate client exists (agora usando CRM centralizado) async function validateClientAccess(clientId: number): Promise { const client = await crmStorage.getClient(clientId); return !!client; } // ========== TENANTS ========== router.get("/tenants", async (req: Request, res: Response) => { try { const userId = (req.user as any).id; const tenants = await compassStorage.getUserTenants(userId); res.json(tenants); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.get("/tenants/:id", async (req: Request, res: Response) => { try { const userId = (req.user as any).id; const tenantId = parseInt(req.params.id); const isMember = await validateTenantMembership(userId, tenantId); if (!isMember) return res.status(403).json({ error: "Acesso negado ao tenant" }); const tenant = await compassStorage.getTenant(tenantId); if (!tenant) return res.status(404).json({ error: "Tenant não encontrado" }); res.json(tenant); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/tenants", async (req: Request, res: Response) => { try { const userId = (req.user as any).id; const parsed = insertTenantSchema.parse(req.body); const tenant = await compassStorage.createTenant(parsed); await compassStorage.addUserToTenant({ tenantId: tenant.id, userId, role: "owner", isOwner: "true" }); res.status(201).json(tenant); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.patch("/tenants/:id", async (req: Request, res: Response) => { try { const userId = (req.user as any).id; const tenantId = parseInt(req.params.id); const isMember = await validateTenantMembership(userId, tenantId); if (!isMember) return res.status(403).json({ error: "Acesso negado ao tenant" }); const tenant = await compassStorage.updateTenant(tenantId, req.body); if (!tenant) return res.status(404).json({ error: "Tenant não encontrado" }); res.json(tenant); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.delete("/tenants/:id", async (req: Request, res: Response) => { try { const userId = (req.user as any).id; const tenantId = parseInt(req.params.id); const isMember = await validateTenantMembership(userId, tenantId); if (!isMember) return res.status(403).json({ error: "Acesso negado ao tenant" }); const deleted = await compassStorage.deleteTenant(tenantId); if (!deleted) return res.status(404).json({ error: "Tenant não encontrado" }); res.status(204).send(); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // ========== TENANT USERS ========== router.get("/tenants/:tenantId/users", async (req: Request, res: Response) => { try { const userId = (req.user as any).id; const tenantId = parseInt(req.params.tenantId); const isMember = await validateTenantMembership(userId, tenantId); if (!isMember) return res.status(403).json({ error: "Acesso negado ao tenant" }); const users = await compassStorage.getTenantUsers(tenantId); res.json(users); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/tenants/:tenantId/users", async (req: Request, res: Response) => { try { const userId = (req.user as any).id; const tenantId = parseInt(req.params.tenantId); const isMember = await validateTenantMembership(userId, tenantId); if (!isMember) return res.status(403).json({ error: "Acesso negado ao tenant" }); const parsed = insertTenantUserSchema.parse({ ...req.body, tenantId }); const tu = await compassStorage.addUserToTenant(parsed); res.status(201).json(tu); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.delete("/tenants/:tenantId/users/:userId", async (req: Request, res: Response) => { try { const currentUserId = (req.user as any).id; const tenantId = parseInt(req.params.tenantId); const isMember = await validateTenantMembership(currentUserId, tenantId); if (!isMember) return res.status(403).json({ error: "Acesso negado ao tenant" }); const deleted = await compassStorage.removeUserFromTenant(tenantId, req.params.userId); if (!deleted) return res.status(404).json({ error: "Usuário não encontrado no tenant" }); res.status(204).send(); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // ========== CLIENTS (usando CRM centralizado) ========== router.get("/clients", async (req: Request, res: Response) => { try { const clients = await crmStorage.getClients(); res.json(clients); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.get("/clients/:id", async (req: Request, res: Response) => { try { const client = await crmStorage.getClient(parseInt(req.params.id)); if (!client) return res.status(404).json({ error: "Cliente não encontrado" }); res.json(client); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // ========== CLIENT CONTACTS (deprecated - contatos agora são do CRM) ========== router.get("/clients/:clientId/contacts", async (req: Request, res: Response) => { try { const clientId = parseInt(req.params.clientId); const client = await crmStorage.getClient(clientId); if (!client) return res.status(404).json({ error: "Cliente não encontrado" }); res.json([]); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.delete("/contacts/:id", async (req: Request, res: Response) => { try { const deleted = await compassStorage.deleteClientContact(parseInt(req.params.id)); if (!deleted) return res.status(404).json({ error: "Contato não encontrado" }); res.status(204).send(); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // ========== PROJECTS ========== router.get("/projects", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(400).json({ error: "Sem tenant associado" }); const clientId = req.query.clientId ? parseInt(req.query.clientId as string) : undefined; const projects = clientId ? await compassStorage.getProjectsByClient(clientId, tenantId) : await compassStorage.getProjects(tenantId); res.json(projects); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.get("/projects/:id", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(400).json({ error: "Sem tenant associado" }); const project = await compassStorage.getProject(parseInt(req.params.id), tenantId); if (!project) return res.status(404).json({ error: "Projeto não encontrado" }); res.json(project); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/projects", async (req: Request, res: Response) => { try { const userId = (req.user as any).id; const tenantId = await getTenantId(req); if (!tenantId) return res.status(400).json({ error: "Sem tenant associado" }); const parsed = insertPcProjectSchema.parse({ ...req.body, userId, tenantId }); const project = await compassStorage.createProject(parsed); // Se for projeto de valuation, criar também na tabela valuation_projects if (parsed.projectType === "valuation") { try { // Buscar dados do cliente para preencher informações do valuation project const client = parsed.clientId ? await crmStorage.getClient(parsed.clientId) : null; await valuationStorage.createProject({ tenantId, companyName: client?.name || parsed.name, cnpj: client?.cnpj || null, sector: client?.segment || "Serviços", businessModel: null, stage: "Growth", size: "Média", status: "draft", consultantId: userId, clientId: parsed.clientId || null, }); } catch (syncError) { console.error("Erro ao sincronizar com valuation_projects:", syncError); } } res.status(201).json(project); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.patch("/projects/:id", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(400).json({ error: "Sem tenant associado" }); const project = await compassStorage.updateProject(parseInt(req.params.id), tenantId, req.body); if (!project) return res.status(404).json({ error: "Projeto não encontrado" }); res.json(project); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.delete("/projects/:id", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(400).json({ error: "Sem tenant associado" }); const deleted = await compassStorage.deleteProject(parseInt(req.params.id), tenantId); if (!deleted) return res.status(404).json({ error: "Projeto não encontrado" }); res.status(204).send(); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // ========== PROJECT MEMBERS ========== router.get("/projects/:projectId/members", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(400).json({ error: "Sem tenant associado" }); const projectId = parseInt(req.params.projectId); if (!await validateProjectAccess(projectId, tenantId)) { return res.status(404).json({ error: "Projeto não encontrado" }); } const members = await compassStorage.getProjectMembers(projectId); res.json(members); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/projects/:projectId/members", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(400).json({ error: "Sem tenant associado" }); const projectId = parseInt(req.params.projectId); if (!await validateProjectAccess(projectId, tenantId)) { return res.status(404).json({ error: "Projeto não encontrado" }); } const parsed = insertPcProjectMemberSchema.parse({ ...req.body, projectId }); const member = await compassStorage.addProjectMember(parsed); res.status(201).json(member); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.delete("/project-members/:id", async (req: Request, res: Response) => { try { const deleted = await compassStorage.removeProjectMember(parseInt(req.params.id)); if (!deleted) return res.status(404).json({ error: "Membro não encontrado" }); res.status(204).send(); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // ========== CANVAS BLOCKS ========== router.get("/projects/:projectId/canvas", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(400).json({ error: "Sem tenant associado" }); const projectId = parseInt(req.params.projectId); if (!await validateProjectAccess(projectId, tenantId)) { return res.status(404).json({ error: "Projeto não encontrado" }); } const blocks = await compassStorage.getCanvasBlocks(projectId); // Add question counts to each block const blocksWithStats = await Promise.all(blocks.map(async (block) => { const questions = await compassStorage.getCanvasQuestions(block.id); const answeredCount = questions.filter(q => q.score !== null && q.score > 0).length; return { ...block, questionsCount: questions.length, answeredCount: answeredCount, }; })); res.json(blocksWithStats); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/projects/:projectId/canvas", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(400).json({ error: "Sem tenant associado" }); const projectId = parseInt(req.params.projectId); if (!await validateProjectAccess(projectId, tenantId)) { return res.status(404).json({ error: "Projeto não encontrado" }); } const parsed = insertPcCanvasBlockSchema.parse({ ...req.body, projectId }); const block = await compassStorage.createCanvasBlock(parsed); res.status(201).json(block); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.patch("/canvas/:id", async (req: Request, res: Response) => { try { const block = await compassStorage.updateCanvasBlock(parseInt(req.params.id), req.body); if (!block) return res.status(404).json({ error: "Bloco não encontrado" }); res.json(block); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.delete("/canvas/:id", async (req: Request, res: Response) => { try { const deleted = await compassStorage.deleteCanvasBlock(parseInt(req.params.id)); if (!deleted) return res.status(404).json({ error: "Bloco não encontrado" }); res.status(204).send(); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // ========== CANVAS DIAGNOSTIC QUESTIONS ========== router.get("/canvas/:blockId/questions", async (req: Request, res: Response) => { try { const blockId = parseInt(req.params.blockId); const questions = await compassStorage.getCanvasQuestions(blockId); res.json(questions); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/canvas/:blockId/questions", async (req: Request, res: Response) => { try { const blockId = parseInt(req.params.blockId); const parsed = insertPcCanvasQuestionSchema.parse({ ...req.body, blockId }); const question = await compassStorage.createCanvasQuestion(parsed); res.status(201).json(question); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.patch("/canvas/questions/:id", async (req: Request, res: Response) => { try { const question = await compassStorage.updateCanvasQuestion(parseInt(req.params.id), req.body); if (!question) return res.status(404).json({ error: "Pergunta não encontrada" }); res.json(question); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.delete("/canvas/questions/:id", async (req: Request, res: Response) => { try { const deleted = await compassStorage.deleteCanvasQuestion(parseInt(req.params.id)); if (!deleted) return res.status(404).json({ error: "Pergunta não encontrada" }); res.status(204).send(); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // ========== CANVAS EXPECTED OUTPUTS ========== router.get("/canvas/:blockId/outputs", async (req: Request, res: Response) => { try { const blockId = parseInt(req.params.blockId); const outputs = await compassStorage.getCanvasExpectedOutputs(blockId); res.json(outputs); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/canvas/:blockId/outputs", async (req: Request, res: Response) => { try { const blockId = parseInt(req.params.blockId); const parsed = insertPcCanvasExpectedOutputSchema.parse({ ...req.body, blockId }); const output = await compassStorage.createCanvasExpectedOutput(parsed); res.status(201).json(output); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.patch("/canvas/outputs/:id", async (req: Request, res: Response) => { try { const output = await compassStorage.updateCanvasExpectedOutput(parseInt(req.params.id), req.body); if (!output) return res.status(404).json({ error: "Saída não encontrada" }); res.json(output); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.delete("/canvas/outputs/:id", async (req: Request, res: Response) => { try { const deleted = await compassStorage.deleteCanvasExpectedOutput(parseInt(req.params.id)); if (!deleted) return res.status(404).json({ error: "Saída não encontrada" }); res.status(204).send(); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // ========== CANVAS PDCA LINKS ========== router.get("/canvas/:blockId/pdca", async (req: Request, res: Response) => { try { const blockId = parseInt(req.params.blockId); const links = await compassStorage.getCanvasPdcaLinks(blockId); res.json(links); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/canvas/:blockId/pdca", async (req: Request, res: Response) => { try { const blockId = parseInt(req.params.blockId); const parsed = insertPcCanvasPdcaLinkSchema.parse({ ...req.body, blockId }); const link = await compassStorage.createCanvasPdcaLink(parsed); res.status(201).json(link); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.patch("/canvas/pdca/:id", async (req: Request, res: Response) => { try { const link = await compassStorage.updateCanvasPdcaLink(parseInt(req.params.id), req.body); if (!link) return res.status(404).json({ error: "Item PDCA não encontrado" }); res.json(link); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.delete("/canvas/pdca/:id", async (req: Request, res: Response) => { try { const deleted = await compassStorage.deleteCanvasPdcaLink(parseInt(req.params.id)); if (!deleted) return res.status(404).json({ error: "Item PDCA não encontrado" }); res.status(204).send(); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // ========== CANVAS SWOT LINKS ========== router.get("/canvas/:blockId/swot", async (req: Request, res: Response) => { try { const blockId = parseInt(req.params.blockId); const links = await compassStorage.getCanvasSwotLinks(blockId); res.json(links); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/canvas/:blockId/swot", async (req: Request, res: Response) => { try { const blockId = parseInt(req.params.blockId); const parsed = insertPcCanvasSwotLinkSchema.parse({ ...req.body, blockId }); const link = await compassStorage.createCanvasSwotLink(parsed); res.status(201).json(link); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.delete("/canvas/swot/:id", async (req: Request, res: Response) => { try { const deleted = await compassStorage.deleteCanvasSwotLink(parseInt(req.params.id)); if (!deleted) return res.status(404).json({ error: "Item SWOT não encontrado" }); res.status(204).send(); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // ========== PROCESSES ========== router.get("/projects/:projectId/processes", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(400).json({ error: "Sem tenant associado" }); const projectId = parseInt(req.params.projectId); if (!await validateProjectAccess(projectId, tenantId)) { return res.status(404).json({ error: "Projeto não encontrado" }); } const processes = await compassStorage.getProcesses(projectId); res.json(processes); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/projects/:projectId/processes", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(400).json({ error: "Sem tenant associado" }); const projectId = parseInt(req.params.projectId); if (!await validateProjectAccess(projectId, tenantId)) { return res.status(404).json({ error: "Projeto não encontrado" }); } const parsed = insertPcProcessSchema.parse({ ...req.body, projectId }); const process = await compassStorage.createProcess(parsed); res.status(201).json(process); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.patch("/processes/:id", async (req: Request, res: Response) => { try { const process = await compassStorage.updateProcess(parseInt(req.params.id), req.body); if (!process) return res.status(404).json({ error: "Processo não encontrado" }); res.json(process); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.patch("/processes/:id/diagram", async (req: Request, res: Response) => { try { const { diagramNodes, diagramEdges, diagramViewport } = req.body; const process = await compassStorage.updateProcess(parseInt(req.params.id), { diagramNodes, diagramEdges, diagramViewport, }); if (!process) return res.status(404).json({ error: "Processo não encontrado" }); res.json(process); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.delete("/processes/:id", async (req: Request, res: Response) => { try { const deleted = await compassStorage.deleteProcess(parseInt(req.params.id)); if (!deleted) return res.status(404).json({ error: "Processo não encontrado" }); res.status(204).send(); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // ========== PROCESS STEPS ========== router.get("/processes/:processId/steps", async (req: Request, res: Response) => { try { const steps = await compassStorage.getProcessSteps(parseInt(req.params.processId)); res.json(steps); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/processes/:processId/steps", async (req: Request, res: Response) => { try { const parsed = insertPcProcessStepSchema.parse({ ...req.body, processId: parseInt(req.params.processId) }); const step = await compassStorage.createProcessStep(parsed); res.status(201).json(step); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.patch("/process-steps/:id", async (req: Request, res: Response) => { try { const step = await compassStorage.updateProcessStep(parseInt(req.params.id), req.body); if (!step) return res.status(404).json({ error: "Etapa não encontrada" }); res.json(step); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.delete("/process-steps/:id", async (req: Request, res: Response) => { try { const deleted = await compassStorage.deleteProcessStep(parseInt(req.params.id)); if (!deleted) return res.status(404).json({ error: "Etapa não encontrada" }); res.status(204).send(); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // ========== SWOT ANALYSES ========== router.get("/projects/:projectId/swot", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(400).json({ error: "Sem tenant associado" }); const projectId = parseInt(req.params.projectId); if (!await validateProjectAccess(projectId, tenantId)) { return res.status(404).json({ error: "Projeto não encontrado" }); } const analyses = await compassStorage.getSwotAnalyses(projectId); res.json(analyses); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/projects/:projectId/swot", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(400).json({ error: "Sem tenant associado" }); const projectId = parseInt(req.params.projectId); if (!await validateProjectAccess(projectId, tenantId)) { return res.status(404).json({ error: "Projeto não encontrado" }); } const parsed = insertPcSwotAnalysisSchema.parse({ ...req.body, projectId }); const analysis = await compassStorage.createSwotAnalysis(parsed); res.status(201).json(analysis); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.patch("/swot/:id", async (req: Request, res: Response) => { try { const analysis = await compassStorage.updateSwotAnalysis(parseInt(req.params.id), req.body); if (!analysis) return res.status(404).json({ error: "Análise não encontrada" }); res.json(analysis); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.delete("/swot/:id", async (req: Request, res: Response) => { try { const deleted = await compassStorage.deleteSwotAnalysis(parseInt(req.params.id)); if (!deleted) return res.status(404).json({ error: "Análise não encontrada" }); res.status(204).send(); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // ========== SWOT ITEMS ========== router.get("/swot/:swotId/items", async (req: Request, res: Response) => { try { const items = await compassStorage.getSwotItems(parseInt(req.params.swotId)); res.json(items); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/swot/:swotId/items", async (req: Request, res: Response) => { try { const parsed = insertPcSwotItemSchema.parse({ ...req.body, swotAnalysisId: parseInt(req.params.swotId) }); const item = await compassStorage.createSwotItem(parsed); res.status(201).json(item); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.patch("/swot-items/:id", async (req: Request, res: Response) => { try { const parsed = updatePcSwotItemSchema.parse(req.body); const item = await compassStorage.updateSwotItem(parseInt(req.params.id), parsed); if (!item) return res.status(404).json({ error: "Item não encontrado" }); res.json(item); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.delete("/swot-items/:id", async (req: Request, res: Response) => { try { const deleted = await compassStorage.deleteSwotItem(parseInt(req.params.id)); if (!deleted) return res.status(404).json({ error: "Item não encontrado" }); res.status(204).send(); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // ========== CRM STAGES ========== router.get("/crm/stages", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(400).json({ error: "Sem tenant associado" }); const stages = await compassStorage.getCrmStages(tenantId); res.json(stages); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/crm/stages", async (req: Request, res: Response) => { try { const userId = (req.user as any).id; const tenantId = await getTenantId(req); if (!tenantId) return res.status(400).json({ error: "Sem tenant associado" }); const parsed = insertPcCrmStageSchema.parse({ ...req.body, userId, tenantId }); const stage = await compassStorage.createCrmStage(parsed); res.status(201).json(stage); } catch (error: any) { res.status(400).json({ error: error.message }); } }); // ========== CRM LEADS ========== router.get("/crm/leads", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(400).json({ error: "Sem tenant associado" }); const leads = await compassStorage.getCrmLeads(tenantId); res.json(leads); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/crm/leads", async (req: Request, res: Response) => { try { const userId = (req.user as any).id; const tenantId = await getTenantId(req); if (!tenantId) return res.status(400).json({ error: "Sem tenant associado" }); const parsed = insertPcCrmLeadSchema.parse({ ...req.body, userId, tenantId }); const lead = await compassStorage.createCrmLead(parsed); res.status(201).json(lead); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.patch("/crm/leads/:id", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(400).json({ error: "Sem tenant associado" }); const lead = await compassStorage.updateCrmLead(parseInt(req.params.id), tenantId, req.body); if (!lead) return res.status(404).json({ error: "Lead não encontrado" }); res.json(lead); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.delete("/crm/leads/:id", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(400).json({ error: "Sem tenant associado" }); const deleted = await compassStorage.deleteCrmLead(parseInt(req.params.id), tenantId); if (!deleted) return res.status(404).json({ error: "Lead não encontrado" }); res.status(204).send(); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // ========== CRM OPPORTUNITIES ========== router.get("/crm/opportunities", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(400).json({ error: "Sem tenant associado" }); const opportunities = await compassStorage.getCrmOpportunities(tenantId); res.json(opportunities); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/crm/opportunities", async (req: Request, res: Response) => { try { const userId = (req.user as any).id; const tenantId = await getTenantId(req); if (!tenantId) return res.status(400).json({ error: "Sem tenant associado" }); const parsed = insertPcCrmOpportunitySchema.parse({ ...req.body, userId, tenantId }); const opportunity = await compassStorage.createCrmOpportunity(parsed); res.status(201).json(opportunity); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.patch("/crm/opportunities/:id", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(400).json({ error: "Sem tenant associado" }); const opportunity = await compassStorage.updateCrmOpportunity(parseInt(req.params.id), tenantId, req.body); if (!opportunity) return res.status(404).json({ error: "Oportunidade não encontrada" }); res.json(opportunity); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.delete("/crm/opportunities/:id", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(400).json({ error: "Sem tenant associado" }); const deleted = await compassStorage.deleteCrmOpportunity(parseInt(req.params.id), tenantId); if (!deleted) return res.status(404).json({ error: "Oportunidade não encontrada" }); res.status(204).send(); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // ========== CRM ACTIVITIES ========== router.get("/crm/activities", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(400).json({ error: "Sem tenant associado" }); const activities = await compassStorage.getCrmActivities(tenantId); res.json(activities); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/crm/activities", async (req: Request, res: Response) => { try { const userId = (req.user as any).id; const tenantId = await getTenantId(req); if (!tenantId) return res.status(400).json({ error: "Sem tenant associado" }); const parsed = insertPcCrmActivitySchema.parse({ ...req.body, userId, tenantId }); const activity = await compassStorage.createCrmActivity(parsed); res.status(201).json(activity); } catch (error: any) { res.status(400).json({ error: error.message }); } }); // ========== DELIVERABLES ========== router.get("/projects/:projectId/deliverables", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(400).json({ error: "Sem tenant associado" }); const projectId = parseInt(req.params.projectId); if (!await validateProjectAccess(projectId, tenantId)) { return res.status(404).json({ error: "Projeto não encontrado" }); } const deliverables = await compassStorage.getDeliverables(projectId); res.json(deliverables); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/projects/:projectId/deliverables", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(400).json({ error: "Sem tenant associado" }); const projectId = parseInt(req.params.projectId); if (!await validateProjectAccess(projectId, tenantId)) { return res.status(404).json({ error: "Projeto não encontrado" }); } const parsed = insertPcDeliverableSchema.parse({ ...req.body, projectId }); const deliverable = await compassStorage.createDeliverable(parsed); res.status(201).json(deliverable); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.patch("/deliverables/:id", async (req: Request, res: Response) => { try { const deliverable = await compassStorage.updateDeliverable(parseInt(req.params.id), req.body); if (!deliverable) return res.status(404).json({ error: "Entregável não encontrado" }); res.json(deliverable); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.delete("/deliverables/:id", async (req: Request, res: Response) => { try { const deleted = await compassStorage.deleteDeliverable(parseInt(req.params.id)); if (!deleted) return res.status(404).json({ error: "Entregável não encontrado" }); res.status(204).send(); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // ========== TASKS ========== router.get("/tasks", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(400).json({ error: "Sem tenant associado" }); const projectId = req.query.projectId ? parseInt(req.query.projectId as string) : undefined; if (projectId && !await validateProjectAccess(projectId, tenantId)) { return res.status(404).json({ error: "Projeto não encontrado" }); } const tasks = await compassStorage.getTasks(projectId); res.json(tasks); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/tasks", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(400).json({ error: "Sem tenant associado" }); const userId = (req.user as any).id; if (req.body.projectId && !await validateProjectAccess(req.body.projectId, tenantId)) { return res.status(404).json({ error: "Projeto não encontrado" }); } const parsed = insertPcTaskSchema.parse({ ...req.body, createdById: userId }); const task = await compassStorage.createTask(parsed); res.status(201).json(task); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.patch("/tasks/:id", async (req: Request, res: Response) => { try { const task = await compassStorage.updateTask(parseInt(req.params.id), req.body); if (!task) return res.status(404).json({ error: "Tarefa não encontrada" }); res.json(task); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.delete("/tasks/:id", async (req: Request, res: Response) => { try { const deleted = await compassStorage.deleteTask(parseInt(req.params.id)); if (!deleted) return res.status(404).json({ error: "Tarefa não encontrada" }); res.status(204).send(); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // ========== PDCA CYCLES ========== router.get("/pdca/overview/:projectId", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const projectId = parseInt(req.params.projectId); if (!await validateProjectAccess(projectId, tenantId)) { return res.status(404).json({ error: "Projeto não encontrado" }); } const overview = await compassStorage.getPdcaOverview(projectId); res.json(overview); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.get("/pdca", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const projectId = req.query.projectId ? parseInt(req.query.projectId as string) : undefined; if (projectId && !await validateProjectAccess(projectId, tenantId)) { return res.status(404).json({ error: "Projeto não encontrado" }); } const cycles = await compassStorage.getPdcaCycles(tenantId, projectId); res.json(cycles); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.get("/pdca/:id", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const cycle = await compassStorage.getPdcaCycle(parseInt(req.params.id), tenantId); if (!cycle) return res.status(404).json({ error: "Ciclo PDCA não encontrado" }); res.json(cycle); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/pdca", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); if (req.body.projectId && !await validateProjectAccess(req.body.projectId, tenantId)) { return res.status(404).json({ error: "Projeto não encontrado" }); } const parsed = insertPcPdcaCycleSchema.parse({ ...req.body, tenantId }); const cycle = await compassStorage.createPdcaCycle(parsed); res.status(201).json(cycle); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.patch("/pdca/:id", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const cycle = await compassStorage.updatePdcaCycle(parseInt(req.params.id), tenantId, req.body); if (!cycle) return res.status(404).json({ error: "Ciclo PDCA não encontrado" }); res.json(cycle); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.delete("/pdca/:id", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const deleted = await compassStorage.deletePdcaCycle(parseInt(req.params.id), tenantId); if (!deleted) return res.status(404).json({ error: "Ciclo PDCA não encontrado" }); res.status(204).send(); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // ========== PDCA ACTIONS ========== router.get("/pdca/:cycleId/actions", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const cycleId = parseInt(req.params.cycleId); const cycle = await compassStorage.getPdcaCycle(cycleId, tenantId); if (!cycle) return res.status(404).json({ error: "Ciclo PDCA não encontrado" }); const actions = await compassStorage.getPdcaActions(cycleId); res.json(actions); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/pdca/:cycleId/actions", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const cycleId = parseInt(req.params.cycleId); const cycle = await compassStorage.getPdcaCycle(cycleId, tenantId); if (!cycle) return res.status(404).json({ error: "Ciclo PDCA não encontrado" }); const parsed = insertPcPdcaActionSchema.parse({ ...req.body, cycleId }); const action = await compassStorage.createPdcaAction(parsed); res.status(201).json(action); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.patch("/pdca/actions/:id", async (req: Request, res: Response) => { try { const action = await compassStorage.updatePdcaAction(parseInt(req.params.id), req.body); if (!action) return res.status(404).json({ error: "Ação não encontrada" }); res.json(action); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.delete("/pdca/actions/:id", async (req: Request, res: Response) => { try { const deleted = await compassStorage.deletePdcaAction(parseInt(req.params.id)); if (!deleted) return res.status(404).json({ error: "Ação não encontrada" }); res.status(204).send(); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // ========== REQUIREMENTS ========== router.get("/requirements", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const projectId = req.query.projectId ? parseInt(req.query.projectId as string) : undefined; if (projectId && !await validateProjectAccess(projectId, tenantId)) { return res.status(404).json({ error: "Projeto não encontrado" }); } const requirements = await compassStorage.getRequirements(tenantId, projectId); res.json(requirements); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.get("/requirements/:id", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const requirement = await compassStorage.getRequirement(parseInt(req.params.id), tenantId); if (!requirement) return res.status(404).json({ error: "Requisito não encontrado" }); res.json(requirement); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/requirements", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); if (req.body.projectId && !await validateProjectAccess(req.body.projectId, tenantId)) { return res.status(404).json({ error: "Projeto não encontrado" }); } const parsed = insertPcRequirementSchema.parse({ ...req.body, tenantId }); const requirement = await compassStorage.createRequirement(parsed); res.status(201).json(requirement); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.patch("/requirements/:id", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const requirement = await compassStorage.updateRequirement(parseInt(req.params.id), tenantId, req.body); if (!requirement) return res.status(404).json({ error: "Requisito não encontrado" }); res.json(requirement); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.delete("/requirements/:id", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const deleted = await compassStorage.deleteRequirement(parseInt(req.params.id), tenantId); if (!deleted) return res.status(404).json({ error: "Requisito não encontrado" }); res.status(204).send(); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // ========== DASHBOARD STATS ========== router.get("/stats", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.json({ clients: 0, projects: 0, leads: 0, opportunities: 0 }); const [clients, projects] = await Promise.all([ crmStorage.getClients(), compassStorage.getProjects(tenantId) ]); res.json({ clients: clients.length, projects: projects.length, leads: 0, opportunities: 0, activeProjects: projects.filter(p => p.status === 'andamento').length, openOpportunities: 0 }); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // ========== INTEGRATION: DIAGNOSTICS TO WORK ITEMS ========== router.post("/insights/to-work-item", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const { type, sourceId, title, description, projectId, priority = "medium" } = req.body; if (!type || !sourceId || !title) { return res.status(400).json({ error: "Campos obrigatórios: type, sourceId, title" }); } const originMap: Record = { swot: "diagnostic_insight", canvas: "diagnostic_insight", process: "diagnostic_insight", backlog: "backlog_item", }; const workItem = await productionStorage.createWorkItem({ tenantId, projectId: projectId || null, title, description: description || "", type: "task", status: "backlog", priority, origin: originMap[type] || "direct", originId: sourceId, }); res.status(201).json(workItem); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/swot-analyses/:analysisId/items/:itemIndex/to-work-item", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const analysisId = parseInt(req.params.analysisId); const itemIndex = parseInt(req.params.itemIndex); const swotItems = await compassStorage.getSwotItems(analysisId); const swotItem = swotItems[itemIndex]; if (!swotItem) return res.status(404).json({ error: "Item SWOT não encontrado" }); const typeLabels: Record = { strength: "Força", weakness: "Fraqueza", opportunity: "Oportunidade", threat: "Ameaça", }; const itemTitle = swotItem.title || swotItem.description.slice(0, 80); const priorityValue = swotItem.priorityLevel || "medium"; const workItem = await productionStorage.createWorkItem({ tenantId, projectId: req.body.projectId || null, title: `[${typeLabels[swotItem.type] || swotItem.type}] ${itemTitle}`, description: swotItem.description, type: swotItem.type === "weakness" || swotItem.type === "threat" ? "improvement" : "story", status: "backlog", priority: priorityValue, origin: "diagnostic_insight", originId: swotItem.id, originType: "swot_item", }); res.status(201).json(workItem); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/canvas-blocks/:id/to-work-item", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const blockId = parseInt(req.params.id); const block = await compassStorage.getCanvasBlock(blockId); if (!block) return res.status(404).json({ error: "Bloco Canvas não encontrado" }); const workItem = await productionStorage.createWorkItem({ tenantId, projectId: req.body.projectId || null, title: `[Canvas ${block.blockType}] ${block.content?.slice(0, 80) || "Item do Canvas"}`, description: block.content || "", type: "story", status: "backlog", priority: "medium", origin: "diagnostic_insight", originId: blockId, originType: "canvas_block", }); res.status(201).json(workItem); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/processes/:id/to-work-items", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const processId = parseInt(req.params.id); const process = await compassStorage.getProcess(processId); if (!process) return res.status(404).json({ error: "Processo não encontrado" }); const steps = await compassStorage.getProcessSteps(processId); const workItems = []; for (const step of steps) { if (step.status === "improvement_needed" || step.status === "critical") { const workItem = await productionStorage.createWorkItem({ tenantId, projectId: req.body.projectId || null, title: `[Melhoria de Processo] ${step.name}`, description: `Processo: ${process.name}\n\nEtapa: ${step.name}\n\nDescrição: ${step.description || ""}`, type: "improvement", status: "backlog", priority: step.status === "critical" ? "high" : "medium", origin: "diagnostic_insight", originId: step.id, originType: "process_step", }); workItems.push(workItem); } } res.status(201).json({ created: workItems.length, workItems }); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // ========== REPORT TEMPLATES ========== router.get("/report-templates", async (req: Request, res: Response) => { try { const templates = await compassStorage.getReportTemplates(); res.json(templates); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.get("/report-templates/:id", async (req: Request, res: Response) => { try { const template = await compassStorage.getReportTemplate(parseInt(req.params.id)); if (!template) return res.status(404).json({ error: "Template não encontrado" }); res.json(template); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/report-templates", async (req: Request, res: Response) => { try { const parsed = insertPcReportTemplateSchema.parse(req.body); const template = await compassStorage.createReportTemplate(parsed); res.status(201).json(template); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.patch("/report-templates/:id", async (req: Request, res: Response) => { try { const template = await compassStorage.updateReportTemplate(parseInt(req.params.id), req.body); if (!template) return res.status(404).json({ error: "Template não encontrado" }); res.json(template); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.delete("/report-templates/:id", async (req: Request, res: Response) => { try { const deleted = await compassStorage.deleteReportTemplate(parseInt(req.params.id)); if (!deleted) return res.status(404).json({ error: "Template não encontrado" }); res.status(204).send(); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // ========== REPORT CONFIGURATIONS ========== router.get("/report-configurations", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const projectId = req.query.projectId ? parseInt(req.query.projectId as string) : undefined; const configs = await compassStorage.getReportConfigurations(tenantId, projectId); res.json(configs); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.get("/report-configurations/:id", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const config = await compassStorage.getReportConfiguration(parseInt(req.params.id), tenantId); if (!config) return res.status(404).json({ error: "Configuração não encontrada" }); res.json(config); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/report-configurations", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const userId = (req.user as any).id; const parsed = insertPcReportConfigurationSchema.parse({ ...req.body, tenantId, createdById: userId }); const config = await compassStorage.createReportConfiguration(parsed); res.status(201).json(config); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.patch("/report-configurations/:id", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const config = await compassStorage.updateReportConfiguration(parseInt(req.params.id), tenantId, req.body); if (!config) return res.status(404).json({ error: "Configuração não encontrada" }); res.json(config); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.delete("/report-configurations/:id", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const deleted = await compassStorage.deleteReportConfiguration(parseInt(req.params.id), tenantId); if (!deleted) return res.status(404).json({ error: "Configuração não encontrada" }); res.status(204).send(); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // ========== GENERATED REPORTS ========== router.get("/generated-reports", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const projectId = req.query.projectId ? parseInt(req.query.projectId as string) : undefined; const reports = await compassStorage.getGeneratedReports(tenantId, projectId); res.json(reports); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.get("/generated-reports/:id", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const report = await compassStorage.getGeneratedReport(parseInt(req.params.id), tenantId); if (!report) return res.status(404).json({ error: "Relatório não encontrado" }); res.json(report); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/generated-reports", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const userId = (req.user as any).id; const { sections, ...body } = req.body; const metadata = { ...(body.metadata || {}), sections: sections || [] }; const parsed = insertPcGeneratedReportSchema.parse({ ...body, tenantId, generatedBy: userId, status: "pending", metadata }); const report = await compassStorage.createGeneratedReport(parsed); res.status(201).json(report); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.post("/generated-reports/:id/generate", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const reportId = parseInt(req.params.id); const report = await compassStorage.getGeneratedReport(reportId, tenantId); if (!report) return res.status(404).json({ error: "Relatório não encontrado" }); await compassStorage.updateGeneratedReport(reportId, tenantId, { status: "generating" }); const reportData = report.projectId ? await compassStorage.getProjectReportData(report.projectId) : null; if (!reportData) { await compassStorage.updateGeneratedReport(reportId, tenantId, { status: "failed" }); return res.status(404).json({ error: "Dados do projeto não encontrados" }); } // Get selected sections from metadata const sections = (report.metadata as any)?.sections || []; // Generate HTML content based on report type and selected sections let htmlContent = generateReportContent(report.reportType || "executive_summary", reportData, sections); await compassStorage.updateGeneratedReport(reportId, tenantId, { status: "completed", content: htmlContent, updatedAt: new Date(), }); const updatedReport = await compassStorage.getGeneratedReport(reportId, tenantId); res.json(updatedReport); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // Save edited report content router.patch("/generated-reports/:id/content", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const reportId = parseInt(req.params.id); const { content, name } = req.body; const updated = await compassStorage.updateGeneratedReport(reportId, tenantId, { content, name, updatedAt: new Date(), }); if (!updated) return res.status(404).json({ error: "Relatório não encontrado" }); res.json(updated); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // Helper function to generate report content function generateReportContent(reportType: string, data: any, sections: string[] = []): string { const project = data.project; const canvasBlocks = data.canvas || []; const swotAnalyses = data.swot || []; const processes = data.processes || []; const requirements = data.requirements || []; const pdcaCycles = data.pdca || []; // If no sections specified, include all for full_diagnostic const includedSections = sections.length > 0 ? sections : [ "project_info", "canvas_atual", "canvas_sistemico", "swot", "processes", "pdca", "requirements" ]; const shouldInclude = (section: string) => includedSections.includes(section); const blockTypeLabels: Record = { key_partners: "Parceiros-Chave", key_activities: "Atividades-Chave", key_resources: "Recursos-Chave", value_propositions: "Proposta de Valor", customer_relationships: "Relacionamento com Clientes", channels: "Canais", customer_segments: "Segmentos de Clientes", cost_structure: "Estrutura de Custos", revenue_streams: "Fontes de Receita", }; let html = `
`; // Cover Page html += `

${project?.name || "Projeto"}

${ reportType === "executive_summary" ? "Sumário Executivo" : reportType === "full_diagnostic" ? "Diagnóstico Completo" : reportType === "swot_report" ? "Análise SWOT" : "Relatório" }

Gerado em: ${new Date().toLocaleDateString('pt-BR')}

`; // Project Description if (shouldInclude("project_info") && project?.description) { html += `

Descrição do Projeto

${project.description}

`; } // Canvas Blocks (for all report types) - Show all 9 BMC blocks const allBlockTypes = [ "key_partners", "key_activities", "key_resources", "value_propositions", "customer_relationships", "channels", "customer_segments", "cost_structure", "revenue_streams", ]; // Group canvas blocks by level (intencao = atual, sistemico) const canvasByLevel: Record = {}; for (const block of canvasBlocks) { const level = block.level || "intencao"; if (!canvasByLevel[level]) canvasByLevel[level] = []; canvasByLevel[level].push(block); } // Canvas Atual (Intenção) const canvasAtual = canvasByLevel["intencao"] || []; if (shouldInclude("canvas_atual") && canvasAtual.length > 0) { html += `

Canvas BMC - Atual (Intenção)

`; for (const blockType of allBlockTypes) { const block = canvasAtual.find((b: any) => b.blockType === blockType); const hasContent = block && (block.content || block.title); html += `
${blockTypeLabels[blockType]}
${block ? ` ${block.title ? `

${block.title}

` : ""}

${block.content || 'Não preenchido'}

` : `

Não preenchido

`}
`; } html += `
`; } // Canvas Sistêmico (if exists) const canvasSistemico = canvasByLevel["sistemico"] || []; if (shouldInclude("canvas_sistemico") && canvasSistemico.length > 0) { html += `

Canvas BMC - Sistêmico

`; for (const blockType of allBlockTypes) { const block = canvasSistemico.find((b: any) => b.blockType === blockType); const hasContent = block && (block.content || block.title); html += `
${blockTypeLabels[blockType]}
${block ? ` ${block.title ? `

${block.title}

` : ""}

${block.content || 'Não preenchido'}

` : `

Não preenchido

`}
`; } html += `
`; } // SWOT Analysis if (shouldInclude("swot") && swotAnalyses.length > 0) { const swotTypeLabels: Record = { strength: { label: "Forças", color: "#22c55e" }, weakness: { label: "Fraquezas", color: "#ef4444" }, opportunity: { label: "Oportunidades", color: "#3b82f6" }, threat: { label: "Ameaças", color: "#f59e0b" }, }; html += `

Análise SWOT

`; for (const swot of swotAnalyses) { html += `

${swot.name || "Análise"}

${swot.description ? `

${swot.description}

` : ""} `; if (swot.items && swot.items.length > 0) { const groupedItems: Record = {}; for (const item of swot.items) { const type = item.itemType || "other"; if (!groupedItems[type]) groupedItems[type] = []; groupedItems[type].push(item); } html += `
`; for (const [type, items] of Object.entries(groupedItems)) { const typeInfo = swotTypeLabels[type] || { label: type, color: "#64748b" }; html += `
${typeInfo.label}
    ${items.map((i: any) => `
  • ${i.content || i.title || ""}
  • `).join("")}
`; } html += `
`; } html += `
`; } html += `
`; } // Processes if (shouldInclude("processes") && processes.length > 0) { html += `

Processos Mapeados

`; for (const process of processes) { html += `

${process.name}

${process.description ? `

${process.description}

` : ""}
`; } html += `
`; } // Requirements if (shouldInclude("requirements") && requirements.length > 0) { html += `

Requisitos

    `; for (const req of requirements) { html += `
  • ${req.title}: ${req.description || ""}
  • `; } html += `
`; } // PDCA Cycles if (shouldInclude("pdca") && pdcaCycles.length > 0) { const pdcaStatusLabels: Record = { plan: { label: "Planejar", color: "#3b82f6" }, do: { label: "Executar", color: "#f59e0b" }, check: { label: "Verificar", color: "#8b5cf6" }, act: { label: "Agir", color: "#22c55e" }, }; html += `

Ciclos PDCA

`; for (const cycle of pdcaCycles) { const statusInfo = pdcaStatusLabels[cycle.status] || { label: cycle.status, color: "#64748b" }; html += `

${cycle.title}

${statusInfo.label}
${cycle.description ? `

${cycle.description}

` : ""} ${cycle.actions && cycle.actions.length > 0 ? `

Ações:

    ${cycle.actions.map((a: any) => `
  • ${a.title || a.description || ""}
  • `).join("")}
` : ""}
`; } html += `
`; } // Footer html += `

Relatório gerado automaticamente pelo Arcádia Suite - Process Compass

`; return html; } router.delete("/generated-reports/:id", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const deleted = await compassStorage.deleteGeneratedReport(parseInt(req.params.id), tenantId); if (!deleted) return res.status(404).json({ error: "Relatório não encontrado" }); res.status(204).send(); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // ========== REPORT DATA AGGREGATION ========== router.get("/projects/:id/report-data", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const projectId = parseInt(req.params.id); const hasAccess = await validateProjectAccess(projectId, tenantId); if (!hasAccess) return res.status(403).json({ error: "Acesso negado ao projeto" }); const data = await compassStorage.getProjectReportData(projectId); if (!data) return res.status(404).json({ error: "Projeto não encontrado" }); res.json(data); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // ========== ERP ADHERENCE ========== router.get("/erp-modules", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const modules = await compassStorage.getErpModules(tenantId); res.json(modules); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/erp-modules", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const parsed = insertPcErpModuleSchema.parse({ ...req.body, tenantId }); const module = await compassStorage.createErpModule(parsed); res.status(201).json(module); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.patch("/erp-modules/:id", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const module = await compassStorage.updateErpModule(parseInt(req.params.id), tenantId, req.body); if (!module) return res.status(404).json({ error: "Módulo não encontrado" }); res.json(module); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.delete("/erp-modules/:id", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const deleted = await compassStorage.deleteErpModule(parseInt(req.params.id), tenantId); if (!deleted) return res.status(404).json({ error: "Módulo não encontrado" }); res.status(204).send(); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // ERP Requirements router.get("/projects/:projectId/erp-requirements", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const projectId = parseInt(req.params.projectId); if (!await validateProjectAccess(projectId, tenantId)) { return res.status(404).json({ error: "Projeto não encontrado" }); } const requirements = await compassStorage.getErpRequirements(projectId); res.json(requirements); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.get("/projects/:projectId/erp-adherence-stats", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const projectId = parseInt(req.params.projectId); if (!await validateProjectAccess(projectId, tenantId)) { return res.status(404).json({ error: "Projeto não encontrado" }); } const stats = await compassStorage.getErpAdherenceStats(projectId); res.json(stats); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/projects/:projectId/erp-requirements", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const projectId = parseInt(req.params.projectId); if (!await validateProjectAccess(projectId, tenantId)) { return res.status(404).json({ error: "Projeto não encontrado" }); } const parsed = insertPcErpRequirementSchema.parse({ ...req.body, tenantId, projectId }); const requirement = await compassStorage.createErpRequirement(parsed); res.status(201).json(requirement); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.patch("/erp-requirements/:id", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const requirement = await compassStorage.updateErpRequirement(parseInt(req.params.id), tenantId, req.body); if (!requirement) return res.status(404).json({ error: "Requisito não encontrado" }); res.json(requirement); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.delete("/erp-requirements/:id", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const deleted = await compassStorage.deleteErpRequirement(parseInt(req.params.id), tenantId); if (!deleted) return res.status(404).json({ error: "Requisito não encontrado" }); res.status(204).send(); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // ERP Parameterization Topics router.get("/projects/:projectId/erp-parameterization", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const projectId = parseInt(req.params.projectId); if (!await validateProjectAccess(projectId, tenantId)) { return res.status(404).json({ error: "Projeto não encontrado" }); } const topics = await compassStorage.getErpParameterizationTopics(projectId); const topicsWithItems = await Promise.all( topics.map(async (topic) => ({ ...topic, items: await compassStorage.getErpParameterizationItems(topic.id), })) ); res.json(topicsWithItems); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/projects/:projectId/erp-parameterization", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const projectId = parseInt(req.params.projectId); if (!await validateProjectAccess(projectId, tenantId)) { return res.status(404).json({ error: "Projeto não encontrado" }); } const parsed = insertPcErpParameterizationTopicSchema.parse({ ...req.body, tenantId, projectId }); const topic = await compassStorage.createErpParameterizationTopic(parsed); res.status(201).json(topic); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.patch("/erp-parameterization-topics/:id", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const topic = await compassStorage.updateErpParameterizationTopic(parseInt(req.params.id), tenantId, req.body); if (!topic) return res.status(404).json({ error: "Tópico não encontrado" }); res.json(topic); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.delete("/erp-parameterization-topics/:id", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const deleted = await compassStorage.deleteErpParameterizationTopic(parseInt(req.params.id), tenantId); if (!deleted) return res.status(404).json({ error: "Tópico não encontrado" }); res.status(204).send(); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // ERP Parameterization Items router.post("/erp-parameterization-topics/:topicId/items", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const parsed = insertPcErpParameterizationItemSchema.parse({ ...req.body, topicId: parseInt(req.params.topicId) }); const item = await compassStorage.createErpParameterizationItem(parsed); res.status(201).json(item); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.patch("/erp-parameterization-items/:id", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const item = await compassStorage.updateErpParameterizationItem(parseInt(req.params.id), tenantId, req.body); if (!item) return res.status(404).json({ error: "Item não encontrado" }); res.json(item); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.delete("/erp-parameterization-items/:id", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const deleted = await compassStorage.deleteErpParameterizationItem(parseInt(req.params.id), tenantId); if (!deleted) return res.status(404).json({ error: "Item não encontrado" }); res.status(204).send(); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // ========== PROJECT TEAM MEMBERS ========== router.get("/projects/:projectId/team", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const projectId = parseInt(req.params.projectId); const hasAccess = await validateProjectAccess(projectId, tenantId); if (!hasAccess) return res.status(403).json({ error: "Acesso negado ao projeto" }); const members = await compassStorage.getProjectTeamMembers(projectId); res.json(members); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/projects/:projectId/team", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const projectId = parseInt(req.params.projectId); const hasAccess = await validateProjectAccess(projectId, tenantId); if (!hasAccess) return res.status(403).json({ error: "Acesso negado ao projeto" }); const member = await compassStorage.addProjectTeamMember({ ...req.body, projectId }); res.status(201).json(member); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.delete("/team/:id", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const deleted = await compassStorage.removeProjectTeamMember(parseInt(req.params.id)); if (!deleted) return res.status(404).json({ error: "Membro não encontrado" }); res.status(204).send(); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // ========== PROJECT TASKS ========== router.get("/projects/:projectId/tasks", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const projectId = parseInt(req.params.projectId); const hasAccess = await validateProjectAccess(projectId, tenantId); if (!hasAccess) return res.status(403).json({ error: "Acesso negado ao projeto" }); const tasks = await compassStorage.getProjectTasks(projectId); res.json(tasks); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/projects/:projectId/tasks", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const projectId = parseInt(req.params.projectId); const hasAccess = await validateProjectAccess(projectId, tenantId); if (!hasAccess) return res.status(403).json({ error: "Acesso negado ao projeto" }); const task = await compassStorage.createProjectTask({ ...req.body, projectId, status: "pending" }); res.status(201).json(task); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.patch("/tasks/:id", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const task = await compassStorage.updateProjectTask(parseInt(req.params.id), req.body); if (!task) return res.status(404).json({ error: "Tarefa não encontrada" }); res.json(task); } catch (error: any) { res.status(400).json({ error: error.message }); } }); // ========== PROJECT FILES ========== import multer from "multer"; import path from "path"; import fs from "fs"; const uploadDir = path.join(process.cwd(), "uploads", "project-files"); if (!fs.existsSync(uploadDir)) { fs.mkdirSync(uploadDir, { recursive: true }); } const projectFileStorage = multer.diskStorage({ destination: (req, file, cb) => { cb(null, uploadDir); }, filename: (req, file, cb) => { const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1E9); cb(null, uniqueSuffix + "-" + file.originalname); } }); const projectFileUpload = multer({ storage: projectFileStorage, limits: { fileSize: 50 * 1024 * 1024 } }); router.get("/projects/:projectId/files", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const projectId = parseInt(req.params.projectId); const hasAccess = await validateProjectAccess(projectId, tenantId); if (!hasAccess) return res.status(403).json({ error: "Acesso negado ao projeto" }); const files = await compassStorage.getProjectFiles(projectId); res.json(files); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/projects/:projectId/files", projectFileUpload.single("file"), async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const projectId = parseInt(req.params.projectId); const hasAccess = await validateProjectAccess(projectId, tenantId); if (!hasAccess) return res.status(403).json({ error: "Acesso negado ao projeto" }); if (!req.file) return res.status(400).json({ error: "Nenhum arquivo enviado" }); const file = await compassStorage.createProjectFile({ projectId, name: req.file.filename, originalName: req.file.originalname, mimeType: req.file.mimetype, size: req.file.size, url: `/uploads/project-files/${req.file.filename}`, }); res.status(201).json(file); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.delete("/files/:id", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const file = await compassStorage.getProjectFile(parseInt(req.params.id)); if (file) { const filePath = path.join(uploadDir, file.name); if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); } } const deleted = await compassStorage.deleteProjectFile(parseInt(req.params.id)); if (!deleted) return res.status(404).json({ error: "Arquivo não encontrado" }); res.status(204).send(); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // ========== PROJECT HISTORY ========== router.get("/projects/:projectId/history", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const projectId = parseInt(req.params.projectId); const hasAccess = await validateProjectAccess(projectId, tenantId); if (!hasAccess) return res.status(403).json({ error: "Acesso negado ao projeto" }); const history = await compassStorage.getProjectHistory(projectId); res.json(history); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.put("/projects/:projectId/history", async (req: Request, res: Response) => { try { const tenantId = await getTenantId(req); if (!tenantId) return res.status(403).json({ error: "Tenant não encontrado" }); const projectId = parseInt(req.params.projectId); const hasAccess = await validateProjectAccess(projectId, tenantId); if (!hasAccess) return res.status(403).json({ error: "Acesso negado ao projeto" }); const history = await compassStorage.saveProjectHistory({ projectId, content: req.body.content }); res.json(history); } catch (error: any) { res.status(400).json({ error: error.message }); } }); export default router;