folder structure modified based on the different providers
This commit is contained in:
parent
7b45abc367
commit
8c1e5309e5
5
src/modules/crm/index.ts
Normal file
5
src/modules/crm/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
// CRM Module Exports
|
||||||
|
export { default as CrmNavigator } from './navigation/CrmNavigator';
|
||||||
|
|
||||||
|
// Zoho CRM exports
|
||||||
|
export * from './zoho';
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createStackNavigator } from '@react-navigation/stack';
|
import { createStackNavigator } from '@react-navigation/stack';
|
||||||
import CrmDashboardScreen from '@/modules/crm/screens/CrmDashboardScreen';
|
import CrmDashboardScreen from '@/modules/crm/zoho/screens/CrmDashboardScreen';
|
||||||
import ZohoCrmDataScreen from '@/modules/crm/screens/ZohoCrmDataScreen';
|
import ZohoCrmDataScreen from '@/modules/crm/zoho/screens/ZohoCrmDataScreen';
|
||||||
|
|
||||||
const Stack = createStackNavigator();
|
const Stack = createStackNavigator();
|
||||||
|
|
||||||
|
|||||||
@ -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';
|
|
||||||
|
|
||||||
7
src/modules/crm/zoho/index.ts
Normal file
7
src/modules/crm/zoho/index.ts
Normal 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';
|
||||||
@ -124,6 +124,15 @@ const ZohoCrmDataScreen: React.FC = () => {
|
|||||||
|
|
||||||
|
|
||||||
const renderTabContent = () => {
|
const renderTabContent = () => {
|
||||||
|
const commonFlatListProps = {
|
||||||
|
numColumns: 1,
|
||||||
|
showsVerticalScrollIndicator: false,
|
||||||
|
contentContainerStyle: styles.listContainer,
|
||||||
|
refreshControl: (
|
||||||
|
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
switch (selectedTab) {
|
switch (selectedTab) {
|
||||||
case 'leads':
|
case 'leads':
|
||||||
return (
|
return (
|
||||||
@ -136,9 +145,7 @@ const ZohoCrmDataScreen: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
keyExtractor={(item) => item.id}
|
keyExtractor={(item) => item.id}
|
||||||
numColumns={1}
|
{...commonFlatListProps}
|
||||||
showsVerticalScrollIndicator={false}
|
|
||||||
contentContainerStyle={styles.listContainer}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'tasks':
|
case 'tasks':
|
||||||
@ -152,9 +159,7 @@ const ZohoCrmDataScreen: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
keyExtractor={(item) => item.id}
|
keyExtractor={(item) => item.id}
|
||||||
numColumns={1}
|
{...commonFlatListProps}
|
||||||
showsVerticalScrollIndicator={false}
|
|
||||||
contentContainerStyle={styles.listContainer}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'contacts':
|
case 'contacts':
|
||||||
@ -168,9 +173,7 @@ const ZohoCrmDataScreen: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
keyExtractor={(item) => item.id}
|
keyExtractor={(item) => item.id}
|
||||||
numColumns={1}
|
{...commonFlatListProps}
|
||||||
showsVerticalScrollIndicator={false}
|
|
||||||
contentContainerStyle={styles.listContainer}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'deals':
|
case 'deals':
|
||||||
@ -184,9 +187,7 @@ const ZohoCrmDataScreen: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
keyExtractor={(item) => item.id}
|
keyExtractor={(item) => item.id}
|
||||||
numColumns={1}
|
{...commonFlatListProps}
|
||||||
showsVerticalScrollIndicator={false}
|
|
||||||
contentContainerStyle={styles.listContainer}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'salesOrders':
|
case 'salesOrders':
|
||||||
@ -200,9 +201,7 @@ const ZohoCrmDataScreen: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
keyExtractor={(item) => item.id}
|
keyExtractor={(item) => item.id}
|
||||||
numColumns={1}
|
{...commonFlatListProps}
|
||||||
showsVerticalScrollIndicator={false}
|
|
||||||
contentContainerStyle={styles.listContainer}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'purchaseOrders':
|
case 'purchaseOrders':
|
||||||
@ -216,9 +215,7 @@ const ZohoCrmDataScreen: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
keyExtractor={(item) => item.id}
|
keyExtractor={(item) => item.id}
|
||||||
numColumns={1}
|
{...commonFlatListProps}
|
||||||
showsVerticalScrollIndicator={false}
|
|
||||||
contentContainerStyle={styles.listContainer}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'invoices':
|
case 'invoices':
|
||||||
@ -232,9 +229,7 @@ const ZohoCrmDataScreen: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
keyExtractor={(item) => item.id}
|
keyExtractor={(item) => item.id}
|
||||||
numColumns={1}
|
{...commonFlatListProps}
|
||||||
showsVerticalScrollIndicator={false}
|
|
||||||
contentContainerStyle={styles.listContainer}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
@ -243,14 +238,8 @@ const ZohoCrmDataScreen: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<View style={[styles.container, { backgroundColor: colors.background }]}>
|
||||||
<ScrollView
|
{/* Fixed Header */}
|
||||||
style={[styles.container, { backgroundColor: colors.background }]}
|
|
||||||
refreshControl={
|
|
||||||
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{/* Header */}
|
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<Text style={[styles.title, { color: colors.text, fontFamily: fonts.bold }]}>
|
<Text style={[styles.title, { color: colors.text, fontFamily: fonts.bold }]}>
|
||||||
Zoho CRM Data
|
Zoho CRM Data
|
||||||
@ -260,64 +249,65 @@ const ZohoCrmDataScreen: React.FC = () => {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Tabs */}
|
{/* Fixed Tabs */}
|
||||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.tabsContainer}>
|
<View style={styles.tabsContainer}>
|
||||||
<View style={styles.tabs}>
|
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
||||||
{tabs.map((tab) => (
|
<View style={styles.tabs}>
|
||||||
<TouchableOpacity
|
{tabs.map((tab) => (
|
||||||
key={tab.key}
|
<TouchableOpacity
|
||||||
style={[
|
key={tab.key}
|
||||||
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={[
|
style={[
|
||||||
styles.tabText,
|
styles.tab,
|
||||||
{
|
selectedTab === tab.key && { backgroundColor: colors.primary },
|
||||||
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 },
|
|
||||||
]}
|
]}
|
||||||
|
onPress={() => setSelectedTab(tab.key)}
|
||||||
|
activeOpacity={0.8}
|
||||||
>
|
>
|
||||||
|
<Icon
|
||||||
|
name={tab.icon}
|
||||||
|
size={20}
|
||||||
|
color={selectedTab === tab.key ? colors.surface : colors.textLight}
|
||||||
|
/>
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
styles.countText,
|
styles.tabText,
|
||||||
{
|
{
|
||||||
color: selectedTab === tab.key ? colors.primary : colors.surface,
|
color: selectedTab === tab.key ? colors.surface : colors.textLight,
|
||||||
fontFamily: fonts.bold,
|
fontFamily: fonts.medium,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{tab.count}
|
{tab.label}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
<View
|
||||||
</TouchableOpacity>
|
style={[
|
||||||
))}
|
styles.countBadge,
|
||||||
</View>
|
{ backgroundColor: selectedTab === tab.key ? colors.surface : colors.primary },
|
||||||
</ScrollView>
|
]}
|
||||||
|
>
|
||||||
|
<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}>
|
<View style={styles.content}>
|
||||||
{renderTabContent()}
|
{renderTabContent()}
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</View>
|
||||||
</Container>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -336,7 +326,10 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
},
|
},
|
||||||
tabsContainer: {
|
tabsContainer: {
|
||||||
marginBottom: 16,
|
backgroundColor: '#FFFFFF',
|
||||||
|
paddingVertical: 12,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: '#E5E7EB',
|
||||||
},
|
},
|
||||||
tabs: {
|
tabs: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
@ -368,9 +361,9 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
paddingHorizontal: 16,
|
|
||||||
},
|
},
|
||||||
listContainer: {
|
listContainer: {
|
||||||
|
paddingHorizontal: 16,
|
||||||
paddingBottom: 20,
|
paddingBottom: 20,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
6
src/modules/finance/index.ts
Normal file
6
src/modules/finance/index.ts
Normal 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';
|
||||||
@ -1,12 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createStackNavigator } from '@react-navigation/stack';
|
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 Stack = createStackNavigator();
|
||||||
|
|
||||||
const FinanceNavigator = () => (
|
const FinanceNavigator = () => (
|
||||||
<Stack.Navigator>
|
<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>
|
</Stack.Navigator>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
135
src/modules/finance/zoho/components/widgets/BarChartWidget.tsx
Normal file
135
src/modules/finance/zoho/components/widgets/BarChartWidget.tsx
Normal 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;
|
||||||
162
src/modules/finance/zoho/components/widgets/KPIWidget.tsx
Normal file
162
src/modules/finance/zoho/components/widgets/KPIWidget.tsx
Normal 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;
|
||||||
227
src/modules/finance/zoho/components/widgets/ListWidget.tsx
Normal file
227
src/modules/finance/zoho/components/widgets/ListWidget.tsx
Normal 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;
|
||||||
166
src/modules/finance/zoho/components/widgets/RevenueChart.tsx
Normal file
166
src/modules/finance/zoho/components/widgets/RevenueChart.tsx
Normal 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;
|
||||||
4
src/modules/finance/zoho/components/widgets/index.ts
Normal file
4
src/modules/finance/zoho/components/widgets/index.ts
Normal 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';
|
||||||
25
src/modules/finance/zoho/index.ts
Normal file
25
src/modules/finance/zoho/index.ts
Normal 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';
|
||||||
425
src/modules/finance/zoho/screens/ZohoBooksDashboardScreen.tsx
Normal file
425
src/modules/finance/zoho/screens/ZohoBooksDashboardScreen.tsx
Normal 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;
|
||||||
736
src/modules/finance/zoho/screens/ZohoBooksDataScreen.tsx
Normal file
736
src/modules/finance/zoho/screens/ZohoBooksDataScreen.tsx
Normal 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;
|
||||||
96
src/modules/finance/zoho/services/zohoBooksAPI.ts
Normal file
96
src/modules/finance/zoho/services/zohoBooksAPI.ts
Normal 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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
133
src/modules/finance/zoho/store/selectors.ts
Normal file
133
src/modules/finance/zoho/store/selectors.ts
Normal 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';
|
||||||
|
};
|
||||||
229
src/modules/finance/zoho/store/zohoBooksSlice.ts
Normal file
229
src/modules/finance/zoho/store/zohoBooksSlice.ts
Normal 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;
|
||||||
333
src/modules/finance/zoho/types/ZohoBooksTypes.ts
Normal file
333
src/modules/finance/zoho/types/ZohoBooksTypes.ts
Normal 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
5
src/modules/hr/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
// HR Module Exports
|
||||||
|
export { default as HRNavigator } from './navigation/HRNavigator';
|
||||||
|
|
||||||
|
// Zoho HR exports
|
||||||
|
export * from './zoho';
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createStackNavigator } from '@react-navigation/stack';
|
import { createStackNavigator } from '@react-navigation/stack';
|
||||||
import HRDashboardScreen from '@/modules/hr/screens/HRDashboardScreen';
|
import HRDashboardScreen from '@/modules/hr/zoho/screens/HRDashboardScreen';
|
||||||
|
|
||||||
const Stack = createStackNavigator();
|
const Stack = createStackNavigator();
|
||||||
|
|
||||||
|
|||||||
4
src/modules/hr/zoho/index.ts
Normal file
4
src/modules/hr/zoho/index.ts
Normal 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';
|
||||||
@ -2,7 +2,7 @@ import React, { useEffect, useMemo } from 'react';
|
|||||||
import { View, Text, StyleSheet } from 'react-native';
|
import { View, Text, StyleSheet } from 'react-native';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { Container, LoadingSpinner, ErrorState } from '@/shared/components/ui';
|
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 type { RootState } from '@/store/store';
|
||||||
import { useTheme } from '@/shared/styles/useTheme';
|
import { useTheme } from '@/shared/styles/useTheme';
|
||||||
|
|
||||||
@ -128,13 +128,12 @@ const ZohoProjectsDashboardScreen: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<Icon name="list" size={24} color={colors.primary} />
|
<Icon name="list" size={24} color={colors.primary} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<Icon name="insights" size={24} color={colors.primary} />
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
{/* Projects Overview Header */}
|
{/* 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>
|
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Projects Overview</Text>
|
||||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<View>
|
<View>
|
||||||
|
|||||||
@ -138,6 +138,15 @@ const ZohoProjectsDataScreen: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const renderTabContent = () => {
|
const renderTabContent = () => {
|
||||||
|
const commonFlatListProps = {
|
||||||
|
numColumns: 1,
|
||||||
|
showsVerticalScrollIndicator: false,
|
||||||
|
contentContainerStyle: styles.listContainer,
|
||||||
|
refreshControl: (
|
||||||
|
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
switch (selectedTab) {
|
switch (selectedTab) {
|
||||||
case 'projects':
|
case 'projects':
|
||||||
return (
|
return (
|
||||||
@ -150,9 +159,7 @@ const ZohoProjectsDataScreen: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
keyExtractor={(item) => item.id}
|
keyExtractor={(item) => item.id}
|
||||||
numColumns={1}
|
{...commonFlatListProps}
|
||||||
showsVerticalScrollIndicator={false}
|
|
||||||
contentContainerStyle={styles.listContainer}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'tasks':
|
case 'tasks':
|
||||||
@ -166,9 +173,7 @@ const ZohoProjectsDataScreen: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
keyExtractor={(item) => item.id}
|
keyExtractor={(item) => item.id}
|
||||||
numColumns={1}
|
{...commonFlatListProps}
|
||||||
showsVerticalScrollIndicator={false}
|
|
||||||
contentContainerStyle={styles.listContainer}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'issues':
|
case 'issues':
|
||||||
@ -182,9 +187,7 @@ const ZohoProjectsDataScreen: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
keyExtractor={(item) => item.id}
|
keyExtractor={(item) => item.id}
|
||||||
numColumns={1}
|
{...commonFlatListProps}
|
||||||
showsVerticalScrollIndicator={false}
|
|
||||||
contentContainerStyle={styles.listContainer}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'phases':
|
case 'phases':
|
||||||
@ -198,9 +201,7 @@ const ZohoProjectsDataScreen: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
keyExtractor={(item) => item.id}
|
keyExtractor={(item) => item.id}
|
||||||
numColumns={1}
|
{...commonFlatListProps}
|
||||||
showsVerticalScrollIndicator={false}
|
|
||||||
contentContainerStyle={styles.listContainer}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
@ -209,13 +210,7 @@ const ZohoProjectsDataScreen: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<View style={[styles.container, { backgroundColor: colors.background }]}>
|
||||||
<ScrollView
|
|
||||||
style={[styles.container, { backgroundColor: colors.background }]}
|
|
||||||
refreshControl={
|
|
||||||
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<Text style={[styles.title, { color: colors.text, fontFamily: fonts.bold }]}>
|
<Text style={[styles.title, { color: colors.text, fontFamily: fonts.bold }]}>
|
||||||
@ -226,67 +221,68 @@ const ZohoProjectsDataScreen: React.FC = () => {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Tabs */}
|
{/* Fixed Tabs */}
|
||||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.tabsContainer}>
|
<View style={styles.tabsContainer}>
|
||||||
<View style={styles.tabs}>
|
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
||||||
{tabs.map((tab) => (
|
<View style={styles.tabs}>
|
||||||
<TouchableOpacity
|
{tabs.map((tab) => (
|
||||||
key={tab.key}
|
<TouchableOpacity
|
||||||
style={[
|
key={tab.key}
|
||||||
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={[
|
style={[
|
||||||
styles.tabText,
|
styles.tab,
|
||||||
{
|
{ backgroundColor: colors.surface, ...shadows.light },
|
||||||
color: selectedTab === tab.key ? colors.surface : colors.textLight,
|
selectedTab === tab.key && { backgroundColor: colors.primary },
|
||||||
fontFamily: fonts.medium,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{tab.label}
|
|
||||||
</Text>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
styles.countBadge,
|
|
||||||
{
|
|
||||||
backgroundColor: selectedTab === tab.key ? colors.surface : colors.primary
|
|
||||||
},
|
|
||||||
]}
|
]}
|
||||||
|
onPress={() => setSelectedTab(tab.key)}
|
||||||
|
activeOpacity={0.8}
|
||||||
>
|
>
|
||||||
|
<Icon
|
||||||
|
name={tab.icon}
|
||||||
|
size={20}
|
||||||
|
color={selectedTab === tab.key ? colors.surface : colors.textLight}
|
||||||
|
/>
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
styles.countText,
|
styles.tabText,
|
||||||
{
|
{
|
||||||
color: selectedTab === tab.key ? colors.primary : colors.surface,
|
color: selectedTab === tab.key ? colors.surface : colors.textLight,
|
||||||
fontFamily: fonts.bold,
|
fontFamily: fonts.medium,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{tab.count}
|
{tab.label}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
<View
|
||||||
</TouchableOpacity>
|
style={[
|
||||||
))}
|
styles.countBadge,
|
||||||
</View>
|
{
|
||||||
</ScrollView>
|
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 */}
|
{/* Content */}
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
{renderTabContent()}
|
{renderTabContent()}
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</View>
|
||||||
</Container>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -305,7 +301,10 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
},
|
},
|
||||||
tabsContainer: {
|
tabsContainer: {
|
||||||
marginBottom: 16,
|
backgroundColor: '#FFFFFF',
|
||||||
|
paddingVertical: 12,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: '#E5E7EB',
|
||||||
},
|
},
|
||||||
tabs: {
|
tabs: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
@ -336,9 +335,9 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
paddingHorizontal: 16,
|
|
||||||
},
|
},
|
||||||
listContainer: {
|
listContainer: {
|
||||||
|
paddingHorizontal: 16,
|
||||||
paddingBottom: 20,
|
paddingBottom: 20,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -5,11 +5,12 @@ import AsyncStorage from '@react-native-async-storage/async-storage';
|
|||||||
// Feature slices (to be added)
|
// Feature slices (to be added)
|
||||||
import uiSlice from '@/shared/store/uiSlice';
|
import uiSlice from '@/shared/store/uiSlice';
|
||||||
import authSlice from '@/modules/auth/store/authSlice';
|
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 zohoProjectsSlice from '@/modules/zohoProjects/store/zohoProjectsSlice';
|
||||||
import profileSlice from '@/modules/profile/store/profileSlice';
|
import profileSlice from '@/modules/profile/store/profileSlice';
|
||||||
import integrationsSlice from '@/modules/integrations/store/integrationsSlice';
|
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({
|
const rootReducer = combineReducers({
|
||||||
auth: authSlice.reducer,
|
auth: authSlice.reducer,
|
||||||
@ -18,13 +19,14 @@ const rootReducer = combineReducers({
|
|||||||
profile: profileSlice.reducer,
|
profile: profileSlice.reducer,
|
||||||
integrations: integrationsSlice.reducer,
|
integrations: integrationsSlice.reducer,
|
||||||
crm: crmSlice,
|
crm: crmSlice,
|
||||||
|
zohoBooks: zohoBooksSlice,
|
||||||
ui: uiSlice.reducer,
|
ui: uiSlice.reducer,
|
||||||
});
|
});
|
||||||
|
|
||||||
const persistConfig = {
|
const persistConfig = {
|
||||||
key: 'root',
|
key: 'root',
|
||||||
storage: AsyncStorage,
|
storage: AsyncStorage,
|
||||||
whitelist: ['auth', 'hr', 'zohoProjects', 'profile', 'integrations', 'crm'],
|
whitelist: ['auth', 'hr', 'zohoProjects', 'profile', 'integrations', 'crm', 'zohoBooks'],
|
||||||
blacklist: ['ui'],
|
blacklist: ['ui'],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user