diff --git a/src/api/controllers/reportsController.js b/src/api/controllers/reportsController.js new file mode 100644 index 0000000..8b3a149 --- /dev/null +++ b/src/api/controllers/reportsController.js @@ -0,0 +1,334 @@ +const { Op } = require('sequelize'); +const ZohoLeadsBulk = require('../../data/models/zohoLeadsBulk'); +const ZohoTasksBulk = require('../../data/models/zohoTasksBulk'); +const { success, failure } = require('../../utils/response'); + +class ReportsController { + /** + * Get comprehensive CRM KPIs combining leads and tasks data + */ + async getCrmKPIs(req, res) { + try { + const { start_date, end_date, period, owner } = req.query; + const userId = req.user.uuid; + + // Build date filter + const dateFilter = this.buildDateFilter(start_date, end_date, period); + + // Build owner filter + const ownerFilter = owner ? { owner: { [Op.like]: `%${owner}%` } } : {}; + + // Base filters + const baseFilters = { + user_id: userId, + provider: 'zoho', + ...dateFilter, + ...ownerFilter + }; + + // Fetch data in parallel + const [leadsData, tasksData] = await Promise.all([ + ZohoLeadsBulk.findAll({ + where: baseFilters, + attributes: [ + 'lead_source', + 'lead_status', + 'owner', + 'created_time', + 'company' + ] + }), + ZohoTasksBulk.findAll({ + where: baseFilters, + attributes: [ + 'status', + 'priority', + 'owner', + 'created_time', + 'due_date', + 'subject' + ] + }) + ]); + + // Calculate KPIs + const kpis = this.calculateCrmKPIs(leadsData, tasksData); + + return res.json(success('CRM KPIs retrieved successfully', kpis)); + } catch (error) { + console.error('Error fetching CRM KPIs:', error); + return res.status(500).json(failure('Failed to fetch CRM KPIs', 'INTERNAL_ERROR')); + } + } + + /** + * Calculate CEO-level CRM KPIs focused on business statistics + */ + calculateCrmKPIs(leadsData, tasksData) { + const now = new Date(); + const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); + const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); + const ninetyDaysAgo = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000); + + // Business Growth Metrics + const totalLeads = leadsData.length; + const totalTasks = tasksData.length; + + // Lead Growth Trends + const leadsLast7Days = leadsData.filter(lead => + lead.created_time && new Date(lead.created_time) >= sevenDaysAgo + ).length; + const leadsLast30Days = leadsData.filter(lead => + lead.created_time && new Date(lead.created_time) >= thirtyDaysAgo + ).length; + const leadsLast90Days = leadsData.filter(lead => + lead.created_time && new Date(lead.created_time) >= ninetyDaysAgo + ).length; + + // Lead Quality Metrics + const qualifiedLeads = leadsData.filter(lead => + lead.lead_status && ( + lead.lead_status.toLowerCase().includes('qualified') || + lead.lead_status.toLowerCase().includes('hot') || + lead.lead_status.toLowerCase().includes('warm') + ) + ).length; + + const convertedLeads = leadsData.filter(lead => + lead.lead_status && lead.lead_status.toLowerCase().includes('converted') + ).length; + + // Lead Source Performance + const leadSourcePerformance = this.getLeadSourcePerformance(leadsData); + + // Business Process Efficiency + const completedTasks = tasksData.filter(task => + task.status && task.status.toLowerCase().includes('completed') + ).length; + + const highPriorityTasks = tasksData.filter(task => + task.priority && task.priority.toLowerCase().includes('high') + ).length; + + const overdueTasks = tasksData.filter(task => + task.due_date && new Date(task.due_date) < now && + task.status && !task.status.toLowerCase().includes('completed') + ).length; + + // Strategic Insights + const conversionRate = totalLeads > 0 ? (convertedLeads / totalLeads * 100).toFixed(2) : 0; + const qualificationRate = totalLeads > 0 ? (qualifiedLeads / totalLeads * 100).toFixed(2) : 0; + const taskCompletionRate = totalTasks > 0 ? (completedTasks / totalTasks * 100).toFixed(2) : 0; + const overdueRate = totalTasks > 0 ? (overdueTasks / totalTasks * 100).toFixed(2) : 0; + + // Monthly Growth Rate + const monthlyGrowthRate = this.calculateGrowthRate(leadsData, thirtyDaysAgo, ninetyDaysAgo); + + // Top Performing Lead Sources + const topLeadSources = Object.entries(leadSourcePerformance) + .map(([source, data]) => ({ + source: source || 'Unknown', + totalLeads: data.total, + qualifiedLeads: data.qualified, + conversionRate: data.total > 0 ? (data.converted / data.total * 100).toFixed(2) : 0, + avgQuality: data.total > 0 ? (data.qualified / data.total * 100).toFixed(2) : 0 + })) + .sort((a, b) => b.totalLeads - a.totalLeads) + .slice(0, 5); + + // Business Health Indicators + const businessHealth = this.calculateBusinessHealth(leadsData, tasksData, now); + + return { + businessOverview: { + totalLeads, + totalTasks, + leadsLast7Days, + leadsLast30Days, + leadsLast90Days, + monthlyGrowthRate: `${monthlyGrowthRate}%`, + conversionRate: `${conversionRate}%`, + qualificationRate: `${qualificationRate}%` + }, + leadQuality: { + qualifiedLeads, + convertedLeads, + leadSourcePerformance: topLeadSources, + statusDistribution: this.getDistribution(leadsData, 'lead_status') + }, + operationalEfficiency: { + completedTasks, + highPriorityTasks, + overdueTasks, + taskCompletionRate: `${taskCompletionRate}%`, + overdueRate: `${overdueRate}%`, + priorityDistribution: this.getDistribution(tasksData, 'priority') + }, + businessHealth: { + overallScore: businessHealth.overallScore, + leadGenerationHealth: businessHealth.leadGeneration, + processEfficiency: businessHealth.processEfficiency, + qualityMetrics: businessHealth.qualityMetrics + }, + strategicInsights: { + topPerformingSources: topLeadSources.slice(0, 3), + growthTrend: monthlyGrowthRate > 0 ? 'positive' : 'negative', + efficiencyTrend: taskCompletionRate > 80 ? 'excellent' : taskCompletionRate > 60 ? 'good' : 'needs_improvement', + qualityTrend: qualificationRate > 70 ? 'high' : qualificationRate > 50 ? 'medium' : 'low' + }, + generatedAt: new Date().toISOString() + }; + } + + /** + * Get distribution of values for a specific field + */ + getDistribution(data, field) { + const distribution = {}; + data.forEach(item => { + const value = item[field] || 'Unknown'; + distribution[value] = (distribution[value] || 0) + 1; + }); + return distribution; + } + + /** + * Get lead source performance metrics + */ + getLeadSourcePerformance(leadsData) { + const performance = {}; + leadsData.forEach(lead => { + const source = lead.lead_source || 'Unknown'; + if (!performance[source]) { + performance[source] = { total: 0, qualified: 0, converted: 0 }; + } + performance[source].total++; + + if (lead.lead_status && ( + lead.lead_status.toLowerCase().includes('qualified') || + lead.lead_status.toLowerCase().includes('hot') || + lead.lead_status.toLowerCase().includes('warm') + )) { + performance[source].qualified++; + } + + if (lead.lead_status && lead.lead_status.toLowerCase().includes('converted')) { + performance[source].converted++; + } + }); + return performance; + } + + /** + * Calculate monthly growth rate + */ + calculateGrowthRate(leadsData, thirtyDaysAgo, ninetyDaysAgo) { + const leadsLast30Days = leadsData.filter(lead => + lead.created_time && new Date(lead.created_time) >= thirtyDaysAgo + ).length; + + const leadsPrevious60Days = leadsData.filter(lead => + lead.created_time && + new Date(lead.created_time) >= ninetyDaysAgo && + new Date(lead.created_time) < thirtyDaysAgo + ).length; + + if (leadsPrevious60Days === 0) return 0; + + const growthRate = ((leadsLast30Days - leadsPrevious60Days) / leadsPrevious60Days) * 100; + return growthRate.toFixed(2); + } + + /** + * Calculate business health indicators + */ + calculateBusinessHealth(leadsData, tasksData, now) { + const totalLeads = leadsData.length; + const totalTasks = tasksData.length; + + // Lead Generation Health (0-100) + const recentLeads = leadsData.filter(lead => + lead.created_time && new Date(lead.created_time) >= new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000) + ).length; + const leadGenerationHealth = Math.min(100, (recentLeads / 10) * 100); // Assuming 10 leads/week is healthy + + // Process Efficiency (0-100) + const completedTasks = tasksData.filter(task => + task.status && task.status.toLowerCase().includes('completed') + ).length; + const processEfficiency = totalTasks > 0 ? (completedTasks / totalTasks) * 100 : 0; + + // Quality Metrics (0-100) + const qualifiedLeads = leadsData.filter(lead => + lead.lead_status && ( + lead.lead_status.toLowerCase().includes('qualified') || + lead.lead_status.toLowerCase().includes('hot') || + lead.lead_status.toLowerCase().includes('warm') + ) + ).length; + const qualityMetrics = totalLeads > 0 ? (qualifiedLeads / totalLeads) * 100 : 0; + + // Overall Score (weighted average) + const overallScore = Math.round( + (leadGenerationHealth * 0.4) + + (processEfficiency * 0.3) + + (qualityMetrics * 0.3) + ); + + return { + overallScore, + leadGeneration: Math.round(leadGenerationHealth), + processEfficiency: Math.round(processEfficiency), + qualityMetrics: Math.round(qualityMetrics) + }; + } + + /** + * Build date filter based on parameters + */ + buildDateFilter(start_date, end_date, period) { + if (start_date && end_date) { + return { + created_time: { + [Op.between]: [new Date(start_date), new Date(end_date)] + } + }; + } + + if (period) { + const now = new Date(); + let startDate; + + switch (period) { + case '7d': + startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); + break; + case '30d': + startDate = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); + break; + case '90d': + startDate = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000); + break; + case '1y': + startDate = new Date(now.getTime() - 365 * 24 * 60 * 60 * 1000); + break; + default: + return {}; + } + + return { + created_time: { + [Op.gte]: startDate + } + }; + } + + return {}; + } +} + +const reportsController = new ReportsController(); + +module.exports = { + getCrmKPIs: reportsController.getCrmKPIs.bind(reportsController) +}; diff --git a/src/api/routes/reportsRoutes.js b/src/api/routes/reportsRoutes.js new file mode 100644 index 0000000..9e7bb84 --- /dev/null +++ b/src/api/routes/reportsRoutes.js @@ -0,0 +1,34 @@ +const express = require('express'); +const Joi = require('joi'); +const auth = require('../middlewares/auth'); +const { getCrmKPIs } = require('../controllers/reportsController'); + +const router = express.Router(); + +// Validation middleware +const validate = (schema, property = 'query') => { + return (req, res, next) => { + const { error } = schema.validate(req[property]); + if (error) { + return res.status(400).json({ + status: 'error', + message: 'Validation error', + details: error.details[0].message + }); + } + next(); + }; +}; + +// Validation schema for CRM KPIs +const crmKPISchema = Joi.object({ + start_date: Joi.date().optional(), + end_date: Joi.date().optional(), + period: Joi.string().valid('7d', '30d', '90d', '1y', 'all').optional(), + owner: Joi.string().optional() +}); + +// Main CRM KPI route - combines leads and tasks data +router.get('/crm/kpis', auth, validate(crmKPISchema), getCrmKPIs); + +module.exports = router; diff --git a/src/app.js b/src/app.js index 9408d3c..2d17647 100644 --- a/src/app.js +++ b/src/app.js @@ -12,6 +12,7 @@ const userRoutes = require('./api/routes/userRoutes'); const authRoutes = require('./api/routes/authRoutes'); const integrationRoutes = require('./api/routes/integrationRoutes'); const bulkReadRoutes = require('./api/routes/bulkReadRoutes'); +const reportsRoutes = require('./api/routes/reportsRoutes'); const sequelize = require('./db/pool'); const app = express(); @@ -43,6 +44,7 @@ app.use(`${config.app.apiPrefix}/auth`, authRoutes); app.use(`${config.app.apiPrefix}/users`, userRoutes); app.use(`${config.app.apiPrefix}/integrations`, integrationRoutes); app.use(`${config.app.apiPrefix}/bulk-read`, bulkReadRoutes); +app.use(`${config.app.apiPrefix}/reports`, reportsRoutes); module.exports = app;