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 { createStackNavigator } from '@react-navigation/stack';
|
||||
import CrmDashboardScreen from '@/modules/crm/screens/CrmDashboardScreen';
|
||||
import ZohoCrmDataScreen from '@/modules/crm/screens/ZohoCrmDataScreen';
|
||||
import CrmDashboardScreen from '@/modules/crm/zoho/screens/CrmDashboardScreen';
|
||||
import ZohoCrmDataScreen from '@/modules/crm/zoho/screens/ZohoCrmDataScreen';
|
||||
|
||||
const Stack = createStackNavigator();
|
||||
|
||||
|
||||
@ -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 commonFlatListProps = {
|
||||
numColumns: 1,
|
||||
showsVerticalScrollIndicator: false,
|
||||
contentContainerStyle: styles.listContainer,
|
||||
refreshControl: (
|
||||
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
|
||||
),
|
||||
};
|
||||
|
||||
switch (selectedTab) {
|
||||
case 'leads':
|
||||
return (
|
||||
@ -136,9 +145,7 @@ const ZohoCrmDataScreen: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
keyExtractor={(item) => item.id}
|
||||
numColumns={1}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={styles.listContainer}
|
||||
{...commonFlatListProps}
|
||||
/>
|
||||
);
|
||||
case 'tasks':
|
||||
@ -152,9 +159,7 @@ const ZohoCrmDataScreen: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
keyExtractor={(item) => item.id}
|
||||
numColumns={1}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={styles.listContainer}
|
||||
{...commonFlatListProps}
|
||||
/>
|
||||
);
|
||||
case 'contacts':
|
||||
@ -168,9 +173,7 @@ const ZohoCrmDataScreen: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
keyExtractor={(item) => item.id}
|
||||
numColumns={1}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={styles.listContainer}
|
||||
{...commonFlatListProps}
|
||||
/>
|
||||
);
|
||||
case 'deals':
|
||||
@ -184,9 +187,7 @@ const ZohoCrmDataScreen: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
keyExtractor={(item) => item.id}
|
||||
numColumns={1}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={styles.listContainer}
|
||||
{...commonFlatListProps}
|
||||
/>
|
||||
);
|
||||
case 'salesOrders':
|
||||
@ -200,9 +201,7 @@ const ZohoCrmDataScreen: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
keyExtractor={(item) => item.id}
|
||||
numColumns={1}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={styles.listContainer}
|
||||
{...commonFlatListProps}
|
||||
/>
|
||||
);
|
||||
case 'purchaseOrders':
|
||||
@ -216,9 +215,7 @@ const ZohoCrmDataScreen: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
keyExtractor={(item) => item.id}
|
||||
numColumns={1}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={styles.listContainer}
|
||||
{...commonFlatListProps}
|
||||
/>
|
||||
);
|
||||
case 'invoices':
|
||||
@ -232,9 +229,7 @@ const ZohoCrmDataScreen: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
keyExtractor={(item) => item.id}
|
||||
numColumns={1}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={styles.listContainer}
|
||||
{...commonFlatListProps}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
@ -243,14 +238,8 @@ const ZohoCrmDataScreen: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<ScrollView
|
||||
style={[styles.container, { backgroundColor: colors.background }]}
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
|
||||
}
|
||||
>
|
||||
{/* Header */}
|
||||
<View style={[styles.container, { backgroundColor: colors.background }]}>
|
||||
{/* Fixed Header */}
|
||||
<View style={styles.header}>
|
||||
<Text style={[styles.title, { color: colors.text, fontFamily: fonts.bold }]}>
|
||||
Zoho CRM Data
|
||||
@ -260,8 +249,9 @@ const ZohoCrmDataScreen: React.FC = () => {
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Tabs */}
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.tabsContainer}>
|
||||
{/* Fixed Tabs */}
|
||||
<View style={styles.tabsContainer}>
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
||||
<View style={styles.tabs}>
|
||||
{tabs.map((tab) => (
|
||||
<TouchableOpacity
|
||||
@ -311,13 +301,13 @@ const ZohoCrmDataScreen: React.FC = () => {
|
||||
))}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
{/* Content */}
|
||||
{/* Scrollable Content */}
|
||||
<View style={styles.content}>
|
||||
{renderTabContent()}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</Container>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@ -336,7 +326,10 @@ const styles = StyleSheet.create({
|
||||
fontSize: 24,
|
||||
},
|
||||
tabsContainer: {
|
||||
marginBottom: 16,
|
||||
backgroundColor: '#FFFFFF',
|
||||
paddingVertical: 12,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#E5E7EB',
|
||||
},
|
||||
tabs: {
|
||||
flexDirection: 'row',
|
||||
@ -368,9 +361,9 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
listContainer: {
|
||||
paddingHorizontal: 16,
|
||||
paddingBottom: 20,
|
||||
},
|
||||
});
|
||||
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 { createStackNavigator } from '@react-navigation/stack';
|
||||
import FinanceDashboardScreen from '@/modules/finance/screens/FinanceDashboardScreen';
|
||||
import ZohoBooksDashboardScreen from '@/modules/finance/zoho/screens/ZohoBooksDashboardScreen';
|
||||
import ZohoBooksDataScreen from '@/modules/finance/zoho/screens/ZohoBooksDataScreen';
|
||||
|
||||
const Stack = createStackNavigator();
|
||||
|
||||
const FinanceNavigator = () => (
|
||||
<Stack.Navigator>
|
||||
<Stack.Screen name="FinanceDashboard" component={FinanceDashboardScreen} options={{headerShown:false}}/>
|
||||
<Stack.Screen name="ZohoBooksDashboard" component={ZohoBooksDashboardScreen} options={{headerShown:false}}/>
|
||||
<Stack.Screen name="ZohoBooksData" component={ZohoBooksDataScreen} options={{headerShown:false}}/>
|
||||
</Stack.Navigator>
|
||||
);
|
||||
|
||||
|
||||
@ -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 { createStackNavigator } from '@react-navigation/stack';
|
||||
import HRDashboardScreen from '@/modules/hr/screens/HRDashboardScreen';
|
||||
import HRDashboardScreen from '@/modules/hr/zoho/screens/HRDashboardScreen';
|
||||
|
||||
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 { useDispatch, useSelector } from 'react-redux';
|
||||
import { Container, LoadingSpinner, ErrorState } from '@/shared/components/ui';
|
||||
import { fetchHRMetrics } from '@/modules/hr/store/hrSlice';
|
||||
import { fetchHRMetrics } from '../store/hrSlice';
|
||||
import type { RootState } from '@/store/store';
|
||||
import { useTheme } from '@/shared/styles/useTheme';
|
||||
|
||||
@ -128,13 +128,12 @@ const ZohoProjectsDashboardScreen: React.FC = () => {
|
||||
>
|
||||
<Icon name="list" size={24} color={colors.primary} />
|
||||
</TouchableOpacity>
|
||||
<Icon name="insights" size={24} color={colors.primary} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.content}>
|
||||
{/* Projects Overview Header */}
|
||||
<View style={[styles.card, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
|
||||
<View style={[styles.card, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF', marginBottom: 16 }]}>
|
||||
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Projects Overview</Text>
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<View>
|
||||
|
||||
@ -138,6 +138,15 @@ const ZohoProjectsDataScreen: React.FC = () => {
|
||||
}
|
||||
|
||||
const renderTabContent = () => {
|
||||
const commonFlatListProps = {
|
||||
numColumns: 1,
|
||||
showsVerticalScrollIndicator: false,
|
||||
contentContainerStyle: styles.listContainer,
|
||||
refreshControl: (
|
||||
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
|
||||
),
|
||||
};
|
||||
|
||||
switch (selectedTab) {
|
||||
case 'projects':
|
||||
return (
|
||||
@ -150,9 +159,7 @@ const ZohoProjectsDataScreen: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
keyExtractor={(item) => item.id}
|
||||
numColumns={1}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={styles.listContainer}
|
||||
{...commonFlatListProps}
|
||||
/>
|
||||
);
|
||||
case 'tasks':
|
||||
@ -166,9 +173,7 @@ const ZohoProjectsDataScreen: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
keyExtractor={(item) => item.id}
|
||||
numColumns={1}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={styles.listContainer}
|
||||
{...commonFlatListProps}
|
||||
/>
|
||||
);
|
||||
case 'issues':
|
||||
@ -182,9 +187,7 @@ const ZohoProjectsDataScreen: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
keyExtractor={(item) => item.id}
|
||||
numColumns={1}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={styles.listContainer}
|
||||
{...commonFlatListProps}
|
||||
/>
|
||||
);
|
||||
case 'phases':
|
||||
@ -198,9 +201,7 @@ const ZohoProjectsDataScreen: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
keyExtractor={(item) => item.id}
|
||||
numColumns={1}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={styles.listContainer}
|
||||
{...commonFlatListProps}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
@ -209,13 +210,7 @@ const ZohoProjectsDataScreen: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<ScrollView
|
||||
style={[styles.container, { backgroundColor: colors.background }]}
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
|
||||
}
|
||||
>
|
||||
<View style={[styles.container, { backgroundColor: colors.background }]}>
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<Text style={[styles.title, { color: colors.text, fontFamily: fonts.bold }]}>
|
||||
@ -226,8 +221,9 @@ const ZohoProjectsDataScreen: React.FC = () => {
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Tabs */}
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.tabsContainer}>
|
||||
{/* Fixed Tabs */}
|
||||
<View style={styles.tabsContainer}>
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
||||
<View style={styles.tabs}>
|
||||
{tabs.map((tab) => (
|
||||
<TouchableOpacity
|
||||
@ -280,13 +276,13 @@ const ZohoProjectsDataScreen: React.FC = () => {
|
||||
))}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
{/* Content */}
|
||||
<View style={styles.content}>
|
||||
{renderTabContent()}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</Container>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@ -305,7 +301,10 @@ const styles = StyleSheet.create({
|
||||
fontSize: 24,
|
||||
},
|
||||
tabsContainer: {
|
||||
marginBottom: 16,
|
||||
backgroundColor: '#FFFFFF',
|
||||
paddingVertical: 12,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#E5E7EB',
|
||||
},
|
||||
tabs: {
|
||||
flexDirection: 'row',
|
||||
@ -336,9 +335,9 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
listContainer: {
|
||||
paddingHorizontal: 16,
|
||||
paddingBottom: 20,
|
||||
},
|
||||
});
|
||||
|
||||
@ -5,11 +5,12 @@ import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
// Feature slices (to be added)
|
||||
import uiSlice from '@/shared/store/uiSlice';
|
||||
import authSlice from '@/modules/auth/store/authSlice';
|
||||
import hrSlice from '@/modules/hr/store/hrSlice';
|
||||
import hrSlice from '@/modules/hr/zoho/store/hrSlice';
|
||||
import zohoProjectsSlice from '@/modules/zohoProjects/store/zohoProjectsSlice';
|
||||
import profileSlice from '@/modules/profile/store/profileSlice';
|
||||
import integrationsSlice from '@/modules/integrations/store/integrationsSlice';
|
||||
import crmSlice from '@/modules/crm/store/crmSlice';
|
||||
import crmSlice from '@/modules/crm/zoho/store/crmSlice';
|
||||
import zohoBooksSlice from '@/modules/finance/zoho/store/zohoBooksSlice';
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
auth: authSlice.reducer,
|
||||
@ -18,13 +19,14 @@ const rootReducer = combineReducers({
|
||||
profile: profileSlice.reducer,
|
||||
integrations: integrationsSlice.reducer,
|
||||
crm: crmSlice,
|
||||
zohoBooks: zohoBooksSlice,
|
||||
ui: uiSlice.reducer,
|
||||
});
|
||||
|
||||
const persistConfig = {
|
||||
key: 'root',
|
||||
storage: AsyncStorage,
|
||||
whitelist: ['auth', 'hr', 'zohoProjects', 'profile', 'integrations', 'crm'],
|
||||
whitelist: ['auth', 'hr', 'zohoProjects', 'profile', 'integrations', 'crm', 'zohoBooks'],
|
||||
blacklist: ['ui'],
|
||||
};
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user