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, useMemo, useState } from 'react'; import { Alert, Image, Pressable, SectionList, StyleSheet, Text, View } from 'react-native'; import Svg, { Circle, G } from 'react-native-svg'; import { colors } from '../theme/colors'; import { formatRupiah, formatRupiahShort } from '../utils/format'; import { getCategoryIcon3d } from '../utils/categoryIcons3d'; import { deleteTransaction, getExpenseByCategory, getMonthSummary, getTransactions } from '../db/repositories'; import type { TransactionType, TransactionWithCategory } from '../types'; import type { RootStackParamList } from '../navigation'; import { SwipeableRow } from '../components/SwipeableRow'; type Filter = 'all' | TransactionType; const FILTERS: Array<{ key: Filter; label: string }> = [ { key: 'all', label: 'Semua' }, { key: 'income', label: 'Pemasukan' }, { key: 'expense', label: 'Pengeluaran' }, ]; export function HistoryScreen() { const navigation = useNavigation>(); const [transactions, setTransactions] = useState([]); const [month, setMonth] = useState({ income: 0, expense: 0 }); const [filter, setFilter] = useState('all'); const [categoryData, setCategoryData] = useState< Array<{ categoryName: string; categoryIcon: string; total: number }> >([]); const load = useCallback(async () => { const [txs, m, cats] = await Promise.all([ getTransactions(500), getMonthSummary(), getExpenseByCategory(), ]); setTransactions(txs); setMonth(m); setCategoryData(cats); }, []); useFocusEffect( useCallback(() => { let active = true; load().then(() => { if (!active) return; }); return () => { active = false; }; }, [load]) ); const sections = useMemo(() => { const filtered = filter === 'all' ? transactions : transactions.filter((t) => t.type === filter); const map = new Map(); for (const t of filtered) { const key = new Date(t.occurredAt).toLocaleDateString('id-ID', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric', }); const arr = map.get(key) ?? []; arr.push(t); map.set(key, arr); } return [...map.entries()].map(([title, data]) => ({ title, data })); }, [transactions, filter]); const monthLabel = new Date().toLocaleDateString('id-ID', { month: 'long', year: 'numeric', }); const net = month.income - month.expense; const handleDelete = (item: TransactionWithCategory) => { const name = item.note || item.categoryName || 'Transaksi'; Alert.alert( 'Hapus transaksi?', `"${name}" (${item.type === 'income' ? '+' : '-'}${formatRupiah(item.amount)}) akan dihapus permanen.${ item.type === 'income' ? '\nSisihan otomatisnya juga ikut terhapus.' : '' }`, [ { text: 'Batal', style: 'cancel' }, { text: 'Hapus', style: 'destructive', onPress: async () => { try { await deleteTransaction(item.id); await load(); } catch (e) { Alert.alert('Gagal menghapus', String(e)); } }, }, ] ); }; return ( String(item.id)} contentContainerStyle={styles.listContent} stickySectionHeadersEnabled={false} ListHeaderComponent={ <> {/* Ringkasan bulan */} RINGKASAN BULAN INI {monthLabel} Pemasukan +{formatRupiah(month.income)} Pengeluaran -{formatRupiah(month.expense)} = 0 ? 'trending-up' : 'trending-down'} size={13} color="#FFFFFF" /> Selisih {net >= 0 ? '+' : '-'} {formatRupiah(Math.abs(net))} {/* Donut pengeluaran per kategori */} PENGELUARAN PER KATEGORI {categoryData.length === 0 ? ( Belum ada pengeluaran bulan ini. ) : ( {categoryData.slice(0, 5).map((c, i) => ( {c.categoryIcon} {c.categoryName} {Math.round((c.total / month.expense) * 100)}% ))} )} {/* Filter */} {FILTERS.map((f) => { const active = f.key === filter; return ( setFilter(f.key)} > {f.label} ); })} } renderSectionHeader={({ section }) => ( {section.title} )} ListEmptyComponent={ 🔍 Belum ada transaksi Transaksi yang kamu catat akan muncul di sini. } renderItem={({ item }) => { const icon3d = getCategoryIcon3d(item.categoryName, item.type); return ( navigation.navigate('EditTransaksi', { transactionId: item.id }) } onDelete={() => handleDelete(item)} > {icon3d ? ( ) : ( {item.categoryIcon ?? '•'} )} {item.note || item.categoryName || 'Tanpa kategori'} {new Date(item.occurredAt).toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit', })} {item.type === 'income' ? '+' : '-'} {formatRupiah(item.amount)} ); }} /> ); } const PALET = ['#0D9488', '#EF4444', '#F59E0B', '#3B82F6', '#8B5CF6', '#22C55E', '#EC4899', '#64748B']; function DonutChart({ data, }: { data: Array<{ categoryName: string; total: number }>; }) { const size = 132; const stroke = 20; const r = (size - stroke) / 2; const c = 2 * Math.PI * r; const total = data.reduce((s, d) => s + d.total, 0); let offset = 0; return ( {data.map((d, i) => { const frac = d.total / total; const dash = frac * c; const el = ( ); offset += dash; return el; })} TOTAL {formatRupiahShort(total)} ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: colors.background }, listContent: { paddingBottom: 110 }, card: { backgroundColor: colors.surface, borderRadius: 16, padding: 16, borderWidth: 1, borderColor: colors.border, }, // Summary card summaryCard: { marginHorizontal: 16, marginTop: 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: -40, top: -50, width: 140, height: 140, borderRadius: 70, backgroundColor: 'rgba(255,255,255,0.06)', }, ornCircleSmall: { position: 'absolute', left: -20, bottom: -30, width: 80, height: 80, borderRadius: 40, backgroundColor: 'rgba(255,255,255,0.05)', }, ornCoin: { position: 'absolute', left: -20, top: 8, width: 180, height: 124, opacity: 0.35, transform: [{ perspective: 600 }, { rotateY: '20deg' }], }, summaryTop: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', }, summaryLabel: { fontSize: 11, fontWeight: '700', letterSpacing: 1, color: 'rgba(255,255,255,0.85)', }, monthBadge: { flexDirection: 'row', alignItems: 'center', gap: 4, backgroundColor: 'rgba(255,255,255,0.92)', borderRadius: 999, paddingHorizontal: 10, paddingVertical: 4, }, monthBadgeText: { fontSize: 11, fontWeight: '700', color: colors.primaryDark }, summaryRow: { flexDirection: 'row', gap: 12, marginTop: 14 }, summaryItem: { flex: 1 }, summaryItemLabelRow: { flexDirection: 'row', alignItems: 'center', gap: 3 }, summaryItemLabel: { fontSize: 10, color: 'rgba(255,255,255,0.75)' }, summaryItemValue: { fontSize: 14, fontWeight: '800', marginTop: 4 }, // Filter filterRow: { flexDirection: 'row', gap: 8, marginHorizontal: 16, marginTop: 14, marginBottom: 4, }, filterChip: { flex: 1, alignItems: 'center', paddingVertical: 9, borderRadius: 999, backgroundColor: colors.surface, borderWidth: 1, borderColor: colors.border, }, filterChipActive: { backgroundColor: colors.primary, borderColor: colors.primary, }, filterText: { fontSize: 13, color: colors.textMuted }, // Donut donutCard: { marginHorizontal: 16, marginTop: 14 }, donutTitle: { fontSize: 11, fontWeight: '700', letterSpacing: 1, color: colors.textMuted, marginBottom: 12, }, donutEmpty: { fontSize: 13, color: colors.textMuted, textAlign: 'center', paddingVertical: 12 }, donutRow: { flexDirection: 'row', alignItems: 'center', gap: 16 }, donutWrap: { position: 'relative' }, donutCenter: { position: 'absolute', left: 0, right: 0, top: 0, bottom: 0, alignItems: 'center', justifyContent: 'center', }, donutCenterLabel: { fontSize: 9, fontWeight: '700', color: colors.textMuted }, donutCenterValue: { fontSize: 15, fontWeight: '800', color: colors.text }, legend: { flex: 1, gap: 7 }, legendRow: { flexDirection: 'row', alignItems: 'center', gap: 6 }, legendDot: { width: 10, height: 10, borderRadius: 5 }, legendIcon: { fontSize: 13 }, legendName: { flex: 1, fontSize: 12, color: colors.text, fontWeight: '600' }, legendPct: { fontSize: 12, fontWeight: '700', color: colors.textMuted }, // Section & items sectionHeader: { fontSize: 13, fontWeight: '700', color: colors.textMuted, marginHorizontal: 20, marginTop: 18, marginBottom: 8, }, txRow: { flexDirection: 'row', alignItems: 'center', backgroundColor: colors.surface, borderWidth: 1, borderColor: colors.border, paddingVertical: 12, paddingHorizontal: 14, shadowColor: '#0F172A', shadowOpacity: 0.04, shadowRadius: 6, shadowOffset: { width: 0, height: 2 }, elevation: 2, }, txIcon: { width: 40, height: 40, borderRadius: 20, alignItems: 'center', justifyContent: 'center', marginRight: 12, }, txIconText: { fontSize: 18 }, txIcon3d: { width: 34, height: 34 }, txInfo: { flex: 1 }, txName: { fontSize: 14, fontWeight: '600', color: colors.text }, txDate: { fontSize: 11, color: colors.textMuted, marginTop: 2 }, txAmount: { fontSize: 14, fontWeight: '700' }, pressed: { opacity: 0.7 }, emptyCard: { alignItems: 'center', marginHorizontal: 16, marginTop: 24, paddingVertical: 28 }, emptyEmoji: { fontSize: 38, marginBottom: 8 }, emptyTitle: { fontSize: 15, fontWeight: '700', color: colors.text }, emptyText: { color: colors.textMuted, fontSize: 13, textAlign: 'center', marginTop: 4 }, });