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 React from 'react';
|
||||||
import { createStackNavigator } from '@react-navigation/stack';
|
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 Stack = createStackNavigator();
|
||||||
|
|
||||||
const HRNavigator = () => (
|
const HRNavigator = () => (
|
||||||
<Stack.Navigator>
|
<Stack.Navigator>
|
||||||
<Stack.Screen name="HRDashboard" component={HRDashboardScreen} options={{headerShown:false}} />
|
<Stack.Screen name="ZohoPeopleDashboard" component={ZohoPeopleDashboardScreen} options={{headerShown:false}} />
|
||||||
</Stack.Navigator>
|
</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 { 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 {
|
// Initial state
|
||||||
id: string;
|
const initialState: ZohoPeopleState = {
|
||||||
name: string;
|
attendanceReport: null,
|
||||||
value: number;
|
employeeForms: null,
|
||||||
}
|
holidays: null,
|
||||||
|
leaveTracker: null,
|
||||||
export interface HRState {
|
leaveData: null,
|
||||||
metrics: EmployeeMetric[];
|
|
||||||
loading: boolean;
|
|
||||||
error: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialState: HRState = {
|
|
||||||
metrics: [],
|
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
|
lastUpdated: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchHRMetrics = createAsyncThunk('hr/fetchMetrics', async () => {
|
// Async thunks
|
||||||
// TODO: integrate real HR API
|
export const fetchZohoPeopleData = createAsyncThunk(
|
||||||
await new Promise(r => setTimeout(r, 300));
|
'zohoPeople/fetchAllData',
|
||||||
return [
|
async () => {
|
||||||
{ id: '1', name: 'Headcount', value: 42 },
|
const response = await zohoPeopleAPI.getAllDashboardData();
|
||||||
{ id: '2', name: 'Attendance %', value: 96 },
|
return response;
|
||||||
] as EmployeeMetric[];
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
const hrSlice = createSlice({
|
export const fetchAttendanceReport = createAsyncThunk(
|
||||||
name: 'hr',
|
'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,
|
initialState,
|
||||||
reducers: {},
|
reducers: {
|
||||||
extraReducers: builder => {
|
clearError: (state) => {
|
||||||
|
state.error = null;
|
||||||
|
},
|
||||||
|
resetState: () => initialState,
|
||||||
|
},
|
||||||
|
extraReducers: (builder) => {
|
||||||
|
// Fetch all data
|
||||||
builder
|
builder
|
||||||
.addCase(fetchHRMetrics.pending, state => {
|
.addCase(fetchZohoPeopleData.pending, (state) => {
|
||||||
state.loading = true;
|
state.loading = true;
|
||||||
state.error = null;
|
state.error = null;
|
||||||
})
|
})
|
||||||
.addCase(fetchHRMetrics.fulfilled, (state, action: PayloadAction<EmployeeMetric[]>) => {
|
.addCase(fetchZohoPeopleData.fulfilled, (state, action) => {
|
||||||
state.loading = false;
|
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.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;
|
let pendingRequest: any = null;
|
||||||
|
|
||||||
const http = create({
|
const http = create({
|
||||||
baseURL: 'http://192.168.1.23:4000',
|
baseURL: 'http://192.168.1.19:4000',
|
||||||
// baseURL: 'http://160.187.167.216',
|
// baseURL: 'http://160.187.167.216',
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
});
|
});
|
||||||
@ -47,7 +47,6 @@ http.addRequestTransform((request) => {
|
|||||||
|
|
||||||
// Add response interceptor for error handling
|
// Add response interceptor for error handling
|
||||||
http.addResponseTransform(async (response) => {
|
http.addResponseTransform(async (response) => {
|
||||||
console.log('unauthorized response',response)
|
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
console.warn('Unauthorized request - token may be expired');
|
console.warn('Unauthorized request - token may be expired');
|
||||||
|
|
||||||
@ -174,7 +173,7 @@ http.addResponseTransform(async (response) => {
|
|||||||
|
|
||||||
// Log successful requests for debugging (optional)
|
// Log successful requests for debugging (optional)
|
||||||
if (response.ok && __DEV__) {
|
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({
|
const rootReducer = combineReducers({
|
||||||
auth: authSlice.reducer,
|
auth: authSlice.reducer,
|
||||||
hr: hrSlice.reducer,
|
zohoPeople: hrSlice.reducer,
|
||||||
zohoProjects: zohoProjectsSlice.reducer,
|
zohoProjects: zohoProjectsSlice.reducer,
|
||||||
profile: profileSlice.reducer,
|
profile: profileSlice.reducer,
|
||||||
integrations: integrationsSlice.reducer,
|
integrations: integrationsSlice.reducer,
|
||||||
@ -26,7 +26,7 @@ const rootReducer = combineReducers({
|
|||||||
const persistConfig = {
|
const persistConfig = {
|
||||||
key: 'root',
|
key: 'root',
|
||||||
storage: AsyncStorage,
|
storage: AsyncStorage,
|
||||||
whitelist: ['auth', 'hr', 'zohoProjects', 'profile', 'integrations', 'crm', 'zohoBooks'],
|
whitelist: ['auth', 'zohoPeople', 'zohoProjects', 'profile', 'integrations', 'crm', 'zohoBooks'],
|
||||||
blacklist: ['ui'],
|
blacklist: ['ui'],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user