import React, { useState, useEffect, useMemo } from 'react'; import { Wallet, Building, ArrowUpCircle, ArrowDownCircle, Plus, Trash2, AlertCircle, Calendar, ChevronDown, ChevronUp, Filter, X, LogIn, UserPlus, LogOut, Lock, User } from 'lucide-react'; // --- IMPORT FIREBASE --- import { initializeApp } from 'firebase/app'; import { getAuth, signInAnonymously, signInWithCustomToken, onAuthStateChanged } from 'firebase/auth'; import { getFirestore, collection, onSnapshot, addDoc, deleteDoc, doc } from 'firebase/firestore'; // --- KONFIGURASI FIREBASE --- // ID Aplikasi (Gunakan default jika tidak ada) const appId = typeof __app_id !== 'undefined' ? __app_id : 'keuangan-app-v1'; // CATATAN UNTUK HOSTINGER: // Ganti bagian di dalam "else" dengan konfigurasi dari project Firebase Anda sendiri. const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : { apiKey: "ISI_API_KEY_ANDA", authDomain: "ISI_AUTH_DOMAIN_ANDA", projectId: "ISI_PROJECT_ID_ANDA", storageBucket: "ISI_STORAGE_BUCKET_ANDA", messagingSenderId: "ISI_MESSAGING_SENDER_ID", appId: "ISI_APP_ID_ANDA" }; // Inisialisasi Firebase const app = initializeApp(firebaseConfig); const auth = getAuth(app); const db = getFirestore(app); export default function FinanceApp() { // ========================================== // STATE AUTENTIKASI // ========================================== const [user, setUser] = useState(null); // ========================================== // STATE DATA TRANSAKSI // ========================================== const [transactions, setTransactions] = useState([]); // State Form Input const [type, setType] = useState('masuk'); const [subType, setSubType] = useState('Transfer Masuk'); const [bank, setBank] = useState('BCA'); const [inputDate, setInputDate] = useState(new Date().toISOString().split('T')[0]); const [amount, setAmount] = useState(''); const [description, setDescription] = useState(''); const [errorMsg, setErrorMsg] = useState(''); // State UI const [viewMode, setViewMode] = useState('dashboard'); // 'dashboard' | 'bank_detail' const [expandedDates, setExpandedDates] = useState([]); const [filterPreset, setFilterPreset] = useState('all'); const [filterStartDate, setFilterStartDate] = useState(''); const [filterEndDate, setFilterEndDate] = useState(''); const [filterBank, setFilterBank] = useState('all'); const [filterCategory, setFilterCategory] = useState('all'); // ========================================== // 1. INISIALISASI AKSES OTOMATIS // ========================================== useEffect(() => { const initAuth = async () => { try { if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) { await signInWithCustomToken(auth, __initial_auth_token); } else { await signInAnonymously(auth); } } catch (error) { console.error("Auth error:", error); } }; initAuth(); const unsubscribe = onAuthStateChanged(auth, (currentUser) => { setUser(currentUser); }); return () => unsubscribe(); }, []); // ========================================== // 2. AMBIL DATA DARI DATABASE (FIRESTORE) // ========================================== useEffect(() => { if (!user) { setTransactions([]); return; // Jangan ambil data jika belum login } // Path Koleksi: /artifacts/{appId}/users/{userId}/transactions const txRef = collection(db, 'artifacts', appId, 'users', user.uid, 'transactions'); // Mengambil data secara real-time (onSnapshot) const unsubscribe = onSnapshot(txRef, (snapshot) => { const fetchedData = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })); // Urutkan data berdasarkan kapan dibuat (terbaru di atas) fetchedData.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); setTransactions(fetchedData); }, (error) => { console.error("Error fetching transactions: ", error); setErrorMsg("Gagal memuat data dari server."); }); return () => unsubscribe(); }, [user]); // ========================================== // FUNGSI CRUD TRANSAKSI KE DATABASE // ========================================== const handleSubmitTx = async (e) => { e.preventDefault(); setErrorMsg(''); if (!amount || isNaN(amount) || amount <= 0) { setErrorMsg('Masukkan nominal saldo yang valid (lebih dari 0).'); return; } if (!description.trim()) { setErrorMsg('Keterangan tidak boleh kosong.'); return; } if (!inputDate) { setErrorMsg('Tanggal harus diisi.'); return; } try { const newTx = { date: inputDate, type, subType, bank, amount: parseFloat(amount), description: description.trim(), createdAt: new Date().toISOString() // Untuk pengurutan }; const txRef = collection(db, 'artifacts', appId, 'users', user.uid, 'transactions'); await addDoc(txRef, newTx); if (!expandedDates.includes(inputDate)) { setExpandedDates(prev => [...prev, inputDate]); } setAmount(''); setDescription(''); } catch (err) { setErrorMsg('Gagal menyimpan transaksi: ' + err.message); } }; const handleDeleteTx = async (id) => { try { const docRef = doc(db, 'artifacts', appId, 'users', user.uid, 'transactions', id); await deleteDoc(docRef); } catch (err) { setErrorMsg('Gagal menghapus transaksi: ' + err.message); } }; // ========================================== // LOGIKA FORMAT & FILTERING // ========================================== const balances = useMemo(() => { const b = { BCA: 0, Mandiri: 0, BRI: 0, Total: 0 }; transactions.forEach(t => { const val = parseFloat(t.amount); if (t.type === 'masuk') { b[t.bank] += val; b.Total += val; } else { b[t.bank] -= val; b.Total -= val; } }); return b; }, [transactions]); const formatRupiah = (number) => { return new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0, maximumFractionDigits: 0 }).format(number); }; const formatTanggal = (dateString) => { const date = new Date(dateString); return date.toLocaleDateString('id-ID', { day: '2-digit', month: 'long', year: 'numeric' }); }; const formatDateToYMD = (date) => { const d = new Date(date); let month = '' + (d.getMonth() + 1); let day = '' + d.getDate(); const year = d.getFullYear(); if (month.length < 2) month = '0' + month; if (day.length < 2) day = '0' + day; return [year, month, day].join('-'); }; const handleTypeChange = (newType) => { setType(newType); if (newType === 'masuk') { setSubType('Transfer Masuk'); } else { setSubType('Top Up'); } }; const handlePresetChange = (e) => { const val = e.target.value; setFilterPreset(val); const today = new Date(); if (val === 'all') { setFilterStartDate(''); setFilterEndDate(''); } else if (val === 'today') { const ymd = formatDateToYMD(today); setFilterStartDate(ymd); setFilterEndDate(ymd); } else if (val === 'yesterday') { const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); const ymd = formatDateToYMD(yesterday); setFilterStartDate(ymd); setFilterEndDate(ymd); } else if (val === '7days') { const last7 = new Date(today); last7.setDate(last7.getDate() - 6); setFilterStartDate(formatDateToYMD(last7)); setFilterEndDate(formatDateToYMD(today)); } else if (val === 'thisMonth') { const start = new Date(today.getFullYear(), today.getMonth(), 1); const end = new Date(today.getFullYear(), today.getMonth() + 1, 0); setFilterStartDate(formatDateToYMD(start)); setFilterEndDate(formatDateToYMD(end)); } else if (val === 'lastMonth') { const start = new Date(today.getFullYear(), today.getMonth() - 1, 1); const end = new Date(today.getFullYear(), today.getMonth(), 0); setFilterStartDate(formatDateToYMD(start)); setFilterEndDate(formatDateToYMD(end)); } else if (val === 'thisYear') { const start = new Date(today.getFullYear(), 0, 1); const end = new Date(today.getFullYear(), 11, 31); setFilterStartDate(formatDateToYMD(start)); setFilterEndDate(formatDateToYMD(end)); } else if (val === 'lastYear') { const start = new Date(today.getFullYear() - 1, 0, 1); const end = new Date(today.getFullYear() - 1, 11, 31); setFilterStartDate(formatDateToYMD(start)); setFilterEndDate(formatDateToYMD(end)); } }; const filteredTransactions = useMemo(() => { return transactions.filter(t => { const d = t.date.includes('T') ? t.date.split('T')[0] : t.date; let isValid = true; if (filterStartDate) isValid = isValid && (d >= filterStartDate); if (filterEndDate) isValid = isValid && (d <= filterEndDate); if (filterBank !== 'all') isValid = isValid && (t.bank === filterBank); if (filterCategory !== 'all') isValid = isValid && (t.subType === filterCategory); return isValid; }); }, [transactions, filterStartDate, filterEndDate, filterBank, filterCategory]); const groupedTransactions = useMemo(() => { const groups = {}; filteredTransactions.forEach(t => { const d = t.date.includes('T') ? t.date.split('T')[0] : t.date; if (!groups[d]) groups[d] = { items: [], totalMasuk: 0, totalKeluar: 0 }; groups[d].items.push(t); if (t.type === 'masuk') groups[d].totalMasuk += parseFloat(t.amount); else groups[d].totalKeluar += parseFloat(t.amount); }); return Object.entries(groups).sort((a, b) => new Date(b[0]) - new Date(a[0])); }, [filteredTransactions]); const toggleDate = (dateStr) => { setExpandedDates(prev => prev.includes(dateStr) ? prev.filter(d => d !== dateStr) : [...prev, dateStr]); }; const getCategoryOptions = () => { const masuk = ["Transfer Masuk", "Saldo Mengantar", "Lainnya"]; const keluar = ["Top Up", "Refund", "Admin Fee", "HPP", "Lainnya"]; return Array.from(new Set([...masuk, ...keluar])); }; // ========================================== // RENDER: SCREEN LOADING // ========================================== if (!user) { return (
); } // ========================================== // RENDER: APLIKASI UTAMA (DASHBOARD) // ========================================== return (
{/* Header Dashboard */}

Rekap Keuangan Belanja Online

Akses Langsung Tanpa Login Aktif

{/* Dashboard Saldo (Cards) */}
{ setFilterBank('all'); setViewMode('dashboard'); }} className="bg-blue-600 rounded-xl p-5 text-white shadow-md relative overflow-hidden cursor-pointer hover:bg-blue-700 hover:shadow-lg transition-all group" title="Klik untuk tampilkan semua riwayat" >

Total Saldo Keseluruhan

{formatRupiah(balances.Total)}

Klik untuk kembali ke dashboard utama →

{['BCA', 'Mandiri', 'BRI'].map((bankName) => (
{ setFilterBank(bankName); setViewMode('bank_detail'); }} className={`bg-white border-2 rounded-xl p-5 shadow-sm cursor-pointer transition-all hover:shadow-md group ${ filterBank === bankName ? 'border-blue-400 bg-blue-50/50' : 'border-gray-200 hover:border-blue-300' }`} title={`Klik untuk lihat riwayat bank ${bankName}`} >

Saldo {bankName}

{formatRupiah(balances[bankName])}

Klik untuk masuk menu khusus {bankName} →

))}
{/* Main Content Layout */}
{/* Kolom Kiri: Form Input (Disembunyikan saat mode detail bank) */} {viewMode === 'dashboard' && (

Tambah Transaksi

{errorMsg && (
{errorMsg}
)}
{/* Jenis Transaksi */}
{/* Kategori Transaksi */}
{/* Bank & Tanggal (Baris Berdampingan) */}
setInputDate(e.target.value)} className="w-full border border-gray-300 rounded-lg p-2.5 outline-none focus:ring-2 focus:ring-blue-500 text-gray-800 bg-white" />
{/* Nominal */}
setAmount(e.target.value.replace(/\D/g, ''))} placeholder="Contoh: 150.000" className="w-full border border-gray-300 rounded-lg p-2.5 outline-none focus:ring-2 focus:ring-blue-500 text-gray-800 font-bold" />
{/* Keterangan */}
)} {/* Kolom Kanan: Tabel Riwayat */}
{/* Header Khusus Mode Detail Bank */} {viewMode === 'bank_detail' && (

Menu Khusus: Riwayat {filterBank}

Total Saldo di Bank ini: {formatRupiah(balances[filterBank])}

Status

{filteredTransactions.length} Transaksi Tercatat

)} {/* Filter Area Baru */}

{viewMode === 'dashboard' ? 'Riwayat Cloud' : `Filter Riwayat ${filterBank}`}

{filteredTransactions.length} Data
{viewMode === 'dashboard' && ( )}
{ setFilterStartDate(e.target.value); setFilterPreset('custom'); }} className="w-full text-xs border border-gray-300 rounded-lg p-2" title="Dari Tanggal"/> { setFilterEndDate(e.target.value); setFilterPreset('custom'); }} className="w-full text-xs border border-gray-300 rounded-lg p-2" title="Sampai Tanggal"/>
{filteredTransactions.length === 0 ? (

Tidak ada transaksi yang ditemukan.

) : (
{groupedTransactions.map(([dateStr, data]) => (
toggleDate(dateStr)} className="flex flex-col sm:flex-row sm:items-center justify-between p-4 cursor-pointer hover:bg-gray-50 transition-colors gap-3 select-none" >

{formatTanggal(dateStr)}

{formatRupiah(data.totalMasuk)} {formatRupiah(data.totalKeluar)}
{expandedDates.includes(dateStr) ? : }
{expandedDates.includes(dateStr) && (
{data.items.map((t) => ( ))}
Tipe & Kategori Bank Keterangan Nominal Aksi
{t.type === 'masuk' ? 'Masuk' : 'Keluar'} {t.subType || '-'}
{t.bank} {t.description} {t.type === 'masuk' ? '+' : '-'}{formatRupiah(t.amount)}
)}
))}
)}
); } // Simple Icon component for the UI function DatabaseIcon() { return ( ) }
-