zoho crm data fetched from bulkread data
This commit is contained in:
parent
5428ac9f3e
commit
536a72ff4a
File diff suppressed because it is too large
Load Diff
@ -34,7 +34,8 @@ import {
|
|||||||
selectDealsPagination,
|
selectDealsPagination,
|
||||||
selectSalesOrdersPagination,
|
selectSalesOrdersPagination,
|
||||||
selectPurchaseOrdersPagination,
|
selectPurchaseOrdersPagination,
|
||||||
selectInvoicesPagination
|
selectInvoicesPagination,
|
||||||
|
selectCrmCounts
|
||||||
} from '../store/selectors';
|
} from '../store/selectors';
|
||||||
import {
|
import {
|
||||||
fetchAllCrmData,
|
fetchAllCrmData,
|
||||||
@ -45,6 +46,7 @@ import {
|
|||||||
fetchSalesOrders,
|
fetchSalesOrders,
|
||||||
fetchPurchaseOrders,
|
fetchPurchaseOrders,
|
||||||
fetchInvoices,
|
fetchInvoices,
|
||||||
|
fetchCrmCounts,
|
||||||
resetLeadsPagination,
|
resetLeadsPagination,
|
||||||
resetTasksPagination,
|
resetTasksPagination,
|
||||||
resetContactsPagination,
|
resetContactsPagination,
|
||||||
@ -80,6 +82,7 @@ const ZohoCrmDataScreen: React.FC = () => {
|
|||||||
const salesOrdersPagination = useSelector(selectSalesOrdersPagination);
|
const salesOrdersPagination = useSelector(selectSalesOrdersPagination);
|
||||||
const purchaseOrdersPagination = useSelector(selectPurchaseOrdersPagination);
|
const purchaseOrdersPagination = useSelector(selectPurchaseOrdersPagination);
|
||||||
const invoicesPagination = useSelector(selectInvoicesPagination);
|
const invoicesPagination = useSelector(selectInvoicesPagination);
|
||||||
|
const counts = useSelector(selectCrmCounts);
|
||||||
|
|
||||||
// Create CRM data object from Redux state
|
// Create CRM data object from Redux state
|
||||||
const crmData: CrmData = useMemo(() => ({
|
const crmData: CrmData = useMemo(() => ({
|
||||||
@ -184,6 +187,8 @@ const ZohoCrmDataScreen: React.FC = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchCrmData();
|
fetchCrmData();
|
||||||
|
// Fetch counts in parallel
|
||||||
|
dispatch(fetchCrmCounts());
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleRefresh = useCallback(() => {
|
const handleRefresh = useCallback(() => {
|
||||||
@ -196,8 +201,9 @@ const ZohoCrmDataScreen: React.FC = () => {
|
|||||||
dispatch(resetPurchaseOrdersPagination());
|
dispatch(resetPurchaseOrdersPagination());
|
||||||
dispatch(resetInvoicesPagination());
|
dispatch(resetInvoicesPagination());
|
||||||
|
|
||||||
// Then fetch fresh data
|
// Then fetch fresh data and counts
|
||||||
fetchCrmData(true);
|
fetchCrmData(true);
|
||||||
|
dispatch(fetchCrmCounts());
|
||||||
}, [fetchCrmData, dispatch]);
|
}, [fetchCrmData, dispatch]);
|
||||||
|
|
||||||
const handleRetry = useCallback(() => {
|
const handleRetry = useCallback(() => {
|
||||||
@ -230,26 +236,52 @@ const ZohoCrmDataScreen: React.FC = () => {
|
|||||||
errors.salesOrders || errors.purchaseOrders || errors.invoices;
|
errors.salesOrders || errors.purchaseOrders || errors.invoices;
|
||||||
|
|
||||||
|
|
||||||
// Tab configuration
|
// Tab configuration with counts from API
|
||||||
const tabs = [
|
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: 'leads',
|
||||||
{ key: 'contacts', label: 'Contacts', icon: 'account-group', count: crmData.contacts.length },
|
label: 'Leads',
|
||||||
{ key: 'deals', label: 'Deals', icon: 'handshake', count: crmData.deals.length },
|
icon: 'account-heart',
|
||||||
{ key: 'salesOrders', label: 'Sales Orders', icon: 'shopping', count: crmData.salesOrders.length },
|
count: counts?.leads || crmData.leads.length
|
||||||
{ key: 'purchaseOrders', label: 'Purchase Orders', icon: 'cart', count: crmData.purchaseOrders.length },
|
},
|
||||||
{ key: 'invoices', label: 'Invoices', icon: 'receipt', count: crmData.invoices.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;
|
] as const;
|
||||||
|
|
||||||
if (isLoading && !crmData.leads.length) {
|
|
||||||
return <LoadingSpinner />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasError && !crmData.leads.length) {
|
|
||||||
return <ErrorState onRetry={handleRetry} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const renderTabContent = useCallback(() => {
|
const renderTabContent = useCallback(() => {
|
||||||
const commonFlatListProps = {
|
const commonFlatListProps = {
|
||||||
numColumns: 1,
|
numColumns: 1,
|
||||||
@ -385,6 +417,15 @@ const ZohoCrmDataScreen: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [selectedTab, crmData, handleCardPress, loadMoreData, renderFooter, refreshing, handleRefresh]);
|
}, [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 (
|
return (
|
||||||
<View style={[styles.container, { backgroundColor: colors.background }]}>
|
<View style={[styles.container, { backgroundColor: colors.background }]}>
|
||||||
{/* Fixed Header */}
|
{/* Fixed Header */}
|
||||||
|
|||||||
@ -76,5 +76,181 @@ export const crmAPI = {
|
|||||||
};
|
};
|
||||||
return http.get<CrmApiResponse<CrmPaginatedResponse<any>>>(`/api/v1/integrations/zoho/crm/invoices`, queryParams);
|
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');
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -63,6 +63,22 @@ export interface CrmState {
|
|||||||
// Statistics
|
// Statistics
|
||||||
stats: CrmStats | null;
|
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
|
// Last updated timestamps
|
||||||
lastUpdated: {
|
lastUpdated: {
|
||||||
leads: string | null;
|
leads: string | null;
|
||||||
@ -115,6 +131,8 @@ const initialState: CrmState = {
|
|||||||
invoices: { page: 1, count: 0, moreRecords: false },
|
invoices: { page: 1, count: 0, moreRecords: false },
|
||||||
},
|
},
|
||||||
stats: null,
|
stats: null,
|
||||||
|
counts: null,
|
||||||
|
kpis: null,
|
||||||
lastUpdated: {
|
lastUpdated: {
|
||||||
leads: null,
|
leads: null,
|
||||||
tasks: 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
|
// Fetch all CRM data
|
||||||
export const fetchAllCrmData = createAsyncThunk(
|
export const fetchAllCrmData = createAsyncThunk(
|
||||||
'crm/fetchAllData',
|
'crm/fetchAllData',
|
||||||
@ -564,6 +617,41 @@ const crmSlice = createSlice({
|
|||||||
state.errors.salesOrders = errorMessage;
|
state.errors.salesOrders = errorMessage;
|
||||||
state.errors.purchaseOrders = errorMessage;
|
state.errors.purchaseOrders = errorMessage;
|
||||||
state.errors.invoices = 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');
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -45,6 +45,12 @@ export const selectPurchaseOrdersError = (state: RootState) => state.crm.errors.
|
|||||||
export const selectInvoicesError = (state: RootState) => state.crm.errors.invoices;
|
export const selectInvoicesError = (state: RootState) => state.crm.errors.invoices;
|
||||||
|
|
||||||
// Computed selectors for dashboard
|
// 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(
|
export const selectCrmStats = createSelector(
|
||||||
[selectLeads, selectTasks, selectContacts, selectDeals, selectSalesOrders, selectPurchaseOrders, selectInvoices],
|
[selectLeads, selectTasks, selectContacts, selectDeals, selectSalesOrders, selectPurchaseOrders, selectInvoices],
|
||||||
(leads, tasks, contacts, deals, salesOrders, purchaseOrders, invoices): CrmStats => {
|
(leads, tasks, contacts, deals, salesOrders, purchaseOrders, invoices): CrmStats => {
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import { useNavigation } from '@react-navigation/native';
|
|||||||
import { manageToken } from '../services/integrationAPI';
|
import { manageToken } from '../services/integrationAPI';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { RootState } from '@/store/store';
|
import { RootState } from '@/store/store';
|
||||||
|
import http from '@/services/http';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
type ServiceKey = 'zohoProjects' | 'zohoCRM' | 'zohoBooks' | 'zohoPeople';
|
type ServiceKey = 'zohoProjects' | 'zohoCRM' | 'zohoBooks' | 'zohoPeople';
|
||||||
@ -38,6 +39,8 @@ interface ZohoAuthState {
|
|||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
currentUrl: string;
|
currentUrl: string;
|
||||||
|
processing: boolean;
|
||||||
|
processingStep: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Zoho OAuth Configuration
|
// Zoho OAuth Configuration
|
||||||
@ -174,14 +177,74 @@ const ZohoAuth: React.FC<ZohoAuthProps> = ({
|
|||||||
loading: true,
|
loading: true,
|
||||||
error: null,
|
error: null,
|
||||||
currentUrl: buildZohoAuthUrl(currentScope),
|
currentUrl: buildZohoAuthUrl(currentScope),
|
||||||
|
processing: false,
|
||||||
|
processingStep: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Backend exchange mode: only log and return the authorization code to the caller
|
// Backend exchange mode: only log and return the authorization code to the caller
|
||||||
const handleAuthorizationCode = useCallback(async (authCode: string) => {
|
const handleAuthorizationCode = useCallback(async (authCode: string) => {
|
||||||
console.log('[ZohoAuth] Authorization code received:', authCode);
|
console.log('[ZohoAuth] Authorization code received:', authCode);
|
||||||
console.log('[ZohoAuth] Send this code to your backend to exchange for tokens.',user);
|
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 });
|
|
||||||
|
// 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);
|
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
|
// Return the code via onAuthSuccess using the existing shape
|
||||||
onAuthSuccess?.({
|
onAuthSuccess?.({
|
||||||
accessToken: authCode, // This is the AUTHORIZATION CODE, not an access token
|
accessToken: authCode, // This is the AUTHORIZATION CODE, not an access token
|
||||||
@ -190,7 +253,22 @@ const ZohoAuth: React.FC<ZohoAuthProps> = ({
|
|||||||
expiresIn: undefined,
|
expiresIn: undefined,
|
||||||
refreshToken: undefined,
|
refreshToken: undefined,
|
||||||
});
|
});
|
||||||
}, [onAuthSuccess, currentScope]);
|
} 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
|
// Handle WebView navigation state changes
|
||||||
const handleNavigationStateChange = useCallback((navState: any) => {
|
const handleNavigationStateChange = useCallback((navState: any) => {
|
||||||
@ -372,6 +450,28 @@ const ZohoAuth: React.FC<ZohoAuthProps> = ({
|
|||||||
</View>
|
</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 */}
|
{/* Loading Overlay */}
|
||||||
{state.loading && !state.error && (
|
{state.loading && !state.error && (
|
||||||
<View style={[styles.loadingOverlay, { backgroundColor: colors.background }]}>
|
<View style={[styles.loadingOverlay, { backgroundColor: colors.background }]}>
|
||||||
@ -383,7 +483,7 @@ const ZohoAuth: React.FC<ZohoAuthProps> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* WebView */}
|
{/* WebView */}
|
||||||
{!state.error && (
|
{!state.error && !state.processing && (
|
||||||
<WebView
|
<WebView
|
||||||
ref={webViewRef}
|
ref={webViewRef}
|
||||||
source={{ uri: state.currentUrl }}
|
source={{ uri: state.currentUrl }}
|
||||||
@ -502,6 +602,53 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
marginTop: 8,
|
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;
|
export default ZohoAuth;
|
||||||
|
|||||||
@ -8,9 +8,9 @@ import { clearSelectedService } from '@/modules/integrations/store/integrationsS
|
|||||||
let pendingRequest: any = null;
|
let pendingRequest: any = null;
|
||||||
|
|
||||||
const http = create({
|
const http = create({
|
||||||
// baseURL: 'http://10.175.59.235:4000',
|
// baseURL: 'http://192.168.1.20:4000',
|
||||||
// baseURL: 'http://160.187.167.216',
|
baseURL: 'http://160.187.167.216',
|
||||||
baseURL: 'https://gold-tires-sniff.loca.lt',
|
// baseURL: 'https://angry-gifts-shave.loca.lt',
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user