zoho people and zoho books api' integrted and project destructured to add multiple service providers
This commit is contained in:
parent
8c1e5309e5
commit
54c751324c
@ -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>
|
||||
);
|
||||
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
637
src/modules/hr/zoho/screens/ZohoPeopleDashboardScreen.tsx
Normal file
637
src/modules/hr/zoho/screens/ZohoPeopleDashboardScreen.tsx
Normal 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;
|
||||
98
src/modules/hr/zoho/services/zohoPeopleAPI.ts
Normal file
98
src/modules/hr/zoho/services/zohoPeopleAPI.ts
Normal 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;
|
||||
}
|
||||
},
|
||||
};
|
||||
@ -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;
|
||||
216
src/modules/hr/zoho/store/selectors.ts
Normal file
216
src/modules/hr/zoho/store/selectors.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
);
|
||||
307
src/modules/hr/zoho/types/zohoPeopleTypes.ts
Normal file
307
src/modules/hr/zoho/types/zohoPeopleTypes.ts
Normal 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;
|
||||
}
|
||||
@ -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}`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -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'],
|
||||
};
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user