bulk report api restructured with more modules
This commit is contained in:
parent
6c0902cd57
commit
ec68e5ca39
@ -962,7 +962,7 @@ const scheduleBulkReadJobs = async (req, res) => {
|
|||||||
const accessToken = req.headers.authorization?.replace('Bearer ', '') || null;
|
const accessToken = req.headers.authorization?.replace('Bearer ', '') || null;
|
||||||
console.log(`🚀 Scheduling bulk read jobs for user: ${userId}`);
|
console.log(`🚀 Scheduling bulk read jobs for user: ${userId}`);
|
||||||
|
|
||||||
// Define the 5 modules with their specific fields
|
// Define the 8 modules with their specific fields
|
||||||
const modules = [
|
const modules = [
|
||||||
{
|
{
|
||||||
name: 'Accounts',
|
name: 'Accounts',
|
||||||
@ -999,6 +999,27 @@ const scheduleBulkReadJobs = async (req, res) => {
|
|||||||
'id', 'First_Name', 'Last_Name', 'Company', 'Lead_Source',
|
'id', 'First_Name', 'Last_Name', 'Company', 'Lead_Source',
|
||||||
'Lead_Status', 'Owner', 'Email', 'Phone', 'Created_Time'
|
'Lead_Status', 'Owner', 'Email', 'Phone', 'Created_Time'
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Invoices',
|
||||||
|
fields: [
|
||||||
|
'id', 'Invoice_Number', 'Invoice_Date', 'Due_Date', 'Status',
|
||||||
|
'Grand_Total', 'Account_Name.Account_Name', 'Owner', 'Created_Time'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Purchase_Orders',
|
||||||
|
fields: [
|
||||||
|
'id', 'Subject', 'Vendor_Name.Vendor_Name', 'Status',
|
||||||
|
'Due_Date', 'Grand_Total', 'Owner', 'Created_Time'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Sales_Orders',
|
||||||
|
fields: [
|
||||||
|
'id', 'Subject', 'Status', 'Due_Date', 'Grand_Total',
|
||||||
|
'Account_Name.Account_Name', 'Owner', 'Created_Time'
|
||||||
|
]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -1049,7 +1070,11 @@ const scheduleBulkReadJobs = async (req, res) => {
|
|||||||
successful: results.length,
|
successful: results.length,
|
||||||
failed: errors.length
|
failed: errors.length
|
||||||
},
|
},
|
||||||
note: 'Jobs are now being processed by Zoho. You will receive webhook notifications when each job completes.'
|
note: 'Jobs are now being processed by Zoho. You will receive webhook notifications when each job completes.',
|
||||||
|
modules: [
|
||||||
|
'Accounts', 'Deals', 'Contacts', 'Tasks', 'Leads',
|
||||||
|
'Invoices', 'Purchase_Orders', 'Sales_Orders'
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
|
|||||||
@ -1,11 +1,18 @@
|
|||||||
const { Op } = require('sequelize');
|
const { Op } = require('sequelize');
|
||||||
const ZohoLeadsBulk = require('../../data/models/zohoLeadsBulk');
|
const ZohoLeadsBulk = require('../../data/models/zohoLeadsBulk');
|
||||||
const ZohoTasksBulk = require('../../data/models/zohoTasksBulk');
|
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');
|
const { success, failure } = require('../../utils/response');
|
||||||
|
|
||||||
class ReportsController {
|
class ReportsController {
|
||||||
/**
|
/**
|
||||||
* Get comprehensive CRM KPIs combining leads and tasks data
|
* Get comprehensive CRM KPIs combining all CRM modules data
|
||||||
*/
|
*/
|
||||||
async getCrmKPIs(req, res) {
|
async getCrmKPIs(req, res) {
|
||||||
try {
|
try {
|
||||||
@ -26,35 +33,70 @@ class ReportsController {
|
|||||||
...ownerFilter
|
...ownerFilter
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fetch data in parallel
|
// Fetch data from all CRM modules in parallel
|
||||||
const [leadsData, tasksData] = await Promise.all([
|
const [
|
||||||
|
leadsData,
|
||||||
|
tasksData,
|
||||||
|
contactsData,
|
||||||
|
accountsData,
|
||||||
|
dealsData,
|
||||||
|
vendorsData,
|
||||||
|
invoicesData,
|
||||||
|
salesOrdersData,
|
||||||
|
purchaseOrdersData
|
||||||
|
] = await Promise.all([
|
||||||
ZohoLeadsBulk.findAll({
|
ZohoLeadsBulk.findAll({
|
||||||
where: baseFilters,
|
where: baseFilters,
|
||||||
attributes: [
|
attributes: ['lead_source', 'lead_status', 'owner', 'created_time', 'company', 'email', 'phone']
|
||||||
'lead_source',
|
|
||||||
'lead_status',
|
|
||||||
'owner',
|
|
||||||
'created_time',
|
|
||||||
'company'
|
|
||||||
]
|
|
||||||
}),
|
}),
|
||||||
ZohoTasksBulk.findAll({
|
ZohoTasksBulk.findAll({
|
||||||
where: baseFilters,
|
where: baseFilters,
|
||||||
attributes: [
|
attributes: ['status', 'priority', 'owner', 'created_time', 'due_date', 'subject', 'what_id']
|
||||||
'status',
|
}),
|
||||||
'priority',
|
ZohoContactsBulk.findAll({
|
||||||
'owner',
|
where: baseFilters,
|
||||||
'created_time',
|
attributes: ['first_name', 'last_name', 'email', 'phone', 'lead_source', 'account_name', 'owner', 'created_time']
|
||||||
'due_date',
|
}),
|
||||||
'subject'
|
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 KPIs
|
// Calculate comprehensive KPIs
|
||||||
const kpis = this.calculateCrmKPIs(leadsData, tasksData);
|
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('CRM KPIs retrieved successfully', kpis));
|
return res.json(success('Comprehensive CRM KPIs retrieved successfully', kpis));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching CRM KPIs:', error);
|
console.error('Error fetching CRM KPIs:', error);
|
||||||
return res.status(500).json(failure('Failed to fetch CRM KPIs', 'INTERNAL_ERROR'));
|
return res.status(500).json(failure('Failed to fetch CRM KPIs', 'INTERNAL_ERROR'));
|
||||||
@ -62,7 +104,91 @@ class ReportsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate CEO-level CRM KPIs focused on business statistics
|
* 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) {
|
calculateCrmKPIs(leadsData, tasksData) {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@ -283,6 +409,366 @@ class ReportsController {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
* Build date filter based on parameters
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -49,7 +49,6 @@ class ZohoBulkReadRepository {
|
|||||||
console.log(`💾 Bulk inserting ${data.length} records for ${module}`);
|
console.log(`💾 Bulk inserting ${data.length} records for ${module}`);
|
||||||
|
|
||||||
const result = await model.bulkCreate(data, {
|
const result = await model.bulkCreate(data, {
|
||||||
ignoreDuplicates: true,
|
|
||||||
validate: true
|
validate: true
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -65,19 +64,19 @@ class ZohoBulkReadRepository {
|
|||||||
* Clear existing data for a user and module
|
* Clear existing data for a user and module
|
||||||
* @param {string} userId - User UUID
|
* @param {string} userId - User UUID
|
||||||
* @param {string} module - Module name
|
* @param {string} module - Module name
|
||||||
* @param {string} jobId - Bulk job ID
|
* @param {string} jobId - Bulk job ID (optional, used for logging)
|
||||||
* @returns {Promise<number>} Number of deleted records
|
* @returns {Promise<number>} Number of deleted records
|
||||||
*/
|
*/
|
||||||
async clearUserData(userId, module, jobId) {
|
async clearUserData(userId, module, jobId = null) {
|
||||||
try {
|
try {
|
||||||
const model = this.getModel(module);
|
const model = this.getModel(module);
|
||||||
console.log(`🗑️ Clearing existing data for user ${userId}, module ${module}, job ${jobId}`);
|
console.log(`🗑️ Clearing ALL existing data for user ${userId}, module ${module}${jobId ? ` (new job: ${jobId})` : ''}`);
|
||||||
|
|
||||||
const result = await model.destroy({
|
const result = await model.destroy({
|
||||||
where: {
|
where: {
|
||||||
user_uuid: userId,
|
user_uuid: userId,
|
||||||
provider: 'zoho',
|
provider: 'zoho'
|
||||||
bulk_job_id: jobId
|
// Removed bulk_job_id filter to clear ALL data for the user and module
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -234,6 +234,7 @@ class BulkReadService {
|
|||||||
'leads': 2,
|
'leads': 2,
|
||||||
'accounts': 1,
|
'accounts': 1,
|
||||||
'tasks': 3,
|
'tasks': 3,
|
||||||
|
'deals': 2,
|
||||||
'vendors': 1,
|
'vendors': 1,
|
||||||
'invoices': 2,
|
'invoices': 2,
|
||||||
'sales_orders': 2,
|
'sales_orders': 2,
|
||||||
@ -288,6 +289,16 @@ class BulkReadService {
|
|||||||
'Due_Date', 'What_Id', 'Created_Time'
|
'Due_Date', 'What_Id', 'Created_Time'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'deals',
|
||||||
|
displayName: 'Deals',
|
||||||
|
description: 'Sales deal information',
|
||||||
|
fields: [
|
||||||
|
'id', 'Deal_Name', 'Stage', 'Amount', 'Closing_Date',
|
||||||
|
'Account_Name', 'Contact_Name', 'Pipeline', 'Probability',
|
||||||
|
'Lead_Source', 'Owner', 'Created_Time', 'Modified_Time'
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'vendors',
|
name: 'vendors',
|
||||||
displayName: 'Vendors',
|
displayName: 'Vendors',
|
||||||
@ -302,7 +313,7 @@ class BulkReadService {
|
|||||||
description: 'Invoice information',
|
description: 'Invoice information',
|
||||||
fields: [
|
fields: [
|
||||||
'id', 'Invoice_Number', 'Invoice_Date', 'Due_Date', 'Status',
|
'id', 'Invoice_Number', 'Invoice_Date', 'Due_Date', 'Status',
|
||||||
'Total', 'Balance', 'Account_Name.Account_Name', 'Owner', 'Created_Time'
|
'Grand_Total', 'Account_Name.Account_Name', 'Owner', 'Created_Time'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -310,8 +321,8 @@ class BulkReadService {
|
|||||||
displayName: 'Sales Orders',
|
displayName: 'Sales Orders',
|
||||||
description: 'Sales order information',
|
description: 'Sales order information',
|
||||||
fields: [
|
fields: [
|
||||||
'id', 'Sales_Order_Number', 'Subject', 'Status', 'Due_Date',
|
'id', 'Subject', 'Status', 'Due_Date', 'Grand_Total',
|
||||||
'Total', 'Account_Name.Account_Name', 'Owner', 'Created_Time'
|
'Account_Name.Account_Name', 'Owner', 'Created_Time'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -319,8 +330,8 @@ class BulkReadService {
|
|||||||
displayName: 'Purchase Orders',
|
displayName: 'Purchase Orders',
|
||||||
description: 'Purchase order information',
|
description: 'Purchase order information',
|
||||||
fields: [
|
fields: [
|
||||||
'id', 'Purchase_Order_Number', 'Subject', 'Vendor_Name.Vendor_Name',
|
'id', 'Subject', 'Vendor_Name.Vendor_Name', 'Status',
|
||||||
'Status', 'Due_Date', 'Total', 'Owner', 'Created_Time'
|
'Due_Date', 'Grand_Total', 'Owner', 'Created_Time'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@ -261,7 +261,7 @@ class CsvService {
|
|||||||
invoice_date: this.parseDate(record.Invoice_Date),
|
invoice_date: this.parseDate(record.Invoice_Date),
|
||||||
due_date: this.parseDate(record.Due_Date),
|
due_date: this.parseDate(record.Due_Date),
|
||||||
status: record.Status,
|
status: record.Status,
|
||||||
total: this.parseDecimal(record.Total),
|
total: this.parseDecimal(record.Grand_Total),
|
||||||
balance: this.parseDecimal(record.Balance),
|
balance: this.parseDecimal(record.Balance),
|
||||||
account_name: record['Account_Name.Account_Name'] || record.Account_Name,
|
account_name: record['Account_Name.Account_Name'] || record.Account_Name,
|
||||||
owner: record.Owner,
|
owner: record.Owner,
|
||||||
@ -277,7 +277,7 @@ class CsvService {
|
|||||||
subject: record.Subject,
|
subject: record.Subject,
|
||||||
status: record.Status,
|
status: record.Status,
|
||||||
due_date: this.parseDate(record.Due_Date),
|
due_date: this.parseDate(record.Due_Date),
|
||||||
total: this.parseDecimal(record.Total),
|
total: this.parseDecimal(record.Grand_Total),
|
||||||
account_name: record['Account_Name.Account_Name'] || record.Account_Name,
|
account_name: record['Account_Name.Account_Name'] || record.Account_Name,
|
||||||
owner: record.Owner,
|
owner: record.Owner,
|
||||||
created_time: this.parseDate(record.Created_Time)
|
created_time: this.parseDate(record.Created_Time)
|
||||||
@ -293,7 +293,7 @@ class CsvService {
|
|||||||
vendor_name: record['Vendor_Name.Vendor_Name'] || record.Vendor_Name,
|
vendor_name: record['Vendor_Name.Vendor_Name'] || record.Vendor_Name,
|
||||||
status: record.Status,
|
status: record.Status,
|
||||||
due_date: this.parseDate(record.Due_Date),
|
due_date: this.parseDate(record.Due_Date),
|
||||||
total: this.parseDecimal(record.Total),
|
total: this.parseDecimal(record.Grand_Total),
|
||||||
owner: record.Owner,
|
owner: record.Owner,
|
||||||
created_time: this.parseDate(record.Created_Time)
|
created_time: this.parseDate(record.Created_Time)
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user