paginationadded for all tabs in zoho crm

This commit is contained in:
yashwin-foxy 2025-09-25 19:02:58 +05:30
parent e53b229c03
commit 5428ac9f3e
4 changed files with 224 additions and 54 deletions

View File

@ -42,10 +42,16 @@ import {
fetchTasks, fetchTasks,
fetchContacts, fetchContacts,
fetchDeals, fetchDeals,
fetchSalesOrders,
fetchPurchaseOrders,
fetchInvoices,
resetLeadsPagination, resetLeadsPagination,
resetTasksPagination, resetTasksPagination,
resetContactsPagination, resetContactsPagination,
resetDealsPagination resetDealsPagination,
resetSalesOrdersPagination,
resetPurchaseOrdersPagination,
resetInvoicesPagination
} from '../store/crmSlice'; } from '../store/crmSlice';
import type { RootState } from '@/store/store'; import type { RootState } from '@/store/store';
@ -112,38 +118,55 @@ const ZohoCrmDataScreen: React.FC = () => {
try { try {
let pagination; let pagination;
let fetchAction; let fetchAction;
let resetAction;
switch (dataType) { switch (dataType) {
case 'leads': case 'leads':
pagination = leadsPagination; pagination = leadsPagination;
fetchAction = fetchLeads; fetchAction = fetchLeads;
resetAction = resetLeadsPagination;
break; break;
case 'tasks': case 'tasks':
pagination = tasksPagination; pagination = tasksPagination;
fetchAction = fetchTasks; fetchAction = fetchTasks;
resetAction = resetTasksPagination;
break; break;
case 'contacts': case 'contacts':
pagination = contactsPagination; pagination = contactsPagination;
fetchAction = fetchContacts; fetchAction = fetchContacts;
resetAction = resetContactsPagination;
break; break;
case 'deals': case 'deals':
pagination = dealsPagination; pagination = dealsPagination;
fetchAction = fetchDeals; 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; break;
default: 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 // Check if there are more records and not currently loading
if (!pagination.moreRecords || loading[dataType as keyof typeof loading]) { if (!pagination.moreRecords || loading[dataType as keyof typeof loading]) {
console.log(`[Infinite Scroll] ${dataType}: Skipping - no more records or already loading`);
return; return;
} }
console.log(`[Infinite Scroll] ${dataType}: Fetching page ${pagination.page + 1}`);
// Fetch next page // Fetch next page
await (dispatch(fetchAction({ await (dispatch(fetchAction({
page: pagination.page + 1, page: pagination.page + 1,
@ -151,18 +174,31 @@ const ZohoCrmDataScreen: React.FC = () => {
append: true append: true
}) as any)).unwrap(); }) as any)).unwrap();
console.log(`[Infinite Scroll] ${dataType}: Successfully loaded page ${pagination.page + 1}`);
} catch (err) { } catch (err) {
console.error(`[Infinite Scroll] ${dataType}: Error:`, err);
showError(`Failed to load more ${dataType}`); showError(`Failed to load more ${dataType}`);
} }
}, [dispatch, leadsPagination, tasksPagination, contactsPagination, dealsPagination, loading]); }, [dispatch, leadsPagination, tasksPagination, contactsPagination, dealsPagination, salesOrdersPagination, purchaseOrdersPagination, invoicesPagination, loading, crmData]);
useEffect(() => { useEffect(() => {
fetchCrmData(); fetchCrmData();
}, []); }, []);
const handleRefresh = useCallback(() => { 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(true);
}, [fetchCrmData]); }, [fetchCrmData, dispatch]);
const handleRetry = useCallback(() => { const handleRetry = useCallback(() => {
fetchCrmData(); fetchCrmData();
@ -304,6 +340,9 @@ const ZohoCrmDataScreen: React.FC = () => {
/> />
)} )}
keyExtractor={(item) => `sales-order-${item.id}`} keyExtractor={(item) => `sales-order-${item.id}`}
onEndReached={() => loadMoreData('salesOrders')}
onEndReachedThreshold={0.1}
ListFooterComponent={() => renderFooter('salesOrders')}
{...commonFlatListProps} {...commonFlatListProps}
/> />
); );
@ -318,6 +357,9 @@ const ZohoCrmDataScreen: React.FC = () => {
/> />
)} )}
keyExtractor={(item) => `purchase-order-${item.id}`} keyExtractor={(item) => `purchase-order-${item.id}`}
onEndReached={() => loadMoreData('purchaseOrders')}
onEndReachedThreshold={0.1}
ListFooterComponent={() => renderFooter('purchaseOrders')}
{...commonFlatListProps} {...commonFlatListProps}
/> />
); );
@ -332,6 +374,9 @@ const ZohoCrmDataScreen: React.FC = () => {
/> />
)} )}
keyExtractor={(item) => `invoice-${item.id}`} keyExtractor={(item) => `invoice-${item.id}`}
onEndReached={() => loadMoreData('invoices')}
onEndReachedThreshold={0.1}
ListFooterComponent={() => renderFooter('invoices')}
{...commonFlatListProps} {...commonFlatListProps}
/> />
); );

View File

@ -174,25 +174,34 @@ export const fetchDeals = createAsyncThunk(
export const fetchSalesOrders = createAsyncThunk( export const fetchSalesOrders = createAsyncThunk(
'crm/fetchSalesOrders', 'crm/fetchSalesOrders',
async (params?: CrmSearchParams) => { async (params?: CrmSearchParams & { append?: boolean }) => {
const response = await crmAPI.getSalesOrders(params); 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( export const fetchPurchaseOrders = createAsyncThunk(
'crm/fetchPurchaseOrders', 'crm/fetchPurchaseOrders',
async (params?: CrmSearchParams) => { async (params?: CrmSearchParams & { append?: boolean }) => {
const response = await crmAPI.getPurchaseOrders(params); 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( export const fetchInvoices = createAsyncThunk(
'crm/fetchInvoices', 'crm/fetchInvoices',
async (params?: CrmSearchParams) => { async (params?: CrmSearchParams & { append?: boolean }) => {
const response = await crmAPI.getInvoices(params); 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.pagination.deals = { page: 1, count: 0, moreRecords: false };
state.deals = []; 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) => { extraReducers: (builder) => {
// Fetch leads // Fetch leads
@ -405,8 +426,17 @@ const crmSlice = createSlice({
}) })
.addCase(fetchSalesOrders.fulfilled, (state, action) => { .addCase(fetchSalesOrders.fulfilled, (state, action) => {
state.loading.salesOrders = false; state.loading.salesOrders = false;
state.salesOrders = action.payload.data || []; const { data, append } = action.payload;
state.pagination.salesOrders = action.payload.info || { page: 1, count: 0, moreRecords: false };
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(); state.lastUpdated.salesOrders = new Date().toISOString();
}) })
.addCase(fetchSalesOrders.rejected, (state, action) => { .addCase(fetchSalesOrders.rejected, (state, action) => {
@ -421,8 +451,17 @@ const crmSlice = createSlice({
}) })
.addCase(fetchPurchaseOrders.fulfilled, (state, action) => { .addCase(fetchPurchaseOrders.fulfilled, (state, action) => {
state.loading.purchaseOrders = false; state.loading.purchaseOrders = false;
state.purchaseOrders = action.payload.data || []; const { data, append } = action.payload;
state.pagination.purchaseOrders = action.payload.info || { page: 1, count: 0, moreRecords: false };
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(); state.lastUpdated.purchaseOrders = new Date().toISOString();
}) })
.addCase(fetchPurchaseOrders.rejected, (state, action) => { .addCase(fetchPurchaseOrders.rejected, (state, action) => {
@ -437,8 +476,17 @@ const crmSlice = createSlice({
}) })
.addCase(fetchInvoices.fulfilled, (state, action) => { .addCase(fetchInvoices.fulfilled, (state, action) => {
state.loading.invoices = false; state.loading.invoices = false;
state.invoices = action.payload.data || []; const { data, append } = action.payload;
state.pagination.invoices = action.payload.info || { page: 1, count: 0, moreRecords: false };
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(); state.lastUpdated.invoices = new Date().toISOString();
}) })
.addCase(fetchInvoices.rejected, (state, action) => { .addCase(fetchInvoices.rejected, (state, action) => {
@ -534,6 +582,9 @@ export const {
resetTasksPagination, resetTasksPagination,
resetContactsPagination, resetContactsPagination,
resetDealsPagination, resetDealsPagination,
resetSalesOrdersPagination,
resetPurchaseOrdersPagination,
resetInvoicesPagination,
} = crmSlice.actions; } = crmSlice.actions;
export default crmSlice.reducer; export default crmSlice.reducer;

View File

@ -63,18 +63,29 @@ const IntegrationCategoryScreen: React.FC<Props> = ({ route }) => {
const [showZohoAuth, setShowZohoAuth] = React.useState(false); const [showZohoAuth, setShowZohoAuth] = React.useState(false);
const [pendingService, setPendingService] = React.useState<string | null>(null); const [pendingService, setPendingService] = React.useState<string | null>(null);
const [isCheckingToken, setIsCheckingToken] = React.useState(false); const [isCheckingToken, setIsCheckingToken] = React.useState(false);
const [authenticatedServices, setAuthenticatedServices] = React.useState<Set<string>>(new Set());
const services = servicesMap[route.params.categoryKey] ?? []; const services = servicesMap[route.params.categoryKey] ?? [];
// Check for existing Zoho token // Check for existing Zoho token
const checkZohoToken = async (serviceKey: string) => { const checkZohoToken = async (serviceKey: string, forceReauth: boolean = false) => {
try { try {
setIsCheckingToken(true); 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 response = await httpClient.get('/api/v1/users/decrypt-token?service_name=zoho');
const responseData = response.data as any; const responseData = response.data as any;
if (responseData.status === 'success' && responseData.data?.accessToken) { 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); console.log('Zoho token found, navigating directly to:', serviceKey);
setAuthenticatedServices(prev => new Set([...prev, serviceKey]));
dispatch(setSelectedService(serviceKey)); dispatch(setSelectedService(serviceKey));
} else { } else {
// No valid token, show auth modal // No valid token, show auth modal
@ -91,6 +102,24 @@ const IntegrationCategoryScreen: React.FC<Props> = ({ 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 ( return (
<View style={[styles.container, { backgroundColor: colors.background }]}> <View style={[styles.container, { backgroundColor: colors.background }]}>
<FlatList <FlatList
@ -98,37 +127,57 @@ const IntegrationCategoryScreen: React.FC<Props> = ({ route }) => {
keyExtractor={item => item.key} keyExtractor={item => item.key}
contentContainerStyle={{ padding: 16 }} contentContainerStyle={{ padding: 16 }}
ItemSeparatorComponent={() => <View style={[styles.sep, { backgroundColor: colors.border }]} />} ItemSeparatorComponent={() => <View style={[styles.sep, { backgroundColor: colors.border }]} />}
renderItem={({ item }) => ( renderItem={({ item }) => {
<TouchableOpacity const requiresZohoAuth = item.key === 'zohoProjects' || item.key === 'zohoPeople' || item.key === 'zohoBooks' || item.key === 'zohoCRM';
style={[styles.row, isCheckingToken && styles.disabledRow]}
activeOpacity={0.8}
disabled={isCheckingToken}
onPress={() => {
// 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) { return (
checkZohoToken(item.key); <View style={styles.serviceItem}>
} else { <View style={[styles.row, isCheckingToken && styles.disabledRow]}>
// For non-Zoho services, navigate to Coming Soon screen <TouchableOpacity
navigation.navigate('ComingSoon' as never); style={styles.mainServiceButton}
} activeOpacity={0.8}
}} disabled={isCheckingToken}
> onPress={() => {
<View style={[styles.iconCircle, { backgroundColor: '#F1F5F9' }]}> console.log('key pressed', item.key);
<Icon name={item.icon} size={20} color={colors.primary} />
</View> if (requiresZohoAuth) {
<Text style={[styles.title, { color: colors.text, fontFamily: fonts.medium }]}>{item.title}</Text> checkZohoToken(item.key);
{isCheckingToken && ( } else {
<View style={styles.loadingContainer}> // For non-Zoho services, navigate to Coming Soon screen
<Text style={[styles.loadingText, { color: colors.textLight, fontFamily: fonts.regular }]}> navigation.navigate('ComingSoon' as never);
Checking... }
</Text> }}
>
<View style={[styles.iconCircle, { backgroundColor: '#F1F5F9' }]}>
<Icon name={item.icon} size={20} color={colors.primary} />
</View>
<Text style={[styles.title, { color: colors.text, fontFamily: fonts.medium }]}>{item.title}</Text>
{isCheckingToken && (
<View style={styles.loadingContainer}>
<Text style={[styles.loadingText, { color: colors.textLight, fontFamily: fonts.regular }]}>
Checking...
</Text>
</View>
)}
</TouchableOpacity>
{/* Re-authentication button for Zoho services - always visible */}
{requiresZohoAuth && (
<TouchableOpacity
style={[styles.reauthButton, { backgroundColor: colors.background, borderColor: colors.border }]}
onPress={() => handleReAuthenticate(item.key)}
activeOpacity={0.7}
>
<Icon name="refresh" size={14} color={colors.textLight} />
<Text style={[styles.reauthText, { color: colors.textLight, fontFamily: fonts.regular }]}>
Re-auth
</Text>
</TouchableOpacity>
)}
</View> </View>
)} </View>
</TouchableOpacity> );
)} }}
/> />
{/* Zoho Auth Modal */} {/* Zoho Auth Modal */}
@ -144,6 +193,8 @@ const IntegrationCategoryScreen: React.FC<Props> = ({ route }) => {
console.log('auth data i got',authData) console.log('auth data i got',authData)
setShowZohoAuth(false); setShowZohoAuth(false);
if (pendingService) { if (pendingService) {
// Mark service as authenticated
setAuthenticatedServices(prev => new Set([...prev, pendingService]));
dispatch(setSelectedService(pendingService)); dispatch(setSelectedService(pendingService));
setPendingService(null); setPendingService(null);
} }
@ -165,14 +216,23 @@ const IntegrationCategoryScreen: React.FC<Props> = ({ route }) => {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { flex: 1 }, container: { flex: 1 },
sep: { height: 1, opacity: 0.6 }, sep: { height: 1, opacity: 0.6 },
serviceItem: {
marginVertical: 4,
},
row: { row: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
paddingVertical: 12, paddingVertical: 12,
justifyContent: 'space-between',
}, },
disabledRow: { disabledRow: {
opacity: 0.6, opacity: 0.6,
}, },
mainServiceButton: {
flexDirection: 'row',
alignItems: 'center',
flex: 1,
},
iconCircle: { iconCircle: {
width: 36, width: 36,
height: 36, height: 36,
@ -192,6 +252,19 @@ const styles = StyleSheet.create({
fontSize: 12, fontSize: 12,
fontStyle: 'italic', 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; export default IntegrationCategoryScreen;

View File

@ -8,8 +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://10.175.59.235:4000',
// baseURL: 'http://160.187.167.216', // baseURL: 'http://160.187.167.216',
baseURL: 'https://gold-tires-sniff.loca.lt',
timeout: 10000, timeout: 10000,
}); });