import { Ionicons } from '@expo/vector-icons'; import { LinearGradient } from 'expo-linear-gradient'; import { useFocusEffect, useNavigation } from '@react-navigation/native'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { useCallback, useState } from 'react'; import { Alert, Image, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'; import { colors } from '../theme/colors'; import { formatDateTime, formatRupiah, formatRupiahShort } from '../utils/format'; import { deleteSavingsTarget, getSavingsEvents, getSavingsMonthComparison, getSavingsStreak, getSavingsTargets, getSavingsTotal, getSettingNumber, getTransactionCount, } from '../db/repositories'; import type { SavingsEvent, SavingsTarget } from '../types'; import type { RootStackParamList } from '../navigation'; interface BadgeDef { icon: string; title: string; desc: string; check: (s: { streak: number; savings: number; txCount: number }) => boolean; } const BADGES: BadgeDef[] = [ { icon: '๐Ÿ”ฅ', title: 'Konsisten', desc: 'Nabung 4 minggu berturut', check: (s) => s.streak >= 4 }, { icon: 'โšก', title: 'Mesin Nabung', desc: 'Nabung 8 minggu berturut', check: (s) => s.streak >= 8 }, { icon: '๐Ÿท', title: 'Pemula', desc: 'Tabungan Rp 100 rb', check: (s) => s.savings >= 100000 }, { icon: '๐Ÿ’ฐ', title: 'Aktif', desc: 'Tabungan Rp 500 rb', check: (s) => s.savings >= 500000 }, { icon: '๐Ÿค‘', title: 'Jutawan Kecil', desc: 'Tabungan Rp 1 jt', check: (s) => s.savings >= 1000000 }, { icon: '๐Ÿ’Ž', title: 'Jago Sisih', desc: 'Tabungan Rp 5 jt', check: (s) => s.savings >= 5000000 }, { icon: '๐Ÿ“', title: 'Rajin Catat', desc: '50 transaksi tercatat', check: (s) => s.txCount >= 50 }, { icon: 'โœ๏ธ', title: 'Sultan Catat', desc: '100 transaksi tercatat', check: (s) => s.txCount >= 100 }, ]; export function SavingsScreen() { const navigation = useNavigation>(); const [total, setTotal] = useState(0); const [streak, setStreak] = useState(0); const [percent, setPercent] = useState(10); const [events, setEvents] = useState([]); const [compare, setCompare] = useState({ current: 0, previous: 0 }); const [targets, setTargets] = useState([]); const [txCount, setTxCount] = useState(0); const load = useCallback(async () => { const [t, s, p, e, c, tg, tx] = await Promise.all([ getSavingsTotal(), getSavingsStreak(), getSettingNumber('auto_save_percent', 10), getSavingsEvents(50), getSavingsMonthComparison(), getSavingsTargets(), getTransactionCount(), ]); setTotal(t); setStreak(s); setPercent(p); setEvents(e); setCompare(c); setTargets(tg); setTxCount(tx); }, []); useFocusEffect( useCallback(() => { let active = true; load().catch(() => {}); return () => { active = false; }; }, [load]) ); const handleDeleteTarget = (target: SavingsTarget) => { Alert.alert( 'Hapus target?', `Target "${target.name}" akan dihapus. Tabunganmu tetap aman.`, [ { text: 'Batal', style: 'cancel' }, { text: 'Hapus', style: 'destructive', onPress: async () => { await deleteSavingsTarget(target.id); setTargets(await getSavingsTargets()); }, }, ] ); }; const badgeStats = { streak, savings: total, txCount }; return ( {/* Total tabungan */} TOTAL TABUNGAN {formatRupiah(total)} โœจ disisihkan otomatis {percent}% dari setiap pemasukan {/* Streak */} ๐Ÿ”ฅ {streak} minggu berturut-turut {streak === 0 ? 'Catat pemasukan pertamamu untuk memulai streak!' : 'Pertahankan! Jangan sampai putus minggu ini.'} {/* Target Tabungan */} Target Tabungan navigation.navigate('TargetForm')} hitSlop={8}> Tambah {targets.length === 0 ? ( ๐ŸŽฏ Belum ada target Bikin target nyata (misal Dana Darurat 5 jt) โ€” progress-nya bikin nabung makin semangat! ) : ( targets.map((target) => { const pct = target.targetAmount > 0 ? Math.min(100, Math.round((total / target.targetAmount) * 100)) : 0; const remaining = Math.max(0, target.targetAmount - total); const now = new Date(); const dayOfMonth = now.getDate(); const perDay = dayOfMonth > 0 ? compare.current / dayOfMonth : 0; const eta = remaining > 0 && perDay > 0 ? new Date(now.getTime() + Math.ceil(remaining / perDay) * 86400000) : null; return ( {target.icon} {target.name} handleDeleteTarget(target)} hitSlop={8}> {formatRupiahShort(total)} dari {formatRupiahShort(target.targetAmount)} {pct}% {remaining <= 0 ? '๐ŸŽ‰ Tercapai! Target kelar โ€” bikin target baru!' : eta ? `ยฑ selesai ${eta.toLocaleDateString('id-ID', { day: 'numeric', month: 'short', })} dengan ritme saat ini` : 'Catat pemasukan untuk mulai estimasi'} ); }) )} {/* Pencapaian */} Pencapaian {BADGES.map((b) => { const unlocked = b.check(badgeStats); return ( {b.icon} {b.title} {b.desc} ); })} Riwayat sisihan {events.length === 0 ? ( ๐Ÿท Belum ada sisihan Setiap pemasukan yang kamu catat akan otomatis menyisihkan {percent}% ke sini. Mulai dari catat pemasukan pertamamu! ) : ( events.map((e) => ( {e.transactionNote || 'Pemasukan'} {formatDateTime(e.occurredAt)} +{formatRupiah(e.amount)} )) )} ); } function ComparisonCard({ current, previous }: { current: number; previous: number }) { const pct = previous > 0 ? Math.round(((current - previous) / previous) * 100) : null; let icon: React.ComponentProps['name'] = 'remove'; let title = ''; let sub = `Bulan ini: ${formatRupiahShort(current)} ยท Bulan lalu: ${formatRupiahShort(previous)}`; let cardStyle = styles.compareNeutral; let iconColor = colors.textMuted; if (current === 0 && previous === 0) { title = 'Belum ada sisihan bulan ini'; sub = 'Catat pemasukan untuk memulai! ๐Ÿš€'; } else if (previous === 0) { icon = 'sparkles'; iconColor = colors.primaryDark; cardStyle = styles.compareUp; title = 'Bulan ini mulai nabung โ€” hebat! ๐ŸŽ‰'; sub = `Tabungan bulan ini: ${formatRupiahShort(current)}`; } else if (pct === null || pct === 0) { icon = 'remove'; title = 'Stabil, sama seperti bulan lalu'; sub = `${sub} โ€” Konsisten itu keren!`; } else if (pct > 0) { icon = 'trending-up'; iconColor = colors.income; cardStyle = styles.compareUp; title = `Naik ${pct}% dari bulan lalu โ€” pertahankan! ๐Ÿ”ฅ`; } else { icon = 'trending-down'; iconColor = '#D97706'; cardStyle = styles.compareDown; title = `Turun ${Math.abs(pct)}% dari bulan lalu`; sub = `${sub} โ€” Santai, bulan ini masih panjang. Yang penting konsisten.`; } return ( {title} {sub} ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: colors.background }, content: { padding: 16, paddingBottom: 110 }, card: { backgroundColor: colors.surface, borderRadius: 16, padding: 16, borderWidth: 1, borderColor: colors.border, shadowColor: '#0F172A', shadowOpacity: 0.04, shadowRadius: 6, shadowOffset: { width: 0, height: 2 }, elevation: 2, }, // Total card totalCard: { marginBottom: 12, borderRadius: 20, padding: 18, overflow: 'hidden', shadowColor: colors.primaryDark, shadowOpacity: 0.25, shadowRadius: 12, shadowOffset: { width: 0, height: 6 }, elevation: 6, }, ornCircleLarge: { position: 'absolute', right: -50, top: -60, width: 180, height: 180, borderRadius: 90, backgroundColor: 'rgba(255,255,255,0.06)', }, ornCircleSmall: { position: 'absolute', left: -24, bottom: -40, width: 100, height: 100, borderRadius: 50, backgroundColor: 'rgba(255,255,255,0.05)', }, totalPiggy: { position: 'absolute', right: -52, bottom: -16, width: 186, height: 151, opacity: 0.55, transform: [{ perspective: 600 }, { rotateY: '-18deg' }, { scaleX: -1 }], }, totalTop: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingRight: 120, }, totalLabel: { fontSize: 11, fontWeight: '700', letterSpacing: 1, color: 'rgba(255,255,255,0.85)', }, totalBadge: { backgroundColor: 'rgba(255,255,255,0.9)', borderRadius: 12, padding: 5, }, totalValue: { fontSize: 32, fontWeight: '800', color: colors.white, marginTop: 8, paddingRight: 110, }, totalHint: { fontSize: 12, color: 'rgba(255,255,255,0.85)', marginTop: 8 }, // Streak streakCard: { flexDirection: 'row', alignItems: 'center', marginBottom: 12, borderColor: '#FDE68A', overflow: 'hidden', }, streakFlame: { position: 'absolute', right: -6, top: -10, }, streakIconCircle: { width: 38, height: 38, borderRadius: 19, alignItems: 'center', justifyContent: 'center', marginRight: 12, }, streakInfo: { flex: 1, paddingRight: 60 }, streakValue: { fontSize: 15, fontWeight: '700' }, streakHint: { fontSize: 12, color: colors.textMuted, marginTop: 2 }, // Comparison compareCard: { flexDirection: 'row', alignItems: 'center', marginBottom: 20, }, compareUp: { backgroundColor: colors.incomeSoft, borderColor: colors.incomeSoft }, compareDown: { backgroundColor: '#FEF3C7', borderColor: '#FEF3C7' }, compareNeutral: {}, compareIconCircle: { width: 36, height: 36, borderRadius: 18, alignItems: 'center', justifyContent: 'center', marginRight: 12, }, compareInfo: { flex: 1 }, compareTitle: { fontSize: 14, fontWeight: '700', color: colors.text }, compareSub: { fontSize: 12, color: colors.textMuted, marginTop: 2, lineHeight: 17 }, // Section sectionHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginTop: 6, marginBottom: 10, }, sectionTitle: { fontSize: 16, fontWeight: '700', color: colors.text, marginBottom: 10, marginTop: 6, }, addTargetBtn: { flexDirection: 'row', alignItems: 'center', gap: 4, backgroundColor: colors.primary, borderRadius: 999, paddingHorizontal: 12, paddingVertical: 6, }, addTargetText: { color: colors.white, fontWeight: '700', fontSize: 12 }, // Target targetCard: { marginBottom: 10 }, targetTop: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10, }, targetTitleRow: { flexDirection: 'row', alignItems: 'center', gap: 8, flex: 1 }, targetEmoji: { fontSize: 20 }, targetName: { fontSize: 15, fontWeight: '700', color: colors.text, flex: 1 }, targetTrack: { height: 10, borderRadius: 5, backgroundColor: colors.background, overflow: 'hidden', }, targetFill: { height: '100%', borderRadius: 5, backgroundColor: colors.primary }, targetMeta: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 6, }, targetAmount: { fontSize: 12, color: colors.textMuted }, targetPct: { fontSize: 12, fontWeight: '800', color: colors.primaryDark }, targetEta: { fontSize: 11, color: colors.textMuted, marginTop: 4, lineHeight: 15 }, // Badge badgeGrid: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginBottom: 16 }, badgeItem: { width: '31%', backgroundColor: colors.surface, borderWidth: 1, borderColor: '#BBF7D0', borderRadius: 14, paddingVertical: 12, paddingHorizontal: 6, alignItems: 'center', }, badgeItemLocked: { borderColor: colors.border, opacity: 0.45 }, badgeIcon: { fontSize: 26, marginBottom: 4 }, badgeIconLocked: { opacity: 0.5 }, badgeTitle: { fontSize: 12, fontWeight: '700', color: colors.text }, badgeDesc: { fontSize: 10, color: colors.textMuted, textAlign: 'center', marginTop: 2 }, // Riwayat emptyCard: { alignItems: 'center', paddingVertical: 26, marginBottom: 12 }, emptyEmoji: { fontSize: 38, marginBottom: 8 }, emptyTitle: { fontSize: 15, fontWeight: '700', color: colors.text }, emptyText: { color: colors.textMuted, fontSize: 13, textAlign: 'center', marginTop: 4, lineHeight: 19, }, eventRow: { flexDirection: 'row', alignItems: 'center', paddingVertical: 12, marginBottom: 8, }, eventIcon: { width: 38, height: 38, borderRadius: 19, backgroundColor: colors.primarySoft, alignItems: 'center', justifyContent: 'center', marginRight: 12, }, eventInfo: { flex: 1 }, eventName: { fontSize: 14, fontWeight: '600', color: colors.text }, eventDate: { fontSize: 11, color: colors.textMuted, marginTop: 2 }, eventAmount: { fontSize: 15, fontWeight: '800', color: colors.primaryDark }, });