// 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
setShowAdd(true)}>
{/* 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 })}
{isHutang ? 'Bayar' : 'Tagih'}
);
}
// ─── 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 setDKind(t)}
style={{height:40,borderRadius:10,border:0,cursor:'pointer',fontWeight:700,fontSize:13,fontFamily:'inherit',
background:dKind===t?(t==='hutang'?'#ef4444':'#10b981'):'transparent',
color:dKind===t?'#fff':theme.fg2}}>
{t==='hutang'?'🔴 Hutang (kita)':'🟢 Piutang (orang)'}
;
})}
{saving ? 'Menyimpan...' : (dKind==='hutang'?'🔴':'🟢')+' Simpan'}
);
}
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 (
setPeriod(p)} style={{
flex: 1, appearance: 'none', border: 0, cursor: 'pointer',
padding: '8px 0', borderRadius: 9, zIndex: 1,
background: on ? theme.surface : 'transparent',
color: on ? theme.fg1 : theme.fg2,
fontSize: 13, fontWeight: 700,
boxShadow: on ? theme.shadow1 : 'none',
transition: 'all 200ms',
}}>{labels[p]}
);
})}
{/* In vs Out bar chart */}
Pemasukan vs Pengeluaran
6 bulan terakhir
{/* Top categories */}
{topCats.map(([catId, val], i) => {
const c = CATEGORIES[catId];
const pct = (val / totalExpense) * 100;
return (
{c.label}
{fmtRp(val, { short: true })}
);
})}
);
}
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 });