From 54c751324c856ea4ecc6586195d89ef135a12a31 Mon Sep 17 00:00:00 2001 From: yashwin-foxy Date: Mon, 22 Sep 2025 19:22:16 +0530 Subject: [PATCH] zoho people and zoho books api' integrted and project destructured to add multiple service providers --- src/modules/hr/navigation/HRNavigator.tsx | 4 +- .../hr/zoho/screens/HRDashboardScreen.tsx | 209 ------ .../screens/ZohoPeopleDashboardScreen.tsx | 637 ++++++++++++++++++ src/modules/hr/zoho/services/zohoPeopleAPI.ts | 98 +++ src/modules/hr/zoho/store/hrSlice.ts | 145 +++- src/modules/hr/zoho/store/selectors.ts | 216 ++++++ src/modules/hr/zoho/types/zohoPeopleTypes.ts | 307 +++++++++ src/services/http.ts | 5 +- src/store/store.ts | 4 +- 9 files changed, 1375 insertions(+), 250 deletions(-) delete mode 100644 src/modules/hr/zoho/screens/HRDashboardScreen.tsx create mode 100644 src/modules/hr/zoho/screens/ZohoPeopleDashboardScreen.tsx create mode 100644 src/modules/hr/zoho/services/zohoPeopleAPI.ts create mode 100644 src/modules/hr/zoho/store/selectors.ts create mode 100644 src/modules/hr/zoho/types/zohoPeopleTypes.ts 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'], };