zoho people and zoho books api' integrted and project destructured to add multiple service providers

This commit is contained in:
yashwin-foxy 2025-09-22 19:22:16 +05:30
parent 8c1e5309e5
commit 54c751324c
9 changed files with 1375 additions and 250 deletions

View File

@ -1,12 +1,12 @@
import React from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import HRDashboardScreen from '@/modules/hr/zoho/screens/HRDashboardScreen';
import ZohoPeopleDashboardScreen from '@/modules/hr/zoho/screens/ZohoPeopleDashboardScreen';
const Stack = createStackNavigator();
const HRNavigator = () => (
<Stack.Navigator>
<Stack.Screen name="HRDashboard" component={HRDashboardScreen} options={{headerShown:false}} />
<Stack.Screen name="ZohoPeopleDashboard" component={ZohoPeopleDashboardScreen} options={{headerShown:false}} />
</Stack.Navigator>
);

View File

@ -1,209 +0,0 @@
import React, { useEffect, useMemo } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import { Container, LoadingSpinner, ErrorState } from '@/shared/components/ui';
import { fetchHRMetrics } from '../store/hrSlice';
import type { RootState } from '@/store/store';
import { useTheme } from '@/shared/styles/useTheme';
const HRDashboardScreen: React.FC = () => {
const dispatch = useDispatch();
const { colors, fonts } = useTheme();
const { metrics, loading, error } = useSelector((s: RootState) => s.hr);
// Mock HR analytics (UI only)
const mock = useMemo(() => {
const headcount = 148;
const newHires30d = 9;
const attritionPct = 6; // lower is better
const attendancePct = 93;
const engagementPct = 78;
const hiresTrend = [2, 1, 3, 1, 2, 0];
const exitsTrend = [1, 0, 1, 0, 1, 1];
const deptDist = [
{ label: 'Engineering', value: 62, color: '#3AA0FF' },
{ label: 'Sales', value: 34, color: '#F59E0B' },
{ label: 'HR', value: 12, color: '#10B981' },
{ label: 'Ops', value: 28, color: '#6366F1' },
{ label: 'Finance', value: 12, color: '#EF4444' },
];
const holidays = [
{ date: '2025-09-10', name: 'Ganesh Chaturthi' },
{ date: '2025-10-02', name: 'Gandhi Jayanti' },
{ date: '2025-10-31', name: 'Diwali' },
];
const topPerformers = [
{ name: 'Aarti N.', score: 96 },
{ name: 'Rahul K.', score: 94 },
{ name: 'Meera S.', score: 92 },
];
return { headcount, newHires30d, attritionPct, attendancePct, engagementPct, hiresTrend, exitsTrend, deptDist, holidays, topPerformers };
}, []);
useEffect(() => {
// @ts-ignore
dispatch(fetchHRMetrics());
}, [dispatch]);
if (loading && metrics.length === 0) {
return <LoadingSpinner />;
}
if (error) {
return <ErrorState message={error} onRetry={() => dispatch(fetchHRMetrics() as any)} />;
}
return (
<Container>
<View style={styles.header}>
<Text style={[styles.title, { color: colors.text, fontFamily: fonts.bold }]}>HR Dashboard</Text>
</View>
<View style={styles.content}>
{/* KPI Cards */}
<View style={styles.kpiGrid}>
<Kpi label="Headcount" value={String(mock.headcount)} color={colors.text} fonts={fonts} accent="#3AA0FF" />
<Kpi label="New hires (30d)" value={String(mock.newHires30d)} color={colors.text} fonts={fonts} accent="#10B981" />
<Kpi label="Attrition" value={`${mock.attritionPct}%`} color={colors.text} fonts={fonts} accent="#EF4444" />
<Kpi label="Attendance" value={`${mock.attendancePct}%`} color={colors.text} fonts={fonts} accent="#6366F1" />
</View>
{/* Trends: Hires vs Exits */}
<View style={[styles.card, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Workforce Movements</Text>
<DualBars data={mock.hiresTrend.map((h, i) => ({ in: h, out: mock.exitsTrend[i] }))} max={Math.max(...mock.hiresTrend.map((h, i) => Math.max(h, mock.exitsTrend[i])))} colorA="#10B981" colorB="#EF4444" />
<View style={styles.legendRow}>
<View style={styles.legendItem}><View style={[styles.legendDot, { backgroundColor: '#10B981' }]} /><Text style={{ fontSize: 12, color: colors.text, fontFamily: fonts.regular }}>Hires</Text></View>
<View style={styles.legendItem}><View style={[styles.legendDot, { backgroundColor: '#EF4444' }]} /><Text style={{ fontSize: 12, color: colors.text, fontFamily: fonts.regular }}>Exits</Text></View>
</View>
</View>
{/* People Health */}
<View style={[styles.card, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>People Health</Text>
<Progress label="Attendance" value={mock.attendancePct} color="#3AA0FF" fonts={fonts} />
<Progress label="Engagement" value={mock.engagementPct} color="#6366F1" fonts={fonts} />
<Progress label="Attrition (inverse)" value={100 - mock.attritionPct} color="#10B981" fonts={fonts} />
</View>
{/* Department Distribution */}
<View style={[styles.card, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Department Distribution</Text>
<Stacked segments={mock.deptDist} total={mock.deptDist.reduce((a, b) => a + b.value, 0)} />
<View style={styles.legendRow}>
{mock.deptDist.map(s => (
<View key={s.label} style={styles.legendItem}>
<View style={[styles.legendDot, { backgroundColor: s.color }]} />
<Text style={{ color: colors.text, fontFamily: fonts.regular }}>{s.label}</Text>
</View>
))}
</View>
</View>
{/* Lists: Holidays and Top Performers */}
<View style={styles.row}>
<View style={[styles.card, styles.col, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Upcoming Holidays</Text>
{mock.holidays.map(h => (
<View key={h.date} style={styles.listRow}>
<Text style={[styles.listPrimary, { color: colors.text, fontFamily: fonts.regular }]}>{h.name}</Text>
<Text style={[styles.listSecondary, { color: colors.text, fontFamily: fonts.medium }]}>{h.date}</Text>
</View>
))}
</View>
<View style={[styles.card, styles.col, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Top Performers</Text>
{mock.topPerformers.map(p => (
<View key={p.name} style={styles.listRow}>
<Text style={[styles.listPrimary, { color: colors.text, fontFamily: fonts.regular }]}>{p.name}</Text>
<Text style={[styles.listSecondary, { color: colors.text, fontFamily: fonts.medium }]}>{p.score}</Text>
</View>
))}
</View>
</View>
</View>
</Container>
);
};
const styles = StyleSheet.create({
header: {
padding: 16,
backgroundColor: '#FFFFFF',
},
title: {
fontSize: 24,
},
content: {
padding: 16,
},
kpiGrid: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-between' },
kpiCard: { width: '48%', borderRadius: 12, borderWidth: 1, borderColor: '#E2E8F0', backgroundColor: '#FFFFFF', padding: 12, marginBottom: 12 },
kpiLabel: { fontSize: 12, opacity: 0.8 },
kpiValueRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginTop: 8 },
card: { borderRadius: 12, borderWidth: 1, padding: 12, marginTop: 12 },
cardTitle: { fontSize: 16, marginBottom: 8 },
dualBarsRow: { flexDirection: 'row', alignItems: 'flex-end' },
dualBarWrap: { flex: 1, marginRight: 8 },
dualBarA: { borderTopLeftRadius: 4, borderTopRightRadius: 4 },
dualBarB: { borderTopLeftRadius: 4, borderTopRightRadius: 4, marginTop: 4 },
legendRow: { flexDirection: 'row', flexWrap: 'wrap', marginTop: 8 },
legendItem: { flexDirection: 'row', alignItems: 'center', marginRight: 12, marginTop: 6 },
legendDot: { width: 8, height: 8, borderRadius: 4, marginRight: 6 },
row: { flexDirection: 'row', marginTop: 12 },
col: { flex: 1, marginRight: 8 },
listRow: { flexDirection: 'row', justifyContent: 'space-between', paddingVertical: 10, borderBottomWidth: StyleSheet.hairlineWidth, borderColor: '#E5E7EB' },
listPrimary: { fontSize: 14, flex: 1, paddingRight: 8 },
listSecondary: { fontSize: 12 },
});
export default HRDashboardScreen;
// UI helpers (no external deps)
const Kpi: React.FC<{ label: string; value: string; color: string; fonts: any; accent: string }> = ({ label, value, color, fonts, accent }) => {
return (
<View style={styles.kpiCard}>
<Text style={[styles.kpiLabel, { color, fontFamily: fonts.regular }]}>{label}</Text>
<View style={styles.kpiValueRow}>
<Text style={{ color, fontSize: 20, fontFamily: fonts.bold }}>{value}</Text>
<View style={{ width: 10, height: 10, borderRadius: 5, backgroundColor: accent }} />
</View>
</View>
);
};
const DualBars: React.FC<{ data: { in: number; out: number }[]; max: number; colorA: string; colorB: string }> = ({ data, max, colorA, colorB }) => {
return (
<View style={styles.dualBarsRow}>
{data.map((d, i) => (
<View key={i} style={styles.dualBarWrap}>
<View style={[styles.dualBarA, { height: Math.max(6, (d.in / Math.max(1, max)) * 64), backgroundColor: colorA }]} />
<View style={[styles.dualBarB, { height: Math.max(6, (d.out / Math.max(1, max)) * 64), backgroundColor: colorB }]} />
</View>
))}
</View>
);
};
const Progress: React.FC<{ label: string; value: number; color: string; fonts: any }> = ({ label, value, color, fonts }) => {
return (
<View style={{ marginTop: 8 }}>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginBottom: 6 }}>
<Text style={{ fontSize: 12, fontFamily: fonts.regular }}>{label}</Text>
<Text style={{ fontSize: 12, fontFamily: fonts.medium }}>{value}%</Text>
</View>
<View style={{ height: 8, borderRadius: 6, backgroundColor: '#E5E7EB', overflow: 'hidden' }}>
<View style={{ height: '100%', width: `${value}%`, backgroundColor: color }} />
</View>
</View>
);
};
const Stacked: React.FC<{ segments: { label: string; value: number; color: string }[]; total: number }> = ({ segments, total }) => {
return (
<View style={{ height: 12, borderRadius: 8, backgroundColor: '#E5E7EB', overflow: 'hidden', flexDirection: 'row', marginTop: 8 }}>
{segments.map(s => (
<View key={s.label} style={{ width: `${(s.value / Math.max(1, total)) * 100}%`, backgroundColor: s.color }} />
))}
</View>
);
};

View File

@ -0,0 +1,637 @@
import React, { useEffect, useMemo } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
RefreshControl,
TouchableOpacity,
Dimensions
} from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import Icon from 'react-native-vector-icons/MaterialIcons';
import { Container, LoadingSpinner, ErrorState } from '@/shared/components/ui';
import { fetchZohoPeopleData } from '../store/hrSlice';
import {
selectZohoPeopleState,
selectAttendanceAnalytics,
selectEmployeeAnalytics,
selectLeaveAnalytics,
selectHolidayAnalytics
} from '../store/selectors';
import { useTheme } from '@/shared/styles/useTheme';
import type { RootState } from '@/store/store';
const { width } = Dimensions.get('window');
const ZohoPeopleDashboardScreen: React.FC = () => {
const dispatch = useDispatch();
const { colors, fonts, spacing, shadows } = useTheme();
const zohoPeopleState = useSelector(selectZohoPeopleState);
const { loading = false, error = null } = zohoPeopleState || {};
const attendanceAnalytics = useSelector(selectAttendanceAnalytics);
const employeeAnalytics = useSelector(selectEmployeeAnalytics);
const leaveAnalytics = useSelector(selectLeaveAnalytics);
const holidayAnalytics = useSelector(selectHolidayAnalytics);
useEffect(() => {
dispatch(fetchZohoPeopleData() as any);
}, [dispatch]);
const handleRefresh = async () => {
try {
await (dispatch(fetchZohoPeopleData() as any) as any).unwrap();
} catch (error) {
console.error('Error refreshing data:', error);
}
};
if (loading && !attendanceAnalytics && !employeeAnalytics) {
return <LoadingSpinner />;
}
if (error) {
return <ErrorState message={error} onRetry={() => dispatch(fetchZohoPeopleData() as any)} />;
}
return (
<Container>
<ScrollView
style={styles.container}
refreshControl={
<RefreshControl refreshing={loading} onRefresh={handleRefresh} />
}
showsVerticalScrollIndicator={false}
>
{/* Header */}
<View style={[styles.header, { backgroundColor: colors.surface, ...shadows.light }]}>
<View style={styles.headerContent}>
<View style={styles.headerLeft}>
<Icon name="people" size={28} color={colors.primary} />
<Text style={[styles.headerTitle, { color: colors.text, fontFamily: fonts.bold }]}>
Zoho People Dashboard
</Text>
</View>
<TouchableOpacity
style={[styles.refreshButton, { backgroundColor: colors.primary }]}
onPress={handleRefresh}
>
<Icon name="refresh" size={20} color={colors.surface} />
</TouchableOpacity>
</View>
</View>
<View style={[styles.content, { padding: spacing.md }]}>
{/* KPI Cards Row 1 */}
<View style={styles.kpiRow}>
<KPICard
title="Total Employees"
value={employeeAnalytics?.totalEmployees?.toString() || '0'}
subtitle="Active"
icon="people"
color={colors.primary}
colors={colors}
fonts={fonts}
shadows={shadows}
/>
<KPICard
title="Present Today"
value={attendanceAnalytics?.presentToday?.toString() || '0'}
subtitle={`${attendanceAnalytics?.attendanceRate?.toFixed(1) || '0'}% attendance`}
icon="check-circle"
color="#10B981"
colors={colors}
fonts={fonts}
shadows={shadows}
/>
</View>
{/* KPI Cards Row 2 */}
<View style={styles.kpiRow}>
<KPICard
title="Leave Balance"
value={leaveAnalytics?.totalLeaveBalance?.toString() || '0'}
subtitle="Total days"
icon="event-available"
color="#3B82F6"
colors={colors}
fonts={fonts}
shadows={shadows}
/>
<KPICard
title="Upcoming Holidays"
value={holidayAnalytics?.upcomingHolidays?.length?.toString() || '0'}
subtitle="Next 30 days"
icon="event"
color="#F59E0B"
colors={colors}
fonts={fonts}
shadows={shadows}
/>
</View>
{/* Attendance Overview */}
{attendanceAnalytics && (
<View style={[styles.card, { backgroundColor: colors.surface, ...shadows.medium }]}>
<View style={styles.cardHeader}>
<Icon name="schedule" size={20} color={colors.primary} />
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>
Attendance Overview
</Text>
</View>
<View style={styles.attendanceGrid}>
<AttendanceItem
label="Present"
count={attendanceAnalytics.presentToday}
total={attendanceAnalytics.totalEmployees}
color="#10B981"
colors={colors}
fonts={fonts}
/>
<AttendanceItem
label="Absent"
count={attendanceAnalytics.absentToday}
total={attendanceAnalytics.totalEmployees}
color="#EF4444"
colors={colors}
fonts={fonts}
/>
<AttendanceItem
label="Weekend"
count={attendanceAnalytics.weekendToday}
total={attendanceAnalytics.totalEmployees}
color="#6B7280"
colors={colors}
fonts={fonts}
/>
</View>
<View style={styles.workingHoursContainer}>
<Text style={[styles.workingHoursLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>
Average Working Hours
</Text>
<Text style={[styles.workingHoursValue, { color: colors.text, fontFamily: fonts.bold }]}>
{attendanceAnalytics.averageWorkingHours.toFixed(1)} hrs
</Text>
</View>
</View>
)}
{/* Department Distribution */}
{employeeAnalytics && (
<View style={[styles.card, { backgroundColor: colors.surface, ...shadows.medium }]}>
<View style={styles.cardHeader}>
<Icon name="business" size={20} color={colors.primary} />
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>
Department Distribution
</Text>
</View>
<View style={styles.departmentList}>
{Object.entries(employeeAnalytics.departmentDistribution).map(([dept, count]) => (
<DepartmentItem
key={dept}
department={dept}
count={count}
total={employeeAnalytics.totalEmployees}
colors={colors}
fonts={fonts}
/>
))}
</View>
</View>
)}
{/* Leave Analytics */}
{leaveAnalytics && (
<View style={[styles.card, { backgroundColor: colors.surface, ...shadows.medium }]}>
<View style={styles.cardHeader}>
<Icon name="event-available" size={20} color={colors.primary} />
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>
Leave Analytics
</Text>
</View>
<View style={styles.leaveStats}>
<LeaveStatItem
label="Paid Leave"
value={leaveAnalytics.paidLeaveBalance}
color="#3B82F6"
colors={colors}
fonts={fonts}
/>
<LeaveStatItem
label="Unpaid Leave"
value={leaveAnalytics.unpaidLeaveBalance}
color="#6B7280"
colors={colors}
fonts={fonts}
/>
<LeaveStatItem
label="Utilization"
value={`${leaveAnalytics.leaveUtilizationRate.toFixed(1)}%`}
color="#10B981"
colors={colors}
fonts={fonts}
/>
</View>
{leaveAnalytics.upcomingLeaves.length > 0 && (
<View style={styles.upcomingLeaves}>
<Text style={[styles.upcomingLeavesTitle, { color: colors.text, fontFamily: fonts.medium }]}>
Upcoming Leaves
</Text>
{leaveAnalytics.upcomingLeaves.slice(0, 3).map((leave, index) => (
<View key={index} style={styles.leaveItem}>
<Text style={[styles.leaveEmployee, { color: colors.text, fontFamily: fonts.regular }]}>
{leave.Employee_ID}
</Text>
<Text style={[styles.leaveDates, { color: colors.textLight, fontFamily: fonts.regular }]}>
{leave.From} - {leave.To}
</Text>
</View>
))}
</View>
)}
</View>
)}
{/* Upcoming Holidays */}
{holidayAnalytics && holidayAnalytics.upcomingHolidays.length > 0 && (
<View style={[styles.card, { backgroundColor: colors.surface, ...shadows.medium }]}>
<View style={styles.cardHeader}>
<Icon name="event" size={20} color={colors.primary} />
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>
Upcoming Holidays
</Text>
</View>
<View style={styles.holidaysList}>
{holidayAnalytics.upcomingHolidays.slice(0, 5).map((holiday, index) => (
<HolidayItem
key={holiday.Id}
holiday={holiday}
colors={colors}
fonts={fonts}
/>
))}
</View>
</View>
)}
</View>
</ScrollView>
</Container>
);
};
// KPI Card Component
const KPICard: React.FC<{
title: string;
value: string;
subtitle: string;
icon: string;
color: string;
colors: any;
fonts: any;
shadows: any;
}> = ({ title, value, subtitle, icon, color, colors, fonts, shadows }) => (
<View style={[styles.kpiCard, { backgroundColor: colors.surface, ...shadows.medium }]}>
<View style={styles.kpiHeader}>
<Icon name={icon} size={24} color={color} />
<Text style={[styles.kpiTitle, { color: colors.textLight, fontFamily: fonts.regular }]}>
{title}
</Text>
</View>
<Text style={[styles.kpiValue, { color: colors.text, fontFamily: fonts.bold }]}>
{value}
</Text>
<Text style={[styles.kpiSubtitle, { color: colors.textLight, fontFamily: fonts.regular }]}>
{subtitle}
</Text>
</View>
);
// Attendance Item Component
const AttendanceItem: React.FC<{
label: string;
count: number;
total: number;
color: string;
colors: any;
fonts: any;
}> = ({ label, count, total, color, colors, fonts }) => {
const percentage = total > 0 ? (count / total) * 100 : 0;
return (
<View style={styles.attendanceItem}>
<View style={styles.attendanceItemHeader}>
<View style={[styles.attendanceDot, { backgroundColor: color }]} />
<Text style={[styles.attendanceLabel, { color: colors.text, fontFamily: fonts.regular }]}>
{label}
</Text>
</View>
<Text style={[styles.attendanceCount, { color: colors.text, fontFamily: fonts.bold }]}>
{count}
</Text>
<Text style={[styles.attendancePercentage, { color: colors.textLight, fontFamily: fonts.regular }]}>
{percentage.toFixed(1)}%
</Text>
</View>
);
};
// Department Item Component
const DepartmentItem: React.FC<{
department: string;
count: number;
total: number;
colors: any;
fonts: any;
}> = ({ department, count, total, colors, fonts }) => {
const percentage = total > 0 ? (count / total) * 100 : 0;
return (
<View style={styles.departmentItem}>
<View style={styles.departmentItemHeader}>
<Text style={[styles.departmentName, { color: colors.text, fontFamily: fonts.regular }]}>
{department}
</Text>
<Text style={[styles.departmentCount, { color: colors.text, fontFamily: fonts.bold }]}>
{count}
</Text>
</View>
<View style={[styles.departmentBar, { backgroundColor: colors.background }]}>
<View
style={[
styles.departmentBarFill,
{
backgroundColor: colors.primary,
width: `${percentage}%`
}
]}
/>
</View>
</View>
);
};
// Leave Stat Item Component
const LeaveStatItem: React.FC<{
label: string;
value: string | number;
color: string;
colors: any;
fonts: any;
}> = ({ label, value, color, colors, fonts }) => (
<View style={styles.leaveStatItem}>
<Text style={[styles.leaveStatLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>
{label}
</Text>
<Text style={[styles.leaveStatValue, { color, fontFamily: fonts.bold }]}>
{value}
</Text>
</View>
);
// Holiday Item Component
const HolidayItem: React.FC<{
holiday: any;
colors: any;
fonts: any;
}> = ({ holiday, colors, fonts }) => (
<View style={styles.holidayItem}>
<View style={styles.holidayItemLeft}>
<Icon
name={holiday.isHalfday ? "schedule" : "event"}
size={16}
color={holiday.isRestrictedHoliday ? "#F59E0B" : colors.primary}
/>
<Text style={[styles.holidayName, { color: colors.text, fontFamily: fonts.regular }]}>
{holiday.Name}
</Text>
</View>
<Text style={[styles.holidayDate, { color: colors.textLight, fontFamily: fonts.regular }]}>
{holiday.Date}
</Text>
</View>
);
const styles = StyleSheet.create({
container: {
flex: 1,
},
header: {
paddingHorizontal: 16,
paddingVertical: 12,
borderBottomWidth: 1,
borderBottomColor: '#E5E7EB',
},
headerContent: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
headerLeft: {
flexDirection: 'row',
alignItems: 'center',
},
headerTitle: {
fontSize: 20,
marginLeft: 8,
},
refreshButton: {
width: 36,
height: 36,
borderRadius: 18,
justifyContent: 'center',
alignItems: 'center',
},
content: {
flex: 1,
},
kpiRow: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 16,
},
kpiCard: {
flex: 1,
marginHorizontal: 4,
padding: 16,
borderRadius: 12,
borderWidth: 1,
borderColor: '#E5E7EB',
},
kpiHeader: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 8,
},
kpiTitle: {
fontSize: 12,
marginLeft: 6,
},
kpiValue: {
fontSize: 24,
marginBottom: 4,
},
kpiSubtitle: {
fontSize: 11,
},
card: {
marginBottom: 16,
padding: 16,
borderRadius: 12,
borderWidth: 1,
borderColor: '#E5E7EB',
},
cardHeader: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 16,
},
cardTitle: {
fontSize: 16,
marginLeft: 8,
},
attendanceGrid: {
flexDirection: 'row',
justifyContent: 'space-around',
marginBottom: 16,
},
attendanceItem: {
alignItems: 'center',
},
attendanceItemHeader: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 8,
},
attendanceDot: {
width: 8,
height: 8,
borderRadius: 4,
marginRight: 6,
},
attendanceLabel: {
fontSize: 12,
},
attendanceCount: {
fontSize: 20,
marginBottom: 4,
},
attendancePercentage: {
fontSize: 11,
},
workingHoursContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingTop: 16,
borderTopWidth: 1,
borderTopColor: '#E5E7EB',
},
workingHoursLabel: {
fontSize: 14,
},
workingHoursValue: {
fontSize: 16,
},
departmentList: {
marginTop: 8,
},
departmentItem: {
marginBottom: 12,
},
departmentItemHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 6,
},
departmentName: {
fontSize: 14,
flex: 1,
},
departmentCount: {
fontSize: 14,
},
departmentBar: {
height: 6,
borderRadius: 3,
overflow: 'hidden',
},
departmentBarFill: {
height: '100%',
borderRadius: 3,
},
leaveStats: {
flexDirection: 'row',
justifyContent: 'space-around',
marginBottom: 16,
},
leaveStatItem: {
alignItems: 'center',
},
leaveStatLabel: {
fontSize: 12,
marginBottom: 4,
},
leaveStatValue: {
fontSize: 18,
},
upcomingLeaves: {
marginTop: 16,
paddingTop: 16,
borderTopWidth: 1,
borderTopColor: '#E5E7EB',
},
upcomingLeavesTitle: {
fontSize: 14,
marginBottom: 12,
},
leaveItem: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 8,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: '#E5E7EB',
},
leaveEmployee: {
fontSize: 14,
flex: 1,
},
leaveDates: {
fontSize: 12,
},
holidaysList: {
marginTop: 8,
},
holidayItem: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 12,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: '#E5E7EB',
},
holidayItemLeft: {
flexDirection: 'row',
alignItems: 'center',
flex: 1,
},
holidayName: {
fontSize: 14,
marginLeft: 8,
flex: 1,
},
holidayDate: {
fontSize: 12,
},
});
export default ZohoPeopleDashboardScreen;

View File

@ -0,0 +1,98 @@
import httpClient from '@/services/http';
import type {
ZohoPeopleApiResponse,
AttendanceReportResponse,
EmployeeFormsResponse,
HolidaysResponse,
LeaveTrackerReportResponse,
LeaveDataResponse,
} from '../types/zohoPeopleTypes';
const API_BASE_URL = '/api/v1/integrations/zoho/people';
export const zohoPeopleAPI = {
// Get attendance report
getAttendanceReport: async (): Promise<ZohoPeopleApiResponse<AttendanceReportResponse>> => {
const response = await httpClient.get(`${API_BASE_URL}/attendance-report?provider=zoho`);
console.log('attendance report response in zoho people api',response)
return response.data as ZohoPeopleApiResponse<AttendanceReportResponse>;
},
// Get employee forms
getEmployeeForms: async (page: number = 1, limit: number = 20): Promise<ZohoPeopleApiResponse<EmployeeFormsResponse>> => {
const response = await httpClient.get(`${API_BASE_URL}/employee-forms?provider=zoho&page=${page}&limit=${limit}`);
console.log('employee forms response in zoho people api',response)
return response.data as ZohoPeopleApiResponse<EmployeeFormsResponse>;
},
// Get holidays
getHolidays: async (): Promise<ZohoPeopleApiResponse<HolidaysResponse>> => {
const response = await httpClient.get(`${API_BASE_URL}/holidays?provider=zoho`);
console.log('holidays response in zoho people api',response)
return response.data as ZohoPeopleApiResponse<HolidaysResponse>;
},
// Get leave tracker report
getLeaveTrackerReport: async (): Promise<ZohoPeopleApiResponse<LeaveTrackerReportResponse>> => {
const response = await httpClient.get(`${API_BASE_URL}/leave-tracker-report?provider=zoho`);
console.log('leave tracker report response in zoho people api',response)
return response.data as ZohoPeopleApiResponse<LeaveTrackerReportResponse>;
},
// Get leave data
getLeaveData: async (): Promise<ZohoPeopleApiResponse<LeaveDataResponse>> => {
const response = await httpClient.get(`${API_BASE_URL}/leave-data?provider=zoho`);
console.log('leave data response in zoho people api',response)
return response.data as ZohoPeopleApiResponse<LeaveDataResponse>;
},
// Get all dashboard data
getAllDashboardData: async () => {
try {
const [attendanceReport, employeeForms, holidays, leaveTracker, leaveData] = await Promise.all([
zohoPeopleAPI.getAttendanceReport(),
zohoPeopleAPI.getEmployeeForms(),
zohoPeopleAPI.getHolidays(),
zohoPeopleAPI.getLeaveTrackerReport(),
zohoPeopleAPI.getLeaveData(),
]);
// Process attendance report
const processedAttendanceReport = attendanceReport?.data?.data || [];
// Process employee forms - handle the nested structure
let processedEmployeeForms: any[] = [];
if (employeeForms?.data?.data) {
// The data is an array of objects where each object contains employee data
processedEmployeeForms = employeeForms.data.data.flatMap((empGroup: any) => {
return Object.values(empGroup).flat();
});
}
// Process holidays
const processedHolidays = holidays?.data?.data || [];
// Process leave tracker - take first item from array
const processedLeaveTracker = leaveTracker?.data?.data?.[0] || null;
// Process leave data - handle the nested structure
let processedLeaveData: any[] = [];
if (leaveData?.data?.data) {
processedLeaveData = leaveData.data.data.flatMap((leaveGroup: any) => {
return Object.values(leaveGroup).flat();
});
}
return {
attendanceReport: processedAttendanceReport,
employeeForms: processedEmployeeForms,
holidays: processedHolidays,
leaveTracker: processedLeaveTracker,
leaveData: processedLeaveData,
};
} catch (error) {
console.error('Error in getAllDashboardData:', error);
throw error;
}
},
};

View File

@ -1,53 +1,130 @@
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import { zohoPeopleAPI } from '../services/zohoPeopleAPI';
import type {
ZohoPeopleState,
AttendanceReportData,
EmployeeFormData,
HolidayData,
LeaveTrackerReportData,
LeaveDataItem
} from '../types/zohoPeopleTypes';
export interface EmployeeMetric {
id: string;
name: string;
value: number;
}
export interface HRState {
metrics: EmployeeMetric[];
loading: boolean;
error: string | null;
}
const initialState: HRState = {
metrics: [],
// Initial state
const initialState: ZohoPeopleState = {
attendanceReport: null,
employeeForms: null,
holidays: null,
leaveTracker: null,
leaveData: null,
loading: false,
error: null,
lastUpdated: null,
};
export const fetchHRMetrics = createAsyncThunk('hr/fetchMetrics', async () => {
// TODO: integrate real HR API
await new Promise(r => setTimeout(r, 300));
return [
{ id: '1', name: 'Headcount', value: 42 },
{ id: '2', name: 'Attendance %', value: 96 },
] as EmployeeMetric[];
});
// Async thunks
export const fetchZohoPeopleData = createAsyncThunk(
'zohoPeople/fetchAllData',
async () => {
const response = await zohoPeopleAPI.getAllDashboardData();
return response;
}
);
const hrSlice = createSlice({
name: 'hr',
export const fetchAttendanceReport = createAsyncThunk(
'zohoPeople/fetchAttendanceReport',
async () => {
const response = await zohoPeopleAPI.getAttendanceReport();
return response.data.data;
}
);
export const fetchEmployeeForms = createAsyncThunk(
'zohoPeople/fetchEmployeeForms',
async (params: { page?: number; limit?: number } = {}) => {
const response = await zohoPeopleAPI.getEmployeeForms(params.page, params.limit);
return response.data.data.flatMap(emp => Object.values(emp).flat());
}
);
export const fetchHolidays = createAsyncThunk(
'zohoPeople/fetchHolidays',
async () => {
const response = await zohoPeopleAPI.getHolidays();
return response.data.data;
}
);
export const fetchLeaveTrackerReport = createAsyncThunk(
'zohoPeople/fetchLeaveTrackerReport',
async () => {
const response = await zohoPeopleAPI.getLeaveTrackerReport();
return response.data.data[0];
}
);
export const fetchLeaveData = createAsyncThunk(
'zohoPeople/fetchLeaveData',
async () => {
const response = await zohoPeopleAPI.getLeaveData();
return response.data.data.flatMap(leave => Object.values(leave).flat());
}
);
// Slice
const zohoPeopleSlice = createSlice({
name: 'zohoPeople',
initialState,
reducers: {},
extraReducers: builder => {
reducers: {
clearError: (state) => {
state.error = null;
},
resetState: () => initialState,
},
extraReducers: (builder) => {
// Fetch all data
builder
.addCase(fetchHRMetrics.pending, state => {
.addCase(fetchZohoPeopleData.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchHRMetrics.fulfilled, (state, action: PayloadAction<EmployeeMetric[]>) => {
.addCase(fetchZohoPeopleData.fulfilled, (state, action) => {
state.loading = false;
state.metrics = action.payload;
state.attendanceReport = action.payload.attendanceReport;
state.employeeForms = action.payload.employeeForms;
state.holidays = action.payload.holidays;
state.leaveTracker = action.payload.leaveTracker;
state.leaveData = action.payload.leaveData;
state.lastUpdated = new Date().toISOString();
})
.addCase(fetchHRMetrics.rejected, (state, action) => {
.addCase(fetchZohoPeopleData.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message || 'Failed to load HR metrics';
state.error = action.error.message || 'Failed to fetch Zoho People data';
});
// Individual data fetchers
builder
.addCase(fetchAttendanceReport.fulfilled, (state, action) => {
state.attendanceReport = action.payload;
state.lastUpdated = new Date().toISOString();
})
.addCase(fetchEmployeeForms.fulfilled, (state, action) => {
state.employeeForms = action.payload;
state.lastUpdated = new Date().toISOString();
})
.addCase(fetchHolidays.fulfilled, (state, action) => {
state.holidays = action.payload;
state.lastUpdated = new Date().toISOString();
})
.addCase(fetchLeaveTrackerReport.fulfilled, (state, action) => {
state.leaveTracker = action.payload;
state.lastUpdated = new Date().toISOString();
})
.addCase(fetchLeaveData.fulfilled, (state, action) => {
state.leaveData = action.payload;
state.lastUpdated = new Date().toISOString();
});
},
});
export default hrSlice;
export const { clearError, resetState } = zohoPeopleSlice.actions;
export default zohoPeopleSlice;

View File

@ -0,0 +1,216 @@
import { createSelector } from '@reduxjs/toolkit';
import type { RootState } from '@/store/store';
import type {
AttendanceAnalytics,
EmployeeAnalytics,
LeaveAnalytics,
HolidayAnalytics
} from '../types/zohoPeopleTypes';
// Base selectors
export const selectZohoPeopleState = (state: RootState) => state.zohoPeople || {
attendanceReport: null,
employeeForms: null,
holidays: null,
leaveTracker: null,
leaveData: null,
loading: false,
error: null,
lastUpdated: null,
};
export const selectAttendanceReport = (state: RootState) => state.zohoPeople?.attendanceReport || null;
export const selectEmployeeForms = (state: RootState) => state.zohoPeople?.employeeForms || null;
export const selectHolidays = (state: RootState) => state.zohoPeople?.holidays || null;
export const selectLeaveTracker = (state: RootState) => state.zohoPeople?.leaveTracker || null;
export const selectLeaveData = (state: RootState) => state.zohoPeople?.leaveData || null;
export const selectZohoPeopleLoading = (state: RootState) => state.zohoPeople?.loading || false;
export const selectZohoPeopleError = (state: RootState) => state.zohoPeople?.error || null;
export const selectZohoPeopleLastUpdated = (state: RootState) => state.zohoPeople?.lastUpdated || null;
// Computed selectors
export const selectAttendanceAnalytics = createSelector(
[selectAttendanceReport],
(attendanceReport): AttendanceAnalytics | null => {
if (!attendanceReport || attendanceReport.length === 0) {
return null;
}
const today = new Date();
const todayISO = today.toISOString().split('T')[0]; // YYYY-MM-DD
const todayFormatted = todayISO.split('-').reverse().join('-'); // DD-MM-YYYY
const todayAlternative = todayISO; // YYYY-MM-DD (in case API uses this format)
let presentToday = 0;
let absentToday = 0;
let weekendToday = 0;
let totalWorkingHours = 0;
let employeesWithHours = 0;
attendanceReport.forEach((employee) => {
if (!employee.attendanceDetails) {
return;
}
// Try different date formats
const todayAttendance = employee.attendanceDetails[todayFormatted] ||
employee.attendanceDetails[todayAlternative] ||
employee.attendanceDetails[todayISO];
if (todayAttendance) {
const status = todayAttendance.Status;
if (status === 'Present' || status === '' || status === 'present') {
presentToday++;
} else if (status === 'Absent' || status === 'absent') {
absentToday++;
} else if (status === 'Weekend' || status === 'weekend') {
weekendToday++;
}
// Calculate working hours
const workingHours = todayAttendance.WorkingHours || todayAttendance.TotalHours;
if (workingHours && workingHours !== '00:00' && workingHours !== '0:00') {
try {
const [hours, minutes] = workingHours.split(':').map(Number);
if (!isNaN(hours) && !isNaN(minutes)) {
totalWorkingHours += hours + minutes / 60;
employeesWithHours++;
}
} catch (error) {
// Silently handle parsing errors
}
}
}
});
const totalEmployees = attendanceReport.length;
const averageWorkingHours = employeesWithHours > 0 ? totalWorkingHours / employeesWithHours : 0;
const attendanceRate = totalEmployees > 0 ? (presentToday / totalEmployees) * 100 : 0;
return {
totalEmployees,
presentToday,
absentToday,
weekendToday,
averageWorkingHours,
attendanceRate,
};
}
);
export const selectEmployeeAnalytics = createSelector(
[selectEmployeeForms],
(employeeForms): EmployeeAnalytics | null => {
if (!employeeForms || employeeForms.length === 0) {
return null;
}
const totalEmployees = employeeForms.length;
const activeEmployees = employeeForms.filter(emp => emp.Employeestatus === 'Active').length;
const departmentDistribution: { [key: string]: number } = {};
const genderDistribution: { [key: string]: number } = {};
let totalAge = 0;
let totalExperience = 0;
let validAgeCount = 0;
let validExpCount = 0;
employeeForms.forEach((emp) => {
// Department distribution
const dept = emp.Department || 'Unknown';
departmentDistribution[dept] = (departmentDistribution[dept] || 0) + 1;
// Gender distribution
const gender = emp.Gender || 'Unknown';
genderDistribution[gender] = (genderDistribution[gender] || 0) + 1;
// Age calculation
if (emp.Age && !isNaN(Number(emp.Age))) {
totalAge += Number(emp.Age);
validAgeCount++;
}
// Experience calculation
if (emp.Experience && !isNaN(Number(emp.Experience))) {
totalExperience += Number(emp.Experience);
validExpCount++;
}
});
return {
totalEmployees,
activeEmployees,
departmentDistribution,
genderDistribution,
averageAge: validAgeCount > 0 ? totalAge / validAgeCount : 0,
averageExperience: validExpCount > 0 ? totalExperience / validExpCount : 0,
};
}
);
export const selectLeaveAnalytics = createSelector(
[selectLeaveTracker, selectLeaveData],
(leaveTracker, leaveData): LeaveAnalytics | null => {
if (!leaveTracker || !leaveData) return null;
let totalLeaveBalance = 0;
let paidLeaveBalance = 0;
let unpaidLeaveBalance = 0;
let totalPaidBooked = 0;
let totalUnpaidBooked = 0;
// Calculate leave balances from tracker
Object.values(leaveTracker.report).forEach(employeeReport => {
totalLeaveBalance += employeeReport.totals.paidBalance + employeeReport.totals.unpaidBalance;
paidLeaveBalance += employeeReport.totals.paidBalance;
unpaidLeaveBalance += employeeReport.totals.unpaidBalance;
totalPaidBooked += employeeReport.totals.paidBooked;
totalUnpaidBooked += employeeReport.totals.unpaidBooked;
});
// Get upcoming leaves (next 30 days)
const today = new Date();
const thirtyDaysFromNow = new Date(today.getTime() + 30 * 24 * 60 * 60 * 1000);
const upcomingLeaves = leaveData.filter(leave => {
const fromDate = new Date(leave.From);
return fromDate >= today && fromDate <= thirtyDaysFromNow;
});
const totalBooked = totalPaidBooked + totalUnpaidBooked;
const leaveUtilizationRate = totalLeaveBalance > 0 ? (totalBooked / (totalLeaveBalance + totalBooked)) * 100 : 0;
return {
totalLeaveBalance,
paidLeaveBalance,
unpaidLeaveBalance,
upcomingLeaves,
leaveUtilizationRate,
};
}
);
export const selectHolidayAnalytics = createSelector(
[selectHolidays],
(holidays): HolidayAnalytics | null => {
if (!holidays || holidays.length === 0) return null;
const today = new Date();
const upcomingHolidays = holidays.filter(holiday => {
const holidayDate = new Date(holiday.Date);
return holidayDate >= today;
}).sort((a, b) => new Date(a.Date).getTime() - new Date(b.Date).getTime());
const totalHolidays = holidays.length;
const restrictedHolidays = holidays.filter(h => h.isRestrictedHoliday).length;
const halfDayHolidays = holidays.filter(h => h.isHalfday).length;
return {
upcomingHolidays,
totalHolidays,
restrictedHolidays,
halfDayHolidays,
};
}
);

View File

@ -0,0 +1,307 @@
// Zoho People API Response Types
export interface ZohoPeopleApiResponse<T> {
status: string;
message: string;
data: T;
timestamp: string;
}
export interface ApiInfo {
count: number;
moreRecords: boolean;
page: number;
message?: string;
status?: number;
}
// Attendance Report Types
export interface AttendanceDetails {
[date: string]: {
ShiftStartTime: string;
Status: string;
FirstIn_Building: string;
LastOut_Location: string;
ShiftName: string;
FirstIn_Latitude: string;
FirstIn: string;
LastOut_Latitude: string;
FirstIn_Longitude: string;
FirstIn_Location: string;
TotalHours: string;
LastOut_Longitude: string;
WorkingHours: string;
LastOut_Building: string;
LastOut: string;
ShiftEndTime: string;
DeviationTime?: string;
OverTime?: string;
};
}
export interface EmployeeDetails {
'mail id': string;
erecno: string;
'last name': string;
'first name': string;
id: string;
}
export interface AttendanceReportData {
attendanceDetails: AttendanceDetails;
employeeDetails: EmployeeDetails;
}
export interface AttendanceReportResponse {
data: AttendanceReportData[];
info: ApiInfo;
}
// Employee Forms Types
export interface EducationDetails {
Specialization: string;
Degree: string;
College: string;
Yearofgraduation: string;
'tabular.ROWID': string;
}
export interface WorkExperience {
Jobtitle: string;
Employer: string;
RELEVANCE: string;
Previous_JobDesc: string;
FromDate: string;
Todate: string;
'tabular.ROWID': string;
'RELEVANCE.id': string;
}
export interface DependentDetails {
[key: string]: any;
}
export interface TabularSections {
'Education Details': EducationDetails[];
'Work experience': WorkExperience[];
'Dependent Details': DependentDetails[];
}
export interface PresentAddressChildValues {
CITY: string;
COUNTRY: string;
STATE: string;
ADDRESS1: string;
PINCODE: string;
ADDRESS2: string;
STATE_CODE: string;
COUNTRY_CODE: string;
}
export interface EmployeeFormData {
EmailID: string;
CreatedTime: string;
'Employee_type.id': string;
Date_of_birth: string;
UAN_Number: string;
AddedTime: string;
Photo: string;
Gender: string;
Marital_status: string;
ModifiedBy: string;
ApprovalStatus: string;
Department: string;
'LocationName.ID': string;
tabularSections: TabularSections;
AddedBy: string;
'Mobile.country_code': string;
Tags: string;
Reporting_To: string;
Photo_downloadUrl: string;
'Source_of_hire.id': string;
'total_experience.displayValue': string;
Employeestatus: string;
Role: string;
Experience: string;
Employee_type: string;
'AddedBy.ID': string;
'Role.ID': string;
LastName: string;
Pan_Number: string;
EmployeeID: string;
ZUID: string;
Aadhaar_Number: string;
Dateofexit: string;
Permanent_Address: string;
Other_Email: string;
LocationName: string;
Work_location: string;
Present_Address: string;
Nick_Name: string;
total_experience: string;
ModifiedTime: string;
'Reporting_To.MailID': string;
Zoho_ID: number;
'Designation.ID': string;
Source_of_hire: string;
Age: string;
Designation: string;
'Age.displayValue': string;
'Marital_status.id': string;
FirstName: string;
AboutMe: string;
Dateofjoining: string;
'Experience.displayValue': string;
Mobile: string;
Extension: string;
'ModifiedBy.ID': string;
'Reporting_To.ID': string;
Work_phone: string;
'Employeestatus.type': number;
'Department.ID': string;
'Present_Address.childValues': PresentAddressChildValues;
Expertise: string;
}
export interface EmployeeFormsResponse {
data: { [employeeId: string]: EmployeeFormData[] }[];
info: ApiInfo;
}
// Holidays Types
export interface HolidayData {
ShiftName: string;
LocationId: string;
classificationType: number;
ShiftId: string;
classification: string;
Date: string;
Name: string;
LocationName: string;
isRestrictedHoliday: boolean;
Remarks: string;
Id: string;
isHalfday: boolean;
Session: number;
}
export interface HolidaysResponse {
data: HolidayData[];
info: ApiInfo;
}
// Leave Tracker Report Types
export interface LeaveType {
unit: string;
code: string;
name: string;
type: string;
}
export interface LeaveBalance {
balance: number;
}
export interface LeaveTotals {
unpaidBooked: number;
unpaidBalance: number;
paidBalance: number;
paidBooked: number;
}
export interface EmployeeLeaveInfo {
name: string;
id: string;
}
export interface EmployeeLeaveReport {
[leaveTypeId: string]: LeaveBalance;
totals: LeaveTotals;
employee: EmployeeLeaveInfo;
}
export interface LeaveTrackerReportData {
leavetypes: { [leaveTypeId: string]: LeaveType };
report: { [employeeId: string]: EmployeeLeaveReport };
employees: string[];
}
export interface LeaveTrackerReportResponse {
data: LeaveTrackerReportData[];
info: ApiInfo;
}
// Leave Data Types
export interface LeaveDataItem {
CreatedTime: string;
Employee_ID: string;
AddedTime: string;
'Leavetype.ID': string;
From: string;
Unit: string;
ModifiedBy: string;
ApprovalStatus: string;
Daystaken: string;
Reasonforleave: string;
'ModifiedBy.ID': string;
TeamEmailID: string;
Leavetype: string;
ModifiedTime: string;
Zoho_ID: number;
'AddedBy.ID': string;
To: string;
AddedBy: string;
'Employee_ID.ID': string;
DateOfRequest: string;
}
export interface LeaveDataResponse {
data: { [leaveId: string]: LeaveDataItem[] }[];
info: ApiInfo;
}
// Dashboard State Types
export interface ZohoPeopleState {
attendanceReport: AttendanceReportData[] | null;
employeeForms: EmployeeFormData[] | null;
holidays: HolidayData[] | null;
leaveTracker: LeaveTrackerReportData | null;
leaveData: LeaveDataItem[] | null;
loading: boolean;
error: string | null;
lastUpdated: string | null;
}
// Dashboard Analytics Types
export interface AttendanceAnalytics {
totalEmployees: number;
presentToday: number;
absentToday: number;
weekendToday: number;
averageWorkingHours: number;
attendanceRate: number;
}
export interface EmployeeAnalytics {
totalEmployees: number;
activeEmployees: number;
departmentDistribution: { [department: string]: number };
genderDistribution: { [gender: string]: number };
averageAge: number;
averageExperience: number;
}
export interface LeaveAnalytics {
totalLeaveBalance: number;
paidLeaveBalance: number;
unpaidLeaveBalance: number;
upcomingLeaves: LeaveDataItem[];
leaveUtilizationRate: number;
}
export interface HolidayAnalytics {
upcomingHolidays: HolidayData[];
totalHolidays: number;
restrictedHolidays: number;
halfDayHolidays: number;
}

View File

@ -8,7 +8,7 @@ import { clearSelectedService } from '@/modules/integrations/store/integrationsS
let pendingRequest: any = null;
const http = create({
baseURL: 'http://192.168.1.23:4000',
baseURL: 'http://192.168.1.19:4000',
// baseURL: 'http://160.187.167.216',
timeout: 10000,
});
@ -47,7 +47,6 @@ http.addRequestTransform((request) => {
// Add response interceptor for error handling
http.addResponseTransform(async (response) => {
console.log('unauthorized response',response)
if (response.status === 401) {
console.warn('Unauthorized request - token may be expired');
@ -174,7 +173,7 @@ http.addResponseTransform(async (response) => {
// Log successful requests for debugging (optional)
if (response.ok && __DEV__) {
console.log(`✅ API Success: ${response.config?.method?.toUpperCase()} ${response.config?.url}`);
// console.log(`✅ API Success: ${response.config?.method?.toUpperCase()} ${response.config?.url}`);
}
});

View File

@ -14,7 +14,7 @@ import zohoBooksSlice from '@/modules/finance/zoho/store/zohoBooksSlice';
const rootReducer = combineReducers({
auth: authSlice.reducer,
hr: hrSlice.reducer,
zohoPeople: hrSlice.reducer,
zohoProjects: zohoProjectsSlice.reducer,
profile: profileSlice.reducer,
integrations: integrationsSlice.reducer,
@ -26,7 +26,7 @@ const rootReducer = combineReducers({
const persistConfig = {
key: 'root',
storage: AsyncStorage,
whitelist: ['auth', 'hr', 'zohoProjects', 'profile', 'integrations', 'crm', 'zohoBooks'],
whitelist: ['auth', 'zohoPeople', 'zohoProjects', 'profile', 'integrations', 'crm', 'zohoBooks'],
blacklist: ['ui'],
};