diff --git a/src/modules/crm/index.ts b/src/modules/crm/index.ts new file mode 100644 index 0000000..dab8c07 --- /dev/null +++ b/src/modules/crm/index.ts @@ -0,0 +1,5 @@ +// CRM Module Exports +export { default as CrmNavigator } from './navigation/CrmNavigator'; + +// Zoho CRM exports +export * from './zoho'; diff --git a/src/modules/crm/navigation/CrmNavigator.tsx b/src/modules/crm/navigation/CrmNavigator.tsx index 04c5959..64b888a 100644 --- a/src/modules/crm/navigation/CrmNavigator.tsx +++ b/src/modules/crm/navigation/CrmNavigator.tsx @@ -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(); diff --git a/src/modules/crm/services/index.ts b/src/modules/crm/services/index.ts deleted file mode 100644 index ca9507a..0000000 --- a/src/modules/crm/services/index.ts +++ /dev/null @@ -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'; - diff --git a/src/modules/crm/components/CrmDataCards.tsx b/src/modules/crm/zoho/components/CrmDataCards.tsx similarity index 100% rename from src/modules/crm/components/CrmDataCards.tsx rename to src/modules/crm/zoho/components/CrmDataCards.tsx diff --git a/src/modules/crm/zoho/index.ts b/src/modules/crm/zoho/index.ts new file mode 100644 index 0000000..90cac13 --- /dev/null +++ b/src/modules/crm/zoho/index.ts @@ -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'; diff --git a/src/modules/crm/screens/CrmDashboardScreen.tsx b/src/modules/crm/zoho/screens/CrmDashboardScreen.tsx similarity index 100% rename from src/modules/crm/screens/CrmDashboardScreen.tsx rename to src/modules/crm/zoho/screens/CrmDashboardScreen.tsx diff --git a/src/modules/crm/screens/ZohoCrmDataScreen.tsx b/src/modules/crm/zoho/screens/ZohoCrmDataScreen.tsx similarity index 76% rename from src/modules/crm/screens/ZohoCrmDataScreen.tsx rename to src/modules/crm/zoho/screens/ZohoCrmDataScreen.tsx index 390712b..3432a98 100644 --- a/src/modules/crm/screens/ZohoCrmDataScreen.tsx +++ b/src/modules/crm/zoho/screens/ZohoCrmDataScreen.tsx @@ -124,6 +124,15 @@ const ZohoCrmDataScreen: React.FC = () => { const renderTabContent = () => { + const commonFlatListProps = { + numColumns: 1, + showsVerticalScrollIndicator: false, + contentContainerStyle: styles.listContainer, + refreshControl: ( + + ), + }; + 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 ( - - - } - > - {/* Header */} + + {/* Fixed Header */} Zoho CRM Data @@ -260,64 +249,65 @@ const ZohoCrmDataScreen: React.FC = () => { - {/* Tabs */} - - - {tabs.map((tab) => ( - setSelectedTab(tab.key)} - activeOpacity={0.8} - > - - + + + {tabs.map((tab) => ( + - {tab.label} - - setSelectedTab(tab.key)} + activeOpacity={0.8} > + - {tab.count} + {tab.label} - - - ))} - - + + + {tab.count} + + + + ))} + + + - {/* Content */} + {/* Scrollable Content */} {renderTabContent()} - - + ); }; @@ -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, }, }); diff --git a/src/modules/crm/services/crmAPI.ts b/src/modules/crm/zoho/services/crmAPI.ts similarity index 100% rename from src/modules/crm/services/crmAPI.ts rename to src/modules/crm/zoho/services/crmAPI.ts diff --git a/src/modules/crm/store/crmSlice.ts b/src/modules/crm/zoho/store/crmSlice.ts similarity index 100% rename from src/modules/crm/store/crmSlice.ts rename to src/modules/crm/zoho/store/crmSlice.ts diff --git a/src/modules/crm/store/selectors.ts b/src/modules/crm/zoho/store/selectors.ts similarity index 100% rename from src/modules/crm/store/selectors.ts rename to src/modules/crm/zoho/store/selectors.ts diff --git a/src/modules/crm/types/CrmTypes.ts b/src/modules/crm/zoho/types/CrmTypes.ts similarity index 100% rename from src/modules/crm/types/CrmTypes.ts rename to src/modules/crm/zoho/types/CrmTypes.ts diff --git a/src/modules/finance/index.ts b/src/modules/finance/index.ts new file mode 100644 index 0000000..0d4ec15 --- /dev/null +++ b/src/modules/finance/index.ts @@ -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'; diff --git a/src/modules/finance/navigation/FinanceNavigator.tsx b/src/modules/finance/navigation/FinanceNavigator.tsx index 375e699..273ae70 100644 --- a/src/modules/finance/navigation/FinanceNavigator.tsx +++ b/src/modules/finance/navigation/FinanceNavigator.tsx @@ -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 = () => ( - + + ); diff --git a/src/modules/finance/screens/FinanceDashboardScreen.tsx b/src/modules/finance/screens/FinanceDashboardScreen.tsx deleted file mode 100644 index b8db1c5..0000000 --- a/src/modules/finance/screens/FinanceDashboardScreen.tsx +++ /dev/null @@ -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 ( - - - Accounts & Finance - - {/* KPIs */} - - - - - - - - {/* Sales Trend */} - - Sales Trend - - - - {/* Taxes & AR Aging */} - - - Taxes - {(() => { - 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 ( - <> - Collected: ${mock.taxes.collected.toLocaleString()} - - Paid: ${mock.taxes.paid.toLocaleString()} - - - ); - })()} - - - A/R Aging - - - 0-30 - 31-60 - 61-90 - 90+ - - - - - {/* Invoice Status Distribution */} - - Invoice Status - a + b.value, 0)} /> - - {mock.invoiceStatus.map(s => ( - - - {s.label} - - ))} - - - - {/* Lists */} - - - Top Customers - {mock.topCustomers.map(r => ( - - {r.client} - ${r.amount.toLocaleString()} - - ))} - - - Bank Accounts - {mock.bankAccounts.map(p => ( - - {p.name} - ${p.balance.toLocaleString()} - - ))} - - - - {/* Estimates & Payment Modes */} - - Estimates - - - Sent: {mock.estimates.sent} - - - Accepted: {mock.estimates.accepted} - - - Declined: {mock.estimates.declined} - - - - - - Payment Modes - a + b.value, 0)} /> - - {mock.paymentModes.map(s => ( - - - {s.label} - - ))} - - - - - ); -}; - -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 ( - - {label} - - {value} - - - - ); -}; - -const Bars: React.FC<{ data: number[]; max: number; color: string }> = ({ data, max, color }) => { - return ( - - {data.map((v, i) => ( - - ))} - - ); -}; - -const Progress: React.FC<{ value: number; color: string; fonts: any }> = ({ value, color, fonts }) => { - return ( - - - - - {value}% - - ); -}; - -const Stacked: React.FC<{ segments: { label: string; value: number; color: string }[]; total: number }> = ({ segments, total }) => { - return ( - - {segments.map(s => ( - - ))} - - ); -}; - - diff --git a/src/modules/finance/zoho/components/widgets/BarChartWidget.tsx b/src/modules/finance/zoho/components/widgets/BarChartWidget.tsx new file mode 100644 index 0000000..1ab924c --- /dev/null +++ b/src/modules/finance/zoho/components/widgets/BarChartWidget.tsx @@ -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 = ({ + 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 = () => ( + + + + + + + {title} + + {subtitle && ( + + {subtitle} + + )} + + + + + + + + + + + + Total: {total.toLocaleString()} + + + Max: {maxValue.toLocaleString()} + + + + + ); + + if (onPress) { + return ( + + + + ); + } + + return ; +}; + +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; diff --git a/src/modules/finance/zoho/components/widgets/KPIWidget.tsx b/src/modules/finance/zoho/components/widgets/KPIWidget.tsx new file mode 100644 index 0000000..4280819 --- /dev/null +++ b/src/modules/finance/zoho/components/widgets/KPIWidget.tsx @@ -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 = ({ + 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 = () => ( + + + + + + {trend && ( + + + + {Math.abs(trend.value)}% + + + )} + + + + + {title} + + + + {formatValue(value)} + + + {subtitle && ( + + {subtitle} + + )} + + + ); + + if (onPress) { + return ( + + + + ); + } + + return ; +}; + +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; diff --git a/src/modules/finance/zoho/components/widgets/ListWidget.tsx b/src/modules/finance/zoho/components/widgets/ListWidget.tsx new file mode 100644 index 0000000..07c4dab --- /dev/null +++ b/src/modules/finance/zoho/components/widgets/ListWidget.tsx @@ -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 = ({ + 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 ( + + + + {title} + + {onViewAllPress && data.length > maxItems && ( + + + View All + + + + )} + + + + {displayData.length === 0 ? ( + + + + {emptyMessage} + + + ) : ( + + {displayData.map((item, index) => ( + onItemPress?.(item)} + disabled={!onItemPress} + > + + {item.icon && ( + + + + )} + + + + {item.title} + + {item.subtitle && ( + + {item.subtitle} + + )} + + + + {showValue && ( + + + {formatValue(item.value)} + + + )} + + ))} + + )} + + + {data.length > maxItems && !onViewAllPress && ( + + + +{data.length - maxItems} more items + + + )} + + ); +}; + +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; diff --git a/src/modules/finance/zoho/components/widgets/RevenueChart.tsx b/src/modules/finance/zoho/components/widgets/RevenueChart.tsx new file mode 100644 index 0000000..cf44670 --- /dev/null +++ b/src/modules/finance/zoho/components/widgets/RevenueChart.tsx @@ -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 = ({ + 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 = () => ( + + + + + + {title} + + + + + + + {chartType === 'pie' ? ( + + ) : ( + + )} + + + + {data.map((item, index) => ( + + + + {item.label} + + + {item.value} + + + ))} + + + + {calculatedTotal > 0 && ( + + + Total: {calculatedTotal.toLocaleString()} + + + )} + + ); + + if (onPress) { + return ( + + + + ); + } + + return ; +}; + +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; diff --git a/src/modules/finance/zoho/components/widgets/index.ts b/src/modules/finance/zoho/components/widgets/index.ts new file mode 100644 index 0000000..cf4970f --- /dev/null +++ b/src/modules/finance/zoho/components/widgets/index.ts @@ -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'; diff --git a/src/modules/finance/zoho/index.ts b/src/modules/finance/zoho/index.ts new file mode 100644 index 0000000..0e3215b --- /dev/null +++ b/src/modules/finance/zoho/index.ts @@ -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'; diff --git a/src/modules/finance/zoho/screens/ZohoBooksDashboardScreen.tsx b/src/modules/finance/zoho/screens/ZohoBooksDashboardScreen.tsx new file mode 100644 index 0000000..3420b6d --- /dev/null +++ b/src/modules/finance/zoho/screens/ZohoBooksDashboardScreen.tsx @@ -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(); + 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 ; + } + + // Error state + if (error) { + return ; + } + + return ( + + + } + showsVerticalScrollIndicator={false} + > + {/* Header */} + + + + + Zoho Books Dashboard + + + navigation.navigate('ZohoBooksData' as never)} + > + + + View Data + + + + + {/* KPI Cards Row 1 */} + + + + + + + + + + {/* KPI Cards Row 2 */} + + + + + + + + + + {/* KPI Cards Row 3 */} + + + + + + + + + + {/* Charts Row */} + + + + + + + + + + {/* Bar Chart */} + + + {/* Lists Row */} + + + + + + + + + + {/* Summary Cards */} + + + + + + Documents + + + + + + Invoices + + + {kpiData.totalInvoices} + + + + + Sales Orders + + + {kpiData.totalSalesOrders} + + + + + Purchase Orders + + + {kpiData.totalPurchaseOrders} + + + + + Bills + + + {kpiData.totalBills} + + + + + + + {/* Bottom spacing */} + + + + ); +}; + +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; diff --git a/src/modules/finance/zoho/screens/ZohoBooksDataScreen.tsx b/src/modules/finance/zoho/screens/ZohoBooksDataScreen.tsx new file mode 100644 index 0000000..5bab8b2 --- /dev/null +++ b/src/modules/finance/zoho/screens/ZohoBooksDataScreen.tsx @@ -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(); + 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 ; + } + + // Error state + if (error) { + return ; + } + + // 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) => ( + + + + + + {item.contact_name} + + + + + {item.status} + + + + + + + Type: + + {item.contact_type_formatted} + + + + {item.email && ( + + Email: + {item.email} + + )} + + {item.phone && ( + + Phone: + {item.phone} + + )} + + + Currency: + {item.currency_code} + + + + + Outstanding Receivable: + + ₹{item.outstanding_receivable_amount.toLocaleString()} + + + + Outstanding Payable: + + ₹{item.outstanding_payable_amount.toLocaleString()} + + + + + + ); + + const renderInvoiceCard = (item: any) => ( + + + + + + {item.invoice_number} + + + + + {item.status} + + + + + + + Customer: + + {item.customer_name} + + + + + Date: + {item.date} + + + + Due Date: + {item.due_date} + + + + + Total: + + {item.currency_symbol}{item.total.toLocaleString()} + + + + Balance: + 0 ? '#EF4444' : '#10B981', fontFamily: fonts.bold }]}> + {item.currency_symbol}{item.balance.toLocaleString()} + + + + + + ); + + const renderExpenseCard = (item: any) => ( + + + + + + {item.reference_number || 'Expense'} + + + + + {item.status} + + + + + + + Account: + + {item.account_name} + + + + + Date: + {item.date} + + + {item.vendor_name && ( + + Vendor: + {item.vendor_name} + + )} + + + + Total: + + {item.currency_code} {item.total.toLocaleString()} + + + + Billable: + + {item.is_billable ? 'Yes' : 'No'} + + + + + + ); + + const renderSalesOrderCard = (item: any) => ( + + + + + + {item.salesorder_number} + + + + + {item.status} + + + + + + + Customer: + + {item.customer_name} + + + + + Date: + {item.date} + + + + Shipment Date: + {item.shipment_date} + + + + + Total: + + {item.currency_code} {item.total.toLocaleString()} + + + + Invoiced: + + {item.currency_code} {item.total_invoiced_amount.toLocaleString()} + + + + + + ); + + const renderPurchaseOrderCard = (item: any) => ( + + + + + + {item.purchaseorder_number} + + + + + {item.status} + + + + + + + Vendor: + + {item.vendor_name} + + + + + Date: + {item.date} + + + + Delivery Date: + {item.delivery_date} + + + + + Total: + + {item.currency_code} {item.total.toLocaleString()} + + + + Quantity: + + {item.quantity_yet_to_receive} + + + + + + ); + + const renderBillCard = (item: any) => ( + + + + + + {item.bill_number} + + + + + {item.status} + + + + + + + Vendor: + + {item.vendor_name} + + + + + Date: + {item.date} + + + + Due Date: + {item.due_date} + + + + + Total: + + {item.currency_code} {item.total.toLocaleString()} + + + + Balance: + 0 ? '#EF4444' : '#10B981', fontFamily: fonts.bold }]}> + {item.currency_code} {item.balance.toLocaleString()} + + + + + + ); + + const renderTabContent = () => { + const commonFlatListProps = { + showsVerticalScrollIndicator: false, + contentContainerStyle: styles.listContainer, + refreshControl: ( + + ), + }; + + switch (selectedTab) { + case 'contacts': + return ( + renderContactCard(item)} + keyExtractor={(item) => item.contact_id} + {...commonFlatListProps} + /> + ); + case 'customers': + return ( + renderContactCard(item)} + keyExtractor={(item) => item.contact_id} + {...commonFlatListProps} + /> + ); + case 'vendors': + return ( + renderContactCard(item)} + keyExtractor={(item) => item.contact_id} + {...commonFlatListProps} + /> + ); + case 'invoices': + return ( + renderInvoiceCard(item)} + keyExtractor={(item) => item.invoice_id} + {...commonFlatListProps} + /> + ); + case 'expenses': + return ( + renderExpenseCard(item)} + keyExtractor={(item) => item.expense_id} + {...commonFlatListProps} + /> + ); + case 'salesOrders': + return ( + renderSalesOrderCard(item)} + keyExtractor={(item) => item.salesorder_id} + {...commonFlatListProps} + /> + ); + case 'purchaseOrders': + return ( + renderPurchaseOrderCard(item)} + keyExtractor={(item) => item.purchaseorder_id} + {...commonFlatListProps} + /> + ); + case 'bills': + return ( + renderBillCard(item)} + keyExtractor={(item) => item.bill_id} + {...commonFlatListProps} + /> + ); + default: + return null; + } + }; + + return ( + + {/* Fixed Header */} + + + Zoho Books Data + + + + + + + {/* Fixed Tabs */} + + + + {tabs.map((tab) => ( + setSelectedTab(tab.key)} + activeOpacity={0.8} + > + + + {tab.label} + + + + {tab.count} + + + + ))} + + + + + {/* Scrollable Content */} + + {renderTabContent()} + + + ); +}; + +// Helper function to get status colors +const getStatusColor = (status: string): string => { + const statusColors: Record = { + 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; diff --git a/src/modules/finance/zoho/services/zohoBooksAPI.ts b/src/modules/finance/zoho/services/zohoBooksAPI.ts new file mode 100644 index 0000000..9821584 --- /dev/null +++ b/src/modules/finance/zoho/services/zohoBooksAPI.ts @@ -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> => { + 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> => { + const response = await httpClient.get(`${BASE_URL}/vendors?provider=zoho`); + return response.data; + }, + + // Get customers only + getCustomers: async (): Promise> => { + const response = await httpClient.get(`${BASE_URL}/customers?provider=zoho`); + return response.data; + }, + + // Get invoices + getInvoices: async (): Promise> => { + const response = await httpClient.get(`${BASE_URL}/invoices?provider=zoho`); + return response.data; + }, + + // Get expenses + getExpenses: async (): Promise> => { + const response = await httpClient.get(`${BASE_URL}/expenses?provider=zoho`); + return response.data; + }, + + // Get sales orders + getSalesOrders: async (): Promise> => { + const response = await httpClient.get(`${BASE_URL}/sales-orders?provider=zoho`); + return response.data; + }, + + // Get purchase orders + getPurchaseOrders: async (): Promise> => { + const response = await httpClient.get(`${BASE_URL}/purchase-orders?provider=zoho`); + return response.data; + }, + + // Get bills + getBills: async (): Promise> => { + 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; + } + }, +}; diff --git a/src/modules/finance/zoho/store/selectors.ts b/src/modules/finance/zoho/store/selectors.ts new file mode 100644 index 0000000..0d23b41 --- /dev/null +++ b/src/modules/finance/zoho/store/selectors.ts @@ -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); + + 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 = { + draft: '#94A3B8', + sent: '#3AA0FF', + invoiced: '#6366F1', + paid: '#10B981', + overdue: '#EF4444', + void: '#6B7280', + }; + return colors[status] || '#6B7280'; +}; diff --git a/src/modules/finance/zoho/store/zohoBooksSlice.ts b/src/modules/finance/zoho/store/zohoBooksSlice.ts new file mode 100644 index 0000000..b196b8b --- /dev/null +++ b/src/modules/finance/zoho/store/zohoBooksSlice.ts @@ -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; diff --git a/src/modules/finance/zoho/types/ZohoBooksTypes.ts b/src/modules/finance/zoho/types/ZohoBooksTypes.ts new file mode 100644 index 0000000..1efdf90 --- /dev/null +++ b/src/modules/finance/zoho/types/ZohoBooksTypes.ts @@ -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; + 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; + 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 { + 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; +} diff --git a/src/modules/hr/index.ts b/src/modules/hr/index.ts new file mode 100644 index 0000000..bdd7428 --- /dev/null +++ b/src/modules/hr/index.ts @@ -0,0 +1,5 @@ +// HR Module Exports +export { default as HRNavigator } from './navigation/HRNavigator'; + +// Zoho HR exports +export * from './zoho'; diff --git a/src/modules/hr/navigation/HRNavigator.tsx b/src/modules/hr/navigation/HRNavigator.tsx index 63f45fa..590d1ac 100644 --- a/src/modules/hr/navigation/HRNavigator.tsx +++ b/src/modules/hr/navigation/HRNavigator.tsx @@ -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(); diff --git a/src/modules/hr/zoho/index.ts b/src/modules/hr/zoho/index.ts new file mode 100644 index 0000000..bd1ff24 --- /dev/null +++ b/src/modules/hr/zoho/index.ts @@ -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'; diff --git a/src/modules/hr/screens/HRDashboardScreen.tsx b/src/modules/hr/zoho/screens/HRDashboardScreen.tsx similarity index 99% rename from src/modules/hr/screens/HRDashboardScreen.tsx rename to src/modules/hr/zoho/screens/HRDashboardScreen.tsx index 4ac85d9..10d0ad5 100644 --- a/src/modules/hr/screens/HRDashboardScreen.tsx +++ b/src/modules/hr/zoho/screens/HRDashboardScreen.tsx @@ -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'; diff --git a/src/modules/hr/services/hrAPI.ts b/src/modules/hr/zoho/services/hrAPI.ts similarity index 100% rename from src/modules/hr/services/hrAPI.ts rename to src/modules/hr/zoho/services/hrAPI.ts diff --git a/src/modules/hr/store/hrSlice.ts b/src/modules/hr/zoho/store/hrSlice.ts similarity index 100% rename from src/modules/hr/store/hrSlice.ts rename to src/modules/hr/zoho/store/hrSlice.ts diff --git a/src/modules/zohoProjects/screens/ZohoProjectsDashboardScreen.tsx b/src/modules/zohoProjects/screens/ZohoProjectsDashboardScreen.tsx index bbcd3b7..fe0f6e7 100644 --- a/src/modules/zohoProjects/screens/ZohoProjectsDashboardScreen.tsx +++ b/src/modules/zohoProjects/screens/ZohoProjectsDashboardScreen.tsx @@ -128,13 +128,12 @@ const ZohoProjectsDashboardScreen: React.FC = () => { > - {/* Projects Overview Header */} - + Projects Overview diff --git a/src/modules/zohoProjects/screens/ZohoProjectsDataScreen.tsx b/src/modules/zohoProjects/screens/ZohoProjectsDataScreen.tsx index 479de16..23e5f70 100644 --- a/src/modules/zohoProjects/screens/ZohoProjectsDataScreen.tsx +++ b/src/modules/zohoProjects/screens/ZohoProjectsDataScreen.tsx @@ -138,6 +138,15 @@ const ZohoProjectsDataScreen: React.FC = () => { } const renderTabContent = () => { + const commonFlatListProps = { + numColumns: 1, + showsVerticalScrollIndicator: false, + contentContainerStyle: styles.listContainer, + refreshControl: ( + + ), + }; + 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 ( - - - } - > + {/* Header */} @@ -226,67 +221,68 @@ const ZohoProjectsDataScreen: React.FC = () => { - {/* Tabs */} - - - {tabs.map((tab) => ( - setSelectedTab(tab.key)} - activeOpacity={0.8} - > - - + + + {tabs.map((tab) => ( + - {tab.label} - - setSelectedTab(tab.key)} + activeOpacity={0.8} > + - {tab.count} + {tab.label} - - - ))} - - + + + {tab.count} + + + + ))} + + + {/* Content */} {renderTabContent()} - - + ); }; @@ -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, }, }); diff --git a/src/store/store.ts b/src/store/store.ts index a4653f3..8ecc04e 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -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'], };