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

View File

@ -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;

View File

@ -63,18 +63,29 @@ const IntegrationCategoryScreen: React.FC<Props> = ({ route }) => {
const [showZohoAuth, setShowZohoAuth] = React.useState(false);
const [pendingService, setPendingService] = React.useState<string | null>(null);
const [isCheckingToken, setIsCheckingToken] = React.useState(false);
const [authenticatedServices, setAuthenticatedServices] = React.useState<Set<string>>(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<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 (
<View style={[styles.container, { backgroundColor: colors.background }]}>
<FlatList
@ -98,37 +127,57 @@ const IntegrationCategoryScreen: React.FC<Props> = ({ route }) => {
keyExtractor={item => item.key}
contentContainerStyle={{ padding: 16 }}
ItemSeparatorComponent={() => <View style={[styles.sep, { backgroundColor: colors.border }]} />}
renderItem={({ item }) => (
<TouchableOpacity
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) {
checkZohoToken(item.key);
} else {
// For non-Zoho services, navigate to Coming Soon screen
navigation.navigate('ComingSoon' as never);
}
}}
>
<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>
renderItem={({ item }) => {
const requiresZohoAuth = item.key === 'zohoProjects' || item.key === 'zohoPeople' || item.key === 'zohoBooks' || item.key === 'zohoCRM';
return (
<View style={styles.serviceItem}>
<View style={[styles.row, isCheckingToken && styles.disabledRow]}>
<TouchableOpacity
style={styles.mainServiceButton}
activeOpacity={0.8}
disabled={isCheckingToken}
onPress={() => {
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);
}
}}
>
<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>
)}
</TouchableOpacity>
)}
</View>
);
}}
/>
{/* Zoho Auth Modal */}
@ -144,6 +193,8 @@ const IntegrationCategoryScreen: React.FC<Props> = ({ 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<Props> = ({ 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;

View File

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