diff --git a/src/modules/crm/zoho/screens/CrmDashboardScreen.tsx b/src/modules/crm/zoho/screens/CrmDashboardScreen.tsx index 0be55a3..6bf40cb 100644 --- a/src/modules/crm/zoho/screens/CrmDashboardScreen.tsx +++ b/src/modules/crm/zoho/screens/CrmDashboardScreen.tsx @@ -1,17 +1,14 @@ -import React, { useEffect } from 'react'; -import { View, Text, StyleSheet, TouchableOpacity, RefreshControl, ScrollView } from 'react-native'; +import React, { useEffect, useState } from 'react'; +import { View, Text, StyleSheet, TouchableOpacity, RefreshControl, ScrollView, ActivityIndicator } from 'react-native'; import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; import { useSelector, useDispatch } from 'react-redux'; import { Container } from '@/shared/components/ui'; import { PieChart, DonutChart, FunnelChart, StackedBarChart, PipelineFlow, PipelineCards, CompactPipeline } from '@/shared/components/charts'; import { useTheme } from '@/shared/styles/useTheme'; import { useNavigation } from '@react-navigation/native'; -import { fetchAllCrmData } from '../store/crmSlice'; +import { fetchCrmKPIs } from '../store/crmSlice'; import { - selectDashboardData, - selectIsAnyLoading, - selectHasAnyError, - selectCrmStats + selectCrmKPIs } from '../store/selectors'; import type { RootState } from '@/store/store'; import type { AppDispatch } from '@/store/store'; @@ -22,24 +19,42 @@ const CrmDashboardScreen: React.FC = () => { const dispatch = useDispatch(); // Redux selectors - const dashboardData = useSelector(selectDashboardData); - const crmStats = useSelector(selectCrmStats); - const isLoading = useSelector(selectIsAnyLoading); - const hasError = useSelector(selectHasAnyError); + const crmKPIs = useSelector(selectCrmKPIs); + const [isLoading, setIsLoading] = useState(false); + const [hasError, setHasError] = useState(false); - // Fetch data on component mount + // Fetch KPIs data on component mount useEffect(() => { - dispatch(fetchAllCrmData()); + fetchKPIsData(); }, [dispatch]); + // Debug log to check KPIs data + useEffect(() => { + // console.log('CRM KPIs Data:', crmKPIs); + }, [crmKPIs]); + + // Fetch KPIs data + const fetchKPIsData = async () => { + try { + setIsLoading(true); + setHasError(false); + await dispatch(fetchCrmKPIs()).unwrap(); + } catch (error) { + console.error('Failed to fetch KPIs data:', error); + setHasError(true); + } finally { + setIsLoading(false); + } + }; + // Handle refresh const handleRefresh = () => { - dispatch(fetchAllCrmData()); + fetchKPIsData(); }; return ( - } @@ -57,218 +72,342 @@ const CrmDashboardScreen: React.FC = () => { - {/* Error State */} - {hasError && ( - - - - Failed to load CRM data. Pull to refresh. + {/* Loading State */} + {isLoading && !crmKPIs && ( + + + + Loading comprehensive KPIs data... )} - {/* KPIs */} - - - - - + {/* Error State */} + {hasError && ( + + + + Failed to load KPIs data. Pull to refresh. + + + )} + + {/* No Data State */} + {!crmKPIs && !isLoading && !hasError && ( + + + + No KPIs data available. Pull to refresh. + + + )} + + {/* Refresh Indicator */} + {isLoading && crmKPIs && ( + + + + Refreshing data... + + + )} + + {/* Hero Stats - Large Cards */} + + + + + Leads + + + {crmKPIs?.businessOverview?.moduleCounts?.leads || 0} + + + Total Leads + - {/* Additional Stats Row */} - - - - - + + + + Deals + + + {crmKPIs?.businessOverview?.moduleCounts?.deals || 0} + + + Total Deals + + - {/* Sales Orders & Purchase Orders Row */} - - - - - + {/* Revenue Overview - Large Card */} + + + + Revenue Overview + + + + Total Revenue + + {formatCurrency(crmKPIs?.businessOverview?.revenueMetrics?.totalRevenue || 0)} + + + + Pipeline Value + + {formatCurrency(crmKPIs?.salesPipeline?.totalPipelineValue || 0)} + + + + Avg Invoice + + {formatCurrency(crmKPIs?.businessOverview?.revenueMetrics?.averageInvoiceValue || 0)} + + + - {/* Invoices Row */} - - - - - + {/* Module Counts - Compact Grid */} + + + + + {crmKPIs?.businessOverview?.moduleCounts?.tasks || 0} + + Tasks + + + + + + {crmKPIs?.businessOverview?.moduleCounts?.contacts || 0} + + Contacts + + + + + + {crmKPIs?.businessOverview?.moduleCounts?.accounts || 0} + + Accounts + + + + + + {crmKPIs?.businessOverview?.moduleCounts?.invoices || 0} + + Invoices + + + + + + {crmKPIs?.businessOverview?.moduleCounts?.salesOrders || 0} + + Sales Orders + + + + + + {crmKPIs?.businessOverview?.moduleCounts?.purchaseOrders || 0} + + Purchase Orders + - {/* Customer & Sales KPIs Row 1 */} - - - - - + {/* Sales Pipeline Progress - Progress Bars */} + + Sales Pipeline Progress + + + + Qualified Leads + + {crmKPIs?.salesPipeline?.qualifiedLeads || 0} + + + + + + + + + + Converted Leads + + {crmKPIs?.salesPipeline?.convertedLeads || 0} + + + + + + + + + + Closed Won Deals + + {crmKPIs?.salesPipeline?.closedWonDeals || 0} + + + + + + - {/* Customer & Sales KPIs Row 2 */} - - - - - + {/* Task Performance - Circular Progress */} + + Task Performance + + + + + + {crmKPIs?.operationalEfficiency?.taskCompletionRate || "0%"} + + + Completion Rate + + + + + + {crmKPIs?.operationalEfficiency?.overdueRate || "0%"} + + + Overdue Rate + + + + + + + {crmKPIs?.operationalEfficiency?.totalTasks || 0} + + Total Tasks + + + + {crmKPIs?.operationalEfficiency?.completedTasks || 0} + + Completed + + + + {crmKPIs?.operationalEfficiency?.overdueTasks || 0} + + Overdue + + - {/* Lead Status Distribution - Pie Chart */} + {/* Key Metrics - Side by Side */} + + + + + {crmKPIs?.salesPipeline?.conversionRate || "0%"} + + Conversion Rate + + + + + + {crmKPIs?.salesPipeline?.winRate || "0%"} + + Win Rate + + + + + + + + {/* Conversion Funnel */} - Lead Status Distribution + Sales Conversion Funnel + + + + Leads + + {crmKPIs?.salesPipeline?.conversionFunnel?.leads?.count || 0} + + + {crmKPIs?.salesPipeline?.conversionFunnel?.leads?.percentage || "0"}% + + + + + + + + + Qualified + + {crmKPIs?.salesPipeline?.conversionFunnel?.qualified?.count || 0} + + + {crmKPIs?.salesPipeline?.conversionFunnel?.qualified?.percentage || "0"}% + + + + + + + + + Converted + + {crmKPIs?.salesPipeline?.conversionFunnel?.converted?.count || 0} + + + {crmKPIs?.salesPipeline?.conversionFunnel?.converted?.percentage || "0"}% + + + + + + + + + Closed Won + + {crmKPIs?.salesPipeline?.conversionFunnel?.closedWon?.count || 0} + + + {crmKPIs?.salesPipeline?.conversionFunnel?.closedWon?.percentage || "0"}% + + + + + + {/* Lead Sources Distribution */} + + Lead Sources Distribution ({ - label: status, - value: count, - color: getStatusColor(status) + data={Object.entries(crmKPIs?.salesPipeline?.leadSources || {}).map(([source, count]) => ({ + label: source, + value: Number(count) || 0, + color: getStatusColor(source) }))} colors={colors} fonts={fonts} @@ -277,11 +416,11 @@ const CrmDashboardScreen: React.FC = () => { {/* Legend */} - {Object.entries(crmStats.leads.byStatus).map(([status, count]) => ( - - + {Object.entries(crmKPIs?.salesPipeline?.leadSources || {}).map(([source, count]) => ( + + - {status} ({count}) + {source} ({Number(count) || 0}) ))} @@ -294,10 +433,10 @@ const CrmDashboardScreen: React.FC = () => { Deal Pipeline Stages ({ - label: stage.label, - value: stage.value, - color: stage.color + data={Object.entries(crmKPIs?.salesPipeline?.dealStages || {}).map(([stage, count]) => ({ + label: stage, + value: Number(count) || 0, + color: getStatusColor(stage) }))} colors={colors} fonts={fonts} @@ -310,10 +449,10 @@ const CrmDashboardScreen: React.FC = () => { ({ - label: source.label, - value: source.value, - color: source.color + data={Object.entries(crmKPIs?.salesPipeline?.leadSources || {}).map(([source, count]) => ({ + label: source, + value: Number(count) || 0, + color: getStatusColor(source) }))} colors={colors} fonts={fonts} @@ -322,11 +461,11 @@ const CrmDashboardScreen: React.FC = () => { {/* Legend */} - {dashboardData.sourceDist.map(source => ( - - + {Object.entries(crmKPIs?.salesPipeline?.leadSources || {}).map(([source, count]) => ( + + - {source.label} ({source.value}) + {source} ({Number(count) || 0}) ))} @@ -336,14 +475,14 @@ const CrmDashboardScreen: React.FC = () => { {/* Tasks by Priority - Stacked Bar Chart */} - Tasks by Priority + Tasks by Status ({ - label: priority, - value: count, - color: getPriorityColor(priority) + data={Object.entries(crmKPIs?.operationalEfficiency?.taskStatus || {}).map(([status, count]) => ({ + label: status, + value: Number(count) || 0, + color: getPriorityColor(status) }))} colors={colors} fonts={fonts} @@ -352,11 +491,11 @@ const CrmDashboardScreen: React.FC = () => { {/* Legend */} - {Object.entries(crmStats.tasks.byPriority).map(([priority, count]) => ( - - + {Object.entries(crmKPIs?.operationalEfficiency?.taskStatus || {}).map(([status, count]) => ( + + - {priority} ({count}) + {status} ({Number(count) || 0}) ))} @@ -370,9 +509,9 @@ const CrmDashboardScreen: React.FC = () => { ({ + data={Object.entries(crmKPIs?.operationalEfficiency?.orderStatus?.salesOrders || {}).map(([status, count]) => ({ label: status, - value: count, + value: Number(count) || 0, color: getStatusColor(status) }))} colors={colors} @@ -382,11 +521,11 @@ const CrmDashboardScreen: React.FC = () => { {/* Legend */} - {Object.entries(crmStats.salesOrders.byStatus).map(([status, count]) => ( + {Object.entries(crmKPIs?.operationalEfficiency?.orderStatus?.salesOrders || {}).map(([status, count]) => ( - {status} ({count}) + {status} ({Number(count) || 0}) ))} @@ -394,16 +533,16 @@ const CrmDashboardScreen: React.FC = () => { - {/* Purchase Orders by Vendor - Pie Chart */} + {/* Purchase Orders by Status - Pie Chart */} - Purchase Orders by Vendor + Purchase Orders by Status ({ - label: vendor, - value: count, - color: getStatusColor(vendor) + data={Object.entries(crmKPIs?.operationalEfficiency?.orderStatus?.purchaseOrders || {}).map(([status, count]) => ({ + label: status, + value: Number(count) || 0, + color: getStatusColor(status) }))} colors={colors} fonts={fonts} @@ -412,11 +551,11 @@ const CrmDashboardScreen: React.FC = () => { {/* Legend */} - {Object.entries(crmStats.purchaseOrders.byVendor).map(([vendor, count]) => ( - - + {Object.entries(crmKPIs?.operationalEfficiency?.orderStatus?.purchaseOrders || {}).map(([status, count]) => ( + + - {vendor} ({count}) + {status} ({Number(count) || 0}) ))} @@ -430,9 +569,9 @@ const CrmDashboardScreen: React.FC = () => { ({ + data={Object.entries(crmKPIs?.operationalEfficiency?.invoiceStatus || {}).map(([status, count]) => ({ label: status, - value: count, + value: Number(count) || 0, color: getStatusColor(status) }))} colors={colors} @@ -442,11 +581,11 @@ const CrmDashboardScreen: React.FC = () => { {/* Legend */} - {Object.entries(crmStats.invoices.byStatus).map(([status, count]) => ( + {Object.entries(crmKPIs?.operationalEfficiency?.invoiceStatus || {}).map(([status, count]) => ( - {status} ({count}) + {status} ({Number(count) || 0}) ))} @@ -454,97 +593,247 @@ const CrmDashboardScreen: React.FC = () => { - {/* Customer & Sales KPIs Summary */} + {/* Advanced Financial KPIs */} - Customer & Sales KPIs Summary + Advanced Financial KPIs - - - Sales Cycle Length - - {crmStats.customerKPIs.salesCycleLength} days + + + Revenue Growth Rate + + {crmKPIs?.advancedFinancialKPIs?.revenueGrowthRate?.percentage || "0%"} - - Avg. time from first touch to close + + {crmKPIs?.advancedFinancialKPIs?.revenueGrowthRate?.trend || "stable"} trend - - Customer LTV - - {formatCurrency(crmStats.customerKPIs.customerLifetimeValue)} + + Gross Margin + + {crmKPIs?.advancedFinancialKPIs?.grossMargin?.percentage || "0%"} - - Average lifetime value per customer + + {formatCurrency(crmKPIs?.advancedFinancialKPIs?.grossMargin?.grossProfit || 0)} profit - - LTV/CAC Ratio - = 3 ? '#22C55E' : '#EF4444', fontFamily: fonts.bold }]}> - {crmStats.customerKPIs.ltvToCacRatio}x + + Net Profit Margin + + {crmKPIs?.advancedFinancialKPIs?.netProfitMargin?.percentage || "0%"} - - {crmStats.customerKPIs.ltvToCacRatio >= 3 ? 'Healthy ratio' : 'Needs improvement'} + + {formatCurrency(crmKPIs?.advancedFinancialKPIs?.netProfitMargin?.netProfit || 0)} net profit - - Churn Rate - - {crmStats.customerKPIs.churnRate}% + + EBITDA + + {formatCurrency(crmKPIs?.advancedFinancialKPIs?.ebitda?.value || 0)} - - {crmStats.customerKPIs.churnRate <= 5 ? 'Low churn' : 'High churn risk'} + + {crmKPIs?.advancedFinancialKPIs?.ebitda?.margin || "0%"} margin + + + + + Cash Runway + + {crmKPIs?.advancedFinancialKPIs?.cashRunway?.months || "0"} months + + + {crmKPIs?.advancedFinancialKPIs?.cashRunway?.status || "unknown"} status + + + + + Days Sales Outstanding + + {crmKPIs?.advancedFinancialKPIs?.daysSalesOutstanding?.days || 0} days + + + {crmKPIs?.advancedFinancialKPIs?.daysSalesOutstanding?.status || "unknown"} status - {/* Lists */} + {/* Business Health Score */} + {crmKPIs?.businessInsights?.businessHealth && ( + + Business Health Score + + + + + {crmKPIs.businessInsights.businessHealth.overallScore}/100 + + + Overall Score + + + + + + Lead Generation + + {crmKPIs.businessInsights.businessHealth.leadGeneration}% + + + + Process Efficiency + + {crmKPIs.businessInsights.businessHealth.processEfficiency}% + + + + Quality Metrics + + {crmKPIs.businessInsights.businessHealth.qualityMetrics}% + + + + + + )} + + {/* Industry Distribution */} + {crmKPIs?.customerRelationships?.industryDistribution && ( + + Industry Distribution + + + ({ + label: industry, + value: Number(count) || 0, + color: getStatusColor(industry) + }))} + colors={colors} + fonts={fonts} + size={140} + /> + + {/* Legend */} + + {Object.entries(crmKPIs.customerRelationships.industryDistribution).map(([industry, count]) => ( + + + + {industry} ({Number(count) || 0}) + + + ))} + + + + )} + + {/* Revenue Trends - Only show months with revenue */} + {crmKPIs?.businessOverview?.revenueMetrics?.revenueByMonth && ( + + Revenue Trends + + + {crmKPIs.businessOverview.revenueMetrics.revenueByMonth + .filter((month: any) => month.revenue > 0) + .slice(-6) + .map((month: any, index: number) => ( + + + {new Date(month.month).toLocaleDateString('en-US', { month: 'short', year: '2-digit' })} + + + {formatCurrency(month.revenue)} + + + ))} + {crmKPIs.businessOverview.revenueMetrics.revenueByMonth.filter((month: any) => month.revenue > 0).length === 0 && ( + + No revenue data available for the selected period + + )} + + + )} + + {/* Top Revenue Accounts */} - Top Opportunities - {dashboardData.topOpps.length > 0 ? dashboardData.topOpps.map(o => ( - + Top Revenue Accounts + {crmKPIs?.businessOverview?.revenueMetrics?.topRevenueAccounts?.length > 0 ? crmKPIs.businessOverview.revenueMetrics.topRevenueAccounts.slice(0, 5).map((account: any, index: number) => ( + - {o.name} + {account.account} - {formatCurrency(o.value)} + {formatCurrency(account.revenue)} )) : ( - No opportunities found + No revenue data available )} - Recent Activity - {dashboardData.recent.length > 0 ? dashboardData.recent.map(r => ( - + Top Performing Sources + {crmKPIs?.businessInsights?.topPerformingSources?.length > 0 ? crmKPIs.businessInsights.topPerformingSources.slice(0, 5).map((source: any, index: number) => ( + - {r.who} + {source.source} - {r.what} · {r.when} + {source.totalLeads} leads · {source.avgQuality}% quality )) : ( - No recent activity + No source data available )} + + {/* Recommendations */} + {crmKPIs?.businessInsights?.recommendations && crmKPIs.businessInsights.recommendations.length > 0 && ( + + Business Recommendations + + {crmKPIs.businessInsights.recommendations.map((recommendation: any, index: number) => ( + + + + {recommendation.title} + + + + {recommendation.priority} + + + + + {recommendation.description} + + + Action: {recommendation.action} + + + ))} + + )} - ); }; const styles = StyleSheet.create({ wrap: { flex: 1, padding: 16 }, + scrollContent: { paddingBottom: 20 }, header: { flexDirection: 'row', justifyContent: 'space-between', @@ -576,6 +865,32 @@ const styles = StyleSheet.create({ fontSize: 14, flex: 1, }, + loadingCard: { + borderRadius: 12, + borderWidth: 1, + padding: 20, + marginTop: 12, + alignItems: 'center', + justifyContent: 'center', + }, + loadingText: { + marginTop: 12, + fontSize: 14, + textAlign: 'center', + }, + refreshIndicator: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + padding: 12, + marginBottom: 16, + borderRadius: 8, + borderWidth: 1, + }, + refreshText: { + marginLeft: 8, + fontSize: 14, + }, kpiGrid: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-between' }, kpiCard: { width: '48%', borderRadius: 12, borderWidth: 1, borderColor: '#E2E8F0', backgroundColor: '#FFFFFF', padding: 12, marginBottom: 12 }, kpiLabel: { fontSize: 12, opacity: 0.8 }, @@ -645,6 +960,373 @@ const styles = StyleSheet.create({ fontSize: 11, lineHeight: 14, }, + healthScoreContainer: { + flexDirection: 'row', + alignItems: 'center', + marginTop: 8, + }, + healthScoreMain: { + alignItems: 'center', + marginRight: 24, + }, + healthScoreValue: { + fontSize: 32, + marginBottom: 4, + }, + healthScoreLabel: { + fontSize: 12, + }, + healthScoreBreakdown: { + flex: 1, + }, + healthScoreItem: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 8, + }, + healthScoreItemLabel: { + fontSize: 12, + flex: 1, + }, + healthScoreItemValue: { + fontSize: 14, + }, + revenueTrendsContainer: { + flexDirection: 'row', + flexWrap: 'wrap', + marginTop: 8, + }, + revenueTrendItem: { + width: '33%', + alignItems: 'center', + marginBottom: 12, + }, + revenueTrendMonth: { + fontSize: 12, + marginBottom: 4, + }, + revenueTrendValue: { + fontSize: 14, + }, + recommendationItem: { + marginBottom: 16, + paddingBottom: 16, + borderBottomWidth: 1, + borderBottomColor: '#E5E7EB', + }, + recommendationHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 8, + }, + recommendationTitle: { + fontSize: 14, + flex: 1, + marginRight: 8, + }, + recommendationPriority: { + paddingHorizontal: 8, + paddingVertical: 4, + borderRadius: 12, + }, + recommendationPriorityText: { + fontSize: 10, + }, + recommendationDescription: { + fontSize: 12, + marginBottom: 8, + lineHeight: 16, + }, + recommendationAction: { + fontSize: 12, + fontStyle: 'italic', + }, + funnelContainer: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + marginTop: 8, + paddingHorizontal: 8, + }, + funnelStage: { + alignItems: 'center', + flex: 1, + }, + funnelStageLabel: { + fontSize: 12, + marginBottom: 4, + }, + funnelStageValue: { + fontSize: 18, + marginBottom: 2, + }, + funnelStagePercent: { + fontSize: 10, + }, + funnelArrow: { + marginHorizontal: 8, + alignItems: 'center', + justifyContent: 'center', + }, + funnelArrowText: { + fontSize: 16, + }, + financialKPIsGrid: { + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'space-between', + marginTop: 8, + }, + financialKPIItem: { + width: '48%', + marginBottom: 16, + padding: 12, + backgroundColor: '#F8F9FA', + borderRadius: 8, + }, + financialKPILabel: { + fontSize: 12, + marginBottom: 4, + }, + financialKPIValue: { + fontSize: 16, + marginBottom: 4, + }, + financialKPIDesc: { + fontSize: 10, + lineHeight: 12, + }, + dealStagesGrid: { + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'space-between', + marginTop: 8, + }, + dealStageItem: { + width: '48%', + marginBottom: 12, + padding: 8, + backgroundColor: '#F8F9FA', + borderRadius: 6, + }, + dealStageLabel: { + fontSize: 12, + marginBottom: 4, + }, + dealStageValue: { + fontSize: 16, + }, + leadSourcesGrid: { + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'space-between', + marginTop: 8, + }, + leadSourceItem: { + width: '48%', + marginBottom: 12, + padding: 8, + backgroundColor: '#F8F9FA', + borderRadius: 6, + }, + leadSourceLabel: { + fontSize: 12, + marginBottom: 4, + }, + leadSourceValue: { + fontSize: 16, + }, + // Hero Stats Styles + heroStatsContainer: { + flexDirection: 'row', + justifyContent: 'space-between', + marginBottom: 16, + }, + heroCard: { + flex: 1, + marginHorizontal: 4, + padding: 20, + borderRadius: 16, + borderWidth: 1, + alignItems: 'center', + }, + heroCardHeader: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: 12, + }, + heroCardTitle: { + fontSize: 16, + marginLeft: 8, + }, + heroCardValue: { + fontSize: 32, + marginBottom: 4, + }, + heroCardSubtitle: { + fontSize: 12, + }, + // Revenue Card Styles + revenueCard: { + padding: 20, + borderRadius: 16, + borderWidth: 1, + marginBottom: 16, + }, + revenueHeader: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: 16, + }, + revenueTitle: { + fontSize: 20, + marginLeft: 12, + }, + revenueStats: { + flexDirection: 'row', + justifyContent: 'space-between', + }, + revenueItem: { + flex: 1, + alignItems: 'center', + }, + revenueLabel: { + fontSize: 12, + marginBottom: 4, + }, + revenueValue: { + fontSize: 18, + }, + // Compact Grid Styles + compactGrid: { + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'space-between', + marginBottom: 16, + }, + compactCard: { + width: '30%', + padding: 12, + borderRadius: 12, + borderWidth: 1, + alignItems: 'center', + marginBottom: 8, + }, + compactValue: { + fontSize: 18, + marginVertical: 4, + }, + compactLabel: { + fontSize: 10, + textAlign: 'center', + }, + // Progress Card Styles + progressCard: { + padding: 16, + borderRadius: 12, + borderWidth: 1, + marginBottom: 16, + }, + progressTitle: { + fontSize: 16, + marginBottom: 16, + }, + progressItem: { + marginBottom: 12, + }, + progressHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + marginBottom: 6, + }, + progressLabel: { + fontSize: 14, + }, + progressValue: { + fontSize: 14, + }, + progressBar: { + height: 8, + borderRadius: 4, + overflow: 'hidden', + }, + progressFill: { + height: '100%', + borderRadius: 4, + }, + // Task Card Styles + taskCard: { + padding: 16, + borderRadius: 12, + borderWidth: 1, + marginBottom: 16, + }, + taskTitle: { + fontSize: 16, + marginBottom: 16, + }, + taskStats: { + flexDirection: 'row', + justifyContent: 'space-around', + marginBottom: 16, + }, + taskStatItem: { + alignItems: 'center', + }, + circularProgress: { + width: 60, + height: 60, + borderRadius: 30, + borderWidth: 4, + justifyContent: 'center', + alignItems: 'center', + marginBottom: 8, + }, + circularValue: { + fontSize: 12, + }, + circularLabel: { + fontSize: 10, + textAlign: 'center', + }, + taskNumbers: { + flexDirection: 'row', + justifyContent: 'space-around', + }, + taskNumberItem: { + alignItems: 'center', + }, + taskNumberValue: { + fontSize: 18, + marginBottom: 4, + }, + taskNumberLabel: { + fontSize: 12, + }, + // Metrics Row Styles + metricsRow: { + flexDirection: 'row', + justifyContent: 'space-between', + marginBottom: 16, + }, + metricCard: { + flex: 1, + marginHorizontal: 4, + padding: 16, + borderRadius: 12, + borderWidth: 1, + alignItems: 'center', + }, + metricValue: { + fontSize: 20, + marginVertical: 8, + }, + metricLabel: { + fontSize: 12, + textAlign: 'center', + }, }); // Helper functions for color coding diff --git a/src/modules/crm/zoho/screens/ZohoCrmDataScreen.tsx b/src/modules/crm/zoho/screens/ZohoCrmDataScreen.tsx index 54956ea..42809a8 100644 --- a/src/modules/crm/zoho/screens/ZohoCrmDataScreen.tsx +++ b/src/modules/crm/zoho/screens/ZohoCrmDataScreen.tsx @@ -34,7 +34,8 @@ import { selectDealsPagination, selectSalesOrdersPagination, selectPurchaseOrdersPagination, - selectInvoicesPagination + selectInvoicesPagination, + selectCrmCounts } from '../store/selectors'; import { fetchAllCrmData, @@ -45,6 +46,7 @@ import { fetchSalesOrders, fetchPurchaseOrders, fetchInvoices, + fetchCrmCounts, resetLeadsPagination, resetTasksPagination, resetContactsPagination, @@ -80,6 +82,7 @@ const ZohoCrmDataScreen: React.FC = () => { const salesOrdersPagination = useSelector(selectSalesOrdersPagination); const purchaseOrdersPagination = useSelector(selectPurchaseOrdersPagination); const invoicesPagination = useSelector(selectInvoicesPagination); + const counts = useSelector(selectCrmCounts); // Create CRM data object from Redux state const crmData: CrmData = useMemo(() => ({ @@ -184,6 +187,8 @@ const ZohoCrmDataScreen: React.FC = () => { useEffect(() => { fetchCrmData(); + // Fetch counts in parallel + dispatch(fetchCrmCounts()); }, []); const handleRefresh = useCallback(() => { @@ -196,8 +201,9 @@ const ZohoCrmDataScreen: React.FC = () => { dispatch(resetPurchaseOrdersPagination()); dispatch(resetInvoicesPagination()); - // Then fetch fresh data + // Then fetch fresh data and counts fetchCrmData(true); + dispatch(fetchCrmCounts()); }, [fetchCrmData, dispatch]); const handleRetry = useCallback(() => { @@ -230,26 +236,52 @@ const ZohoCrmDataScreen: React.FC = () => { errors.salesOrders || errors.purchaseOrders || errors.invoices; - // Tab configuration + // Tab configuration with counts from API const tabs = [ - { key: 'leads', label: 'Leads', icon: 'account-heart', count: crmData.leads.length }, - { key: 'tasks', label: 'Tasks', icon: 'check-circle', count: crmData.tasks.length }, - { key: 'contacts', label: 'Contacts', icon: 'account-group', count: crmData.contacts.length }, - { key: 'deals', label: 'Deals', icon: 'handshake', count: crmData.deals.length }, - { key: 'salesOrders', label: 'Sales Orders', icon: 'shopping', count: crmData.salesOrders.length }, - { key: 'purchaseOrders', label: 'Purchase Orders', icon: 'cart', count: crmData.purchaseOrders.length }, - { key: 'invoices', label: 'Invoices', icon: 'receipt', count: crmData.invoices.length }, + { + key: 'leads', + label: 'Leads', + icon: 'account-heart', + count: counts?.leads || crmData.leads.length + }, + { + key: 'tasks', + label: 'Tasks', + icon: 'check-circle', + count: counts?.tasks || crmData.tasks.length + }, + { + key: 'contacts', + label: 'Contacts', + icon: 'account-group', + count: counts?.contacts || crmData.contacts.length + }, + { + key: 'deals', + label: 'Deals', + icon: 'handshake', + count: counts?.deals || crmData.deals.length + }, + { + key: 'salesOrders', + label: 'Sales Orders', + icon: 'shopping', + count: counts?.salesOrders || crmData.salesOrders.length + }, + { + key: 'purchaseOrders', + label: 'Purchase Orders', + icon: 'cart', + count: counts?.purchaseOrders || crmData.purchaseOrders.length + }, + { + key: 'invoices', + label: 'Invoices', + icon: 'receipt', + count: counts?.invoices || crmData.invoices.length + }, ] as const; - if (isLoading && !crmData.leads.length) { - return ; - } - - if (hasError && !crmData.leads.length) { - return ; - } - - const renderTabContent = useCallback(() => { const commonFlatListProps = { numColumns: 1, @@ -385,6 +417,15 @@ const ZohoCrmDataScreen: React.FC = () => { } }, [selectedTab, crmData, handleCardPress, loadMoreData, renderFooter, refreshing, handleRefresh]); + // Conditional returns after all hooks + if (isLoading && !crmData.leads.length) { + return ; + } + + if (hasError && !crmData.leads.length) { + return ; + } + return ( {/* Fixed Header */} diff --git a/src/modules/crm/zoho/services/crmAPI.ts b/src/modules/crm/zoho/services/crmAPI.ts index 8c196bc..7c26f20 100644 --- a/src/modules/crm/zoho/services/crmAPI.ts +++ b/src/modules/crm/zoho/services/crmAPI.ts @@ -76,5 +76,181 @@ export const crmAPI = { }; return http.get>>(`/api/v1/integrations/zoho/crm/invoices`, queryParams); }, + + // Get counts for all CRM modules + getCrmCounts: () => { + return http.get<{ + status: string; + message: string; + data: { + data: { + accounts: { count: number; success: boolean }; + leads: { count: number; success: boolean }; + tasks: { count: number; success: boolean }; + invoices: { count: number; success: boolean }; + sales_orders: { count: number; success: boolean }; + purchase_orders: { count: number; success: boolean }; + deals: { count: number; success: boolean }; + contacts: { count: number; success: boolean }; + vendors: { count: number; success: boolean }; + }; + info: { + totalModules: number; + successfulModules: number; + failedModules: number; + }; + }; + timestamp: string; + }>('/api/v1/integrations/zoho/crm/counts', { provider: 'zoho' }); + }, + + // Get comprehensive CRM KPIs report + getCrmKPIs: () => { + return http.get<{ + status: string; + message: string; + data: { + businessOverview: { + totalRecords: number; + moduleCounts: { + leads: number; + contacts: number; + accounts: number; + deals: number; + tasks: number; + vendors: number; + invoices: number; + salesOrders: number; + purchaseOrders: number; + }; + revenueMetrics: { + totalRevenue: number; + totalDealValue: number; + totalInvoices: number; + averageInvoiceValue: number; + overdueAmount: number; + overdueCount: number; + revenueByMonth: Array<{ month: string; revenue: number }>; + topRevenueAccounts: Array<{ account: string; revenue: number }>; + }; + growthMetrics: { + recentLeads: number; + previousLeads: number; + recentDeals: number; + previousDeals: number; + leadGrowthRate: string; + dealGrowthRate: string; + }; + }; + salesPipeline: { + totalLeads: number; + totalDeals: number; + qualifiedLeads: number; + convertedLeads: number; + openDeals: number; + closedWonDeals: number; + totalPipelineValue: number; + conversionRate: string; + qualificationRate: string; + winRate: string; + dealStages: Record; + leadSources: Record; + conversionFunnel: { + leads: { count: number; percentage: number }; + qualified: { count: number; percentage: string }; + converted: { count: number; percentage: string }; + deals: { count: number; percentage: number }; + closedWon: { count: number; percentage: string }; + }; + }; + operationalEfficiency: { + totalTasks: number; + completedTasks: number; + overdueTasks: number; + highPriorityTasks: number; + taskCompletionRate: string; + overdueRate: string; + taskStatus: Record; + invoiceStatus: Record; + orderStatus: { + salesOrders: Record; + purchaseOrders: Record; + }; + }; + customerRelationships: { + totalContacts: number; + totalAccounts: number; + totalVendors: number; + recentContacts: number; + accountsWithRevenue: number; + contactToAccountRatio: string; + industryDistribution: Record; + contactSources: Record; + accountOwnership: Record; + }; + financialHealth: { + totalRevenue: number; + totalInvoices: number; + averageInvoiceValue: number; + overdueAmount: number; + revenueByMonth: Array<{ month: string; revenue: number }>; + topRevenueAccounts: Array<{ account: string; revenue: number }>; + }; + businessInsights: { + topPerformingSources: Array<{ + source: string; + totalLeads: number; + qualifiedLeads: number; + conversionRate: string; + avgQuality: string; + }>; + topRevenueOwners: Array<{ + owner: string; + count: number; + total: number; + }>; + mostActiveOwners: Array<{ + owner: string; + count: number; + value: number; + }>; + businessHealth: { + overallScore: number; + leadGeneration: number; + processEfficiency: number; + qualityMetrics: number; + }; + recommendations: Array<{ + category: string; + priority: string; + title: string; + description: string; + action: string; + }>; + }; + generatedAt: string; + advancedFinancialKPIs: { + revenueGrowthRate: { value: number; percentage: string; trend: string }; + grossMargin: { value: number; percentage: string; totalRevenue: number; totalCOGS: number; grossProfit: number }; + netProfitMargin: { value: number; percentage: string; totalRevenue: number; totalCosts: number; netProfit: number }; + operatingCashFlow: { cashIn: number; cashOut: number; netCashFlow: number; cashFlowRatio: string }; + cashRunway: { months: string; status: string; monthlyCashIn: number; monthlyCashOut: number }; + customerAcquisitionCost: { value: number; marketingSpend: number; newCustomers: number; status: string }; + customerLifetimeValue: { value: number; totalRevenue: number; totalCustomers: number; status: string }; + ltvToCacRatio: { ratio: string; status: string }; + netRevenueRetention: { percentage: string; recurringRevenue: number; currentCustomers: number; status: string }; + churnRate: { percentage: string; lostCustomers: number; totalCustomers: number; status: string }; + averageRevenuePerAccount: { value: number; totalRevenue: number; totalAccounts: number; status: string }; + burnMultiple: { multiple: string; totalBurn: number; newARR: number; status: string }; + salesCycleLength: { averageDays: number; status: string }; + netPromoterScore: { score: number; promoters: number; totalCustomers: number; status: string; note: string }; + daysSalesOutstanding: { days: number; totalOutstanding: number; averageDailySales: number; status: string }; + growthEfficiencyRatio: { ratio: string; status: string }; + ebitda: { value: number; totalRevenue: number; totalCosts: number; margin: string; status: string }; + }; + }; + timestamp: string; + }>('/api/v1/reports/crm/kpis'); + }, }; diff --git a/src/modules/crm/zoho/store/crmSlice.ts b/src/modules/crm/zoho/store/crmSlice.ts index 745712c..b0a252d 100644 --- a/src/modules/crm/zoho/store/crmSlice.ts +++ b/src/modules/crm/zoho/store/crmSlice.ts @@ -63,6 +63,22 @@ export interface CrmState { // Statistics stats: CrmStats | null; + // Module counts + counts: { + leads: number; + tasks: number; + contacts: number; + deals: number; + salesOrders: number; + purchaseOrders: number; + invoices: number; + accounts: number; + vendors: number; + } | null; + + // Comprehensive KPIs report + kpis: any | null; + // Last updated timestamps lastUpdated: { leads: string | null; @@ -115,6 +131,8 @@ const initialState: CrmState = { invoices: { page: 1, count: 0, moreRecords: false }, }, stats: null, + counts: null, + kpis: null, lastUpdated: { leads: null, tasks: null, @@ -205,6 +223,41 @@ export const fetchInvoices = createAsyncThunk( } ); +// Fetch CRM counts +export const fetchCrmCounts = createAsyncThunk( + 'crm/fetchCounts', + async () => { + const response = await crmAPI.getCrmCounts(); + return response.data?.data || { + data: { + accounts: { count: 0, success: false }, + leads: { count: 0, success: false }, + tasks: { count: 0, success: false }, + invoices: { count: 0, success: false }, + sales_orders: { count: 0, success: false }, + purchase_orders: { count: 0, success: false }, + deals: { count: 0, success: false }, + contacts: { count: 0, success: false }, + vendors: { count: 0, success: false }, + }, + info: { + totalModules: 0, + successfulModules: 0, + failedModules: 0, + } + }; + } +); + +// Fetch comprehensive CRM KPIs +export const fetchCrmKPIs = createAsyncThunk( + 'crm/fetchKPIs', + async () => { + const response = await crmAPI.getCrmKPIs(); + return response.data?.data || null; + } +); + // Fetch all CRM data export const fetchAllCrmData = createAsyncThunk( 'crm/fetchAllData', @@ -564,6 +617,41 @@ const crmSlice = createSlice({ state.errors.salesOrders = errorMessage; state.errors.purchaseOrders = errorMessage; state.errors.invoices = errorMessage; + }) + + // Fetch CRM counts + .addCase(fetchCrmCounts.pending, (state) => { + // No loading state needed for counts as it's fetched in background + }) + .addCase(fetchCrmCounts.fulfilled, (state, action) => { + const { data } = action.payload; + state.counts = { + leads: data.leads?.count || 0, + tasks: data.tasks?.count || 0, + contacts: data.contacts?.count || 0, + deals: data.deals?.count || 0, + salesOrders: data.sales_orders?.count || 0, + purchaseOrders: data.purchase_orders?.count || 0, + invoices: data.invoices?.count || 0, + accounts: data.accounts?.count || 0, + vendors: data.vendors?.count || 0, + }; + }) + .addCase(fetchCrmCounts.rejected, (state) => { + // Keep existing counts on error + console.warn('Failed to fetch CRM counts'); + }) + + // Fetch CRM KPIs + .addCase(fetchCrmKPIs.pending, (state) => { + // No loading state needed for KPIs as it's fetched in background + }) + .addCase(fetchCrmKPIs.fulfilled, (state, action) => { + state.kpis = action.payload; + }) + .addCase(fetchCrmKPIs.rejected, (state) => { + // Keep existing KPIs on error + console.warn('Failed to fetch CRM KPIs'); }); }, }); diff --git a/src/modules/crm/zoho/store/selectors.ts b/src/modules/crm/zoho/store/selectors.ts index ba9615b..c00866d 100644 --- a/src/modules/crm/zoho/store/selectors.ts +++ b/src/modules/crm/zoho/store/selectors.ts @@ -45,6 +45,12 @@ export const selectPurchaseOrdersError = (state: RootState) => state.crm.errors. export const selectInvoicesError = (state: RootState) => state.crm.errors.invoices; // Computed selectors for dashboard +// Counts selector +export const selectCrmCounts = (state: RootState) => state.crm.counts; + +// KPIs selector +export const selectCrmKPIs = (state: RootState) => state.crm.kpis; + export const selectCrmStats = createSelector( [selectLeads, selectTasks, selectContacts, selectDeals, selectSalesOrders, selectPurchaseOrders, selectInvoices], (leads, tasks, contacts, deals, salesOrders, purchaseOrders, invoices): CrmStats => { diff --git a/src/modules/integrations/screens/ZohoAuth.tsx b/src/modules/integrations/screens/ZohoAuth.tsx index f266ef2..50b7348 100644 --- a/src/modules/integrations/screens/ZohoAuth.tsx +++ b/src/modules/integrations/screens/ZohoAuth.tsx @@ -15,6 +15,7 @@ import { useNavigation } from '@react-navigation/native'; import { manageToken } from '../services/integrationAPI'; import { useSelector } from 'react-redux'; import { RootState } from '@/store/store'; +import http from '@/services/http'; // Types type ServiceKey = 'zohoProjects' | 'zohoCRM' | 'zohoBooks' | 'zohoPeople'; @@ -38,6 +39,8 @@ interface ZohoAuthState { loading: boolean; error: string | null; currentUrl: string; + processing: boolean; + processingStep: string; } // Zoho OAuth Configuration @@ -174,23 +177,98 @@ const ZohoAuth: React.FC = ({ loading: true, error: null, currentUrl: buildZohoAuthUrl(currentScope), + processing: false, + processingStep: '', }); // Backend exchange mode: only log and return the authorization code to the caller const handleAuthorizationCode = useCallback(async (authCode: string) => { console.log('[ZohoAuth] Authorization code received:', authCode); console.log('[ZohoAuth] Send this code to your backend to exchange for tokens.',user); - const response = await manageToken.manageToken({ authorization_code: authCode, id: user?.uuid, service_name: 'zoho', access_token: accessToken }); - console.log('[ZohoAuth] Response from manageToken:', response); - // Return the code via onAuthSuccess using the existing shape - onAuthSuccess?.({ - accessToken: authCode, // This is the AUTHORIZATION CODE, not an access token - tokenType: 'authorization_code', - scope: currentScope, - expiresIn: undefined, - refreshToken: undefined, - }); - }, [onAuthSuccess, currentScope]); + + // Show processing modal + setState(prev => ({ + ...prev, + processing: true, + processingStep: 'Storing credentials and preparing data setup...' + })); + + try { + const response = await manageToken.manageToken({ + authorization_code: authCode, + id: user?.uuid || '', + service_name: 'zoho', + access_token: accessToken || '' + }); + + console.log('[ZohoAuth] Response from manageToken:', response); + + // Check if response status is success + if (response?.data && typeof response.data === 'object' && 'status' in response.data && response.data.status === 'success') { + console.log('[ZohoAuth] Token exchange successful, scheduling bulk read job...'); + + // Update processing step + setState(prev => ({ + ...prev, + processingStep: 'Initializing data import and bulk read job...' + })); + + try { + // Schedule bulk read job + const bulkReadResponse = await http.post('/api/v1/integrations/zoho/bulk-read/schedule?provider=zoho', {}, { + headers: { + 'Authorization': `Bearer ${accessToken}`, + }, + }); + + console.log('[ZohoAuth] Bulk read job scheduled:', bulkReadResponse.data); + + // Update processing step + setState(prev => ({ + ...prev, + processingStep: 'Data import process initiated successfully!' + })); + + // Wait a moment to show success message + await new Promise(resolve => setTimeout(resolve, 1500)); + } catch (bulkReadError) { + console.warn('[ZohoAuth] Failed to schedule bulk read job:', bulkReadError); + // Don't fail the auth process if bulk read scheduling fails + } + } else { + const errorMessage = response?.data && typeof response.data === 'object' && 'message' in response.data + ? response.data.message + : 'Unknown error'; + console.warn('[ZohoAuth] Token exchange failed:', errorMessage); + } + + // Hide processing modal and call success callback + setState(prev => ({ ...prev, processing: false, processingStep: '' })); + + // Return the code via onAuthSuccess using the existing shape + onAuthSuccess?.({ + accessToken: authCode, // This is the AUTHORIZATION CODE, not an access token + tokenType: 'authorization_code', + scope: currentScope, + expiresIn: undefined, + refreshToken: undefined, + }); + } catch (error) { + console.error('[ZohoAuth] Error during token exchange:', error); + + // Hide processing modal and call success callback + setState(prev => ({ ...prev, processing: false, processingStep: '' })); + + // Still call onAuthSuccess with the auth code even if bulk read fails + onAuthSuccess?.({ + accessToken: authCode, + tokenType: 'authorization_code', + scope: currentScope, + expiresIn: undefined, + refreshToken: undefined, + }); + } + }, [onAuthSuccess, currentScope, user?.uuid, accessToken]); // Handle WebView navigation state changes const handleNavigationStateChange = useCallback((navState: any) => { @@ -372,6 +450,28 @@ const ZohoAuth: React.FC = ({ )} + {/* Processing Modal */} + {state.processing && ( + + + + + + + Setting Up Integration + + + {state.processingStep} + + + + + + + + + )} + {/* Loading Overlay */} {state.loading && !state.error && ( @@ -383,7 +483,7 @@ const ZohoAuth: React.FC = ({ )} {/* WebView */} - {!state.error && ( + {!state.error && !state.processing && (