Files
CBS-Mobile-App/src/modules/deposit/screens/DepositDetailsScreen.tsx
T

566 lines
18 KiB
TypeScript

import React, { useMemo, useState } from 'react';
import {
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 { AppModal, Button, Card, EmptyState, Loader, ScreenShell } from '../../../components';
import { useToast } from '../../../hooks/useToast';
import type { MainTabParamList, ServicesStackParamList } from '../../../navigation/types';
import { useTheme } from '../../../theme';
import { formatCurrency, formatDate } from '../../../utils/formatters';
import { TransactionItem } from '../../cards/components/TransactionItem';
import { ExpandableSection } from '../../cards/components/ExpandableSection';
import { DepositStatusPill } from '../components/DepositStatusPill';
import {
useCloseDepositMutation,
useGetDepositDetailQuery,
} from '../services/depositApi';
import {
calculatePrematureClosureAmount,
getDaysUntilMaturity,
getDepositProgress,
isDepositNearMaturity,
} from '../utils/depositCalculators';
type Props = CompositeScreenProps<
NativeStackScreenProps<ServicesStackParamList, 'DepositDetails'>,
BottomTabScreenProps<MainTabParamList, 'Services'>
>;
export default function DepositDetailsScreen({ navigation, route }: Props) {
const theme = useTheme();
const toast = useToast();
const [confirmVisible, setConfirmVisible] = useState(false);
const { data: deposit, isLoading, isError, refetch } = useGetDepositDetailQuery({
depositId: route.params.depositId,
kind: route.params.kind,
});
const [closeDeposit, closeState] = useCloseDepositMutation();
const nearMaturity = deposit ? isDepositNearMaturity(deposit) : false;
const closureQuote = deposit ? calculatePrematureClosureAmount(deposit) : null;
const progress = deposit
? getDepositProgress(deposit.startDate, deposit.maturityDate)
: 0;
const daysUntilMaturity = deposit
? Math.max(getDaysUntilMaturity(deposit.maturityDate), 0)
: 0;
const history = useMemo(
() =>
(deposit?.transactions ?? []).map((transaction) => ({
id: transaction.id,
merchant: transaction.title,
amount: transaction.amount,
direction: transaction.direction,
status: transaction.status,
occurredAt: transaction.occurredAt,
category: transaction.category,
})),
[deposit?.transactions],
);
const handleCloseDeposit = async () => {
if (!deposit) {
return;
}
try {
await closeDeposit({
depositId: deposit.id,
kind: deposit.kind,
}).unwrap();
setConfirmVisible(false);
toast(
deposit.status === 'matured' ? 'Maturity proceeds credited' : 'Deposit closed',
'success',
deposit.status === 'matured'
? 'The maturity amount has been credited to your linked account.'
: 'Closure request completed successfully.',
);
} catch {
toast(
'Unable to close deposit',
'error',
'Please try again in a moment.',
);
}
};
if (isLoading) {
return (
<ScreenShell title="Deposit details" subtitle="Loading selected deposit">
<Card>
<Loader />
</Card>
</ScreenShell>
);
}
if (isError || !deposit) {
return (
<ScreenShell title="Deposit details" subtitle="We could not load the selected deposit">
<EmptyState
title="Deposit unavailable"
description="Pull back and try opening the deposit again."
actionLabel="Retry"
onAction={refetch}
/>
</ScreenShell>
);
}
const canClose = deposit.status !== 'closed' && deposit.canWithdrawPrematurely;
const closeActionLabel =
deposit.status === 'matured' ? 'Credit maturity proceeds' : 'Premature close';
const investedAmount =
deposit.kind === 'fd'
? deposit.principalAmount
: deposit.totalInvestment;
return (
<ScreenShell
title="Deposit details"
subtitle={`${deposit.maskedNumber} · ${deposit.type}`}
scrollable={false}
>
<ScrollView
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.scrollContent}
>
<Card style={styles.heroCard}>
<View style={styles.heroTopRow}>
<View style={styles.heroCopy}>
<Text style={[styles.heroEyebrow, { color: theme.colors.textSecondary }]}>
{deposit.type}
</Text>
<Text style={[styles.heroTitle, { color: theme.colors.textPrimary }]}>
{deposit.maskedNumber}
</Text>
</View>
<DepositStatusPill status={deposit.status} nearMaturity={nearMaturity} />
</View>
<View style={styles.heroMetrics}>
<View style={[styles.heroMetricTile, { backgroundColor: theme.colors.surfaceMuted }]}>
<Text style={[styles.heroMetricLabel, { color: theme.colors.textSecondary }]}>
Invested amount
</Text>
<Text style={[styles.heroMetricValue, { color: theme.colors.textPrimary }]}>
{formatCurrency(investedAmount)}
</Text>
</View>
<View style={[styles.heroMetricTile, { backgroundColor: theme.colors.surfaceMuted }]}>
<Text style={[styles.heroMetricLabel, { color: theme.colors.textSecondary }]}>
Maturity value
</Text>
<Text style={[styles.heroMetricValue, { color: theme.colors.textPrimary }]}>
{formatCurrency(deposit.maturityAmount)}
</Text>
</View>
<View style={[styles.heroMetricTile, { backgroundColor: theme.colors.surfaceMuted }]}>
<Text style={[styles.heroMetricLabel, { color: theme.colors.textSecondary }]}>
Interest earned
</Text>
<Text style={[styles.heroMetricValue, { color: theme.colors.textPrimary }]}>
{formatCurrency(deposit.interestEarned)}
</Text>
</View>
<View style={[styles.heroMetricTile, { backgroundColor: theme.colors.surfaceMuted }]}>
<Text style={[styles.heroMetricLabel, { color: theme.colors.textSecondary }]}>
Tracking
</Text>
<Text
style={[
styles.heroMetricValue,
{
color:
deposit.status === 'closed'
? theme.colors.textSecondary
: nearMaturity
? theme.colors.warning
: theme.colors.textPrimary,
},
]}
>
{deposit.status === 'closed'
? 'Closed'
: deposit.status === 'matured'
? 'Ready'
: `${daysUntilMaturity} days left`}
</Text>
</View>
</View>
<View style={styles.progressWrap}>
<View style={[styles.progressTrack, { backgroundColor: theme.colors.surfaceMuted }]}>
<View
style={[
styles.progressFill,
{
width: `${progress * 100}%`,
backgroundColor:
deposit.status === 'closed'
? theme.colors.textMuted
: nearMaturity
? theme.colors.warning
: theme.colors.accent,
},
]}
/>
</View>
<Text style={[styles.progressCaption, { color: theme.colors.textSecondary }]}>
Started on {formatDate(deposit.startDate)} · Matures on {formatDate(deposit.maturityDate)}
</Text>
</View>
</Card>
<ExpandableSection
title="Deposit snapshot"
subtitle="Principal, interest, payout, nominee, and settlement account"
icon="document-text-outline"
defaultExpanded
>
<View style={styles.detailGrid}>
<View style={[styles.detailTile, { backgroundColor: theme.colors.surfaceMuted }]}>
<Text style={[styles.detailLabel, { color: theme.colors.textSecondary }]}>
Interest rate
</Text>
<Text style={[styles.detailValue, { color: theme.colors.textPrimary }]}>
{deposit.interestRate}% p.a.
</Text>
</View>
<View style={[styles.detailTile, { backgroundColor: theme.colors.surfaceMuted }]}>
<Text style={[styles.detailLabel, { color: theme.colors.textSecondary }]}>
Tenure
</Text>
<Text style={[styles.detailValue, { color: theme.colors.textPrimary }]}>
{deposit.tenureMonths} months
</Text>
</View>
<View style={[styles.detailTile, { backgroundColor: theme.colors.surfaceMuted }]}>
<Text style={[styles.detailLabel, { color: theme.colors.textSecondary }]}>
Payout frequency
</Text>
<Text style={[styles.detailValue, { color: theme.colors.textPrimary }]}>
{deposit.payoutLabel}
</Text>
</View>
<View style={[styles.detailTile, { backgroundColor: theme.colors.surfaceMuted }]}>
<Text style={[styles.detailLabel, { color: theme.colors.textSecondary }]}>
Settlement account
</Text>
<Text style={[styles.detailValue, { color: theme.colors.textPrimary }]}>
{deposit.settlementAccountMasked}
</Text>
</View>
{deposit.nominee ? (
<View style={[styles.detailTile, styles.detailTileWide, { backgroundColor: theme.colors.surfaceMuted }]}>
<Text style={[styles.detailLabel, { color: theme.colors.textSecondary }]}>
Nominee
</Text>
<Text style={[styles.detailValue, { color: theme.colors.textPrimary }]}>
{deposit.nominee.name} · {deposit.nominee.relationship}
</Text>
</View>
) : null}
</View>
</ExpandableSection>
<ExpandableSection
title="Returns and maturity"
subtitle="Projected value, accrued gains, and current closure estimate"
icon="analytics-outline"
defaultExpanded
>
<View style={styles.detailGrid}>
<View style={[styles.detailTile, { backgroundColor: theme.colors.surfaceMuted }]}>
<Text style={[styles.detailLabel, { color: theme.colors.textSecondary }]}>
Total investment
</Text>
<Text style={[styles.detailValue, { color: theme.colors.textPrimary }]}>
{formatCurrency(deposit.totalInvestment)}
</Text>
</View>
<View style={[styles.detailTile, { backgroundColor: theme.colors.surfaceMuted }]}>
<Text style={[styles.detailLabel, { color: theme.colors.textSecondary }]}>
Estimated proceeds
</Text>
<Text style={[styles.detailValue, { color: theme.colors.textPrimary }]}>
{formatCurrency(closureQuote?.netAmount ?? deposit.maturityAmount)}
</Text>
</View>
<View style={[styles.detailTile, { backgroundColor: theme.colors.surfaceMuted }]}>
<Text style={[styles.detailLabel, { color: theme.colors.textSecondary }]}>
Penalty
</Text>
<Text style={[styles.detailValue, { color: theme.colors.textPrimary }]}>
{formatCurrency(closureQuote?.penaltyAmount ?? 0)}
</Text>
</View>
<View style={[styles.detailTile, { backgroundColor: theme.colors.surfaceMuted }]}>
<Text style={[styles.detailLabel, { color: theme.colors.textSecondary }]}>
Maturity date
</Text>
<Text style={[styles.detailValue, { color: theme.colors.textPrimary }]}>
{formatDate(deposit.maturityDate)}
</Text>
</View>
</View>
</ExpandableSection>
<ExpandableSection
title="Transaction history"
subtitle="Creation, installments, interest payouts, and closure credits"
icon="swap-horizontal-outline"
badge={`${history.length} entries`}
>
{history.length ? (
history.map((item, index) => (
<TransactionItem
key={item.id}
item={item}
showDivider={index !== history.length - 1}
/>
))
) : (
<EmptyState
title="No transactions yet"
description="Transaction history will appear here after the deposit is created."
/>
)}
</ExpandableSection>
<Card style={styles.actionsCard}>
<Text style={[styles.sectionTitle, { color: theme.colors.textPrimary }]}>
Actions
</Text>
<Text style={[styles.sectionSubtitle, { color: theme.colors.textSecondary }]}>
Estimate new returns or close the deposit if you need funds earlier
</Text>
<Button
title="Open calculator"
variant="secondary"
onPress={() =>
navigation.navigate('DepositCalculator', { initialKind: deposit.kind })
}
leftAccessory={<Ionicons name="calculator-outline" size={18} color={theme.colors.textPrimary} />}
/>
{canClose ? (
<Button
title={closeActionLabel}
variant={deposit.status === 'matured' ? 'primary' : 'danger'}
loading={closeState.isLoading}
onPress={() => setConfirmVisible(true)}
leftAccessory={
<Ionicons
name={deposit.status === 'matured' ? 'cash-outline' : 'close-circle-outline'}
size={18}
color={theme.colors.white}
/>
}
/>
) : null}
</Card>
</ScrollView>
<AppModal
visible={confirmVisible}
title={closeActionLabel}
description={
deposit.status === 'matured'
? 'The maturity amount will be credited to your linked settlement account.'
: 'This will close the deposit before maturity and credit the net proceeds after penalty.'
}
onClose={() => setConfirmVisible(false)}
>
<View style={styles.modalList}>
<View style={styles.modalRow}>
<Text style={[styles.modalLabel, { color: theme.colors.textSecondary }]}>
Net proceeds
</Text>
<Text style={[styles.modalValue, { color: theme.colors.textPrimary }]}>
{formatCurrency(closureQuote?.netAmount ?? deposit.maturityAmount)}
</Text>
</View>
<View style={styles.modalRow}>
<Text style={[styles.modalLabel, { color: theme.colors.textSecondary }]}>
Penalty
</Text>
<Text style={[styles.modalValue, { color: theme.colors.textPrimary }]}>
{formatCurrency(closureQuote?.penaltyAmount ?? 0)}
</Text>
</View>
<View style={styles.modalRow}>
<Text style={[styles.modalLabel, { color: theme.colors.textSecondary }]}>
Credit account
</Text>
<Text style={[styles.modalValue, { color: theme.colors.textPrimary }]}>
{deposit.settlementAccountMasked}
</Text>
</View>
</View>
<View style={styles.modalActions}>
<Button
title="Cancel"
variant="secondary"
onPress={() => setConfirmVisible(false)}
style={styles.modalButton}
fullWidth={false}
/>
<Button
title="Confirm"
variant={deposit.status === 'matured' ? 'primary' : 'danger'}
onPress={handleCloseDeposit}
loading={closeState.isLoading}
style={styles.modalButton}
fullWidth={false}
/>
</View>
</AppModal>
</ScreenShell>
);
}
const styles = StyleSheet.create({
scrollContent: {
gap: 18,
paddingBottom: 40,
},
heroCard: {
gap: 18,
},
heroTopRow: {
flexDirection: 'row',
justifyContent: 'space-between',
gap: 12,
},
heroCopy: {
flex: 1,
gap: 4,
},
heroEyebrow: {
fontSize: 11,
fontWeight: '700',
textTransform: 'uppercase',
letterSpacing: 0.8,
},
heroTitle: {
fontSize: 24,
fontWeight: '900',
letterSpacing: -0.6,
},
heroMetrics: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 12,
},
heroMetricTile: {
width: '48%',
borderRadius: 18,
padding: 14,
gap: 6,
},
heroMetricLabel: {
fontSize: 11,
fontWeight: '700',
textTransform: 'uppercase',
letterSpacing: 0.8,
},
heroMetricValue: {
fontSize: 16,
fontWeight: '900',
lineHeight: 22,
},
progressWrap: {
gap: 10,
},
progressTrack: {
height: 12,
borderRadius: 999,
overflow: 'hidden',
},
progressFill: {
height: 12,
borderRadius: 999,
},
progressCaption: {
fontSize: 12,
fontWeight: '500',
},
detailGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 12,
},
detailTile: {
width: '48%',
borderRadius: 18,
padding: 14,
gap: 6,
},
detailTileWide: {
width: '100%',
},
detailLabel: {
fontSize: 11,
fontWeight: '700',
textTransform: 'uppercase',
letterSpacing: 0.8,
},
detailValue: {
fontSize: 14,
fontWeight: '800',
lineHeight: 20,
},
actionsCard: {
gap: 12,
},
sectionTitle: {
fontSize: 18,
fontWeight: '800',
},
sectionSubtitle: {
fontSize: 13,
lineHeight: 18,
},
modalList: {
gap: 10,
marginTop: 4,
},
modalRow: {
flexDirection: 'row',
justifyContent: 'space-between',
gap: 12,
},
modalLabel: {
fontSize: 13,
fontWeight: '500',
},
modalValue: {
fontSize: 13,
fontWeight: '800',
textAlign: 'right',
flex: 1,
},
modalActions: {
flexDirection: 'row',
gap: 12,
marginTop: 8,
},
modalButton: {
flex: 1,
},
});