paginationadded for all tabs in zoho crm
This commit is contained in:
parent
e53b229c03
commit
5428ac9f3e
@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user