/** * NetWorthScreen — MD3 refactored. * Replaces system Modals with CustomBottomSheet. * Uses MD3 theme tokens (useTheme), Reanimated animations, and haptic feedback. */ import React, {useCallback, useRef, useState} from 'react'; import { View, Text, StyleSheet, ScrollView, Pressable, Alert, RefreshControl, StatusBar, } from 'react-native'; import {useFocusEffect} from '@react-navigation/native'; import {SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context'; import {useTranslation} from 'react-i18next'; import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; import {LineChart} from 'react-native-gifted-charts'; import Animated, {FadeIn, FadeInDown, FadeInUp} from 'react-native-reanimated'; import { SectionHeader, EmptyState, CustomBottomSheet, BottomSheetInput, BottomSheetChipSelector, triggerHaptic, } from '../components'; import type {CustomBottomSheetHandle} from '../components'; import {useNetWorthStore, useSettingsStore} from '../store'; import {getNetWorthHistory} from '../db'; import {formatCurrency, formatCompact} from '../utils'; import {useTheme} from '../theme'; import type {MD3Theme} from '../theme'; import {Asset, AssetType, Liability, LiabilityType, NetWorthSnapshot} from '../types'; const ASSET_TYPES: AssetType[] = [ 'Bank', 'Stocks', 'Gold', 'EPF', 'Mutual Funds', 'Fixed Deposit', 'PPF', 'Real Estate', 'Other', ]; const LIABILITY_TYPES: LiabilityType[] = [ 'Home Loan', 'Car Loan', 'Personal Loan', 'Education Loan', 'Credit Card', 'Other', ]; const ASSET_ICONS: Record = { Bank: 'bank', Stocks: 'chart-line', Gold: 'gold', EPF: 'shield-account', 'Real Estate': 'home-city', 'Mutual Funds': 'chart-areaspline', 'Fixed Deposit': 'safe', PPF: 'piggy-bank', Other: 'dots-horizontal', }; const ASSET_ICON_COLORS: Record = { Bank: '#1E88E5', Stocks: '#7E57C2', Gold: '#D4AF37', EPF: '#00ACC1', 'Real Estate': '#8D6E63', 'Mutual Funds': '#26A69A', 'Fixed Deposit': '#3949AB', PPF: '#43A047', Other: '#78909C', }; const LIABILITY_ICON_COLORS: Record = { 'Home Loan': {icon: 'home-city', color: '#EF6C00'}, 'Car Loan': {icon: 'car', color: '#5E35B1'}, 'Personal Loan': {icon: 'account-cash', color: '#E53935'}, 'Education Loan': {icon: 'school', color: '#1E88E5'}, 'Credit Card': {icon: 'credit-card', color: '#D81B60'}, Other: {icon: 'alert-circle-outline', color: '#757575'}, }; const NetWorthScreen: React.FC = () => { const {t} = useTranslation(); const baseCurrency = useSettingsStore(s => s.baseCurrency); const theme = useTheme(); const s = makeStyles(theme); const {colors, spacing, shape} = theme; const insets = useSafeAreaInsets(); const assetSheetRef = useRef(null); const liabilitySheetRef = useRef(null); const { assets, liabilities, totalAssets, totalLiabilities, netWorth, isLoading, loadNetWorth, addAsset, removeAsset, editAsset, addLiability, removeLiability, editLiability, takeSnapshot, } = useNetWorthStore(); const [history, setHistory] = useState([]); const [editingAsset, setEditingAsset] = useState(null); const [editingLiability, setEditingLiability] = useState(null); // Asset form const [assetName, setAssetName] = useState(''); const [assetType, setAssetType] = useState('Bank'); const [assetValue, setAssetValue] = useState(''); const [assetNote, setAssetNote] = useState(''); // Liability form const [liabName, setLiabName] = useState(''); const [liabType, setLiabType] = useState('Home Loan'); const [liabAmount, setLiabAmount] = useState(''); const [liabRate, setLiabRate] = useState(''); const [liabEmi, setLiabEmi] = useState(''); const [liabNote, setLiabNote] = useState(''); const loadAll = useCallback(async () => { await loadNetWorth(); const hist = await getNetWorthHistory(12); setHistory(hist); }, [loadNetWorth]); useFocusEffect(useCallback(() => { loadAll(); }, [loadAll])); // Chart data const lineData = history.map(snap => ({ value: snap.netWorth, label: snap.snapshotDate.slice(5, 10), dataPointText: formatCompact(snap.netWorth, baseCurrency), })); // Asset type chip options const assetTypeOptions = ASSET_TYPES.map(at => ({ value: at, label: at, icon: ASSET_ICONS[at], })); const liabTypeOptions = LIABILITY_TYPES.map(lt => ({ value: lt, label: lt, })); // ── Save Asset ────────────────────────────────────────────────── const handleSaveAsset = async () => { const val = parseFloat(assetValue); if (!assetName.trim() || isNaN(val) || val <= 0) { Alert.alert('Invalid', 'Please enter a valid name and value.'); return; } if (editingAsset) { await editAsset(editingAsset.id, { name: assetName.trim(), type: assetType, currentValue: val, note: assetNote, }); } else { await addAsset({ name: assetName.trim(), type: assetType, currentValue: val, currency: baseCurrency, note: assetNote, }); } triggerHaptic('notificationSuccess'); await takeSnapshot(baseCurrency); setAssetName(''); setAssetValue(''); setAssetNote(''); setEditingAsset(null); assetSheetRef.current?.dismiss(); loadAll(); }; // ── Save Liability ────────────────────────────────────────────── const handleSaveLiability = async () => { const amt = parseFloat(liabAmount); if (!liabName.trim() || isNaN(amt) || amt <= 0) { Alert.alert('Invalid', 'Please enter a valid name and amount.'); return; } if (editingLiability) { await editLiability(editingLiability.id, { name: liabName.trim(), type: liabType, outstandingAmount: amt, interestRate: parseFloat(liabRate) || 0, emiAmount: parseFloat(liabEmi) || 0, note: liabNote, }); } else { await addLiability({ name: liabName.trim(), type: liabType, outstandingAmount: amt, currency: baseCurrency, interestRate: parseFloat(liabRate) || 0, emiAmount: parseFloat(liabEmi) || 0, note: liabNote, }); } triggerHaptic('notificationSuccess'); await takeSnapshot(baseCurrency); setLiabName(''); setLiabAmount(''); setLiabRate(''); setLiabEmi(''); setLiabNote(''); setEditingLiability(null); liabilitySheetRef.current?.dismiss(); loadAll(); }; // ── Delete / Edit handlers ────────────────────────────────────── const handleDeleteAsset = (asset: Asset) => { Alert.alert('Delete Asset', `Remove "${asset.name}"?`, [ {text: 'Cancel', style: 'cancel'}, {text: 'Delete', style: 'destructive', onPress: () => { triggerHaptic('impactMedium'); removeAsset(asset.id); }}, ]); }; const handleDeleteLiability = (liab: Liability) => { Alert.alert('Delete Liability', `Remove "${liab.name}"?`, [ {text: 'Cancel', style: 'cancel'}, {text: 'Delete', style: 'destructive', onPress: () => { triggerHaptic('impactMedium'); removeLiability(liab.id); }}, ]); }; const handleEditAsset = (asset: Asset) => { setEditingAsset(asset); setAssetName(asset.name); setAssetType(asset.type as AssetType); setAssetValue(asset.currentValue.toString()); setAssetNote(asset.note || ''); assetSheetRef.current?.present(); }; const handleEditLiability = (liab: Liability) => { setEditingLiability(liab); setLiabName(liab.name); setLiabType(liab.type as LiabilityType); setLiabAmount(liab.outstandingAmount.toString()); setLiabRate(liab.interestRate.toString()); setLiabEmi(liab.emiAmount.toString()); setLiabNote(liab.note || ''); liabilitySheetRef.current?.present(); }; const handleOpenAddAsset = () => { setEditingAsset(null); setAssetName(''); setAssetType('Bank'); setAssetValue(''); setAssetNote(''); assetSheetRef.current?.present(); }; const handleOpenAddLiability = () => { setEditingLiability(null); setLiabName(''); setLiabType('Home Loan'); setLiabAmount(''); setLiabRate(''); setLiabEmi(''); setLiabNote(''); liabilitySheetRef.current?.present(); }; // ── Render ────────────────────────────────────────────────────── return ( {t('netWorth.title')} }> {/* Hero Card */} {t('dashboard.netWorth')} = 0 ? colors.success : colors.error}]} numberOfLines={1} adjustsFontSizeToFit> {formatCurrency(netWorth, baseCurrency)} Assets {formatCurrency(totalAssets, baseCurrency)} Liabilities {formatCurrency(totalLiabilities, baseCurrency)} {/* Chart */} {lineData.length > 1 && ( )} {/* Assets List */} {assets.length > 0 ? ( assets.map((asset, idx) => { const iconColor = ASSET_ICON_COLORS[asset.type as AssetType] || colors.primary; const iconName = ASSET_ICONS[asset.type as AssetType] || 'dots-horizontal'; return ( handleEditAsset(asset)} onLongPress={() => handleDeleteAsset(asset)} android_ripple={{color: colors.primary + '12'}}> {asset.name} {asset.type} {formatCurrency(asset.currentValue, baseCurrency)} ); }) ) : ( )} {/* Liabilities List */} {liabilities.length > 0 ? ( liabilities.map((liab, idx) => { const liabVisual = LIABILITY_ICON_COLORS[liab.type as LiabilityType] || LIABILITY_ICON_COLORS.Other; return ( handleEditLiability(liab)} onLongPress={() => handleDeleteLiability(liab)} android_ripple={{color: colors.primary + '12'}}> {liab.name} {liab.type} · {liab.interestRate}% · EMI {formatCompact(liab.emiAmount, baseCurrency)} {formatCurrency(liab.outstandingAmount, baseCurrency)} ); }) ) : ( )} {/* ── Asset Bottom Sheet ─────────────────────────────────────── */} assetSheetRef.current?.dismiss()}} headerRight={{label: t('common.save'), onPress: handleSaveAsset}}> setAssetType(at as AssetType)} /> {/* ── Liability Bottom Sheet ─────────────────────────────────── */} liabilitySheetRef.current?.dismiss()}} headerRight={{label: t('common.save'), onPress: handleSaveLiability}}> setLiabType(lt as LiabilityType)} /> ); }; export default NetWorthScreen; // ─── Styles ──────────────────────────────────────────────────────────── function makeStyles(theme: MD3Theme) { const {colors, typography, elevation, shape, spacing} = theme; return StyleSheet.create({ screen: {flex: 1, backgroundColor: colors.background}, header: { paddingHorizontal: spacing.xl, paddingTop: spacing.lg, paddingBottom: spacing.sm, }, headerTitle: { ...typography.headlineMedium, color: colors.onSurface, fontWeight: '700', }, heroCard: { backgroundColor: colors.surfaceContainerLow, marginHorizontal: spacing.xl, marginTop: spacing.md, borderRadius: shape.extraLarge, borderWidth: 1.5, borderColor: colors.outlineVariant, padding: spacing.xl, ...elevation.level2, }, heroLabel: { ...typography.labelSmall, color: colors.onSurfaceVariant, textTransform: 'uppercase', letterSpacing: 1, }, heroValue: { ...typography.displaySmall, fontWeight: '800', marginTop: spacing.xs, }, heroSplit: { flexDirection: 'row', marginTop: spacing.lg, paddingTop: spacing.lg, borderTopWidth: 1, borderTopColor: colors.outlineVariant + '40', }, heroSplitItem: { flex: 1, alignItems: 'center', gap: 2, }, heroSplitDivider: { width: 1, marginHorizontal: spacing.md, }, heroSplitLabel: { ...typography.bodySmall, }, heroSplitValue: { ...typography.titleSmall, fontWeight: '700', }, chartCard: { backgroundColor: colors.surfaceContainerLow, marginHorizontal: spacing.xl, borderRadius: shape.large, borderWidth: 1, borderColor: colors.outlineVariant, padding: spacing.xl, alignItems: 'center', ...elevation.level1, }, listCard: { backgroundColor: colors.surfaceContainerLow, marginHorizontal: spacing.xl, borderRadius: shape.large, borderWidth: 1, borderColor: colors.outlineVariant, overflow: 'hidden', ...elevation.level1, }, listItem: { flexDirection: 'row', alignItems: 'center', paddingVertical: spacing.lg, paddingHorizontal: spacing.lg, }, listItemBorder: { borderBottomWidth: 1, borderBottomColor: colors.outlineVariant + '30', }, listIcon: { width: 42, height: 42, borderRadius: 4, justifyContent: 'center', alignItems: 'center', }, listDetails: {flex: 1, marginLeft: spacing.md}, listName: { ...typography.bodyLarge, color: colors.onSurface, fontWeight: '600', }, listType: { ...typography.bodySmall, color: colors.onSurfaceVariant, marginTop: 2, }, listValue: { ...typography.titleSmall, fontWeight: '700', }, }); }