// screen-debts-reports.jsx — Hutang/Piutang + Laporan // ─── DEBTS / HUTANG-PIUTANG ────────────────────────────────────── function ScreenDebts({ theme, accent, scenario, auth }) { const [realDebts, setRealDebts] = React.useState(null); const familyId = auth && auth.family && auth.family.id; React.useEffect(function() { if (!familyId || !window.sbGetDebts) return; sbGetDebts(familyId).then(function(r) { if (!r.error) setRealDebts(r.data||[]); }); }, [familyId]); const isAuth = !!(auth && auth.family); const [showAdd, setShowAdd] = React.useState(false); const [dKind, setDKind] = React.useState('hutang'); const [dWho, setDWho] = React.useState(''); const [dAmount, setDAmount] = React.useState(''); const [dNote, setDNote] = React.useState(''); const [dDue, setDDue] = React.useState(''); const [dSaving, setDSaving] = React.useState(false); async function handleAddDebt() { if (!dWho || !dAmount) return; setDSaving(true); var amount = parseInt(dAmount.replace(/\D/g,''), 10); if (window.sbAddDebt && auth && auth.family && auth.user) { await sbAddDebt(auth.family.id, auth.user.id, { kind: dKind, who: dWho, amount: amount, note: dNote, due: dDue || null, }); var r = await sbGetDebts(auth.family.id); if (!r.error) setRealDebts(r.data||[]); } setShowAdd(false); setDWho(''); setDAmount(''); setDNote(''); setDDue(''); setDSaving(false); } const [tab, setTab] = React.useState('all'); const totals = { hutang: (isAuth?(realDebts||[]):scenario.debts).filter((d) => (d.kind||d.type) === 'hutang') .reduce((a, d) => a + d.amount, 0), piutang: (isAuth?(realDebts||[]):scenario.debts).filter((d) => (d.kind||d.type) === 'piutang') .reduce((a, d) => a + d.amount, 0), }; function normDebt(d) { if (d.original_amount !== undefined) { return { id: d.id, kind: d.type, who: d.counterparty, amount: d.original_amount, paid: d.paid_amount||0, status: d.status, due: d.due_date, desc: d.description, isReal: true, outstanding: d.original_amount - (d.paid_amount||0), }; } return d; } let items = (isAuth ? (realDebts||[]) : scenario.debts).map(normDebt); if (tab !== 'all') items = items.filter((d) => d.kind === tab); return (

Hutang & Piutang

{items.length} catatan aktif
{/* Two summary cards */}
{/* Tabs */}
{[ { id: 'all', label: 'Semua' }, { id: 'hutang', label: 'Hutang' }, { id: 'piutang', label: 'Piutang' }, ].map((t) => ( setTab(t.id)} theme={theme}> {t.label} ))}
{/* List */}
{items.map((d) => )} {items.length === 0 && }
setShowAdd(false)} dKind={dKind} setDKind={setDKind} dWho={dWho} setDWho={setDWho} dAmount={dAmount} setDAmount={setDAmount} dNote={dNote} setDNote={setDNote} dDue={dDue} setDDue={setDDue} onSave={handleAddDebt} saving={dSaving} theme={theme} accent={accent} />
); } function DebtSummary({ theme, kind, amount }) { const isHutang = kind === 'hutang'; const color = isHutang ? theme.danger : theme.success; const soft = isHutang ? theme.dangerSoft : theme.successSoft; return (
{isHutang ? 'Hutang' : 'Piutang'}
{fmtRp(amount, { short: true })}
{isHutang ? 'yang harus dibayar' : 'yang akan diterima'}
); } function DebtRow({ debt, theme, accent }) { const isHutang = debt.kind === 'hutang'; const isOver = debt.status === 'overdue'; const due = new Date(debt.due); const daysLeft = Math.round((due - new Date()) / 86400000); return (
{debt.who} {isOver && Telat} {!isOver && daysLeft <= 7 && {daysLeft}h lagi}
{debt.note}
Jatuh tempo {fmtDateLong(debt.due)}
{fmtRp(debt.amount, { short: true })}
); } // ─── REPORTS / LAPORAN ─────────────────────────────────────────── function ScreenDebtAddModal({ show, onClose, dKind, setDKind, dWho, setDWho, dAmount, setDAmount, dNote, setDNote, dDue, setDDue, onSave, saving, theme, accent }) { if (!show) return null; return (
{ if(e.target===e.currentTarget) onClose(); }}>
Catat Hutang/Piutang
{['hutang','piutang'].map(function(t){ return ; })}
{dKind==='hutang'?'Nama Pemberi Hutang':'Nama Peminjam'}
setDWho(e.target.value)} autoFocus style={{width:'100%',height:48,borderRadius:12,border:'1.5px solid '+theme.border,background:theme.bg,color:theme.fg1,fontSize:14,padding:'0 14px',fontFamily:'inherit',outline:'none'}}/>
Jumlah
Rp setDAmount(parseInt(e.target.value.replace(/\D/g,'')||'0').toLocaleString('id-ID'))} style={{flex:1,height:48,border:0,outline:'none',background:'transparent',fontSize:16,fontWeight:700,color:theme.fg1,fontFamily:'inherit'}}/>
Keterangan (opsional)
setDNote(e.target.value)} style={{width:'100%',height:48,borderRadius:12,border:'1.5px solid '+theme.border,background:theme.bg,color:theme.fg1,fontSize:14,padding:'0 14px',fontFamily:'inherit',outline:'none'}}/>
Jatuh Tempo (opsional)
setDDue(e.target.value)} style={{width:'100%',height:48,borderRadius:12,border:'1.5px solid '+theme.border,background:theme.bg,color:theme.fg1,fontSize:14,padding:'0 14px',fontFamily:'inherit',outline:'none'}}/>
); } function ScreenReport({ theme, accent, scenario, 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; 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||[]); }); }, [familyId]); const isAuth = !!(auth && auth.family); const [period, setPeriod] = React.useState('month'); // Build category breakdown from transactions const expensesByCat = {}; (isAuth?(realTx||[]):scenario.transactions).filter(function(t){ return (t.kind||t.type)==='expense'; }).forEach((t) => { expensesByCat[t.cat] = (expensesByCat[t.cat] || 0) + t.amount; }); // Also add budget.used so categories without recent tx still show (isAuth?(realBudgets||[]):scenario.budgets).forEach((b) => { expensesByCat[b.cat] = Math.max(expensesByCat[b.cat] || 0, b.used); }); const topCats = Object.entries(expensesByCat) .sort((a, b) => b[1] - a[1]).slice(0, 6); const totalExpense = Object.values(expensesByCat).reduce((a, v) => a + v, 0); // Build last-6-month bars (mock by varying current values) const months = ['Des','Jan','Feb','Mar','Apr','Mei']; const realIncome = isAuth ? (realTx||[]).filter(function(t){return (t.kind||t.type)==='income';}).reduce(function(a,t){return a+t.amount;},0) : scenario.monthlyIncome; const incomeSeries = isAuth ? [0,0,0,0,0,realIncome] : [0.78, 0.82, 0.91, 0.88, 0.96, 1.00].map((f) => scenario.monthlyIncome * f); const realExpense = isAuth ? (realTx||[]).filter(function(t){return (t.kind||t.type)==='expense';}).reduce(function(a,t){return a+t.amount;},0) : scenario.monthlyExpense; const expenseSeries = isAuth ? [0,0,0,0,0,realExpense] : [0.71, 0.85, 0.78, 0.92, 0.88, 1.00].map((f) => scenario.monthlyExpense * f); return (

Laporan

Analisis keuangan keluarga
{/* Period segmented control */}
{['week','month','year'].map((p, i) => { const labels = { week: 'Minggu', month: 'Bulan', year: 'Tahun' }; const on = period === p; return ( ); })}
{/* In vs Out bar chart */}
Pemasukan vs Pengeluaran
6 bulan terakhir
Pemasukan
Pengeluaran
{/* Top categories */}
{topCats.map(([catId, val], i) => { const c = CATEGORIES[catId]; const pct = (val / totalExpense) * 100; return (
{c.label}
{fmtRp(val, { short: true })}
{pct.toFixed(0)}%
); })}
); } function BarChartInOut({ theme, accent, months, income, expense }) { const max = Math.max(...income, ...expense); const barW = 12; const gap = 4; const groupW = barW * 2 + gap; const groupGap = 16; const chartW = months.length * groupW + (months.length - 1) * groupGap; const chartH = 130; return (
{months.map((m, i) => { const x = i * (groupW + groupGap); const hI = max > 0 ? (income[i] / max) * (chartH - 28) : 0; const hE = max > 0 ? (expense[i] / max) * (chartH - 28) : 0; return ( {m} ); })}
); } Object.assign(window, { ScreenDebts, ScreenReport, BarChartInOut });