diff --git a/src/modules/hr/navigation/HRNavigator.tsx b/src/modules/hr/navigation/HRNavigator.tsx
index 590d1ac..2d09aaa 100644
--- a/src/modules/hr/navigation/HRNavigator.tsx
+++ b/src/modules/hr/navigation/HRNavigator.tsx
@@ -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 = () => (
-
+
);
diff --git a/src/modules/hr/zoho/screens/HRDashboardScreen.tsx b/src/modules/hr/zoho/screens/HRDashboardScreen.tsx
deleted file mode 100644
index 10d0ad5..0000000
--- a/src/modules/hr/zoho/screens/HRDashboardScreen.tsx
+++ /dev/null
@@ -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 ;
- }
- if (error) {
- return dispatch(fetchHRMetrics() as any)} />;
- }
- return (
-
-
- HR Dashboard
-
-
- {/* KPI Cards */}
-
-
-
-
-
-
-
- {/* Trends: Hires vs Exits */}
-
- Workforce Movements
- ({ in: h, out: mock.exitsTrend[i] }))} max={Math.max(...mock.hiresTrend.map((h, i) => Math.max(h, mock.exitsTrend[i])))} colorA="#10B981" colorB="#EF4444" />
-
- Hires
- Exits
-
-
-
- {/* People Health */}
-
- People Health
-
-
-
-
-
- {/* Department Distribution */}
-
- Department Distribution
- a + b.value, 0)} />
-
- {mock.deptDist.map(s => (
-
-
- {s.label}
-
- ))}
-
-
-
- {/* Lists: Holidays and Top Performers */}
-
-
- Upcoming Holidays
- {mock.holidays.map(h => (
-
- {h.name}
- {h.date}
-
- ))}
-
-
- Top Performers
- {mock.topPerformers.map(p => (
-
- {p.name}
- {p.score}
-
- ))}
-
-
-
-
- );
-};
-
-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 (
-
- {label}
-
- {value}
-
-
-
- );
-};
-
-const DualBars: React.FC<{ data: { in: number; out: number }[]; max: number; colorA: string; colorB: string }> = ({ data, max, colorA, colorB }) => {
- return (
-
- {data.map((d, i) => (
-
-
-
-
- ))}
-
- );
-};
-
-const Progress: React.FC<{ label: string; value: number; color: string; fonts: any }> = ({ label, value, color, fonts }) => {
- return (
-
-
- {label}
- {value}%
-
-
-
-
-
- );
-};
-
-const Stacked: React.FC<{ segments: { label: string; value: number; color: string }[]; total: number }> = ({ segments, total }) => {
- return (
-
- {segments.map(s => (
-
- ))}
-
- );
-};
-
-
diff --git a/src/modules/hr/zoho/screens/ZohoPeopleDashboardScreen.tsx b/src/modules/hr/zoho/screens/ZohoPeopleDashboardScreen.tsx
new file mode 100644
index 0000000..f177e79
--- /dev/null
+++ b/src/modules/hr/zoho/screens/ZohoPeopleDashboardScreen.tsx
@@ -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 ;
+ }
+
+ if (error) {
+ return dispatch(fetchZohoPeopleData() as any)} />;
+ }
+
+ return (
+
+
+ }
+ showsVerticalScrollIndicator={false}
+ >
+ {/* Header */}
+
+
+
+
+
+ Zoho People Dashboard
+
+
+
+
+
+
+
+
+
+ {/* KPI Cards Row 1 */}
+
+
+
+
+
+ {/* KPI Cards Row 2 */}
+
+
+
+
+
+ {/* Attendance Overview */}
+ {attendanceAnalytics && (
+
+
+
+
+ Attendance Overview
+
+
+
+
+
+
+
+
+
+
+
+ Average Working Hours
+
+
+ {attendanceAnalytics.averageWorkingHours.toFixed(1)} hrs
+
+
+
+ )}
+
+ {/* Department Distribution */}
+ {employeeAnalytics && (
+
+
+
+
+ Department Distribution
+
+
+
+
+ {Object.entries(employeeAnalytics.departmentDistribution).map(([dept, count]) => (
+
+ ))}
+
+
+ )}
+
+ {/* Leave Analytics */}
+ {leaveAnalytics && (
+
+
+
+
+ Leave Analytics
+
+
+
+
+
+
+
+
+
+ {leaveAnalytics.upcomingLeaves.length > 0 && (
+
+
+ Upcoming Leaves
+
+ {leaveAnalytics.upcomingLeaves.slice(0, 3).map((leave, index) => (
+
+
+ {leave.Employee_ID}
+
+
+ {leave.From} - {leave.To}
+
+
+ ))}
+
+ )}
+
+ )}
+
+ {/* Upcoming Holidays */}
+ {holidayAnalytics && holidayAnalytics.upcomingHolidays.length > 0 && (
+
+
+
+
+ Upcoming Holidays
+
+
+
+
+ {holidayAnalytics.upcomingHolidays.slice(0, 5).map((holiday, index) => (
+
+ ))}
+
+
+ )}
+
+
+
+ );
+};
+
+// 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 }) => (
+
+
+
+
+ {title}
+
+
+
+ {value}
+
+
+ {subtitle}
+
+
+);
+
+// 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 (
+
+
+
+
+ {label}
+
+
+
+ {count}
+
+
+ {percentage.toFixed(1)}%
+
+
+ );
+};
+
+// 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 (
+
+
+
+ {department}
+
+
+ {count}
+
+
+
+
+
+
+ );
+};
+
+// Leave Stat Item Component
+const LeaveStatItem: React.FC<{
+ label: string;
+ value: string | number;
+ color: string;
+ colors: any;
+ fonts: any;
+}> = ({ label, value, color, colors, fonts }) => (
+
+
+ {label}
+
+
+ {value}
+
+
+);
+
+// Holiday Item Component
+const HolidayItem: React.FC<{
+ holiday: any;
+ colors: any;
+ fonts: any;
+}> = ({ holiday, colors, fonts }) => (
+
+
+
+
+ {holiday.Name}
+
+
+
+ {holiday.Date}
+
+
+);
+
+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;
diff --git a/src/modules/hr/zoho/services/zohoPeopleAPI.ts b/src/modules/hr/zoho/services/zohoPeopleAPI.ts
new file mode 100644
index 0000000..70a0e97
--- /dev/null
+++ b/src/modules/hr/zoho/services/zohoPeopleAPI.ts
@@ -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> => {
+ 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;
+ },
+
+ // Get employee forms
+ getEmployeeForms: async (page: number = 1, limit: number = 20): Promise> => {
+ 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;
+ },
+
+ // Get holidays
+ getHolidays: async (): Promise> => {
+ 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;
+ },
+
+ // Get leave tracker report
+ getLeaveTrackerReport: async (): Promise> => {
+ 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;
+ },
+
+ // Get leave data
+ getLeaveData: async (): Promise> => {
+ 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;
+ },
+
+ // 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;
+ }
+ },
+};
diff --git a/src/modules/hr/zoho/store/hrSlice.ts b/src/modules/hr/zoho/store/hrSlice.ts
index 08a2f5c..aff2d28 100644
--- a/src/modules/hr/zoho/store/hrSlice.ts
+++ b/src/modules/hr/zoho/store/hrSlice.ts
@@ -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) => {
+ .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;
\ No newline at end of file
diff --git a/src/modules/hr/zoho/store/selectors.ts b/src/modules/hr/zoho/store/selectors.ts
new file mode 100644
index 0000000..c27b248
--- /dev/null
+++ b/src/modules/hr/zoho/store/selectors.ts
@@ -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,
+ };
+ }
+);
diff --git a/src/modules/hr/zoho/types/zohoPeopleTypes.ts b/src/modules/hr/zoho/types/zohoPeopleTypes.ts
new file mode 100644
index 0000000..2ab66b2
--- /dev/null
+++ b/src/modules/hr/zoho/types/zohoPeopleTypes.ts
@@ -0,0 +1,307 @@
+// Zoho People API Response Types
+
+export interface ZohoPeopleApiResponse {
+ 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;
+}
diff --git a/src/services/http.ts b/src/services/http.ts
index 5da2b8b..bb5e50b 100644
--- a/src/services/http.ts
+++ b/src/services/http.ts
@@ -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}`);
}
});
diff --git a/src/store/store.ts b/src/store/store.ts
index 8ecc04e..e6538b2 100644
--- a/src/store/store.ts
+++ b/src/store/store.ts
@@ -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'],
};