diff --git a/src/modules/crm/zoho/screens/ZohoCrmDataScreen.tsx b/src/modules/crm/zoho/screens/ZohoCrmDataScreen.tsx index 3432a98..a6689ba 100644 --- a/src/modules/crm/zoho/screens/ZohoCrmDataScreen.tsx +++ b/src/modules/crm/zoho/screens/ZohoCrmDataScreen.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useMemo } from 'react'; +import React, { useState, useEffect, useMemo, useCallback } from 'react'; import { View, Text, @@ -8,6 +8,7 @@ import { RefreshControl, FlatList, Alert, + ActivityIndicator, } from 'react-native'; import { useSelector, useDispatch } from 'react-redux'; import type { AppDispatch } from '@/store/store'; @@ -26,9 +27,26 @@ import { selectPurchaseOrders, selectInvoices, selectCrmLoading, - selectCrmErrors + selectCrmErrors, + selectLeadsPagination, + selectTasksPagination, + selectContactsPagination, + selectDealsPagination, + selectSalesOrdersPagination, + selectPurchaseOrdersPagination, + selectInvoicesPagination } from '../store/selectors'; -import { fetchAllCrmData } from '../store/crmSlice'; +import { + fetchAllCrmData, + fetchLeads, + fetchTasks, + fetchContacts, + fetchDeals, + resetLeadsPagination, + resetTasksPagination, + resetContactsPagination, + resetDealsPagination +} from '../store/crmSlice'; import type { RootState } from '@/store/store'; const ZohoCrmDataScreen: React.FC = () => { @@ -47,6 +65,15 @@ const ZohoCrmDataScreen: React.FC = () => { const invoices = useSelector(selectInvoices); const loading = useSelector(selectCrmLoading); const errors = useSelector(selectCrmErrors); + + // Pagination selectors + const leadsPagination = useSelector(selectLeadsPagination); + const tasksPagination = useSelector(selectTasksPagination); + const contactsPagination = useSelector(selectContactsPagination); + const dealsPagination = useSelector(selectDealsPagination); + const salesOrdersPagination = useSelector(selectSalesOrdersPagination); + const purchaseOrdersPagination = useSelector(selectPurchaseOrdersPagination); + const invoicesPagination = useSelector(selectInvoicesPagination); // Create CRM data object from Redux state const crmData: CrmData = useMemo(() => ({ @@ -60,7 +87,7 @@ const ZohoCrmDataScreen: React.FC = () => { }), [leads, tasks, contacts, deals, salesOrders, purchaseOrders, invoices]); // Fetch CRM data using Redux - const fetchCrmData = async (showRefresh = false) => { + const fetchCrmData = useCallback(async (showRefresh = false) => { try { if (showRefresh) { setRefreshing(true); @@ -78,23 +105,87 @@ const ZohoCrmDataScreen: React.FC = () => { } finally { setRefreshing(false); } - }; + }, [dispatch]); + + // Load more data for infinite scrolling + const loadMoreData = useCallback(async (dataType: string) => { + try { + let pagination; + let fetchAction; + let resetAction; + + switch (dataType) { + case 'leads': + pagination = leadsPagination; + fetchAction = fetchLeads; + resetAction = resetLeadsPagination; + break; + case 'tasks': + pagination = tasksPagination; + fetchAction = fetchTasks; + resetAction = resetTasksPagination; + break; + case 'contacts': + pagination = contactsPagination; + fetchAction = fetchContacts; + resetAction = resetContactsPagination; + break; + case 'deals': + pagination = dealsPagination; + fetchAction = fetchDeals; + resetAction = resetDealsPagination; + break; + default: + return; // Only leads, tasks, contacts, and deals support infinite scrolling for now + } + + // Check if there are more records and not currently loading + if (!pagination.moreRecords || loading[dataType as keyof typeof loading]) { + return; + } + + // Fetch next page + await (dispatch(fetchAction({ + page: pagination.page + 1, + limit: 20, + append: true + }) as any)).unwrap(); + + } catch (err) { + showError(`Failed to load more ${dataType}`); + } + }, [dispatch, leadsPagination, tasksPagination, contactsPagination, dealsPagination, loading]); useEffect(() => { fetchCrmData(); }, []); - const handleRefresh = () => { + const handleRefresh = useCallback(() => { fetchCrmData(true); - }; + }, [fetchCrmData]); - const handleRetry = () => { + const handleRetry = useCallback(() => { fetchCrmData(); - }; + }, [fetchCrmData]); - const handleCardPress = (item: any, type: string) => { + const handleCardPress = useCallback((item: any, type: string) => { showInfo(`Viewing ${type}: ${item.name || item.subject || `${item.firstName} ${item.lastName}`}`); - }; + }, []); + + // Render loading footer for infinite scroll + const renderFooter = useCallback((dataType: string) => { + const isLoadingMore = loading[dataType as keyof typeof loading]; + if (!isLoadingMore) return null; + + return ( + + + + Loading more... + + + ); + }, [loading, colors]); // Get current loading state and error const isLoading = loading.leads || loading.tasks || loading.contacts || loading.deals || @@ -123,7 +214,7 @@ const ZohoCrmDataScreen: React.FC = () => { } - const renderTabContent = () => { + const renderTabContent = useCallback(() => { const commonFlatListProps = { numColumns: 1, showsVerticalScrollIndicator: false, @@ -144,7 +235,10 @@ const ZohoCrmDataScreen: React.FC = () => { onPress={() => handleCardPress(item, 'Lead')} /> )} - keyExtractor={(item) => item.id} + keyExtractor={(item) => `lead-${item.id}`} + onEndReached={() => loadMoreData('leads')} + onEndReachedThreshold={0.1} + ListFooterComponent={() => renderFooter('leads')} {...commonFlatListProps} /> ); @@ -158,7 +252,10 @@ const ZohoCrmDataScreen: React.FC = () => { onPress={() => handleCardPress(item, 'Task')} /> )} - keyExtractor={(item) => item.id} + keyExtractor={(item) => `task-${item.id}`} + onEndReached={() => loadMoreData('tasks')} + onEndReachedThreshold={0.1} + ListFooterComponent={() => renderFooter('tasks')} {...commonFlatListProps} /> ); @@ -172,7 +269,10 @@ const ZohoCrmDataScreen: React.FC = () => { onPress={() => handleCardPress(item, 'Contact')} /> )} - keyExtractor={(item) => item.id} + keyExtractor={(item) => `contact-${item.id}`} + onEndReached={() => loadMoreData('contacts')} + onEndReachedThreshold={0.1} + ListFooterComponent={() => renderFooter('contacts')} {...commonFlatListProps} /> ); @@ -186,7 +286,10 @@ const ZohoCrmDataScreen: React.FC = () => { onPress={() => handleCardPress(item, 'Deal')} /> )} - keyExtractor={(item) => item.id} + keyExtractor={(item) => `deal-${item.id}`} + onEndReached={() => loadMoreData('deals')} + onEndReachedThreshold={0.1} + ListFooterComponent={() => renderFooter('deals')} {...commonFlatListProps} /> ); @@ -200,7 +303,7 @@ const ZohoCrmDataScreen: React.FC = () => { onPress={() => handleCardPress(item, 'Sales Order')} /> )} - keyExtractor={(item) => item.id} + keyExtractor={(item) => `sales-order-${item.id}`} {...commonFlatListProps} /> ); @@ -214,7 +317,7 @@ const ZohoCrmDataScreen: React.FC = () => { onPress={() => handleCardPress(item, 'Purchase Order')} /> )} - keyExtractor={(item) => item.id} + keyExtractor={(item) => `purchase-order-${item.id}`} {...commonFlatListProps} /> ); @@ -228,14 +331,14 @@ const ZohoCrmDataScreen: React.FC = () => { onPress={() => handleCardPress(item, 'Invoice')} /> )} - keyExtractor={(item) => item.id} + keyExtractor={(item) => `invoice-${item.id}`} {...commonFlatListProps} /> ); default: return null; } - }; + }, [selectedTab, crmData, handleCardPress, loadMoreData, renderFooter, refreshing, handleRefresh]); return ( @@ -366,6 +469,16 @@ const styles = StyleSheet.create({ paddingHorizontal: 16, paddingBottom: 20, }, + footerLoader: { + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + paddingVertical: 16, + }, + footerText: { + marginLeft: 8, + fontSize: 14, + }, }); export default ZohoCrmDataScreen; diff --git a/src/modules/crm/zoho/services/crmAPI.ts b/src/modules/crm/zoho/services/crmAPI.ts index 3efc410..8c196bc 100644 --- a/src/modules/crm/zoho/services/crmAPI.ts +++ b/src/modules/crm/zoho/services/crmAPI.ts @@ -25,8 +25,8 @@ export const crmAPI = { provider: 'zoho', service: 'crm', resource, - page: 1, - limit: 20, + page: params?.page || 1, + limit: params?.limit || 20, ...params }; @@ -47,13 +47,34 @@ export const crmAPI = { crmAPI.getCrmData('deals', params), // New API endpoints for sales orders, purchase orders, and invoices - getSalesOrders: (params?: CrmSearchParams) => - http.get>>(`/api/v1/integrations/zoho/crm/sales-orders?provider=zoho`, params), + getSalesOrders: (params?: CrmSearchParams) => { + const queryParams = { + provider: 'zoho', + page: params?.page || 1, + limit: params?.limit || 20, + ...params + }; + return http.get>>(`/api/v1/integrations/zoho/crm/sales-orders`, queryParams); + }, - getPurchaseOrders: (params?: CrmSearchParams) => - http.get>>(`/api/v1/integrations/zoho/crm/purchase-orders?provider=zoho`, params), + getPurchaseOrders: (params?: CrmSearchParams) => { + const queryParams = { + provider: 'zoho', + page: params?.page || 1, + limit: params?.limit || 20, + ...params + }; + return http.get>>(`/api/v1/integrations/zoho/crm/purchase-orders`, queryParams); + }, - getInvoices: (params?: CrmSearchParams) => - http.get>>(`/api/v1/integrations/zoho/crm/invoices?provider=zoho`, params), + getInvoices: (params?: CrmSearchParams) => { + const queryParams = { + provider: 'zoho', + page: params?.page || 1, + limit: params?.limit || 20, + ...params + }; + return http.get>>(`/api/v1/integrations/zoho/crm/invoices`, queryParams); + }, }; diff --git a/src/modules/crm/zoho/store/crmSlice.ts b/src/modules/crm/zoho/store/crmSlice.ts index 8c6620b..11d854f 100644 --- a/src/modules/crm/zoho/store/crmSlice.ts +++ b/src/modules/crm/zoho/store/crmSlice.ts @@ -127,36 +127,48 @@ const initialState: CrmState = { }, }; -// Async thunks +// Async thunks for infinite scrolling export const fetchLeads = createAsyncThunk( 'crm/fetchLeads', - async (params?: CrmSearchParams) => { + async (params?: CrmSearchParams & { append?: boolean }) => { const response = await crmAPI.getLeads(params); - return response.data?.data || { data: [], info: { count: 0, moreRecords: false, page: 1 } }; + return { + data: response.data?.data || { data: [], info: { count: 0, moreRecords: false, page: 1 } }, + append: params?.append || false + }; } ); export const fetchTasks = createAsyncThunk( 'crm/fetchTasks', - async (params?: CrmSearchParams) => { + async (params?: CrmSearchParams & { append?: boolean }) => { const response = await crmAPI.getTasks(params); - return response.data?.data || { data: [], info: { count: 0, moreRecords: false, page: 1 } }; + return { + data: response.data?.data || { data: [], info: { count: 0, moreRecords: false, page: 1 } }, + append: params?.append || false + }; } ); export const fetchContacts = createAsyncThunk( 'crm/fetchContacts', - async (params?: CrmSearchParams) => { + async (params?: CrmSearchParams & { append?: boolean }) => { const response = await crmAPI.getContacts(params); - return response.data?.data || { data: [], info: { count: 0, moreRecords: false, page: 1 } }; + return { + data: response.data?.data || { data: [], info: { count: 0, moreRecords: false, page: 1 } }, + append: params?.append || false + }; } ); export const fetchDeals = createAsyncThunk( 'crm/fetchDeals', - async (params?: CrmSearchParams) => { + async (params?: CrmSearchParams & { append?: boolean }) => { const response = await crmAPI.getDeals(params); - return response.data?.data || { data: [], info: { count: 0, moreRecords: false, page: 1 } }; + return { + data: response.data?.data || { data: [], info: { count: 0, moreRecords: false, page: 1 } }, + append: params?.append || false + }; } ); @@ -266,6 +278,23 @@ const crmSlice = createSlice({ setInvoicesPage: (state, action: PayloadAction) => { state.pagination.invoices.page = action.payload; }, + // Reset pagination for specific data types + resetLeadsPagination: (state) => { + state.pagination.leads = { page: 1, count: 0, moreRecords: false }; + state.leads = []; + }, + resetTasksPagination: (state) => { + state.pagination.tasks = { page: 1, count: 0, moreRecords: false }; + state.tasks = []; + }, + resetContactsPagination: (state) => { + state.pagination.contacts = { page: 1, count: 0, moreRecords: false }; + state.contacts = []; + }, + resetDealsPagination: (state) => { + state.pagination.deals = { page: 1, count: 0, moreRecords: false }; + state.deals = []; + }, }, extraReducers: (builder) => { // Fetch leads @@ -276,8 +305,17 @@ const crmSlice = createSlice({ }) .addCase(fetchLeads.fulfilled, (state, action) => { state.loading.leads = false; - state.leads = action.payload.data || []; - state.pagination.leads = action.payload.info || { page: 1, count: 0, moreRecords: false }; + const { data, append } = action.payload; + + if (append) { + // Append new data to existing data for infinite scrolling + state.leads = [...state.leads, ...(data.data || [])]; + } else { + // Replace data for initial load or refresh + state.leads = data.data || []; + } + + state.pagination.leads = data.info || { page: 1, count: 0, moreRecords: false }; state.lastUpdated.leads = new Date().toISOString(); }) .addCase(fetchLeads.rejected, (state, action) => { @@ -292,8 +330,17 @@ const crmSlice = createSlice({ }) .addCase(fetchTasks.fulfilled, (state, action) => { state.loading.tasks = false; - state.tasks = action.payload.data || []; - state.pagination.tasks = action.payload.info || { page: 1, count: 0, moreRecords: false }; + const { data, append } = action.payload; + + if (append) { + // Append new data to existing data for infinite scrolling + state.tasks = [...state.tasks, ...(data.data || [])]; + } else { + // Replace data for initial load or refresh + state.tasks = data.data || []; + } + + state.pagination.tasks = data.info || { page: 1, count: 0, moreRecords: false }; state.lastUpdated.tasks = new Date().toISOString(); }) .addCase(fetchTasks.rejected, (state, action) => { @@ -308,8 +355,17 @@ const crmSlice = createSlice({ }) .addCase(fetchContacts.fulfilled, (state, action) => { state.loading.contacts = false; - state.contacts = action.payload.data || []; - state.pagination.contacts = action.payload.info || { page: 1, count: 0, moreRecords: false }; + const { data, append } = action.payload; + + if (append) { + // Append new data to existing data for infinite scrolling + state.contacts = [...state.contacts, ...(data.data || [])]; + } else { + // Replace data for initial load or refresh + state.contacts = data.data || []; + } + + state.pagination.contacts = data.info || { page: 1, count: 0, moreRecords: false }; state.lastUpdated.contacts = new Date().toISOString(); }) .addCase(fetchContacts.rejected, (state, action) => { @@ -324,8 +380,17 @@ const crmSlice = createSlice({ }) .addCase(fetchDeals.fulfilled, (state, action) => { state.loading.deals = false; - state.deals = action.payload.data || []; - state.pagination.deals = action.payload.info || { page: 1, count: 0, moreRecords: false }; + const { data, append } = action.payload; + + if (append) { + // Append new data to existing data for infinite scrolling + state.deals = [...state.deals, ...(data.data || [])]; + } else { + // Replace data for initial load or refresh + state.deals = data.data || []; + } + + state.pagination.deals = data.info || { page: 1, count: 0, moreRecords: false }; state.lastUpdated.deals = new Date().toISOString(); }) .addCase(fetchDeals.rejected, (state, action) => { @@ -465,6 +530,10 @@ export const { setSalesOrdersPage, setPurchaseOrdersPage, setInvoicesPage, + resetLeadsPagination, + resetTasksPagination, + resetContactsPagination, + resetDealsPagination, } = crmSlice.actions; export default crmSlice.reducer; diff --git a/src/modules/crm/zoho/store/selectors.ts b/src/modules/crm/zoho/store/selectors.ts index 988f75a..ba9615b 100644 --- a/src/modules/crm/zoho/store/selectors.ts +++ b/src/modules/crm/zoho/store/selectors.ts @@ -17,6 +17,15 @@ export const selectCrmLoading = (state: RootState) => state.crm.loading; export const selectCrmErrors = (state: RootState) => state.crm.errors; export const selectCrmPagination = (state: RootState) => state.crm.pagination; +// Pagination selectors for infinite scrolling +export const selectLeadsPagination = (state: RootState) => state.crm.pagination.leads; +export const selectTasksPagination = (state: RootState) => state.crm.pagination.tasks; +export const selectContactsPagination = (state: RootState) => state.crm.pagination.contacts; +export const selectDealsPagination = (state: RootState) => state.crm.pagination.deals; +export const selectSalesOrdersPagination = (state: RootState) => state.crm.pagination.salesOrders; +export const selectPurchaseOrdersPagination = (state: RootState) => state.crm.pagination.purchaseOrders; +export const selectInvoicesPagination = (state: RootState) => state.crm.pagination.invoices; + // Loading selectors export const selectLeadsLoading = (state: RootState) => state.crm.loading.leads; export const selectTasksLoading = (state: RootState) => state.crm.loading.tasks; diff --git a/src/services/http.ts b/src/services/http.ts index b3df1cb..5b711cf 100644 --- a/src/services/http.ts +++ b/src/services/http.ts @@ -8,7 +8,7 @@ import { clearSelectedService } from '@/modules/integrations/store/integrationsS let pendingRequest: any = null; const http = create({ - baseURL: 'http://192.168.1.17:4000', + baseURL: 'http://10.175.59.235:4000', // baseURL: 'http://160.187.167.216', timeout: 10000, });