Fix form keyboard issues and sticky footers in agent screens
This commit is contained in:
@@ -0,0 +1,650 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
RefreshControl,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import type { CompositeScreenProps } from '@react-navigation/native';
|
||||
import type { BottomTabScreenProps } from '@react-navigation/bottom-tabs';
|
||||
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
import Ionicons from '@react-native-vector-icons/ionicons';
|
||||
|
||||
import { Button, Card, EmptyState, Loader, ScreenShell } from '../../../components';
|
||||
import type { MainTabParamList, ServicesStackParamList } from '../../../navigation/types';
|
||||
import { useTheme } from '../../../theme';
|
||||
import { formatCompactCurrency, formatCurrency } from '../../../utils/formatters';
|
||||
import { DepositAccountCard } from '../components/DepositAccountCard';
|
||||
import { DepositOptionChip } from '../components/DepositOptionChip';
|
||||
import { useDepositPortfolio } from '../hooks/useDepositPortfolio';
|
||||
import { getDaysUntilMaturity, isDepositNearMaturity } from '../utils/depositCalculators';
|
||||
|
||||
type Props = CompositeScreenProps<
|
||||
NativeStackScreenProps<ServicesStackParamList, 'DepositsOverview'>,
|
||||
BottomTabScreenProps<MainTabParamList, 'Services'>
|
||||
>;
|
||||
|
||||
type FilterKey = 'all' | 'fd' | 'rd' | 'maturing';
|
||||
|
||||
const FILTERS: Array<{ id: FilterKey; label: string }> = [
|
||||
{ id: 'all', label: 'All deposits' },
|
||||
{ id: 'fd', label: 'Fixed deposit' },
|
||||
{ id: 'rd', label: 'Recurring deposit' },
|
||||
{ id: 'maturing', label: 'Maturing soon' },
|
||||
];
|
||||
|
||||
export default function DepositsOverviewScreen({ navigation, route }: Props) {
|
||||
const theme = useTheme();
|
||||
const { deposits, isLoading, isError, onRefresh, refreshing } = useDepositPortfolio();
|
||||
const [filter, setFilter] = useState<FilterKey>(route.params?.initialKind ?? 'all');
|
||||
|
||||
useEffect(() => {
|
||||
if (route.params?.initialKind) {
|
||||
setFilter(route.params.initialKind);
|
||||
}
|
||||
}, [route.params?.initialKind]);
|
||||
|
||||
const summary = useMemo(() => {
|
||||
const activeDeposits = deposits.filter((deposit) => deposit.status === 'active');
|
||||
const nearMaturityDeposits = deposits.filter((deposit) => isDepositNearMaturity(deposit));
|
||||
|
||||
return {
|
||||
totalDeposits: deposits.length,
|
||||
activeDepositsCount: activeDeposits.length,
|
||||
totalActiveValue: activeDeposits.reduce(
|
||||
(sum, deposit) => sum + deposit.totalInvestment,
|
||||
0,
|
||||
),
|
||||
projectedMaturityValue: deposits.reduce(
|
||||
(sum, deposit) => sum + deposit.maturityAmount,
|
||||
0,
|
||||
),
|
||||
nearMaturityDeposits,
|
||||
};
|
||||
}, [deposits]);
|
||||
|
||||
const filteredDeposits = useMemo(() => {
|
||||
switch (filter) {
|
||||
case 'fd':
|
||||
return deposits.filter((deposit) => deposit.kind === 'fd');
|
||||
case 'rd':
|
||||
return deposits.filter((deposit) => deposit.kind === 'rd');
|
||||
case 'maturing':
|
||||
return deposits.filter((deposit) => isDepositNearMaturity(deposit));
|
||||
case 'all':
|
||||
default:
|
||||
return deposits;
|
||||
}
|
||||
}, [deposits, filter]);
|
||||
|
||||
const selectedFilterLabel =
|
||||
FILTERS.find((item) => item.id === filter)?.label ?? 'All deposits';
|
||||
|
||||
const handleOpenFixedDeposit = () => navigation.navigate('OpenFixedDeposit');
|
||||
const handleOpenRecurringDeposit = () => navigation.navigate('OpenRecurringDeposit');
|
||||
const handleOpenCalculator = () => navigation.navigate('DepositCalculator');
|
||||
|
||||
return (
|
||||
<ScreenShell
|
||||
title="Deposits"
|
||||
subtitle="Fixed and recurring deposits, maturity tracking, and calculators"
|
||||
scrollable={false}
|
||||
>
|
||||
<ScrollView
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={onRefresh}
|
||||
tintColor={theme.colors.primary}
|
||||
colors={[theme.colors.primary]}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<View style={styles.summaryGrid}>
|
||||
<Card style={styles.heroCard}>
|
||||
<View style={styles.heroHeader}>
|
||||
<View
|
||||
style={[
|
||||
styles.heroIcon,
|
||||
{ backgroundColor: `${theme.colors.primary}12` },
|
||||
]}
|
||||
>
|
||||
<Ionicons name="wallet-outline" size={20} color={theme.colors.primary} />
|
||||
</View>
|
||||
<View style={styles.heroCopy}>
|
||||
<Text style={[styles.heroEyebrow, { color: theme.colors.primary }]}>
|
||||
Portfolio snapshot
|
||||
</Text>
|
||||
<Text style={[styles.heroTitle, { color: theme.colors.textPrimary }]}>
|
||||
Keep every deposit journey in one clear view
|
||||
</Text>
|
||||
<Text style={[styles.heroSubtitle, { color: theme.colors.textSecondary }]}>
|
||||
Review portfolio value, upcoming maturities, and quick actions without
|
||||
jumping across sections.
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={[
|
||||
styles.heroHighlight,
|
||||
{ backgroundColor: theme.colors.surfaceMuted },
|
||||
]}
|
||||
>
|
||||
<View style={styles.heroHighlightCopy}>
|
||||
<Text
|
||||
style={[
|
||||
styles.heroHighlightLabel,
|
||||
{ color: theme.colors.textSecondary },
|
||||
]}
|
||||
>
|
||||
Active portfolio
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
styles.heroHighlightValue,
|
||||
{ color: theme.colors.textPrimary },
|
||||
]}
|
||||
>
|
||||
{formatCompactCurrency(summary.totalActiveValue)}
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
styles.heroHighlightCaption,
|
||||
{ color: theme.colors.textMuted },
|
||||
]}
|
||||
>
|
||||
{summary.activeDepositsCount} active plans across fixed and recurring
|
||||
deposits
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={[
|
||||
styles.heroBadge,
|
||||
{ backgroundColor: `${theme.colors.warning}14` },
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.heroBadgeValue, { color: theme.colors.warning }]}>
|
||||
{summary.nearMaturityDeposits.length}
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
styles.heroBadgeLabel,
|
||||
{ color: theme.colors.textSecondary },
|
||||
]}
|
||||
>
|
||||
maturing soon
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.heroMetrics}>
|
||||
<View
|
||||
style={[
|
||||
styles.heroMetricCard,
|
||||
{ backgroundColor: `${theme.colors.primary}08` },
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.heroMetricLabel, { color: theme.colors.textSecondary }]}>
|
||||
Projected maturity
|
||||
</Text>
|
||||
<Text style={[styles.heroMetricValue, { color: theme.colors.textPrimary }]}>
|
||||
{formatCompactCurrency(summary.projectedMaturityValue)}
|
||||
</Text>
|
||||
<Text style={[styles.heroMetricCaption, { color: theme.colors.textMuted }]}>
|
||||
Estimated portfolio value when all plans mature
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={[
|
||||
styles.heroMetricCard,
|
||||
{ backgroundColor: theme.colors.surfaceMuted },
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.heroMetricLabel, { color: theme.colors.textSecondary }]}>
|
||||
Total deposits
|
||||
</Text>
|
||||
<Text style={[styles.heroMetricValue, { color: theme.colors.textPrimary }]}>
|
||||
{summary.totalDeposits}
|
||||
</Text>
|
||||
<Text style={[styles.heroMetricCaption, { color: theme.colors.textMuted }]}>
|
||||
{summary.totalDeposits === 1 ? '1 deposit' : `${summary.totalDeposits} deposits`} currently tracked
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Card>
|
||||
</View>
|
||||
|
||||
<Card style={styles.actionCard}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<View>
|
||||
<Text style={[styles.sectionTitle, { color: theme.colors.textPrimary }]}>
|
||||
Quick actions
|
||||
</Text>
|
||||
<Text style={[styles.sectionSubtitle, { color: theme.colors.textSecondary }]}>
|
||||
Start a new plan or estimate returns from one place
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.actionGrid}>
|
||||
<View style={styles.actionRow}>
|
||||
<View style={styles.actionColumn}>
|
||||
<Button
|
||||
title="Open FD"
|
||||
style={styles.actionButton}
|
||||
onPress={handleOpenFixedDeposit}
|
||||
leftAccessory={
|
||||
<Ionicons
|
||||
name="trending-up-outline"
|
||||
size={18}
|
||||
color={theme.colors.white}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.actionColumn}>
|
||||
<Button
|
||||
title="Open RD"
|
||||
variant="secondary"
|
||||
style={styles.actionButton}
|
||||
onPress={handleOpenRecurringDeposit}
|
||||
leftAccessory={
|
||||
<Ionicons
|
||||
name="repeat-outline"
|
||||
size={18}
|
||||
color={theme.colors.textPrimary}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<Button
|
||||
title="Calculator"
|
||||
variant="secondary"
|
||||
style={styles.actionButton}
|
||||
onPress={handleOpenCalculator}
|
||||
leftAccessory={
|
||||
<Ionicons
|
||||
name="calculator-outline"
|
||||
size={18}
|
||||
color={theme.colors.textPrimary}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</Card>
|
||||
|
||||
{summary.nearMaturityDeposits.length ? (
|
||||
<Card style={styles.watchCard}>
|
||||
<View style={styles.watchHeader}>
|
||||
<View style={[styles.watchIcon, { backgroundColor: `${theme.colors.warning}16` }]}>
|
||||
<Ionicons name="time-outline" size={18} color={theme.colors.warning} />
|
||||
</View>
|
||||
<View style={styles.watchCopy}>
|
||||
<Text style={[styles.watchTitle, { color: theme.colors.textPrimary }]}>
|
||||
Nearing maturity
|
||||
</Text>
|
||||
<Text style={[styles.watchSubtitle, { color: theme.colors.textSecondary }]}>
|
||||
Review and plan your next action before these deposits mature
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{summary.nearMaturityDeposits.map((deposit) => (
|
||||
<View
|
||||
key={`${deposit.id}_watch`}
|
||||
style={[styles.watchRow, { backgroundColor: theme.colors.surfaceMuted }]}
|
||||
>
|
||||
<View style={styles.watchRowCopy}>
|
||||
<Text style={[styles.watchDeposit, { color: theme.colors.textPrimary }]}>
|
||||
{deposit.maskedNumber}
|
||||
</Text>
|
||||
<Text style={[styles.watchMeta, { color: theme.colors.textSecondary }]}>
|
||||
{deposit.type} · {formatCurrency(deposit.maturityAmount)}
|
||||
</Text>
|
||||
</View>
|
||||
<Text
|
||||
style={[
|
||||
styles.watchDays,
|
||||
{
|
||||
color: theme.colors.warning,
|
||||
backgroundColor: `${theme.colors.warning}12`,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{Math.max(getDaysUntilMaturity(deposit.maturityDate), 0)} days
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
<View style={styles.portfolioSection}>
|
||||
<View style={styles.portfolioHeader}>
|
||||
<View style={styles.portfolioCopy}>
|
||||
<Text style={[styles.sectionTitle, { color: theme.colors.textPrimary }]}>
|
||||
Your deposits
|
||||
</Text>
|
||||
<Text style={[styles.sectionSubtitle, { color: theme.colors.textSecondary }]}>
|
||||
{filteredDeposits.length} shown in {selectedFilterLabel.toLowerCase()}
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
styles.portfolioCountBadge,
|
||||
{ backgroundColor: theme.colors.surfaceMuted },
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[styles.portfolioCountValue, { color: theme.colors.textPrimary }]}
|
||||
>
|
||||
{summary.totalDeposits}
|
||||
</Text>
|
||||
<Text
|
||||
style={[styles.portfolioCountLabel, { color: theme.colors.textSecondary }]}
|
||||
>
|
||||
total
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.filterWrap}>
|
||||
{FILTERS.map((item) => (
|
||||
<DepositOptionChip
|
||||
key={item.id}
|
||||
label={item.label}
|
||||
selected={filter === item.id}
|
||||
onPress={() => setFilter(item.id)}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{isLoading && !deposits.length ? (
|
||||
<Card>
|
||||
<Loader />
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
{isError && !deposits.length ? (
|
||||
<EmptyState
|
||||
title="Unable to load deposits"
|
||||
description="Please pull to refresh or try again in a moment."
|
||||
actionLabel="Refresh"
|
||||
onAction={onRefresh}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{!isLoading && !isError && !filteredDeposits.length ? (
|
||||
<EmptyState
|
||||
title="No deposits found"
|
||||
description="Open a new fixed or recurring deposit to start building your savings ladder."
|
||||
actionLabel="Open fixed deposit"
|
||||
onAction={handleOpenFixedDeposit}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{filteredDeposits.map((deposit) => (
|
||||
<DepositAccountCard
|
||||
key={deposit.id}
|
||||
deposit={deposit}
|
||||
onPress={() =>
|
||||
navigation.navigate('DepositDetails', {
|
||||
depositId: deposit.id,
|
||||
kind: deposit.kind,
|
||||
})
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<View style={styles.bottomSpacer} />
|
||||
</ScrollView>
|
||||
</ScreenShell>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
scrollContent: {
|
||||
gap: 18,
|
||||
paddingBottom: 24,
|
||||
},
|
||||
summaryGrid: {
|
||||
gap: 12,
|
||||
},
|
||||
heroCard: {
|
||||
gap: 16,
|
||||
},
|
||||
heroHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
gap: 12,
|
||||
},
|
||||
heroIcon: {
|
||||
width: 46,
|
||||
height: 46,
|
||||
borderRadius: 16,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
heroCopy: {
|
||||
flex: 1,
|
||||
gap: 4,
|
||||
},
|
||||
heroEyebrow: {
|
||||
fontSize: 11,
|
||||
fontWeight: '700',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.8,
|
||||
},
|
||||
heroTitle: {
|
||||
fontSize: 20,
|
||||
fontWeight: '900',
|
||||
lineHeight: 26,
|
||||
},
|
||||
heroSubtitle: {
|
||||
fontSize: 12,
|
||||
lineHeight: 18,
|
||||
},
|
||||
heroHighlight: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 12,
|
||||
borderRadius: 18,
|
||||
padding: 16,
|
||||
},
|
||||
heroHighlightCopy: {
|
||||
flex: 1,
|
||||
gap: 4,
|
||||
},
|
||||
heroHighlightLabel: {
|
||||
fontSize: 11,
|
||||
fontWeight: '700',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.8,
|
||||
},
|
||||
heroHighlightValue: {
|
||||
fontSize: 28,
|
||||
fontWeight: '900',
|
||||
letterSpacing: -0.8,
|
||||
},
|
||||
heroHighlightCaption: {
|
||||
fontSize: 12,
|
||||
lineHeight: 18,
|
||||
},
|
||||
heroBadge: {
|
||||
minWidth: 88,
|
||||
borderRadius: 18,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 2,
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 12,
|
||||
},
|
||||
heroBadgeValue: {
|
||||
fontSize: 22,
|
||||
fontWeight: '900',
|
||||
letterSpacing: -0.5,
|
||||
},
|
||||
heroBadgeLabel: {
|
||||
fontSize: 11,
|
||||
fontWeight: '700',
|
||||
textAlign: 'center',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
heroMetrics: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
gap: 12,
|
||||
},
|
||||
heroMetricCard: {
|
||||
flex: 1,
|
||||
minWidth: 140,
|
||||
borderRadius: 18,
|
||||
padding: 14,
|
||||
gap: 6,
|
||||
},
|
||||
heroMetricLabel: {
|
||||
fontSize: 11,
|
||||
fontWeight: '700',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.8,
|
||||
},
|
||||
heroMetricValue: {
|
||||
fontSize: 20,
|
||||
fontWeight: '900',
|
||||
letterSpacing: -0.4,
|
||||
},
|
||||
heroMetricCaption: {
|
||||
fontSize: 12,
|
||||
lineHeight: 18,
|
||||
},
|
||||
actionCard: {
|
||||
gap: 16,
|
||||
},
|
||||
sectionHeader: {
|
||||
gap: 4,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '800',
|
||||
},
|
||||
sectionSubtitle: {
|
||||
fontSize: 13,
|
||||
lineHeight: 18,
|
||||
},
|
||||
actionGrid: {
|
||||
gap: 12,
|
||||
},
|
||||
actionRow: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
},
|
||||
actionColumn: {
|
||||
flex: 1,
|
||||
},
|
||||
actionButton: {
|
||||
width: '100%',
|
||||
},
|
||||
watchCard: {
|
||||
gap: 14,
|
||||
},
|
||||
watchHeader: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
},
|
||||
watchIcon: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 14,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
watchCopy: {
|
||||
flex: 1,
|
||||
gap: 4,
|
||||
},
|
||||
watchTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '800',
|
||||
},
|
||||
watchSubtitle: {
|
||||
fontSize: 13,
|
||||
lineHeight: 18,
|
||||
},
|
||||
watchRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 12,
|
||||
borderRadius: 18,
|
||||
padding: 14,
|
||||
},
|
||||
watchRowCopy: {
|
||||
flex: 1,
|
||||
gap: 4,
|
||||
},
|
||||
watchDeposit: {
|
||||
fontSize: 14,
|
||||
fontWeight: '800',
|
||||
},
|
||||
watchMeta: {
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
},
|
||||
watchDays: {
|
||||
fontSize: 13,
|
||||
fontWeight: '800',
|
||||
borderRadius: 999,
|
||||
overflow: 'hidden',
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
portfolioSection: {
|
||||
gap: 14,
|
||||
},
|
||||
portfolioHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 12,
|
||||
},
|
||||
portfolioCopy: {
|
||||
flex: 1,
|
||||
gap: 4,
|
||||
},
|
||||
portfolioCountBadge: {
|
||||
minWidth: 72,
|
||||
borderRadius: 18,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 2,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
},
|
||||
portfolioCountValue: {
|
||||
fontSize: 20,
|
||||
fontWeight: '900',
|
||||
letterSpacing: -0.4,
|
||||
},
|
||||
portfolioCountLabel: {
|
||||
fontSize: 11,
|
||||
fontWeight: '700',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.8,
|
||||
},
|
||||
filterWrap: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
gap: 10,
|
||||
},
|
||||
bottomSpacer: {
|
||||
height: 60,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user