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;
|
||||
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 = [
|
||||
{
|
||||
name: 'Accounts',
|
||||
@ -999,6 +999,27 @@ const scheduleBulkReadJobs = async (req, res) => {
|
||||
'id', 'First_Name', 'Last_Name', 'Company', 'Lead_Source',
|
||||
'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,
|
||||
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) {
|
||||
|
||||
@ -1,11 +1,18 @@
|
||||
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 leads and tasks data
|
||||
* Get comprehensive CRM KPIs combining all CRM modules data
|
||||
*/
|
||||
async getCrmKPIs(req, res) {
|
||||
try {
|
||||
@ -26,35 +33,70 @@ class ReportsController {
|
||||
...ownerFilter
|
||||
};
|
||||
|
||||
// Fetch data in parallel
|
||||
const [leadsData, tasksData] = await Promise.all([
|
||||
// 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'
|
||||
]
|
||||
attributes: ['lead_source', 'lead_status', 'owner', 'created_time', 'company', 'email', 'phone']
|
||||
}),
|
||||
ZohoTasksBulk.findAll({
|
||||
where: baseFilters,
|
||||
attributes: [
|
||||
'status',
|
||||
'priority',
|
||||
'owner',
|
||||
'created_time',
|
||||
'due_date',
|
||||
'subject'
|
||||
]
|
||||
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 KPIs
|
||||
const kpis = this.calculateCrmKPIs(leadsData, tasksData);
|
||||
// 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('CRM KPIs retrieved successfully', kpis));
|
||||
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'));
|
||||
@ -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) {
|
||||
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
|
||||
*/
|
||||
|
||||
@ -49,7 +49,6 @@ class ZohoBulkReadRepository {
|
||||
console.log(`💾 Bulk inserting ${data.length} records for ${module}`);
|
||||
|
||||
const result = await model.bulkCreate(data, {
|
||||
ignoreDuplicates: true,
|
||||
validate: true
|
||||
});
|
||||
|
||||
@ -65,19 +64,19 @@ class ZohoBulkReadRepository {
|
||||
* Clear existing data for a user and module
|
||||
* @param {string} userId - User UUID
|
||||
* @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
|
||||
*/
|
||||
async clearUserData(userId, module, jobId) {
|
||||
async clearUserData(userId, module, jobId = null) {
|
||||
try {
|
||||
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({
|
||||
where: {
|
||||
user_uuid: userId,
|
||||
provider: 'zoho',
|
||||
bulk_job_id: jobId
|
||||
provider: 'zoho'
|
||||
// Removed bulk_job_id filter to clear ALL data for the user and module
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -234,6 +234,7 @@ class BulkReadService {
|
||||
'leads': 2,
|
||||
'accounts': 1,
|
||||
'tasks': 3,
|
||||
'deals': 2,
|
||||
'vendors': 1,
|
||||
'invoices': 2,
|
||||
'sales_orders': 2,
|
||||
@ -288,6 +289,16 @@ class BulkReadService {
|
||||
'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',
|
||||
displayName: 'Vendors',
|
||||
@ -302,7 +313,7 @@ class BulkReadService {
|
||||
description: 'Invoice information',
|
||||
fields: [
|
||||
'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',
|
||||
description: 'Sales order information',
|
||||
fields: [
|
||||
'id', 'Sales_Order_Number', 'Subject', 'Status', 'Due_Date',
|
||||
'Total', 'Account_Name.Account_Name', 'Owner', 'Created_Time'
|
||||
'id', 'Subject', 'Status', 'Due_Date', 'Grand_Total',
|
||||
'Account_Name.Account_Name', 'Owner', 'Created_Time'
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -319,8 +330,8 @@ class BulkReadService {
|
||||
displayName: 'Purchase Orders',
|
||||
description: 'Purchase order information',
|
||||
fields: [
|
||||
'id', 'Purchase_Order_Number', 'Subject', 'Vendor_Name.Vendor_Name',
|
||||
'Status', 'Due_Date', 'Total', 'Owner', 'Created_Time'
|
||||
'id', 'Subject', 'Vendor_Name.Vendor_Name', 'Status',
|
||||
'Due_Date', 'Grand_Total', 'Owner', 'Created_Time'
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
@ -261,7 +261,7 @@ class CsvService {
|
||||
invoice_date: this.parseDate(record.Invoice_Date),
|
||||
due_date: this.parseDate(record.Due_Date),
|
||||
status: record.Status,
|
||||
total: this.parseDecimal(record.Total),
|
||||
total: this.parseDecimal(record.Grand_Total),
|
||||
balance: this.parseDecimal(record.Balance),
|
||||
account_name: record['Account_Name.Account_Name'] || record.Account_Name,
|
||||
owner: record.Owner,
|
||||
@ -277,7 +277,7 @@ class CsvService {
|
||||
subject: record.Subject,
|
||||
status: record.Status,
|
||||
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,
|
||||
owner: record.Owner,
|
||||
created_time: this.parseDate(record.Created_Time)
|
||||
@ -293,7 +293,7 @@ class CsvService {
|
||||
vendor_name: record['Vendor_Name.Vendor_Name'] || record.Vendor_Name,
|
||||
status: record.Status,
|
||||
due_date: this.parseDate(record.Due_Date),
|
||||
total: this.parseDecimal(record.Total),
|
||||
total: this.parseDecimal(record.Grand_Total),
|
||||
owner: record.Owner,
|
||||
created_time: this.parseDate(record.Created_Time)
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user