folder structure modified based on the different providers

This commit is contained in:
yashwin-foxy 2025-09-22 14:15:51 +05:30
parent 7b45abc367
commit 8c1e5309e5
35 changed files with 2846 additions and 404 deletions

5
src/modules/crm/index.ts Normal file
View File

@ -0,0 +1,5 @@
// CRM Module Exports
export { default as CrmNavigator } from './navigation/CrmNavigator';
// Zoho CRM exports
export * from './zoho';

View File

@ -1,7 +1,7 @@
import React from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import CrmDashboardScreen from '@/modules/crm/screens/CrmDashboardScreen';
import ZohoCrmDataScreen from '@/modules/crm/screens/ZohoCrmDataScreen';
import CrmDashboardScreen from '@/modules/crm/zoho/screens/CrmDashboardScreen';
import ZohoCrmDataScreen from '@/modules/crm/zoho/screens/ZohoCrmDataScreen';
const Stack = createStackNavigator();

View File

@ -1,21 +0,0 @@
// 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';

View File

@ -0,0 +1,7 @@
// Zoho CRM Module Exports
export { default as CrmDashboardScreen } from './screens/CrmDashboardScreen';
export { default as ZohoCrmDataScreen } from './screens/ZohoCrmDataScreen';
export { default as crmSlice } from './store/crmSlice';
export { selectDashboardData, selectIsAnyLoading, selectHasAnyError, selectCrmStats } from './store/selectors';
export * from './types/CrmTypes';
export { crmAPI } from './services/crmAPI';

View File

@ -124,6 +124,15 @@ const ZohoCrmDataScreen: React.FC = () => {
const renderTabContent = () => {
const commonFlatListProps = {
numColumns: 1,
showsVerticalScrollIndicator: false,
contentContainerStyle: styles.listContainer,
refreshControl: (
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
),
};
switch (selectedTab) {
case 'leads':
return (
@ -136,9 +145,7 @@ const ZohoCrmDataScreen: React.FC = () => {
/>
)}
keyExtractor={(item) => item.id}
numColumns={1}
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.listContainer}
{...commonFlatListProps}
/>
);
case 'tasks':
@ -152,9 +159,7 @@ const ZohoCrmDataScreen: React.FC = () => {
/>
)}
keyExtractor={(item) => item.id}
numColumns={1}
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.listContainer}
{...commonFlatListProps}
/>
);
case 'contacts':
@ -168,9 +173,7 @@ const ZohoCrmDataScreen: React.FC = () => {
/>
)}
keyExtractor={(item) => item.id}
numColumns={1}
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.listContainer}
{...commonFlatListProps}
/>
);
case 'deals':
@ -184,9 +187,7 @@ const ZohoCrmDataScreen: React.FC = () => {
/>
)}
keyExtractor={(item) => item.id}
numColumns={1}
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.listContainer}
{...commonFlatListProps}
/>
);
case 'salesOrders':
@ -200,9 +201,7 @@ const ZohoCrmDataScreen: React.FC = () => {
/>
)}
keyExtractor={(item) => item.id}
numColumns={1}
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.listContainer}
{...commonFlatListProps}
/>
);
case 'purchaseOrders':
@ -216,9 +215,7 @@ const ZohoCrmDataScreen: React.FC = () => {
/>
)}
keyExtractor={(item) => item.id}
numColumns={1}
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.listContainer}
{...commonFlatListProps}
/>
);
case 'invoices':
@ -232,9 +229,7 @@ const ZohoCrmDataScreen: React.FC = () => {
/>
)}
keyExtractor={(item) => item.id}
numColumns={1}
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.listContainer}
{...commonFlatListProps}
/>
);
default:
@ -243,14 +238,8 @@ const ZohoCrmDataScreen: React.FC = () => {
};
return (
<Container>
<ScrollView
style={[styles.container, { backgroundColor: colors.background }]}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
}
>
{/* Header */}
<View style={[styles.container, { backgroundColor: colors.background }]}>
{/* Fixed Header */}
<View style={styles.header}>
<Text style={[styles.title, { color: colors.text, fontFamily: fonts.bold }]}>
Zoho CRM Data
@ -260,64 +249,65 @@ const ZohoCrmDataScreen: React.FC = () => {
</TouchableOpacity>
</View>
{/* Tabs */}
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.tabsContainer}>
<View style={styles.tabs}>
{tabs.map((tab) => (
<TouchableOpacity
key={tab.key}
style={[
styles.tab,
selectedTab === tab.key && { backgroundColor: colors.primary },
]}
onPress={() => setSelectedTab(tab.key)}
activeOpacity={0.8}
>
<Icon
name={tab.icon}
size={20}
color={selectedTab === tab.key ? colors.surface : colors.textLight}
/>
<Text
{/* Fixed Tabs */}
<View style={styles.tabsContainer}>
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
<View style={styles.tabs}>
{tabs.map((tab) => (
<TouchableOpacity
key={tab.key}
style={[
styles.tabText,
{
color: selectedTab === tab.key ? colors.surface : colors.textLight,
fontFamily: fonts.medium,
},
]}
>
{tab.label}
</Text>
<View
style={[
styles.countBadge,
{ backgroundColor: selectedTab === tab.key ? colors.surface : colors.primary },
styles.tab,
selectedTab === tab.key && { backgroundColor: colors.primary },
]}
onPress={() => setSelectedTab(tab.key)}
activeOpacity={0.8}
>
<Icon
name={tab.icon}
size={20}
color={selectedTab === tab.key ? colors.surface : colors.textLight}
/>
<Text
style={[
styles.countText,
styles.tabText,
{
color: selectedTab === tab.key ? colors.primary : colors.surface,
fontFamily: fonts.bold,
color: selectedTab === tab.key ? colors.surface : colors.textLight,
fontFamily: fonts.medium,
},
]}
>
{tab.count}
{tab.label}
</Text>
</View>
</TouchableOpacity>
))}
</View>
</ScrollView>
<View
style={[
styles.countBadge,
{ backgroundColor: selectedTab === tab.key ? colors.surface : colors.primary },
]}
>
<Text
style={[
styles.countText,
{
color: selectedTab === tab.key ? colors.primary : colors.surface,
fontFamily: fonts.bold,
},
]}
>
{tab.count}
</Text>
</View>
</TouchableOpacity>
))}
</View>
</ScrollView>
</View>
{/* Content */}
{/* Scrollable Content */}
<View style={styles.content}>
{renderTabContent()}
</View>
</ScrollView>
</Container>
</View>
);
};
@ -336,7 +326,10 @@ const styles = StyleSheet.create({
fontSize: 24,
},
tabsContainer: {
marginBottom: 16,
backgroundColor: '#FFFFFF',
paddingVertical: 12,
borderBottomWidth: 1,
borderBottomColor: '#E5E7EB',
},
tabs: {
flexDirection: 'row',
@ -368,9 +361,9 @@ const styles = StyleSheet.create({
},
content: {
flex: 1,
paddingHorizontal: 16,
},
listContainer: {
paddingHorizontal: 16,
paddingBottom: 20,
},
});

View File

@ -0,0 +1,6 @@
// Finance Module Exports
export { default as FinanceDashboardScreen } from './screens/FinanceDashboardScreen';
export { default as FinanceNavigator } from './navigation/FinanceNavigator';
// Zoho Books exports
export * from './zoho';

View File

@ -1,12 +1,14 @@
import React from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import FinanceDashboardScreen from '@/modules/finance/screens/FinanceDashboardScreen';
import ZohoBooksDashboardScreen from '@/modules/finance/zoho/screens/ZohoBooksDashboardScreen';
import ZohoBooksDataScreen from '@/modules/finance/zoho/screens/ZohoBooksDataScreen';
const Stack = createStackNavigator();
const FinanceNavigator = () => (
<Stack.Navigator>
<Stack.Screen name="FinanceDashboard" component={FinanceDashboardScreen} options={{headerShown:false}}/>
<Stack.Screen name="ZohoBooksDashboard" component={ZohoBooksDashboardScreen} options={{headerShown:false}}/>
<Stack.Screen name="ZohoBooksData" component={ZohoBooksDataScreen} options={{headerShown:false}}/>
</Stack.Navigator>
);

View File

@ -1,230 +0,0 @@
import React, { useMemo } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { Container } from '@/shared/components/ui';
import { useTheme } from '@/shared/styles/useTheme';
const FinanceDashboardScreen: React.FC = () => {
const { colors, fonts } = useTheme();
const mock = useMemo(() => {
// Zoho Books oriented metrics
const cash = 246000;
const invoices = { total: 485000, outstanding: 162000, overdue: 48000, paidThisMonth: 92000 };
const monthlySales = [84, 92, 88, 104, 112, 118]; // in thousands
const arAging = [45, 30, 18, 7]; // 0-30, 31-60, 61-90, 90+
const invoiceStatus = [
{ label: 'Draft', value: 30, color: '#94A3B8' },
{ label: 'Sent', value: 80, color: '#3AA0FF' },
{ label: 'Viewed', value: 50, color: '#6366F1' },
{ label: 'Paid', value: 210, color: '#10B981' },
{ label: 'Overdue', value: 18, color: '#EF4444' },
];
const taxes = { collected: 38000, paid: 12500 };
const topCustomers = [
{ client: 'Acme Corp', amount: 82000 },
{ client: 'Initech', amount: 54000 },
{ client: 'Umbrella', amount: 48000 },
];
const bankAccounts = [
{ name: 'HDFC 1234', balance: 152000 },
{ name: 'ICICI 9981', balance: 78000 },
];
const paymentModes = [
{ label: 'Online', value: 120, color: '#3AA0FF' },
{ label: 'Bank Transfer', value: 90, color: '#10B981' },
{ label: 'Cash', value: 40, color: '#F59E0B' },
{ label: 'Cheque', value: 12, color: '#6366F1' },
];
const estimates = { sent: 24, accepted: 16, declined: 3 };
return { cash, invoices, monthlySales, arAging, invoiceStatus, taxes, topCustomers, bankAccounts, paymentModes, estimates };
}, []);
return (
<Container>
<View style={[styles.wrap, { backgroundColor: colors.background }]}>
<Text style={[styles.title, { color: colors.text, fontFamily: fonts.bold }]}>Accounts & Finance</Text>
{/* KPIs */}
<View style={styles.kpiGrid}>
<Kpi label="Outstanding" value={`$${mock.invoices.outstanding.toLocaleString()}`} color={colors.text} fonts={fonts} accent="#3AA0FF" />
<Kpi label="Overdue" value={`$${mock.invoices.overdue.toLocaleString()}`} color={colors.text} fonts={fonts} accent="#EF4444" />
<Kpi label="Paid (This Month)" value={`$${mock.invoices.paidThisMonth.toLocaleString()}`} color={colors.text} fonts={fonts} accent="#10B981" />
<Kpi label="Cash" value={`$${mock.cash.toLocaleString()}`} color={colors.text} fonts={fonts} accent="#6366F1" />
</View>
{/* Sales Trend */}
<View style={[styles.card, { borderColor: colors.border, backgroundColor: colors.surface }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Sales Trend</Text>
<Bars data={mock.monthlySales} max={Math.max(...mock.monthlySales)} color="#3AA0FF" />
</View>
{/* Taxes & AR Aging */}
<View style={styles.row}>
<View style={[styles.card, styles.col, { borderColor: colors.border, backgroundColor: colors.surface }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Taxes</Text>
{(() => {
const total = Math.max(1, mock.taxes.collected + mock.taxes.paid);
const colPct = Math.round((mock.taxes.collected / total) * 100);
const paidPct = Math.round((mock.taxes.paid / total) * 100);
return (
<>
<Text style={{ fontSize: 12, color: colors.text, fontFamily: fonts.regular }}>Collected: ${mock.taxes.collected.toLocaleString()}</Text>
<Progress value={colPct} color="#10B981" fonts={fonts} />
<Text style={{ fontSize: 12, color: colors.text, fontFamily: fonts.regular, marginTop: 6 }}>Paid: ${mock.taxes.paid.toLocaleString()}</Text>
<Progress value={paidPct} color="#F59E0B" fonts={fonts} />
</>
);
})()}
</View>
<View style={[styles.card, styles.col, { borderColor: colors.border, backgroundColor: colors.surface }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>A/R Aging</Text>
<Bars data={mock.arAging} max={Math.max(...mock.arAging)} color="#F59E0B" />
<View style={styles.rowJustify}>
<Text style={{ fontSize: 12, color: colors.text, fontFamily: fonts.regular }}>0-30</Text>
<Text style={{ fontSize: 12, color: colors.text, fontFamily: fonts.regular }}>31-60</Text>
<Text style={{ fontSize: 12, color: colors.text, fontFamily: fonts.regular }}>61-90</Text>
<Text style={{ fontSize: 12, color: colors.text, fontFamily: fonts.regular }}>90+</Text>
</View>
</View>
</View>
{/* Invoice Status Distribution */}
<View style={[styles.card, { borderColor: colors.border, backgroundColor: colors.surface }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Invoice Status</Text>
<Stacked segments={mock.invoiceStatus} total={mock.invoiceStatus.reduce((a, b) => a + b.value, 0)} />
<View style={styles.legendRow}>
{mock.invoiceStatus.map(s => (
<View key={s.label} style={styles.legendItem}>
<View style={[styles.legendDot, { backgroundColor: s.color }]} />
<Text style={{ color: colors.text, fontFamily: fonts.regular }}>{s.label}</Text>
</View>
))}
</View>
</View>
{/* Lists */}
<View style={styles.row}>
<View style={[styles.card, styles.col, { borderColor: colors.border, backgroundColor: colors.surface }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Top Customers</Text>
{mock.topCustomers.map(r => (
<View key={r.client} style={styles.listRow}>
<Text style={[styles.listPrimary, { color: colors.text, fontFamily: fonts.regular }]}>{r.client}</Text>
<Text style={[styles.listSecondary, { color: colors.text, fontFamily: fonts.medium }]}>${r.amount.toLocaleString()}</Text>
</View>
))}
</View>
<View style={[styles.card, styles.col, { borderColor: colors.border, backgroundColor: colors.surface }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Bank Accounts</Text>
{mock.bankAccounts.map(p => (
<View key={p.name} style={styles.listRow}>
<Text style={[styles.listPrimary, { color: colors.text, fontFamily: fonts.regular }]}>{p.name}</Text>
<Text style={[styles.listSecondary, { color: colors.text, fontFamily: fonts.medium }]}>${p.balance.toLocaleString()}</Text>
</View>
))}
</View>
</View>
{/* Estimates & Payment Modes */}
<View style={[styles.card, { borderColor: colors.border, backgroundColor: colors.surface }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Estimates</Text>
<View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
<View style={{ paddingVertical: 6, paddingHorizontal: 10, backgroundColor: '#F3F4F6', borderRadius: 14, marginRight: 8, marginTop: 8 }}>
<Text style={{ fontSize: 12, fontFamily: fonts.regular, color: colors.text }}>Sent: {mock.estimates.sent}</Text>
</View>
<View style={{ paddingVertical: 6, paddingHorizontal: 10, backgroundColor: '#E9FAF2', borderRadius: 14, marginRight: 8, marginTop: 8 }}>
<Text style={{ fontSize: 12, fontFamily: fonts.regular, color: colors.text }}>Accepted: {mock.estimates.accepted}</Text>
</View>
<View style={{ paddingVertical: 6, paddingHorizontal: 10, backgroundColor: '#FFF4E6', borderRadius: 14, marginRight: 8, marginTop: 8 }}>
<Text style={{ fontSize: 12, fontFamily: fonts.regular, color: colors.text }}>Declined: {mock.estimates.declined}</Text>
</View>
</View>
</View>
<View style={[styles.card, { borderColor: colors.border, backgroundColor: colors.surface }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Payment Modes</Text>
<Stacked segments={mock.paymentModes} total={mock.paymentModes.reduce((a, b) => a + b.value, 0)} />
<View style={styles.legendRow}>
{mock.paymentModes.map(s => (
<View key={s.label} style={styles.legendItem}>
<View style={[styles.legendDot, { backgroundColor: s.color }]} />
<Text style={{ color: colors.text, fontFamily: fonts.regular }}>{s.label}</Text>
</View>
))}
</View>
</View>
</View>
</Container>
);
};
const styles = StyleSheet.create({
wrap: { flex: 1, padding: 16 },
title: { fontSize: 18, marginBottom: 8 },
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 },
row: { flexDirection: 'row', marginTop: 12 },
col: { flex: 1, marginRight: 8 },
bars: { flexDirection: 'row', alignItems: 'flex-end' },
bar: { flex: 1, marginRight: 6, borderTopLeftRadius: 4, borderTopRightRadius: 4 },
progressWrap: { marginTop: 8 },
progressTrack: { height: 8, borderRadius: 6, backgroundColor: '#E5E7EB', overflow: 'hidden' },
progressFill: { height: '100%', borderRadius: 6 },
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 },
rowJustify: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 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 FinanceDashboardScreen;
// UI bits
const Kpi: React.FC<{ label: string; value: string; color: string; fonts: any; accent: string }> = ({ label, value, color, fonts, accent }) => {
return (
<View style={styles.kpiCard}>
<Text style={[styles.kpiLabel, { color, fontFamily: fonts.regular }]}>{label}</Text>
<View style={styles.kpiValueRow}>
<Text style={{ color, fontSize: 20, fontFamily: fonts.bold }}>{value}</Text>
<View style={{ width: 10, height: 10, borderRadius: 5, backgroundColor: accent }} />
</View>
</View>
);
};
const Bars: React.FC<{ data: number[]; max: number; color: string }> = ({ data, max, color }) => {
return (
<View style={styles.bars}>
{data.map((v, i) => (
<View key={i} style={[styles.bar, { height: Math.max(6, (v / Math.max(1, max)) * 64), backgroundColor: color }]} />
))}
</View>
);
};
const Progress: React.FC<{ value: number; color: string; fonts: any }> = ({ value, color, fonts }) => {
return (
<View style={styles.progressWrap}>
<View style={styles.progressTrack}>
<View style={[styles.progressFill, { width: `${value}%`, backgroundColor: color }]} />
</View>
<Text style={{ fontSize: 12, marginTop: 6, fontFamily: fonts.medium }}>{value}%</Text>
</View>
);
};
const Stacked: React.FC<{ segments: { label: string; value: number; color: string }[]; total: number }> = ({ segments, total }) => {
return (
<View style={{ height: 12, borderRadius: 8, backgroundColor: '#E5E7EB', overflow: 'hidden', flexDirection: 'row', marginTop: 8 }}>
{segments.map(s => (
<View key={s.label} style={{ width: `${(s.value / Math.max(1, total)) * 100}%`, backgroundColor: s.color }} />
))}
</View>
);
};

View File

@ -0,0 +1,135 @@
import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import Icon from 'react-native-vector-icons/MaterialIcons';
import { useTheme } from '@/shared/styles/useTheme';
import { StackedBarChart } from '@/shared/components/charts';
interface BarChartWidgetProps {
data: Array<{
label: string;
value: number;
color: string;
}>;
title: string;
subtitle?: string;
onPress?: () => void;
height?: number;
}
const BarChartWidget: React.FC<BarChartWidgetProps> = ({
data,
title,
subtitle,
onPress,
height = 120,
}) => {
const { colors, fonts, spacing, shadows } = useTheme();
const maxValue = Math.max(...data.map(item => item.value));
const total = data.reduce((sum, item) => sum + item.value, 0);
const WidgetContent = () => (
<View style={[
styles.container,
{
backgroundColor: colors.surface,
borderColor: colors.border,
...shadows.light,
}
]}>
<View style={styles.header}>
<View style={styles.titleContainer}>
<Icon name="bar-chart" size={20} color={colors.primary} />
<View style={styles.titleTextContainer}>
<Text style={[styles.title, { color: colors.text, fontFamily: fonts.medium }]}>
{title}
</Text>
{subtitle && (
<Text style={[styles.subtitle, { color: colors.textLight, fontFamily: fonts.regular }]}>
{subtitle}
</Text>
)}
</View>
</View>
</View>
<View style={styles.content}>
<StackedBarChart
data={data}
colors={colors}
fonts={fonts}
height={height}
/>
</View>
<View style={styles.footer}>
<View style={styles.summaryContainer}>
<Text style={[styles.summaryLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>
Total: {total.toLocaleString()}
</Text>
<Text style={[styles.summaryLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>
Max: {maxValue.toLocaleString()}
</Text>
</View>
</View>
</View>
);
if (onPress) {
return (
<TouchableOpacity onPress={onPress} activeOpacity={0.8}>
<WidgetContent />
</TouchableOpacity>
);
}
return <WidgetContent />;
};
const styles = StyleSheet.create({
container: {
borderRadius: 12,
borderWidth: 1,
padding: 16,
marginBottom: 12,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 16,
},
titleContainer: {
flexDirection: 'row',
alignItems: 'center',
},
titleTextContainer: {
marginLeft: 8,
},
title: {
fontSize: 16,
},
subtitle: {
fontSize: 12,
marginTop: 2,
},
content: {
alignItems: 'center',
justifyContent: 'center',
},
footer: {
marginTop: 12,
paddingTop: 12,
borderTopWidth: 1,
borderTopColor: '#E5E7EB',
},
summaryContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
},
summaryLabel: {
fontSize: 12,
},
});
export default BarChartWidget;

View File

@ -0,0 +1,162 @@
import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import Icon from 'react-native-vector-icons/MaterialIcons';
import { useTheme } from '@/shared/styles/useTheme';
interface KPIWidgetProps {
title: string;
value: string | number;
subtitle?: string;
icon: string;
color: string;
backgroundColor?: string;
onPress?: () => void;
trend?: {
value: number;
isPositive: boolean;
};
}
const KPIWidget: React.FC<KPIWidgetProps> = ({
title,
value,
subtitle,
icon,
color,
backgroundColor,
onPress,
trend,
}) => {
const { colors, fonts, spacing, shadows } = useTheme();
const formatValue = (val: string | number): string => {
if (typeof val === 'number') {
return val.toLocaleString();
}
return val;
};
const WidgetContent = () => (
<View style={[
styles.container,
{
backgroundColor: backgroundColor || colors.surface,
borderColor: colors.border,
...shadows.light,
}
]}>
<View style={styles.header}>
<View style={styles.iconContainer}>
<Icon name={icon} size={20} color={color} />
</View>
{trend && (
<View style={[
styles.trendContainer,
{ backgroundColor: trend.isPositive ? '#E9FAF2' : '#FEF2F2' }
]}>
<Icon
name={trend.isPositive ? 'trending-up' : 'trending-down'}
size={12}
color={trend.isPositive ? '#10B981' : '#EF4444'}
/>
<Text style={[
styles.trendText,
{
color: trend.isPositive ? '#10B981' : '#EF4444',
fontFamily: fonts.medium,
}
]}>
{Math.abs(trend.value)}%
</Text>
</View>
)}
</View>
<View style={styles.content}>
<Text style={[
styles.title,
{ color: colors.textLight, fontFamily: fonts.regular }
]}>
{title}
</Text>
<Text style={[
styles.value,
{ color: colors.text, fontFamily: fonts.bold }
]}>
{formatValue(value)}
</Text>
{subtitle && (
<Text style={[
styles.subtitle,
{ color: colors.textLight, fontFamily: fonts.regular }
]}>
{subtitle}
</Text>
)}
</View>
</View>
);
if (onPress) {
return (
<TouchableOpacity onPress={onPress} activeOpacity={0.8}>
<WidgetContent />
</TouchableOpacity>
);
}
return <WidgetContent />;
};
const styles = StyleSheet.create({
container: {
borderRadius: 12,
borderWidth: 1,
padding: 16,
marginBottom: 12,
minHeight: 120,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
},
iconContainer: {
width: 36,
height: 36,
borderRadius: 8,
backgroundColor: 'rgba(0, 0, 0, 0.05)',
justifyContent: 'center',
alignItems: 'center',
},
trendContainer: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 6,
paddingVertical: 2,
borderRadius: 4,
},
trendText: {
fontSize: 10,
marginLeft: 2,
},
content: {
flex: 1,
},
title: {
fontSize: 12,
marginBottom: 4,
},
value: {
fontSize: 24,
marginBottom: 4,
},
subtitle: {
fontSize: 11,
},
});
export default KPIWidget;

View File

@ -0,0 +1,227 @@
import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity, ScrollView } from 'react-native';
import Icon from 'react-native-vector-icons/MaterialIcons';
import { useTheme } from '@/shared/styles/useTheme';
interface ListItem {
id: string;
title: string;
subtitle?: string;
value: string | number;
icon?: string;
color?: string;
}
interface ListWidgetProps {
title: string;
data: ListItem[];
onItemPress?: (item: ListItem) => void;
onViewAllPress?: () => void;
maxItems?: number;
showValue?: boolean;
emptyMessage?: string;
}
const ListWidget: React.FC<ListWidgetProps> = ({
title,
data,
onItemPress,
onViewAllPress,
maxItems = 5,
showValue = true,
emptyMessage = 'No data available',
}) => {
const { colors, fonts, spacing, shadows } = useTheme();
const displayData = data.slice(0, maxItems);
const formatValue = (value: string | number): string => {
if (typeof value === 'number') {
return value.toLocaleString();
}
return value;
};
return (
<View style={[
styles.container,
{
backgroundColor: colors.surface,
borderColor: colors.border,
...shadows.light,
}
]}>
<View style={styles.header}>
<Text style={[styles.title, { color: colors.text, fontFamily: fonts.medium }]}>
{title}
</Text>
{onViewAllPress && data.length > maxItems && (
<TouchableOpacity onPress={onViewAllPress} style={styles.viewAllButton}>
<Text style={[styles.viewAllText, { color: colors.primary, fontFamily: fonts.medium }]}>
View All
</Text>
<Icon name="chevron-right" size={16} color={colors.primary} />
</TouchableOpacity>
)}
</View>
<View style={styles.content}>
{displayData.length === 0 ? (
<View style={styles.emptyContainer}>
<Icon name="inbox" size={32} color={colors.textLight} />
<Text style={[styles.emptyText, { color: colors.textLight, fontFamily: fonts.regular }]}>
{emptyMessage}
</Text>
</View>
) : (
<ScrollView showsVerticalScrollIndicator={false}>
{displayData.map((item, index) => (
<TouchableOpacity
key={item.id}
style={[
styles.listItem,
index === displayData.length - 1 && styles.lastListItem,
{ borderBottomColor: colors.border }
]}
onPress={() => onItemPress?.(item)}
disabled={!onItemPress}
>
<View style={styles.itemContent}>
{item.icon && (
<View style={[
styles.iconContainer,
{ backgroundColor: item.color ? `${item.color}20` : `${colors.primary}20` }
]}>
<Icon
name={item.icon}
size={16}
color={item.color || colors.primary}
/>
</View>
)}
<View style={styles.textContainer}>
<Text style={[styles.itemTitle, { color: colors.text, fontFamily: fonts.medium }]}>
{item.title}
</Text>
{item.subtitle && (
<Text style={[styles.itemSubtitle, { color: colors.textLight, fontFamily: fonts.regular }]}>
{item.subtitle}
</Text>
)}
</View>
</View>
{showValue && (
<View style={styles.valueContainer}>
<Text style={[styles.itemValue, { color: colors.text, fontFamily: fonts.bold }]}>
{formatValue(item.value)}
</Text>
</View>
)}
</TouchableOpacity>
))}
</ScrollView>
)}
</View>
{data.length > maxItems && !onViewAllPress && (
<View style={styles.footer}>
<Text style={[styles.footerText, { color: colors.textLight, fontFamily: fonts.regular }]}>
+{data.length - maxItems} more items
</Text>
</View>
)}
</View>
);
};
const styles = StyleSheet.create({
container: {
borderRadius: 12,
borderWidth: 1,
padding: 16,
marginBottom: 12,
maxHeight: 300,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 16,
},
title: {
fontSize: 16,
},
viewAllButton: {
flexDirection: 'row',
alignItems: 'center',
},
viewAllText: {
fontSize: 12,
marginRight: 4,
},
content: {
flex: 1,
},
emptyContainer: {
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 32,
},
emptyText: {
fontSize: 14,
marginTop: 8,
textAlign: 'center',
},
listItem: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: 12,
borderBottomWidth: StyleSheet.hairlineWidth,
},
lastListItem: {
borderBottomWidth: 0,
},
itemContent: {
flexDirection: 'row',
alignItems: 'center',
flex: 1,
},
iconContainer: {
width: 32,
height: 32,
borderRadius: 6,
justifyContent: 'center',
alignItems: 'center',
marginRight: 12,
},
textContainer: {
flex: 1,
},
itemTitle: {
fontSize: 14,
marginBottom: 2,
},
itemSubtitle: {
fontSize: 12,
},
valueContainer: {
marginLeft: 12,
},
itemValue: {
fontSize: 14,
},
footer: {
paddingTop: 12,
borderTopWidth: 1,
borderTopColor: '#E5E7EB',
alignItems: 'center',
},
footerText: {
fontSize: 12,
},
});
export default ListWidget;

View File

@ -0,0 +1,166 @@
import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import Icon from 'react-native-vector-icons/MaterialIcons';
import { useTheme } from '@/shared/styles/useTheme';
import { PieChart, DonutChart } from '@/shared/components/charts';
interface RevenueChartProps {
data: Array<{
label: string;
value: number;
color: string;
}>;
title: string;
total?: number;
chartType?: 'pie' | 'donut';
onPress?: () => void;
}
const RevenueChart: React.FC<RevenueChartProps> = ({
data,
title,
total,
chartType = 'donut',
onPress,
}) => {
const { colors, fonts, spacing, shadows } = useTheme();
const calculatedTotal = total || data.reduce((sum, item) => sum + item.value, 0);
const WidgetContent = () => (
<View style={[
styles.container,
{
backgroundColor: colors.surface,
borderColor: colors.border,
...shadows.light,
}
]}>
<View style={styles.header}>
<View style={styles.titleContainer}>
<Icon name="pie-chart" size={20} color={colors.primary} />
<Text style={[styles.title, { color: colors.text, fontFamily: fonts.medium }]}>
{title}
</Text>
</View>
</View>
<View style={styles.content}>
<View style={styles.chartContainer}>
{chartType === 'pie' ? (
<PieChart
data={data}
size={140}
colors={colors}
fonts={fonts}
/>
) : (
<DonutChart
data={data}
size={140}
colors={colors}
fonts={fonts}
/>
)}
</View>
<View style={styles.legendContainer}>
{data.map((item, index) => (
<View key={index} style={styles.legendItem}>
<View style={[styles.legendDot, { backgroundColor: item.color }]} />
<Text style={[styles.legendLabel, { color: colors.text, fontFamily: fonts.regular }]}>
{item.label}
</Text>
<Text style={[styles.legendValue, { color: colors.text, fontFamily: fonts.medium }]}>
{item.value}
</Text>
</View>
))}
</View>
</View>
{calculatedTotal > 0 && (
<View style={styles.footer}>
<Text style={[styles.totalLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>
Total: {calculatedTotal.toLocaleString()}
</Text>
</View>
)}
</View>
);
if (onPress) {
return (
<TouchableOpacity onPress={onPress} activeOpacity={0.8}>
<WidgetContent />
</TouchableOpacity>
);
}
return <WidgetContent />;
};
const styles = StyleSheet.create({
container: {
borderRadius: 12,
borderWidth: 1,
padding: 16,
marginBottom: 12,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 16,
},
titleContainer: {
flexDirection: 'row',
alignItems: 'center',
},
title: {
fontSize: 16,
marginLeft: 8,
},
content: {
flexDirection: 'row',
alignItems: 'center',
},
chartContainer: {
flex: 1,
alignItems: 'center',
},
legendContainer: {
flex: 1,
paddingLeft: 16,
},
legendItem: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 8,
},
legendDot: {
width: 8,
height: 8,
borderRadius: 4,
marginRight: 8,
},
legendLabel: {
fontSize: 12,
flex: 1,
},
legendValue: {
fontSize: 12,
},
footer: {
marginTop: 12,
paddingTop: 12,
borderTopWidth: 1,
borderTopColor: '#E5E7EB',
},
totalLabel: {
fontSize: 12,
textAlign: 'center',
},
});
export default RevenueChart;

View File

@ -0,0 +1,4 @@
export { default as KPIWidget } from './KPIWidget';
export { default as RevenueChart } from './RevenueChart';
export { default as BarChartWidget } from './BarChartWidget';
export { default as ListWidget } from './ListWidget';

View File

@ -0,0 +1,25 @@
// Zoho Books Module Exports
export { default as ZohoBooksDashboardScreen } from './screens/ZohoBooksDashboardScreen';
export { default as ZohoBooksDataScreen } from './screens/ZohoBooksDataScreen';
export { default as zohoBooksSlice } from './store/zohoBooksSlice';
export {
selectZohoBooksLoading,
selectZohoBooksError,
selectZohoBooksKPIData,
selectCustomers,
selectVendors,
selectActiveInvoices,
selectPaidInvoices,
selectDraftInvoices,
selectTopCustomersByOutstanding,
selectTopVendorsByOutstanding,
selectInvoiceStatusDistribution,
} from './store/selectors';
export * from './types/ZohoBooksTypes';
export { zohoBooksAPI } from './services/zohoBooksAPI';
export {
KPIWidget,
RevenueChart,
BarChartWidget,
ListWidget,
} from './components/widgets';

View File

@ -0,0 +1,425 @@
import React, { useEffect, useMemo, useState } from 'react';
import {
View,
Text,
ScrollView,
RefreshControl,
StyleSheet,
TouchableOpacity,
} from 'react-native';
import { useSelector, useDispatch } from 'react-redux';
import { useNavigation } from '@react-navigation/native';
import type { AppDispatch } from '@/store/store';
import Icon from 'react-native-vector-icons/MaterialIcons';
import { Container, LoadingSpinner, ErrorState } from '@/shared/components/ui';
import { useTheme } from '@/shared/styles/useTheme';
import { fetchZohoBooksData } from '../store/zohoBooksSlice';
import {
selectZohoBooksLoading,
selectZohoBooksError,
selectZohoBooksKPIData,
selectCustomers,
selectVendors,
selectActiveInvoices,
selectPaidInvoices,
selectDraftInvoices,
selectTopCustomersByOutstanding,
selectTopVendorsByOutstanding,
selectInvoiceStatusDistribution,
} from '../store/selectors';
import {
KPIWidget,
RevenueChart,
BarChartWidget,
ListWidget,
} from '../components/widgets';
const ZohoBooksDashboardScreen: React.FC = () => {
const { colors, fonts, spacing, shadows } = useTheme();
const navigation = useNavigation();
const [refreshing, setRefreshing] = useState(false);
// Redux
const dispatch = useDispatch<AppDispatch>();
const loading = useSelector(selectZohoBooksLoading);
const error = useSelector(selectZohoBooksError);
const kpiData = useSelector(selectZohoBooksKPIData);
const customers = useSelector(selectCustomers);
const vendors = useSelector(selectVendors);
const activeInvoices = useSelector(selectActiveInvoices);
const paidInvoices = useSelector(selectPaidInvoices);
const draftInvoices = useSelector(selectDraftInvoices);
const topCustomers = useSelector(selectTopCustomersByOutstanding);
const topVendors = useSelector(selectTopVendorsByOutstanding);
const invoiceStatusDistribution = useSelector(selectInvoiceStatusDistribution);
// Effects
useEffect(() => {
dispatch(fetchZohoBooksData());
}, [dispatch]);
// Handlers
const handleRefresh = async () => {
setRefreshing(true);
try {
await dispatch(fetchZohoBooksData()).unwrap();
} catch (error) {
console.error('Error refreshing data:', error);
} finally {
setRefreshing(false);
}
};
const handleRetry = () => {
dispatch(fetchZohoBooksData());
};
// Computed data for charts
const invoiceStatusData = useMemo(() => {
return invoiceStatusDistribution.map(item => ({
label: item.label,
value: item.value,
color: item.color,
}));
}, [invoiceStatusDistribution]);
const customerVsVendorData = useMemo(() => {
return [
{ label: 'Customers', value: kpiData.totalCustomers, color: colors.chartPrimary },
{ label: 'Vendors', value: kpiData.totalVendors, color: colors.chartSecondary },
];
}, [kpiData.totalCustomers, kpiData.totalVendors, colors]);
const revenueVsExpensesData = useMemo(() => {
return [
{ label: 'Revenue', value: Math.round(kpiData.totalRevenue / 1000), color: colors.success },
{ label: 'Expenses', value: Math.round(kpiData.totalExpensesAmount / 1000), color: colors.error },
];
}, [kpiData.totalRevenue, kpiData.totalExpensesAmount, colors]);
// Top customers list data
const topCustomersListData = useMemo(() => {
return topCustomers.map(customer => ({
id: customer.contact_id,
title: customer.contact_name,
subtitle: customer.company_name || 'Individual',
value: `${customer.outstanding_receivable_amount.toLocaleString()}`,
icon: 'person',
color: colors.primary,
}));
}, [topCustomers, colors]);
// Top vendors list data
const topVendorsListData = useMemo(() => {
return topVendors.map(vendor => ({
id: vendor.contact_id,
title: vendor.contact_name,
subtitle: vendor.company_name || 'Individual',
value: `${vendor.outstanding_payable_amount.toLocaleString()}`,
icon: 'business',
color: colors.secondary,
}));
}, [topVendors, colors]);
// Loading state
if (loading && !kpiData.totalCustomers && !kpiData.totalVendors) {
return <LoadingSpinner />;
}
// Error state
if (error) {
return <ErrorState onRetry={handleRetry} />;
}
return (
<Container>
<ScrollView
style={[styles.container, { backgroundColor: colors.background }]}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
}
showsVerticalScrollIndicator={false}
>
{/* Header */}
<View style={styles.header}>
<View style={styles.headerContent}>
<Icon name="account-balance-wallet" size={28} color={colors.primary} />
<Text style={[styles.title, { color: colors.text, fontFamily: fonts.bold }]}>
Zoho Books Dashboard
</Text>
</View>
<TouchableOpacity
style={styles.dataButton}
onPress={() => navigation.navigate('ZohoBooksData' as never)}
>
<Icon name="list" size={20} color={colors.primary} />
<Text style={[styles.dataButtonText, { color: colors.primary, fontFamily: fonts.medium }]}>
View Data
</Text>
</TouchableOpacity>
</View>
{/* KPI Cards Row 1 */}
<View style={styles.kpiRow}>
<View style={styles.kpiColumn}>
<KPIWidget
title="Total Customers"
value={kpiData.totalCustomers}
icon="people"
color={colors.primary}
/>
</View>
<View style={styles.kpiColumn}>
<KPIWidget
title="Total Vendors"
value={kpiData.totalVendors}
icon="business"
color={colors.secondary}
/>
</View>
</View>
{/* KPI Cards Row 2 */}
<View style={styles.kpiRow}>
<View style={styles.kpiColumn}>
<KPIWidget
title="Outstanding Receivables"
value={`${kpiData.outstandingReceivables.toLocaleString()}`}
icon="trending-up"
color={colors.success}
/>
</View>
<View style={styles.kpiColumn}>
<KPIWidget
title="Outstanding Payables"
value={`${kpiData.outstandingPayables.toLocaleString()}`}
icon="trending-down"
color={colors.error}
/>
</View>
</View>
{/* KPI Cards Row 3 */}
<View style={styles.kpiRow}>
<View style={styles.kpiColumn}>
<KPIWidget
title="Total Revenue"
value={`${kpiData.totalRevenue.toLocaleString()}`}
icon="attach-money"
color={colors.chartTertiary}
/>
</View>
<View style={styles.kpiColumn}>
<KPIWidget
title="Total Expenses"
value={`${kpiData.totalExpensesAmount.toLocaleString()}`}
icon="receipt"
color={colors.warning}
/>
</View>
</View>
{/* Charts Row */}
<View style={styles.chartsRow}>
<View style={styles.chartColumn}>
<RevenueChart
title="Invoice Status"
data={invoiceStatusData}
chartType="donut"
/>
</View>
<View style={styles.chartColumn}>
<RevenueChart
title="Customers vs Vendors"
data={customerVsVendorData}
chartType="pie"
/>
</View>
</View>
{/* Bar Chart */}
<BarChartWidget
title="Revenue vs Expenses"
subtitle="Amount in thousands"
data={revenueVsExpensesData}
/>
{/* Lists Row */}
<View style={styles.listsRow}>
<View style={styles.listColumn}>
<ListWidget
title="Top Customers"
data={topCustomersListData}
maxItems={4}
emptyMessage="No customer data available"
/>
</View>
<View style={styles.listColumn}>
<ListWidget
title="Top Vendors"
data={topVendorsListData}
maxItems={4}
emptyMessage="No vendor data available"
/>
</View>
</View>
{/* Summary Cards */}
<View style={styles.summaryRow}>
<View style={[styles.summaryCard, { backgroundColor: colors.surface, borderColor: colors.border }]}>
<View style={styles.summaryHeader}>
<Icon name="description" size={20} color={colors.primary} />
<Text style={[styles.summaryTitle, { color: colors.text, fontFamily: fonts.medium }]}>
Documents
</Text>
</View>
<View style={styles.summaryContent}>
<View style={styles.summaryItem}>
<Text style={[styles.summaryLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>
Invoices
</Text>
<Text style={[styles.summaryValue, { color: colors.text, fontFamily: fonts.bold }]}>
{kpiData.totalInvoices}
</Text>
</View>
<View style={styles.summaryItem}>
<Text style={[styles.summaryLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>
Sales Orders
</Text>
<Text style={[styles.summaryValue, { color: colors.text, fontFamily: fonts.bold }]}>
{kpiData.totalSalesOrders}
</Text>
</View>
<View style={styles.summaryItem}>
<Text style={[styles.summaryLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>
Purchase Orders
</Text>
<Text style={[styles.summaryValue, { color: colors.text, fontFamily: fonts.bold }]}>
{kpiData.totalPurchaseOrders}
</Text>
</View>
<View style={styles.summaryItem}>
<Text style={[styles.summaryLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>
Bills
</Text>
<Text style={[styles.summaryValue, { color: colors.text, fontFamily: fonts.bold }]}>
{kpiData.totalBills}
</Text>
</View>
</View>
</View>
</View>
{/* Bottom spacing */}
<View style={styles.bottomSpacing} />
</ScrollView>
</Container>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: 16,
paddingBottom: 8,
},
headerContent: {
flexDirection: 'row',
alignItems: 'center',
},
title: {
fontSize: 24,
marginLeft: 12,
},
dataButton: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 12,
paddingVertical: 8,
borderRadius: 8,
backgroundColor: 'rgba(44, 95, 74, 0.1)',
borderWidth: 1,
borderColor: 'rgba(44, 95, 74, 0.3)',
},
dataButtonText: {
fontSize: 14,
marginLeft: 6,
},
kpiRow: {
flexDirection: 'row',
paddingHorizontal: 16,
marginBottom: 8,
},
kpiColumn: {
flex: 1,
marginHorizontal: 4,
},
chartsRow: {
flexDirection: 'column',
paddingHorizontal: 16,
marginBottom: 8,
},
chartColumn: {
marginVertical: 4,
},
listsRow: {
flexDirection: 'row',
paddingHorizontal: 16,
marginBottom: 8,
},
listColumn: {
flex: 1,
marginHorizontal: 4,
},
summaryRow: {
paddingHorizontal: 16,
marginBottom: 8,
},
summaryCard: {
borderRadius: 12,
borderWidth: 1,
padding: 16,
...StyleSheet.flatten({
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
}),
},
summaryHeader: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 16,
},
summaryTitle: {
fontSize: 16,
marginLeft: 8,
},
summaryContent: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
},
summaryItem: {
width: '48%',
marginBottom: 12,
},
summaryLabel: {
fontSize: 12,
marginBottom: 4,
},
summaryValue: {
fontSize: 18,
},
bottomSpacing: {
height: 20,
},
});
export default ZohoBooksDashboardScreen;

View File

@ -0,0 +1,736 @@
import React, { useState, useEffect, useMemo } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
TouchableOpacity,
RefreshControl,
FlatList,
} from 'react-native';
import { useSelector, useDispatch } from 'react-redux';
import type { AppDispatch } from '@/store/store';
import Icon from 'react-native-vector-icons/MaterialIcons';
import { Container, LoadingSpinner, ErrorState } from '@/shared/components/ui';
import { useTheme } from '@/shared/styles/useTheme';
import { fetchZohoBooksData } from '../store/zohoBooksSlice';
import {
selectZohoBooksLoading,
selectZohoBooksError,
selectZohoBooksContacts,
selectZohoBooksInvoices,
selectZohoBooksExpenses,
selectZohoBooksSalesOrders,
selectZohoBooksPurchaseOrders,
selectZohoBooksBills,
selectCustomers,
selectVendors,
} from '../store/selectors';
import type { RootState } from '@/store/store';
const ZohoBooksDataScreen: React.FC = () => {
const { colors, fonts, spacing, shadows } = useTheme();
const dispatch = useDispatch<AppDispatch>();
const [selectedTab, setSelectedTab] = useState<'contacts' | 'customers' | 'vendors' | 'invoices' | 'expenses' | 'salesOrders' | 'purchaseOrders' | 'bills'>('contacts');
const [refreshing, setRefreshing] = useState(false);
// Redux selectors
const contacts = useSelector(selectZohoBooksContacts);
const invoices = useSelector(selectZohoBooksInvoices);
const expenses = useSelector(selectZohoBooksExpenses);
const salesOrders = useSelector(selectZohoBooksSalesOrders);
const purchaseOrders = useSelector(selectZohoBooksPurchaseOrders);
const bills = useSelector(selectZohoBooksBills);
const customers = useSelector(selectCustomers);
const vendors = useSelector(selectVendors);
const loading = useSelector(selectZohoBooksLoading);
const error = useSelector(selectZohoBooksError);
// Fetch data using Redux
const fetchData = async (showRefresh = false) => {
try {
if (showRefresh) {
setRefreshing(true);
}
await dispatch(fetchZohoBooksData()).unwrap();
} catch (err) {
console.error('Error fetching Zoho Books data:', err);
} finally {
setRefreshing(false);
}
};
useEffect(() => {
fetchData();
}, []);
const handleRefresh = () => {
fetchData(true);
};
const handleRetry = () => {
fetchData();
};
const handleItemPress = (item: any, type: string) => {
console.log(`Viewing ${type}:`, item);
};
// Loading state
if (loading && !contacts.length && !invoices.length) {
return <LoadingSpinner />;
}
// Error state
if (error) {
return <ErrorState onRetry={handleRetry} />;
}
// Tab configuration
const tabs = [
{ key: 'contacts', label: 'All Contacts', icon: 'contacts', count: contacts.length },
{ key: 'customers', label: 'Customers', icon: 'person', count: customers.length },
{ key: 'vendors', label: 'Vendors', icon: 'business', count: vendors.length },
{ key: 'invoices', label: 'Invoices', icon: 'receipt', count: invoices.length },
{ key: 'expenses', label: 'Expenses', icon: 'money-off', count: expenses.length },
{ key: 'salesOrders', label: 'Sales Orders', icon: 'shopping-cart', count: salesOrders.length },
{ key: 'purchaseOrders', label: 'Purchase Orders', icon: 'add-shopping-cart', count: purchaseOrders.length },
{ key: 'bills', label: 'Bills', icon: 'description', count: bills.length },
] as const;
const renderContactCard = (item: any) => (
<View style={[styles.card, { backgroundColor: colors.surface, borderColor: colors.border }]}>
<View style={styles.cardHeader}>
<View style={styles.cardTitleContainer}>
<Icon name="person" size={20} color={colors.primary} />
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.bold }]}>
{item.contact_name}
</Text>
</View>
<View style={[styles.statusBadge, { backgroundColor: item.status === 'active' ? '#E9FAF2' : '#FEF2F2' }]}>
<Text style={[styles.statusText, { color: item.status === 'active' ? '#10B981' : '#EF4444', fontFamily: fonts.medium }]}>
{item.status}
</Text>
</View>
</View>
<View style={styles.cardContent}>
<View style={styles.infoRow}>
<Text style={[styles.infoLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Type:</Text>
<Text style={[styles.infoValue, { color: colors.text, fontFamily: fonts.medium }]}>
{item.contact_type_formatted}
</Text>
</View>
{item.email && (
<View style={styles.infoRow}>
<Text style={[styles.infoLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Email:</Text>
<Text style={[styles.infoValue, { color: colors.text, fontFamily: fonts.medium }]}>{item.email}</Text>
</View>
)}
{item.phone && (
<View style={styles.infoRow}>
<Text style={[styles.infoLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Phone:</Text>
<Text style={[styles.infoValue, { color: colors.text, fontFamily: fonts.medium }]}>{item.phone}</Text>
</View>
)}
<View style={styles.infoRow}>
<Text style={[styles.infoLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Currency:</Text>
<Text style={[styles.infoValue, { color: colors.text, fontFamily: fonts.medium }]}>{item.currency_code}</Text>
</View>
<View style={styles.amountRow}>
<View style={styles.amountItem}>
<Text style={[styles.amountLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Outstanding Receivable:</Text>
<Text style={[styles.amountValue, { color: '#10B981', fontFamily: fonts.bold }]}>
{item.outstanding_receivable_amount.toLocaleString()}
</Text>
</View>
<View style={styles.amountItem}>
<Text style={[styles.amountLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Outstanding Payable:</Text>
<Text style={[styles.amountValue, { color: '#EF4444', fontFamily: fonts.bold }]}>
{item.outstanding_payable_amount.toLocaleString()}
</Text>
</View>
</View>
</View>
</View>
);
const renderInvoiceCard = (item: any) => (
<View style={[styles.card, { backgroundColor: colors.surface, borderColor: colors.border }]}>
<View style={styles.cardHeader}>
<View style={styles.cardTitleContainer}>
<Icon name="receipt" size={20} color={colors.primary} />
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.bold }]}>
{item.invoice_number}
</Text>
</View>
<View style={[styles.statusBadge, { backgroundColor: getStatusColor(item.status) }]}>
<Text style={[styles.statusText, { color: colors.text, fontFamily: fonts.medium }]}>
{item.status}
</Text>
</View>
</View>
<View style={styles.cardContent}>
<View style={styles.infoRow}>
<Text style={[styles.infoLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Customer:</Text>
<Text style={[styles.infoValue, { color: colors.text, fontFamily: fonts.medium }]}>
{item.customer_name}
</Text>
</View>
<View style={styles.infoRow}>
<Text style={[styles.infoLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Date:</Text>
<Text style={[styles.infoValue, { color: colors.text, fontFamily: fonts.medium }]}>{item.date}</Text>
</View>
<View style={styles.infoRow}>
<Text style={[styles.infoLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Due Date:</Text>
<Text style={[styles.infoValue, { color: colors.text, fontFamily: fonts.medium }]}>{item.due_date}</Text>
</View>
<View style={styles.amountRow}>
<View style={styles.amountItem}>
<Text style={[styles.amountLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Total:</Text>
<Text style={[styles.amountValue, { color: colors.primary, fontFamily: fonts.bold }]}>
{item.currency_symbol}{item.total.toLocaleString()}
</Text>
</View>
<View style={styles.amountItem}>
<Text style={[styles.amountLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Balance:</Text>
<Text style={[styles.amountValue, { color: item.balance > 0 ? '#EF4444' : '#10B981', fontFamily: fonts.bold }]}>
{item.currency_symbol}{item.balance.toLocaleString()}
</Text>
</View>
</View>
</View>
</View>
);
const renderExpenseCard = (item: any) => (
<View style={[styles.card, { backgroundColor: colors.surface, borderColor: colors.border }]}>
<View style={styles.cardHeader}>
<View style={styles.cardTitleContainer}>
<Icon name="money-off" size={20} color={colors.primary} />
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.bold }]}>
{item.reference_number || 'Expense'}
</Text>
</View>
<View style={[styles.statusBadge, { backgroundColor: item.is_billable ? '#E9FAF2' : '#FEF2F2' }]}>
<Text style={[styles.statusText, { color: item.is_billable ? '#10B981' : '#EF4444', fontFamily: fonts.medium }]}>
{item.status}
</Text>
</View>
</View>
<View style={styles.cardContent}>
<View style={styles.infoRow}>
<Text style={[styles.infoLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Account:</Text>
<Text style={[styles.infoValue, { color: colors.text, fontFamily: fonts.medium }]}>
{item.account_name}
</Text>
</View>
<View style={styles.infoRow}>
<Text style={[styles.infoLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Date:</Text>
<Text style={[styles.infoValue, { color: colors.text, fontFamily: fonts.medium }]}>{item.date}</Text>
</View>
{item.vendor_name && (
<View style={styles.infoRow}>
<Text style={[styles.infoLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Vendor:</Text>
<Text style={[styles.infoValue, { color: colors.text, fontFamily: fonts.medium }]}>{item.vendor_name}</Text>
</View>
)}
<View style={styles.amountRow}>
<View style={styles.amountItem}>
<Text style={[styles.amountLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Total:</Text>
<Text style={[styles.amountValue, { color: '#EF4444', fontFamily: fonts.bold }]}>
{item.currency_code} {item.total.toLocaleString()}
</Text>
</View>
<View style={styles.amountItem}>
<Text style={[styles.amountLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Billable:</Text>
<Text style={[styles.amountValue, { color: item.is_billable ? '#10B981' : '#6B7280', fontFamily: fonts.bold }]}>
{item.is_billable ? 'Yes' : 'No'}
</Text>
</View>
</View>
</View>
</View>
);
const renderSalesOrderCard = (item: any) => (
<View style={[styles.card, { backgroundColor: colors.surface, borderColor: colors.border }]}>
<View style={styles.cardHeader}>
<View style={styles.cardTitleContainer}>
<Icon name="shopping-cart" size={20} color={colors.primary} />
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.bold }]}>
{item.salesorder_number}
</Text>
</View>
<View style={[styles.statusBadge, { backgroundColor: getStatusColor(item.status) }]}>
<Text style={[styles.statusText, { color: colors.text, fontFamily: fonts.medium }]}>
{item.status}
</Text>
</View>
</View>
<View style={styles.cardContent}>
<View style={styles.infoRow}>
<Text style={[styles.infoLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Customer:</Text>
<Text style={[styles.infoValue, { color: colors.text, fontFamily: fonts.medium }]}>
{item.customer_name}
</Text>
</View>
<View style={styles.infoRow}>
<Text style={[styles.infoLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Date:</Text>
<Text style={[styles.infoValue, { color: colors.text, fontFamily: fonts.medium }]}>{item.date}</Text>
</View>
<View style={styles.infoRow}>
<Text style={[styles.infoLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Shipment Date:</Text>
<Text style={[styles.infoValue, { color: colors.text, fontFamily: fonts.medium }]}>{item.shipment_date}</Text>
</View>
<View style={styles.amountRow}>
<View style={styles.amountItem}>
<Text style={[styles.amountLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Total:</Text>
<Text style={[styles.amountValue, { color: colors.primary, fontFamily: fonts.bold }]}>
{item.currency_code} {item.total.toLocaleString()}
</Text>
</View>
<View style={styles.amountItem}>
<Text style={[styles.amountLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Invoiced:</Text>
<Text style={[styles.amountValue, { color: '#10B981', fontFamily: fonts.bold }]}>
{item.currency_code} {item.total_invoiced_amount.toLocaleString()}
</Text>
</View>
</View>
</View>
</View>
);
const renderPurchaseOrderCard = (item: any) => (
<View style={[styles.card, { backgroundColor: colors.surface, borderColor: colors.border }]}>
<View style={styles.cardHeader}>
<View style={styles.cardTitleContainer}>
<Icon name="add-shopping-cart" size={20} color={colors.primary} />
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.bold }]}>
{item.purchaseorder_number}
</Text>
</View>
<View style={[styles.statusBadge, { backgroundColor: getStatusColor(item.status) }]}>
<Text style={[styles.statusText, { color: colors.text, fontFamily: fonts.medium }]}>
{item.status}
</Text>
</View>
</View>
<View style={styles.cardContent}>
<View style={styles.infoRow}>
<Text style={[styles.infoLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Vendor:</Text>
<Text style={[styles.infoValue, { color: colors.text, fontFamily: fonts.medium }]}>
{item.vendor_name}
</Text>
</View>
<View style={styles.infoRow}>
<Text style={[styles.infoLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Date:</Text>
<Text style={[styles.infoValue, { color: colors.text, fontFamily: fonts.medium }]}>{item.date}</Text>
</View>
<View style={styles.infoRow}>
<Text style={[styles.infoLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Delivery Date:</Text>
<Text style={[styles.infoValue, { color: colors.text, fontFamily: fonts.medium }]}>{item.delivery_date}</Text>
</View>
<View style={styles.amountRow}>
<View style={styles.amountItem}>
<Text style={[styles.amountLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Total:</Text>
<Text style={[styles.amountValue, { color: colors.primary, fontFamily: fonts.bold }]}>
{item.currency_code} {item.total.toLocaleString()}
</Text>
</View>
<View style={styles.amountItem}>
<Text style={[styles.amountLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Quantity:</Text>
<Text style={[styles.amountValue, { color: '#10B981', fontFamily: fonts.bold }]}>
{item.quantity_yet_to_receive}
</Text>
</View>
</View>
</View>
</View>
);
const renderBillCard = (item: any) => (
<View style={[styles.card, { backgroundColor: colors.surface, borderColor: colors.border }]}>
<View style={styles.cardHeader}>
<View style={styles.cardTitleContainer}>
<Icon name="description" size={20} color={colors.primary} />
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.bold }]}>
{item.bill_number}
</Text>
</View>
<View style={[styles.statusBadge, { backgroundColor: getStatusColor(item.status) }]}>
<Text style={[styles.statusText, { color: colors.text, fontFamily: fonts.medium }]}>
{item.status}
</Text>
</View>
</View>
<View style={styles.cardContent}>
<View style={styles.infoRow}>
<Text style={[styles.infoLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Vendor:</Text>
<Text style={[styles.infoValue, { color: colors.text, fontFamily: fonts.medium }]}>
{item.vendor_name}
</Text>
</View>
<View style={styles.infoRow}>
<Text style={[styles.infoLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Date:</Text>
<Text style={[styles.infoValue, { color: colors.text, fontFamily: fonts.medium }]}>{item.date}</Text>
</View>
<View style={styles.infoRow}>
<Text style={[styles.infoLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Due Date:</Text>
<Text style={[styles.infoValue, { color: colors.text, fontFamily: fonts.medium }]}>{item.due_date}</Text>
</View>
<View style={styles.amountRow}>
<View style={styles.amountItem}>
<Text style={[styles.amountLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Total:</Text>
<Text style={[styles.amountValue, { color: colors.primary, fontFamily: fonts.bold }]}>
{item.currency_code} {item.total.toLocaleString()}
</Text>
</View>
<View style={styles.amountItem}>
<Text style={[styles.amountLabel, { color: colors.textLight, fontFamily: fonts.regular }]}>Balance:</Text>
<Text style={[styles.amountValue, { color: item.balance > 0 ? '#EF4444' : '#10B981', fontFamily: fonts.bold }]}>
{item.currency_code} {item.balance.toLocaleString()}
</Text>
</View>
</View>
</View>
</View>
);
const renderTabContent = () => {
const commonFlatListProps = {
showsVerticalScrollIndicator: false,
contentContainerStyle: styles.listContainer,
refreshControl: (
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
),
};
switch (selectedTab) {
case 'contacts':
return (
<FlatList
data={contacts}
renderItem={({ item }) => renderContactCard(item)}
keyExtractor={(item) => item.contact_id}
{...commonFlatListProps}
/>
);
case 'customers':
return (
<FlatList
data={customers}
renderItem={({ item }) => renderContactCard(item)}
keyExtractor={(item) => item.contact_id}
{...commonFlatListProps}
/>
);
case 'vendors':
return (
<FlatList
data={vendors}
renderItem={({ item }) => renderContactCard(item)}
keyExtractor={(item) => item.contact_id}
{...commonFlatListProps}
/>
);
case 'invoices':
return (
<FlatList
data={invoices}
renderItem={({ item }) => renderInvoiceCard(item)}
keyExtractor={(item) => item.invoice_id}
{...commonFlatListProps}
/>
);
case 'expenses':
return (
<FlatList
data={expenses}
renderItem={({ item }) => renderExpenseCard(item)}
keyExtractor={(item) => item.expense_id}
{...commonFlatListProps}
/>
);
case 'salesOrders':
return (
<FlatList
data={salesOrders}
renderItem={({ item }) => renderSalesOrderCard(item)}
keyExtractor={(item) => item.salesorder_id}
{...commonFlatListProps}
/>
);
case 'purchaseOrders':
return (
<FlatList
data={purchaseOrders}
renderItem={({ item }) => renderPurchaseOrderCard(item)}
keyExtractor={(item) => item.purchaseorder_id}
{...commonFlatListProps}
/>
);
case 'bills':
return (
<FlatList
data={bills}
renderItem={({ item }) => renderBillCard(item)}
keyExtractor={(item) => item.bill_id}
{...commonFlatListProps}
/>
);
default:
return null;
}
};
return (
<View style={[styles.container, { backgroundColor: colors.background }]}>
{/* Fixed Header */}
<View style={styles.header}>
<Text style={[styles.title, { color: colors.text, fontFamily: fonts.bold }]}>
Zoho Books Data
</Text>
<TouchableOpacity onPress={handleRefresh} disabled={refreshing}>
<Icon name="refresh" size={24} color={colors.primary} />
</TouchableOpacity>
</View>
{/* Fixed Tabs */}
<View style={styles.tabsContainer}>
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
<View style={styles.tabs}>
{tabs.map((tab) => (
<TouchableOpacity
key={tab.key}
style={[
styles.tab,
selectedTab === tab.key && { backgroundColor: colors.primary },
]}
onPress={() => setSelectedTab(tab.key)}
activeOpacity={0.8}
>
<Icon
name={tab.icon}
size={20}
color={selectedTab === tab.key ? colors.surface : colors.textLight}
/>
<Text
style={[
styles.tabText,
{
color: selectedTab === tab.key ? colors.surface : colors.textLight,
fontFamily: fonts.medium,
},
]}
>
{tab.label}
</Text>
<View
style={[
styles.countBadge,
{ backgroundColor: selectedTab === tab.key ? colors.surface : colors.primary },
]}
>
<Text
style={[
styles.countText,
{
color: selectedTab === tab.key ? colors.primary : colors.surface,
fontFamily: fonts.bold,
},
]}
>
{tab.count}
</Text>
</View>
</TouchableOpacity>
))}
</View>
</ScrollView>
</View>
{/* Scrollable Content */}
<View style={styles.content}>
{renderTabContent()}
</View>
</View>
);
};
// Helper function to get status colors
const getStatusColor = (status: string): string => {
const statusColors: Record<string, string> = {
active: '#E9FAF2',
inactive: '#FEF2F2',
draft: '#F3F4F6',
sent: '#E0F2FE',
paid: '#E9FAF2',
overdue: '#FEF2F2',
open: '#E0F2FE',
confirmed: '#E9FAF2',
received: '#E9FAF2',
cancelled: '#FEF2F2',
billable: '#E9FAF2',
nonbillable: '#FEF2F2',
};
return statusColors[status.toLowerCase()] || '#F3F4F6';
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 16,
paddingVertical: 12,
},
title: {
fontSize: 24,
},
tabsContainer: {
backgroundColor: '#FFFFFF',
paddingVertical: 12,
borderBottomWidth: 1,
borderBottomColor: '#E5E7EB',
},
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,
},
listContainer: {
paddingHorizontal: 16,
paddingBottom: 20,
},
card: {
borderRadius: 12,
borderWidth: 1,
padding: 16,
marginBottom: 12,
...StyleSheet.flatten({
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
}),
},
cardHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
},
cardTitleContainer: {
flexDirection: 'row',
alignItems: 'center',
flex: 1,
},
cardTitle: {
fontSize: 16,
marginLeft: 8,
},
statusBadge: {
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 12,
},
statusText: {
fontSize: 12,
textTransform: 'capitalize',
},
cardContent: {
gap: 8,
},
infoRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
infoLabel: {
fontSize: 14,
flex: 1,
},
infoValue: {
fontSize: 14,
textAlign: 'right',
flex: 1,
},
amountRow: {
flexDirection: 'row',
justifyContent: 'space-between',
marginTop: 8,
paddingTop: 8,
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: '#E5E7EB',
},
amountItem: {
flex: 1,
alignItems: 'center',
},
amountLabel: {
fontSize: 12,
marginBottom: 4,
},
amountValue: {
fontSize: 16,
fontWeight: 'bold',
},
});
export default ZohoBooksDataScreen;

View File

@ -0,0 +1,96 @@
import httpClient from '@/services/http';
import type {
ZohoBooksApiResponse,
ZohoBooksContact,
ZohoBooksInvoice,
ZohoBooksExpense,
ZohoBooksSalesOrder,
ZohoBooksPurchaseOrder,
ZohoBooksBill,
} from '../types/ZohoBooksTypes';
const BASE_URL = '/api/v1/integrations/zoho/books';
export const zohoBooksAPI = {
// Get all contacts (customers and vendors)
getContacts: async (): Promise<ZohoBooksApiResponse<ZohoBooksContact>> => {
const response = await httpClient.get(`${BASE_URL}/contacts?provider=zoho`);
console.log('contacts response in zoho books api',response)
return response.data;
},
// Get vendors only
getVendors: async (): Promise<ZohoBooksApiResponse<ZohoBooksContact>> => {
const response = await httpClient.get(`${BASE_URL}/vendors?provider=zoho`);
return response.data;
},
// Get customers only
getCustomers: async (): Promise<ZohoBooksApiResponse<ZohoBooksContact>> => {
const response = await httpClient.get(`${BASE_URL}/customers?provider=zoho`);
return response.data;
},
// Get invoices
getInvoices: async (): Promise<ZohoBooksApiResponse<ZohoBooksInvoice>> => {
const response = await httpClient.get(`${BASE_URL}/invoices?provider=zoho`);
return response.data;
},
// Get expenses
getExpenses: async (): Promise<ZohoBooksApiResponse<ZohoBooksExpense>> => {
const response = await httpClient.get(`${BASE_URL}/expenses?provider=zoho`);
return response.data;
},
// Get sales orders
getSalesOrders: async (): Promise<ZohoBooksApiResponse<ZohoBooksSalesOrder>> => {
const response = await httpClient.get(`${BASE_URL}/sales-orders?provider=zoho`);
return response.data;
},
// Get purchase orders
getPurchaseOrders: async (): Promise<ZohoBooksApiResponse<ZohoBooksPurchaseOrder>> => {
const response = await httpClient.get(`${BASE_URL}/purchase-orders?provider=zoho`);
return response.data;
},
// Get bills
getBills: async (): Promise<ZohoBooksApiResponse<ZohoBooksBill>> => {
const response = await httpClient.get(`${BASE_URL}/bills?provider=zoho`);
return response.data;
},
// Get all data for dashboard
getAllData: async () => {
try {
const [
contactsResponse,
invoicesResponse,
expensesResponse,
salesOrdersResponse,
purchaseOrdersResponse,
billsResponse,
] = await Promise.all([
zohoBooksAPI.getContacts(),
zohoBooksAPI.getInvoices(),
zohoBooksAPI.getExpenses(),
zohoBooksAPI.getSalesOrders(),
zohoBooksAPI.getPurchaseOrders(),
zohoBooksAPI.getBills(),
]);
return {
contacts: contactsResponse.data.data,
invoices: invoicesResponse.data.data,
expenses: expensesResponse.data.data,
salesOrders: salesOrdersResponse.data.data,
purchaseOrders: purchaseOrdersResponse.data.data,
bills: billsResponse.data.data,
};
} catch (error) {
console.error('Error fetching Zoho Books data:', error);
throw error;
}
},
};

View File

@ -0,0 +1,133 @@
import { createSelector } from '@reduxjs/toolkit';
import type { RootState } from '@/store/store';
import type { ZohoBooksContact, ZohoBooksInvoice } from '../types/ZohoBooksTypes';
// Base selectors
export const selectZohoBooksState = (state: RootState) => state.zohoBooks;
export const selectZohoBooksLoading = createSelector(
[selectZohoBooksState],
(state) => state.loading
);
export const selectZohoBooksError = createSelector(
[selectZohoBooksState],
(state) => state.error
);
export const selectZohoBooksLastUpdated = createSelector(
[selectZohoBooksState],
(state) => state.lastUpdated
);
// Data selectors
export const selectZohoBooksContacts = createSelector(
[selectZohoBooksState],
(state) => state.contacts
);
export const selectZohoBooksInvoices = createSelector(
[selectZohoBooksState],
(state) => state.invoices
);
export const selectZohoBooksExpenses = createSelector(
[selectZohoBooksState],
(state) => state.expenses
);
export const selectZohoBooksSalesOrders = createSelector(
[selectZohoBooksState],
(state) => state.salesOrders
);
export const selectZohoBooksPurchaseOrders = createSelector(
[selectZohoBooksState],
(state) => state.purchaseOrders
);
export const selectZohoBooksBills = createSelector(
[selectZohoBooksState],
(state) => state.bills
);
export const selectZohoBooksKPIData = createSelector(
[selectZohoBooksState],
(state) => state.kpiData
);
// Computed selectors
export const selectCustomers = createSelector(
[selectZohoBooksContacts],
(contacts) => contacts.filter((contact: ZohoBooksContact) => contact.contact_type === 'customer')
);
export const selectVendors = createSelector(
[selectZohoBooksContacts],
(contacts) => contacts.filter((contact: ZohoBooksContact) => contact.contact_type === 'vendor')
);
export const selectActiveInvoices = createSelector(
[selectZohoBooksInvoices],
(invoices) => invoices.filter((invoice: ZohoBooksInvoice) =>
invoice.status === 'sent' || invoice.status === 'overdue'
)
);
export const selectPaidInvoices = createSelector(
[selectZohoBooksInvoices],
(invoices) => invoices.filter((invoice: ZohoBooksInvoice) => invoice.status === 'paid')
);
export const selectDraftInvoices = createSelector(
[selectZohoBooksInvoices],
(invoices) => invoices.filter((invoice: ZohoBooksInvoice) => invoice.status === 'draft')
);
// Top customers by outstanding amount
export const selectTopCustomersByOutstanding = createSelector(
[selectCustomers],
(customers) =>
customers
.sort((a, b) => b.outstanding_receivable_amount - a.outstanding_receivable_amount)
.slice(0, 5)
);
// Top vendors by outstanding amount
export const selectTopVendorsByOutstanding = createSelector(
[selectVendors],
(vendors) =>
vendors
.sort((a, b) => b.outstanding_payable_amount - a.outstanding_payable_amount)
.slice(0, 5)
);
// Invoice status distribution
export const selectInvoiceStatusDistribution = createSelector(
[selectZohoBooksInvoices],
(invoices) => {
const distribution = invoices.reduce((acc, invoice) => {
acc[invoice.status] = (acc[invoice.status] || 0) + 1;
return acc;
}, {} as Record<string, number>);
return Object.entries(distribution).map(([status, count]) => ({
label: status.charAt(0).toUpperCase() + status.slice(1),
value: count,
color: getStatusColor(status),
}));
}
);
// Helper function to get status colors
const getStatusColor = (status: string): string => {
const colors: Record<string, string> = {
draft: '#94A3B8',
sent: '#3AA0FF',
invoiced: '#6366F1',
paid: '#10B981',
overdue: '#EF4444',
void: '#6B7280',
};
return colors[status] || '#6B7280';
};

View File

@ -0,0 +1,229 @@
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import { zohoBooksAPI } from '../services/zohoBooksAPI';
import type {
ZohoBooksDashboardState,
ZohoBooksContact,
ZohoBooksInvoice,
ZohoBooksExpense,
ZohoBooksSalesOrder,
ZohoBooksPurchaseOrder,
ZohoBooksBill,
ZohoBooksKPIData,
} from '../types/ZohoBooksTypes';
// Initial state
const initialState: ZohoBooksDashboardState = {
contacts: [],
invoices: [],
expenses: [],
salesOrders: [],
purchaseOrders: [],
bills: [],
kpiData: {
totalCustomers: 0,
totalVendors: 0,
totalInvoices: 0,
totalExpenses: 0,
totalSalesOrders: 0,
totalPurchaseOrders: 0,
totalBills: 0,
outstandingReceivables: 0,
outstandingPayables: 0,
totalRevenue: 0,
totalExpensesAmount: 0,
},
loading: false,
error: null,
lastUpdated: null,
};
// Helper function to calculate KPI data
const calculateKPIData = (
contacts: ZohoBooksContact[],
invoices: ZohoBooksInvoice[],
expenses: ZohoBooksExpense[],
salesOrders: ZohoBooksSalesOrder[],
purchaseOrders: ZohoBooksPurchaseOrder[],
bills: ZohoBooksBill[]
): ZohoBooksKPIData => {
// Calculate totals
const totalCustomers = contacts.filter(c => c.contact_type === 'customer').length;
const totalVendors = contacts.filter(c => c.contact_type === 'vendor').length;
const totalInvoices = invoices.length;
const totalExpenses = expenses.length;
const totalSalesOrders = salesOrders.length;
const totalPurchaseOrders = purchaseOrders.length;
const totalBills = bills.length;
// Calculate financial metrics
const outstandingReceivables = invoices
.filter(inv => inv.status === 'sent' || inv.status === 'overdue')
.reduce((sum, inv) => sum + inv.balance, 0);
const outstandingPayables = bills
.filter(bill => bill.status === 'open' || bill.status === 'overdue')
.reduce((sum, bill) => sum + bill.balance, 0);
const totalRevenue = invoices
.filter(inv => inv.status === 'paid')
.reduce((sum, inv) => sum + inv.total, 0);
const totalExpensesAmount = expenses.reduce((sum, exp) => sum + exp.total, 0);
return {
totalCustomers,
totalVendors,
totalInvoices,
totalExpenses,
totalSalesOrders,
totalPurchaseOrders,
totalBills,
outstandingReceivables,
outstandingPayables,
totalRevenue,
totalExpensesAmount,
};
};
// Async thunks
export const fetchZohoBooksData = createAsyncThunk(
'zohoBooks/fetchAllData',
async (_, { rejectWithValue }) => {
try {
const data = await zohoBooksAPI.getAllData();
return data;
} catch (error: any) {
return rejectWithValue(error.message || 'Failed to fetch Zoho Books data');
}
}
);
export const fetchZohoBooksContacts = createAsyncThunk(
'zohoBooks/fetchContacts',
async (_, { rejectWithValue }) => {
try {
const response = await zohoBooksAPI.getContacts();
return response.data.data;
} catch (error: any) {
return rejectWithValue(error.message || 'Failed to fetch contacts');
}
}
);
export const fetchZohoBooksInvoices = createAsyncThunk(
'zohoBooks/fetchInvoices',
async (_, { rejectWithValue }) => {
try {
const response = await zohoBooksAPI.getInvoices();
return response.data.data;
} catch (error: any) {
return rejectWithValue(error.message || 'Failed to fetch invoices');
}
}
);
export const fetchZohoBooksExpenses = createAsyncThunk(
'zohoBooks/fetchExpenses',
async (_, { rejectWithValue }) => {
try {
const response = await zohoBooksAPI.getExpenses();
return response.data.data;
} catch (error: any) {
return rejectWithValue(error.message || 'Failed to fetch expenses');
}
}
);
// Slice
const zohoBooksSlice = createSlice({
name: 'zohoBooks',
initialState,
reducers: {
clearError: (state) => {
state.error = null;
},
resetState: () => initialState,
},
extraReducers: (builder) => {
builder
// Fetch all data
.addCase(fetchZohoBooksData.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchZohoBooksData.fulfilled, (state, action) => {
state.loading = false;
state.contacts = action.payload.contacts;
state.invoices = action.payload.invoices;
state.expenses = action.payload.expenses;
state.salesOrders = action.payload.salesOrders;
state.purchaseOrders = action.payload.purchaseOrders;
state.bills = action.payload.bills;
// Calculate KPI data
state.kpiData = calculateKPIData(
action.payload.contacts,
action.payload.invoices,
action.payload.expenses,
action.payload.salesOrders,
action.payload.purchaseOrders,
action.payload.bills
);
state.lastUpdated = new Date().toISOString();
state.error = null;
})
.addCase(fetchZohoBooksData.rejected, (state, action) => {
state.loading = false;
state.error = action.payload as string;
})
// Fetch contacts
.addCase(fetchZohoBooksContacts.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchZohoBooksContacts.fulfilled, (state, action) => {
state.loading = false;
state.contacts = action.payload;
state.error = null;
})
.addCase(fetchZohoBooksContacts.rejected, (state, action) => {
state.loading = false;
state.error = action.payload as string;
})
// Fetch invoices
.addCase(fetchZohoBooksInvoices.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchZohoBooksInvoices.fulfilled, (state, action) => {
state.loading = false;
state.invoices = action.payload;
state.error = null;
})
.addCase(fetchZohoBooksInvoices.rejected, (state, action) => {
state.loading = false;
state.error = action.payload as string;
})
// Fetch expenses
.addCase(fetchZohoBooksExpenses.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchZohoBooksExpenses.fulfilled, (state, action) => {
state.loading = false;
state.expenses = action.payload;
state.error = null;
})
.addCase(fetchZohoBooksExpenses.rejected, (state, action) => {
state.loading = false;
state.error = action.payload as string;
});
},
});
export const { clearError, resetState } = zohoBooksSlice.actions;
export default zohoBooksSlice.reducer;

View File

@ -0,0 +1,333 @@
// Zoho Books API Response Types
export interface ZohoBooksContact {
contact_id: string;
contact_name: string;
customer_name: string;
vendor_name: string;
company_name: string;
website: string;
language_code: string;
language_code_formatted: string;
contact_type: 'customer' | 'vendor';
contact_type_formatted: string;
status: 'active' | 'inactive';
customer_sub_type: 'business' | 'individual';
source: string;
is_linked_with_zohocrm: boolean;
payment_terms: number;
payment_terms_label: string;
currency_id: string;
twitter: string;
facebook: string;
currency_code: string;
outstanding_receivable_amount: number;
outstanding_receivable_amount_bcy: number;
outstanding_payable_amount: number;
outstanding_payable_amount_bcy: number;
unused_credits_receivable_amount: number;
unused_credits_receivable_amount_bcy: number;
unused_credits_payable_amount: number;
unused_credits_payable_amount_bcy: number;
first_name: string;
last_name: string;
email: string;
phone: string;
mobile: string;
has_photo: boolean;
portal_status: 'enabled' | 'disabled';
portal_status_formatted: string;
created_time: string;
created_time_formatted: string;
last_modified_time: string;
last_modified_time_formatted: string;
custom_fields: any[];
custom_field_hash: Record<string, any>;
tags: string[];
ach_supported: boolean;
has_attachment: boolean;
pan_no: string;
}
export interface ZohoBooksInvoice {
ach_payment_initiated: boolean;
invoice_id: string;
zcrm_potential_id: string;
customer_id: string;
zcrm_potential_name: string;
customer_name: string;
company_name: string;
status: 'draft' | 'sent' | 'invoiced' | 'paid' | 'overdue' | 'void';
invoice_number: string;
reference_number: string;
date: string;
due_date: string;
due_days: string;
email: string;
type: 'invoice' | 'estimate' | 'creditnote';
project_name: string;
billing_address: {
address: string;
street2: string;
city: string;
state: string;
zipcode: string;
country: string;
phone: string;
fax: string;
attention: string;
};
shipping_address: {
address: string;
street2: string;
city: string;
state: string;
zipcode: string;
country: string;
phone: string;
fax: string;
attention: string;
};
country: string;
phone: string;
created_by: string;
total: number;
balance: number;
payment_expected_date: string;
custom_fields: any[];
custom_field_hash: Record<string, any>;
tags: string[];
salesperson_name: string;
shipping_charge: number;
adjustment: number;
created_time: string;
last_modified_time: string;
updated_time: string;
is_viewed_by_client: boolean;
has_attachment: boolean;
client_viewed_time: string;
is_emailed: boolean;
color_code: string;
current_sub_status_id: string;
current_sub_status: string;
currency_id: string;
schedule_time: string;
currency_code: string;
currency_symbol: string;
is_pre_gst: boolean;
template_type: string;
no_of_copies: number;
show_no_of_copies: boolean;
transaction_type: string;
reminders_sent: number;
last_reminder_sent_date: string;
last_payment_date: string;
template_id: string;
documents: string;
salesperson_id: string;
write_off_amount: number;
exchange_rate: number;
unprocessed_payment_amount: number;
}
export interface ZohoBooksExpense {
expense_id: string;
date: string;
user_name: string;
paid_through_account_name: string;
account_name: string;
description: string;
currency_id: string;
currency_code: string;
bcy_total: number;
bcy_total_without_tax: number;
total: number;
total_without_tax: number;
is_billable: boolean;
reference_number: string;
customer_id: string;
is_personal: boolean;
customer_name: string;
vendor_id: string;
vendor_name: string;
status: 'billable' | 'nonbillable';
created_time: string;
last_modified_time: string;
expense_receipt_name: string;
exchange_rate: number;
distance: number;
mileage_rate: number;
mileage_unit: string;
mileage_type: string;
expense_type: string;
report_id: string;
start_reading: string;
end_reading: string;
report_name: string;
report_number: string;
has_attachment: boolean;
custom_fields_list: string;
}
export interface ZohoBooksSalesOrder {
salesorder_id: string;
zcrm_potential_id: string;
zcrm_potential_name: string;
customer_name: string;
customer_id: string;
email: string;
delivery_date: string;
company_name: string;
color_code: string;
current_sub_status_id: string;
current_sub_status: string;
pickup_location_id: string;
salesorder_number: string;
reference_number: string;
date: string;
shipment_date: string;
shipment_days: string;
due_by_days: string;
due_in_days: string;
currency_id: string;
source: string;
currency_code: string;
total: number;
bcy_total: number;
total_invoiced_amount: number;
created_time: string;
last_modified_time: string;
is_emailed: boolean;
quantity_invoiced: number;
order_status: 'draft' | 'confirmed' | 'delivered' | 'cancelled';
invoiced_status: string;
paid_status: string;
shipped_status: string;
status: 'draft' | 'confirmed' | 'delivered' | 'cancelled';
order_fulfillment_type: string;
is_manually_fulfilled: boolean;
salesperson_name: string;
has_attachment: boolean;
tags: string[];
custom_fields_list: string;
delivery_method: string;
delivery_method_id: string;
is_viewed_in_mail: boolean;
mail_first_viewed_time: string;
mail_last_viewed_time: string;
is_scheduled_for_quick_shipment_create: boolean;
}
export interface ZohoBooksPurchaseOrder {
purchaseorder_id: string;
vendor_id: string;
vendor_name: string;
company_name: string;
order_status: 'draft' | 'confirmed' | 'received' | 'cancelled';
billed_status: string;
received_status: string;
status: 'draft' | 'confirmed' | 'received' | 'cancelled';
color_code: string;
current_sub_status_id: string;
current_sub_status: string;
purchaseorder_number: string;
reference_number: string;
date: string;
delivery_date: string;
expected_delivery_date: string;
delivery_days: string;
due_by_days: string;
due_in_days: string;
currency_id: string;
currency_code: string;
price_precision: string;
total: number;
has_attachment: boolean;
tags: string[];
created_time: string;
last_modified_time: string;
quantity_yet_to_receive: number;
quantity_marked_as_received: number;
is_po_marked_as_received: boolean;
receives: any[];
client_viewed_time: string;
is_viewed_by_client: boolean;
}
export interface ZohoBooksBill {
bill_id: string;
vendor_id: string;
vendor_name: string;
status: 'draft' | 'open' | 'paid' | 'overdue' | 'void';
color_code: string;
current_sub_status_id: string;
current_sub_status: string;
bill_number: string;
reference_number: string;
date: string;
due_date: string;
due_days: string;
currency_id: string;
currency_code: string;
price_precision: number;
exchange_rate: number;
total: number;
tds_total: number;
balance: number;
unprocessed_payment_amount: number;
created_time: string;
last_modified_time: string;
is_opening_balance: string;
attachment_name: string;
has_attachment: boolean;
tags: string[];
is_uber_bill: boolean;
is_tally_bill: boolean;
entity_type: string;
client_viewed_time: string;
is_viewed_by_client: boolean;
is_bill_reconciliation_violated: boolean;
balance_due: number;
}
export interface ZohoBooksApiResponse<T> {
status: 'success' | 'error';
message: string;
data: {
data: T[];
info: {
count: number;
moreRecords: boolean;
page: number;
};
};
timestamp: string;
}
// Dashboard specific types
export interface ZohoBooksKPIData {
totalCustomers: number;
totalVendors: number;
totalInvoices: number;
totalExpenses: number;
totalSalesOrders: number;
totalPurchaseOrders: number;
totalBills: number;
outstandingReceivables: number;
outstandingPayables: number;
totalRevenue: number;
totalExpensesAmount: number;
}
export interface ZohoBooksDashboardState {
contacts: ZohoBooksContact[];
invoices: ZohoBooksInvoice[];
expenses: ZohoBooksExpense[];
salesOrders: ZohoBooksSalesOrder[];
purchaseOrders: ZohoBooksPurchaseOrder[];
bills: ZohoBooksBill[];
kpiData: ZohoBooksKPIData;
loading: boolean;
error: string | null;
lastUpdated: string | null;
}

5
src/modules/hr/index.ts Normal file
View File

@ -0,0 +1,5 @@
// HR Module Exports
export { default as HRNavigator } from './navigation/HRNavigator';
// Zoho HR exports
export * from './zoho';

View File

@ -1,6 +1,6 @@
import React from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import HRDashboardScreen from '@/modules/hr/screens/HRDashboardScreen';
import HRDashboardScreen from '@/modules/hr/zoho/screens/HRDashboardScreen';
const Stack = createStackNavigator();

View File

@ -0,0 +1,4 @@
// Zoho HR Module Exports
export { default as HRDashboardScreen } from './screens/HRDashboardScreen';
export { default as hrSlice } from './store/hrSlice';
export { hrAPI } from './services/hrAPI';

View File

@ -2,7 +2,7 @@ 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 '@/modules/hr/store/hrSlice';
import { fetchHRMetrics } from '../store/hrSlice';
import type { RootState } from '@/store/store';
import { useTheme } from '@/shared/styles/useTheme';

View File

@ -128,13 +128,12 @@ const ZohoProjectsDashboardScreen: React.FC = () => {
>
<Icon name="list" size={24} color={colors.primary} />
</TouchableOpacity>
<Icon name="insights" size={24} color={colors.primary} />
</View>
</View>
<View style={styles.content}>
{/* Projects Overview Header */}
<View style={[styles.card, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<View style={[styles.card, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF', marginBottom: 16 }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Projects Overview</Text>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<View>

View File

@ -138,6 +138,15 @@ const ZohoProjectsDataScreen: React.FC = () => {
}
const renderTabContent = () => {
const commonFlatListProps = {
numColumns: 1,
showsVerticalScrollIndicator: false,
contentContainerStyle: styles.listContainer,
refreshControl: (
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
),
};
switch (selectedTab) {
case 'projects':
return (
@ -150,9 +159,7 @@ const ZohoProjectsDataScreen: React.FC = () => {
/>
)}
keyExtractor={(item) => item.id}
numColumns={1}
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.listContainer}
{...commonFlatListProps}
/>
);
case 'tasks':
@ -166,9 +173,7 @@ const ZohoProjectsDataScreen: React.FC = () => {
/>
)}
keyExtractor={(item) => item.id}
numColumns={1}
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.listContainer}
{...commonFlatListProps}
/>
);
case 'issues':
@ -182,9 +187,7 @@ const ZohoProjectsDataScreen: React.FC = () => {
/>
)}
keyExtractor={(item) => item.id}
numColumns={1}
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.listContainer}
{...commonFlatListProps}
/>
);
case 'phases':
@ -198,9 +201,7 @@ const ZohoProjectsDataScreen: React.FC = () => {
/>
)}
keyExtractor={(item) => item.id}
numColumns={1}
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.listContainer}
{...commonFlatListProps}
/>
);
default:
@ -209,13 +210,7 @@ const ZohoProjectsDataScreen: React.FC = () => {
};
return (
<Container>
<ScrollView
style={[styles.container, { backgroundColor: colors.background }]}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
}
>
<View style={[styles.container, { backgroundColor: colors.background }]}>
{/* Header */}
<View style={styles.header}>
<Text style={[styles.title, { color: colors.text, fontFamily: fonts.bold }]}>
@ -226,67 +221,68 @@ const ZohoProjectsDataScreen: React.FC = () => {
</TouchableOpacity>
</View>
{/* Tabs */}
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.tabsContainer}>
<View style={styles.tabs}>
{tabs.map((tab) => (
<TouchableOpacity
key={tab.key}
style={[
styles.tab,
{ backgroundColor: colors.surface, ...shadows.light },
selectedTab === tab.key && { backgroundColor: colors.primary },
]}
onPress={() => setSelectedTab(tab.key)}
activeOpacity={0.8}
>
<Icon
name={tab.icon}
size={20}
color={selectedTab === tab.key ? colors.surface : colors.textLight}
/>
<Text
{/* Fixed Tabs */}
<View style={styles.tabsContainer}>
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
<View style={styles.tabs}>
{tabs.map((tab) => (
<TouchableOpacity
key={tab.key}
style={[
styles.tabText,
{
color: selectedTab === tab.key ? colors.surface : colors.textLight,
fontFamily: fonts.medium,
},
]}
>
{tab.label}
</Text>
<View
style={[
styles.countBadge,
{
backgroundColor: selectedTab === tab.key ? colors.surface : colors.primary
},
styles.tab,
{ backgroundColor: colors.surface, ...shadows.light },
selectedTab === tab.key && { backgroundColor: colors.primary },
]}
onPress={() => setSelectedTab(tab.key)}
activeOpacity={0.8}
>
<Icon
name={tab.icon}
size={20}
color={selectedTab === tab.key ? colors.surface : colors.textLight}
/>
<Text
style={[
styles.countText,
styles.tabText,
{
color: selectedTab === tab.key ? colors.primary : colors.surface,
fontFamily: fonts.bold,
color: selectedTab === tab.key ? colors.surface : colors.textLight,
fontFamily: fonts.medium,
},
]}
>
{tab.count}
{tab.label}
</Text>
</View>
</TouchableOpacity>
))}
</View>
</ScrollView>
<View
style={[
styles.countBadge,
{
backgroundColor: selectedTab === tab.key ? colors.surface : colors.primary
},
]}
>
<Text
style={[
styles.countText,
{
color: selectedTab === tab.key ? colors.primary : colors.surface,
fontFamily: fonts.bold,
},
]}
>
{tab.count}
</Text>
</View>
</TouchableOpacity>
))}
</View>
</ScrollView>
</View>
{/* Content */}
<View style={styles.content}>
{renderTabContent()}
</View>
</ScrollView>
</Container>
</View>
);
};
@ -305,7 +301,10 @@ const styles = StyleSheet.create({
fontSize: 24,
},
tabsContainer: {
marginBottom: 16,
backgroundColor: '#FFFFFF',
paddingVertical: 12,
borderBottomWidth: 1,
borderBottomColor: '#E5E7EB',
},
tabs: {
flexDirection: 'row',
@ -336,9 +335,9 @@ const styles = StyleSheet.create({
},
content: {
flex: 1,
paddingHorizontal: 16,
},
listContainer: {
paddingHorizontal: 16,
paddingBottom: 20,
},
});

View File

@ -5,11 +5,12 @@ import AsyncStorage from '@react-native-async-storage/async-storage';
// Feature slices (to be added)
import uiSlice from '@/shared/store/uiSlice';
import authSlice from '@/modules/auth/store/authSlice';
import hrSlice from '@/modules/hr/store/hrSlice';
import hrSlice from '@/modules/hr/zoho/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';
import crmSlice from '@/modules/crm/zoho/store/crmSlice';
import zohoBooksSlice from '@/modules/finance/zoho/store/zohoBooksSlice';
const rootReducer = combineReducers({
auth: authSlice.reducer,
@ -18,13 +19,14 @@ const rootReducer = combineReducers({
profile: profileSlice.reducer,
integrations: integrationsSlice.reducer,
crm: crmSlice,
zohoBooks: zohoBooksSlice,
ui: uiSlice.reducer,
});
const persistConfig = {
key: 'root',
storage: AsyncStorage,
whitelist: ['auth', 'hr', 'zohoProjects', 'profile', 'integrations', 'crm'],
whitelist: ['auth', 'hr', 'zohoProjects', 'profile', 'integrations', 'crm', 'zohoBooks'],
blacklist: ['ui'],
};