// screen-dashboard.jsx — Beranda / Home (priority screen)
function ScreenDashboard({ theme, accent, scenario, hideBalance, onToggleHide, onFab, onNavigate, onOpenSheet, auth }) {
const [realTx, setRealTx] = React.useState(null);
const [realBudgets, setRealBudgets] = React.useState(null);
const familyId = auth && auth.family && auth.family.id;
React.useEffect(function() {
if (!familyId) return;
function reload() {
if (window.sbGetTransactions) sbGetTransactions(familyId).then(function(r) { if (!r.error) setRealTx(r.data || []); });
if (window.sbGetBudgets) sbGetBudgets(familyId).then(function(r) { if (!r.error) setRealBudgets(r.data || []); });
}
reload();
window.addEventListener('tx-added', reload);
return function() { window.removeEventListener('tx-added', reload); };
}, [familyId]);
const s = scenario;
const isAuth = !!(auth && auth.family);
// Ketika authenticated, JANGAN pakai dummy — pakai real data (bisa [] kalau kosong)
const transactions = isAuth ? (realTx || []) : s.transactions;
const budgets = isAuth ? (realBudgets || []) : s.budgets;
const incomeTotal = transactions.filter(function(t){ return (t.type||t.kind)==='income'; }).reduce(function(a,t){ return a+t.amount; }, 0);
const expenseTotal = transactions.filter(function(t){ return (t.type||t.kind)==='expense'; }).reduce(function(a,t){ return a+t.amount; }, 0);
const monthDelta = isAuth ? incomeTotal - expenseTotal : s.monthlyIncome - s.monthlyExpense;
const overBudgets = isAuth
? budgets.filter(function(b){ return (b.used_pct||0) >= 90; })
: s.budgets.filter(function(b){ return b.used/b.limit >= 0.9; });
// Patch tx dari Supabase ke shape yang dipahami TxRow
function patchTx(tx) {
if (tx.kind) return tx;
// Map category name to CATEGORIES key
var catName = tx.category ? (tx.category.name||'').toLowerCase().replace(/\s+/g,'_') : 'lainnya_ex';
// Try to find matching key in CATEGORIES
var catKey = catName;
if (!CATEGORIES[catKey]) {
// Try with _ex or _in suffix
catKey = catName + '_ex';
if (!CATEGORIES[catKey]) catKey = catName + '_in';
if (!CATEGORIES[catKey]) catKey = (tx.type==='income' ? 'lainnya_in' : 'lainnya_ex');
}
return Object.assign({}, tx, {
kind: tx.type,
cat: catKey,
by: tx.user_id,
profileObj: tx.profile,
});
}
const recent = transactions.slice(0, 5).map(patchTx);
const patchedRecent = recent;
// Build displayScenario — real data jika authenticated, dummy jika demo
var memberShort = isAuth && auth.profile
? ((auth.profile.full_name || '').split(' ')[0] || 'Kamu')
: s.members[0].short;
var displayScenario = isAuth ? Object.assign({}, s, {
family: { name: auth.family.name, code: auth.family.invite_code },
members: [{ id: auth.profile && auth.profile.id, name: auth.profile && auth.profile.full_name || 'Kamu', short: memberShort, role: 'admin', color: '#10b981' }],
balance: monthDelta,
monthlyIncome: incomeTotal,
monthlyExpense: expenseTotal,
transactions: transactions.map(patchTx),
budgets: budgets,
activity: [],
week: [0,0,0,0,0,0,0],
weekLabels: ['Sen','Sel','Rab','Kam','Jum','Sab','Min'],
}) : s;
return (
{overBudgets.length > 0 && (
onNavigate('budget')} />
)}
onNavigate('tx')} />
);
}
// ─── Top app bar ────────────────────────────────────────────────
function DashHeader({ theme, accent, scenario }) {
const me = scenario.members[0]; // assume "me" is first member
return (
Halo, {me?.short || me?.name?.split(' ')[0] || 'Bram'} 👋
{scenario.family.name}
);
}
function iconBtn(theme) {
return {
appearance: 'none', border: 0, cursor: 'pointer',
width: 40, height: 40, borderRadius: 12,
background: theme.surface, position: 'relative',
display: 'flex', alignItems: 'center', justifyContent: 'center',
boxShadow: theme.shadow1,
border: `0.5px solid ${theme.border}`,
};
}
// ─── Balance card (gradient hero) ───────────────────────────────
function BalanceCard({ theme, accent, scenario, hide, onToggleHide, auth }) {
const s = scenario;
const monthDelta = s.monthlyIncome - s.monthlyExpense;
const [breakdown, setBreakdown] = React.useState(null);
const familyId = auth && auth.family && auth.family.id;
React.useEffect(function() {
if (!familyId || !window.SB) return;
function loadBreakdown() {
Promise.all([
window.SB.from('accounts').select('balance,type,cost_basis,is_active,credit_limit').eq('family_id', familyId).eq('is_active', true),
window.SB.from('savings_goals').select('current_amount,account_id').eq('family_id', familyId),
]).then(function(res) {
var accs = res[0].data || [];
var goals = res[1].data || [];
// totalPocket = sum current_amount semua goals aktif
var totalPocket = goals.reduce(function(s,g){return s+(g.current_amount||0);}, 0);
var investasi = accs.filter(function(a){return a.type==='investment';}).reduce(function(s,a){return s+a.balance;}, 0);
var investasiCost = accs.filter(function(a){return a.type==='investment'&&a.cost_basis>0;}).reduce(function(s,a){return s+a.cost_basis;}, 0);
var hutangKK = accs.filter(function(a){return a.type==='credit';}).reduce(function(s,a){return s+a.balance;}, 0);
// kasTotal = hanya rekening bank/cash/ewallet (bukan investasi, bukan kredit)
var kasTotal = accs.filter(function(a){return a.type==='bank'||a.type==='cash'||a.type==='ewallet';}).reduce(function(s,a){return s+a.balance;}, 0);
// kasTotal = saldo fisik total (sudah include pocket di dalamnya)
// tersedia = bebas dipakai = kasTotal - pocket
var aktif = kasTotal;
var tersedia = kasTotal - totalPocket;
// netWorth = kas + investasi - hutang KK
var netWorth = kasTotal + investasi - hutangKK;
setBreakdown({
aktif, tersedia, pocket: totalPocket, investasi,
investasiPnl: investasi - investasiCost,
hutangKK, kasTotal, netWorth,
});
});
}
loadBreakdown();
window.addEventListener('tx-added', loadBreakdown);
return function() { window.removeEventListener('tx-added', loadBreakdown); };
}, [familyId]);
return (
{/* decorative orbs */}
{/* Label saldo */}
{breakdown ? 'Net Worth' : 'Saldo bulan ini'}
{fmtRp(breakdown ? breakdown.netWorth : s.balance, { hide })}
{/* Breakdown aktif / pocket / investasi */}
{breakdown ? (
{[
{ label:'Bebas', val: breakdown.tersedia, icon:'✓', note: 'diluar pocket' },
{ label:'Pocket', val: breakdown.pocket, icon:'🔒', note: 'ada di rekening' },
{ label:'Investasi', val: breakdown.investasi, icon:'📈', note: breakdown.investasiPnl !== 0 ? (breakdown.investasiPnl>0?'+':'')+fmtRp(breakdown.investasiPnl,{short:true})+' P&L' : 'portofolio' },
].map(function(item) {
return (
{item.icon} {item.label}
{hide ? '••••' : fmtRp(item.val,{short:true})}
{item.note &&
{item.note}
}
);
})}
{breakdown.hutangKK > 0 && (
💳 Tagihan KK: -{hide ? '••••' : fmtRp(breakdown.hutangKK,{short:true})}
)}
) : null}
);
}
function DeltaCol({ label, amount, hide, positive }) {
const isPos = positive || amount > 0;
return (
{label}
{fmtRp(amount, { short: true, sign: !positive && amount !== 0, hide })}
);
}
// ─── Quick actions ──────────────────────────────────────────────
function QuickActions({ theme, accent, onOpenSheet }) {
return (
onOpenSheet('income')} />
onOpenSheet('expense')} />
);
}
// ─── 7-day chart card ───────────────────────────────────────────
function WeekChartCard({ theme, accent, scenario }) {
const total = scenario.week.reduce((a, b) => a + b, 0);
const avg = total / scenario.week.length;
return (
Pengeluaran 7 hari
{fmtRp(total)}
12% lebih hemat
Rata-rata · {fmtRp(avg, { short: true })}/hari
Tertinggi · {fmtRp(Math.max(...scenario.week), { short: true })}
);
}
// ─── Budget alerts (over-budget chips) ──────────────────────────
function BudgetAlerts({ theme, accent, budgets, onSeeAll }) {
return (
{budgets.map((b) => {
const c = CATEGORIES[b.cat];
const pct = Math.min(100, Math.round((b.used / b.limit) * 100));
const isOver = b.used > b.limit;
const color = pct >= 100 ? theme.danger : pct >= 90 ? theme.warning : theme.accent[500];
return (
{c.label}
{isOver &&
Over }
{pct}%
{fmtRp(b.used, { short: true })} / {fmtRp(b.limit, { short: true })}
);
})}
);
}
// ─── Recent transactions ────────────────────────────────────────
function RecentTransactions({ theme, accent, scenario, items, onSeeAll }) {
return (
{items.map((tx, i) => (
))}
);
}
// Single transaction row — reused across Dashboard & Tx list
function TxRow({ tx, scenario, theme, accent, divider }) {
const cat = CATEGORIES[tx.cat] || { label: tx.cat || 'Lainnya', color: '#94a3b8', icon: '📋', kind: tx.kind||'expense' };
const m = getMember(scenario, tx.by) || {
short: tx.profileObj?.full_name?.split(' ')[0] || tx.profile?.full_name?.split(' ')[0] || 'Kamu',
name: tx.profileObj?.full_name || tx.profile?.full_name || 'Anggota',
color: '#10b981',
};
const isIncome = tx.kind === 'income';
return (
{tx.note || cat.label}
{m.short}
·
{fmtRelDay(tx.date)}
·
{cat.label}
{fmtRp(tx.amount, { sign: true, short: tx.amount >= 1_000_000 })
.replace('+', isIncome ? '+' : '')
.replace('−', '−')}
{!isIncome && !String(tx.amount).startsWith('−') && tx.kind === 'expense' &&
/* fmtRp already handles negative; we keep formatting consistent */ ''}
);
}
// ─── Activity feed (WhatsApp-style) ─────────────────────────────
function ActivityFeed({ theme, accent, scenario }) {
return (
{scenario.activity.map((a, i) => {
const m = getMember(scenario, a.m) || {
short: 'Kamu', name: 'Anggota', color: '#10b981',
};
const isLast = i === scenario.activity.length - 1;
return (
{m.short}
{' '}{a.verb}{' '}
{a.what}
{fmtRelDay(a.ts)} · {fmtTime(a.ts)}
);
})}
);
}
function FireWidget({ theme, accent, auth, monthlyExpense, onOpenFire }) {
var T = theme;
var _fw1 = React.useState(0), netWorth = _fw1[0], setNetWorth = _fw1[1];
var familyId = auth && auth.family && auth.family.id;
React.useEffect(function() {
if (!familyId || !window.SB) return;
window.SB.from('accounts').select('balance,type').eq('family_id', familyId).eq('is_active', true)
.then(function(r) {
if (!r.error && r.data) {
var nw = r.data.filter(function(a){return a.type!=='credit';}).reduce(function(s,a){return s+a.balance;},0);
setNetWorth(nw);
}
});
}, [familyId]);
var monthlyExp = monthlyExpense || 0;
var annualExp = monthlyExp * 12;
var fireNum = annualExp > 0 ? annualExp / 0.04 : 0;
var pct = fireNum > 0 ? Math.min(100, Math.round((netWorth / fireNum) * 100)) : 0;
// Monthly savings needed (30yr horizon, 10% return)
var r = 0.10 / 12, n = 30 * 12;
var fvExisting = netWorth * Math.pow(1+r, n);
var gap = Math.max(0, fireNum - fvExisting);
var needPerMonth = gap > 0 ? Math.round(gap * r / (Math.pow(1+r, n) - 1)) : 0;
if (!auth || !auth.family) return null;
if (fireNum === 0 && netWorth === 0) return null;
return (
🔥
FIRE Progress
{pct}%
menuju Financial Independence
Target FIRE
{fmtRp(fireNum, {short:true})}
{netWorth > 0 && (
{fmtRp(netWorth, {short:true})} terkumpul
)}
=100 ? '#10b981'
: 'linear-gradient(90deg,'+accent.grad[0]+','+accent.grad[1]+')' }}/>
{pct >= 100 ? (
🎉 Kamu sudah bisa FIRE!
) : needPerMonth > 0 ? (
Nabung{' '}
{fmtRp(needPerMonth,{short:true})}/bln
{' '}lagi selama 30 thn
) : (
Catat transaksi untuk kalkulasi
)}
Lihat detail →
);
}
Object.assign(window, {
ScreenDashboard, TxRow, FireWidget,
});