import { Router } from "express"; import { crmStorage } from "./storage"; import { whatsappBridge } from "./whatsapp-bridge"; import { communicationService } from "./communication"; whatsappBridge.init(communicationService); import { insertCrmPartnerSchema, insertCrmPartnerCertificationSchema, insertCrmPartnerPerformanceSchema, insertCrmContractSchema, insertCrmChannelSchema, insertCrmThreadSchema, insertCrmMessageSchema, insertCrmEventSchema, insertCrmCommissionRuleSchema, insertCrmProductSchema, insertCrmClientSchema, insertCrmPipelineStageSchema, insertCrmLeadSchema, insertCrmOpportunitySchema, insertCrmOpportunityProductSchema, insertCrmProposalSchema, insertCrmProposalItemSchema, insertCrmContractMilestoneSchema, insertCrmFrappeConnectorSchema, insertCrmFrappeMappingSchema, } from "@shared/schema"; import { createFrappeService } from "./frappe-service"; import { z } from "zod"; const router = Router(); function requireAuth(req: any, res: any, next: any) { if (!req.isAuthenticated()) { return res.status(401).json({ error: "Not authenticated" }); } next(); } router.get("/partners", requireAuth, async (req, res) => { try { const tenantId = req.query.tenantId ? Number(req.query.tenantId) : undefined; const partners = await crmStorage.getPartners(tenantId); res.json(partners); } catch (error) { res.status(500).json({ error: "Failed to fetch partners" }); } }); router.get("/partners/:id", requireAuth, async (req, res) => { try { const partner = await crmStorage.getPartner(Number(req.params.id)); if (!partner) { return res.status(404).json({ error: "Partner not found" }); } res.json(partner); } catch (error) { res.status(500).json({ error: "Failed to fetch partner" }); } }); router.post("/partners", requireAuth, async (req, res) => { try { const data = insertCrmPartnerSchema.parse(req.body); const partner = await crmStorage.createPartner(data); res.status(201).json(partner); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ error: error.errors }); } res.status(500).json({ error: "Failed to create partner" }); } }); router.patch("/partners/:id", requireAuth, async (req, res) => { try { const data = insertCrmPartnerSchema.partial().parse(req.body); const partner = await crmStorage.updatePartner(Number(req.params.id), data); if (!partner) { return res.status(404).json({ error: "Partner not found" }); } res.json(partner); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ error: error.errors }); } res.status(500).json({ error: "Failed to update partner" }); } }); router.delete("/partners/:id", requireAuth, async (req, res) => { try { const deleted = await crmStorage.deletePartner(Number(req.params.id)); if (!deleted) { return res.status(404).json({ error: "Partner not found" }); } res.status(204).send(); } catch (error) { res.status(500).json({ error: "Failed to delete partner" }); } }); router.get("/partners/:id/certifications", requireAuth, async (req, res) => { try { const certs = await crmStorage.getPartnerCertifications(Number(req.params.id)); res.json(certs); } catch (error) { res.status(500).json({ error: "Failed to fetch certifications" }); } }); router.post("/partners/:id/certifications", requireAuth, async (req, res) => { try { const data = insertCrmPartnerCertificationSchema.parse({ ...req.body, partnerId: Number(req.params.id) }); const cert = await crmStorage.createCertification(data); res.status(201).json(cert); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ error: error.errors }); } res.status(500).json({ error: "Failed to create certification" }); } }); router.get("/partners/:id/performance", requireAuth, async (req, res) => { try { const perf = await crmStorage.getPartnerPerformance(Number(req.params.id)); res.json(perf); } catch (error) { res.status(500).json({ error: "Failed to fetch performance" }); } }); router.post("/partners/:id/performance", requireAuth, async (req, res) => { try { const data = insertCrmPartnerPerformanceSchema.parse({ ...req.body, partnerId: Number(req.params.id) }); const perf = await crmStorage.createPerformance(data); res.status(201).json(perf); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ error: error.errors }); } res.status(500).json({ error: "Failed to create performance record" }); } }); router.get("/contracts", requireAuth, async (req, res) => { try { const tenantId = req.query.tenantId ? Number(req.query.tenantId) : undefined; const partnerId = req.query.partnerId ? Number(req.query.partnerId) : undefined; const clientId = req.query.clientId ? Number(req.query.clientId) : undefined; const contracts = await crmStorage.getContracts(tenantId, partnerId, clientId); res.json(contracts); } catch (error) { res.status(500).json({ error: "Failed to fetch contracts" }); } }); router.get("/contracts/:id", requireAuth, async (req, res) => { try { const contract = await crmStorage.getContract(Number(req.params.id)); if (!contract) { return res.status(404).json({ error: "Contract not found" }); } res.json(contract); } catch (error) { res.status(500).json({ error: "Failed to fetch contract" }); } }); router.post("/contracts", requireAuth, async (req, res) => { try { const data = insertCrmContractSchema.parse(req.body); const contract = await crmStorage.createContract(data); res.status(201).json(contract); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ error: error.errors }); } res.status(500).json({ error: "Failed to create contract" }); } }); router.patch("/contracts/:id", requireAuth, async (req, res) => { try { const data = insertCrmContractSchema.partial().parse(req.body); const contract = await crmStorage.updateContract(Number(req.params.id), data); if (!contract) { return res.status(404).json({ error: "Contract not found" }); } res.json(contract); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ error: error.errors }); } res.status(500).json({ error: "Failed to update contract" }); } }); router.get("/contracts/:id/revenue", requireAuth, async (req, res) => { try { const schedule = await crmStorage.getRevenueSchedule(Number(req.params.id)); res.json(schedule); } catch (error) { res.status(500).json({ error: "Failed to fetch revenue schedule" }); } }); router.get("/channels", requireAuth, async (req, res) => { try { const tenantId = req.query.tenantId ? Number(req.query.tenantId) : undefined; const channels = await crmStorage.getChannels(tenantId); res.json(channels); } catch (error) { res.status(500).json({ error: "Failed to fetch channels" }); } }); router.post("/channels", requireAuth, async (req, res) => { try { const data = insertCrmChannelSchema.parse(req.body); const channel = await crmStorage.createChannel(data); res.status(201).json(channel); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ error: error.errors }); } res.status(500).json({ error: "Failed to create channel" }); } }); router.patch("/channels/:id", requireAuth, async (req, res) => { try { const data = insertCrmChannelSchema.partial().parse(req.body); const channel = await crmStorage.updateChannel(Number(req.params.id), data); if (!channel) { return res.status(404).json({ error: "Channel not found" }); } res.json(channel); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ error: error.errors }); } res.status(500).json({ error: "Failed to update channel" }); } }); router.get("/threads", requireAuth, async (req, res) => { try { const tenantId = req.query.tenantId ? Number(req.query.tenantId) : undefined; const channelId = req.query.channelId ? Number(req.query.channelId) : undefined; const status = req.query.status as string | undefined; const threads = await crmStorage.getThreads(tenantId, channelId, status); res.json(threads); } catch (error) { res.status(500).json({ error: "Failed to fetch threads" }); } }); router.get("/threads/:id", requireAuth, async (req, res) => { try { const thread = await crmStorage.getThread(Number(req.params.id)); if (!thread) { return res.status(404).json({ error: "Thread not found" }); } res.json(thread); } catch (error) { res.status(500).json({ error: "Failed to fetch thread" }); } }); router.post("/threads", requireAuth, async (req, res) => { try { const data = insertCrmThreadSchema.parse(req.body); const thread = await crmStorage.createThread(data); res.status(201).json(thread); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ error: error.errors }); } res.status(500).json({ error: "Failed to create thread" }); } }); router.patch("/threads/:id", requireAuth, async (req, res) => { try { const data = insertCrmThreadSchema.partial().parse(req.body); const thread = await crmStorage.updateThread(Number(req.params.id), data); if (!thread) { return res.status(404).json({ error: "Thread not found" }); } res.json(thread); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ error: error.errors }); } res.status(500).json({ error: "Failed to update thread" }); } }); router.get("/threads/:id/messages", requireAuth, async (req, res) => { try { const limit = req.query.limit ? Number(req.query.limit) : 50; const messages = await crmStorage.getMessages(Number(req.params.id), limit); res.json(messages); } catch (error) { res.status(500).json({ error: "Failed to fetch messages" }); } }); router.post("/threads/:id/messages", requireAuth, async (req, res) => { try { const data = insertCrmMessageSchema.parse({ ...req.body, threadId: Number(req.params.id) }); const message = await crmStorage.createMessage(data); res.status(201).json(message); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ error: error.errors }); } res.status(500).json({ error: "Failed to create message" }); } }); router.get("/events", requireAuth, async (req, res) => { try { const userId = req.user!.id; const startDate = req.query.startDate ? new Date(req.query.startDate as string) : undefined; const endDate = req.query.endDate ? new Date(req.query.endDate as string) : undefined; const events = await crmStorage.getEvents(userId, startDate, endDate); const serializedEvents = events.map(e => ({ ...e, startAt: e.startAt instanceof Date ? e.startAt.toISOString() : e.startAt, endAt: e.endAt instanceof Date ? e.endAt.toISOString() : e.endAt, createdAt: e.createdAt instanceof Date ? e.createdAt.toISOString() : e.createdAt, updatedAt: e.updatedAt instanceof Date ? e.updatedAt.toISOString() : e.updatedAt, completedAt: e.completedAt instanceof Date ? e.completedAt.toISOString() : e.completedAt, })); res.json(serializedEvents); } catch (error) { res.status(500).json({ error: "Failed to fetch events" }); } }); router.post("/events", requireAuth, async (req, res) => { try { const data = insertCrmEventSchema.parse({ ...req.body, userId: req.user!.id }); const event = await crmStorage.createEvent(data); res.status(201).json(event); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ error: error.errors }); } res.status(500).json({ error: "Failed to create event" }); } }); router.patch("/events/:id", requireAuth, async (req, res) => { try { const data = insertCrmEventSchema.partial().parse(req.body); const event = await crmStorage.updateEvent(Number(req.params.id), data); if (!event) { return res.status(404).json({ error: "Event not found" }); } res.json(event); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ error: error.errors }); } res.status(500).json({ error: "Failed to update event" }); } }); router.delete("/events/:id", requireAuth, async (req, res) => { try { const deleted = await crmStorage.deleteEvent(Number(req.params.id)); if (!deleted) { return res.status(404).json({ error: "Event not found" }); } res.status(204).send(); } catch (error) { res.status(500).json({ error: "Failed to delete event" }); } }); router.get("/commission-rules", requireAuth, async (req, res) => { try { const rules = await crmStorage.getCommissionRules(); res.json(rules); } catch (error) { res.status(500).json({ error: "Failed to fetch commission rules" }); } }); router.post("/commission-rules", requireAuth, async (req, res) => { try { const data = insertCrmCommissionRuleSchema.parse(req.body); const rule = await crmStorage.createCommissionRule(data); res.status(201).json(rule); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ error: error.errors }); } res.status(500).json({ error: "Failed to create commission rule" }); } }); router.get("/commissions", requireAuth, async (req, res) => { try { const partnerId = req.query.partnerId ? Number(req.query.partnerId) : undefined; const userId = req.query.userId as string | undefined; const commissions = await crmStorage.getCommissions(partnerId, userId); res.json(commissions); } catch (error) { res.status(500).json({ error: "Failed to fetch commissions" }); } }); router.get("/stats", requireAuth, async (req, res) => { try { const tenantId = req.query.tenantId ? Number(req.query.tenantId) : undefined; const stats = await crmStorage.getStats(tenantId); res.json(stats); } catch (error) { res.status(500).json({ error: "Failed to fetch stats" }); } }); import { commissionEngine } from "./commission-engine"; router.post("/channels/:id/whatsapp/connect", requireAuth, async (req, res) => { try { const channelId = Number(req.params.id); const tenantId = req.body.tenantId || 1; const channelName = req.body.name || "WhatsApp Principal"; const channel = await communicationService.connectWhatsAppChannel(channelId, tenantId, channelName); res.json(channel); } catch (error: any) { res.status(500).json({ error: error.message || "Failed to connect WhatsApp" }); } }); router.post("/channels/whatsapp/new", requireAuth, async (req, res) => { try { const tenantId = req.body.tenantId || 1; const channelName = req.body.name || "WhatsApp Principal"; const channel = await communicationService.connectNewWhatsAppChannel(tenantId, channelName); res.json(channel); } catch (error: any) { res.status(500).json({ error: error.message || "Failed to create WhatsApp channel" }); } }); router.post("/channels/:id/whatsapp/disconnect", requireAuth, async (req, res) => { try { await communicationService.disconnectWhatsAppChannel(Number(req.params.id)); res.json({ success: true }); } catch (error: any) { res.status(500).json({ error: error.message || "Failed to disconnect WhatsApp" }); } }); router.post("/threads/:id/send", requireAuth, async (req, res) => { try { const { content, type } = req.body; if (!content) { return res.status(400).json({ error: "content is required" }); } const message = await communicationService.sendMessage( Number(req.params.id), content, req.user!.id, type || "text", "whatsapp" ); res.status(201).json(message); } catch (error: any) { res.status(500).json({ error: error.message || "Failed to send message" }); } }); router.post("/threads/:id/read", requireAuth, async (req, res) => { try { await communicationService.markThreadRead(Number(req.params.id)); res.json({ success: true }); } catch (error) { res.status(500).json({ error: "Failed to mark thread as read" }); } }); router.post("/threads/:id/assign", requireAuth, async (req, res) => { try { const { assignedToId } = req.body; if (!assignedToId) { return res.status(400).json({ error: "assignedToId is required" }); } await communicationService.assignThread(Number(req.params.id), assignedToId); res.json({ success: true }); } catch (error) { res.status(500).json({ error: "Failed to assign thread" }); } }); router.post("/threads/:id/close", requireAuth, async (req, res) => { try { await communicationService.closeThread(Number(req.params.id)); res.json({ success: true }); } catch (error) { res.status(500).json({ error: "Failed to close thread" }); } }); router.post("/threads/:id/reopen", requireAuth, async (req, res) => { try { await communicationService.reopenThread(Number(req.params.id)); res.json({ success: true }); } catch (error) { res.status(500).json({ error: "Failed to reopen thread" }); } }); router.get("/quick-messages", requireAuth, async (req, res) => { try { const tenantId = req.query.tenantId ? Number(req.query.tenantId) : undefined; const messages = await communicationService.getQuickMessages(tenantId, req.user!.id); res.json(messages); } catch (error) { res.status(500).json({ error: "Failed to fetch quick messages" }); } }); router.post("/quick-messages", requireAuth, async (req, res) => { try { const { tenantId, shortcut, title, content, category, isGlobal } = req.body; if (!shortcut || !title || !content) { return res.status(400).json({ error: "shortcut, title, and content are required" }); } const message = await communicationService.createQuickMessage( tenantId, req.user!.id, shortcut, title, content, category, isGlobal ); res.status(201).json(message); } catch (error) { res.status(500).json({ error: "Failed to create quick message" }); } }); router.get("/threads/stats", requireAuth, async (req, res) => { try { const tenantId = req.query.tenantId ? Number(req.query.tenantId) : undefined; const stats = await communicationService.getThreadStats(tenantId); res.json(stats); } catch (error) { res.status(500).json({ error: "Failed to fetch thread stats" }); } }); router.post("/commission-rules/seed", requireAuth, async (req, res) => { try { await commissionEngine.seedDefaultRules(); res.json({ success: true, message: "Default commission rules created" }); } catch (error) { res.status(500).json({ error: "Failed to seed commission rules" }); } }); router.post("/contracts/:id/process-commissions", requireAuth, async (req, res) => { try { const salesUserId = req.body.salesUserId || req.user!.id; const processed = await commissionEngine.processContractCommissions(Number(req.params.id), salesUserId); res.json({ success: true, processed }); } catch (error: any) { res.status(500).json({ error: error.message || "Failed to process commissions" }); } }); router.get("/commissions/summary", requireAuth, async (req, res) => { try { const partnerId = req.query.partnerId ? Number(req.query.partnerId) : undefined; const userId = req.query.userId as string | undefined; const startDate = req.query.startDate ? new Date(req.query.startDate as string) : undefined; const endDate = req.query.endDate ? new Date(req.query.endDate as string) : undefined; const summary = await commissionEngine.getCommissionSummary(partnerId, userId, startDate, endDate); res.json(summary); } catch (error) { res.status(500).json({ error: "Failed to fetch commission summary" }); } }); router.post("/commissions/:id/mark-paid", requireAuth, async (req, res) => { try { await commissionEngine.markCommissionPaid(Number(req.params.id)); res.json({ success: true }); } catch (error) { res.status(500).json({ error: "Failed to mark commission as paid" }); } }); router.post("/contracts/:id/extend-schedule", requireAuth, async (req, res) => { try { const monthsAhead = req.body.monthsAhead || 12; const schedules = await commissionEngine.extendRevenueSchedule(Number(req.params.id), monthsAhead); res.json({ success: true, schedules }); } catch (error: any) { res.status(500).json({ error: error.message || "Failed to extend schedule" }); } }); router.post("/contracts/extend-all", requireAuth, async (req, res) => { try { const monthsAhead = req.body.monthsAhead || 12; const extended = await commissionEngine.extendAllActiveContracts(monthsAhead); res.json({ success: true, extended }); } catch (error) { res.status(500).json({ error: "Failed to extend schedules" }); } }); import { googleCalendarService } from "./google-calendar"; router.get("/google/auth", requireAuth, (req, res) => { try { const authUrl = googleCalendarService.getAuthUrl(req.user!.id); res.json({ url: authUrl }); } catch (error: any) { res.status(500).json({ error: error.message || "Failed to get auth URL" }); } }); router.get("/google/callback", async (req, res) => { try { const { code, state } = req.query; if (!code || !state) { return res.status(400).send("Missing code or state"); } await googleCalendarService.handleCallback(code as string, state as string); res.redirect("/crm?google_connected=true"); } catch (error: any) { res.status(500).send(`Failed to connect: ${error.message}`); } }); router.get("/google/status", requireAuth, async (req, res) => { try { const connected = await googleCalendarService.isConnected(req.user!.id); res.json({ connected }); } catch (error) { res.status(500).json({ error: "Failed to check status" }); } }); router.post("/google/disconnect", requireAuth, async (req, res) => { try { await googleCalendarService.disconnect(req.user!.id); res.json({ success: true }); } catch (error) { res.status(500).json({ error: "Failed to disconnect" }); } }); router.post("/google/sync", requireAuth, async (req, res) => { try { const startDate = req.body.startDate ? new Date(req.body.startDate) : new Date(); const endDate = req.body.endDate ? new Date(req.body.endDate) : new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); await googleCalendarService.syncEvents(req.user!.id, startDate, endDate); res.json({ success: true }); } catch (error: any) { res.status(500).json({ error: error.message || "Failed to sync events" }); } }); router.get("/google/events", requireAuth, async (req, res) => { try { const startDate = req.query.startDate ? new Date(req.query.startDate as string) : undefined; const endDate = req.query.endDate ? new Date(req.query.endDate as string) : undefined; const events = await googleCalendarService.getEvents(req.user!.id, startDate, endDate); res.json(events); } catch (error) { res.status(500).json({ error: "Failed to fetch events" }); } }); router.post("/google/events", requireAuth, async (req, res) => { try { const data = insertCrmEventSchema.parse(req.body); const event = await crmStorage.createEvent({ ...data, userId: req.user!.id }); const isConnected = await googleCalendarService.isConnected(req.user!.id); if (isConnected) { const googleEventId = await googleCalendarService.createEvent(req.user!.id, event); if (googleEventId) { await crmStorage.updateEvent(event.id, { googleEventId }); } } res.status(201).json(event); } catch (error: any) { if (error.errors) { return res.status(400).json({ error: error.errors }); } res.status(500).json({ error: "Failed to create event" }); } }); router.put("/events/:id", requireAuth, async (req, res) => { try { const event = await crmStorage.updateEvent(Number(req.params.id), req.body); if (!event) { return res.status(404).json({ error: "Event not found" }); } if (event.googleEventId) { await googleCalendarService.updateEvent(req.user!.id, event.googleEventId, event); } res.json(event); } catch (error) { res.status(500).json({ error: "Failed to update event" }); } }); router.delete("/events/:id", requireAuth, async (req, res) => { try { const event = await crmStorage.getEvent(Number(req.params.id)); if (!event) { return res.status(404).json({ error: "Event not found" }); } if (event.googleEventId) { await googleCalendarService.deleteEvent(req.user!.id, event.googleEventId); } await crmStorage.deleteEvent(event.id); res.json({ success: true }); } catch (error) { res.status(500).json({ error: "Failed to delete event" }); } }); // ========== PRODUCTS ========== router.get("/products", requireAuth, async (req, res) => { try { const tenantId = req.query.tenantId ? Number(req.query.tenantId) : undefined; const products = await crmStorage.getProducts(tenantId); res.json(products); } catch (error) { res.status(500).json({ error: "Failed to fetch products" }); } }); router.get("/products/:id", requireAuth, async (req, res) => { try { const product = await crmStorage.getProduct(Number(req.params.id)); if (!product) return res.status(404).json({ error: "Product not found" }); res.json(product); } catch (error) { res.status(500).json({ error: "Failed to fetch product" }); } }); router.post("/products", requireAuth, async (req, res) => { try { const data = insertCrmProductSchema.parse(req.body); const product = await crmStorage.createProduct(data); res.status(201).json(product); } catch (error) { if (error instanceof z.ZodError) return res.status(400).json({ error: error.errors }); res.status(500).json({ error: "Failed to create product" }); } }); router.patch("/products/:id", requireAuth, async (req, res) => { try { const data = insertCrmProductSchema.partial().parse(req.body); const product = await crmStorage.updateProduct(Number(req.params.id), data); if (!product) return res.status(404).json({ error: "Product not found" }); res.json(product); } catch (error) { if (error instanceof z.ZodError) return res.status(400).json({ error: error.errors }); res.status(500).json({ error: "Failed to update product" }); } }); router.delete("/products/:id", requireAuth, async (req, res) => { try { const deleted = await crmStorage.deleteProduct(Number(req.params.id)); if (!deleted) return res.status(404).json({ error: "Product not found" }); res.status(204).send(); } catch (error) { res.status(500).json({ error: "Failed to delete product" }); } }); // ========== CLIENTS ========== router.get("/clients", requireAuth, async (req, res) => { try { const tenantId = req.query.tenantId ? Number(req.query.tenantId) : undefined; const user = req.user as any; const allowedModules = user?.allowedModules || user?.profile?.allowedModules || []; const isAdmin = user?.role === "admin" || allowedModules.includes("admin"); const partnerId = !isAdmin && user?.partnerId ? Number(user.partnerId) : undefined; const userId = !isAdmin && user?.partnerId ? user.id : undefined; const clients = await crmStorage.getClients(tenantId, partnerId, userId); res.json(clients); } catch (error) { res.status(500).json({ error: "Failed to fetch clients" }); } }); router.get("/clients/:id", requireAuth, async (req, res) => { try { const client = await crmStorage.getClient(Number(req.params.id)); if (!client) return res.status(404).json({ error: "Client not found" }); res.json(client); } catch (error) { res.status(500).json({ error: "Failed to fetch client" }); } }); router.post("/clients", requireAuth, async (req, res) => { try { const data = insertCrmClientSchema.parse(req.body); const client = await crmStorage.createClient(data); res.status(201).json(client); } catch (error) { if (error instanceof z.ZodError) return res.status(400).json({ error: error.errors }); res.status(500).json({ error: "Failed to create client" }); } }); router.patch("/clients/:id", requireAuth, async (req, res) => { try { const data = insertCrmClientSchema.partial().parse(req.body); const client = await crmStorage.updateClient(Number(req.params.id), data); if (!client) return res.status(404).json({ error: "Client not found" }); res.json(client); } catch (error) { if (error instanceof z.ZodError) return res.status(400).json({ error: error.errors }); res.status(500).json({ error: "Failed to update client" }); } }); router.delete("/clients/:id", requireAuth, async (req, res) => { try { const deleted = await crmStorage.deleteClient(Number(req.params.id)); if (!deleted) return res.status(404).json({ error: "Client not found" }); res.status(204).send(); } catch (error) { res.status(500).json({ error: "Failed to delete client" }); } }); router.post("/partners/:id/convert-to-client", requireAuth, async (req, res) => { try { const partnerId = Number(req.params.id); const additionalData = req.body || {}; const client = await crmStorage.convertPartnerToClient(partnerId, additionalData); if (!client) return res.status(404).json({ error: "Partner not found" }); res.status(201).json(client); } catch (error) { res.status(500).json({ error: "Failed to convert partner to client" }); } }); // ========== PIPELINE STAGES ========== router.get("/pipeline-stages", requireAuth, async (req, res) => { try { const tenantId = req.query.tenantId ? Number(req.query.tenantId) : undefined; const stages = await crmStorage.getPipelineStages(tenantId); res.json(stages); } catch (error) { res.status(500).json({ error: "Failed to fetch pipeline stages" }); } }); router.post("/pipeline-stages", requireAuth, async (req, res) => { try { const data = insertCrmPipelineStageSchema.parse(req.body); const stage = await crmStorage.createPipelineStage(data); res.status(201).json(stage); } catch (error) { if (error instanceof z.ZodError) return res.status(400).json({ error: error.errors }); res.status(500).json({ error: "Failed to create pipeline stage" }); } }); router.patch("/pipeline-stages/:id", requireAuth, async (req, res) => { try { const data = insertCrmPipelineStageSchema.partial().parse(req.body); const stage = await crmStorage.updatePipelineStage(Number(req.params.id), data); if (!stage) return res.status(404).json({ error: "Stage not found" }); res.json(stage); } catch (error) { if (error instanceof z.ZodError) return res.status(400).json({ error: error.errors }); res.status(500).json({ error: "Failed to update pipeline stage" }); } }); router.delete("/pipeline-stages/:id", requireAuth, async (req, res) => { try { const deleted = await crmStorage.deletePipelineStage(Number(req.params.id)); if (!deleted) return res.status(404).json({ error: "Stage not found" }); res.status(204).send(); } catch (error) { res.status(500).json({ error: "Failed to delete pipeline stage" }); } }); // ========== LEADS ========== router.get("/leads", requireAuth, async (req, res) => { try { const tenantId = req.query.tenantId ? Number(req.query.tenantId) : undefined; const status = req.query.status as string | undefined; const leads = await crmStorage.getLeads(tenantId, status); res.json(leads); } catch (error) { res.status(500).json({ error: "Failed to fetch leads" }); } }); router.get("/leads/:id", requireAuth, async (req, res) => { try { const lead = await crmStorage.getLead(Number(req.params.id)); if (!lead) return res.status(404).json({ error: "Lead not found" }); res.json(lead); } catch (error) { res.status(500).json({ error: "Failed to fetch lead" }); } }); router.post("/leads", requireAuth, async (req, res) => { try { const data = insertCrmLeadSchema.parse({ ...req.body, userId: req.user!.id }); const lead = await crmStorage.createLead(data); res.status(201).json(lead); } catch (error) { if (error instanceof z.ZodError) return res.status(400).json({ error: error.errors }); res.status(500).json({ error: "Failed to create lead" }); } }); router.patch("/leads/:id", requireAuth, async (req, res) => { try { const data = insertCrmLeadSchema.partial().parse(req.body); const lead = await crmStorage.updateLead(Number(req.params.id), data); if (!lead) return res.status(404).json({ error: "Lead not found" }); res.json(lead); } catch (error) { if (error instanceof z.ZodError) return res.status(400).json({ error: error.errors }); res.status(500).json({ error: "Failed to update lead" }); } }); router.delete("/leads/:id", requireAuth, async (req, res) => { try { const deleted = await crmStorage.deleteLead(Number(req.params.id)); if (!deleted) return res.status(404).json({ error: "Lead not found" }); res.status(204).send(); } catch (error) { res.status(500).json({ error: "Failed to delete lead" }); } }); router.post("/leads/:id/convert", requireAuth, async (req, res) => { try { const leadId = Number(req.params.id); const lead = await crmStorage.getLead(leadId); if (!lead) return res.status(404).json({ error: "Lead not found" }); const opportunityData = insertCrmOpportunitySchema.parse({ tenantId: lead.tenantId, userId: req.user!.id, name: req.body.name || `Oportunidade - ${lead.name}`, description: req.body.description || lead.notes, stageId: req.body.stageId, value: req.body.value || 0, expectedCloseDate: req.body.expectedCloseDate, assignedTo: lead.assignedTo || req.user!.id, }); const opportunity = await crmStorage.convertLeadToOpportunity(leadId, opportunityData); res.status(201).json(opportunity); } catch (error) { if (error instanceof z.ZodError) return res.status(400).json({ error: error.errors }); res.status(500).json({ error: "Failed to convert lead" }); } }); // ========== OPPORTUNITIES ========== router.get("/opportunities", requireAuth, async (req, res) => { try { const tenantId = req.query.tenantId ? Number(req.query.tenantId) : undefined; const stageId = req.query.stageId ? Number(req.query.stageId) : undefined; const status = req.query.status as string | undefined; const opportunities = await crmStorage.getOpportunities(tenantId, stageId, status); res.json(opportunities); } catch (error) { res.status(500).json({ error: "Failed to fetch opportunities" }); } }); router.get("/opportunities/:id", requireAuth, async (req, res) => { try { const opportunity = await crmStorage.getOpportunity(Number(req.params.id)); if (!opportunity) return res.status(404).json({ error: "Opportunity not found" }); res.json(opportunity); } catch (error) { res.status(500).json({ error: "Failed to fetch opportunity" }); } }); router.post("/opportunities", requireAuth, async (req, res) => { try { const data = insertCrmOpportunitySchema.parse({ ...req.body, userId: req.user!.id }); const opportunity = await crmStorage.createOpportunity(data); res.status(201).json(opportunity); } catch (error) { if (error instanceof z.ZodError) return res.status(400).json({ error: error.errors }); res.status(500).json({ error: "Failed to create opportunity" }); } }); router.patch("/opportunities/:id", requireAuth, async (req, res) => { try { const data = insertCrmOpportunitySchema.partial().parse(req.body); const opportunity = await crmStorage.updateOpportunity(Number(req.params.id), data); if (!opportunity) return res.status(404).json({ error: "Opportunity not found" }); res.json(opportunity); } catch (error) { if (error instanceof z.ZodError) return res.status(400).json({ error: error.errors }); res.status(500).json({ error: "Failed to update opportunity" }); } }); router.delete("/opportunities/:id", requireAuth, async (req, res) => { try { const deleted = await crmStorage.deleteOpportunity(Number(req.params.id)); if (!deleted) return res.status(404).json({ error: "Opportunity not found" }); res.status(204).send(); } catch (error) { res.status(500).json({ error: "Failed to delete opportunity" }); } }); router.patch("/opportunities/:id/stage", requireAuth, async (req, res) => { try { const { stageId } = req.body; if (!stageId) return res.status(400).json({ error: "stageId is required" }); const opportunity = await crmStorage.moveOpportunityToStage(Number(req.params.id), stageId); if (!opportunity) return res.status(404).json({ error: "Opportunity not found" }); res.json(opportunity); } catch (error) { res.status(500).json({ error: "Failed to move opportunity" }); } }); router.post("/opportunities/:id/won", requireAuth, async (req, res) => { try { const opportunity = await crmStorage.updateOpportunity(Number(req.params.id), { status: "won" }); if (!opportunity) return res.status(404).json({ error: "Opportunity not found" }); res.json(opportunity); } catch (error) { res.status(500).json({ error: "Failed to mark opportunity as won" }); } }); router.post("/opportunities/:id/lost", requireAuth, async (req, res) => { try { const { lossReason } = req.body; const opportunity = await crmStorage.updateOpportunity(Number(req.params.id), { status: "lost", lossReason }); if (!opportunity) return res.status(404).json({ error: "Opportunity not found" }); res.json(opportunity); } catch (error) { res.status(500).json({ error: "Failed to mark opportunity as lost" }); } }); // ========== OPPORTUNITY PRODUCTS ========== router.get("/opportunities/:id/products", requireAuth, async (req, res) => { try { const products = await crmStorage.getOpportunityProducts(Number(req.params.id)); res.json(products); } catch (error) { res.status(500).json({ error: "Failed to fetch opportunity products" }); } }); router.post("/opportunities/:id/products", requireAuth, async (req, res) => { try { const data = insertCrmOpportunityProductSchema.parse({ ...req.body, opportunityId: Number(req.params.id) }); const product = await crmStorage.addProductToOpportunity(data); res.status(201).json(product); } catch (error) { if (error instanceof z.ZodError) return res.status(400).json({ error: error.errors }); res.status(500).json({ error: "Failed to add product to opportunity" }); } }); router.delete("/opportunity-products/:id", requireAuth, async (req, res) => { try { const deleted = await crmStorage.removeProductFromOpportunity(Number(req.params.id)); if (!deleted) return res.status(404).json({ error: "Product not found" }); res.status(204).send(); } catch (error) { res.status(500).json({ error: "Failed to remove product from opportunity" }); } }); // ========== OPPORTUNITY APPROVAL & PROJECT CREATION ========== router.post("/opportunities/:id/approve", requireAuth, async (req, res) => { try { const opportunity = await crmStorage.getOpportunity(Number(req.params.id)); if (!opportunity) return res.status(404).json({ error: "Opportunity not found" }); if (opportunity.approvalStatus === "approved") { return res.status(400).json({ error: "Opportunity already approved" }); } const updated = await crmStorage.updateOpportunity(Number(req.params.id), { approvalStatus: "approved", approvedAt: new Date(), approvedBy: req.user!.id, status: "won" }); res.json(updated); } catch (error) { res.status(500).json({ error: "Failed to approve opportunity" }); } }); router.post("/opportunities/:id/reject", requireAuth, async (req, res) => { try { const { reason } = req.body; const opportunity = await crmStorage.getOpportunity(Number(req.params.id)); if (!opportunity) return res.status(404).json({ error: "Opportunity not found" }); const updated = await crmStorage.updateOpportunity(Number(req.params.id), { approvalStatus: "rejected", lossReason: reason, status: "lost" }); res.json(updated); } catch (error) { res.status(500).json({ error: "Failed to reject opportunity" }); } }); router.post("/opportunities/:id/open-project", requireAuth, async (req, res) => { try { const opportunity = await crmStorage.getOpportunity(Number(req.params.id)); if (!opportunity) return res.status(404).json({ error: "Opportunity not found" }); if (opportunity.approvalStatus !== "approved") { return res.status(400).json({ error: "Opportunity must be approved before opening a project" }); } if (opportunity.processCompassProjectId) { return res.status(400).json({ error: "Project already created for this opportunity" }); } const { tenantId, clientId } = req.body; if (!tenantId || !clientId) { return res.status(400).json({ error: "tenantId and clientId are required" }); } const project = await crmStorage.createProcessCompassProject({ tenantId, clientId, userId: req.user!.id, name: `Projeto: ${opportunity.name}`, description: opportunity.description || `Projeto originado da oportunidade #${opportunity.id}`, status: "backlog" }); const updated = await crmStorage.updateOpportunity(Number(req.params.id), { processCompassProjectId: project.id }); res.json({ opportunity: updated, project }); } catch (error) { console.error("Error creating project:", error); res.status(500).json({ error: "Failed to create project" }); } }); router.post("/opportunities/:id/bill", requireAuth, async (req, res) => { try { const opportunity = await crmStorage.getOpportunity(Number(req.params.id)); if (!opportunity) return res.status(404).json({ error: "Opportunity not found" }); if (opportunity.approvalStatus !== "approved") { return res.status(400).json({ error: "Opportunity must be approved before billing" }); } const updated = await crmStorage.updateOpportunity(Number(req.params.id), { billingStatus: "pending" }); res.json(updated); } catch (error) { res.status(500).json({ error: "Failed to update billing status" }); } }); // ========== CRM STATS ========== router.get("/stats/sales", requireAuth, async (req, res) => { try { const tenantId = req.query.tenantId ? Number(req.query.tenantId) : undefined; const stats = await crmStorage.getCrmStats(tenantId); res.json(stats); } catch (error) { res.status(500).json({ error: "Failed to fetch CRM stats" }); } }); // ========== Frappe Connector Routes ========== router.get("/frappe/connectors", requireAuth, async (req, res) => { try { const tenantId = req.query.tenantId ? Number(req.query.tenantId) : undefined; const connectors = await crmStorage.getFrappeConnectors(tenantId); const safeConnectors = connectors.map(({ apiKey, apiSecret, ...rest }) => ({ ...rest, hasCredentials: Boolean(apiKey && apiSecret), })); res.json(safeConnectors); } catch (error) { res.status(500).json({ error: "Failed to fetch Frappe connectors" }); } }); router.get("/frappe/connectors/:id", requireAuth, async (req, res) => { try { const connector = await crmStorage.getFrappeConnector(Number(req.params.id)); if (!connector) { return res.status(404).json({ error: "Connector not found" }); } const { apiKey, apiSecret, ...safeConnector } = connector; res.json({ ...safeConnector, hasCredentials: Boolean(apiKey && apiSecret) }); } catch (error) { res.status(500).json({ error: "Failed to fetch connector" }); } }); router.post("/frappe/connectors", requireAuth, async (req, res) => { try { const data = insertCrmFrappeConnectorSchema.parse(req.body); const connector = await crmStorage.createFrappeConnector(data); const { apiKey, apiSecret, ...safeConnector } = connector; res.status(201).json({ ...safeConnector, hasCredentials: true }); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ error: error.errors }); } res.status(500).json({ error: "Failed to create connector" }); } }); router.patch("/frappe/connectors/:id", requireAuth, async (req, res) => { try { const data = insertCrmFrappeConnectorSchema.partial().parse(req.body); const connector = await crmStorage.updateFrappeConnector(Number(req.params.id), data); if (!connector) { return res.status(404).json({ error: "Connector not found" }); } const { apiKey, apiSecret, ...safeConnector } = connector; res.json({ ...safeConnector, hasCredentials: Boolean(apiKey && apiSecret) }); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ error: error.errors }); } res.status(500).json({ error: "Failed to update connector" }); } }); router.delete("/frappe/connectors/:id", requireAuth, async (req, res) => { try { const deleted = await crmStorage.deleteFrappeConnector(Number(req.params.id)); if (!deleted) { return res.status(404).json({ error: "Connector not found" }); } res.status(204).send(); } catch (error) { res.status(500).json({ error: "Failed to delete connector" }); } }); router.post("/frappe/connectors/:id/test", requireAuth, async (req, res) => { try { const frappeService = await createFrappeService(Number(req.params.id)); if (!frappeService) { return res.status(404).json({ error: "Connector not found" }); } const result = await frappeService.testConnection(); await crmStorage.updateFrappeConnector(Number(req.params.id), { status: result.success ? "active" : "error", errorMessage: result.error || null, }); res.json(result); } catch (error: any) { res.status(500).json({ success: false, error: error.message }); } }); router.post("/frappe/connectors/:id/sync", requireAuth, async (req, res) => { try { const connectorId = Number(req.params.id); const { entities } = req.body; const frappeService = await createFrappeService(connectorId); if (!frappeService) { return res.status(404).json({ error: "Connector not found" }); } const syncLog = await crmStorage.createSyncLog({ connectorId, syncType: "push", status: "running", }); const results: Record = {}; let totalSuccess = 0; let totalFailed = 0; const allErrors: string[] = []; if (!entities || entities.includes("leads")) { const leads = await crmStorage.getLeads(); const leadResult = await frappeService.syncLeadsToFrappe(leads); results.leads = leadResult; totalSuccess += leadResult.success; totalFailed += leadResult.failed; allErrors.push(...leadResult.errors); } if (!entities || entities.includes("opportunities")) { const opportunities = await crmStorage.getOpportunities(); const oppResult = await frappeService.syncOpportunitiesToFrappe(opportunities); results.opportunities = oppResult; totalSuccess += oppResult.success; totalFailed += oppResult.failed; allErrors.push(...oppResult.errors); } if (!entities || entities.includes("products")) { const products = await crmStorage.getProducts(); const productResult = await frappeService.syncProductsToFrappe(products); results.products = productResult; totalSuccess += productResult.success; totalFailed += productResult.failed; allErrors.push(...productResult.errors); } if (!entities || entities.includes("partners")) { const partners = await crmStorage.getPartners(); const partnerResult = await frappeService.syncPartnersToFrappe(partners); results.partners = partnerResult; totalSuccess += partnerResult.success; totalFailed += partnerResult.failed; allErrors.push(...partnerResult.errors); } await crmStorage.updateSyncLog(syncLog.id, { status: totalFailed > 0 ? "completed_with_errors" : "completed", recordsProcessed: totalSuccess + totalFailed, recordsSuccess: totalSuccess, recordsFailed: totalFailed, errorDetails: allErrors.length > 0 ? allErrors.join("\n") : null, completedAt: new Date(), }); await crmStorage.updateFrappeConnector(connectorId, { lastSyncAt: new Date(), }); res.json({ success: true, results, totalSuccess, totalFailed }); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.get("/frappe/connectors/:id/logs", requireAuth, async (req, res) => { try { const logs = await crmStorage.getSyncLogs(Number(req.params.id)); res.json(logs); } catch (error) { res.status(500).json({ error: "Failed to fetch sync logs" }); } }); router.get("/frappe/connectors/:id/mappings", requireAuth, async (req, res) => { try { const mappings = await crmStorage.getFrappeMappings(Number(req.params.id)); res.json(mappings); } catch (error) { res.status(500).json({ error: "Failed to fetch mappings" }); } }); router.post("/frappe/connectors/:id/mappings", requireAuth, async (req, res) => { try { const data = insertCrmFrappeMappingSchema.parse({ ...req.body, connectorId: Number(req.params.id), }); const mapping = await crmStorage.createFrappeMapping(data); res.status(201).json(mapping); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ error: error.errors }); } res.status(500).json({ error: "Failed to create mapping" }); } }); // ========== PROPOSALS (PROPOSTAS COMERCIAIS) ========== router.get("/proposals", requireAuth, async (req, res) => { try { const tenantId = req.headers["x-tenant-id"] ? Number(req.headers["x-tenant-id"]) : 1; const proposals = await crmStorage.getProposals(tenantId); res.json(proposals); } catch (error) { res.status(500).json({ error: "Failed to fetch proposals" }); } }); router.get("/proposals/:id", requireAuth, async (req, res) => { try { const tenantId = req.headers["x-tenant-id"] ? Number(req.headers["x-tenant-id"]) : 1; const proposal = await crmStorage.getProposal(Number(req.params.id), tenantId); if (!proposal) { return res.status(404).json({ error: "Proposal not found" }); } res.json(proposal); } catch (error) { res.status(500).json({ error: "Failed to fetch proposal" }); } }); router.get("/opportunities/:id/proposals", requireAuth, async (req, res) => { try { const proposals = await crmStorage.getProposalsByOpportunity(Number(req.params.id)); res.json(proposals); } catch (error) { res.status(500).json({ error: "Failed to fetch proposals" }); } }); router.post("/proposals", requireAuth, async (req, res) => { try { const tenantId = req.headers["x-tenant-id"] ? Number(req.headers["x-tenant-id"]) : 1; const userId = (req.user as any)?.id; const data = insertCrmProposalSchema.parse({ ...req.body, tenantId, createdById: userId }); const proposal = await crmStorage.createProposal(data); res.status(201).json(proposal); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ error: error.errors }); } res.status(500).json({ error: "Failed to create proposal" }); } }); router.patch("/proposals/:id", requireAuth, async (req, res) => { try { const tenantId = req.headers["x-tenant-id"] ? Number(req.headers["x-tenant-id"]) : 1; const proposal = await crmStorage.updateProposal(Number(req.params.id), tenantId, req.body); if (!proposal) { return res.status(404).json({ error: "Proposal not found" }); } res.json(proposal); } catch (error) { res.status(500).json({ error: "Failed to update proposal" }); } }); router.delete("/proposals/:id", requireAuth, async (req, res) => { try { const tenantId = req.headers["x-tenant-id"] ? Number(req.headers["x-tenant-id"]) : 1; const deleted = await crmStorage.deleteProposal(Number(req.params.id), tenantId); if (!deleted) { return res.status(404).json({ error: "Proposal not found" }); } res.status(204).send(); } catch (error) { res.status(500).json({ error: "Failed to delete proposal" }); } }); // ========== PROPOSAL ITEMS ========== router.get("/proposals/:id/items", requireAuth, async (req, res) => { try { const items = await crmStorage.getProposalItems(Number(req.params.id)); res.json(items); } catch (error) { res.status(500).json({ error: "Failed to fetch proposal items" }); } }); router.post("/proposals/:id/items", requireAuth, async (req, res) => { try { const data = insertCrmProposalItemSchema.parse({ ...req.body, proposalId: Number(req.params.id) }); const item = await crmStorage.createProposalItem(data); res.status(201).json(item); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ error: error.errors }); } res.status(500).json({ error: "Failed to create proposal item" }); } }); router.patch("/proposal-items/:id", requireAuth, async (req, res) => { try { const item = await crmStorage.updateProposalItem(Number(req.params.id), req.body); if (!item) { return res.status(404).json({ error: "Proposal item not found" }); } res.json(item); } catch (error) { res.status(500).json({ error: "Failed to update proposal item" }); } }); router.delete("/proposal-items/:id", requireAuth, async (req, res) => { try { const deleted = await crmStorage.deleteProposalItem(Number(req.params.id)); if (!deleted) { return res.status(404).json({ error: "Proposal item not found" }); } res.status(204).send(); } catch (error) { res.status(500).json({ error: "Failed to delete proposal item" }); } }); // ========== CONTRACT MILESTONES ========== router.get("/contracts/:id/milestones", requireAuth, async (req, res) => { try { const milestones = await crmStorage.getContractMilestones(Number(req.params.id)); res.json(milestones); } catch (error) { res.status(500).json({ error: "Failed to fetch contract milestones" }); } }); router.post("/contracts/:id/milestones", requireAuth, async (req, res) => { try { const data = insertCrmContractMilestoneSchema.parse({ ...req.body, contractId: Number(req.params.id) }); const milestone = await crmStorage.createContractMilestone(data); res.status(201).json(milestone); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ error: error.errors }); } res.status(500).json({ error: "Failed to create milestone" }); } }); router.patch("/milestones/:id", requireAuth, async (req, res) => { try { const milestone = await crmStorage.updateContractMilestone(Number(req.params.id), req.body); if (!milestone) { return res.status(404).json({ error: "Milestone not found" }); } res.json(milestone); } catch (error) { res.status(500).json({ error: "Failed to update milestone" }); } }); router.delete("/milestones/:id", requireAuth, async (req, res) => { try { const deleted = await crmStorage.deleteContractMilestone(Number(req.params.id)); if (!deleted) { return res.status(404).json({ error: "Milestone not found" }); } res.status(204).send(); } catch (error) { res.status(500).json({ error: "Failed to delete milestone" }); } }); export default router;