diff --git a/src/modules/crm/zoho/screens/ZohoCrmDataScreen.tsx b/src/modules/crm/zoho/screens/ZohoCrmDataScreen.tsx index a6689ba..54956ea 100644 --- a/src/modules/crm/zoho/screens/ZohoCrmDataScreen.tsx +++ b/src/modules/crm/zoho/screens/ZohoCrmDataScreen.tsx @@ -42,10 +42,16 @@ import { fetchTasks, fetchContacts, fetchDeals, + fetchSalesOrders, + fetchPurchaseOrders, + fetchInvoices, resetLeadsPagination, resetTasksPagination, resetContactsPagination, - resetDealsPagination + resetDealsPagination, + resetSalesOrdersPagination, + resetPurchaseOrdersPagination, + resetInvoicesPagination } from '../store/crmSlice'; import type { RootState } from '@/store/store'; @@ -112,38 +118,55 @@ const ZohoCrmDataScreen: React.FC = () => { 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; + case 'salesOrders': + pagination = salesOrdersPagination; + fetchAction = fetchSalesOrders; + break; + case 'purchaseOrders': + pagination = purchaseOrdersPagination; + fetchAction = fetchPurchaseOrders; + break; + case 'invoices': + pagination = invoicesPagination; + fetchAction = fetchInvoices; break; default: - return; // Only leads, tasks, contacts, and deals support infinite scrolling for now + return; // Unknown data type } + console.log(`[Infinite Scroll] ${dataType}:`, { + currentPage: pagination.page, + moreRecords: pagination.moreRecords, + isLoading: loading[dataType as keyof typeof loading], + currentDataLength: crmData[dataType as keyof CrmData]?.length || 0 + }); + // Check if there are more records and not currently loading if (!pagination.moreRecords || loading[dataType as keyof typeof loading]) { + console.log(`[Infinite Scroll] ${dataType}: Skipping - no more records or already loading`); return; } + console.log(`[Infinite Scroll] ${dataType}: Fetching page ${pagination.page + 1}`); + // Fetch next page await (dispatch(fetchAction({ page: pagination.page + 1, @@ -151,18 +174,31 @@ const ZohoCrmDataScreen: React.FC = () => { append: true }) as any)).unwrap(); + console.log(`[Infinite Scroll] ${dataType}: Successfully loaded page ${pagination.page + 1}`); + } catch (err) { + console.error(`[Infinite Scroll] ${dataType}: Error:`, err); showError(`Failed to load more ${dataType}`); } - }, [dispatch, leadsPagination, tasksPagination, contactsPagination, dealsPagination, loading]); + }, [dispatch, leadsPagination, tasksPagination, contactsPagination, dealsPagination, salesOrdersPagination, purchaseOrdersPagination, invoicesPagination, loading, crmData]); useEffect(() => { fetchCrmData(); }, []); const handleRefresh = useCallback(() => { + // Reset pagination for all data types before refreshing + dispatch(resetLeadsPagination()); + dispatch(resetTasksPagination()); + dispatch(resetContactsPagination()); + dispatch(resetDealsPagination()); + dispatch(resetSalesOrdersPagination()); + dispatch(resetPurchaseOrdersPagination()); + dispatch(resetInvoicesPagination()); + + // Then fetch fresh data fetchCrmData(true); - }, [fetchCrmData]); + }, [fetchCrmData, dispatch]); const handleRetry = useCallback(() => { fetchCrmData(); @@ -304,6 +340,9 @@ const ZohoCrmDataScreen: React.FC = () => { /> )} keyExtractor={(item) => `sales-order-${item.id}`} + onEndReached={() => loadMoreData('salesOrders')} + onEndReachedThreshold={0.1} + ListFooterComponent={() => renderFooter('salesOrders')} {...commonFlatListProps} /> ); @@ -318,6 +357,9 @@ const ZohoCrmDataScreen: React.FC = () => { /> )} keyExtractor={(item) => `purchase-order-${item.id}`} + onEndReached={() => loadMoreData('purchaseOrders')} + onEndReachedThreshold={0.1} + ListFooterComponent={() => renderFooter('purchaseOrders')} {...commonFlatListProps} /> ); @@ -332,6 +374,9 @@ const ZohoCrmDataScreen: React.FC = () => { /> )} keyExtractor={(item) => `invoice-${item.id}`} + onEndReached={() => loadMoreData('invoices')} + onEndReachedThreshold={0.1} + ListFooterComponent={() => renderFooter('invoices')} {...commonFlatListProps} /> ); diff --git a/src/modules/crm/zoho/store/crmSlice.ts b/src/modules/crm/zoho/store/crmSlice.ts index 11d854f..745712c 100644 --- a/src/modules/crm/zoho/store/crmSlice.ts +++ b/src/modules/crm/zoho/store/crmSlice.ts @@ -174,25 +174,34 @@ export const fetchDeals = createAsyncThunk( export const fetchSalesOrders = createAsyncThunk( 'crm/fetchSalesOrders', - async (params?: CrmSearchParams) => { + async (params?: CrmSearchParams & { append?: boolean }) => { const response = await crmAPI.getSalesOrders(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 fetchPurchaseOrders = createAsyncThunk( 'crm/fetchPurchaseOrders', - async (params?: CrmSearchParams) => { + async (params?: CrmSearchParams & { append?: boolean }) => { const response = await crmAPI.getPurchaseOrders(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 fetchInvoices = createAsyncThunk( 'crm/fetchInvoices', - async (params?: CrmSearchParams) => { + async (params?: CrmSearchParams & { append?: boolean }) => { const response = await crmAPI.getInvoices(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 + }; } ); @@ -295,6 +304,18 @@ const crmSlice = createSlice({ state.pagination.deals = { page: 1, count: 0, moreRecords: false }; state.deals = []; }, + resetSalesOrdersPagination: (state) => { + state.pagination.salesOrders = { page: 1, count: 0, moreRecords: false }; + state.salesOrders = []; + }, + resetPurchaseOrdersPagination: (state) => { + state.pagination.purchaseOrders = { page: 1, count: 0, moreRecords: false }; + state.purchaseOrders = []; + }, + resetInvoicesPagination: (state) => { + state.pagination.invoices = { page: 1, count: 0, moreRecords: false }; + state.invoices = []; + }, }, extraReducers: (builder) => { // Fetch leads @@ -405,8 +426,17 @@ const crmSlice = createSlice({ }) .addCase(fetchSalesOrders.fulfilled, (state, action) => { state.loading.salesOrders = false; - state.salesOrders = action.payload.data || []; - state.pagination.salesOrders = 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.salesOrders = [...state.salesOrders, ...(data.data || [])]; + } else { + // Replace data for initial load or refresh + state.salesOrders = data.data || []; + } + + state.pagination.salesOrders = data.info || { page: 1, count: 0, moreRecords: false }; state.lastUpdated.salesOrders = new Date().toISOString(); }) .addCase(fetchSalesOrders.rejected, (state, action) => { @@ -421,8 +451,17 @@ const crmSlice = createSlice({ }) .addCase(fetchPurchaseOrders.fulfilled, (state, action) => { state.loading.purchaseOrders = false; - state.purchaseOrders = action.payload.data || []; - state.pagination.purchaseOrders = 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.purchaseOrders = [...state.purchaseOrders, ...(data.data || [])]; + } else { + // Replace data for initial load or refresh + state.purchaseOrders = data.data || []; + } + + state.pagination.purchaseOrders = data.info || { page: 1, count: 0, moreRecords: false }; state.lastUpdated.purchaseOrders = new Date().toISOString(); }) .addCase(fetchPurchaseOrders.rejected, (state, action) => { @@ -437,8 +476,17 @@ const crmSlice = createSlice({ }) .addCase(fetchInvoices.fulfilled, (state, action) => { state.loading.invoices = false; - state.invoices = action.payload.data || []; - state.pagination.invoices = 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.invoices = [...state.invoices, ...(data.data || [])]; + } else { + // Replace data for initial load or refresh + state.invoices = data.data || []; + } + + state.pagination.invoices = data.info || { page: 1, count: 0, moreRecords: false }; state.lastUpdated.invoices = new Date().toISOString(); }) .addCase(fetchInvoices.rejected, (state, action) => { @@ -534,6 +582,9 @@ export const { resetTasksPagination, resetContactsPagination, resetDealsPagination, + resetSalesOrdersPagination, + resetPurchaseOrdersPagination, + resetInvoicesPagination, } = crmSlice.actions; export default crmSlice.reducer; diff --git a/src/modules/integrations/screens/IntegrationCategoryScreen.tsx b/src/modules/integrations/screens/IntegrationCategoryScreen.tsx index e3e7c0a..ecb641f 100644 --- a/src/modules/integrations/screens/IntegrationCategoryScreen.tsx +++ b/src/modules/integrations/screens/IntegrationCategoryScreen.tsx @@ -63,18 +63,29 @@ const IntegrationCategoryScreen: React.FC = ({ route }) => { const [showZohoAuth, setShowZohoAuth] = React.useState(false); const [pendingService, setPendingService] = React.useState(null); const [isCheckingToken, setIsCheckingToken] = React.useState(false); + const [authenticatedServices, setAuthenticatedServices] = React.useState>(new Set()); const services = servicesMap[route.params.categoryKey] ?? []; + // Check for existing Zoho token - const checkZohoToken = async (serviceKey: string) => { + const checkZohoToken = async (serviceKey: string, forceReauth: boolean = false) => { try { setIsCheckingToken(true); + + if (forceReauth) { + // Force re-authentication by showing auth modal + setPendingService(serviceKey); + setShowZohoAuth(true); + return; + } + const response = await httpClient.get('/api/v1/users/decrypt-token?service_name=zoho'); const responseData = response.data as any; if (responseData.status === 'success' && responseData.data?.accessToken) { - // Token exists and is valid, navigate directly + // Token exists and is valid, mark as authenticated and navigate directly console.log('Zoho token found, navigating directly to:', serviceKey); + setAuthenticatedServices(prev => new Set([...prev, serviceKey])); dispatch(setSelectedService(serviceKey)); } else { // No valid token, show auth modal @@ -91,6 +102,24 @@ const IntegrationCategoryScreen: React.FC = ({ route }) => { } }; + // Handle re-authentication + const handleReAuthenticate = (serviceKey: string) => { + Alert.alert( + 'Re-authenticate', + 'This will allow you to change your organization or re-authorize access. Continue?', + [ + { + text: 'Cancel', + style: 'cancel', + }, + { + text: 'Re-authenticate', + onPress: () => checkZohoToken(serviceKey, true), + }, + ] + ); + }; + return ( = ({ route }) => { keyExtractor={item => item.key} contentContainerStyle={{ padding: 16 }} ItemSeparatorComponent={() => } - renderItem={({ item }) => ( - { - // Here we decide whether to require Zoho authentication first - const requiresZohoAuth = item.key === 'zohoProjects' || item.key === 'zohoPeople' || item.key === 'zohoBooks' || item.key === 'zohoCRM'; - console.log('key pressed', item.key); - - if (requiresZohoAuth) { - checkZohoToken(item.key); - } else { - // For non-Zoho services, navigate to Coming Soon screen - navigation.navigate('ComingSoon' as never); - } - }} - > - - - - {item.title} - {isCheckingToken && ( - - - Checking... - + renderItem={({ item }) => { + const requiresZohoAuth = item.key === 'zohoProjects' || item.key === 'zohoPeople' || item.key === 'zohoBooks' || item.key === 'zohoCRM'; + + return ( + + + { + console.log('key pressed', item.key); + + if (requiresZohoAuth) { + checkZohoToken(item.key); + } else { + // For non-Zoho services, navigate to Coming Soon screen + navigation.navigate('ComingSoon' as never); + } + }} + > + + + + {item.title} + {isCheckingToken && ( + + + Checking... + + + )} + + + {/* Re-authentication button for Zoho services - always visible */} + {requiresZohoAuth && ( + handleReAuthenticate(item.key)} + activeOpacity={0.7} + > + + + Re-auth + + + )} - )} - - )} + + ); + }} /> {/* Zoho Auth Modal */} @@ -144,6 +193,8 @@ const IntegrationCategoryScreen: React.FC = ({ route }) => { console.log('auth data i got',authData) setShowZohoAuth(false); if (pendingService) { + // Mark service as authenticated + setAuthenticatedServices(prev => new Set([...prev, pendingService])); dispatch(setSelectedService(pendingService)); setPendingService(null); } @@ -165,14 +216,23 @@ const IntegrationCategoryScreen: React.FC = ({ route }) => { const styles = StyleSheet.create({ container: { flex: 1 }, sep: { height: 1, opacity: 0.6 }, + serviceItem: { + marginVertical: 4, + }, row: { flexDirection: 'row', alignItems: 'center', paddingVertical: 12, + justifyContent: 'space-between', }, disabledRow: { opacity: 0.6, }, + mainServiceButton: { + flexDirection: 'row', + alignItems: 'center', + flex: 1, + }, iconCircle: { width: 36, height: 36, @@ -192,6 +252,19 @@ const styles = StyleSheet.create({ fontSize: 12, fontStyle: 'italic', }, + reauthButton: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 8, + paddingVertical: 6, + borderRadius: 6, + borderWidth: 1, + marginLeft: 8, + }, + reauthText: { + fontSize: 11, + marginLeft: 4, + }, }); export default IntegrationCategoryScreen; diff --git a/src/services/http.ts b/src/services/http.ts index 5b711cf..f29cbb6 100644 --- a/src/services/http.ts +++ b/src/services/http.ts @@ -8,8 +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://10.175.59.235:4000', // baseURL: 'http://160.187.167.216', + baseURL: 'https://gold-tires-sniff.loca.lt', timeout: 10000, });