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}`}
>
{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 && (
)}
)}
{/* 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
{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) && (
| Tipe & Kategori |
Bank |
Keterangan |
Nominal |
Aksi |
{data.items.map((t) => (
|
{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 (
)
}