Centralized_Reporting_Backend/src/api/controllers/reportsController.js
2025-09-25 19:02:08 +05:30

821 lines
28 KiB
JavaScript

const { Op } = require('sequelize');
const ZohoLeadsBulk = require('../../data/models/zohoLeadsBulk');
const ZohoTasksBulk = require('../../data/models/zohoTasksBulk');
const ZohoContactsBulk = require('../../data/models/zohoContactsBulk');
const ZohoAccountsBulk = require('../../data/models/zohoAccountsBulk');
const ZohoDealsBulk = require('../../data/models/zohoDealsBulk');
const ZohoVendorsBulk = require('../../data/models/zohoVendorsBulk');
const ZohoInvoicesBulk = require('../../data/models/zohoInvoicesBulk');
const ZohoSalesOrdersBulk = require('../../data/models/zohoSalesOrdersBulk');
const ZohoPurchaseOrdersBulk = require('../../data/models/zohoPurchaseOrdersBulk');
const { success, failure } = require('../../utils/response');
class ReportsController {
/**
* Get comprehensive CRM KPIs combining all CRM modules 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_uuid: userId,
provider: 'zoho',
...dateFilter,
...ownerFilter
};
// Fetch data from all CRM modules in parallel
const [
leadsData,
tasksData,
contactsData,
accountsData,
dealsData,
vendorsData,
invoicesData,
salesOrdersData,
purchaseOrdersData
] = await Promise.all([
ZohoLeadsBulk.findAll({
where: baseFilters,
attributes: ['lead_source', 'lead_status', 'owner', 'created_time', 'company', 'email', 'phone']
}),
ZohoTasksBulk.findAll({
where: baseFilters,
attributes: ['status', 'priority', 'owner', 'created_time', 'due_date', 'subject', 'what_id']
}),
ZohoContactsBulk.findAll({
where: baseFilters,
attributes: ['first_name', 'last_name', 'email', 'phone', 'lead_source', 'account_name', 'owner', 'created_time']
}),
ZohoAccountsBulk.findAll({
where: baseFilters,
attributes: ['account_name', 'phone', 'website', 'industry', 'ownership', 'annual_revenue', 'owner', 'created_time']
}),
ZohoDealsBulk.findAll({
where: baseFilters,
attributes: ['deal_name', 'stage', 'amount', 'closing_date', 'account_name', 'contact_name', 'probability', 'lead_source', 'owner', 'created_time']
}),
ZohoVendorsBulk.findAll({
where: baseFilters,
attributes: ['vendor_name', 'email', 'phone', 'website', 'owner', 'created_time']
}),
ZohoInvoicesBulk.findAll({
where: baseFilters,
attributes: ['invoice_number', 'invoice_date', 'due_date', 'status', 'total', 'account_name', 'owner', 'created_time']
}),
ZohoSalesOrdersBulk.findAll({
where: baseFilters,
attributes: ['subject', 'status', 'due_date', 'total', 'account_name', 'owner', 'created_time']
}),
ZohoPurchaseOrdersBulk.findAll({
where: baseFilters,
attributes: ['subject', 'vendor_name', 'status', 'due_date', 'total', 'owner', 'created_time']
})
]);
// Calculate comprehensive KPIs
const kpis = this.calculateComprehensiveCrmKPIs({
leads: leadsData,
tasks: tasksData,
contacts: contactsData,
accounts: accountsData,
deals: dealsData,
vendors: vendorsData,
invoices: invoicesData,
salesOrders: salesOrdersData,
purchaseOrders: purchaseOrdersData
});
return res.json(success('Comprehensive 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 comprehensive CRM KPIs from all modules
*/
calculateComprehensiveCrmKPIs(data) {
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);
// Module counts
const moduleCounts = {
leads: data.leads.length,
contacts: data.contacts.length,
accounts: data.accounts.length,
deals: data.deals.length,
tasks: data.tasks.length,
vendors: data.vendors.length,
invoices: data.invoices.length,
salesOrders: data.salesOrders.length,
purchaseOrders: data.purchaseOrders.length
};
// Revenue and financial metrics
const revenueMetrics = this.calculateRevenueMetrics(data);
// Lead and sales pipeline metrics
const pipelineMetrics = this.calculatePipelineMetrics(data, thirtyDaysAgo, sevenDaysAgo);
// Operational efficiency metrics
const operationalMetrics = this.calculateOperationalMetrics(data, now);
// Customer and vendor metrics
const customerMetrics = this.calculateCustomerMetrics(data, thirtyDaysAgo);
// Business growth trends
const growthMetrics = this.calculateGrowthMetrics(data, thirtyDaysAgo, ninetyDaysAgo);
// Top performers and insights
const insights = this.calculateBusinessInsights(data, revenueMetrics, pipelineMetrics);
return {
businessOverview: {
totalRecords: Object.values(moduleCounts).reduce((sum, count) => sum + count, 0),
moduleCounts,
revenueMetrics,
growthMetrics
},
salesPipeline: {
...pipelineMetrics,
dealStages: this.getDistribution(data.deals, 'stage'),
leadSources: this.getDistribution(data.leads, 'lead_source'),
conversionFunnel: this.calculateConversionFunnel(data)
},
operationalEfficiency: {
...operationalMetrics,
taskStatus: this.getDistribution(data.tasks, 'status'),
invoiceStatus: this.getDistribution(data.invoices, 'status'),
orderStatus: {
salesOrders: this.getDistribution(data.salesOrders, 'status'),
purchaseOrders: this.getDistribution(data.purchaseOrders, 'status')
}
},
customerRelationships: {
...customerMetrics,
industryDistribution: this.getDistribution(data.accounts, 'industry'),
contactSources: this.getDistribution(data.contacts, 'lead_source'),
accountOwnership: this.getDistribution(data.accounts, 'owner')
},
financialHealth: {
totalRevenue: revenueMetrics.totalRevenue,
totalInvoices: revenueMetrics.totalInvoices,
averageInvoiceValue: revenueMetrics.averageInvoiceValue,
overdueAmount: revenueMetrics.overdueAmount,
revenueByMonth: revenueMetrics.revenueByMonth,
topRevenueAccounts: revenueMetrics.topRevenueAccounts
},
businessInsights: {
...insights,
recommendations: this.generateRecommendations(data, revenueMetrics, pipelineMetrics, operationalMetrics)
},
generatedAt: new Date().toISOString()
};
}
/**
* Calculate CEO-level CRM KPIs focused on business statistics (Legacy method)
*/
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)
};
}
/**
* Calculate revenue metrics from invoices and deals
*/
calculateRevenueMetrics(data) {
const totalRevenue = data.invoices.reduce((sum, invoice) =>
sum + (parseFloat(invoice.total) || 0), 0);
const totalDealValue = data.deals.reduce((sum, deal) =>
sum + (parseFloat(deal.amount) || 0), 0);
const overdueInvoices = data.invoices.filter(invoice => {
if (!invoice.due_date) return false;
return new Date(invoice.due_date) < new Date() &&
invoice.status && !invoice.status.toLowerCase().includes('paid');
});
const overdueAmount = overdueInvoices.reduce((sum, invoice) =>
sum + (parseFloat(invoice.total) || 0), 0);
const averageInvoiceValue = data.invoices.length > 0 ?
totalRevenue / data.invoices.length : 0;
// Revenue by month (last 12 months)
const revenueByMonth = this.calculateRevenueByMonth(data.invoices);
// Top revenue accounts
const accountRevenue = {};
data.invoices.forEach(invoice => {
const account = invoice.account_name || 'Unknown';
const amount = parseFloat(invoice.total) || 0;
accountRevenue[account] = (accountRevenue[account] || 0) + amount;
});
const topRevenueAccounts = Object.entries(accountRevenue)
.map(([account, revenue]) => ({ account, revenue }))
.sort((a, b) => b.revenue - a.revenue)
.slice(0, 10);
return {
totalRevenue,
totalDealValue,
totalInvoices: data.invoices.length,
averageInvoiceValue,
overdueAmount,
overdueCount: overdueInvoices.length,
revenueByMonth,
topRevenueAccounts
};
}
/**
* Calculate pipeline metrics from leads and deals
*/
calculatePipelineMetrics(data, thirtyDaysAgo, sevenDaysAgo) {
const totalLeads = data.leads.length;
const totalDeals = data.deals.length;
const qualifiedLeads = data.leads.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 = data.leads.filter(lead =>
lead.lead_status && lead.lead_status.toLowerCase().includes('converted')
).length;
const openDeals = data.deals.filter(deal =>
deal.stage && !deal.stage.toLowerCase().includes('closed')
);
const closedWonDeals = data.deals.filter(deal =>
deal.stage && deal.stage.toLowerCase().includes('closed won')
);
const totalPipelineValue = openDeals.reduce((sum, deal) =>
sum + (parseFloat(deal.amount) || 0), 0);
const conversionRate = totalLeads > 0 ? (convertedLeads / totalLeads * 100).toFixed(2) : 0;
const qualificationRate = totalLeads > 0 ? (qualifiedLeads / totalLeads * 100).toFixed(2) : 0;
const winRate = totalDeals > 0 ? (closedWonDeals.length / totalDeals * 100).toFixed(2) : 0;
return {
totalLeads,
totalDeals,
qualifiedLeads,
convertedLeads,
openDeals: openDeals.length,
closedWonDeals: closedWonDeals.length,
totalPipelineValue,
conversionRate: `${conversionRate}%`,
qualificationRate: `${qualificationRate}%`,
winRate: `${winRate}%`
};
}
/**
* Calculate operational efficiency metrics
*/
calculateOperationalMetrics(data, now) {
const totalTasks = data.tasks.length;
const completedTasks = data.tasks.filter(task =>
task.status && task.status.toLowerCase().includes('completed')
).length;
const overdueTasks = data.tasks.filter(task =>
task.due_date && new Date(task.due_date) < now &&
task.status && !task.status.toLowerCase().includes('completed')
).length;
const highPriorityTasks = data.tasks.filter(task =>
task.priority && task.priority.toLowerCase().includes('high')
).length;
const taskCompletionRate = totalTasks > 0 ? (completedTasks / totalTasks * 100).toFixed(2) : 0;
const overdueRate = totalTasks > 0 ? (overdueTasks / totalTasks * 100).toFixed(2) : 0;
return {
totalTasks,
completedTasks,
overdueTasks,
highPriorityTasks,
taskCompletionRate: `${taskCompletionRate}%`,
overdueRate: `${overdueRate}%`
};
}
/**
* Calculate customer relationship metrics
*/
calculateCustomerMetrics(data, thirtyDaysAgo) {
const totalContacts = data.contacts.length;
const totalAccounts = data.accounts.length;
const totalVendors = data.vendors.length;
const recentContacts = data.contacts.filter(contact =>
contact.created_time && new Date(contact.created_time) >= thirtyDaysAgo
).length;
const accountsWithRevenue = new Set(data.invoices.map(invoice => invoice.account_name)).size;
const contactToAccountRatio = totalAccounts > 0 ? (totalContacts / totalAccounts).toFixed(2) : 0;
return {
totalContacts,
totalAccounts,
totalVendors,
recentContacts,
accountsWithRevenue,
contactToAccountRatio
};
}
/**
* Calculate growth metrics
*/
calculateGrowthMetrics(data, thirtyDaysAgo, ninetyDaysAgo) {
const recentLeads = data.leads.filter(lead =>
lead.created_time && new Date(lead.created_time) >= thirtyDaysAgo
).length;
const previousLeads = data.leads.filter(lead =>
lead.created_time &&
new Date(lead.created_time) >= ninetyDaysAgo &&
new Date(lead.created_time) < thirtyDaysAgo
).length;
const recentDeals = data.deals.filter(deal =>
deal.created_time && new Date(deal.created_time) >= thirtyDaysAgo
).length;
const previousDeals = data.deals.filter(deal =>
deal.created_time &&
new Date(deal.created_time) >= ninetyDaysAgo &&
new Date(deal.created_time) < thirtyDaysAgo
).length;
const leadGrowthRate = previousLeads > 0 ?
((recentLeads - previousLeads) / previousLeads * 100).toFixed(2) : 0;
const dealGrowthRate = previousDeals > 0 ?
((recentDeals - previousDeals) / previousDeals * 100).toFixed(2) : 0;
return {
recentLeads,
previousLeads,
recentDeals,
previousDeals,
leadGrowthRate: `${leadGrowthRate}%`,
dealGrowthRate: `${dealGrowthRate}%`
};
}
/**
* Calculate business insights
*/
calculateBusinessInsights(data, revenueMetrics, pipelineMetrics) {
const now = new Date();
const leadSourcePerformance = this.getLeadSourcePerformance(data.leads);
const topPerformingSources = 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);
const topRevenueOwners = this.getOwnerPerformance(data.invoices, 'total')
.slice(0, 5);
const mostActiveOwners = this.getOwnerPerformance(data.tasks, null, 'count')
.slice(0, 5);
return {
topPerformingSources,
topRevenueOwners,
mostActiveOwners,
businessHealth: this.calculateBusinessHealth(data.leads, data.tasks, now)
};
}
/**
* Calculate conversion funnel
*/
calculateConversionFunnel(data) {
const totalLeads = data.leads.length;
const qualifiedLeads = data.leads.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 = data.leads.filter(lead =>
lead.lead_status && lead.lead_status.toLowerCase().includes('converted')
).length;
const totalDeals = data.deals.length;
const closedWonDeals = data.deals.filter(deal =>
deal.stage && deal.stage.toLowerCase().includes('closed won')
).length;
return {
leads: { count: totalLeads, percentage: 100 },
qualified: { count: qualifiedLeads, percentage: totalLeads > 0 ? (qualifiedLeads / totalLeads * 100).toFixed(2) : 0 },
converted: { count: convertedLeads, percentage: totalLeads > 0 ? (convertedLeads / totalLeads * 100).toFixed(2) : 0 },
deals: { count: totalDeals, percentage: convertedLeads > 0 ? (totalDeals / convertedLeads * 100).toFixed(2) : 0 },
closedWon: { count: closedWonDeals, percentage: totalDeals > 0 ? (closedWonDeals / totalDeals * 100).toFixed(2) : 0 }
};
}
/**
* Calculate revenue by month
*/
calculateRevenueByMonth(invoices) {
const monthlyRevenue = {};
const now = new Date();
// Initialize last 12 months
for (let i = 11; i >= 0; i--) {
const date = new Date(now.getFullYear(), now.getMonth() - i, 1);
const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
monthlyRevenue[monthKey] = 0;
}
invoices.forEach(invoice => {
if (invoice.created_time) {
const date = new Date(invoice.created_time);
const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
if (monthlyRevenue.hasOwnProperty(monthKey)) {
monthlyRevenue[monthKey] += parseFloat(invoice.total) || 0;
}
}
});
return Object.entries(monthlyRevenue).map(([month, revenue]) => ({ month, revenue }));
}
/**
* Get owner performance metrics
*/
getOwnerPerformance(data, valueField = null, metric = 'revenue') {
const ownerStats = {};
data.forEach(item => {
const owner = item.owner || 'Unassigned';
if (!ownerStats[owner]) {
ownerStats[owner] = { count: 0, total: 0 };
}
ownerStats[owner].count++;
if (valueField && item[valueField]) {
ownerStats[owner].total += parseFloat(item[valueField]) || 0;
}
});
return Object.entries(ownerStats)
.map(([owner, stats]) => ({
owner,
count: stats.count,
[metric === 'revenue' ? 'total' : 'value']: stats.total
}))
.sort((a, b) => (metric === 'revenue' ? b.total - a.total : b.count - a.count));
}
/**
* Generate business recommendations
*/
generateRecommendations(data, revenueMetrics, pipelineMetrics, operationalMetrics) {
const recommendations = [];
// Revenue recommendations
if (revenueMetrics.overdueAmount > revenueMetrics.totalRevenue * 0.2) {
recommendations.push({
category: 'Financial',
priority: 'High',
title: 'Improve Collections',
description: `${revenueMetrics.overdueCount} overdue invoices worth $${revenueMetrics.overdueAmount.toFixed(2)} need attention.`,
action: 'Review and follow up on overdue invoices immediately.'
});
}
// Pipeline recommendations
if (parseFloat(pipelineMetrics.conversionRate) < 10) {
recommendations.push({
category: 'Sales',
priority: 'High',
title: 'Improve Lead Conversion',
description: `Current conversion rate is ${pipelineMetrics.conversionRate}. Industry average is 15-20%.`,
action: 'Review lead qualification process and follow-up procedures.'
});
}
// Operational recommendations
if (parseFloat(operationalMetrics.overdueRate) > 20) {
recommendations.push({
category: 'Operations',
priority: 'Medium',
title: 'Reduce Task Overdue Rate',
description: `${operationalMetrics.overdueRate} of tasks are overdue.`,
action: 'Implement better task prioritization and deadline management.'
});
}
// Growth recommendations
if (data.leads.length < 50) {
recommendations.push({
category: 'Growth',
priority: 'Medium',
title: 'Increase Lead Generation',
description: `Only ${data.leads.length} leads in the system. Consider expanding marketing efforts.`,
action: 'Invest in lead generation campaigns and improve website conversion.'
});
}
return recommendations;
}
/**
* 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)
};