<!DOCTYPE html>

<html lang="id">

<head>

<base target="_top">

<meta charset="utf-8">

<meta name="viewport" content="width=device-width, initial-scale=1">

<title>HartaKita</title>

<link rel="preconnect" href="https://fonts.googleapis.com">

<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">

<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js"></script>

<style>

:root{

  --ink:#14262A; --muted:#5E716C; --line:#D6DFDA; --paper:#EEF2EF; --surface:#FFFFFF;

  --jade:#0F4C45; --jade-2:#1F7A6B; --jade-soft:#E3EEEA; --gold:#B8892B;

  --in:#2F7D4E; --in-soft:#E4F1E8; --out:#B4443A; --out-soft:#F6E5E2;

  --r-lg:14px; --r-sm:8px;

}

*{box-sizing:border-box}

html,body{margin:0;height:100%}

body{font-family:'Plus Jakarta Sans',system-ui,-apple-system,'Segoe UI',sans-serif;background:var(--paper);color:var(--ink);font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased}

.num{font-variant-numeric:tabular-nums;white-space:nowrap}

button,input,select,textarea{font:inherit;color:inherit}

:focus-visible{outline:2px solid var(--gold);outline-offset:2px}


/* ---------- Layout ---------- */

.app{display:grid;grid-template-columns:232px 1fr;min-height:100vh}

.side{background:var(--jade);color:#DCEBE6;padding:24px 16px;display:flex;flex-direction:column;gap:28px;position:sticky;top:0;height:100vh}

.brand{display:flex;align-items:center;gap:10px;padding:0 8px}

.brand-mark{width:34px;height:34px;border-radius:10px;background:var(--gold);display:grid;place-items:center;color:var(--jade);font-weight:800;font-size:18px}

.brand b{font-size:20px;font-weight:800;color:#fff;letter-spacing:-.02em}

.nav{display:flex;flex-direction:column;gap:4px}

.nav button{all:unset;cursor:pointer;padding:10px 12px;border-radius:var(--r-sm);display:flex;align-items:center;gap:10px;font-weight:500;color:#C6DCD5}

.nav button svg{width:18px;height:18px;flex:none}

.nav button:hover{background:rgba(255,255,255,.06);color:#fff}

.nav button.active{background:rgba(255,255,255,.12);color:#fff;font-weight:600;box-shadow:inset 3px 0 0 var(--gold)}

.side-foot{margin-top:auto;padding:12px 8px 0;border-top:1px solid rgba(255,255,255,.12);display:grid;gap:2px}

.side-foot b{color:#fff;font-size:13.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

.side-foot span{font-size:12px;color:#9FC2B8;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

.side-foot button{all:unset;cursor:pointer;margin-top:10px;font-size:13px;font-weight:600;color:#fff;padding:7px 10px;border:1px solid rgba(255,255,255,.25);border-radius:var(--r-sm);text-align:center}

.side-foot button:hover{background:rgba(255,255,255,.08)}

[hidden]{display:none!important}


/* ---------- Layar autentikasi ---------- */

.auth{min-height:100vh;display:grid;grid-template-columns:minmax(300px,5fr) 7fr}

.auth-art{background:var(--jade);color:#DCEBE6;padding:48px;display:flex;flex-direction:column;justify-content:space-between;position:relative;overflow:hidden}

.auth-art::after{content:'';position:absolute;right:-120px;bottom:-120px;width:360px;height:360px;border-radius:50%;border:56px solid rgba(196,154,58,.18)}

.auth-art h2{font-size:clamp(26px,3vw,38px);line-height:1.15;letter-spacing:-.025em;color:#fff;margin:0 0 12px;max-width:14ch}

.auth-art p{max-width:36ch;margin:0;color:#B9D3CB}

.auth-main{display:grid;place-items:center;padding:40px 24px}

.auth-form{width:min(400px,100%);display:grid;gap:14px}

.auth-form h1{font-size:26px;letter-spacing:-.02em;margin:0}

.auth-form .sub{color:var(--muted);margin:-8px 0 6px}

.auth-form input{width:100%}

.auth-form .btn{justify-content:center;width:100%;min-height:44px}

.linkbtn{all:unset;cursor:pointer;color:var(--jade-2);font-weight:600}

.linkbtn:hover{text-decoration:underline}

.linkbtn:disabled{color:var(--muted);cursor:default;text-decoration:none}

.auth-alt{text-align:center;color:var(--muted);margin:4px 0 0}

.pw{position:relative}.pw input{padding-right:64px}

.pw button{position:absolute;right:6px;top:50%;transform:translateY(-50%);border:0;background:none;color:var(--jade-2);font-weight:600;font-size:12.5px;cursor:pointer;padding:6px}

.otp{font-size:26px;letter-spacing:.5em;text-align:center;font-weight:700;font-variant-numeric:tabular-nums}

.form-err{background:var(--out-soft);color:var(--out);padding:10px 12px;border-radius:var(--r-sm);font-size:13px}

.form-ok{background:var(--in-soft);color:var(--in);padding:10px 12px;border-radius:var(--r-sm);font-size:13px}


main{padding:24px 32px 48px;min-width:0}

.top{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:24px}

.top h1{font-size:24px;font-weight:800;letter-spacing:-.02em;margin:0 auto 0 0}

.period{display:flex;gap:8px;align-items:center}

select,input,textarea{background:var(--surface);border:1px solid var(--line);border-radius:var(--r-sm);padding:8px 10px;min-height:38px}

textarea{resize:vertical;min-height:70px;width:100%}

.btn{border:0;border-radius:var(--r-sm);padding:9px 14px;font-weight:600;cursor:pointer;min-height:38px;display:inline-flex;align-items:center;gap:6px}

.btn-primary{background:var(--jade);color:#fff}.btn-primary:hover{background:var(--jade-2)}

.btn-ghost{background:transparent;border:1px solid var(--line)}.btn-ghost:hover{background:var(--surface)}

.btn-gold{background:var(--gold);color:#fff}

.icon-btn{border:0;background:transparent;cursor:pointer;padding:6px;border-radius:6px;color:var(--muted)}

.icon-btn:hover{background:var(--jade-soft);color:var(--jade)}

.icon-btn.del:hover{background:var(--out-soft);color:var(--out)}


.page{display:none}.page.active{display:block}

.panel{background:var(--surface);border:1px solid var(--line);border-radius:var(--r-lg);padding:20px}

.panel h2{font-size:15px;font-weight:700;margin:0 0 4px}

.panel .sub{color:var(--muted);font-size:13px;margin:0 0 16px}

.grid{display:grid;gap:16px}

.g-2{grid-template-columns:1fr 1fr}

.g-cash{grid-template-columns:minmax(260px,1fr) 2fr}

.mt{margin-top:16px}


/* ---------- Hero: total harta + bar komposisi ---------- */

.hero{background:var(--surface);border:1px solid var(--line);border-radius:var(--r-lg);padding:28px;display:grid;grid-template-columns:1fr auto;gap:24px 40px}

.hero-label{color:var(--muted);font-weight:500}

.hero-total{font-size:clamp(32px,4.6vw,52px);font-weight:800;letter-spacing:-.035em;line-height:1.05;margin:4px 0 0}

.hero-total small{font-size:.42em;font-weight:600;color:var(--muted);letter-spacing:0;margin-right:6px;vertical-align:.55em}

.hero-side{display:grid;grid-template-columns:repeat(2,auto);gap:14px 36px;align-content:end;margin:0}

.hero-side div{display:grid;gap:2px}

.hero-note{color:var(--muted);font-size:13px;margin-top:6px}

.hero-side dt{color:var(--muted);font-size:12px}

.hero-side dd{margin:0;font-weight:700;font-size:16px}

.stack{grid-column:1/-1}

.stack-bar{display:flex;height:22px;border-radius:6px;overflow:hidden;background:var(--paper);gap:2px}

.stack-bar span{display:block;height:100%;transition:flex-grow .5s ease}

.stack-legend{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:10px 20px;margin-top:14px}

.leg{display:grid;grid-template-columns:10px 1fr;gap:2px 8px;align-items:center}

.leg i{width:10px;height:10px;border-radius:3px}

.leg b{font-weight:600;font-size:13px}

.leg span{grid-column:2;color:var(--muted);font-size:12.5px}

.up{color:var(--in)}.down{color:var(--out)}


/* ---------- Arus kas ---------- */

.flow{display:flex;flex-direction:column;gap:14px}

.flow-row{display:flex;justify-content:space-between;align-items:baseline;gap:8px;padding-bottom:12px;border-bottom:1px dashed var(--line)}

.flow-row:last-of-type{border-bottom:0}

.flow-row .v{font-size:20px;font-weight:700}

.ratio{height:8px;border-radius:4px;background:var(--out-soft);overflow:hidden}

.ratio span{display:block;height:100%;background:var(--in)}

.chart-box{position:relative;height:280px}


/* ---------- Tabel ---------- */

.table-wrap{overflow-x:auto;border:1px solid var(--line);border-radius:var(--r-sm)}

table{width:100%;border-collapse:collapse;font-size:13.5px}

th{text-align:left;font-weight:600;color:var(--muted);background:#F6F8F7;padding:10px 12px;border-bottom:1px solid var(--line);white-space:nowrap}

td{padding:10px 12px;border-bottom:1px solid #EAEFEC;vertical-align:top}

tr:last-child td{border-bottom:0}

td.r,th.r{text-align:right}

tr.group td{background:var(--jade-soft);font-weight:700;color:var(--jade)}

tr.total td{background:#F6F8F7;font-weight:700}

.badge{display:inline-block;white-space:nowrap;padding:2px 8px;border-radius:999px;font-size:12px;font-weight:600}

.b-in{background:var(--in-soft);color:var(--in)}.b-out{background:var(--out-soft);color:var(--out)}

.ket{color:var(--muted);max-width:260px}

.empty{padding:32px;text-align:center;color:var(--muted)}

.b-hutang{background:#F4EBDB;color:#8A6420}.b-piutang{background:#E1EAF2;color:#3D6E8C}

.b-lunas{background:var(--in-soft);color:var(--in)}.b-telat{background:var(--out-soft);color:var(--out)}.b-dekat{background:#F4EBDB;color:#8A6420}.b-jalan{background:#EEF2EF;color:var(--muted)}

.prog{height:6px;border-radius:3px;background:var(--paper);overflow:hidden;min-width:90px;margin-top:6px}

.prog span{display:block;height:100%;background:var(--jade-2)}

tr.pay-row>td{background:#FAFBFA;padding:0}

.pay-list{padding:12px 16px 14px 40px}

.pay-list table{background:var(--surface);border:1px solid var(--line);border-radius:var(--r-sm)}

.pay-list p{margin:0 0 8px;font-weight:600;font-size:13px}

.lock{font-size:12px;color:var(--muted)}

.btn-sm{min-height:30px;padding:5px 10px;font-size:12.5px}

.empty .btn{margin-top:12px}

.toolbar{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px;align-items:center}

.toolbar input[type=search]{flex:1;min-width:180px}

.summary{display:flex;gap:28px;flex-wrap:wrap;margin-bottom:14px}

.summary div span{display:block;color:var(--muted);font-size:12px}

.summary div b{font-size:17px}

.pivot td,.pivot th{padding:8px 10px;font-size:12.5px}

.pivot td:first-child,.pivot th:first-child{position:sticky;left:0;background:inherit;min-width:200px}

.pivot tr:not(.total):not(.group) td:first-child{background:var(--surface)}


/* ---------- Dialog ---------- */

dialog{border:0;border-radius:var(--r-lg);padding:0;width:min(520px,94vw);box-shadow:0 24px 60px rgba(15,40,36,.25)}

dialog::backdrop{background:rgba(15,40,36,.45)}

.dlg-head{padding:18px 22px;border-bottom:1px solid var(--line);display:flex;justify-content:space-between;align-items:center}

.dlg-head h3{margin:0;font-size:17px}

.dlg-body{padding:20px 22px;display:grid;gap:14px;grid-template-columns:1fr 1fr}

.dlg-foot{padding:14px 22px;border-top:1px solid var(--line);display:flex;justify-content:flex-end;gap:8px}

.field{display:flex;flex-direction:column;gap:6px}

.field.full{grid-column:1/-1}

.field label{font-weight:600;font-size:13px}

.field .hint{font-size:12px;color:var(--muted)}

.money{position:relative}

.money input{padding-left:36px;width:100%;text-align:right;font-weight:600}

.money::before{content:'Rp';position:absolute;left:11px;top:50%;transform:translateY(-50%);color:var(--muted);font-size:13px}

.seg{display:grid;grid-template-columns:1fr 1fr;border:1px solid var(--line);border-radius:var(--r-sm);overflow:hidden}

.seg input{position:absolute;opacity:0;pointer-events:none}

.seg label{text-align:center;padding:9px;cursor:pointer;font-weight:600;color:var(--muted)}

.seg input:checked+label.in{background:var(--in);color:#fff}

.seg input:checked+label.out{background:var(--out);color:#fff}

.seg input:focus-visible+label{outline:2px solid var(--gold);outline-offset:-2px}


#toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%) translateY(120px);background:var(--ink);color:#fff;padding:12px 18px;border-radius:var(--r-sm);font-weight:500;transition:transform .25s;z-index:50;max-width:90vw}

#toast.show{transform:translateX(-50%) translateY(0)}

#toast.err{background:var(--out)}

#loader{position:fixed;top:0;left:0;height:3px;background:var(--gold);width:0;z-index:60;transition:width .4s}

#loader.on{width:70%;transition:width 8s cubic-bezier(.1,.7,.3,1)}


/* ---------- Responsif ---------- */

@media (max-width:1100px){.g-2,.g-cash{grid-template-columns:1fr}.hero{grid-template-columns:1fr}.hero-side{grid-template-columns:repeat(2,auto);justify-content:start}}

@media (max-width:760px){

  .auth{grid-template-columns:1fr}.auth-art{padding:28px 24px;min-height:auto}.auth-art h2{font-size:24px;margin-top:24px}

  .app{grid-template-columns:1fr}

  .side{position:fixed;bottom:0;top:auto;height:auto;width:100%;padding:6px;z-index:40;flex-direction:row}

  .brand,.side-foot{display:none}

  .nav{flex-direction:row;width:100%;justify-content:space-around}

  .nav button{flex-direction:column;gap:2px;font-size:11px;padding:6px}

  .nav button.active{box-shadow:inset 0 -3px 0 var(--gold)}

  main{padding:16px 14px 90px}

  .hero{padding:20px}.hero-side{grid-template-columns:1fr 1fr}

  .dlg-body{grid-template-columns:1fr}

}

@media (prefers-reduced-motion:reduce){*{transition:none!important}}

</style>

</head>

<body>

<div id="loader"></div>

<!-- ================= AUTENTIKASI ================= -->

<section class="auth" id="auth" hidden>

  <div class="auth-art">

    <div class="brand"><div class="brand-mark">H</div><b>HartaKita</b></div>

    <div>

      <h2>HartaKita - Harta dan Transaksi Tercatat dan Akurat, Insya Allah Keberkahan Meningkat</h2>

      <p>Setiap akun mempunyai catatan sendiri yang terpisah dan terjaga kerahasiaannya</p>

    </div>

  </div>

  <div class="auth-main">

    <form class="auth-form" id="fLogin" data-view="login" novalidate>

      <h1>Masuk</h1><p class="sub">Gunakan username atau email yang sudah terverifikasi.</p>

      <div class="form-err" hidden></div>

      <div class="field"><label for="lId">Username atau email</label><input id="lId" name="ident" autocomplete="username" required></div>

      <div class="field"><label for="lPw">Password</label><div class="pw"><input type="password" id="lPw" name="pw" autocomplete="current-password" required><button type="button" data-toggle-pw>Lihat</button></div></div>

      <div style="text-align:right"><button type="button" class="linkbtn" data-go="forgot">Lupa password?</button></div>

      <button class="btn btn-primary" type="submit">Masuk</button>

      <p class="auth-alt">Belum punya akun? <button type="button" class="linkbtn" data-go="register">Daftar</button></p>

    </form>


    <form class="auth-form" id="fRegister" data-view="register" novalidate hidden>

      <h1>Daftar akun</h1><p class="sub">Kode verifikasi akan dikirim ke email Anda.</p>

      <div class="form-err" hidden></div>

      <div class="field"><label for="rNama">Nama lengkap</label><input id="rNama" name="nama" autocomplete="name" required></div>

      <div class="field"><label for="rUser">Username</label><input id="rUser" name="username" autocomplete="username" required pattern="[a-z0-9_]{4,20}"><span class="hint">4–20 karakter: huruf kecil, angka, atau garis bawah.</span></div>

      <div class="field"><label for="rEmail">Email</label><input type="email" id="rEmail" name="email" autocomplete="email" required></div>

      <div class="field"><label for="rPw">Password</label><div class="pw"><input type="password" id="rPw" name="pw" autocomplete="new-password" required><button type="button" data-toggle-pw>Lihat</button></div><span class="hint">Minimal 8 karakter, berisi huruf dan angka.</span></div>

      <div class="field"><label for="rPw2">Ulangi password</label><input type="password" id="rPw2" name="pw2" autocomplete="new-password" required></div>

      <button class="btn btn-primary" type="submit">Daftar dan kirim kode</button>

      <p class="auth-alt">Sudah punya akun? <button type="button" class="linkbtn" data-go="login">Masuk</button></p>

    </form>


    <form class="auth-form" id="fVerify" data-view="verify" novalidate hidden>

      <h1>Verifikasi email</h1><p class="sub">Masukkan 6 digit kode yang kami kirim ke <b class="sentTo"></b>. Periksa juga folder spam.</p>

      <div class="form-err" hidden></div>

      <div class="field"><label for="vCode">Kode verifikasi</label><input id="vCode" name="code" class="otp" inputmode="numeric" maxlength="6" autocomplete="one-time-code" required></div>

      <button class="btn btn-primary" type="submit">Verifikasi dan masuk</button>

      <p class="auth-alt">Belum menerima kode? <button type="button" class="linkbtn" data-resend>Kirim ulang kode</button></p>

      <p class="auth-alt"><button type="button" class="linkbtn" data-go="login">Kembali ke halaman masuk</button></p>

    </form>


    <form class="auth-form" id="fForgot" data-view="forgot" novalidate hidden>

      <h1>Lupa password</h1><p class="sub">Masukkan email akun Anda. Kami akan mengirim kode untuk membuat password baru.</p>

      <div class="form-err" hidden></div>

      <div class="field"><label for="fEmail">Email</label><input type="email" id="fEmail" name="email" autocomplete="email" required></div>

      <button class="btn btn-primary" type="submit">Kirim kode reset</button>

      <p class="auth-alt"><button type="button" class="linkbtn" data-go="login">Kembali ke halaman masuk</button></p>

    </form>


    <form class="auth-form" id="fReset" data-view="reset" novalidate hidden>

      <h1>Buat password baru</h1><p class="sub">Masukkan kode yang dikirim ke <b class="sentTo"></b> dan password baru Anda.</p>

      <div class="form-err" hidden></div>

      <div class="field"><label for="sCode">Kode reset</label><input id="sCode" name="code" class="otp" inputmode="numeric" maxlength="6" autocomplete="one-time-code" required></div>

      <div class="field"><label for="sPw">Password baru</label><div class="pw"><input type="password" id="sPw" name="pw" autocomplete="new-password" required><button type="button" data-toggle-pw>Lihat</button></div><span class="hint">Minimal 8 karakter, berisi huruf dan angka.</span></div>

      <div class="field"><label for="sPw2">Ulangi password baru</label><input type="password" id="sPw2" name="pw2" autocomplete="new-password" required></div>

      <button class="btn btn-primary" type="submit">Simpan password baru</button>

      <p class="auth-alt">Belum menerima kode? <button type="button" class="linkbtn" data-resend>Kirim ulang kode</button></p>

    </form>

  </div>

</section>


<div class="app" hidden>

  <aside class="side">

    <div class="brand"><div class="brand-mark">H</div><b>HartaKita</b></div>

    <nav class="nav" aria-label="Menu utama">

      <button data-page="dashboard" class="active"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 13h8V3H3zM13 21h8V11h-8zM3 21h8v-6H3zM13 3v6h8V3z"/></svg>Ringkasan</button>

      <button data-page="harta"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 10l9-6 9 6v10a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1z"/><path d="M9 21v-7h6v7"/></svg>Data Harta</button>

      <button data-page="transaksi"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M7 7h13l-3-3M17 17H4l3 3"/></svg>Transaksi</button>

      <button data-page="hutang"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 7h16M4 12h10M4 17h7"/><path d="M17 14l3 3-3 3"/></svg>Hutang &amp; Piutang</button>

      <button data-page="rekap"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16v16H4zM4 10h16M10 10v10"/></svg>Rekapitulasi</button>

    </nav>

    <div class="side-foot"><b id="uNama"></b><span id="uInfo"></span><button type="button" id="btnLogout">Keluar</button></div>

  </aside>


  <main>

    <header class="top">

      <h1 id="pageTitle">Ringkasan</h1>

      <div class="period">

        <select id="fTahun" aria-label="Tahun"></select>

        <select id="fBulan" aria-label="Bulan"></select>

      </div>

      <button class="btn btn-ghost" id="btnReload" title="Muat ulang data">Muat ulang</button>

      <button class="btn btn-primary" id="btnAddTrx">+ Catat transaksi</button>

    </header>


    <!-- ================= RINGKASAN ================= -->

    <section class="page active" id="page-dashboard">

      <div class="hero">

        <div>

          <div class="hero-label">Kekayaan bersih</div>

          <div class="hero-total num" id="hTotal">–</div>

          <div class="hero-note num" id="hNote"></div>

        </div>

        <dl class="hero-side">

          <div><dt>Nilai perolehan aset</dt><dd class="num" id="hPerolehan">–</dd></div>

          <div><dt>Perubahan nilai aset</dt><dd class="num" id="hDelta">–</dd></div>

          <div><dt>Saldo kas &amp; rekening</dt><dd class="num" id="hKas">–</dd></div>

          <div><dt>Sisa hutang</dt><dd class="num" id="hHutang">–</dd></div>

        </dl>

        <div class="stack">

          <div class="stack-bar" id="stackBar" role="img" aria-label="Komposisi harta per kategori"></div>

          <div class="stack-legend" id="stackLegend"></div>

        </div>

      </div>


      <div class="grid g-cash mt">

        <div class="panel">

          <h2>Arus kas</h2><p class="sub" id="flowPeriod">–</p>

          <div class="flow">

            <div class="flow-row"><span>Pemasukan</span><span class="v num up" id="fIn">–</span></div>

            <div class="flow-row"><span>Pengeluaran</span><span class="v num down" id="fOut">–</span></div>

            <div class="flow-row"><span>Surplus / defisit</span><span class="v num" id="fNet">–</span></div>

            <div>

              <div style="display:flex;justify-content:space-between;font-size:13px;margin-bottom:6px"><span>Rasio tabungan</span><b id="fRatio">–</b></div>

              <div class="ratio"><span id="fRatioBar" style="width:0"></span></div>

            </div>

          </div>

        </div>

        <div class="panel">

          <h2>Pemasukan dan pengeluaran per bulan</h2><p class="sub" id="monthlySub">–</p>

          <div class="chart-box"><canvas id="chMonthly"></canvas></div>

        </div>

      </div>


      <div class="grid g-2 mt">

        <div class="panel">

          <h2>Pengeluaran per jenis</h2><p class="sub">Pada periode terpilih, dari yang terbesar</p>

          <div class="chart-box" id="expBox"><canvas id="chExpense"></canvas></div>

        </div>

        <div class="panel">

          <h2>Saldo per rekening</h2><p class="sub">Saldo awal dari Data Harta ditambah mutasi transaksi dan pencairan hutang/piutang</p>

          <div class="table-wrap"><table id="tblSaldo"></table></div>

        </div>

      </div>


      <div class="panel mt" id="duePanel">

        <h2>Hutang &amp; piutang yang perlu diperhatikan</h2><p class="sub">Belum lunas, terlambat atau jatuh tempo dalam 30 hari</p>

        <div class="table-wrap"><table id="tblDue"></table></div>

      </div>


      <div class="panel mt">

        <h2>Transaksi terakhir</h2><p class="sub">Delapan catatan terbaru</p>

        <div class="table-wrap"><table id="tblRecent"></table></div>

      </div>

    </section>


    <!-- ================= DATA HARTA ================= -->

    <section class="page" id="page-harta">

      <div class="panel">

        <div class="toolbar">

          <input type="search" id="hSearch" placeholder="Cari nama aset, jenis, atau keterangan">

          <select id="hKat"><option value="">Semua kategori</option></select>

          <button class="btn btn-primary" id="btnAddHarta">+ Tambah harta</button>

        </div>

        <div class="summary" id="hSummary"></div>

        <div class="table-wrap"><table id="tblHarta"></table></div>

      </div>

    </section>


    <!-- ================= TRANSAKSI ================= -->

    <section class="page" id="page-transaksi">

      <div class="panel">

        <div class="toolbar">

          <input type="search" id="tSearch" placeholder="Cari jenis atau keterangan">

          <select id="tKat"><option value="">Semua kategori</option><option>Pemasukan</option><option>Pengeluaran</option></select>

          <select id="tRek"><option value="">Semua rekening</option></select>

        </div>

        <div class="summary" id="tSummary"></div>

        <div class="table-wrap"><table id="tblTrx"></table></div>

      </div>

    </section>


    <!-- ================= HUTANG & PIUTANG ================= -->

    <section class="page" id="page-hutang">

      <div class="panel">

        <div class="toolbar">

          <input type="search" id="dSearch" placeholder="Cari nama pihak atau keterangan">

          <select id="dJenis"><option value="">Hutang &amp; piutang</option><option>Hutang</option><option>Piutang</option></select>

          <select id="dStatus"><option value="aktif">Belum lunas</option><option value="lunas">Lunas</option><option value="">Semua status</option></select>

          <button class="btn btn-primary" id="btnAddHutang">+ Catat hutang/piutang</button>

        </div>

        <div class="summary" id="dSummary"></div>

        <div class="table-wrap"><table id="tblHutang"></table></div>

      </div>

    </section>


    <!-- ================= REKAPITULASI ================= -->

    <section class="page" id="page-rekap">

      <div class="panel">

        <div class="toolbar" style="justify-content:space-between">

          <div><h2 id="rekapTitle">Rekap transaksi</h2><p class="sub" style="margin:0">Jumlah per jenis transaksi untuk setiap bulan</p></div>

          <button class="btn btn-gold" id="btnExport">Unduh rekap (CSV)</button>

        </div>

        <div class="table-wrap"><table class="pivot" id="tblPivot"></table></div>

      </div>

      <div class="panel mt">

        <h2>Rekap harta per kategori</h2><p class="sub">Perbandingan nilai perolehan dengan nilai saat ini</p>

        <div class="table-wrap"><table id="tblRekapHarta"></table></div>

      </div>

    </section>

  </main>

</div>


<!-- ================= DIALOG HARTA ================= -->

<dialog id="dlgHarta">

  <form method="dialog" id="formHarta">

    <div class="dlg-head"><h3 id="dlgHartaTitle">Tambah harta</h3><button type="button" class="icon-btn" data-close aria-label="Tutup">✕</button></div>

    <div class="dlg-body">

      <input type="hidden" name="id">

      <div class="field"><label for="hfKat">Kategori</label><select id="hfKat" name="kategori" required></select></div>

      <div class="field"><label for="hfJenis">Jenis</label><select id="hfJenis" name="jenis" required></select></div>

      <div class="field full"><label for="hfNama">Nama aset</label><input id="hfNama" name="nama" placeholder="Contoh: Rumah Bintaro, Honda Vario, Tabungan utama"></div>

      <div class="field full"><label for="hfTgl" id="hfTglLabel">Tanggal perolehan</label><input type="date" id="hfTgl" name="tanggal"></div>

      <div class="field" id="wrapPerolehan"><label for="hfPer">Nilai perolehan</label><div class="money"><input id="hfPer" name="nilaiPerolehan" inputmode="numeric" data-money></div></div>

      <div class="field" id="wrapKini"><label for="hfKini" id="hfKiniLabel">Nilai saat ini</label><div class="money"><input id="hfKini" name="nilaiKini" inputmode="numeric" data-money required></div></div>

      <p class="hint field full" id="kasHint" hidden>Isi saldo rekening pada saat Anda mulai mencatat. Saldo terkini dihitung otomatis dari transaksi yang memakai rekening ini.</p>

      <div class="field full"><label for="hfKet">Keterangan</label><textarea id="hfKet" name="keterangan" placeholder="Opsional: nomor sertifikat, plat nomor, lokasi, dll."></textarea></div>

    </div>

    <div class="dlg-foot"><button type="button" class="btn btn-ghost" data-close>Batal</button><button class="btn btn-primary" type="submit">Simpan harta</button></div>

  </form>

</dialog>


<!-- ================= DIALOG TRANSAKSI ================= -->

<dialog id="dlgTrx">

  <form method="dialog" id="formTrx">

    <div class="dlg-head"><h3 id="dlgTrxTitle">Catat transaksi</h3><button type="button" class="icon-btn" data-close aria-label="Tutup">✕</button></div>

    <div class="dlg-body">

      <input type="hidden" name="id">

      <div class="field full">

        <label>Kategori transaksi</label>

        <div class="seg">

          <input type="radio" name="kategori" id="kIn" value="Pemasukan"><label for="kIn" class="in">Pemasukan</label>

          <input type="radio" name="kategori" id="kOut" value="Pengeluaran" checked><label for="kOut" class="out">Pengeluaran</label>

        </div>

      </div>

      <div class="field"><label for="tfTgl">Tanggal</label><input type="date" id="tfTgl" name="tanggal" required></div>

      <div class="field"><label for="tfRek" id="tfRekLabel">Rekening sumber</label><select id="tfRek" name="rekening" required></select></div>

      <div class="field full"><label for="tfJenis">Jenis transaksi</label><select id="tfJenis" name="jenis" required></select></div>

      <div class="field full"><label for="tfNom">Nominal</label><div class="money"><input id="tfNom" name="nominal" inputmode="numeric" data-money required placeholder="0"></div></div>

      <div class="field full"><label for="tfKet">Keterangan tambahan</label><textarea id="tfKet" name="keterangan" placeholder="Opsional"></textarea></div>

    </div>

    <div class="dlg-foot"><button type="button" class="btn btn-ghost" data-close>Batal</button><button class="btn btn-primary" type="submit">Simpan transaksi</button></div>

  </form>

</dialog>


<!-- ================= DIALOG HUTANG/PIUTANG ================= -->

<dialog id="dlgHutang">

  <form method="dialog" id="formHutang">

    <div class="dlg-head"><h3 id="dlgHutangTitle">Catat hutang/piutang</h3><button type="button" class="icon-btn" data-close aria-label="Tutup">✕</button></div>

    <div class="dlg-body">

      <input type="hidden" name="id">

      <div class="field full">

        <label>Jenis</label>

        <div class="seg">

          <input type="radio" name="jenis" id="dH" value="Hutang" checked><label for="dH" class="out">Hutang (saya meminjam)</label>

          <input type="radio" name="jenis" id="dP" value="Piutang"><label for="dP" class="in">Piutang (saya meminjamkan)</label>

        </div>

      </div>

      <div class="field full"><label for="dfPihak" id="dfPihakLabel">Pemberi pinjaman</label><input id="dfPihak" name="pihak" required placeholder="Nama orang atau lembaga"></div>

      <div class="field"><label for="dfTgl">Tanggal pinjaman</label><input type="date" id="dfTgl" name="tanggal" required></div>

      <div class="field"><label for="dfJt">Jatuh tempo</label><input type="date" id="dfJt" name="jatuhTempo"><span class="hint">Opsional</span></div>

      <div class="field full"><label for="dfNom">Nominal pokok</label><div class="money"><input id="dfNom" name="nominal" inputmode="numeric" data-money required placeholder="0"></div></div>

      <div class="field full"><label for="dfRek" id="dfRekLabel">Dana masuk ke rekening</label><select id="dfRek" name="rekening"></select>

        <span class="hint" id="dfRekHint">Pilih rekening bila dana pinjaman tercatat masuk ke saldo. Pilih "Tidak memengaruhi saldo" untuk hutang lama sebelum Anda mulai mencatat.</span></div>

      <div class="field full"><label for="dfKet">Keterangan</label><textarea id="dfKet" name="keterangan" placeholder="Opsional: tujuan pinjaman, skema cicilan, jaminan, dll."></textarea></div>

    </div>

    <div class="dlg-foot"><button type="button" class="btn btn-ghost" data-close>Batal</button><button class="btn btn-primary" type="submit">Simpan</button></div>

  </form>

</dialog>


<!-- ================= DIALOG PEMBAYARAN ================= -->

<dialog id="dlgBayar">

  <form method="dialog" id="formBayar">

    <div class="dlg-head"><h3 id="dlgBayarTitle">Catat pembayaran</h3><button type="button" class="icon-btn" data-close aria-label="Tutup">✕</button></div>

    <div class="dlg-body">

      <input type="hidden" name="refId">

      <p id="bfInfo" style="grid-column:1/-1;margin:0;padding:10px 12px;background:var(--jade-soft);border-radius:var(--r-sm)"></p>

      <div class="field"><label for="bfTgl">Tanggal</label><input type="date" id="bfTgl" name="tanggal" required></div>

      <div class="field"><label for="bfRek" id="bfRekLabel">Dibayar dari rekening</label><select id="bfRek" name="rekening" required></select></div>

      <div class="field full"><label for="bfNom">Nominal</label><div class="money"><input id="bfNom" name="nominal" inputmode="numeric" data-money required></div>

        <span class="hint">Otomatis tercatat di Transaksi sebagai <b id="bfJenisTrx"></b>.</span></div>

      <div class="field full"><label for="bfKet">Keterangan</label><textarea id="bfKet" name="keterangan" placeholder="Opsional, contoh: cicilan ke-3"></textarea></div>

    </div>

    <div class="dlg-foot"><button type="button" class="btn btn-ghost" data-close>Batal</button><button class="btn btn-primary" type="submit">Simpan pembayaran</button></div>

  </form>

</dialog>


<div id="toast" role="status"></div>


<script>

const BULAN = ['Januari','Februari','Maret','April','Mei','Juni','Juli','Agustus','September','Oktober','November','Desember'];

const BLN = ['Jan','Feb','Mar','Apr','Mei','Jun','Jul','Agu','Sep','Okt','Nov','Des'];

const KAT_COLORS = {

  'Harta Tidak Bergerak':'#0F4C45','Harta Bergerak':'#3E8E85','Harta Bergerak Lainnya':'#8A9A5B',

  'Surat Berharga':'#5B6F8C','Rekening Kas dan Setara Kas':'#C49A3A','Harta Lainnya':'#7A4E6E','Piutang':'#4F8FB0'

};

const S = { ref:null, harta:[], trx:[], hutang:[], bayar:[], openPay:new Set(), charts:{}, ready:false, token:null, user:null, pendingEmail:'', codePurpose:'VERIFIKASI' };

const TOKEN_KEY = 'hartakita_token';

const $ = s => document.querySelector(s);

const $$ = s => Array.from(document.querySelectorAll(s));

const rp = n => 'Rp ' + Math.round(n||0).toLocaleString('id-ID');

const rpSigned = n => (n<0?'− ':'') + rp(Math.abs(n));

const pct = n => (isFinite(n) ? n.toLocaleString('id-ID',{maximumFractionDigits:1}) : '0') + '%';

const esc = s => String(s??'').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));

const parseMoney = s => Number(String(s||'').replace(/\D/g,'')) || 0;

const fmtTgl = s => { if(!s) return '–'; const [y,m,d] = s.split('-'); return `${+d} ${BLN[+m-1]} ${y}`; };

const today = () => { const d=new Date(); d.setMinutes(d.getMinutes()-d.getTimezoneOffset()); return d.toISOString().slice(0,10); };

const short = n => { const a=Math.abs(n); return a>=1e9?(n/1e9).toFixed(1)+' M':a>=1e6?(n/1e6).toFixed(1)+' jt':a>=1e3?(n/1e3).toFixed(0)+' rb':String(n); };


/* ------------------------------------------------------------------

 * API_URL: isi dengan URL Web App (berakhiran /exec) bila file ini

 * di-hosting di domain sendiri. Biarkan kosong bila dibuka langsung

 * dari Apps Script — aplikasi otomatis memakai google.script.run.

 * ------------------------------------------------------------------ */

const API_URL = 'https://script.google.com/macros/s/AKfycbyDGBblIvbTyXfKh7LE-u_oQbzv_TIdeqEocVtEOLccDZwonkLXCHSM3SQhotDR1Pqc/exec';


function call(fn, ...args){

  if(window.google && google.script && google.script.run){

    return new Promise((res, rej) => google.script.run.withSuccessHandler(res).withFailureHandler(rej)[fn](...args));

  }

  if(!API_URL) return Promise.reject(new Error('API_URL belum diisi di Index.html.'));

  // text/plain agar browser tidak mengirim preflight CORS (Apps Script tidak mendukung OPTIONS)

  return fetch(API_URL, { method:'POST', redirect:'follow', headers:{ 'Content-Type':'text/plain;charset=utf-8' },

                          body: JSON.stringify({ fn, args }) })

    .then(r => { if(!r.ok) throw new Error('Server tidak merespons (' + r.status + '). Coba lagi.'); return r.json(); })

    .then(r => { if(!r.ok) throw new Error(r.error); return r.data; })

    .catch(e => { throw (e instanceof TypeError ? new Error('Tidak dapat terhubung ke server. Periksa koneksi internet.') : e); });

}

// Panggilan API yang butuh login: token otomatis disisipkan sebagai argumen pertama

function api(fn, ...args){

  return call(fn, S.token, ...args).catch(e => {

    if(String(e && e.message).includes('SESI_HABIS')){ endSession(); toast('Sesi berakhir. Silakan masuk kembali.', true); }

    throw e;

  });

}

function saveToken(t){ S.token = t; try{ t ? localStorage.setItem(TOKEN_KEY, t) : localStorage.removeItem(TOKEN_KEY); }catch(e){} }

function readToken(){ try{ return localStorage.getItem(TOKEN_KEY); }catch(e){ return null; } }

function loading(on){ $('#loader').className = on ? 'on' : ''; if(!on){ $('#loader').style.width='100%'; setTimeout(()=>$('#loader').style.width='',300);} }

function toast(msg, err){ const t=$('#toast'); t.textContent=msg; t.className='show'+(err?' err':''); clearTimeout(t._h); t._h=setTimeout(()=>t.className=err?'err':'',3200); }


/* ===================== Data load ===================== */

async function load(){

  loading(true);

  try{

    const d = await api('getInitData');

    Object.assign(S, { ref:d.ref, harta:d.harta, trx:d.trx, hutang:d.hutang, bayar:d.bayar, user:d.user });

    $('#uNama').textContent = d.user.nama;

    $('#uInfo').textContent = '@' + d.user.username + ' · ' + d.user.email;

    if(!S.ready){ initUI(); S.ready = true; }

    $('#auth').hidden = true; $('.app').hidden = false;

    refreshYears();

    renderAll();

  }catch(e){ if(!String(e.message).includes('SESI_HABIS')) toast('Gagal memuat data: ' + e.message, true); }

  finally{ loading(false); }

}


/* ===================== Init UI ===================== */

function opt(v, t){ return `<option value="${esc(v)}">${esc(t??v)}</option>`; }


function initUI(){

  $('#fBulan').innerHTML = opt('', 'Sepanjang tahun') + BULAN.map((b,i)=>opt(i+1,b)).join('');

  $('#fBulan').value = new Date().getMonth()+1;

  const kats = Object.keys(S.ref.harta);

  $('#hfKat').innerHTML = kats.map(k=>opt(k)).join('');

  $('#hKat').innerHTML += kats.map(k=>opt(k)).join('');

  $('#tfRek').innerHTML = S.ref.rekening.map(r=>opt(r)).join('');

  $('#tRek').innerHTML += S.ref.rekening.map(r=>opt(r)).join('');


  $$('.nav button').forEach(b => b.onclick = () => goPage(b.dataset.page));

  ['#fTahun','#fBulan'].forEach(s => $(s).onchange = renderAll);

  ['#hSearch','#hKat'].forEach(s => $(s).oninput = renderHarta);

  ['#tSearch','#tKat','#tRek'].forEach(s => $(s).oninput = renderTrx);

  $('#btnReload').onclick = load;

  $('#btnAddTrx').onclick = () => openTrx();

  $('#btnAddHarta').onclick = () => openHarta();

  $('#btnExport').onclick = exportRekap;

  $('#btnLogout').onclick = doLogout;

  ['#dSearch','#dJenis','#dStatus'].forEach(s => $(s).oninput = renderHutang);

  $('#btnAddHutang').onclick = () => openHutang();

  $('#dfRek').innerHTML = opt('', 'Tidak memengaruhi saldo') + S.ref.rekening.map(r=>opt(r)).join('');

  $('#bfRek').innerHTML = S.ref.rekening.map(r=>opt(r)).join('');

  $$('input[name=jenis]').forEach(r => r.onchange = syncHutangForm);

  $('#formHutang').onsubmit = submitHutang;

  $('#formBayar').onsubmit = submitBayar;


  $$('[data-money]').forEach(el => el.addEventListener('input', () => {

    const d = el.value.replace(/\D/g,''); el.value = d ? Number(d).toLocaleString('id-ID') : '';

  }));

  $$('[data-close]').forEach(b => b.onclick = () => b.closest('dialog').close());


  $('#hfKat').onchange = syncHartaForm;

  $$('input[name=kategori]').forEach(r => r.onchange = syncTrxForm);

  $('#formHarta').onsubmit = submitHarta;

  $('#formTrx').onsubmit = submitTrx;


  document.addEventListener('click', e => {

    const b = e.target.closest('[data-act]'); if(!b) return;

    const { act, id } = b.dataset;

    if(act==='edit-h') openHarta(S.harta.find(h=>h.id===id));

    if(act==='del-h') removeItem('deleteHarta', id, 'harta');

    if(act==='edit-t') openTrx(S.trx.find(t=>t.id===id));

    if(act==='del-t') removeItem('deleteTrx', id, 'trx');

    if(act==='add-h') openHarta();

    if(act==='add-t') openTrx();

    if(act==='add-d') openHutang();

    if(act==='edit-d') openHutang(S.hutang.find(h=>h.id===id));

    if(act==='del-d') removeHutang(id);

    if(act==='pay-d') openBayar(id);

    if(act==='hist-d'){ S.openPay.has(id) ? S.openPay.delete(id) : S.openPay.add(id); renderHutang(); }

    if(act==='del-b') removeBayar(id);

  });

}


function refreshYears(){

  const cur = $('#fTahun').value;

  const ys = new Set([new Date().getFullYear()]);

  S.trx.forEach(t => ys.add(+t.tanggal.slice(0,4)));

  $('#fTahun').innerHTML = [...ys].sort((a,b)=>b-a).map(y=>opt(y)).join('');

  $('#fTahun').value = cur && ys.has(+cur) ? cur : new Date().getFullYear();

}


function goPage(p){

  $$('.nav button').forEach(b => b.classList.toggle('active', b.dataset.page===p));

  $$('.page').forEach(s => s.classList.toggle('active', s.id==='page-'+p));

  $('#pageTitle').textContent = { dashboard:'Ringkasan', harta:'Data Harta', transaksi:'Transaksi', hutang:'Hutang & Piutang', rekap:'Rekapitulasi' }[p];

  $('.period').style.visibility = (p==='harta' || p==='hutang') ? 'hidden' : 'visible';

  window.scrollTo(0,0);

}


/* ===================== Perhitungan ===================== */

function period(){ return { y: $('#fTahun').value, m: $('#fBulan').value }; }

function inPeriod(t){ const {y,m} = period(); return t.tanggal.slice(0,4)===y && (!m || +t.tanggal.slice(5,7)===+m); }

function periodLabel(){ const {y,m} = period(); return m ? `${BULAN[m-1]} ${y}` : `Tahun ${y}`; }

const sum = (arr, f) => arr.reduce((a,x)=>a+(f?f(x):x), 0);


function saldoRekening(){

  const map = {};

  S.ref.rekening.forEach(r => map[r] = { awal:0, masuk:0, keluar:0 });

  S.harta.filter(h => h.kategori===S.ref.kategoriKas).forEach(h => {

    const r = S.ref.aliasRekening[h.jenis] || h.jenis;

    (map[r] ||= { awal:0, masuk:0, keluar:0 }).awal += h.nilaiKini;

  });

  S.trx.forEach(t => {

    const m = (map[t.rekening] ||= { awal:0, masuk:0, keluar:0 });

    t.kategori==='Pemasukan' ? m.masuk += t.nominal : m.keluar += t.nominal;

  });

  // Pencairan pokok: hutang menambah saldo, piutang mengurangi saldo (bila rekening dipilih)

  S.hutang.filter(h => h.rekening).forEach(h => {

    const m = (map[h.rekening] ||= { awal:0, masuk:0, keluar:0 });

    h.jenis==='Hutang' ? m.masuk += h.nominal : m.keluar += h.nominal;

  });

  Object.values(map).forEach(m => m.akhir = m.awal + m.masuk - m.keluar);

  return map;

}


function komposisi(){

  const saldo = saldoRekening();

  const kasTotal = sum(Object.values(saldo), m=>m.akhir);

  const rows = Object.keys(S.ref.harta).map(k => {

    const items = S.harta.filter(h=>h.kategori===k);

    const isKas = k===S.ref.kategoriKas;

    return { k, n: items.length, perolehan: sum(items,h=>h.nilaiPerolehan),

             kini: isKas ? kasTotal : sum(items,h=>h.nilaiKini), isKas };

  });

  const hs = hutangStatus().filter(h=>!h.lunas);

  const piu = hs.filter(h=>h.jenis==='Piutang'), hut = hs.filter(h=>h.jenis==='Hutang');

  const sisaPiutang = sum(piu,h=>h.sisa), sisaHutang = sum(hut,h=>h.sisa);

  rows.push({ k:'Piutang', n:piu.length, perolehan:sisaPiutang, kini:sisaPiutang, isPiutang:true });

  const totalHarta = sum(rows,r=>r.kini);

  return { rows, kasTotal, saldo, sisaHutang, nHutang:hut.length, totalHarta, bersih: totalHarta - sisaHutang };

}


function daysTo(d){ return Math.round((new Date(d+'T00:00:00') - new Date(today()+'T00:00:00')) / 864e5); }

function hutangStatus(){

  return S.hutang.map(h => {

    const pays = S.bayar.filter(b=>b.refId===h.id).sort((a,b)=>a.tanggal.localeCompare(b.tanggal));

    const paid = sum(pays,b=>b.nominal), sisa = Math.max(0, h.nominal - paid);

    const lunas = sisa <= 0, dd = h.jatuhTempo ? daysTo(h.jatuhTempo) : null;

    const overdue = !lunas && dd !== null && dd < 0, dueSoon = !lunas && dd !== null && dd >= 0 && dd <= 30;

    return { ...h, pays, paid, sisa, lunas, dd, overdue, dueSoon };

  });

}

function statusBadge(h){

  if(h.lunas) return '<span class="badge b-lunas">Lunas</span>';

  if(h.overdue) return `<span class="badge b-telat">Terlambat ${-h.dd} hari</span>`;

  if(h.dueSoon) return `<span class="badge b-dekat">${h.dd===0?'Jatuh tempo hari ini':h.dd+' hari lagi'}</span>`;

  return '<span class="badge b-jalan">Berjalan</span>';

}


/* ===================== Render ===================== */

function renderAll(){ renderDashboard(); renderHarta(); renderTrx(); renderHutang(); renderRekap(); }


function renderDashboard(){

  const { rows, kasTotal, saldo, sisaHutang, nHutang, totalHarta, bersih } = komposisi();

  const aset = rows.filter(r=>!r.isKas && !r.isPiutang);

  const asetKini = sum(aset,r=>r.kini), asetPer = sum(aset,r=>r.perolehan);

  const total = totalHarta;

  const delta = asetKini - asetPer;


  $('#hTotal').innerHTML = `<small>Rp</small>${bersih<0?'−':''}${Math.round(Math.abs(bersih)).toLocaleString('id-ID')}`;

  $('#hNote').textContent = `Total harta ${rpSigned(totalHarta)} dikurangi sisa hutang ${rp(sisaHutang)}`;

  $('#hHutang').innerHTML = sisaHutang ? `<span class="down">${rp(sisaHutang)}</span> <span style="font-weight:500;color:var(--muted);font-size:12px">(${nHutang})</span>` : rp(0);

  $('#hPerolehan').textContent = rp(asetPer);

  $('#hDelta').innerHTML = `<span class="${delta>=0?'up':'down'}">${delta>=0?'+':'−'}${rp(Math.abs(delta)).slice(3)} (${pct(asetPer?delta/asetPer*100:0)})</span>`;

  $('#hKas').textContent = rpSigned(kasTotal);


  const pos = rows.filter(r=>r.kini>0);

  $('#stackBar').innerHTML = pos.length

    ? pos.map(r=>`<span title="${esc(r.k)}: ${rp(r.kini)}" style="flex-grow:${r.kini};background:${KAT_COLORS[r.k]}"></span>`).join('')

    : '';

  $('#stackLegend').innerHTML = S.harta.length || S.trx.length || S.hutang.length

    ? rows.map(r=>`<div class="leg"><i style="background:${KAT_COLORS[r.k]}"></i><b>${esc(r.k)}</b><span class="num">${rpSigned(r.kini)} · ${pct(total>0? r.kini/total*100 : 0)}</span></div>`).join('')

    : `<div class="empty" style="grid-column:1/-1">Belum ada data harta. <br><button class="btn btn-primary" data-act="add-h">Tambah harta pertama</button></div>`;


  // Arus kas

  const pt = S.trx.filter(inPeriod);

  const tin = sum(pt.filter(t=>t.kategori==='Pemasukan'), t=>t.nominal);

  const tout = sum(pt.filter(t=>t.kategori==='Pengeluaran'), t=>t.nominal);

  const net = tin - tout, ratio = tin ? Math.max(0, net/tin*100) : 0;

  $('#flowPeriod').textContent = periodLabel();

  $('#fIn').textContent = rp(tin); $('#fOut').textContent = rp(tout);

  $('#fNet').textContent = rpSigned(net); $('#fNet').className = 'v num ' + (net>=0?'up':'down');

  $('#fRatio').textContent = pct(ratio); $('#fRatioBar').style.width = Math.min(100,ratio)+'%';


  // Grafik bulanan

  const y = period().y;

  const mIn = Array(12).fill(0), mOut = Array(12).fill(0);

  S.trx.filter(t=>t.tanggal.startsWith(y)).forEach(t => (t.kategori==='Pemasukan'?mIn:mOut)[+t.tanggal.slice(5,7)-1] += t.nominal);

  $('#monthlySub').textContent = 'Tahun ' + y;

  drawChart('chMonthly', {

    type:'bar',

    data:{ labels:BLN, datasets:[

      { label:'Pemasukan', data:mIn, backgroundColor:'#2F7D4E', borderRadius:4, maxBarThickness:18 },

      { label:'Pengeluaran', data:mOut, backgroundColor:'#B4443A', borderRadius:4, maxBarThickness:18 } ]},

    options: baseOpts({ scales:{ y:{ ticks:{ callback:v=>short(v) }, grid:{ color:'#EEF2EF' } }, x:{ grid:{ display:false } } } })

  });


  // Pengeluaran per jenis

  const byJenis = {};

  pt.filter(t=>t.kategori==='Pengeluaran').forEach(t => byJenis[t.jenis] = (byJenis[t.jenis]||0) + t.nominal);

  const ent = Object.entries(byJenis).sort((a,b)=>b[1]-a[1]);

  $('#expBox').style.height = Math.max(200, ent.length*30 + 40) + 'px';

  drawChart('chExpense', {

    type:'bar',

    data:{ labels:ent.map(e=>e[0]), datasets:[{ label:'Pengeluaran', data:ent.map(e=>e[1]), backgroundColor:'#0F4C45', borderRadius:4, maxBarThickness:16 }]},

    options: baseOpts({ indexAxis:'y', plugins:{ legend:{ display:false } },

      scales:{ x:{ ticks:{ callback:v=>short(v) }, grid:{ color:'#EEF2EF' } }, y:{ grid:{ display:false } } } })

  }, ent.length ? '' : 'Belum ada pengeluaran pada ' + periodLabel());


  // Saldo rekening

  const rek = Object.entries(saldo);

  $('#tblSaldo').innerHTML = `<thead><tr><th>Rekening</th><th class="r">Saldo awal</th><th class="r">Masuk</th><th class="r">Keluar</th><th class="r">Saldo akhir</th></tr></thead><tbody>` +

    rek.map(([r,m])=>`<tr><td><b>${esc(r)}</b></td><td class="r num">${rp(m.awal)}</td><td class="r num up">${rp(m.masuk)}</td><td class="r num down">${rp(m.keluar)}</td><td class="r num"><b class="${m.akhir<0?'down':''}">${rpSigned(m.akhir)}</b></td></tr>`).join('') +

    `<tr class="total"><td>Total</td><td class="r num">${rp(sum(rek,e=>e[1].awal))}</td><td class="r num">${rp(sum(rek,e=>e[1].masuk))}</td><td class="r num">${rp(sum(rek,e=>e[1].keluar))}</td><td class="r num">${rpSigned(kasTotal)}</td></tr></tbody>`;


  // Hutang & piutang yang perlu diperhatikan

  const due = hutangStatus().filter(h=>h.overdue||h.dueSoon).sort((a,b)=>a.dd-b.dd);

  $('#duePanel').hidden = !due.length;

  $('#tblDue').innerHTML = `<thead><tr><th>Jenis</th><th>Pihak</th><th>Jatuh tempo</th><th class="r">Sisa</th><th>Status</th><th></th></tr></thead><tbody>` +

    due.map(h=>`<tr><td><span class="badge ${h.jenis==='Hutang'?'b-hutang':'b-piutang'}">${h.jenis}</span></td><td><b>${esc(h.pihak)}</b></td>

      <td class="num">${fmtTgl(h.jatuhTempo)}</td><td class="r num"><b>${rp(h.sisa)}</b></td><td>${statusBadge(h)}</td>

      <td class="r"><button class="btn btn-ghost btn-sm" data-act="pay-d" data-id="${esc(h.id)}">${h.jenis==='Hutang'?'Bayar':'Terima'}</button></td></tr>`).join('') + '</tbody>';


  // Transaksi terakhir

  const recent = [...S.trx].sort((a,b)=>b.tanggal.localeCompare(a.tanggal)).slice(0,8);

  $('#tblRecent').innerHTML = recent.length ? trxTable(recent, false)

    : `<tbody><tr><td class="empty">Belum ada transaksi.<br><button class="btn btn-primary" data-act="add-t">Catat transaksi pertama</button></td></tr></tbody>`;

}


function baseOpts(extra){

  const horiz = extra.indexAxis === 'y';

  return Object.assign({ responsive:true, maintainAspectRatio:false }, extra, {

    plugins: Object.assign({

      legend:{ position:'bottom', labels:{ boxWidth:10, boxHeight:10, usePointStyle:true } },

      tooltip:{ callbacks:{ label: c => ` ${c.dataset.label}: ${rp(horiz ? c.parsed.x : c.parsed.y)}` } }

    }, extra.plugins || {})

  });

}


function drawChart(id, cfg, emptyMsg){

  if(typeof Chart==='undefined') return;

  S.charts[id]?.destroy();

  const cv = document.getElementById(id), box = cv.parentElement;

  box.querySelector('.empty')?.remove();

  cv.style.display = emptyMsg ? 'none' : '';

  if(emptyMsg){ box.insertAdjacentHTML('beforeend', `<div class="empty">${esc(emptyMsg)}</div>`); return; }

  Chart.defaults.font.family = "'Plus Jakarta Sans', system-ui, sans-serif";

  Chart.defaults.color = '#5E716C';

  S.charts[id] = new Chart(cv, cfg);

}


function trxTable(list, actions=true){

  return `<thead><tr><th>Tanggal</th><th>Kategori</th><th>Jenis transaksi</th><th>Rekening</th><th class="r">Nominal</th><th>Keterangan</th>${actions?'<th></th>':''}</tr></thead><tbody>` +

    list.map(t=>`<tr><td class="num">${fmtTgl(t.tanggal)}</td>

      <td><span class="badge ${t.kategori==='Pemasukan'?'b-in':'b-out'}">${esc(t.kategori)}</span></td>

      <td>${esc(t.jenis)}</td><td>${esc(t.rekening)}</td>

      <td class="r num"><b class="${t.kategori==='Pemasukan'?'up':'down'}">${t.kategori==='Pemasukan'?'+':'−'} ${rp(t.nominal)}</b></td>

      <td class="ket">${esc(t.keterangan)||'–'}</td>

      ${!actions?'':t.id.startsWith('B-')

        ? `<td class="r"><span class="lock" title="Ubah atau hapus lewat menu Hutang & Piutang">Dari hutang/piutang</span></td>`

        : `<td class="r" style="white-space:nowrap"><button class="icon-btn" data-act="edit-t" data-id="${esc(t.id)}" title="Ubah">✎</button><button class="icon-btn del" data-act="del-t" data-id="${esc(t.id)}" title="Hapus">🗑</button></td>`}</tr>`).join('') + '</tbody>';

}


function renderHarta(){

  const q = $('#hSearch').value.toLowerCase(), kf = $('#hKat').value;

  const list = S.harta.filter(h => (!kf || h.kategori===kf) &&

    (!q || [h.nama,h.jenis,h.keterangan].join(' ').toLowerCase().includes(q)));

  const per = sum(list,h=>h.nilaiPerolehan), kini = sum(list,h=>h.nilaiKini);

  $('#hSummary').innerHTML = `<div><span>Jumlah item</span><b>${list.length}</b></div>

    <div><span>Total nilai perolehan</span><b class="num">${rp(per)}</b></div>

    <div><span>Total nilai saat ini</span><b class="num">${rp(kini)}</b></div>`;

  if(!list.length){

    $('#tblHarta').innerHTML = `<tbody><tr><td class="empty">${S.harta.length?'Tidak ada harta yang cocok dengan pencarian.':'Belum ada data harta.<br><button class="btn btn-primary" data-act="add-h">Tambah harta</button>'}</td></tr></tbody>`;

    return;

  }

  let html = `<thead><tr><th>Jenis</th><th>Nama aset</th><th>Tgl perolehan</th><th class="r">Nilai perolehan</th><th class="r">Nilai saat ini</th><th class="r">Perubahan</th><th>Keterangan</th><th></th></tr></thead><tbody>`;

  Object.keys(S.ref.harta).forEach(k => {

    const items = list.filter(h=>h.kategori===k); if(!items.length) return;

    const isKas = k===S.ref.kategoriKas;

    html += `<tr class="group"><td colspan="3">${esc(k)}${isKas?' <span style="font-weight:500">(saldo awal)</span>':''}</td><td class="r num">${rp(sum(items,h=>h.nilaiPerolehan))}</td><td class="r num">${rp(sum(items,h=>h.nilaiKini))}</td><td colspan="3"></td></tr>`;

    items.forEach(h => {

      const d = h.nilaiKini - h.nilaiPerolehan, p = h.nilaiPerolehan ? d/h.nilaiPerolehan*100 : 0;

      html += `<tr><td>${esc(h.jenis)}</td><td><b>${esc(h.nama)||'–'}</b></td><td class="num">${fmtTgl(h.tanggal)}</td>

        <td class="r num">${rp(h.nilaiPerolehan)}</td><td class="r num">${rp(h.nilaiKini)}</td>

        <td class="r num ${d>0?'up':d<0?'down':''}">${isKas?'–':(d>=0?'+':'−')+pct(Math.abs(p))}</td>

        <td class="ket">${esc(h.keterangan)||'–'}</td>

        <td class="r" style="white-space:nowrap"><button class="icon-btn" data-act="edit-h" data-id="${esc(h.id)}" title="Ubah">✎</button><button class="icon-btn del" data-act="del-h" data-id="${esc(h.id)}" title="Hapus">🗑</button></td></tr>`;

    });

  });

  $('#tblHarta').innerHTML = html + '</tbody>';

}


function renderTrx(){

  const q = $('#tSearch').value.toLowerCase(), kf = $('#tKat').value, rf = $('#tRek').value;

  const list = S.trx.filter(t => inPeriod(t) && (!kf||t.kategori===kf) && (!rf||t.rekening===rf) &&

    (!q || (t.jenis+' '+t.keterangan).toLowerCase().includes(q))).sort((a,b)=>b.tanggal.localeCompare(a.tanggal));

  const tin = sum(list.filter(t=>t.kategori==='Pemasukan'),t=>t.nominal), tout = sum(list.filter(t=>t.kategori==='Pengeluaran'),t=>t.nominal);

  $('#tSummary').innerHTML = `<div><span>Periode</span><b>${periodLabel()}</b></div>

    <div><span>Pemasukan</span><b class="num up">${rp(tin)}</b></div>

    <div><span>Pengeluaran</span><b class="num down">${rp(tout)}</b></div>

    <div><span>Selisih</span><b class="num">${rpSigned(tin-tout)}</b></div>

    <div><span>Jumlah catatan</span><b>${list.length}</b></div>`;

  $('#tblTrx').innerHTML = list.length ? trxTable(list)

    : `<tbody><tr><td class="empty">Tidak ada transaksi pada ${periodLabel()} dengan filter ini.<br><button class="btn btn-primary" data-act="add-t">Catat transaksi</button></td></tr></tbody>`;

}


function renderRekap(){

  const y = period().y;

  $('#rekapTitle').textContent = 'Rekap transaksi ' + y;

  const trx = S.trx.filter(t=>t.tanggal.startsWith(y));

  let html = `<thead><tr><th>Jenis transaksi</th>${BLN.map(b=>`<th class="r">${b}</th>`).join('')}<th class="r">Total</th></tr></thead><tbody>`;

  const tot = {};

  S.ref.kategoriTrx.forEach(kat => {

    const extra = [...new Set(trx.filter(t=>t.kategori===kat && !S.ref.jenisTrx[kat].includes(t.jenis)).map(t=>t.jenis))];

    const t13 = Array(13).fill(0);

    html += `<tr class="group"><td colspan="14">${kat}</td></tr>`;

    S.ref.jenisTrx[kat].concat(extra).forEach(j => {

      const r = Array(12).fill(0);

      trx.filter(t=>t.kategori===kat && t.jenis===j).forEach(t => r[+t.tanggal.slice(5,7)-1] += t.nominal);

      const s = sum(r); r.forEach((v,i)=>t13[i]+=v); t13[12]+=s;

      html += `<tr><td>${esc(j)}</td>${r.map(v=>`<td class="r num">${v?short(v):'–'}</td>`).join('')}<td class="r num"><b>${s?short(s):'–'}</b></td></tr>`;

    });

    html += `<tr class="total"><td>Total ${kat.toLowerCase()}</td>${t13.map(v=>`<td class="r num">${short(v)}</td>`).join('')}</tr>`;

    tot[kat] = t13;

  });

  const net = tot.Pemasukan.map((v,i)=>v-tot.Pengeluaran[i]);

  html += `<tr class="total"><td>Surplus / defisit</td>${net.map(v=>`<td class="r num ${v<0?'down':'up'}">${short(v)}</td>`).join('')}</tr></tbody>`;

  $('#tblPivot').innerHTML = html;


  const { rows, sisaHutang, nHutang, bersih } = komposisi();

  const tp = sum(rows,r=>r.perolehan), tk = sum(rows,r=>r.kini);

  $('#tblRekapHarta').innerHTML = `<thead><tr><th>Kategori</th><th class="r">Item</th><th class="r">Nilai perolehan</th><th class="r">Nilai saat ini</th><th class="r">Selisih</th><th class="r">Porsi</th></tr></thead><tbody>` +

    rows.map(r=>{ const d=r.kini-r.perolehan; return `<tr><td>${esc(r.k)}${r.isKas?' <span class="ket">(saldo berjalan)</span>':r.isPiutang?' <span class="ket">(sisa belum dibayar)</span>':''}</td><td class="r">${r.n}</td><td class="r num">${rp(r.perolehan)}</td><td class="r num">${rpSigned(r.kini)}</td><td class="r num ${d>=0?'up':'down'}">${rpSigned(d)}</td><td class="r num">${pct(tk>0?r.kini/tk*100:0)}</td></tr>`; }).join('') +

    `<tr class="total"><td>Total harta</td><td class="r">${sum(rows,r=>r.n)}</td><td class="r num">${rp(tp)}</td><td class="r num">${rpSigned(tk)}</td><td class="r num">${rpSigned(tk-tp)}</td><td class="r">100%</td></tr>` +

    `<tr><td>Dikurangi sisa hutang</td><td class="r">${nHutang}</td><td></td><td class="r num down">− ${rp(sisaHutang)}</td><td></td><td></td></tr>` +

    `<tr class="group"><td>Kekayaan bersih</td><td></td><td></td><td class="r num">${rpSigned(bersih)}</td><td></td><td></td></tr></tbody>`;

}


/* ===================== Hutang & Piutang ===================== */

function renderHutang(){

  const q = $('#dSearch').value.toLowerCase(), jf = $('#dJenis').value, sf = $('#dStatus').value;

  const all = hutangStatus();

  const aktif = all.filter(h=>!h.lunas);

  const sH = sum(aktif.filter(h=>h.jenis==='Hutang'),h=>h.sisa), sP = sum(aktif.filter(h=>h.jenis==='Piutang'),h=>h.sisa);

  const telat = aktif.filter(h=>h.overdue).length;

  $('#dSummary').innerHTML = `<div><span>Sisa hutang</span><b class="num down">${rp(sH)}</b></div>

    <div><span>Sisa piutang</span><b class="num" style="color:#3D6E8C">${rp(sP)}</b></div>

    <div><span>Posisi bersih</span><b class="num ${sP-sH>=0?'up':'down'}">${rpSigned(sP-sH)}</b></div>

    <div><span>Terlambat bayar</span><b class="${telat?'down':''}">${telat} catatan</b></div>`;


  const list = all.filter(h => (!jf||h.jenis===jf) && (sf==='' || (sf==='lunas') === h.lunas) &&

    (!q || (h.pihak+' '+h.keterangan).toLowerCase().includes(q)))

    .sort((a,b)=> (a.lunas-b.lunas) || ((a.jatuhTempo||'9999') .localeCompare(b.jatuhTempo||'9999')) || b.tanggal.localeCompare(a.tanggal));


  if(!list.length){

    $('#tblHutang').innerHTML = `<tbody><tr><td class="empty">${S.hutang.length?'Tidak ada catatan yang cocok dengan filter.':'Belum ada catatan hutang atau piutang.'}<br><button class="btn btn-primary" data-act="add-d">Catat hutang/piutang</button></td></tr></tbody>`;

    return;

  }

  let html = `<thead><tr><th>Jenis</th><th>Pihak</th><th>Jatuh tempo</th><th class="r">Pokok</th><th class="r">Terbayar</th><th class="r">Sisa</th><th>Status</th><th></th></tr></thead><tbody>`;

  list.forEach(h => {

    const p = h.nominal ? Math.min(100, h.paid/h.nominal*100) : 0, open = S.openPay.has(h.id);

    html += `<tr><td><span class="badge ${h.jenis==='Hutang'?'b-hutang':'b-piutang'}">${h.jenis}</span></td>

      <td><b>${esc(h.pihak)}</b><div class="ket">Sejak ${fmtTgl(h.tanggal)}${h.keterangan?' · '+esc(h.keterangan):''}</div></td>

      <td class="num">${fmtTgl(h.jatuhTempo)}</td>

      <td class="r num">${rp(h.nominal)}</td>

      <td class="r num">${rp(h.paid)}<div class="prog"><span style="width:${p}%"></span></div></td>

      <td class="r num"><b>${rp(h.sisa)}</b></td><td>${statusBadge(h)}</td>

      <td class="r" style="white-space:nowrap">

        ${h.lunas?'':`<button class="btn btn-ghost btn-sm" data-act="pay-d" data-id="${esc(h.id)}">${h.jenis==='Hutang'?'Bayar':'Terima'}</button>`}

        <button class="icon-btn" data-act="hist-d" data-id="${esc(h.id)}" title="Riwayat pembayaran" aria-expanded="${open}">${open?'▴':'▾'} ${h.pays.length}</button>

        <button class="icon-btn" data-act="edit-d" data-id="${esc(h.id)}" title="Ubah">✎</button>

        <button class="icon-btn del" data-act="del-d" data-id="${esc(h.id)}" title="Hapus">🗑</button></td></tr>`;

    if(open){

      html += `<tr class="pay-row"><td colspan="8"><div class="pay-list"><p>Riwayat ${h.jenis==='Hutang'?'pembayaran':'penerimaan'}</p>` +

        (h.pays.length ? `<table><thead><tr><th>Tanggal</th><th>Rekening</th><th class="r">Nominal</th><th>Keterangan</th><th></th></tr></thead><tbody>` +

          h.pays.map(b=>`<tr><td class="num">${fmtTgl(b.tanggal)}</td><td>${esc(b.rekening)}</td><td class="r num">${rp(b.nominal)}</td><td class="ket">${esc(b.keterangan)||'–'}</td>

            <td class="r"><button class="icon-btn del" data-act="del-b" data-id="${esc(b.id)}" title="Hapus pembayaran">🗑</button></td></tr>`).join('') + '</tbody></table>'

          : '<div class="ket">Belum ada pembayaran.</div>') + '</div></td></tr>';

    }

  });

  $('#tblHutang').innerHTML = html + '</tbody>';

}


function syncHutangForm(){

  const isH = document.querySelector('input[name=jenis]:checked').value === 'Hutang';

  $('#dfPihakLabel').textContent = isH ? 'Pemberi pinjaman' : 'Peminjam';

  $('#dfRekLabel').textContent = isH ? 'Dana pinjaman masuk ke rekening' : 'Dana dipinjamkan dari rekening';

}

function openHutang(h){

  const f = $('#formHutang'); f.reset();

  $('#dlgHutangTitle').textContent = h ? 'Ubah hutang/piutang' : 'Catat hutang/piutang';

  f.id.value = h?.id || '';

  (h?.jenis==='Piutang' ? $('#dP') : $('#dH')).checked = true;

  syncHutangForm();

  f.tanggal.value = h?.tanggal || today();

  if(h){ f.pihak.value = h.pihak; f.jatuhTempo.value = h.jatuhTempo; f.nominal.value = h.nominal.toLocaleString('id-ID');

         f.rekening.value = h.rekening; f.keterangan.value = h.keterangan; }

  $('#dlgHutang').showModal();

}

async function submitHutang(e){

  e.preventDefault(); const f = e.target;

  const o = { id:f.id.value, jenis:f.jenis.value, pihak:f.pihak.value, tanggal:f.tanggal.value, jatuhTempo:f.jatuhTempo.value,

    nominal:parseMoney(f.nominal.value), rekening:f.rekening.value, keterangan:f.keterangan.value };

  if(!o.pihak.trim()){ toast('Isi nama pihak', true); return; }

  if(o.nominal<=0){ toast('Isi nominal lebih dari 0', true); return; }

  await saveMulti('saveHutang', o, $('#dlgHutang'), o.id ? 'Perubahan disimpan' : (o.jenis + ' dicatat'));

}

function openBayar(id){

  const h = hutangStatus().find(x=>x.id===id); if(!h) return;

  const f = $('#formBayar'); f.reset();

  const isH = h.jenis==='Hutang';

  $('#dlgBayarTitle').textContent = isH ? 'Bayar hutang' : 'Terima pembayaran piutang';

  $('#bfInfo').innerHTML = `${isH?'Hutang ke':'Piutang dari'} <b>${esc(h.pihak)}</b>. Sisa <b>${rp(h.sisa)}</b> dari pokok ${rp(h.nominal)}.`;

  $('#bfRekLabel').textContent = isH ? 'Dibayar dari rekening' : 'Diterima di rekening';

  $('#bfJenisTrx').textContent = isH ? 'Pengeluaran · Pembayaran Hutang/Cicilan' : 'Pemasukan · Pengembalian Hutang';

  f.refId.value = id; f.tanggal.value = today(); f.nominal.value = h.sisa.toLocaleString('id-ID');

  if(h.rekening) f.rekening.value = h.rekening;

  $('#dlgBayar').showModal();

  setTimeout(()=>$('#bfNom').select(), 50);

}

async function submitBayar(e){

  e.preventDefault(); const f = e.target;

  const o = { refId:f.refId.value, tanggal:f.tanggal.value, rekening:f.rekening.value, nominal:parseMoney(f.nominal.value), keterangan:f.keterangan.value };

  if(o.nominal<=0){ toast('Isi nominal lebih dari 0', true); return; }

  S.openPay.add(o.refId);

  await saveMulti('saveBayar', o, $('#dlgBayar'), 'Pembayaran dicatat');

}

async function removeHutang(id){

  const h = hutangStatus().find(x=>x.id===id); if(!h) return;

  const msg = h.pays.length ? `Hapus catatan ini beserta ${h.pays.length} pembayaran dan transaksi terkaitnya?` : 'Hapus catatan ini?';

  if(!confirm(msg + ' Tindakan ini tidak dapat dibatalkan.')) return;

  loading(true);

  try{ Object.assign(S, await api('deleteHutang', id)); refreshYears(); renderAll(); toast('Catatan dihapus'); }

  catch(e){ toast(e.message, true); } finally{ loading(false); }

}

async function removeBayar(id){

  if(!confirm('Hapus pembayaran ini? Transaksi terkait di menu Transaksi juga akan dihapus.')) return;

  loading(true);

  try{ Object.assign(S, await api('deleteBayar', id)); refreshYears(); renderAll(); toast('Pembayaran dihapus'); }

  catch(e){ toast(e.message, true); } finally{ loading(false); }

}

async function saveMulti(fn, obj, dlg, msg){

  const btn = dlg.querySelector('[type=submit]'); btn.disabled = true; loading(true);

  try{ Object.assign(S, await api(fn, obj)); dlg.close(); refreshYears(); renderAll(); toast(msg); }

  catch(e){ toast(e.message, true); }

  finally{ btn.disabled = false; loading(false); }

}


/* ===================== Form Harta ===================== */

function syncHartaForm(keepJenis){

  const k = $('#hfKat').value, isKas = k===S.ref.kategoriKas;

  const cur = $('#hfJenis').value;

  $('#hfJenis').innerHTML = S.ref.harta[k].map(j=>opt(j)).join('');

  if(keepJenis===true && S.ref.harta[k].includes(cur)) $('#hfJenis').value = cur;

  $('#wrapPerolehan').hidden = isKas;

  $('#kasHint').hidden = !isKas;

  $('#hfKiniLabel').textContent = isKas ? 'Saldo awal rekening' : 'Nilai saat ini';

  $('#hfTglLabel').textContent = isKas ? 'Tanggal saldo awal' : 'Tanggal perolehan';

  $('#wrapKini').classList.toggle('full', isKas);

}

function openHarta(h){

  const f = $('#formHarta'); f.reset();

  $('#dlgHartaTitle').textContent = h ? 'Ubah harta' : 'Tambah harta';

  f.id.value = h?.id || '';

  $('#hfKat').value = h?.kategori || Object.keys(S.ref.harta)[0];

  syncHartaForm();

  if(h){

    $('#hfJenis').value = h.jenis; f.nama.value = h.nama; f.tanggal.value = h.tanggal;

    f.nilaiPerolehan.value = h.nilaiPerolehan ? h.nilaiPerolehan.toLocaleString('id-ID') : '';

    f.nilaiKini.value = h.nilaiKini ? h.nilaiKini.toLocaleString('id-ID') : '';

    f.keterangan.value = h.keterangan;

  }

  $('#dlgHarta').showModal();

}

async function submitHarta(e){

  e.preventDefault();

  const f = e.target;

  const o = { id:f.id.value, kategori:f.kategori.value, jenis:f.jenis.value, nama:f.nama.value, tanggal:f.tanggal.value,

    nilaiPerolehan:parseMoney(f.nilaiPerolehan.value), nilaiKini:parseMoney(f.nilaiKini.value), keterangan:f.keterangan.value };

  await save('saveHarta', o, 'harta', $('#dlgHarta'), o.id ? 'Perubahan harta disimpan' : 'Harta disimpan');

}


/* ===================== Form Transaksi ===================== */

function syncTrxForm(){

  const kat = document.querySelector('input[name=kategori]:checked').value;

  const cur = $('#tfJenis').value;

  $('#tfJenis').innerHTML = S.ref.jenisTrx[kat].map(j=>opt(j)).join('');

  if(S.ref.jenisTrx[kat].includes(cur)) $('#tfJenis').value = cur;

  $('#tfRekLabel').textContent = kat==='Pemasukan' ? 'Rekening tujuan' : 'Rekening sumber';

}

function openTrx(t){

  const f = $('#formTrx'); f.reset();

  $('#dlgTrxTitle').textContent = t ? 'Ubah transaksi' : 'Catat transaksi';

  f.id.value = t?.id || '';

  (t?.kategori==='Pemasukan' ? $('#kIn') : $('#kOut')).checked = true;

  syncTrxForm();

  f.tanggal.value = t?.tanggal || today();

  if(t){ $('#tfJenis').value = t.jenis; f.rekening.value = t.rekening; f.nominal.value = t.nominal.toLocaleString('id-ID'); f.keterangan.value = t.keterangan; }

  $('#dlgTrx').showModal();

  setTimeout(()=>$('#tfNom').focus(), 50);

}

async function submitTrx(e){

  e.preventDefault();

  const f = e.target;

  const o = { id:f.id.value, tanggal:f.tanggal.value, kategori:f.kategori.value, jenis:f.jenis.value,

    rekening:f.rekening.value, nominal:parseMoney(f.nominal.value), keterangan:f.keterangan.value };

  if(o.nominal<=0){ toast('Isi nominal lebih dari 0', true); return; }

  await save('saveTrx', o, 'trx', $('#dlgTrx'), o.id ? 'Perubahan transaksi disimpan' : 'Transaksi dicatat');

}


/* ===================== Simpan / hapus ===================== */

async function save(fn, obj, key, dlg, msg){

  const btn = dlg.querySelector('[type=submit]'); btn.disabled = true; loading(true);

  try{

    S[key] = await api(fn, obj);

    dlg.close(); refreshYears(); renderAll(); toast(msg);

  }catch(e){ toast(e.message, true); }

  finally{ btn.disabled = false; loading(false); }

}

async function removeItem(fn, id, key){

  if(!confirm('Hapus data ini? Tindakan ini tidak dapat dibatalkan.')) return;

  loading(true);

  try{ S[key] = await api(fn, id); refreshYears(); renderAll(); toast('Data dihapus'); }

  catch(e){ toast(e.message, true); }

  finally{ loading(false); }

}

function exportRekap(){

  const y = period().y, trx = S.trx.filter(t=>t.tanggal.startsWith(y));

  const rows = [['Kategori','Jenis Transaksi',...BLN,'Total']], tot = {};

  S.ref.kategoriTrx.forEach(kat => {

    const extra = [...new Set(trx.filter(t=>t.kategori===kat && !S.ref.jenisTrx[kat].includes(t.jenis)).map(t=>t.jenis))];

    const t13 = Array(13).fill(0);

    S.ref.jenisTrx[kat].concat(extra).forEach(j => {

      const r = Array(12).fill(0);

      trx.filter(t=>t.kategori===kat && t.jenis===j).forEach(t => r[+t.tanggal.slice(5,7)-1] += t.nominal);

      const s = sum(r); r.forEach((v,i)=>t13[i]+=v); t13[12]+=s;

      rows.push([kat, j, ...r, s]);

    });

    rows.push(['Total ' + kat, '', ...t13]); tot[kat] = t13;

  });

  rows.push(['Surplus / Defisit', '', ...tot.Pemasukan.map((v,i)=>v-tot.Pengeluaran[i])]);

  const csv = '\ufeff' + rows.map(r => r.map(c => typeof c==='number' ? c : `"${String(c).replace(/"/g,'""')}"`).join(';')).join('\r\n');

  const a = document.createElement('a');

  a.href = URL.createObjectURL(new Blob([csv], { type:'text/csv;charset=utf-8' }));

  a.download = `Rekap_HartaKita_${S.user.username}_${y}.csv`;

  document.body.appendChild(a); a.click(); a.remove();

  setTimeout(()=>URL.revokeObjectURL(a.href), 2000);

  toast('Rekap ' + y + ' diunduh');

}


/* ===================== Autentikasi ===================== */

function showAuth(view){

  $('.app').hidden = true; $('#auth').hidden = false;

  $$('.auth-form').forEach(f => { f.hidden = f.dataset.view !== view; f.querySelector('.form-err').hidden = true; });

  $$('.sentTo').forEach(el => el.textContent = S.pendingEmail);

  const first = document.querySelector(`.auth-form[data-view="${view}"] input`);

  if(first) setTimeout(()=>first.focus(), 30);

}

function formMsg(form, msg, ok){

  const box = form.querySelector('.form-err');

  box.textContent = msg; box.className = ok ? 'form-ok' : 'form-err'; box.hidden = !msg;

}

async function authSubmit(form, task){

  const btn = form.querySelector('[type=submit]');

  btn.disabled = true; formMsg(form, ''); loading(true);

  try{ await task(); }

  catch(e){ formMsg(form, String(e.message || e).replace(/^Error:\s*/,'')); }

  finally{ btn.disabled = false; loading(false); }

}

function startSession(res){

  saveToken(res.token);

  $$('.auth-form').forEach(f => f.reset());

  load();

}

function endSession(){

  saveToken(null);

  Object.assign(S, { harta:[], trx:[], hutang:[], bayar:[], user:null }); S.openPay.clear();

  Object.values(S.charts).forEach(c => c.destroy()); S.charts = {};

  ['#hSearch','#tSearch','#dSearch'].forEach(s => { const el=$(s); if(el) el.value=''; });

  goPage('dashboard');

  showAuth('login');

}

async function doLogout(){

  const t = S.token;

  endSession();

  try{ await call('logout', t); }catch(e){}

  toast('Anda telah keluar');

}

let resendTimer = null;

function cooldownResend(sec){

  clearInterval(resendTimer);

  const btns = $$('[data-resend]');

  const tick = () => {

    btns.forEach(b => { b.disabled = sec > 0; b.textContent = sec > 0 ? `Kirim ulang (${sec} dtk)` : 'Kirim ulang kode'; });

    if(sec-- <= 0) clearInterval(resendTimer);

  };

  tick(); resendTimer = setInterval(tick, 1000);

}


function initAuth(){

  $$('[data-go]').forEach(b => b.onclick = () => showAuth(b.dataset.go));

  $$('[data-toggle-pw]').forEach(b => b.onclick = () => {

    const i = b.previousElementSibling; const show = i.type === 'password';

    i.type = show ? 'text' : 'password'; b.textContent = show ? 'Sembunyikan' : 'Lihat';

  });

  $$('.otp').forEach(i => i.addEventListener('input', () => { i.value = i.value.replace(/\D/g,'').slice(0,6); }));

  $('#rUser').addEventListener('input', e => { e.target.value = e.target.value.toLowerCase().replace(/[^a-z0-9_]/g,''); });


  $('#fLogin').onsubmit = e => { e.preventDefault(); const f = e.target;

    authSubmit(f, async () => {

      if(!f.ident.value.trim() || !f.pw.value) throw new Error('Isi username/email dan password.');

      try{ startSession(await call('login', f.ident.value.trim(), f.pw.value)); }

      catch(err){

        const m = String(err.message);

        if(m.includes('BELUM_VERIFIKASI:')){

          S.pendingEmail = m.split('BELUM_VERIFIKASI:')[1].trim(); S.codePurpose = 'VERIFIKASI';

          showAuth('verify');

          formMsg($('#fVerify'), 'Akun belum diverifikasi. Masukkan kode dari email pendaftaran, atau kirim ulang kode.', true);

          return;

        }

        throw err;

      }

    });

  };


  $('#fRegister').onsubmit = e => { e.preventDefault(); const f = e.target;

    authSubmit(f, async () => {

      if(f.pw.value !== f.pw2.value) throw new Error('Ulangi password tidak sama.');

      const r = await call('register', { nama:f.nama.value, username:f.username.value, email:f.email.value, password:f.pw.value });

      S.pendingEmail = r.email; S.codePurpose = 'VERIFIKASI';

      showAuth('verify'); cooldownResend(60);

    });

  };


  $('#fVerify').onsubmit = e => { e.preventDefault(); const f = e.target;

    authSubmit(f, async () => {

      if(f.code.value.length !== 6) throw new Error('Kode terdiri dari 6 digit.');

      startSession(await call('verifyEmail', S.pendingEmail, f.code.value));

      toast('Email terverifikasi. Selamat datang di HartaKita!');

    });

  };


  $('#fForgot').onsubmit = e => { e.preventDefault(); const f = e.target;

    authSubmit(f, async () => {

      const email = f.email.value.trim().toLowerCase();

      if(!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(email)) throw new Error('Format email tidak valid.');

      const msg = await call('forgotPassword', email);

      S.pendingEmail = email; S.codePurpose = 'RESET';

      showAuth('reset'); formMsg($('#fReset'), msg, true); cooldownResend(60);

    });

  };


  $('#fReset').onsubmit = e => { e.preventDefault(); const f = e.target;

    authSubmit(f, async () => {

      if(f.code.value.length !== 6) throw new Error('Kode terdiri dari 6 digit.');

      if(f.pw.value !== f.pw2.value) throw new Error('Ulangi password tidak sama.');

      startSession(await call('resetPassword', S.pendingEmail, f.code.value, f.pw.value));

      toast('Password diperbarui. Anda sudah masuk.');

    });

  };


  $$('[data-resend]').forEach(b => b.onclick = async () => {

    const form = b.closest('form');

    try{ formMsg(form, await call('resendCode', S.pendingEmail, S.codePurpose), true); cooldownResend(60); }

    catch(e){ formMsg(form, String(e.message).replace(/^Error:\s*/,'')); }

  });

}


initAuth();

S.token = readToken();

if(S.token) load(); else showAuth('login');

</script>

</body>

</html>