zoho crm data fetched from bulkread data

This commit is contained in:
yashwin-foxy 2025-09-26 18:09:24 +05:30
parent 5428ac9f3e
commit 536a72ff4a
7 changed files with 1465 additions and 325 deletions

File diff suppressed because it is too large Load Diff

View File

@ -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 <LoadingSpinner />;
}
if (hasError && !crmData.leads.length) {
return <ErrorState onRetry={handleRetry} />;
}
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 <LoadingSpinner />;
}
if (hasError && !crmData.leads.length) {
return <ErrorState onRetry={handleRetry} />;
}
return (
<View style={[styles.container, { backgroundColor: colors.background }]}>
{/* Fixed Header */}

View File

@ -76,5 +76,181 @@ export const crmAPI = {
};
return http.get<CrmApiResponse<CrmPaginatedResponse<any>>>(`/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<string, number>;
leadSources: Record<string, number>;
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<string, number>;
invoiceStatus: Record<string, number>;
orderStatus: {
salesOrders: Record<string, number>;
purchaseOrders: Record<string, number>;
};
};
customerRelationships: {
totalContacts: number;
totalAccounts: number;
totalVendors: number;
recentContacts: number;
accountsWithRevenue: number;
contactToAccountRatio: string;
industryDistribution: Record<string, number>;
contactSources: Record<string, number>;
accountOwnership: Record<string, number>;
};
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');
},
};

View File

@ -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');
});
},
});

View File

@ -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 => {

View File

@ -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<ZohoAuthProps> = ({
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<void>(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<ZohoAuthProps> = ({
</View>
)}
{/* Processing Modal */}
{state.processing && (
<View style={[styles.processingOverlay, { backgroundColor: 'rgba(0, 0, 0, 0.7)' }]}>
<View style={[styles.processingModal, { backgroundColor: colors.surface }]}>
<View style={styles.processingIconContainer}>
<ActivityIndicator size="large" color={colors.primary} />
</View>
<Text style={[styles.processingTitle, { color: colors.text, fontFamily: fonts.bold }]}>
Setting Up Integration
</Text>
<Text style={[styles.processingStep, { color: colors.textLight, fontFamily: fonts.regular }]}>
{state.processingStep}
</Text>
<View style={styles.processingDots}>
<View style={[styles.dot, { backgroundColor: colors.primary }]} />
<View style={[styles.dot, { backgroundColor: colors.primary }]} />
<View style={[styles.dot, { backgroundColor: colors.primary }]} />
</View>
</View>
</View>
)}
{/* Loading Overlay */}
{state.loading && !state.error && (
<View style={[styles.loadingOverlay, { backgroundColor: colors.background }]}>
@ -383,7 +483,7 @@ const ZohoAuth: React.FC<ZohoAuthProps> = ({
)}
{/* WebView */}
{!state.error && (
{!state.error && !state.processing && (
<WebView
ref={webViewRef}
source={{ uri: state.currentUrl }}
@ -502,6 +602,53 @@ const styles = StyleSheet.create({
fontSize: 14,
marginTop: 8,
},
processingOverlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
justifyContent: 'center',
alignItems: 'center',
zIndex: 2000,
},
processingModal: {
borderRadius: 16,
padding: 32,
alignItems: 'center',
minWidth: 280,
maxWidth: 320,
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.2,
shadowRadius: 8,
elevation: 8,
},
processingIconContainer: {
marginBottom: 20,
},
processingTitle: {
fontSize: 20,
marginBottom: 12,
textAlign: 'center',
},
processingStep: {
fontSize: 14,
textAlign: 'center',
lineHeight: 20,
marginBottom: 20,
},
processingDots: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
},
dot: {
width: 8,
height: 8,
borderRadius: 4,
marginHorizontal: 4,
},
});
export default ZohoAuth;

View File

@ -8,9 +8,9 @@ import { clearSelectedService } from '@/modules/integrations/store/integrationsS
let pendingRequest: any = null;
const http = create({
// baseURL: 'http://10.175.59.235:4000',
// baseURL: 'http://160.187.167.216',
baseURL: 'https://gold-tires-sniff.loca.lt',
// baseURL: 'http://192.168.1.20:4000',
baseURL: 'http://160.187.167.216',
// baseURL: 'https://angry-gifts-shave.loca.lt',
timeout: 10000,
});