821 lines
28 KiB
JavaScript
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)
|
|
};
|