// screen-settings.jsx // ─── ACCOUNTS (Kekayaan Bersih) ─────────────────────────────────── function ScreenAccounts({ theme, accent, auth }) { var T = theme; var _s1 = React.useState(null), accounts = _s1[0], setAccounts = _s1[1]; var _s2 = React.useState(false), showAdd = _s2[0], setShowAdd = _s2[1]; var _s3 = React.useState(null), editAcc = _s3[0], setEditAcc = _s3[1]; var _s4 = React.useState(null), wealth = _s4[0], setWealth = _s4[1]; var _s5 = React.useState(null), monthFlow = _s5[0], setMonthFlow = _s5[1]; var _s6 = React.useState(null), updateValAcc = _s6[0], setUpdateValAcc = _s6[1]; var familyId = auth && auth.family && auth.family.id; React.useEffect(function() { if (!familyId || !window.SB) return; loadAll(); }, [familyId]); async function loadAll() { // Load accounts dengan available balance view var r = await window.SB.from('account_available').select('*') .eq('family_id', familyId).order('type').order('name'); if (!r.error) setAccounts(r.data || []); else { // Fallback ke tabel biasa kalau view belum ada var r2 = await window.SB.from('accounts').select('*') .eq('family_id', familyId).order('type').order('name'); if (!r2.error) setAccounts((r2.data||[]).map(function(a){ return Object.assign({}, a, {available_balance: a.balance, locked_in_goals: 0, unrealized_pnl: a.cost_basis > 0 ? a.balance - a.cost_basis : 0}); })); } // Monthly cash flow dari transaksi var now = new Date(); var start = now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0') + '-01'; var rf = await window.SB.from('transactions').select('type,amount') .eq('family_id', familyId).gte('date', start); if (!rf.error) { var inc = (rf.data||[]).filter(function(t){return t.type==='income';}).reduce(function(s,t){return s+t.amount;},0); var exp = (rf.data||[]).filter(function(t){return t.type==='expense';}).reduce(function(s,t){return s+t.amount;},0); setMonthFlow({income:inc, expense:exp, net:inc-exp}); } } var TYPE_INFO = { bank: {label:'Rekening Bank', icon:'🏦', color:'#3b82f6'}, cash: {label:'Uang Tunai', icon:'💵', color:'#10b981'}, investment: {label:'Investasi', icon:'📈', color:'#8b5cf6'}, ewallet: {label:'E-Wallet', icon:'📱', color:'#f59e0b'}, credit: {label:'Kartu Kredit', icon:'💳', color:'#ef4444'}, }; var kas = (accounts||[]).filter(function(a){return a.type==='bank'||a.type==='cash'||a.type==='ewallet';}); var nonCredit = (accounts||[]).filter(function(a){return a.type!=='credit';}); // grandTotal = kas (fisik total) + investasi - hutang KK var hutangKKtotal = (accounts||[]).filter(function(a){return a.type==='credit';}).reduce(function(s,a){return s+a.balance;},0); var grandTotal = nonCredit.reduce(function(s,a){return s+a.balance;},0) - hutangKKtotal; // totalLocked = total pocket goals (hanya dari kas) var totalLocked = kas.reduce(function(s,a){return s+(a.locked_in_goals||0);},0); // totalAvail = saldo kas yg bebas (saldo fisik - pocket), bukan termasuk investasi var totalKas = kas.reduce(function(s,a){return s+a.balance;},0); // available_balance dari view = balance - locked_in_goals (sudah benar) var totalAvail = kas.reduce(function(s,a){return s+(a.available_balance!==undefined ? a.available_balance : a.balance-( a.locked_in_goals||0));},0); var totalInvest = (accounts||[]).filter(function(a){return a.type==='investment';}).reduce(function(s,a){return s+a.balance;},0); var totalPnl = (accounts||[]).filter(function(a){return a.type==='investment'&&a.cost_basis>0;}).reduce(function(s,a){return s+a.unrealized_pnl;},0); var grouped = {}; (accounts||[]).forEach(function(a){ if(!grouped[a.type]) grouped[a.type]=[]; grouped[a.type].push(a); }); return (

Kekayaan Bersih

Rekening, cash & investasi
{/* Net worth hero card */}
Total Kekayaan Bersih
{fmtRp(grandTotal)}
{/* Breakdown row */}
{[ {label:'Bebas', val:totalAvail, sub:'diluar pocket'}, {label:'Di Pocket', val:totalLocked, sub:'ada di rekening'}, {label:'Investasi', val:totalInvest, sub:totalPnl !== 0 ? (totalPnl>0?'+':'')+fmtRp(totalPnl,{short:true})+' P&L' : 'portofolio'}, ].map(function(item){ return (
{item.label}
{fmtRp(item.val,{short:true})}
{item.sub}
); })}
{/* Monthly flow */} {monthFlow && (
Masuk bulan ini
+{fmtRp(monthFlow.income,{short:true})}
Keluar bulan ini
-{fmtRp(monthFlow.expense,{short:true})}
Net
=0?'#a7f3d0':'#fca5a5'}}> {monthFlow.net>=0?'+':''}{fmtRp(monthFlow.net,{short:true})}
)}
{/* Account list by type */} {accounts===null ? (
Memuat...
) : Object.keys(grouped).length===0 ? (
🏦
Belum ada rekening
Tap + untuk tambah bank, investasi, atau e-wallet
) : ( Object.keys(TYPE_INFO).filter(function(t){return grouped[t];}).map(function(type){ var info = TYPE_INFO[type]; var typeTotal = (grouped[type]||[]).reduce(function(s,a){return s+a.balance;},0); return (
{info.icon} {info.label}
{fmtRp(typeTotal,{short:true})}
{(grouped[type]||[]).map(function(acc){ var pnl = acc.unrealized_pnl || 0; var locked = acc.locked_in_goals || 0; var avail = acc.available_balance !== undefined ? acc.available_balance : acc.balance; return (
{acc.icon}
{acc.name}
{acc.institution &&
{acc.institution}
} {/* Available vs locked */} {locked > 0 && (
✓ {fmtRp(avail,{short:true})} bebas 🔒 {fmtRp(locked,{short:true})} pocket
)} {/* Investment P&L */} {type === 'investment' && acc.cost_basis > 0 && (
=0?'#10b981':'#ef4444'}}> {'Modal '+fmtRp(acc.cost_basis,{short:true})+' · P&L '+(pnl>=0?'+':'')+fmtRp(pnl,{short:true})}
)} {type === 'investment' && ( )}
Update: {new Date(acc.last_updated).toLocaleDateString('id-ID')}
{type==='credit'?'-':''}{fmtRp(acc.balance,{short:true})}
{type !== 'credit' && locked > 0 && (
{fmtRp(avail,{short:true})} tersedia
)}
); })}
); }) )} {showAdd && ( )} {updateValAcc && ( )}
); } // ─── ACCOUNT MODAL ──────────────────────────────────────────────── function AccountModal({ theme, accent, auth, account, onClose, onSaved }) { var T = theme, isEdit = !!account; var _s1=React.useState(account&&account.name||''), name=_s1[0], setName=_s1[1]; var _s2=React.useState(account&&account.type||'bank'), type=_s2[0], setType=_s2[1]; var _s3=React.useState(account&&account.institution||''), inst=_s3[0], setInst=_s3[1]; var _s4=React.useState(account?account.balance.toLocaleString('id-ID'):''), balance=_s4[0], setBalance=_s4[1]; var _s5=React.useState(account&&account.cost_basis?account.cost_basis.toLocaleString('id-ID'):''), costBasis=_s5[0], setCostBasis=_s5[1]; var _s6=React.useState(account&&account.icon||'🏦'), icon=_s6[0], setIcon=_s6[1]; var _s7=React.useState(account&&account.color||'#10b981'), color=_s7[0], setColor=_s7[1]; var _s8=React.useState(account&&account.notes||''), notes=_s8[0], setNotes=_s8[1]; var _s9=React.useState(false), saving=_s9[0], setSaving=_s9[1]; var _s10=React.useState(false), deleting=_s10[0], setDeleting=_s10[1]; var TYPE_ICONS={bank:'🏦',cash:'💵',investment:'📈',ewallet:'📱',credit:'💳'}; var TYPE_LABELS={bank:'Bank',cash:'Tunai',investment:'Investasi',ewallet:'E-Wallet',credit:'Kredit'}; var COLORS=['#10b981','#3b82f6','#8b5cf6','#f59e0b','#ef4444','#0d9488','#ec4899','#6366f1']; var ICONS=['🏦','💵','📈','📱','💳','🏧','💰','🏛️','💼','📊','🪙','💎']; React.useEffect(function(){ if (!isEdit) setIcon(TYPE_ICONS[type]||'🏦'); }, [type]); async function handleSave() { if (!name.trim()||!window.SB) return; setSaving(true); var amt = parseInt(balance.replace(/\D/g,''),10)||0; var basis = parseInt(costBasis.replace(/\D/g,''),10)||0; var payload = { family_id: auth.family.id, created_by: auth.user.id, name: name.trim(), type: type, institution: inst||null, balance: amt, cost_basis: type==='investment' ? basis : 0, credit_limit: type==='credit' ? (parseInt(creditLimit.replace(/\D/g,''),10)||0) : 0, icon: icon, color: color, notes: notes||null, last_updated: new Date().toISOString().split('T')[0], }; var r = isEdit ? await window.SB.from('accounts').update(payload).eq('id',account.id) : await window.SB.from('accounts').insert(payload); setSaving(false); if (r.error) { alert('Error: '+r.error.message); return; } onSaved(); onClose(); } async function handleDelete() { if (!confirm('Hapus akun ini? Goals yang terhubung akan di-unlink.')) return; setDeleting(true); await window.SB.from('accounts').delete().eq('id',account.id); setDeleting(false); onSaved(); onClose(); } var balanceLabel = type==='credit' ? 'Tagihan Saat Ini (outstanding)' : type==='investment' ? 'Nilai Sekarang (current value)' : 'Saldo Saat Ini'; return (
{isEdit?'Edit Akun':'Tambah Akun'}
{/* Type selector */}
Tipe
{Object.keys(TYPE_LABELS).map(function(t){ return ; })}
{/* Investment info box */} {type==='investment' && (
Cara pakai: Isi "Modal" = total uang yang pernah kamu masukkan. Isi "Nilai sekarang" = harga pasar terkini. App hitung P&L otomatis. Update "Nilai sekarang" secara berkala (saham: bulanan, kripto: mingguan).
)} {/* Icon + Color */}
Icon
{ICONS.map(function(ic){ return ; })}
Warna
{COLORS.map(function(col){ return
{/* Nama */}
Nama Akun
{/* Institusi */}
Bank / Institusi (opsional)
{/* Modal investasi */} {type==='investment' && (
Modal (cost basis) — total uang yang dimasukkan
Rp
)} {/* Saldo / current value */}
{balanceLabel}
Rp
{/* Live P&L preview for investment */} {(function(){ if (type!=='investment'||!costBasis||!balance) return null; var cb = parseInt(costBasis.replace(/\D/g,''),10)||0; var cur = parseInt(balance.replace(/\D/g,''),10)||0; if (cb===0) return null; var pnl = cur - cb; var pct = cb > 0 ? ((pnl/cb)*100).toFixed(1) : 0; return (
=0?'#10b981':'#ef4444',textAlign:'right'}}> {'P&L: '+(pnl>=0?'+':'')+fmtRp(pnl,{short:true})+' ('+(pnl>=0?'+':'')+pct+'%)'}
); })()}
{/* Notes */} {type==='credit' && (
Limit Kartu Kredit
Rp
{creditLimit && balance && (function(){ var lim = parseInt(creditLimit.replace(/\D/g,''),10)||0; var used = parseInt(balance.replace(/\D/g,''),10)||0; var avail = lim - used; var pct = lim > 0 ? Math.round((used/lim)*100) : 0; return (
80?'rgba(239,68,68,0.06)':'rgba(16,185,129,0.06)',borderRadius:10}}>
Terpakai 80?'#ef4444':'#10b981'}}>{pct}%
80?'#ef4444':pct>60?'#f59e0b':'#10b981'}}/>
Tagihan: {fmtRp(used,{short:true})} Tersedia: {fmtRp(avail,{short:true})}
); })()}
)}
Catatan (opsional)
{isEdit && ( )}
); } // ─── GOALS (updated — dengan account link) ──────────────────────── function ScreenGoals({ theme, accent, scenario, auth }) { var T = theme; var _s1 = React.useState(null), realGoals = _s1[0], setRealGoals = _s1[1]; var _s2 = React.useState(null), accounts = _s2[0], setAccounts = _s2[1]; var _s3 = React.useState(false), showAddGoal = _s3[0], setShowAddGoal = _s3[1]; var _s4 = React.useState(''), goalName = _s4[0], setGoalName = _s4[1]; var _s5 = React.useState('🎯'), goalEmoji = _s5[0], setGoalEmoji = _s5[1]; var _s6 = React.useState(''), goalTarget = _s6[0], setGoalTarget = _s6[1]; var _s7 = React.useState(''), goalDate = _s7[0], setGoalDate = _s7[1]; var _s8 = React.useState(''), goalAccountId = _s8[0], setGoalAccountId = _s8[1]; var _s9 = React.useState(''), monthlyAuto = _s9[0], setMonthlyAuto = _s9[1]; var _s13 = React.useState('monthly'), autoFreq = _s13[0], setAutoFreq = _s13[1]; var _s14 = React.useState([]), autoDays = _s14[0], setAutoDays = _s14[1]; var _s10 = React.useState(false), savingGoal = _s10[0], setSavingGoal = _s10[1]; var _s11 = React.useState(null), topupGoal = _s11[0], setTopupGoal = _s11[1]; var _s12 = React.useState(''), topupAmt = _s12[0], setTopupAmt = _s12[1]; var _s15 = React.useState(null), editGoal = _s15[0], setEditGoal = _s15[1]; var _s16 = React.useState(null), withdrawGoal = _s16[0], setWithdrawGoal = _s16[1]; var _s17 = React.useState(''), withdrawAmt = _s17[0], setWithdrawAmt = _s17[1]; var familyId = auth && auth.family && auth.family.id; var isAuth = !!(auth && auth.family); React.useEffect(function() { if (!familyId || !window.SB) return; sbGetGoals(familyId).then(function(r){ if(!r.error) setRealGoals(r.data||[]); }); window.SB.from('accounts').select('id,name,icon,type').eq('family_id',familyId) .order('name').then(function(r){ if(!r.error) setAccounts(r.data||[]); }); }, [familyId]); function normGoal(g) { if (g.target_amount !== undefined) { return {id:g.id,name:g.name,emoji:g.icon||'🎯', target:g.target_amount,saved:g.current_amount||0, due:g.target_date,contribs:[],isReal:true, account_id:g.account_id,monthly_auto:g.monthly_auto||0}; } return g; } var goals = (isAuth ? (realGoals||[]) : scenario.goals).map(normGoal); var totalSaved = goals.reduce(function(a,g){return a+g.saved;},0); var totalTarget = goals.reduce(function(a,g){return a+g.target;},0); async function handleAddGoal() { if (!goalName||!goalTarget) return; setSavingGoal(true); var amount = parseInt(goalTarget.replace(/\D/g,''),10); var monthly = parseInt(monthlyAuto.replace(/\D/g,''),10)||0; if (window.sbAddGoal && auth && auth.family && auth.user) { var r = await window.SB.from('savings_goals').insert({ family_id: auth.family.id, created_by: auth.user.id, name: goalName, icon: goalEmoji, target_amount: amount, current_amount:0, target_date: goalDate||null, account_id: goalAccountId||null, monthly_auto: monthly, }).select().single(); if (!r.error) { var fresh = await sbGetGoals(auth.family.id); if (!fresh.error) setRealGoals(fresh.data||[]); } } setShowAddGoal(false); setGoalName(''); setGoalTarget(''); setGoalDate(''); setGoalAccountId(''); setMonthlyAuto(''); setAutoFreq('monthly'); setAutoDays([]); setSavingGoal(false); } async function handleWithdraw() { if (!withdrawGoal || !withdrawAmt) return; var amt = parseInt(withdrawAmt.replace(/\D/g,''), 10); if (amt <= 0 || amt > withdrawGoal.saved) return; await window.SB.from('savings_goals').update({current_amount: withdrawGoal.saved - amt}).eq('id', withdrawGoal.id); var fresh = await sbGetGoals(familyId); if (!fresh.error) setRealGoals(fresh.data||[]); setWithdrawGoal(null); setWithdrawAmt(''); } async function handleEditGoal(g, updates) { await window.SB.from('savings_goals').update(updates).eq('id', g.id); var fresh = await sbGetGoals(familyId); if (!fresh.error) setRealGoals(fresh.data||[]); setEditGoal(null); } async function handleDeleteGoal(g) { if (!confirm('Hapus goal "'+g.name+'"?')) return; await window.SB.from('savings_goals').delete().eq('id', g.id); var fresh = await sbGetGoals(familyId); if (!fresh.error) setRealGoals(fresh.data||[]); } async function handleTopup() { if (!topupGoal||!topupAmt) return; var amt = parseInt(topupAmt.replace(/\D/g,''),10); if (amt <= 0) return; await window.SB.from('savings_goals').update({current_amount: topupGoal.saved + amt}).eq('id',topupGoal.id); var fresh = await sbGetGoals(familyId); if (!fresh.error) setRealGoals(fresh.data||[]); setTopupGoal(null); setTopupAmt(''); } var spinner =
; var EMOJIS = ['🎯','🏠','🚗','✈️','🎓','💍','📱','💻','🏖️','💰','🐷','🎸','🏥','🎮','📷']; return (

Tabungan

{goals.length} tujuan keluarga
{/* Summary hero */}
Total terkumpul
{fmtRp(totalSaved)}
dari target {fmtRp(totalTarget,{short:true})}
0?(totalSaved/totalTarget*100):0)+'%',height:'100%',background:'#fff',borderRadius:4}}/>
{/* Goals list */}
{goals.length===0 ? (
🐷
Belum ada goals tabungan
Tap + untuk mulai rencanakan impianmu
) : goals.map(function(g){ var pct = Math.min(100, g.target>0?g.saved/g.target*100:0); var remaining = g.target - g.saved; var linkedAcc = accounts && g.account_id && accounts.find(function(a){return a.id===g.account_id;}); return (
{g.emoji}
{g.name}
{g.due && Target: {new Date(g.due).toLocaleDateString('id-ID',{month:'short',year:'numeric'})}} {linkedAcc && 🔗 {linkedAcc.icon} {linkedAcc.name}} {g.monthly_auto > 0 && (function(){ var sc = null; try{sc=JSON.parse(g.notes||'');}catch(e){} var fl = {daily:'hari',weekly:'minggu',monthly:'bln',custom:'pilihan'}[(sc&&sc.freq)||'monthly']||'bln'; var dl = sc&&sc.days&&sc.days.length?' ('+sc.days.join(',')+'%)':''; return ⚡ {fmtRp(g.monthly_auto,{short:true})}/{fl}{dl}; })()}
{fmtRp(g.saved,{short:true})}
dari {fmtRp(g.target,{short:true})}
=100?'#10b981':'linear-gradient(90deg,'+accent.grad[0]+','+accent.grad[1]+')', width:pct+'%',transition:'width 0.4s ease'}}/>
{pct.toFixed(0)}% · sisa {fmtRp(Math.max(remaining,0),{short:true})}
{g.saved > 0 && ( )}
); })}
{/* Add goal modal */} {showAddGoal && (
Tambah Goal
{/* Emoji picker */}
{EMOJIS.map(function(e){ return ; })}
{/* Nama */}
Nama Goal
{/* Target dana */}
Target Dana
Rp
{/* Nabung otomatis */}
Nabung Otomatis (opsional)
{[{v:'daily',l:'Harian'},{v:'weekly',l:'Mingguan'},{v:'monthly',l:'Bulanan'},{v:'custom',l:'Pilih hari'}].map(function(f){ return ; })}
{(autoFreq==='weekly'||autoFreq==='custom') && (
{autoFreq==='weekly'?'Pilih 1 hari dalam seminggu:':'Pilih hari-hari tertentu:'}
{['Sen','Sel','Rab','Kam','Jum','Sab','Min'].map(function(day){ var on = autoDays.indexOf(day)!==-1; return ; })}
)}
Rp {autoFreq==='daily'?'/hari':autoFreq==='weekly'?'/minggu':autoFreq==='custom'?'/hari':'/bulan'}
{!!monthlyAuto && (function(){ var amt = parseInt(monthlyAuto.replace(/\D/g,'')||'0',10)||0; var est = autoFreq==='daily'?amt*30:autoFreq==='weekly'?amt*4:autoFreq==='custom'?amt*(autoDays.length||1)*4:amt*12; var lbl = autoFreq==='daily'?'est. /bulan':autoFreq==='weekly'?'est. /bulan':autoFreq==='custom'?'est. /bulan':'est. /tahun'; return
{fmtRp(est,{short:true})} {lbl}
; })()}
Pengingat saja — kamu konfirmasi sendiri setiap periode
{/* Link ke rekening */} {accounts && accounts.filter(function(a){return a.type!=='credit';}).length > 0 && (
Linked ke rekening (opsional)
{goalAccountId &&
Saldo rekening ini akan dikurangi sebesar goal terkumpul (virtual pocket)
}
)} {/* Target tanggal */}
Target Tanggal (opsional)
)} {/* Withdraw / Cairkan modal */} {withdrawGoal && (
{withdrawGoal.emoji}
Cairkan Tabungan
{withdrawGoal.name}
Terkumpul: {fmtRp(withdrawGoal.saved)}
Rp
{withdrawAmt && parseInt(withdrawAmt.replace(/\D/g,''),10) > withdrawGoal.saved && (
Melebihi saldo terkumpul
)}
Dana akan dikurangi dari goal. Pastikan sudah ditransfer ke rekening kamu.
)} {/* Edit Goal modal */} {editGoal && } {/* Topup modal */} {topupGoal && (
{topupGoal.emoji}
{topupGoal.name}
{fmtRp(topupGoal.saved,{short:true})} / {fmtRp(topupGoal.target,{short:true})}
Rp
)}
); } // ─── PROFILE ───────────────────────────────────────────────────── function ScreenProfile({ theme, accent, auth }) { var T = theme; var _s1=React.useState(false),editing=_s1[0],setEditing=_s1[1]; var _s2=React.useState((auth&&auth.profile&&auth.profile.full_name)||''),name=_s2[0],setName=_s2[1]; var _s3=React.useState(false),saving=_s3[0],setSaving=_s3[1]; var _s4=React.useState(''),msg=_s4[0],setMsg=_s4[1]; var _s5=React.useState((auth&&auth.profile&&auth.profile.birth_date)||''),birthDate=_s5[0],setBirthDate=_s5[1]; var _s6=React.useState(false),editingBirth=_s6[0],setEditingBirth=_s6[1]; var profile = auth&&auth.profile; // Hitung usia dari birth_date var age = null; if (birthDate) { var bd = new Date(birthDate); var now = new Date(); age = now.getFullYear() - bd.getFullYear() - (now.getMonth() < bd.getMonth() || (now.getMonth()===bd.getMonth() && now.getDate()
{initial}
{editing ? (
) : (
{(profile&&profile.full_name)||'Kamu'}
)}
{auth&&auth.user&&auth.user.email}
{msg&&
{msg}
}
{/* Birth date field — editable */}
Tanggal Lahir
{editingBirth ? (
) : (
{birthDate ? <>{new Date(birthDate).toLocaleDateString('id-ID',{day:'numeric',month:'long',year:'numeric'})} {age} tahun : Belum diisi — tap untuk isi }
✏️
)} {!birthDate && (
⚠️ Isi tanggal lahir agar usia di FIRE Calculator otomatis terkunci
)}
{[ {label:'Keluarga',value:(auth&&auth.family&&auth.family.name)||'-',icon:'🏠'}, {label:'Kode Undangan',value:(auth&&auth.family&&auth.family.invite_code)||'-',icon:'🎟️',mono:true}, {label:'Role',value:(profile&&profile.role)==='admin'?'👑 Admin':'👤 Member',icon:null}, {label:'Bergabung',value:(profile&&profile.joined_at)?new Date(profile.joined_at).toLocaleDateString('id-ID'):'-',icon:'📅'}, ].map(function(item){ return (
{item.label}
{item.icon?item.icon+' ':''}{item.value}
); })}
); } // ─── SECURITY ──────────────────────────────────────────────────── function ScreenSecurity({ theme, accent, auth }) { var T = theme; var _s1=React.useState(''),np=_s1[0],setNp=_s1[1]; var _s2=React.useState(''),cp=_s2[0],setCp=_s2[1]; var _s3=React.useState(false),saving=_s3[0],setSaving=_s3[1]; var _s4=React.useState(''),msg=_s4[0],setMsg=_s4[1]; async function handleChange() { if(np.length<6){setMsg('Minimal 6 karakter');return;} if(np!==cp){setMsg('Password tidak cocok');return;} setSaving(true); var r = await window.SB.auth.updateUser({password:np}); setSaving(false); if(r.error){setMsg('Error: '+r.error.message);return;} setMsg('Password berhasil diubah ✓'); setNp(''); setCp(''); setTimeout(function(){setMsg('');},3000); } return (

Keamanan

Ubah password akun kamu

🔑 Ubah Password
{[{label:'Password Baru',val:np,set:setNp,ph:'Minimal 6 karakter'},{label:'Konfirmasi',val:cp,set:setCp,ph:'Ulangi password baru'}].map(function(f){ return (
{f.label}
); })} {msg&&
{msg}
}
); } // ─── EXPORT ────────────────────────────────────────────────────── function ScreenExport({ theme, accent, auth }) { var T = theme; var _s1=React.useState(false),loading=_s1[0],setLoading=_s1[1]; var _s2=React.useState(''),msg=_s2[0],setMsg=_s2[1]; async function exportCSV() { if(!auth||!auth.family||!window.SB) return; setLoading(true); var r = await window.SB.from('transactions').select('date,type,amount,note,account,category:categories(name)') .eq('family_id',auth.family.id).order('date',{ascending:false}); setLoading(false); if(r.error){setMsg('Error: '+r.error.message);return;} var rows = r.data||[]; var csv = 'Tanggal,Tipe,Jumlah,Kategori,Catatan,Akun\n'; rows.forEach(function(t){ csv += [t.date,t.type,t.amount,(t.category&&t.category.name)||'',(t.note||'').replace(/,/g,' '),t.account||''].join(',')+'\n'; }); var blob = new Blob([csv],{type:'text/csv;charset=utf-8;'}); var a = document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='indiecash-'+new Date().toISOString().split('T')[0]+'.csv'; a.click(); setMsg(rows.length+' transaksi diekspor ✓'); setTimeout(function(){setMsg('');},3000); } return (

Export Data

Download semua data transaksi

📊 Export CSV

Semua transaksi dalam format spreadsheet (Excel / Google Sheets)

{msg&&
{msg}
}
); } // ─── HELP ──────────────────────────────────────────────────────── function ScreenHelp({ theme, accent }) { var T = theme; var _s1=React.useState(null),open=_s1[0],setOpen=_s1[1]; var faqs = [ {q:'Cara tambah transaksi?',a:'Tap tombol + hijau di tengah navbar. Pilih tipe, masukkan nominal, pilih kategori, lalu Simpan.'}, {q:'Cara undang anggota keluarga?',a:'Buka Lainnya → Akun saya. Salin Kode Undangan dan bagikan ke anggota keluarga.'}, {q:'Apa itu FIRE Calculator?',a:'FIRE (Financial Independence, Retire Early) membantu kamu menghitung kapan bisa pensiun berdasarkan tabungan dan pengeluaran.'}, {q:'Bagaimana pocket/envelope budgeting bekerja?',a:'Ketika kamu membuat savings goal dan menghubungkannya ke rekening bank, saldo "tersedia" di rekening itu akan dikurangi sebesar jumlah yang sudah terkumpul di goal tersebut. Contoh: BCA 2.2jt, ada goal Jalan-jalan 800rb yang linked → BCA tersedia = 1.4jt.'}, {q:'Bagaimana pencatatan investasi saham/kripto?',a:'Tambah akun tipe Investasi. Isi "Modal" = total uang yang pernah kamu masukkan. Isi "Nilai sekarang" = harga pasar saat ini. App akan menghitung P&L (profit/loss) otomatis. Update nilai secara berkala sesuai frekuensi yang kamu mau.'}, {q:'Bagaimana scan struk bekerja?',a:'Buka Lainnya → Scan Struk. Foto atau upload gambar struk, AI akan otomatis membaca nominal dan kategori.'}, {q:'Apakah data saya aman?',a:'Data disimpan dengan Row Level Security di Supabase — hanya anggota keluarga yang bisa mengakses data kamu.'}, ]; return (

Bantuan

FAQ & cara penggunaan INDIECASH

{faqs.map(function(faq,i){ return (
{faq.q}
{open===i&&(
{faq.a}
)}
); })}
💬 Butuh bantuan lebih?
📧 hello@indiecash.id
); } function UpdateValModal({ theme, accent, account, onClose, onSave }) { var T = theme; var _s1=React.useState(account.balance?account.balance.toLocaleString('id-ID'):''),newVal=_s1[0],setNewVal=_s1[1]; var _s2=React.useState(false),saving=_s2[0],setSaving=_s2[1]; var currentVal = account.balance || 0; var newValNum = parseInt((newVal||'').replace(/\D/g,''),10) || 0; var pnlChange = newValNum - currentVal; var totalPnl = newValNum - (account.cost_basis||0); async function handleSave() { if (!newValNum || saving) return; setSaving(true); await window.SB.from('accounts').update({ balance: newValNum, last_updated: new Date().toISOString().split('T')[0], }).eq('id', account.id); window.dispatchEvent(new Event('tx-added')); onSave && onSave(); onClose(); } return (
Update Nilai Investasi
{account.name}
{/* Current vs New */}
Nilai Saat Ini
{fmtRp(currentVal,{short:true})}
{account.cost_basis > 0 && (
Modal: {fmtRp(account.cost_basis,{short:true})}
)}
0?'rgba(16,185,129,0.08)':pnlChange<0?'rgba(239,68,68,0.08)':T.surfaceAlt, borderRadius:12,padding:'12px 14px', border: pnlChange>0?'1px solid rgba(16,185,129,0.2)':pnlChange<0?'1px solid rgba(239,68,68,0.2)':'none'}}>
Perubahan
0?'#10b981':pnlChange<0?'#ef4444':T.fg3}}> {pnlChange>0?'+':''}{pnlChange!==0?fmtRp(pnlChange,{short:true}):'—'}
{account.cost_basis > 0 && newValNum > 0 && (
=0?'#10b981':'#ef4444',marginTop:2}}> Total P&L: {totalPnl>=0?'+':''}{fmtRp(totalPnl,{short:true})}
)}
{/* Input nilai baru */}
Nilai Baru
Rp
); } function GoalEditModal({ theme, accent, goal, accounts, onClose, onSave, onDelete }) { var T = theme; var _s1 = React.useState(goal.name||''), gName = _s1[0], setGName = _s1[1]; var _s2 = React.useState(goal.emoji||'🎯'), gEmoji = _s2[0], setGEmoji = _s2[1]; var _s3 = React.useState(goal.target?goal.target.toLocaleString('id-ID'):''), gTarget = _s3[0], setGTarget = _s3[1]; var _s4 = React.useState(goal.due||''), gDate = _s4[0], setGDate = _s4[1]; var _s5 = React.useState(goal.account_id||''), gAcct = _s5[0], setGAcct = _s5[1]; var _s6 = React.useState(false), saving = _s6[0], setSaving = _s6[1]; var EMOJIS = ['🎯','🏠','🚗','✈️','🎓','💍','📱','💻','🏖️','💰','🐷','🎸','🏥','🎮','📷']; async function handleSave() { if (!gName.trim()) return; setSaving(true); await onSave(goal, { name: gName.trim(), icon: gEmoji, target_amount:parseInt(gTarget.replace(/\D/g,''),10)||goal.target, target_date: gDate||null, account_id: gAcct||null, }); setSaving(false); } return (
Edit Goal
{EMOJIS.map(function(e){ return ; })}
Nama
Target Dana
Rp
{accounts && accounts.filter(function(a){return a.type!=='credit';}).length > 0 && (
Linked ke Rekening
)}
Target Tanggal
); } Object.assign(window, { ScreenAccounts, AccountModal, ScreenGoals, GoalEditModal, ScreenProfile, ScreenSecurity, ScreenExport, ScreenHelp });