zoho peple screen modified and re authentication rmoved based on token availability
This commit is contained in:
parent
54c751324c
commit
e479b59ae3
@ -1,12 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createStackNavigator } from '@react-navigation/stack';
|
import { createStackNavigator } from '@react-navigation/stack';
|
||||||
import ZohoPeopleDashboardScreen from '@/modules/hr/zoho/screens/ZohoPeopleDashboardScreen';
|
import ZohoPeopleDashboardScreen from '@/modules/hr/zoho/screens/ZohoPeopleDashboardScreen';
|
||||||
|
import ZohoPeopleDataScreen from '@/modules/hr/zoho/screens/ZohoPeopleDataScreen';
|
||||||
|
|
||||||
const Stack = createStackNavigator();
|
const Stack = createStackNavigator();
|
||||||
|
|
||||||
const HRNavigator = () => (
|
const HRNavigator = () => (
|
||||||
<Stack.Navigator>
|
<Stack.Navigator>
|
||||||
<Stack.Screen name="ZohoPeopleDashboard" component={ZohoPeopleDashboardScreen} options={{headerShown:false}} />
|
<Stack.Screen name="ZohoPeopleDashboard" component={ZohoPeopleDashboardScreen} options={{headerShown:false}} />
|
||||||
|
<Stack.Screen name="ZohoPeopleData" component={ZohoPeopleDataScreen} options={{headerShown:false}} />
|
||||||
</Stack.Navigator>
|
</Stack.Navigator>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import {
|
|||||||
Dimensions
|
Dimensions
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import Icon from 'react-native-vector-icons/MaterialIcons';
|
import Icon from 'react-native-vector-icons/MaterialIcons';
|
||||||
import { Container, LoadingSpinner, ErrorState } from '@/shared/components/ui';
|
import { Container, LoadingSpinner, ErrorState } from '@/shared/components/ui';
|
||||||
import { fetchZohoPeopleData } from '../store/hrSlice';
|
import { fetchZohoPeopleData } from '../store/hrSlice';
|
||||||
@ -26,6 +27,7 @@ const { width } = Dimensions.get('window');
|
|||||||
|
|
||||||
const ZohoPeopleDashboardScreen: React.FC = () => {
|
const ZohoPeopleDashboardScreen: React.FC = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const navigation = useNavigation();
|
||||||
const { colors, fonts, spacing, shadows } = useTheme();
|
const { colors, fonts, spacing, shadows } = useTheme();
|
||||||
|
|
||||||
const zohoPeopleState = useSelector(selectZohoPeopleState);
|
const zohoPeopleState = useSelector(selectZohoPeopleState);
|
||||||
@ -74,6 +76,13 @@ const ZohoPeopleDashboardScreen: React.FC = () => {
|
|||||||
Zoho People Dashboard
|
Zoho People Dashboard
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
<View style={styles.headerRight}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.dataButton, { backgroundColor: '#3B82F6', marginRight: 8 }]}
|
||||||
|
onPress={() => navigation.navigate('ZohoPeopleData' as never)}
|
||||||
|
>
|
||||||
|
<Icon name="list" size={20} color={colors.surface} />
|
||||||
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.refreshButton, { backgroundColor: colors.primary }]}
|
style={[styles.refreshButton, { backgroundColor: colors.primary }]}
|
||||||
onPress={handleRefresh}
|
onPress={handleRefresh}
|
||||||
@ -82,6 +91,7 @@ const ZohoPeopleDashboardScreen: React.FC = () => {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
<View style={[styles.content, { padding: spacing.md }]}>
|
<View style={[styles.content, { padding: spacing.md }]}>
|
||||||
{/* KPI Cards Row 1 */}
|
{/* KPI Cards Row 1 */}
|
||||||
@ -442,6 +452,10 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
marginLeft: 8,
|
marginLeft: 8,
|
||||||
},
|
},
|
||||||
|
headerRight: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
refreshButton: {
|
refreshButton: {
|
||||||
width: 36,
|
width: 36,
|
||||||
height: 36,
|
height: 36,
|
||||||
@ -449,6 +463,13 @@ const styles = StyleSheet.create({
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
|
dataButton: {
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
borderRadius: 18,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
content: {
|
content: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
|
|||||||
607
src/modules/hr/zoho/screens/ZohoPeopleDataScreen.tsx
Normal file
607
src/modules/hr/zoho/screens/ZohoPeopleDataScreen.tsx
Normal file
@ -0,0 +1,607 @@
|
|||||||
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
StyleSheet,
|
||||||
|
ScrollView,
|
||||||
|
TouchableOpacity,
|
||||||
|
RefreshControl,
|
||||||
|
FlatList,
|
||||||
|
} from 'react-native';
|
||||||
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
|
import type { AppDispatch } from '@/store/store';
|
||||||
|
import Icon from 'react-native-vector-icons/MaterialIcons';
|
||||||
|
import { Container, LoadingSpinner, ErrorState } from '@/shared/components/ui';
|
||||||
|
import { useTheme } from '@/shared/styles/useTheme';
|
||||||
|
import { fetchZohoPeopleData } from '../store/hrSlice';
|
||||||
|
import {
|
||||||
|
selectZohoPeopleLoading,
|
||||||
|
selectZohoPeopleError,
|
||||||
|
selectAttendanceReport,
|
||||||
|
selectEmployeeForms,
|
||||||
|
selectHolidays,
|
||||||
|
selectLeaveData,
|
||||||
|
} from '../store/selectors';
|
||||||
|
|
||||||
|
const ZohoPeopleDataScreen: React.FC = () => {
|
||||||
|
const { colors, fonts, spacing, shadows } = useTheme();
|
||||||
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
|
const [selectedTab, setSelectedTab] = useState<'employees' | 'attendance' | 'leaves' | 'holidays'>('employees');
|
||||||
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
|
||||||
|
// Redux selectors
|
||||||
|
const attendanceReport = useSelector(selectAttendanceReport);
|
||||||
|
const employeeForms = useSelector(selectEmployeeForms);
|
||||||
|
const holidays = useSelector(selectHolidays);
|
||||||
|
const leaveData = useSelector(selectLeaveData);
|
||||||
|
const loading = useSelector(selectZohoPeopleLoading);
|
||||||
|
const error = useSelector(selectZohoPeopleError);
|
||||||
|
|
||||||
|
// Fetch data using Redux
|
||||||
|
const fetchData = useCallback(async (showRefresh = false) => {
|
||||||
|
try {
|
||||||
|
if (showRefresh) {
|
||||||
|
setRefreshing(true);
|
||||||
|
}
|
||||||
|
await dispatch(fetchZohoPeopleData()).unwrap();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching Zoho People data:', err);
|
||||||
|
} finally {
|
||||||
|
setRefreshing(false);
|
||||||
|
}
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData();
|
||||||
|
}, [fetchData]);
|
||||||
|
|
||||||
|
const handleRefresh = useCallback(() => {
|
||||||
|
fetchData(true);
|
||||||
|
}, [fetchData]);
|
||||||
|
|
||||||
|
const handleRetry = useCallback(() => {
|
||||||
|
fetchData();
|
||||||
|
}, [fetchData]);
|
||||||
|
|
||||||
|
// Loading state
|
||||||
|
if (loading && !employeeForms?.length && !attendanceReport?.length) {
|
||||||
|
return <LoadingSpinner />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error state
|
||||||
|
if (error) {
|
||||||
|
return <ErrorState onRetry={handleRetry} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab configuration
|
||||||
|
const tabs = [
|
||||||
|
{ key: 'employees', label: 'Employees', icon: 'people', count: employeeForms?.length || 0 },
|
||||||
|
{ key: 'attendance', label: 'Attendance', icon: 'schedule', count: attendanceReport?.length || 0 },
|
||||||
|
{ key: 'leaves', label: 'Leaves', icon: 'event-available', count: leaveData?.length || 0 },
|
||||||
|
{ key: 'holidays', label: 'Holidays', icon: 'event', count: holidays?.length || 0 },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const renderEmployeeCard = useCallback((item: any) => (
|
||||||
|
<View style={[styles.card, { backgroundColor: colors.surface, borderColor: colors.border }]}>
|
||||||
|
<View style={styles.cardHeader}>
|
||||||
|
<View style={styles.cardTitleContainer}>
|
||||||
|
<Icon name="person" size={20} color={colors.primary} />
|
||||||
|
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.bold }]}>
|
||||||
|
{item.FirstName} {item.LastName}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[styles.statusBadge, { backgroundColor: item.Employeestatus === 'Active' ? '#E9FAF2' : '#FEF2F2' }]}>
|
||||||
|
<Text style={[styles.statusText, { color: item.Employeestatus === 'Active' ? '#10B981' : '#EF4444', fontFamily: fonts.medium }]}>
|
||||||
|
{item.Employeestatus}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.cardContent}>
|
||||||
|
<View style={styles.infoRow}>
|
||||||
|
<Text style={[styles.infoLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Employee ID:</Text>
|
||||||
|
<Text style={[styles.infoValue, { color: colors.text, fontFamily: fonts.medium }]}>
|
||||||
|
{item.EmployeeID}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.infoRow}>
|
||||||
|
<Text style={[styles.infoLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Department:</Text>
|
||||||
|
<Text style={[styles.infoValue, { color: colors.text, fontFamily: fonts.medium }]}>
|
||||||
|
{item.Department || 'N/A'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.infoRow}>
|
||||||
|
<Text style={[styles.infoLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Role:</Text>
|
||||||
|
<Text style={[styles.infoValue, { color: colors.text, fontFamily: fonts.medium }]}>
|
||||||
|
{item.Role || 'N/A'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{item.EmailID && (
|
||||||
|
<View style={styles.infoRow}>
|
||||||
|
<Text style={[styles.infoLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Email:</Text>
|
||||||
|
<Text style={[styles.infoValue, { color: colors.text, fontFamily: fonts.medium }]}>{item.EmailID}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<View style={styles.infoRow}>
|
||||||
|
<Text style={[styles.infoLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Experience:</Text>
|
||||||
|
<Text style={[styles.infoValue, { color: colors.text, fontFamily: fonts.medium }]}>
|
||||||
|
{item.Experience || 'N/A'} years
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.infoRow}>
|
||||||
|
<Text style={[styles.infoLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Date of Joining:</Text>
|
||||||
|
<Text style={[styles.infoValue, { color: colors.text, fontFamily: fonts.medium }]}>
|
||||||
|
{item.Dateofjoining || 'N/A'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
), [colors, fonts]);
|
||||||
|
|
||||||
|
const renderAttendanceCard = useCallback((item: any) => {
|
||||||
|
const today = new Date();
|
||||||
|
const todayISO = today.toISOString().split('T')[0];
|
||||||
|
const todayFormatted = todayISO.split('-').reverse().join('-');
|
||||||
|
const todayAttendance = item.attendanceDetails?.[todayFormatted] || item.attendanceDetails?.[todayISO];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[styles.card, { backgroundColor: colors.surface, borderColor: colors.border }]}>
|
||||||
|
<View style={styles.cardHeader}>
|
||||||
|
<View style={styles.cardTitleContainer}>
|
||||||
|
<Icon name="schedule" size={20} color={colors.primary} />
|
||||||
|
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.bold }]}>
|
||||||
|
{item.employeeDetails['first name']} {item.employeeDetails['last name']}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[styles.statusBadge, { backgroundColor: getAttendanceStatusColor(todayAttendance?.Status) }]}>
|
||||||
|
<Text style={[styles.statusText, { color: colors.text, fontFamily: fonts.medium }]}>
|
||||||
|
{todayAttendance?.Status || 'No Data'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.cardContent}>
|
||||||
|
<View style={styles.infoRow}>
|
||||||
|
<Text style={[styles.infoLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Employee ID:</Text>
|
||||||
|
<Text style={[styles.infoValue, { color: colors.text, fontFamily: fonts.medium }]}>
|
||||||
|
{item.employeeDetails.erecno}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{todayAttendance && (
|
||||||
|
<>
|
||||||
|
<View style={styles.infoRow}>
|
||||||
|
<Text style={[styles.infoLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>First In:</Text>
|
||||||
|
<Text style={[styles.infoValue, { color: colors.text, fontFamily: fonts.medium }]}>
|
||||||
|
{todayAttendance.FirstIn || 'N/A'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.infoRow}>
|
||||||
|
<Text style={[styles.infoLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Last Out:</Text>
|
||||||
|
<Text style={[styles.infoValue, { color: colors.text, fontFamily: fonts.medium }]}>
|
||||||
|
{todayAttendance.LastOut || 'N/A'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.amountRow}>
|
||||||
|
<View style={styles.amountItem}>
|
||||||
|
<Text style={[styles.amountLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Working Hours:</Text>
|
||||||
|
<Text style={[styles.amountValue, { color: colors.primary, fontFamily: fonts.bold }]}>
|
||||||
|
{todayAttendance.WorkingHours || '00:00'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.amountItem}>
|
||||||
|
<Text style={[styles.amountLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Total Hours:</Text>
|
||||||
|
<Text style={[styles.amountValue, { color: '#10B981', fontFamily: fonts.bold }]}>
|
||||||
|
{todayAttendance.TotalHours || '00:00'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}, [colors, fonts]);
|
||||||
|
|
||||||
|
const renderLeaveCard = useCallback((item: any) => (
|
||||||
|
<View style={[styles.card, { backgroundColor: colors.surface, borderColor: colors.border }]}>
|
||||||
|
<View style={styles.cardHeader}>
|
||||||
|
<View style={styles.cardTitleContainer}>
|
||||||
|
<Icon name="event-available" size={20} color={colors.primary} />
|
||||||
|
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.bold }]}>
|
||||||
|
{item.Employee_ID}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[styles.statusBadge, { backgroundColor: getLeaveStatusColor(item.ApprovalStatus) }]}>
|
||||||
|
<Text style={[styles.statusText, { color: colors.text, fontFamily: fonts.medium }]}>
|
||||||
|
{item.ApprovalStatus}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.cardContent}>
|
||||||
|
<View style={styles.infoRow}>
|
||||||
|
<Text style={[styles.infoLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Leave Type:</Text>
|
||||||
|
<Text style={[styles.infoValue, { color: colors.text, fontFamily: fonts.medium }]}>
|
||||||
|
{item.Leavetype}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.infoRow}>
|
||||||
|
<Text style={[styles.infoLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>From:</Text>
|
||||||
|
<Text style={[styles.infoValue, { color: colors.text, fontFamily: fonts.medium }]}>
|
||||||
|
{item.From || 'N/A'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.infoRow}>
|
||||||
|
<Text style={[styles.infoLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>To:</Text>
|
||||||
|
<Text style={[styles.infoValue, { color: colors.text, fontFamily: fonts.medium }]}>
|
||||||
|
{item.To || 'N/A'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.infoRow}>
|
||||||
|
<Text style={[styles.infoLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Days Taken:</Text>
|
||||||
|
<Text style={[styles.infoValue, { color: colors.text, fontFamily: fonts.medium }]}>
|
||||||
|
{item.Daystaken} days
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{item.Reasonforleave && (
|
||||||
|
<View style={styles.infoRow}>
|
||||||
|
<Text style={[styles.infoLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Reason:</Text>
|
||||||
|
<Text style={[styles.infoValue, { color: colors.text, fontFamily: fonts.medium }]}>
|
||||||
|
{item.Reasonforleave}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
), [colors, fonts]);
|
||||||
|
|
||||||
|
const renderHolidayCard = useCallback((item: any) => (
|
||||||
|
<View style={[styles.card, { backgroundColor: colors.surface, borderColor: colors.border }]}>
|
||||||
|
<View style={styles.cardHeader}>
|
||||||
|
<View style={styles.cardTitleContainer}>
|
||||||
|
<Icon name="event" size={20} color={colors.primary} />
|
||||||
|
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.bold }]}>
|
||||||
|
{item.Name}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[styles.statusBadge, { backgroundColor: getHolidayTypeColor(item) }]}>
|
||||||
|
<Text style={[styles.statusText, { color: colors.text, fontFamily: fonts.medium }]}>
|
||||||
|
{item.isRestrictedHoliday ? 'Restricted' : item.isHalfday ? 'Half Day' : 'Full Day'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.cardContent}>
|
||||||
|
<View style={styles.infoRow}>
|
||||||
|
<Text style={[styles.infoLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Date:</Text>
|
||||||
|
<Text style={[styles.infoValue, { color: colors.text, fontFamily: fonts.medium }]}>
|
||||||
|
{item.Date || 'N/A'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.infoRow}>
|
||||||
|
<Text style={[styles.infoLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Location:</Text>
|
||||||
|
<Text style={[styles.infoValue, { color: colors.text, fontFamily: fonts.medium }]}>
|
||||||
|
{item.LocationName || 'All Locations'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.infoRow}>
|
||||||
|
<Text style={[styles.infoLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Shift:</Text>
|
||||||
|
<Text style={[styles.infoValue, { color: colors.text, fontFamily: fonts.medium }]}>
|
||||||
|
{item.ShiftName || 'N/A'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{item.Remarks && (
|
||||||
|
<View style={styles.infoRow}>
|
||||||
|
<Text style={[styles.infoLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Remarks:</Text>
|
||||||
|
<Text style={[styles.infoValue, { color: colors.text, fontFamily: fonts.medium }]}>
|
||||||
|
{item.Remarks}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
), [colors, fonts]);
|
||||||
|
|
||||||
|
const renderTabContent = useCallback(() => {
|
||||||
|
const commonFlatListProps = {
|
||||||
|
showsVerticalScrollIndicator: false,
|
||||||
|
contentContainerStyle: styles.listContainer,
|
||||||
|
refreshControl: (
|
||||||
|
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
|
||||||
|
),
|
||||||
|
removeClippedSubviews: true,
|
||||||
|
maxToRenderPerBatch: 10,
|
||||||
|
windowSize: 10,
|
||||||
|
initialNumToRender: 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (selectedTab) {
|
||||||
|
case 'employees':
|
||||||
|
return (
|
||||||
|
<FlatList
|
||||||
|
data={employeeForms || []}
|
||||||
|
renderItem={({ item }) => renderEmployeeCard(item)}
|
||||||
|
keyExtractor={(_, index) => `emp-${index}`}
|
||||||
|
{...commonFlatListProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'attendance':
|
||||||
|
return (
|
||||||
|
<FlatList
|
||||||
|
data={attendanceReport || []}
|
||||||
|
renderItem={({ item }) => renderAttendanceCard(item)}
|
||||||
|
keyExtractor={(_, index) => `att-${index}`}
|
||||||
|
{...commonFlatListProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'leaves':
|
||||||
|
return (
|
||||||
|
<FlatList
|
||||||
|
data={leaveData || []}
|
||||||
|
renderItem={({ item }) => renderLeaveCard(item)}
|
||||||
|
keyExtractor={(_, index) => `leave-${index}`}
|
||||||
|
{...commonFlatListProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'holidays':
|
||||||
|
return (
|
||||||
|
<FlatList
|
||||||
|
data={holidays || []}
|
||||||
|
renderItem={({ item }) => renderHolidayCard(item)}
|
||||||
|
keyExtractor={(_, index) => `hol-${index}`}
|
||||||
|
{...commonFlatListProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}, [selectedTab, employeeForms, attendanceReport, leaveData, holidays, refreshing, handleRefresh, renderEmployeeCard, renderAttendanceCard, renderLeaveCard, renderHolidayCard]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[styles.container, { backgroundColor: colors.background }]}>
|
||||||
|
{/* Fixed Header */}
|
||||||
|
<View style={styles.header}>
|
||||||
|
<Text style={[styles.title, { color: colors.text, fontFamily: fonts.bold }]}>
|
||||||
|
Zoho People Data
|
||||||
|
</Text>
|
||||||
|
<TouchableOpacity onPress={handleRefresh} disabled={refreshing}>
|
||||||
|
<Icon name="refresh" size={24} color={colors.primary} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Fixed Tabs */}
|
||||||
|
<View style={styles.tabsContainer}>
|
||||||
|
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
||||||
|
<View style={styles.tabs}>
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={tab.key}
|
||||||
|
style={[
|
||||||
|
styles.tab,
|
||||||
|
selectedTab === tab.key && { backgroundColor: colors.primary },
|
||||||
|
]}
|
||||||
|
onPress={() => setSelectedTab(tab.key)}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name={tab.icon}
|
||||||
|
size={20}
|
||||||
|
color={selectedTab === tab.key ? colors.surface : colors.textLight}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.tabText,
|
||||||
|
{
|
||||||
|
color: selectedTab === tab.key ? colors.surface : colors.textLight,
|
||||||
|
fontFamily: fonts.medium,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</Text>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.countBadge,
|
||||||
|
{ backgroundColor: selectedTab === tab.key ? colors.surface : colors.primary },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.countText,
|
||||||
|
{
|
||||||
|
color: selectedTab === tab.key ? colors.primary : colors.surface,
|
||||||
|
fontFamily: fonts.bold,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{tab.count}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Scrollable Content */}
|
||||||
|
<View style={styles.content}>
|
||||||
|
{renderTabContent()}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper functions to get status colors
|
||||||
|
const getAttendanceStatusColor = (status: string): string => {
|
||||||
|
const statusColors: Record<string, string> = {
|
||||||
|
present: '#E9FAF2',
|
||||||
|
absent: '#FEF2F2',
|
||||||
|
weekend: '#F3F4F6',
|
||||||
|
'': '#F3F4F6',
|
||||||
|
};
|
||||||
|
return statusColors[status?.toLowerCase()] || '#F3F4F6';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLeaveStatusColor = (status: string): string => {
|
||||||
|
const statusColors: Record<string, string> = {
|
||||||
|
approved: '#E9FAF2',
|
||||||
|
pending: '#FEF3C7',
|
||||||
|
rejected: '#FEF2F2',
|
||||||
|
draft: '#F3F4F6',
|
||||||
|
};
|
||||||
|
return statusColors[status?.toLowerCase()] || '#F3F4F6';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getHolidayTypeColor = (holiday: any): string => {
|
||||||
|
if (holiday.isRestrictedHoliday) return '#FEF3C7';
|
||||||
|
if (holiday.isHalfday) return '#E0F2FE';
|
||||||
|
return '#E9FAF2';
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 12,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 24,
|
||||||
|
},
|
||||||
|
tabsContainer: {
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
paddingVertical: 12,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: '#E5E7EB',
|
||||||
|
},
|
||||||
|
tabs: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
},
|
||||||
|
tab: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 10,
|
||||||
|
marginRight: 8,
|
||||||
|
borderRadius: 20,
|
||||||
|
backgroundColor: '#F1F5F9',
|
||||||
|
},
|
||||||
|
tabText: {
|
||||||
|
marginLeft: 6,
|
||||||
|
fontSize: 14,
|
||||||
|
},
|
||||||
|
countBadge: {
|
||||||
|
marginLeft: 6,
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
paddingVertical: 2,
|
||||||
|
borderRadius: 10,
|
||||||
|
minWidth: 20,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
countText: {
|
||||||
|
fontSize: 12,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
listContainer: {
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingBottom: 20,
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
borderRadius: 12,
|
||||||
|
borderWidth: 1,
|
||||||
|
padding: 16,
|
||||||
|
marginBottom: 12,
|
||||||
|
...StyleSheet.flatten({
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
shadowRadius: 4,
|
||||||
|
elevation: 3,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
cardHeader: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
cardTitleContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
cardTitle: {
|
||||||
|
fontSize: 16,
|
||||||
|
marginLeft: 8,
|
||||||
|
},
|
||||||
|
statusBadge: {
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
paddingVertical: 4,
|
||||||
|
borderRadius: 12,
|
||||||
|
},
|
||||||
|
statusText: {
|
||||||
|
fontSize: 12,
|
||||||
|
textTransform: 'capitalize',
|
||||||
|
},
|
||||||
|
cardContent: {
|
||||||
|
gap: 8,
|
||||||
|
},
|
||||||
|
infoRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
infoLabel: {
|
||||||
|
fontSize: 14,
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
infoValue: {
|
||||||
|
fontSize: 14,
|
||||||
|
textAlign: 'right',
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
amountRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
marginTop: 8,
|
||||||
|
paddingTop: 8,
|
||||||
|
borderTopWidth: StyleSheet.hairlineWidth,
|
||||||
|
borderTopColor: '#E5E7EB',
|
||||||
|
},
|
||||||
|
amountItem: {
|
||||||
|
flex: 1,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
amountLabel: {
|
||||||
|
fontSize: 12,
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
amountValue: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ZohoPeopleDataScreen;
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, Text, StyleSheet, FlatList, TouchableOpacity } from 'react-native';
|
import { View, Text, StyleSheet, FlatList, TouchableOpacity, Alert } from 'react-native';
|
||||||
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
|
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||||
import { useTheme } from '@/shared/styles/useTheme';
|
import { useTheme } from '@/shared/styles/useTheme';
|
||||||
import type { RouteProp } from '@react-navigation/native';
|
import type { RouteProp } from '@react-navigation/native';
|
||||||
@ -9,6 +9,7 @@ import { setSelectedService } from '@/modules/integrations/store/integrationsSli
|
|||||||
import type { AppDispatch } from '@/store/store';
|
import type { AppDispatch } from '@/store/store';
|
||||||
import ZohoAuth from './ZohoAuth';
|
import ZohoAuth from './ZohoAuth';
|
||||||
import { Modal } from 'react-native';
|
import { Modal } from 'react-native';
|
||||||
|
import httpClient from '@/services/http';
|
||||||
|
|
||||||
type Route = RouteProp<IntegrationsStackParamList, 'IntegrationCategory'>;
|
type Route = RouteProp<IntegrationsStackParamList, 'IntegrationCategory'>;
|
||||||
|
|
||||||
@ -59,8 +60,35 @@ const IntegrationCategoryScreen: React.FC<Props> = ({ route }) => {
|
|||||||
const dispatch = useDispatch<AppDispatch>();
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
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 services = servicesMap[route.params.categoryKey] ?? [];
|
const services = servicesMap[route.params.categoryKey] ?? [];
|
||||||
|
|
||||||
|
// Check for existing Zoho token
|
||||||
|
const checkZohoToken = async (serviceKey: string) => {
|
||||||
|
try {
|
||||||
|
setIsCheckingToken(true);
|
||||||
|
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
|
||||||
|
console.log('Zoho token found, navigating directly to:', serviceKey);
|
||||||
|
dispatch(setSelectedService(serviceKey));
|
||||||
|
} else {
|
||||||
|
// No valid token, show auth modal
|
||||||
|
setPendingService(serviceKey);
|
||||||
|
setShowZohoAuth(true);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('No valid Zoho token found, showing auth modal');
|
||||||
|
// Token doesn't exist or is invalid, show auth modal
|
||||||
|
setPendingService(serviceKey);
|
||||||
|
setShowZohoAuth(true);
|
||||||
|
} finally {
|
||||||
|
setIsCheckingToken(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.container, { backgroundColor: colors.background }]}>
|
<View style={[styles.container, { backgroundColor: colors.background }]}>
|
||||||
<FlatList
|
<FlatList
|
||||||
@ -70,15 +98,16 @@ const IntegrationCategoryScreen: React.FC<Props> = ({ route }) => {
|
|||||||
ItemSeparatorComponent={() => <View style={[styles.sep, { backgroundColor: colors.border }]} />}
|
ItemSeparatorComponent={() => <View style={[styles.sep, { backgroundColor: colors.border }]} />}
|
||||||
renderItem={({ item }) => (
|
renderItem={({ item }) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.row}
|
style={[styles.row, isCheckingToken && styles.disabledRow]}
|
||||||
activeOpacity={0.8}
|
activeOpacity={0.8}
|
||||||
|
disabled={isCheckingToken}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
// Here we decide whether to require Zoho authentication first
|
// Here we decide whether to require Zoho authentication first
|
||||||
const requiresZohoAuth = item.key === 'zohoProjects' || item.key === 'zohoPeople' || item.key === 'zohoBooks' || item.key === 'zohoCRM';
|
const requiresZohoAuth = item.key === 'zohoProjects' || item.key === 'zohoPeople' || item.key === 'zohoBooks' || item.key === 'zohoCRM';
|
||||||
console.log('key pressed',item.key)
|
console.log('key pressed', item.key);
|
||||||
|
|
||||||
if (requiresZohoAuth) {
|
if (requiresZohoAuth) {
|
||||||
setPendingService(item.key);
|
checkZohoToken(item.key);
|
||||||
setShowZohoAuth(true);
|
|
||||||
} else {
|
} else {
|
||||||
dispatch(setSelectedService(item.key));
|
dispatch(setSelectedService(item.key));
|
||||||
}
|
}
|
||||||
@ -88,6 +117,13 @@ const IntegrationCategoryScreen: React.FC<Props> = ({ route }) => {
|
|||||||
<Icon name={item.icon} size={20} color={colors.primary} />
|
<Icon name={item.icon} size={20} color={colors.primary} />
|
||||||
</View>
|
</View>
|
||||||
<Text style={[styles.title, { color: colors.text, fontFamily: fonts.medium }]}>{item.title}</Text>
|
<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>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -131,6 +167,9 @@ const styles = StyleSheet.create({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingVertical: 12,
|
paddingVertical: 12,
|
||||||
},
|
},
|
||||||
|
disabledRow: {
|
||||||
|
opacity: 0.6,
|
||||||
|
},
|
||||||
iconCircle: {
|
iconCircle: {
|
||||||
width: 36,
|
width: 36,
|
||||||
height: 36,
|
height: 36,
|
||||||
@ -139,7 +178,17 @@ const styles = StyleSheet.create({
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
marginRight: 12,
|
marginRight: 12,
|
||||||
},
|
},
|
||||||
title: { fontSize: 14 },
|
title: {
|
||||||
|
fontSize: 14,
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
loadingContainer: {
|
||||||
|
marginLeft: 8,
|
||||||
|
},
|
||||||
|
loadingText: {
|
||||||
|
fontSize: 12,
|
||||||
|
fontStyle: 'italic',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default IntegrationCategoryScreen;
|
export default IntegrationCategoryScreen;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user