report route added
This commit is contained in:
parent
81dcfcd843
commit
9e785012fe
334
src/api/controllers/reportsController.js
Normal file
334
src/api/controllers/reportsController.js
Normal file
@ -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)
|
||||||
|
};
|
||||||
34
src/api/routes/reportsRoutes.js
Normal file
34
src/api/routes/reportsRoutes.js
Normal file
@ -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;
|
||||||
@ -12,6 +12,7 @@ const userRoutes = require('./api/routes/userRoutes');
|
|||||||
const authRoutes = require('./api/routes/authRoutes');
|
const authRoutes = require('./api/routes/authRoutes');
|
||||||
const integrationRoutes = require('./api/routes/integrationRoutes');
|
const integrationRoutes = require('./api/routes/integrationRoutes');
|
||||||
const bulkReadRoutes = require('./api/routes/bulkReadRoutes');
|
const bulkReadRoutes = require('./api/routes/bulkReadRoutes');
|
||||||
|
const reportsRoutes = require('./api/routes/reportsRoutes');
|
||||||
const sequelize = require('./db/pool');
|
const sequelize = require('./db/pool');
|
||||||
|
|
||||||
const app = express();
|
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}/users`, userRoutes);
|
||||||
app.use(`${config.app.apiPrefix}/integrations`, integrationRoutes);
|
app.use(`${config.app.apiPrefix}/integrations`, integrationRoutes);
|
||||||
app.use(`${config.app.apiPrefix}/bulk-read`, bulkReadRoutes);
|
app.use(`${config.app.apiPrefix}/bulk-read`, bulkReadRoutes);
|
||||||
|
app.use(`${config.app.apiPrefix}/reports`, reportsRoutes);
|
||||||
|
|
||||||
|
|
||||||
module.exports = app;
|
module.exports = app;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user