diff --git a/App.tsx b/App.tsx
index 3ba1d32..450bf64 100644
--- a/App.tsx
+++ b/App.tsx
@@ -18,6 +18,7 @@ import AuthNavigator from '@/modules/auth/navigation/AuthNavigator';
import type { RootState } from '@/store/store';
import IntegrationsNavigator from '@/modules/integrations/navigation/IntegrationsNavigator';
import { StatusBar } from 'react-native';
+import Toast from 'react-native-toast-message';
function AppContent(): React.JSX.Element {
const isAuthenticated = useSelector((s: RootState) => Boolean(s.auth.isAuthenticated));
@@ -63,6 +64,7 @@ function AppContent(): React.JSX.Element {
)
)}
+
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 5a82dba..613f59b 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -9,6 +9,7 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:allowBackup="false"
android:theme="@style/AppTheme"
+ android:networkSecurityConfig="@xml/network_security_config"
android:supportsRtl="true">
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/auth/index.ts b/src/modules/auth/index.ts
new file mode 100644
index 0000000..b872034
--- /dev/null
+++ b/src/modules/auth/index.ts
@@ -0,0 +1,15 @@
+// Export auth slice and actions
+export { default as authSlice } from './store/authSlice';
+export { login, logout, clearError } from './store/authSlice';
+
+// Export selectors
+export * from './store/selectors';
+
+// Export services
+export { authAPI } from './services/authAPI';
+
+// Export navigation
+export { default as AuthNavigator } from './navigation/AuthNavigator';
+
+// Export screens
+export { default as LoginScreen } from './screens/LoginScreen';
diff --git a/src/modules/auth/screens/LoginScreen.tsx b/src/modules/auth/screens/LoginScreen.tsx
index 92e9309..0c298da 100644
--- a/src/modules/auth/screens/LoginScreen.tsx
+++ b/src/modules/auth/screens/LoginScreen.tsx
@@ -16,6 +16,7 @@ import { login, clearError } from '@/modules/auth/store/authSlice';
import type { RootState, AppDispatch } from '@/store/store';
import { useTheme } from '@/shared/styles/useTheme';
import { validateLoginForm } from '@/shared/utils/validation';
+import { showError, showSuccess, showWarning, showInfo } from '@/shared/utils/Toast';
const LoginScreen: React.FC = () => {
const dispatch = useDispatch();
@@ -64,6 +65,14 @@ const LoginScreen: React.FC = () => {
if (!validation.isValid) {
setValidationErrors(validation.errors);
+
+ // Show toast messages for validation errors
+ if (validation.errors.email) {
+ showError(validation.errors.email);
+ }
+ if (validation.errors.password) {
+ showError(validation.errors.password);
+ }
return;
}
@@ -73,15 +82,17 @@ const LoginScreen: React.FC = () => {
// Check if login was successful
if (login.fulfilled.match(result)) {
- // Login successful - navigation will be handled by the app navigator
- // based on isAuthenticated state
- Alert.alert('Success', 'Login successful!', [
- { text: 'OK', style: 'default' }
- ]);
+ // Login successful - show success toast
+ showSuccess('Login successful! Welcome back!');
+ // Navigation will be handled by the app navigator based on isAuthenticated state
+ } else if (login.rejected.match(result)) {
+ // Login failed - show error toast
+ showError(result.payload as string || 'Login failed. Please try again.');
}
} catch (err) {
// Error handling is done in the slice
console.error('Login error:', err);
+ showError('An unexpected error occurred. Please try again.');
}
};
@@ -222,12 +233,18 @@ const LoginScreen: React.FC = () => {
{/* Row: Remember me + Forgot password */}
- setRememberMe(v => !v)}>
+ {
+ setRememberMe(v => !v);
+ showInfo(rememberMe ? 'Will not remember login' : 'Will remember login');
+ }}
+ >
Remember me
-
+ showInfo('Forgot password feature coming soon!')}>
Forgot Password ?
@@ -263,21 +280,30 @@ const LoginScreen: React.FC = () => {
{/* Social buttons */}
-
+ showInfo('Google login coming soon!')}
+ >
-
+ showInfo('Facebook login coming soon!')}
+ >
-
+ showInfo('Apple login coming soon!')}
+ >
{/* Sign up */}
- Don’t have an account?
-
+ Don't have an account?
+ showInfo('Sign up feature coming soon!')}>
Sign up
diff --git a/src/modules/auth/store/selectors.ts b/src/modules/auth/store/selectors.ts
new file mode 100644
index 0000000..78670ec
--- /dev/null
+++ b/src/modules/auth/store/selectors.ts
@@ -0,0 +1,35 @@
+import { createSelector } from '@reduxjs/toolkit';
+import type { RootState } from '@/store/store';
+
+// Base selectors
+export const selectAuthState = (state: RootState) => state.auth;
+
+export const selectUser = createSelector(
+ [selectAuthState],
+ (auth) => auth.user
+);
+
+export const selectAccessToken = createSelector(
+ [selectAuthState],
+ (auth) => auth.accessToken
+);
+
+export const selectRefreshToken = createSelector(
+ [selectAuthState],
+ (auth) => auth.refreshToken
+);
+
+export const selectIsAuthenticated = createSelector(
+ [selectAuthState],
+ (auth) => auth.isAuthenticated
+);
+
+export const selectAuthLoading = createSelector(
+ [selectAuthState],
+ (auth) => auth.loading
+);
+
+export const selectAuthError = createSelector(
+ [selectAuthState],
+ (auth) => auth.error
+);
diff --git a/src/modules/crm/components/CrmDataCards.tsx b/src/modules/crm/components/CrmDataCards.tsx
new file mode 100644
index 0000000..7619b09
--- /dev/null
+++ b/src/modules/crm/components/CrmDataCards.tsx
@@ -0,0 +1,353 @@
+import React from 'react';
+import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
+import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
+import { useTheme } from '@/shared/styles/useTheme';
+import type { CrmLead, CrmTask, CrmContact, CrmDeal } from '../types/CrmTypes';
+
+interface BaseCardProps {
+ onPress: () => void;
+}
+
+interface LeadCardProps extends BaseCardProps {
+ lead: CrmLead;
+}
+
+interface TaskCardProps extends BaseCardProps {
+ task: CrmTask;
+}
+
+interface ContactCardProps extends BaseCardProps {
+ contact: CrmContact;
+}
+
+interface DealCardProps extends BaseCardProps {
+ deal: CrmDeal;
+}
+
+const getStatusColor = (status: string, colors: any) => {
+ // return '#3AA0FF';
+ switch (status.toLowerCase()) {
+ case 'new':
+ case 'not started':
+ case 'attempted to contact':
+ return '#3AA0FF';
+ case 'contacted':
+ case 'in progress':
+ case 'qualification':
+ return '#F59E0B';
+ case 'qualified':
+ case 'proposal':
+ return '#10B981';
+ case 'completed':
+ case 'closed won':
+ return '#22C55E';
+ case 'unqualified':
+ case 'cancelled':
+ case 'lost lead':
+ return '#EF4444';
+ default:
+ return colors.textLight;
+ }
+};
+
+const getPriorityColor = (priority: string) => {
+ return '#EF4444';
+ switch (priority.toLowerCase()) {
+ case 'high':
+ return '#EF4444';
+ case 'medium':
+ return '#F59E0B';
+ case 'low':
+ return '#10B981';
+ default:
+ return '#6B7280';
+ }
+};
+
+export const LeadCard: React.FC = ({ lead, onPress }) => {
+ const { colors, fonts, shadows } = useTheme();
+
+ return (
+
+
+
+
+ {lead.Full_Name}
+
+
+
+ {lead.Lead_Status}
+
+
+
+
+ {lead.Company}
+
+
+
+
+
+
+
+ {lead.Email}
+
+
+
+
+
+ {lead.Phone}
+
+
+
+
+
+ Source: {lead.Lead_Source}
+
+
+ {lead.Annual_Revenue && (
+
+
+
+ ${lead.Annual_Revenue.toLocaleString()}
+
+
+ )}
+
+
+
+
+ Created: {new Date(lead.Created_Time)?.toLocaleDateString()}
+
+
+
+ );
+};
+
+export const TaskCard: React.FC = ({ task, onPress }) => {
+ const { colors, fonts, shadows } = useTheme();
+
+ return (
+
+
+
+
+ {task.Subject}
+
+
+
+ {task.Status}
+
+
+
+
+ {task.Priority}
+
+
+
+
+
+ {task.Description}
+
+
+
+
+ Priority: {task.Priority}
+
+
+
+
+
+ Assigned: {task.Owner.name}
+
+
+
+
+
+
+ Due: {new Date(task.Due_Date)?.toLocaleDateString()}
+
+
+
+ );
+};
+
+export const ContactCard: React.FC = ({ contact, onPress }) => {
+ const { colors, fonts, shadows } = useTheme();
+
+ return (
+
+
+
+
+ {contact.Full_Name}
+
+
+
+ Active
+
+
+
+
+ {contact.Title} at {contact.Account_Name?.name || 'N/A'}
+
+
+
+
+
+
+
+ {contact.Email}
+
+
+
+
+
+ {contact.Phone}
+
+
+
+
+
+ Source: {contact.Lead_Source}
+
+
+
+
+
+
+ Last contact: {new Date(contact.Last_Activity_Time)?.toLocaleDateString()}
+
+
+
+ );
+};
+
+export const DealCard: React.FC = ({ deal, onPress }) => {
+ const { colors, fonts, shadows } = useTheme();
+
+ return (
+
+
+
+
+ {deal.Deal_Name}
+
+
+
+ {deal.Stage}
+
+
+
+
+ {deal.Account_Name?.name || 'N/A'}
+
+
+
+
+
+
+
+ ${deal.Amount?.toLocaleString()}
+
+
+
+
+
+ Probability: {deal.Probability}%
+
+
+
+
+
+ Contact: {deal.Contact_Name?.name || 'N/A'}
+
+
+
+
+
+
+ Close date: {new Date(deal.Closing_Date)?.toLocaleDateString()}
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ card: {
+ borderRadius: 12,
+ borderWidth: 1,
+ marginBottom: 16,
+ overflow: 'hidden',
+ },
+ cardHeader: {
+ padding: 16,
+ paddingBottom: 12,
+ },
+ cardTitleRow: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'flex-start',
+ marginBottom: 4,
+ },
+ cardTitle: {
+ fontSize: 16,
+ flex: 1,
+ marginRight: 8,
+ },
+ cardSubtitle: {
+ fontSize: 14,
+ },
+ statusBadge: {
+ paddingHorizontal: 8,
+ paddingVertical: 4,
+ borderRadius: 12,
+ },
+ statusText: {
+ fontSize: 12,
+ },
+ cardContent: {
+ paddingHorizontal: 16,
+ paddingBottom: 12,
+ },
+ infoRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginBottom: 6,
+ },
+ infoText: {
+ marginLeft: 8,
+ fontSize: 14,
+ flex: 1,
+ },
+ descriptionText: {
+ fontSize: 14,
+ marginBottom: 8,
+ lineHeight: 20,
+ },
+ cardFooter: {
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ backgroundColor: '#F8F9FA',
+ },
+ dateText: {
+ fontSize: 12,
+ },
+});
+
diff --git a/src/modules/crm/navigation/CrmNavigator.tsx b/src/modules/crm/navigation/CrmNavigator.tsx
index a413358..04c5959 100644
--- a/src/modules/crm/navigation/CrmNavigator.tsx
+++ b/src/modules/crm/navigation/CrmNavigator.tsx
@@ -1,12 +1,14 @@
import React from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import CrmDashboardScreen from '@/modules/crm/screens/CrmDashboardScreen';
+import ZohoCrmDataScreen from '@/modules/crm/screens/ZohoCrmDataScreen';
const Stack = createStackNavigator();
const CrmNavigator = () => (
+
);
diff --git a/src/modules/crm/screens/CrmDashboardScreen.tsx b/src/modules/crm/screens/CrmDashboardScreen.tsx
index a2ad302..c8c6924 100644
--- a/src/modules/crm/screens/CrmDashboardScreen.tsx
+++ b/src/modules/crm/screens/CrmDashboardScreen.tsx
@@ -1,87 +1,238 @@
-import React, { useMemo } from 'react';
-import { View, Text, StyleSheet } from 'react-native';
+import React, { useEffect } from 'react';
+import { View, Text, StyleSheet, TouchableOpacity, RefreshControl, ScrollView } from 'react-native';
+import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
+import { useSelector, useDispatch } from 'react-redux';
import { Container } from '@/shared/components/ui';
+import { PieChart, DonutChart, FunnelChart, StackedBarChart, PipelineFlow, PipelineCards, CompactPipeline } from '@/shared/components/charts';
import { useTheme } from '@/shared/styles/useTheme';
+import { useNavigation } from '@react-navigation/native';
+import { fetchAllCrmData } from '../store/crmSlice';
+import {
+ selectDashboardData,
+ selectIsAnyLoading,
+ selectHasAnyError,
+ selectCrmStats
+} from '../store/selectors';
+import type { RootState } from '@/store/store';
+import type { AppDispatch } from '@/store/store';
const CrmDashboardScreen: React.FC = () => {
const { colors, fonts } = useTheme();
+ const navigation = useNavigation();
+ const dispatch = useDispatch();
- const mock = useMemo(() => {
- const leads = 420;
- const opportunities = 76;
- const wonDeals = 28;
- const conversionPct = 37;
- const leadsTrend = [60, 62, 68, 70, 76, 84];
- const pipeline = [
- { label: 'Prospecting', value: 28, color: '#3AA0FF' },
- { label: 'Qualified', value: 18, color: '#10B981' },
- { label: 'Proposal', value: 12, color: '#F59E0B' },
- { label: 'Negotiation', value: 9, color: '#6366F1' },
- { label: 'Closed Won', value: 6, color: '#22C55E' },
- { label: 'Closed Lost', value: 7, color: '#EF4444' },
- ];
- const topOpps = [
- { name: 'Acme Upgrade', value: 48000 },
- { name: 'Globex Renewal', value: 36000 },
- { name: 'Initech Expansion', value: 29000 },
- ];
- const recent = [
- { who: 'Jane D.', what: 'Follow-up call completed', when: '2h' },
- { who: 'Sam R.', what: 'Demo scheduled', when: '5h' },
- { who: 'Priya K.', what: 'Proposal sent', when: '1d' },
- ];
- const sourceDist = [
- { label: 'Website', value: 180, color: '#3AA0FF' },
- { label: 'Referral', value: 120, color: '#10B981' },
- { label: 'Events', value: 64, color: '#F59E0B' },
- { label: 'Ads', value: 56, color: '#EF4444' },
- ];
- return { leads, opportunities, wonDeals, conversionPct, leadsTrend, pipeline, topOpps, recent, sourceDist };
- }, []);
+ // Redux selectors
+ const dashboardData = useSelector(selectDashboardData);
+ const crmStats = useSelector(selectCrmStats);
+ const isLoading = useSelector(selectIsAnyLoading);
+ const hasError = useSelector(selectHasAnyError);
+
+ // Fetch data on component mount
+ useEffect(() => {
+ dispatch(fetchAllCrmData());
+ }, [dispatch]);
+
+ // Handle refresh
+ const handleRefresh = () => {
+ dispatch(fetchAllCrmData());
+ };
return (
-
- CRM & Sales
+
+ }
+ >
+
+ CRM & Sales
+ navigation.navigate('ZohoCrmData' as never)}
+ >
+
+
+ View Data
+
+
+
+
+ {/* Error State */}
+ {hasError && (
+
+
+
+ Failed to load CRM data. Pull to refresh.
+
+
+ )}
{/* KPIs */}
-
-
-
-
+
+
+
+
- {/* Leads Trend */}
-
- Leads Trend
-
+ {/* Additional Stats Row */}
+
+
+
+
+
- {/* Pipeline distribution */}
+ {/* Lead Status Distribution - Pie Chart */}
- Pipeline Stages
- a + b.value, 0)} />
-
- {mock.pipeline.map(s => (
-
-
- {s.label}
-
- ))}
+ Lead Status Distribution
+
+
+ ({
+ label: status,
+ value: count,
+ color: getStatusColor(status)
+ }))}
+ colors={colors}
+ fonts={fonts}
+ size={140}
+ />
+
+ {/* Legend */}
+
+ {Object.entries(crmStats.leads.byStatus).map(([status, count]) => (
+
+
+
+ {status} ({count})
+
+
+ ))}
+
- {/* Lead Sources */}
+ {/* Deal Pipeline Stages - Compact View */}
+
+ Deal Pipeline Stages
+
+ ({
+ label: stage.label,
+ value: stage.value,
+ color: stage.color
+ }))}
+ colors={colors}
+ fonts={fonts}
+ />
+
+
+ {/* Leads by Source - Donut Chart */}
Leads by Source
- a + b.value, 0)} />
-
- {mock.sourceDist.map(s => (
-
-
- {s.label}
-
- ))}
+
+
+ ({
+ label: source.label,
+ value: source.value,
+ color: source.color
+ }))}
+ colors={colors}
+ fonts={fonts}
+ size={140}
+ />
+
+ {/* Legend */}
+
+ {dashboardData.sourceDist.map(source => (
+
+
+
+ {source.label} ({source.value})
+
+
+ ))}
+
+
+
+
+ {/* Tasks by Priority - Stacked Bar Chart */}
+
+ Tasks by Priority
+
+
+ ({
+ label: priority,
+ value: count,
+ color: getPriorityColor(priority)
+ }))}
+ colors={colors}
+ fonts={fonts}
+ height={120}
+ />
+
+ {/* Legend */}
+
+ {Object.entries(crmStats.tasks.byPriority).map(([priority, count]) => (
+
+
+
+ {priority} ({count})
+
+
+ ))}
+
@@ -89,31 +240,77 @@ const CrmDashboardScreen: React.FC = () => {
Top Opportunities
- {mock.topOpps.map(o => (
+ {dashboardData.topOpps.length > 0 ? dashboardData.topOpps.map(o => (
- {o.name}
- ${o.value.toLocaleString()}
+
+ {o.name}
+
+
+ {formatCurrency(o.value)}
+
- ))}
+ )) : (
+
+ No opportunities found
+
+ )}
Recent Activity
- {mock.recent.map(r => (
+ {dashboardData.recent.length > 0 ? dashboardData.recent.map(r => (
- {r.who}
- {r.what} · {r.when}
+
+ {r.who}
+
+
+ {r.what} · {r.when}
+
- ))}
+ )) : (
+
+ No recent activity
+
+ )}
-
+
);
};
const styles = StyleSheet.create({
wrap: { flex: 1, padding: 16 },
- title: { fontSize: 18, marginBottom: 8 },
+ header: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: 8,
+ },
+ title: { fontSize: 18 },
+ dataButton: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: 16,
+ paddingVertical: 8,
+ borderRadius: 20,
+ },
+ dataButtonText: {
+ marginLeft: 6,
+ fontSize: 14,
+ },
+ errorCard: {
+ borderRadius: 12,
+ borderWidth: 1,
+ padding: 12,
+ marginTop: 12,
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ errorText: {
+ marginLeft: 8,
+ fontSize: 14,
+ flex: 1,
+ },
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 },
@@ -125,13 +322,156 @@ const styles = StyleSheet.create({
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 },
+ chartContainer: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ paddingVertical: 10,
+ },
+ pieLegend: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ justifyContent: 'center',
+ marginTop: 12,
+ gap: 8,
+ },
+ barLegend: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ justifyContent: 'center',
+ marginTop: 8,
+ gap: 6,
+ },
+ legendText: {
+ fontSize: 12,
+ },
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 },
+ emptyText: {
+ fontSize: 14,
+ textAlign: 'center',
+ paddingVertical: 20,
+ fontStyle: 'italic'
+ },
});
+// Helper functions for color coding
+const getStatusColor = (status: string): string => {
+ // Define a comprehensive color palette with distinct colors
+ const colorPalette = [
+ '#3B82F6', // Bright Blue
+ '#8B5CF6', // Purple
+ '#06B6D4', // Cyan
+ '#F59E0B', // Amber
+ '#10B981', // Emerald
+ '#F97316', // Orange
+ '#22C55E', // Green
+ '#84CC16', // Lime
+ '#14B8A6', // Teal
+ '#059669', // Dark Green
+ '#EF4444', // Red
+ '#DC2626', // Dark Red
+ '#991B1B', // Darker Red
+ '#9CA3AF', // Gray
+ '#EC4899', // Pink
+ '#8B5A2B', // Brown
+ '#B91C1C', // Lost Red
+ '#16A34A', // Success Green
+ '#6366F1', // Indigo
+ '#7C3AED', // Violet
+ '#0891B2', // Sky Blue
+ '#CA8A04', // Gold
+ '#1F2937', // Dark Gray
+ '#BE185D', // Rose
+ '#0D9488', // Emerald Dark
+ '#7C2D12', // Brown Dark
+ '#1E40AF', // Blue Dark
+ '#C2410C', // Orange Dark
+ '#9333EA', // Purple Dark
+ '#059669', // Green Dark
+ ];
+
+ // Create a consistent hash from the status string
+ let hash = 0;
+ const normalizedStatus = status.toLowerCase().trim();
+ for (let i = 0; i < normalizedStatus.length; i++) {
+ const char = normalizedStatus.charCodeAt(i);
+ hash = ((hash << 5) - hash) + char;
+ hash = hash & hash; // Convert to 32-bit integer
+ }
+
+ // Ensure positive index and get color from palette
+ const colorIndex = Math.abs(hash) % colorPalette.length;
+ return colorPalette[colorIndex];
+};
+
+const getPriorityColor = (priority: string): string => {
+ // Define a comprehensive color palette with distinct colors for priorities
+ const colorPalette = [
+ '#DC2626', // Dark Red
+ '#B91C1C', // Urgent Red
+ '#991B1B', // Critical Dark Red
+ '#F59E0B', // Amber
+ '#10B981', // Emerald
+ '#059669', // Dark Green
+ '#16A34A', // Success Green
+ '#9CA3AF', // Gray
+ '#6B7280', // Light Gray
+ '#6366F1', // Indigo
+ '#EF4444', // Red
+ '#8B5CF6', // Purple
+ '#06B6D4', // Cyan
+ '#F97316', // Orange
+ '#22C55E', // Green
+ '#84CC16', // Lime
+ '#14B8A6', // Teal
+ '#EC4899', // Pink
+ '#8B5A2B', // Brown
+ '#7C3AED', // Violet
+ '#0891B2', // Sky Blue
+ '#CA8A04', // Gold
+ '#1F2937', // Dark Gray
+ '#BE185D', // Rose
+ '#0D9488', // Emerald Dark
+ '#7C2D12', // Brown Dark
+ '#1E40AF', // Blue Dark
+ '#C2410C', // Orange Dark
+ '#9333EA', // Purple Dark
+ '#3B82F6', // Info Blue
+ '#0077B5', // LinkedIn Blue
+ ];
+
+ // Create a consistent hash from the priority string
+ let hash = 0;
+ const normalizedPriority = priority.toLowerCase().trim();
+ for (let i = 0; i < normalizedPriority.length; i++) {
+ const char = normalizedPriority.charCodeAt(i);
+ hash = ((hash << 5) - hash) + char;
+ hash = hash & hash; // Convert to 32-bit integer
+ }
+
+ // Ensure positive index and get color from palette
+ const colorIndex = Math.abs(hash) % colorPalette.length;
+ return colorPalette[colorIndex];
+};
+
+// Helper function to format currency values safely
+const formatCurrency = (value: number | undefined | null): string => {
+ if (value === undefined || value === null || isNaN(value)) {
+ return '$0';
+ }
+
+ if (value >= 1000000) {
+ return `$${(value / 1000000).toFixed(1)}M`;
+ } else if (value >= 1000) {
+ return `$${Math.round(value / 1000)}K`;
+ } else {
+ return `$${Math.round(value)}`;
+ }
+};
+
export default CrmDashboardScreen;
// UI helpers
diff --git a/src/modules/crm/screens/ZohoCrmDataScreen.tsx b/src/modules/crm/screens/ZohoCrmDataScreen.tsx
new file mode 100644
index 0000000..1c907f8
--- /dev/null
+++ b/src/modules/crm/screens/ZohoCrmDataScreen.tsx
@@ -0,0 +1,316 @@
+import React, { useState, useEffect, useMemo } from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ ScrollView,
+ TouchableOpacity,
+ RefreshControl,
+ FlatList,
+ Alert,
+} from 'react-native';
+import { useSelector, useDispatch } from 'react-redux';
+import type { AppDispatch } from '@/store/store';
+import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
+import { Container, LoadingSpinner, ErrorState } from '@/shared/components/ui';
+import { useTheme } from '@/shared/styles/useTheme';
+import { showError, showSuccess, showInfo } from '@/shared/utils/Toast';
+import type { CrmData, CrmLead, CrmTask, CrmContact, CrmDeal } from '../types/CrmTypes';
+import { LeadCard, TaskCard, ContactCard, DealCard } from '../components/CrmDataCards';
+import {
+ selectLeads,
+ selectTasks,
+ selectContacts,
+ selectDeals,
+ selectCrmLoading,
+ selectCrmErrors
+} from '../store/selectors';
+import { fetchAllCrmData } from '../store/crmSlice';
+import type { RootState } from '@/store/store';
+
+const ZohoCrmDataScreen: React.FC = () => {
+ const { colors, fonts, spacing, shadows } = useTheme();
+ const dispatch = useDispatch();
+ const [selectedTab, setSelectedTab] = useState<'leads' | 'tasks' | 'contacts' | 'deals'>('leads');
+ const [refreshing, setRefreshing] = useState(false);
+
+ // Redux selectors
+ const leads = useSelector(selectLeads);
+ const tasks = useSelector(selectTasks);
+ const contacts = useSelector(selectContacts);
+ const deals = useSelector(selectDeals);
+ const loading = useSelector(selectCrmLoading);
+ const errors = useSelector(selectCrmErrors);
+
+ // Create CRM data object from Redux state
+ const crmData: CrmData = useMemo(() => ({
+ leads: leads || [],
+ tasks: tasks || [],
+ contacts: contacts || [],
+ deals: deals || [],
+ }), [leads, tasks, contacts, deals]);
+
+ // Fetch CRM data using Redux
+ const fetchCrmData = async (showRefresh = false) => {
+ try {
+ if (showRefresh) {
+ setRefreshing(true);
+ }
+
+ // Dispatch Redux action to fetch all CRM data
+ await dispatch(fetchAllCrmData()).unwrap();
+
+ if (showRefresh) {
+ showSuccess('CRM data refreshed successfully');
+ }
+ } catch (err) {
+ const errorMessage = 'Failed to fetch CRM data';
+ showError(errorMessage);
+ } finally {
+ setRefreshing(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchCrmData();
+ }, []);
+
+ const handleRefresh = () => {
+ fetchCrmData(true);
+ };
+
+ const handleRetry = () => {
+ fetchCrmData();
+ };
+
+ const handleCardPress = (item: any, type: string) => {
+ showInfo(`Viewing ${type}: ${item.name || item.subject || `${item.firstName} ${item.lastName}`}`);
+ };
+
+ // Get current loading state and error
+ const isLoading = loading.leads || loading.tasks || loading.contacts || loading.deals;
+ const hasError = errors.leads || errors.tasks || errors.contacts || errors.deals;
+
+
+ // Tab configuration
+ const tabs = [
+ { key: 'leads', label: 'Leads', icon: 'account-heart', count: crmData.leads.length },
+ { key: 'tasks', label: 'Tasks', icon: 'check-circle', count: crmData.tasks.length },
+ { key: 'contacts', label: 'Contacts', icon: 'account-group', count: crmData.contacts.length },
+ { key: 'deals', label: 'Deals', icon: 'handshake', count: crmData.deals.length },
+ ] as const;
+
+ if (isLoading && !crmData.leads.length) {
+ return ;
+ }
+
+ if (hasError && !crmData.leads.length) {
+ return ;
+ }
+
+
+ const renderTabContent = () => {
+ switch (selectedTab) {
+ case 'leads':
+ return (
+ (
+ handleCardPress(item, 'Lead')}
+ />
+ )}
+ keyExtractor={(item) => item.id}
+ numColumns={1}
+ showsVerticalScrollIndicator={false}
+ contentContainerStyle={styles.listContainer}
+ />
+ );
+ case 'tasks':
+ return (
+ (
+ handleCardPress(item, 'Task')}
+ />
+ )}
+ keyExtractor={(item) => item.id}
+ numColumns={1}
+ showsVerticalScrollIndicator={false}
+ contentContainerStyle={styles.listContainer}
+ />
+ );
+ case 'contacts':
+ return (
+ (
+ handleCardPress(item, 'Contact')}
+ />
+ )}
+ keyExtractor={(item) => item.id}
+ numColumns={1}
+ showsVerticalScrollIndicator={false}
+ contentContainerStyle={styles.listContainer}
+ />
+ );
+ case 'deals':
+ return (
+ (
+ handleCardPress(item, 'Deal')}
+ />
+ )}
+ keyExtractor={(item) => item.id}
+ numColumns={1}
+ showsVerticalScrollIndicator={false}
+ contentContainerStyle={styles.listContainer}
+ />
+ );
+ default:
+ return null;
+ }
+ };
+
+ return (
+
+
+ }
+ >
+ {/* Header */}
+
+
+ Zoho CRM Data
+
+
+
+
+
+
+ {/* Tabs */}
+
+
+ {tabs.map((tab) => (
+ setSelectedTab(tab.key)}
+ activeOpacity={0.8}
+ >
+
+
+ {tab.label}
+
+
+
+ {tab.count}
+
+
+
+ ))}
+
+
+
+ {/* Content */}
+
+ {renderTabContent()}
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ header: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ },
+ title: {
+ fontSize: 24,
+ },
+ tabsContainer: {
+ marginBottom: 16,
+ },
+ tabs: {
+ flexDirection: 'row',
+ paddingHorizontal: 16,
+ },
+ tab: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: 16,
+ paddingVertical: 10,
+ marginRight: 8,
+ borderRadius: 20,
+ backgroundColor: '#F1F5F9',
+ },
+ tabText: {
+ marginLeft: 6,
+ fontSize: 14,
+ },
+ countBadge: {
+ marginLeft: 6,
+ paddingHorizontal: 8,
+ paddingVertical: 2,
+ borderRadius: 10,
+ minWidth: 20,
+ alignItems: 'center',
+ },
+ countText: {
+ fontSize: 12,
+ },
+ content: {
+ flex: 1,
+ paddingHorizontal: 16,
+ },
+ listContainer: {
+ paddingBottom: 20,
+ },
+});
+
+export default ZohoCrmDataScreen;
diff --git a/src/modules/crm/services/crmAPI.ts b/src/modules/crm/services/crmAPI.ts
new file mode 100644
index 0000000..dc76e4d
--- /dev/null
+++ b/src/modules/crm/services/crmAPI.ts
@@ -0,0 +1,49 @@
+import http from '@/services/http';
+import type {
+ CrmLead,
+ CrmTask,
+ CrmContact,
+ CrmDeal,
+ CrmSearchParams,
+ CrmApiResponse,
+ CrmPaginatedResponse
+} from '../types/CrmTypes';
+
+// Available CRM resource types
+export type CrmResourceType = 'leads' | 'tasks' | 'deals' | 'contacts';
+
+// Base API endpoint
+const CRM_BASE_URL = '/api/v1/integrations/data';
+
+export const crmAPI = {
+ // Generic method to get CRM data by resource type
+ getCrmData: (
+ resource: CrmResourceType,
+ params?: CrmSearchParams
+ ) => {
+ const queryParams = {
+ provider: 'zoho',
+ service: 'crm',
+ resource,
+ page: 1,
+ limit: 20,
+ ...params
+ };
+
+ return http.get>>(CRM_BASE_URL, queryParams);
+ },
+
+ // Specific resource methods for type safety
+ getLeads: (params?: CrmSearchParams) =>
+ crmAPI.getCrmData('leads', params),
+
+ getTasks: (params?: CrmSearchParams) =>
+ crmAPI.getCrmData('tasks', params),
+
+ getContacts: (params?: CrmSearchParams) =>
+ crmAPI.getCrmData('contacts', params),
+
+ getDeals: (params?: CrmSearchParams) =>
+ crmAPI.getCrmData('deals', params),
+};
+
diff --git a/src/modules/crm/services/index.ts b/src/modules/crm/services/index.ts
new file mode 100644
index 0000000..ca9507a
--- /dev/null
+++ b/src/modules/crm/services/index.ts
@@ -0,0 +1,21 @@
+// CRM Services Exports
+export { crmAPI } from './crmAPI';
+
+// Re-export types for convenience
+export type {
+ CrmData,
+ CrmLead,
+ CrmTask,
+ CrmContact,
+ CrmDeal,
+ CreateLeadForm,
+ CreateTaskForm,
+ CreateContactForm,
+ CreateDealForm,
+ CrmSearchParams,
+ CrmApiResponse,
+ CrmPaginatedResponse,
+ CrmStats,
+ CrmFilters,
+} from '../types/CrmTypes';
+
diff --git a/src/modules/crm/store/crmSlice.ts b/src/modules/crm/store/crmSlice.ts
new file mode 100644
index 0000000..94e68da
--- /dev/null
+++ b/src/modules/crm/store/crmSlice.ts
@@ -0,0 +1,309 @@
+import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
+import { crmAPI } from '../services/crmAPI';
+import type {
+ CrmLead,
+ CrmTask,
+ CrmContact,
+ CrmDeal,
+ CrmStats,
+ CrmSearchParams,
+ CrmApiResponse,
+ CrmPaginatedResponse
+} from '../types/CrmTypes';
+
+// State interface
+export interface CrmState {
+ // Data
+ leads: CrmLead[];
+ tasks: CrmTask[];
+ contacts: CrmContact[];
+ deals: CrmDeal[];
+
+ // Loading states
+ loading: {
+ leads: boolean;
+ tasks: boolean;
+ contacts: boolean;
+ deals: boolean;
+ stats: boolean;
+ };
+
+ // Error states
+ errors: {
+ leads: string | null;
+ tasks: string | null;
+ contacts: string | null;
+ deals: string | null;
+ stats: string | null;
+ };
+
+ // Pagination
+ pagination: {
+ leads: { page: number; count: number; moreRecords: boolean };
+ tasks: { page: number; count: number; moreRecords: boolean };
+ contacts: { page: number; count: number; moreRecords: boolean };
+ deals: { page: number; count: number; moreRecords: boolean };
+ };
+
+ // Statistics
+ stats: CrmStats | null;
+
+ // Last updated timestamps
+ lastUpdated: {
+ leads: string | null;
+ tasks: string | null;
+ contacts: string | null;
+ deals: string | null;
+ stats: string | null;
+ };
+}
+
+// Initial state
+const initialState: CrmState = {
+ leads: [],
+ tasks: [],
+ contacts: [],
+ deals: [],
+ loading: {
+ leads: false,
+ tasks: false,
+ contacts: false,
+ deals: false,
+ stats: false,
+ },
+ errors: {
+ leads: null,
+ tasks: null,
+ contacts: null,
+ deals: null,
+ stats: null,
+ },
+ pagination: {
+ leads: { page: 1, count: 0, moreRecords: false },
+ tasks: { page: 1, count: 0, moreRecords: false },
+ contacts: { page: 1, count: 0, moreRecords: false },
+ deals: { page: 1, count: 0, moreRecords: false },
+ },
+ stats: null,
+ lastUpdated: {
+ leads: null,
+ tasks: null,
+ contacts: null,
+ deals: null,
+ stats: null,
+ },
+};
+
+// Async thunks
+export const fetchLeads = createAsyncThunk(
+ 'crm/fetchLeads',
+ async (params?: CrmSearchParams) => {
+ const response = await crmAPI.getLeads(params);
+ return response.data?.data || { data: [], info: { count: 0, moreRecords: false, page: 1 } };
+ }
+);
+
+export const fetchTasks = createAsyncThunk(
+ 'crm/fetchTasks',
+ async (params?: CrmSearchParams) => {
+ const response = await crmAPI.getTasks(params);
+ return response.data?.data || { data: [], info: { count: 0, moreRecords: false, page: 1 } };
+ }
+);
+
+export const fetchContacts = createAsyncThunk(
+ 'crm/fetchContacts',
+ async (params?: CrmSearchParams) => {
+ const response = await crmAPI.getContacts(params);
+ return response.data?.data || { data: [], info: { count: 0, moreRecords: false, page: 1 } };
+ }
+);
+
+export const fetchDeals = createAsyncThunk(
+ 'crm/fetchDeals',
+ async (params?: CrmSearchParams) => {
+ const response = await crmAPI.getDeals(params);
+ return response.data?.data || { data: [], info: { count: 0, moreRecords: false, page: 1 } };
+ }
+);
+
+// Fetch all CRM data
+export const fetchAllCrmData = createAsyncThunk(
+ 'crm/fetchAllData',
+ async (params?: CrmSearchParams) => {
+ const [leadsResponse, tasksResponse, contactsResponse, dealsResponse] = await Promise.all([
+ crmAPI.getLeads(params),
+ crmAPI.getTasks(params),
+ crmAPI.getContacts(params),
+ crmAPI.getDeals(params),
+ ]);
+ console.log('leads response data',leadsResponse)
+ return {
+ leads: leadsResponse.data?.data || { data: [], info: { count: 0, moreRecords: false, page: 1 } },
+ tasks: tasksResponse.data?.data || { data: [], info: { count: 0, moreRecords: false, page: 1 } },
+ contacts: contactsResponse.data?.data || { data: [], info: { count: 0, moreRecords: false, page: 1 } },
+ deals: dealsResponse.data?.data || { data: [], info: { count: 0, moreRecords: false, page: 1 } },
+ };
+ }
+);
+
+// Slice
+const crmSlice = createSlice({
+ name: 'crm',
+ initialState,
+ reducers: {
+ clearErrors: (state) => {
+ state.errors = {
+ leads: null,
+ tasks: null,
+ contacts: null,
+ deals: null,
+ stats: null,
+ };
+ },
+ clearData: (state) => {
+ state.leads = [];
+ state.tasks = [];
+ state.contacts = [];
+ state.deals = [];
+ state.stats = null;
+ },
+ setLeadsPage: (state, action: PayloadAction) => {
+ state.pagination.leads.page = action.payload;
+ },
+ setTasksPage: (state, action: PayloadAction) => {
+ state.pagination.tasks.page = action.payload;
+ },
+ setContactsPage: (state, action: PayloadAction) => {
+ state.pagination.contacts.page = action.payload;
+ },
+ setDealsPage: (state, action: PayloadAction) => {
+ state.pagination.deals.page = action.payload;
+ },
+ },
+ extraReducers: (builder) => {
+ // Fetch leads
+ builder
+ .addCase(fetchLeads.pending, (state) => {
+ state.loading.leads = true;
+ state.errors.leads = null;
+ })
+ .addCase(fetchLeads.fulfilled, (state, action) => {
+ state.loading.leads = false;
+ state.leads = action.payload.data || [];
+ state.pagination.leads = action.payload.info || { page: 1, count: 0, moreRecords: false };
+ state.lastUpdated.leads = new Date().toISOString();
+ })
+ .addCase(fetchLeads.rejected, (state, action) => {
+ state.loading.leads = false;
+ state.errors.leads = action.error.message || 'Failed to fetch leads';
+ })
+
+ // Fetch tasks
+ .addCase(fetchTasks.pending, (state) => {
+ state.loading.tasks = true;
+ state.errors.tasks = null;
+ })
+ .addCase(fetchTasks.fulfilled, (state, action) => {
+ state.loading.tasks = false;
+ state.tasks = action.payload.data || [];
+ state.pagination.tasks = action.payload.info || { page: 1, count: 0, moreRecords: false };
+ state.lastUpdated.tasks = new Date().toISOString();
+ })
+ .addCase(fetchTasks.rejected, (state, action) => {
+ state.loading.tasks = false;
+ state.errors.tasks = action.error.message || 'Failed to fetch tasks';
+ })
+
+ // Fetch contacts
+ .addCase(fetchContacts.pending, (state) => {
+ state.loading.contacts = true;
+ state.errors.contacts = null;
+ })
+ .addCase(fetchContacts.fulfilled, (state, action) => {
+ state.loading.contacts = false;
+ state.contacts = action.payload.data || [];
+ state.pagination.contacts = action.payload.info || { page: 1, count: 0, moreRecords: false };
+ state.lastUpdated.contacts = new Date().toISOString();
+ })
+ .addCase(fetchContacts.rejected, (state, action) => {
+ state.loading.contacts = false;
+ state.errors.contacts = action.error.message || 'Failed to fetch contacts';
+ })
+
+ // Fetch deals
+ .addCase(fetchDeals.pending, (state) => {
+ state.loading.deals = true;
+ state.errors.deals = null;
+ })
+ .addCase(fetchDeals.fulfilled, (state, action) => {
+ state.loading.deals = false;
+ state.deals = action.payload.data || [];
+ state.pagination.deals = action.payload.info || { page: 1, count: 0, moreRecords: false };
+ state.lastUpdated.deals = new Date().toISOString();
+ })
+ .addCase(fetchDeals.rejected, (state, action) => {
+ state.loading.deals = false;
+ state.errors.deals = action.error.message || 'Failed to fetch deals';
+ })
+
+ // Fetch all CRM data
+ .addCase(fetchAllCrmData.pending, (state) => {
+ state.loading.leads = true;
+ state.loading.tasks = true;
+ state.loading.contacts = true;
+ state.loading.deals = true;
+ state.errors.leads = null;
+ state.errors.tasks = null;
+ state.errors.contacts = null;
+ state.errors.deals = null;
+ })
+ .addCase(fetchAllCrmData.fulfilled, (state, action) => {
+ const { leads, tasks, contacts, deals } = action.payload;
+
+ state.loading.leads = false;
+ state.loading.tasks = false;
+ state.loading.contacts = false;
+ state.loading.deals = false;
+
+ state.leads = leads.data || [];
+ state.tasks = tasks.data || [];
+ state.contacts = contacts.data || [];
+ state.deals = deals.data || [];
+
+ state.pagination.leads = leads.info || { page: 1, count: 0, moreRecords: false };
+ state.pagination.tasks = tasks.info || { page: 1, count: 0, moreRecords: false };
+ state.pagination.contacts = contacts.info || { page: 1, count: 0, moreRecords: false };
+ state.pagination.deals = deals.info || { page: 1, count: 0, moreRecords: false };
+
+ const now = new Date().toISOString();
+ state.lastUpdated.leads = now;
+ state.lastUpdated.tasks = now;
+ state.lastUpdated.contacts = now;
+ state.lastUpdated.deals = now;
+ })
+ .addCase(fetchAllCrmData.rejected, (state, action) => {
+ state.loading.leads = false;
+ state.loading.tasks = false;
+ state.loading.contacts = false;
+ state.loading.deals = false;
+
+ const errorMessage = action.error.message || 'Failed to fetch CRM data';
+ state.errors.leads = errorMessage;
+ state.errors.tasks = errorMessage;
+ state.errors.contacts = errorMessage;
+ state.errors.deals = errorMessage;
+ });
+ },
+});
+
+export const {
+ clearErrors,
+ clearData,
+ setLeadsPage,
+ setTasksPage,
+ setContactsPage,
+ setDealsPage,
+} = crmSlice.actions;
+
+export default crmSlice.reducer;
diff --git a/src/modules/crm/store/selectors.ts b/src/modules/crm/store/selectors.ts
new file mode 100644
index 0000000..5e85c38
--- /dev/null
+++ b/src/modules/crm/store/selectors.ts
@@ -0,0 +1,289 @@
+import { createSelector } from '@reduxjs/toolkit';
+import type { RootState } from '@/store/store';
+import type { CrmStats, LeadSource, DealStage, TaskStatus } from '../types/CrmTypes';
+
+// Base selectors
+export const selectCrmState = (state: RootState) => state.crm;
+
+export const selectLeads = (state: RootState) => state.crm.leads;
+export const selectTasks = (state: RootState) => state.crm.tasks;
+export const selectContacts = (state: RootState) => state.crm.contacts;
+export const selectDeals = (state: RootState) => state.crm.deals;
+
+export const selectCrmLoading = (state: RootState) => state.crm.loading;
+export const selectCrmErrors = (state: RootState) => state.crm.errors;
+export const selectCrmPagination = (state: RootState) => state.crm.pagination;
+
+// Loading selectors
+export const selectLeadsLoading = (state: RootState) => state.crm.loading.leads;
+export const selectTasksLoading = (state: RootState) => state.crm.loading.tasks;
+export const selectContactsLoading = (state: RootState) => state.crm.loading.contacts;
+export const selectDealsLoading = (state: RootState) => state.crm.loading.deals;
+
+// Error selectors
+export const selectLeadsError = (state: RootState) => state.crm.errors.leads;
+export const selectTasksError = (state: RootState) => state.crm.errors.tasks;
+export const selectContactsError = (state: RootState) => state.crm.errors.contacts;
+export const selectDealsError = (state: RootState) => state.crm.errors.deals;
+
+// Computed selectors for dashboard
+export const selectCrmStats = createSelector(
+ [selectLeads, selectTasks, selectContacts, selectDeals],
+ (leads, tasks, contacts, deals): CrmStats => {
+ // Ensure arrays are defined and are actually arrays, fallback to empty arrays
+ const safeLeads = Array.isArray(leads) ? leads : [];
+ const safeTasks = Array.isArray(tasks) ? tasks : [];
+ const safeContacts = Array.isArray(contacts) ? contacts : [];
+ const safeDeals = Array.isArray(deals) ? deals : [];
+
+ // Calculate leads stats with safe property access and better fallbacks
+ const leadsByStatus = safeLeads.reduce((acc, lead) => {
+ const status = lead?.Lead_Status && lead.Lead_Status.trim() !== '' ? lead.Lead_Status : 'Not Set';
+ acc[status] = (acc[status] || 0) + 1;
+ return acc;
+ }, {} as Record);
+
+ const leadsBySource = safeLeads.reduce((acc, lead) => {
+ const source = lead?.Lead_Source && lead.Lead_Source.trim() !== '' ? lead.Lead_Source : 'Not Set';
+ acc[source] = (acc[source] || 0) + 1;
+ return acc;
+ }, {} as Record);
+
+ // Calculate tasks stats with safe property access and better fallbacks
+ const tasksByStatus = safeTasks.reduce((acc, task) => {
+ const status = task?.Status && task.Status.trim() !== '' ? task.Status : 'Not Set';
+ acc[status] = (acc[status] || 0) + 1;
+ return acc;
+ }, {} as Record);
+
+ const tasksByPriority = safeTasks.reduce((acc, task) => {
+ const priority = task?.Priority && task.Priority.trim() !== '' ? task.Priority : 'Not Set';
+ acc[priority] = (acc[priority] || 0) + 1;
+ return acc;
+ }, {} as Record);
+
+ // Calculate contacts stats with safe property access
+ const contactsByCompany = safeContacts.reduce((acc, contact) => {
+ const company = contact?.Account_Name?.name || 'Unknown';
+ acc[company] = (acc[company] || 0) + 1;
+ return acc;
+ }, {} as Record);
+
+ // Calculate deals stats with safe property access and better fallbacks
+ const dealsByStage = safeDeals.reduce((acc, deal) => {
+ const stage = deal?.Stage && deal.Stage.trim() !== '' ? deal.Stage : 'Not Set';
+ acc[stage] = (acc[stage] || 0) + 1;
+ return acc;
+ }, {} as Record);
+
+ const totalDealValue = safeDeals.reduce((sum, deal) => {
+ const amount = deal?.Amount || 0;
+ return sum + (typeof amount === 'number' && !isNaN(amount) ? amount : 0);
+ }, 0);
+
+ const wonDeals = safeDeals.filter(deal => deal?.Stage === 'Closed Won');
+ const lostDeals = safeDeals.filter(deal => deal?.Stage === 'Closed Lost');
+
+ return {
+ leads: {
+ total: safeLeads.length,
+ new: leadsByStatus['New'] || 0,
+ qualified: leadsByStatus['Qualified'] || 0,
+ converted: leadsByStatus['Converted'] || 0,
+ bySource: leadsBySource as Record,
+ byStatus: leadsByStatus as Record,
+ },
+ tasks: {
+ total: safeTasks.length,
+ completed: tasksByStatus['Completed'] || 0,
+ overdue: safeTasks.filter(task =>
+ new Date(task.Due_Date) < new Date() && task.Status !== 'Completed'
+ ).length,
+ byStatus: tasksByStatus as Record,
+ byPriority: tasksByPriority as Record,
+ },
+ contacts: {
+ total: safeContacts.length,
+ active: safeContacts.length, // All contacts are considered active for now
+ inactive: 0, // No inactive contacts for now
+ byCompany: contactsByCompany,
+ },
+ deals: {
+ total: safeDeals.length,
+ won: wonDeals.length,
+ lost: lostDeals.length,
+ totalValue: totalDealValue,
+ averageDealSize: safeDeals.length > 0 ? totalDealValue / safeDeals.length : 0,
+ byStage: dealsByStage as Record,
+ pipelineValue: safeDeals
+ .filter(deal => !['Closed Won', 'Closed Lost'].includes(deal.Stage))
+ .reduce((sum, deal) => sum + (deal.Amount || 0), 0),
+ },
+ };
+ }
+);
+
+// Dashboard specific selectors
+export const selectDashboardData = createSelector(
+ [selectCrmStats, selectLeads, selectDeals, selectTasks],
+ (stats, leads, deals, tasks) => {
+ // Ensure arrays are defined, fallback to empty arrays
+ const safeLeads = leads || [];
+ const safeDeals = deals || [];
+ const safeTasks = tasks || [];
+
+ // Top opportunities (deals by value) with safe property access
+ const topOpportunities = [...safeDeals]
+ .filter(deal => {
+ const stage = deal?.Stage || '';
+ return !['Closed Won', 'Closed Lost'].includes(stage);
+ })
+ .sort((a, b) => {
+ const amountA = a?.Amount || 0;
+ const amountB = b?.Amount || 0;
+ return (typeof amountB === 'number' && !isNaN(amountB) ? amountB : 0) -
+ (typeof amountA === 'number' && !isNaN(amountA) ? amountA : 0);
+ })
+ .slice(0, 3)
+ .map(deal => ({
+ name: deal?.Deal_Name || 'Unnamed Deal',
+ value: deal?.Amount || 0,
+ }));
+
+ // Recent activity (recent tasks) with safe property access
+ const recentActivity = [...safeTasks]
+ .sort((a, b) => {
+ const dateA = a?.Created_Time ? new Date(a.Created_Time).getTime() : 0;
+ const dateB = b?.Created_Time ? new Date(b.Created_Time).getTime() : 0;
+ return dateB - dateA;
+ })
+ .slice(0, 3)
+ .map(task => ({
+ who: task?.Owner?.name || 'Unknown',
+ what: task?.Subject || 'No subject',
+ when: task?.Created_Time ? getTimeAgo(task.Created_Time) : 'Unknown',
+ }));
+
+ // Pipeline distribution
+ const pipelineStages = [
+ { label: 'Prospecting', value: stats.deals.byStage['Prospecting'] || 0, color: '#3AA0FF' },
+ { label: 'Qualified', value: stats.deals.byStage['Qualification'] || 0, color: '#10B981' },
+ { label: 'Proposal', value: stats.deals.byStage['Proposal'] || 0, color: '#F59E0B' },
+ { label: 'Negotiation', value: stats.deals.byStage['Negotiation'] || 0, color: '#6366F1' },
+ { label: 'Closed Won', value: stats.deals.won, color: '#22C55E' },
+ { label: 'Closed Lost', value: stats.deals.lost, color: '#EF4444' },
+ ];
+
+ // Lead sources distribution
+ const leadSources = Object.entries(stats.leads.bySource)
+ .map(([source, count]) => ({
+ label: source,
+ value: count,
+ color: getSourceColor(source),
+ }))
+ .sort((a, b) => b.value - a.value);
+
+ // Leads trend (mock data for now - could be calculated from historical data)
+ const leadsTrend = [60, 62, 68, 70, 76, 84]; // This would come from historical data
+
+ return {
+ // KPIs with safe calculations
+ leads: stats.leads.total || 0,
+ opportunities: stats.deals.total || 0,
+ wonDeals: stats.deals.won || 0,
+ conversionPct: stats.leads.total > 0 ? Math.round(((stats.leads.converted || 0) / stats.leads.total) * 100) : 0,
+
+ // Charts and visualizations
+ leadsTrend,
+ pipeline: pipelineStages,
+ sourceDist: leadSources,
+ topOpps: topOpportunities,
+ recent: recentActivity,
+ };
+ }
+);
+
+// Helper functions
+function getTimeAgo(dateString: string): string {
+ const now = new Date();
+ const date = new Date(dateString);
+ const diffInHours = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60));
+
+ if (diffInHours < 1) return 'now';
+ if (diffInHours < 24) return `${diffInHours}h`;
+ const diffInDays = Math.floor(diffInHours / 24);
+ return `${diffInDays}d`;
+}
+
+function getSourceColor(source: string): string {
+ // Define a comprehensive color palette with distinct colors for sources
+ const colorPalette = [
+ '#3B82F6', // Bright Blue
+ '#8B5CF6', // Purple
+ '#06B6D4', // Cyan
+ '#F59E0B', // Amber
+ '#10B981', // Emerald
+ '#F97316', // Orange
+ '#22C55E', // Green
+ '#84CC16', // Lime
+ '#14B8A6', // Teal
+ '#059669', // Dark Green
+ '#EF4444', // Red
+ '#DC2626', // Dark Red
+ '#991B1B', // Darker Red
+ '#9CA3AF', // Gray
+ '#EC4899', // Pink
+ '#8B5A2B', // Brown
+ '#B91C1C', // Lost Red
+ '#16A34A', // Success Green
+ '#6366F1', // Indigo
+ '#7C3AED', // Violet
+ '#0891B2', // Sky Blue
+ '#CA8A04', // Gold
+ '#1F2937', // Dark Gray
+ '#BE185D', // Rose
+ '#0D9488', // Emerald Dark
+ '#7C2D12', // Brown Dark
+ '#1E40AF', // Blue Dark
+ '#C2410C', // Orange Dark
+ '#9333EA', // Purple Dark
+ '#059669', // Green Dark
+ '#DC2626', // Hot Red
+ '#F97316', // Warm Orange
+ '#6B7280', // Cold Gray
+ '#EF4444', // Error Red
+ '#10B981', // Success Green
+ '#F59E0B', // Warning Orange
+ '#3B82F6', // Info Blue
+ '#0077B5', // LinkedIn Blue
+ '#1877F2', // Facebook Blue
+ '#1DA1F2', // Twitter Blue
+ '#E4405F', // Instagram Pink
+ '#FF0000', // YouTube Red
+ ];
+
+ // Create a consistent hash from the source string
+ let hash = 0;
+ const normalizedSource = source.toLowerCase().trim();
+ for (let i = 0; i < normalizedSource.length; i++) {
+ const char = normalizedSource.charCodeAt(i);
+ hash = ((hash << 5) - hash) + char;
+ hash = hash & hash; // Convert to 32-bit integer
+ }
+
+ // Ensure positive index and get color from palette
+ const colorIndex = Math.abs(hash) % colorPalette.length;
+ return colorPalette[colorIndex];
+}
+
+// Loading state selectors
+export const selectIsAnyLoading = createSelector(
+ [selectCrmLoading],
+ (loading) => Object.values(loading).some(Boolean)
+);
+
+export const selectHasAnyError = createSelector(
+ [selectCrmErrors],
+ (errors) => Object.values(errors).some(error => error !== null)
+);
+
diff --git a/src/modules/crm/types/CrmTypes.ts b/src/modules/crm/types/CrmTypes.ts
new file mode 100644
index 0000000..b99bc07
--- /dev/null
+++ b/src/modules/crm/types/CrmTypes.ts
@@ -0,0 +1,321 @@
+// CRM Data Types for Zoho CRM Integration
+
+export interface CrmLead {
+ id: string;
+ Owner: {
+ name: string;
+ id: string;
+ email: string;
+ };
+ Company: string;
+ Email: string;
+ Phone: string;
+ Mobile: string;
+ Lead_Status: LeadStatus;
+ Lead_Source: LeadSource;
+ Created_Time: string;
+ Last_Activity_Time: string;
+ Annual_Revenue?: number;
+ Description?: string;
+ Industry?: string;
+ Street?: string;
+ City?: string;
+ State?: string;
+ Country?: string;
+ Zip_Code?: string;
+ Website?: string;
+ First_Name: string;
+ Last_Name: string;
+ Full_Name: string;
+ Salutation?: string;
+ Designation?: string;
+ Twitter?: string;
+ Skype_ID?: string;
+ Secondary_Email?: string;
+ Fax?: string;
+ No_of_Employees?: number;
+ Tag: string[];
+}
+
+export interface CrmTask {
+ id: string;
+ Owner: {
+ name: string;
+ id: string;
+ email: string;
+ };
+ Subject: string;
+ Description: string;
+ Status: TaskStatus;
+ Priority: TaskPriority;
+ Due_Date: string;
+ Created_Time: string;
+ Closed_Time?: string;
+ Who_Id?: {
+ name: string;
+ id: string;
+ };
+ What_Id?: {
+ name: string;
+ id: string;
+ };
+ Remind_At?: string;
+ Send_Notification_Email: boolean;
+ Recurring_Activity?: string;
+ Tag: string[];
+}
+
+export interface CrmContact {
+ id: string;
+ Owner: {
+ name: string;
+ id: string;
+ email: string;
+ };
+ First_Name: string;
+ Last_Name: string;
+ Full_Name: string;
+ Email: string;
+ Phone: string;
+ Mobile: string;
+ Home_Phone?: string;
+ Other_Phone?: string;
+ Account_Name?: {
+ name: string;
+ id: string;
+ };
+ Title: string;
+ Department?: string;
+ Lead_Source: LeadSource;
+ Last_Activity_Time: string;
+ Created_Time: string;
+ Mailing_Street?: string;
+ Mailing_City?: string;
+ Mailing_State?: string;
+ Mailing_Country?: string;
+ Mailing_Zip?: string;
+ Other_Street?: string;
+ Other_City?: string;
+ Other_State?: string;
+ Other_Country?: string;
+ Other_Zip?: string;
+ Salutation?: string;
+ Assistant?: string;
+ Asst_Phone?: string;
+ Reporting_To?: string;
+ Date_of_Birth?: string;
+ Secondary_Email?: string;
+ Twitter?: string;
+ Skype_ID?: string;
+ Fax?: string;
+ Description?: string;
+ Tag: string[];
+}
+
+export interface CrmDeal {
+ id: string;
+ Owner: {
+ name: string;
+ id: string;
+ email: string;
+ };
+ Deal_Name: string;
+ Amount: number;
+ Stage: DealStage;
+ Probability: number;
+ Closing_Date: string;
+ Contact_Name?: {
+ name: string;
+ id: string;
+ };
+ Account_Name?: {
+ name: string;
+ id: string;
+ };
+ Description?: string;
+ Lead_Source?: LeadSource;
+ Type?: string;
+ Next_Step?: string;
+ Created_Time: string;
+ Modified_Time: string;
+ Expected_Revenue?: number;
+ Overall_Sales_Duration?: number;
+ Sales_Cycle_Duration?: number;
+ Lead_Conversion_Time?: string;
+ Reason_For_Loss__s?: string;
+ Campaign_Source?: string;
+ Tag: string[];
+}
+
+// Enums and Union Types
+export type LeadStatus = 'New' | 'Contacted' | 'Qualified' | 'Unqualified' | 'Converted' | 'Lost Lead' | 'Not Contacted' | 'Junk Lead';
+export type TaskType = 'Call' | 'Email' | 'Meeting' | 'Follow-up' | 'Demo' | 'Proposal' | 'Other';
+export type TaskPriority = 'Highest' | 'High' | 'Medium' | 'Low' | 'Lowest';
+export type TaskStatus = 'Not Started' | 'In Progress' | 'Completed' | 'Cancelled' | 'Deferred' | 'Waiting on someone else';
+export type ContactStatus = 'Active' | 'Inactive' | 'Unsubscribed';
+export type DealStage = 'Prospecting' | 'Qualification' | 'Identify Decision Makers' | 'Proposal' | 'Negotiation' | 'Closed Won' | 'Closed Lost';
+export type LeadSource = 'Website' | 'Referral' | 'Event' | 'Cold Call' | 'Email Campaign' | 'Social Media' | 'Partner' | 'Trade Show' | 'Other';
+
+// Combined CRM Data Interface
+export interface CrmData {
+ leads: CrmLead[];
+ tasks: CrmTask[];
+ contacts: CrmContact[];
+ deals: CrmDeal[];
+}
+
+// API Response Types
+export interface CrmApiResponse {
+ status: 'success' | 'error';
+ message: string;
+ data?: T;
+ error?: string;
+ timestamp: string;
+}
+
+export interface CrmPaginatedResponse {
+ data: T[];
+ info: {
+ count: number;
+ moreRecords: boolean;
+ page: number;
+ };
+}
+
+// Filter and Search Types
+export interface CrmFilters {
+ leads?: {
+ status?: LeadStatus[];
+ source?: LeadSource[];
+ assignedTo?: string[];
+ dateRange?: {
+ start: string;
+ end: string;
+ };
+ };
+ tasks?: {
+ status?: TaskStatus[];
+ priority?: TaskPriority[];
+ type?: TaskType[];
+ assignedTo?: string[];
+ dueDateRange?: {
+ start: string;
+ end: string;
+ };
+ };
+ contacts?: {
+ status?: ContactStatus[];
+ company?: string[];
+ assignedTo?: string[];
+ };
+ deals?: {
+ stage?: DealStage[];
+ owner?: string[];
+ amountRange?: {
+ min: number;
+ max: number;
+ };
+ closeDateRange?: {
+ start: string;
+ end: string;
+ };
+ };
+}
+
+export interface CrmSearchParams {
+ query?: string;
+ type?: 'leads' | 'tasks' | 'contacts' | 'deals' | 'all';
+ filters?: CrmFilters;
+ sortBy?: string;
+ sortOrder?: 'asc' | 'desc';
+ page?: number;
+ limit?: number;
+}
+
+// Statistics and Analytics Types
+export interface CrmStats {
+ leads: {
+ total: number;
+ new: number;
+ qualified: number;
+ converted: number;
+ bySource: Record;
+ byStatus: Record;
+ };
+ tasks: {
+ total: number;
+ completed: number;
+ overdue: number;
+ byStatus: Record;
+ byPriority: Record;
+ };
+ contacts: {
+ total: number;
+ active: number;
+ inactive: number;
+ byCompany: Record;
+ };
+ deals: {
+ total: number;
+ won: number;
+ lost: number;
+ totalValue: number;
+ averageDealSize: number;
+ byStage: Record;
+ pipelineValue: number;
+ };
+}
+
+// Form Types for Creating/Editing CRM Records
+export interface CreateLeadForm {
+ name: string;
+ company: string;
+ email: string;
+ phone: string;
+ status: LeadStatus;
+ source: LeadSource;
+ value?: number;
+ description?: string;
+ industry?: string;
+ assignedTo?: string;
+}
+
+export interface CreateTaskForm {
+ subject: string;
+ description: string;
+ type: TaskType;
+ priority: TaskPriority;
+ dueDate: string;
+ assignedTo: string;
+ relatedTo: string;
+ relatedId?: string;
+}
+
+export interface CreateContactForm {
+ firstName: string;
+ lastName: string;
+ email: string;
+ phone: string;
+ company: string;
+ title: string;
+ status: ContactStatus;
+ leadSource: LeadSource;
+ address?: string;
+ website?: string;
+ notes?: string;
+ assignedTo?: string;
+}
+
+export interface CreateDealForm {
+ name: string;
+ amount: number;
+ stage: DealStage;
+ probability: number;
+ closeDate: string;
+ contactName: string;
+ accountName: string;
+ owner: string;
+ description?: string;
+ source?: LeadSource;
+}
+
diff --git a/src/modules/integrations/screens/ZohoAuth.tsx b/src/modules/integrations/screens/ZohoAuth.tsx
index 446a5a5..5d7e107 100644
--- a/src/modules/integrations/screens/ZohoAuth.tsx
+++ b/src/modules/integrations/screens/ZohoAuth.tsx
@@ -55,9 +55,11 @@ const ZOHO_CONFIG = {
// across Projects, CRM, Books, and People. Tailor scopes to your app's needs and compliance.
const getScopeForService = (_serviceKey?: ServiceKey): string => {
return [
+ //zoho portals
+ 'ZohoProjects.portals.READ',
// Zoho Projects
'ZohoProjects.projects.READ',
- 'ZohoProjects.tasks.READ',
+ 'ZohoProjects.tasklists.READ',
'ZohoProjects.timesheets.READ',
// Zoho CRM (adjust modules per your needs)
'ZohoCRM.users.READ',
diff --git a/src/modules/profile/screens/ProfileScreen.tsx b/src/modules/profile/screens/ProfileScreen.tsx
index 1e4c9ec..e44eb65 100644
--- a/src/modules/profile/screens/ProfileScreen.tsx
+++ b/src/modules/profile/screens/ProfileScreen.tsx
@@ -5,21 +5,28 @@ import { Container, ConfirmModal } from '@/shared/components/ui';
import type { RootState } from '@/store/store';
import { useTheme } from '@/shared/styles/useTheme';
import { logout } from '@/modules/auth/store/authSlice';
-import { setProfile } from '@/modules/profile/store/profileSlice';
+import { setProfile, setProfileFromAuth } from '@/modules/profile/store/profileSlice';
+import { selectUserDisplayName, selectUserEmail, selectUserRole, selectAuthUser } from '@/modules/profile/store/selectors';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import { clearSelectedService } from '@/modules/integrations/store/integrationsSlice';
const ProfileScreen: React.FC = () => {
const dispatch = useDispatch();
const { colors, fonts } = useTheme();
- const { name, email } = useSelector((s: RootState) => s.profile);
+
+ // Get user data using selectors
+ const authUser = useSelector(selectAuthUser);
+ const displayName = useSelector(selectUserDisplayName);
+ const email = useSelector(selectUserEmail);
+ const role = useSelector(selectUserRole);
+ const { name } = useSelector((s: RootState) => s.profile);
useEffect(() => {
- // Seed dummy data if empty
- if (!name && !email) {
- dispatch(setProfile({ name: 'Jane Doe', email: 'jane.doe@example.com' }));
+ // Sync profile data with auth user data when available
+ if (authUser && (!name || !email)) {
+ dispatch(setProfileFromAuth(authUser));
}
- }, [dispatch, name, email]);
+ }, [dispatch, authUser, name, email]);
const [showLogout, setShowLogout] = React.useState(false);
const handleLogout = () => setShowLogout(true);
@@ -45,12 +52,25 @@ const ProfileScreen: React.FC = () => {
{/* Name */}
- {name || 'Sana Afzal'}
+
+ {displayName}
+
{/* Email pill */}
- {email || 'sanaaafzal291@gmail.com'}
+
+ {email}
+
+
+ {/* Role badge */}
+ {role && (
+
+
+ {role.toUpperCase()}
+
+
+ )}
{/* Settings card */}
@@ -119,6 +139,12 @@ const styles = StyleSheet.create({
borderRadius: 14,
marginTop: 10,
},
+ roleBadge: {
+ paddingHorizontal: 16,
+ paddingVertical: 6,
+ borderRadius: 12,
+ marginTop: 8,
+ },
card: {
marginTop: 20,
marginHorizontal: 16,
diff --git a/src/modules/profile/store/profileSlice.ts b/src/modules/profile/store/profileSlice.ts
index d10d66c..2feaab7 100644
--- a/src/modules/profile/store/profileSlice.ts
+++ b/src/modules/profile/store/profileSlice.ts
@@ -1,8 +1,12 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+import type { AuthUser } from '@/modules/auth/store/authSlice';
export interface ProfileState {
name: string;
email: string;
+ role: string;
+ userId: number | null;
+ uuid: string;
loading: boolean;
error: string | null;
}
@@ -10,6 +14,9 @@ export interface ProfileState {
const initialState: ProfileState = {
name: '',
email: '',
+ role: '',
+ userId: null,
+ uuid: '',
loading: false,
error: null,
};
@@ -18,9 +25,19 @@ const profileSlice = createSlice({
name: 'profile',
initialState,
reducers: {
- setProfile: (state, action: PayloadAction<{ name: string; email: string }>) => {
+ setProfile: (state, action: PayloadAction<{ name: string; email: string; role?: string; userId?: number; uuid?: string }>) => {
state.name = action.payload.name;
state.email = action.payload.email;
+ if (action.payload.role) state.role = action.payload.role;
+ if (action.payload.userId) state.userId = action.payload.userId;
+ if (action.payload.uuid) state.uuid = action.payload.uuid;
+ },
+ setProfileFromAuth: (state, action: PayloadAction) => {
+ state.name = action.payload.displayName;
+ state.email = action.payload.email;
+ state.role = action.payload.role;
+ state.userId = action.payload.id;
+ state.uuid = action.payload.uuid;
},
setLoading: (state, action: PayloadAction) => {
state.loading = action.payload;
@@ -32,7 +49,7 @@ const profileSlice = createSlice({
},
});
-export const { setProfile, setLoading, setError, resetState } = profileSlice.actions;
+export const { setProfile, setProfileFromAuth, setLoading, setError, resetState } = profileSlice.actions;
export default profileSlice;
diff --git a/src/modules/profile/store/selectors.ts b/src/modules/profile/store/selectors.ts
new file mode 100644
index 0000000..2bf5fdc
--- /dev/null
+++ b/src/modules/profile/store/selectors.ts
@@ -0,0 +1,49 @@
+import { createSelector } from '@reduxjs/toolkit';
+import type { RootState } from '@/store/store';
+
+// Profile selectors
+export const selectProfile = (state: RootState) => state.profile;
+export const selectProfileName = (state: RootState) => state.profile.name;
+export const selectProfileEmail = (state: RootState) => state.profile.email;
+export const selectProfileRole = (state: RootState) => state.profile.role;
+export const selectProfileUserId = (state: RootState) => state.profile.userId;
+export const selectProfileUuid = (state: RootState) => state.profile.uuid;
+export const selectProfileLoading = (state: RootState) => state.profile.loading;
+export const selectProfileError = (state: RootState) => state.profile.error;
+
+// Auth selectors
+export const selectAuthUser = (state: RootState) => state.auth.user;
+export const selectIsAuthenticated = (state: RootState) => state.auth.isAuthenticated;
+
+// Combined selectors for user data
+export const selectUserDisplayName = createSelector(
+ [selectProfileName, selectAuthUser],
+ (profileName, authUser) => profileName || authUser?.displayName || 'User'
+);
+
+export const selectUserEmail = createSelector(
+ [selectProfileEmail, selectAuthUser],
+ (profileEmail, authUser) => profileEmail || authUser?.email || 'user@example.com'
+);
+
+export const selectUserRole = createSelector(
+ [selectProfileRole, selectAuthUser],
+ (profileRole, authUser) => profileRole || authUser?.role || ''
+);
+
+export const selectUserData = createSelector(
+ [selectAuthUser, selectProfile],
+ (authUser, profile) => ({
+ name: profile.name || authUser?.displayName || 'User',
+ email: profile.email || authUser?.email || 'user@example.com',
+ role: profile.role || authUser?.role || '',
+ userId: profile.userId || authUser?.id || null,
+ uuid: profile.uuid || authUser?.uuid || '',
+ })
+);
+
+// Check if profile data is available
+export const selectHasProfileData = createSelector(
+ [selectUserDisplayName, selectUserEmail],
+ (name, email) => !!(name && email && name !== 'User' && email !== 'user@example.com')
+);
diff --git a/src/services/http.ts b/src/services/http.ts
index 939e1bd..790d686 100644
--- a/src/services/http.ts
+++ b/src/services/http.ts
@@ -1,10 +1,58 @@
import { create } from 'apisauce';
+import { store } from '@/store/store';
+import { selectAccessToken } from '@/modules/auth/store/selectors';
const http = create({
- baseURL: 'http://192.168.1.16:4000',
+ baseURL: 'http://192.168.1.12:4000',
+ // baseURL: 'http://160.187.167.216',
timeout: 10000,
});
+// Add request interceptor to include auth token
+http.addRequestTransform((request) => {
+ // Skip adding token for authentication and public endpoints
+ const publicEndpoints = [
+ '/api/v1/auth/login', // All auth endpoints
+ '/api/v1/users/register', // User registration
+ '/api/v1/users/signup', // User signup
+ '/api/v1/public/', // Any public endpoints
+ ];
+
+ const isPublicEndpoint = publicEndpoints.some(endpoint =>
+ request.url?.startsWith(endpoint)
+ );
+
+ if (isPublicEndpoint) {
+ return; // Skip adding token for public endpoints
+ }
+
+ const state = store.getState();
+ const token = selectAccessToken(state);
+
+ if (token) {
+ request.headers = {
+ ...request.headers,
+ Authorization: `Bearer ${token}`,
+ };
+ } else {
+ console.warn('No access token found for API request to:', request.url);
+ }
+});
+
+// Add response interceptor for error handling
+http.addResponseTransform((response) => {
+ if (response.status === 401) {
+ console.warn('Unauthorized request - token may be expired');
+ // You could dispatch a logout action here if needed
+ // dispatch(logout());
+ }
+
+ // Log successful requests for debugging (optional)
+ if (response.ok && __DEV__) {
+ console.log(`✅ API Success: ${response.config?.method?.toUpperCase()} ${response.config?.url}`);
+ }
+});
+
export default http;
diff --git a/src/shared/components/charts/CompactPipeline.tsx b/src/shared/components/charts/CompactPipeline.tsx
new file mode 100644
index 0000000..ec12fae
--- /dev/null
+++ b/src/shared/components/charts/CompactPipeline.tsx
@@ -0,0 +1,181 @@
+import React from 'react';
+import { View, Text, StyleSheet } from 'react-native';
+import Svg, { Rect, Text as SvgText } from 'react-native-svg';
+
+interface CompactPipelineData {
+ label: string;
+ value: number;
+ color: string;
+}
+
+interface CompactPipelineProps {
+ data: CompactPipelineData[];
+ colors: any;
+ fonts: any;
+}
+
+const CompactPipeline: React.FC = ({
+ data,
+ colors,
+ fonts
+}) => {
+ const total = data.reduce((sum, item) => sum + item.value, 0);
+ const maxValue = Math.max(...data.map(item => item.value));
+
+ if (total === 0) {
+ return (
+
+
+ No Pipeline Data
+
+
+ );
+ }
+
+ const containerWidth = 320;
+ const barHeight = 24;
+ const labelWidth = 80;
+ const valueWidth = 40;
+ const barWidth = containerWidth - labelWidth - valueWidth - 60; // 60 for padding
+
+ return (
+
+
+
+ {/* Summary Row */}
+
+
+
+ Total Stages
+
+
+ {data.length}
+
+
+
+
+ Total Deals
+
+
+ {total}
+
+
+
+
+ Avg/Stage
+
+
+ {data.length > 0 ? Math.round(total / data.length) : 0}
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ paddingVertical: 8,
+ paddingHorizontal: 5,
+ },
+ noDataText: {
+ fontSize: 14,
+ textAlign: 'center',
+ fontStyle: 'italic',
+ paddingVertical: 20,
+ },
+ summaryRow: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ marginTop: 12,
+ paddingTop: 8,
+ },
+ summaryItem: {
+ flex: 1,
+ alignItems: 'center',
+ paddingVertical: 6,
+ borderTopWidth: 1,
+ marginHorizontal: 4,
+ },
+ summaryLabel: {
+ fontSize: 10,
+ marginBottom: 2,
+ },
+ summaryValue: {
+ fontSize: 14,
+ fontWeight: 'bold',
+ },
+});
+
+export default CompactPipeline;
diff --git a/src/shared/components/charts/DonutChart.tsx b/src/shared/components/charts/DonutChart.tsx
new file mode 100644
index 0000000..eec0c09
--- /dev/null
+++ b/src/shared/components/charts/DonutChart.tsx
@@ -0,0 +1,133 @@
+import React from 'react';
+import { View, Text, StyleSheet } from 'react-native';
+import Svg, { Circle, G } from 'react-native-svg';
+
+interface DonutChartData {
+ label: string;
+ value: number;
+ color: string;
+}
+
+interface DonutChartProps {
+ data: DonutChartData[];
+ size?: number;
+ colors: any;
+ fonts: any;
+}
+
+const DonutChart: React.FC = ({
+ data,
+ size = 140,
+ colors,
+ fonts
+}) => {
+ const total = data.reduce((sum, item) => sum + item.value, 0);
+
+ if (total === 0) {
+ return (
+
+
+
+ No Data
+
+
+
+ );
+ }
+
+ let currentAngle = 0;
+ const radius = (size - 40) / 2;
+ const innerRadius = radius * 0.6;
+ const centerX = size / 2;
+ const centerY = size / 2;
+
+ return (
+
+
+
+
+
+ {total}
+
+
+ Sources
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ alignItems: 'center',
+ },
+ chartContainer: {
+ position: 'relative',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ centerText: {
+ position: 'absolute',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ totalText: {
+ fontSize: 16,
+ fontWeight: 'bold',
+ },
+ totalLabel: {
+ fontSize: 9,
+ marginTop: 2,
+ },
+ noDataText: {
+ fontSize: 14,
+ textAlign: 'center',
+ },
+});
+
+export default DonutChart;
diff --git a/src/shared/components/charts/FunnelChart.tsx b/src/shared/components/charts/FunnelChart.tsx
new file mode 100644
index 0000000..5f05f67
--- /dev/null
+++ b/src/shared/components/charts/FunnelChart.tsx
@@ -0,0 +1,121 @@
+import React from 'react';
+import { View, Text, StyleSheet } from 'react-native';
+import Svg, { Path, Text as SvgText } from 'react-native-svg';
+
+interface FunnelChartData {
+ label: string;
+ value: number;
+ color: string;
+}
+
+interface FunnelChartProps {
+ data: FunnelChartData[];
+ colors: any;
+ fonts: any;
+}
+
+const FunnelChart: React.FC = ({
+ data,
+ colors,
+ fonts
+}) => {
+ const total = data.reduce((sum, item) => sum + item.value, 0);
+ const maxValue = Math.max(...data.map(item => item.value));
+
+ if (total === 0) {
+ return (
+
+
+ No Pipeline Data
+
+
+ );
+ }
+
+ // Increased dimensions for better spacing
+ const funnelWidth = 280;
+ const funnelHeight = 220;
+ const segmentHeight = funnelHeight / data.length;
+ const centerX = funnelWidth / 2;
+ const textPadding = 20; // Space for text on sides
+
+ const createFunnelPath = (index: number, value: number) => {
+ const availableWidth = funnelWidth - (textPadding * 2);
+ const width = (value / maxValue) * (availableWidth * 0.7) + (availableWidth * 0.15);
+ const y = index * segmentHeight;
+ const nextY = (index + 1) * segmentHeight;
+ const nextWidth = index < data.length - 1 ?
+ (data[index + 1].value / maxValue) * (availableWidth * 0.7) + (availableWidth * 0.15) :
+ (availableWidth * 0.15);
+
+ const leftX = centerX - width / 2;
+ const rightX = centerX + width / 2;
+ const nextLeftX = centerX - nextWidth / 2;
+ const nextRightX = centerX + nextWidth / 2;
+
+ return `M ${leftX} ${y} L ${rightX} ${y} L ${nextRightX} ${nextY} L ${nextLeftX} ${nextY} Z`;
+ };
+
+ return (
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ paddingVertical: 15,
+ paddingHorizontal: 10,
+ width: '100%',
+ },
+ noDataText: {
+ fontSize: 14,
+ textAlign: 'center',
+ fontStyle: 'italic',
+ },
+});
+
+export default FunnelChart;
diff --git a/src/shared/components/charts/PieChart.tsx b/src/shared/components/charts/PieChart.tsx
new file mode 100644
index 0000000..e48a914
--- /dev/null
+++ b/src/shared/components/charts/PieChart.tsx
@@ -0,0 +1,131 @@
+import React from 'react';
+import { View, Text, StyleSheet } from 'react-native';
+import Svg, { Circle, G } from 'react-native-svg';
+
+interface PieChartData {
+ label: string;
+ value: number;
+ color: string;
+}
+
+interface PieChartProps {
+ data: PieChartData[];
+ size?: number;
+ colors: any;
+ fonts: any;
+}
+
+const PieChart: React.FC = ({
+ data,
+ size = 120,
+ colors,
+ fonts
+}) => {
+ const total = data.reduce((sum, item) => sum + item.value, 0);
+
+ if (total === 0) {
+ return (
+
+
+
+ No Data
+
+
+
+ );
+ }
+
+ let currentAngle = 0;
+ const radius = (size - 40) / 2;
+ const centerX = size / 2;
+ const centerY = size / 2;
+
+ const createArcPath = (startAngle: number, endAngle: number, radius: number) => {
+ const start = polarToCartesian(centerX, centerY, radius, endAngle);
+ const end = polarToCartesian(centerX, centerY, radius, startAngle);
+ const largeArcFlag = endAngle - startAngle <= 180 ? "0" : "1";
+
+ return `M ${start.x} ${start.y} A ${radius} ${radius} 0 ${largeArcFlag} 0 ${end.x} ${end.y} L ${centerX} ${centerY} Z`;
+ };
+
+ const polarToCartesian = (centerX: number, centerY: number, radius: number, angleInDegrees: number) => {
+ const angleInRadians = (angleInDegrees - 90) * Math.PI / 180.0;
+ return {
+ x: centerX + (radius * Math.cos(angleInRadians)),
+ y: centerY + (radius * Math.sin(angleInRadians))
+ };
+ };
+
+ return (
+
+
+
+
+
+ {total}
+
+
+ Total
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ alignItems: 'center',
+ },
+ chartContainer: {
+ position: 'relative',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ centerText: {
+ position: 'absolute',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ totalText: {
+ fontSize: 18,
+ fontWeight: 'bold',
+ },
+ totalLabel: {
+ fontSize: 11,
+ marginTop: 2,
+ },
+ noDataText: {
+ fontSize: 14,
+ textAlign: 'center',
+ },
+});
+
+export default PieChart;
diff --git a/src/shared/components/charts/PipelineCards.tsx b/src/shared/components/charts/PipelineCards.tsx
new file mode 100644
index 0000000..9ce80a7
--- /dev/null
+++ b/src/shared/components/charts/PipelineCards.tsx
@@ -0,0 +1,189 @@
+import React from 'react';
+import { View, Text, StyleSheet } from 'react-native';
+
+interface PipelineCardsData {
+ label: string;
+ value: number;
+ color: string;
+}
+
+interface PipelineCardsProps {
+ data: PipelineCardsData[];
+ colors: any;
+ fonts: any;
+}
+
+const PipelineCards: React.FC = ({
+ data,
+ colors,
+ fonts
+}) => {
+ const total = data.reduce((sum, item) => sum + item.value, 0);
+ const maxValue = Math.max(...data.map(item => item.value));
+
+ if (total === 0) {
+ return (
+
+
+ No Pipeline Data
+
+
+ );
+ }
+
+ return (
+
+ {data.map((stage, index) => {
+ const percentage = total > 0 ? (stage.value / total) * 100 : 0;
+ const progressWidth = maxValue > 0 ? (stage.value / maxValue) * 100 : 0;
+
+ return (
+
+ {/* Stage Header */}
+
+
+
+
+ {stage.label}
+
+
+
+
+ {stage.value}
+
+
+ {percentage.toFixed(1)}%
+
+
+
+
+ {/* Progress Bar */}
+
+
+
+
+ {/* Stage Details */}
+
+
+
+ Deals
+
+
+ {stage.value}
+
+
+
+
+ Pipeline %
+
+
+ {percentage.toFixed(1)}%
+
+
+
+
+ Rank
+
+
+ #{index + 1}
+
+
+
+
+ );
+ })}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ paddingVertical: 8,
+ },
+ noDataText: {
+ fontSize: 14,
+ textAlign: 'center',
+ fontStyle: 'italic',
+ paddingVertical: 20,
+ },
+ stageCard: {
+ borderRadius: 12,
+ borderWidth: 1,
+ padding: 16,
+ marginBottom: 12,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ elevation: 3,
+ },
+ stageHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: 12,
+ },
+ stageInfo: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ flex: 1,
+ },
+ stageIndicator: {
+ width: 12,
+ height: 12,
+ borderRadius: 6,
+ marginRight: 10,
+ },
+ stageLabel: {
+ fontSize: 16,
+ fontWeight: '600',
+ flex: 1,
+ },
+ stageStats: {
+ alignItems: 'flex-end',
+ },
+ stageValue: {
+ fontSize: 20,
+ fontWeight: 'bold',
+ },
+ stagePercentage: {
+ fontSize: 12,
+ marginTop: 2,
+ },
+ progressContainer: {
+ height: 8,
+ borderRadius: 4,
+ marginBottom: 12,
+ overflow: 'hidden',
+ },
+ progressBar: {
+ height: '100%',
+ borderRadius: 4,
+ },
+ stageDetails: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ },
+ detailItem: {
+ alignItems: 'center',
+ flex: 1,
+ },
+ detailLabel: {
+ fontSize: 11,
+ marginBottom: 4,
+ },
+ detailValue: {
+ fontSize: 14,
+ fontWeight: 'bold',
+ },
+});
+
+export default PipelineCards;
diff --git a/src/shared/components/charts/PipelineFlow.tsx b/src/shared/components/charts/PipelineFlow.tsx
new file mode 100644
index 0000000..b87a3f7
--- /dev/null
+++ b/src/shared/components/charts/PipelineFlow.tsx
@@ -0,0 +1,159 @@
+import React from 'react';
+import { View, Text, StyleSheet } from 'react-native';
+import Svg, { Rect, Circle, Line, Text as SvgText } from 'react-native-svg';
+
+interface PipelineFlowData {
+ label: string;
+ value: number;
+ color: string;
+}
+
+interface PipelineFlowProps {
+ data: PipelineFlowData[];
+ colors: any;
+ fonts: any;
+}
+
+const PipelineFlow: React.FC = ({
+ data,
+ colors,
+ fonts
+}) => {
+ const total = data.reduce((sum, item) => sum + item.value, 0);
+ const maxValue = Math.max(...data.map(item => item.value));
+
+ if (total === 0) {
+ return (
+
+
+ No Pipeline Data
+
+
+ );
+ }
+
+ const containerWidth = 300;
+ const stageHeight = 50;
+ const circleRadius = 12;
+ const lineWidth = 40;
+ const startX = 30;
+ const textOffset = 60;
+
+ return (
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ alignItems: 'flex-start',
+ justifyContent: 'center',
+ paddingVertical: 10,
+ paddingHorizontal: 5,
+ },
+ noDataText: {
+ fontSize: 14,
+ textAlign: 'center',
+ fontStyle: 'italic',
+ },
+});
+
+export default PipelineFlow;
diff --git a/src/shared/components/charts/StackedBarChart.tsx b/src/shared/components/charts/StackedBarChart.tsx
new file mode 100644
index 0000000..d3103ec
--- /dev/null
+++ b/src/shared/components/charts/StackedBarChart.tsx
@@ -0,0 +1,104 @@
+import React from 'react';
+import { View, Text, StyleSheet } from 'react-native';
+import Svg, { Rect, Text as SvgText } from 'react-native-svg';
+
+interface StackedBarChartData {
+ label: string;
+ value: number;
+ color: string;
+}
+
+interface StackedBarChartProps {
+ data: StackedBarChartData[];
+ colors: any;
+ fonts: any;
+ height?: number;
+}
+
+const StackedBarChart: React.FC = ({
+ data,
+ colors,
+ fonts,
+ height = 120
+}) => {
+ const total = data.reduce((sum, item) => sum + item.value, 0);
+ const maxValue = Math.max(...data.map(item => item.value));
+
+ if (total === 0) {
+ return (
+
+
+ No Task Data
+
+
+ );
+ }
+
+ const chartWidth = 250;
+ const barWidth = chartWidth / data.length - 10;
+ const maxBarHeight = height - 40;
+
+ return (
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ paddingVertical: 10,
+ },
+ noDataText: {
+ fontSize: 14,
+ textAlign: 'center',
+ fontStyle: 'italic',
+ },
+});
+
+export default StackedBarChart;
diff --git a/src/shared/components/charts/index.ts b/src/shared/components/charts/index.ts
new file mode 100644
index 0000000..1f5bd9b
--- /dev/null
+++ b/src/shared/components/charts/index.ts
@@ -0,0 +1,7 @@
+export { default as PieChart } from './PieChart';
+export { default as DonutChart } from './DonutChart';
+export { default as FunnelChart } from './FunnelChart';
+export { default as StackedBarChart } from './StackedBarChart';
+export { default as PipelineFlow } from './PipelineFlow';
+export { default as PipelineCards } from './PipelineCards';
+export { default as CompactPipeline } from './CompactPipeline';
diff --git a/src/shared/constants/API_ENDPOINTS.ts b/src/shared/constants/API_ENDPOINTS.ts
index 0824046..c86ea89 100644
--- a/src/shared/constants/API_ENDPOINTS.ts
+++ b/src/shared/constants/API_ENDPOINTS.ts
@@ -5,6 +5,9 @@ export const API_ENDPOINTS = {
HR_METRICS: '/hr/metrics',
ZOHO_PROJECTS: '/zoho/projects',
PROFILE: '/profile',
+
+ // CRM API Endpoints
+ CRM_DATA: '/api/v1/crm/data',
} as const;
export type ApiEndpointKey = keyof typeof API_ENDPOINTS;
diff --git a/src/store/store.ts b/src/store/store.ts
index 5b32204..a4653f3 100644
--- a/src/store/store.ts
+++ b/src/store/store.ts
@@ -9,6 +9,7 @@ import hrSlice from '@/modules/hr/store/hrSlice';
import zohoProjectsSlice from '@/modules/zohoProjects/store/zohoProjectsSlice';
import profileSlice from '@/modules/profile/store/profileSlice';
import integrationsSlice from '@/modules/integrations/store/integrationsSlice';
+import crmSlice from '@/modules/crm/store/crmSlice';
const rootReducer = combineReducers({
auth: authSlice.reducer,
@@ -16,13 +17,14 @@ const rootReducer = combineReducers({
zohoProjects: zohoProjectsSlice.reducer,
profile: profileSlice.reducer,
integrations: integrationsSlice.reducer,
+ crm: crmSlice,
ui: uiSlice.reducer,
});
const persistConfig = {
key: 'root',
storage: AsyncStorage,
- whitelist: ['auth', 'hr', 'zohoProjects', 'profile', 'integrations'],
+ whitelist: ['auth', 'hr', 'zohoProjects', 'profile', 'integrations', 'crm'],
blacklist: ['ui'],
};