This commit is contained in:
DIGIDEV\corer committed 2026-10-20 13:16:38 +09:00
commit 3e4a056b14
67 files changed
+13926

No files matched your search

+43
View File
@@ -0,0 +1,43 @@
<?php
defined('SERUBI_ADMIN') || exit;
// Expects: $prefix (input name prefix), $rows (list of ['max' => str, 'rate' => str]), $tierId.
$last = count($rows) - 1;
?>
<div class="tier-editor" data-tiers="<?= e($tierId) ?>" data-prefix="<?= e($prefix) ?>">
<table class="tier-table">
<thead>
<tr>
<th scope="col">Nominal</th>
<th scope="col">Rate p.a.</th>
<th scope="col"><span class="visually-hidden">Hapus</span></th>
</tr>
</thead>
<tbody>
<?php foreach ($rows as $i => $r): ?>
<tr class="tier-row">
<td data-label="Nominal">
<div class="tier-range">
<span class="tier-from"></span>
<div class="input-affix is-idr tier-max-wrap">
<span>Rp</span>
<input name="<?= e($prefix) ?>[<?= $i ?>][max]" class="t-max" value="<?= e($i === $last ? '' : ($r['max'] ?? '')) ?>"
inputmode="numeric" autocomplete="off" aria-label="Sampai dengan nominal, baris <?= $i + 1 ?>">
</div>
</div>
</td>
<td data-label="Rate p.a.">
<div class="input-affix tier-rate">
<input name="<?= e($prefix) ?>[<?= $i ?>][rate]" class="t-rate" value="<?= e($r['rate'] ?? '') ?>"
inputmode="decimal" autocomplete="off" aria-label="Rate persen per tahun, baris <?= $i + 1 ?>">
<span>%</span>
</div>
</td>
<td class="tier-del-cell">
<button type="button" class="icon-btn tier-del" aria-label="Hapus baris <?= $i + 1 ?>"><?= icon('trash') ?></button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<button type="button" class="btn btn-ghost tier-add"><?= icon('plus') ?><span>Tambah tingkat</span></button>
</div>
+52
View File
@@ -0,0 +1,52 @@
<?php
defined('SERUBI_ADMIN') || exit;
if ($draft !== null) {
$cbRows = array_values($draft['cb'] ?? []);
$cbTenors = $draft['cbtenor'] ?? [];
} else {
$cbRows = array_map(fn($t) => ['min' => fmt_idr($t['min']), 'share' => fmt_pct($t['nasabah'])], $params['cashbackTiers']);
$cbTenors = $params['cashbackTenors'];
}
?>
<form method="post" id="paramForm" data-kind="cashback" novalidate>
<?= csrf_field() ?>
<input type="hidden" name="action" value="save_cashback">
<section class="panel">
<header class="panel-head"><h2>Jangka waktu program</h2><p>Hanya jangka waktu yang dicentang yang mendapat cashback.</p></header>
<div class="check-row">
<?php foreach ($params['tenors'] as $t): ?>
<label class="check">
<input type="checkbox" name="cbtenor[<?= (int)$t ?>]" value="1" data-cbtenor="<?= (int)$t ?>" <?= in_array($t, $cbTenors, true) ? 'checked' : '' ?>>
<span><?= (int)$t ?> bulan</span>
</label>
<?php endforeach; ?>
</div>
</section>
<section class="panel">
<header class="panel-head"><h2>Porsi spread untuk nasabah</h2><p>Tingkat dengan nominal minimal tertinggi yang tercapai yang dipakai. Porsi bank = 100% − porsi nasabah. Nominal di bawah tingkat terendah tidak mendapat cashback.</p></header>
<?= field_error($errors, 'cb') ?>
<table class="tier-table cb-table" id="cbTable">
<thead><tr><th scope="col">Nominal minimal</th><th scope="col">Porsi nasabah</th><th scope="col">Porsi bank</th><th scope="col"><span class="visually-hidden">Hapus</span></th></tr></thead>
<tbody>
<?php foreach ($cbRows as $i => $r): ?>
<tr class="cb-row">
<td data-label="Nominal minimal">
<div class="input-affix is-idr"><span>Rp</span><input name="cb[<?= $i ?>][min]" class="cb-min" value="<?= e($r['min'] ?? '') ?>" inputmode="numeric" autocomplete="off" aria-label="Nominal minimal, baris <?= $i + 1 ?>"></div>
</td>
<td data-label="Porsi nasabah">
<div class="input-affix tier-rate"><input name="cb[<?= $i ?>][share]" class="cb-share" value="<?= e($r['share'] ?? '') ?>" inputmode="decimal" autocomplete="off" aria-label="Porsi nasabah persen, baris <?= $i + 1 ?>"><span>%</span></div>
</td>
<td data-label="Porsi bank" class="cb-bank">—</td>
<td class="tier-del-cell"><button type="button" class="icon-btn cb-del" aria-label="Hapus baris <?= $i + 1 ?>"><?= icon('trash') ?></button></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<button type="button" class="btn btn-ghost cb-add"><?= icon('plus') ?><span>Tambah tingkat</span></button>
</section>
<div class="form-actions">
<button class="btn btn-primary" type="submit"><?= icon('save') ?><span>Simpan porsi cashback</span></button>
</div>
</form>
+65
View File
@@ -0,0 +1,65 @@
<?php
defined('SERUBI_ADMIN') || exit;
$tenors = $params['tenors'];
if ($draft !== null) {
$bands = array_values($draft['bands'] ?? []);
$rates = array_values($draft['rates'] ?? []);
} else {
$bands = array_map('fmt_idr', $params['depositoBands']);
$rates = [];
foreach ($params['depositoBands'] as $i => $_) {
foreach ($tenors as $t) {
$rates[$i][$t] = fmt_pct($params['depositoRates'][$t][$i]['rate'] ?? 0);
}
}
}
$last = count($bands) - 1;
?>
<form method="post" id="paramForm" data-kind="deposito" novalidate>
<?= csrf_field() ?>
<input type="hidden" name="action" value="save_deposito">
<p class="hint-block"><?= icon('info') ?><span>Nominal di bawah Rp<?= e(fmt_idr($params['settings']['deposito_min'])) ?> tidak mendapat bunga deposito (atur di Parameter bank). Rate dalam persen per tahun.</span></p>
<section class="panel">
<header class="panel-head"><h2>Tabel rate deposito</h2><p>Baris = kelompok nominal, kolom = jangka waktu.</p></header>
<?= field_error($errors, 'deposito') ?>
<div class="matrix-scroll" tabindex="0" role="region" aria-label="Tabel rate deposito, dapat digeser">
<table class="matrix" id="depoMatrix">
<thead>
<tr>
<th scope="col">Nominal</th>
<?php foreach ($tenors as $t): ?><th scope="col"><?= (int)$t ?> bln</th><?php endforeach; ?>
<th scope="col"><span class="visually-hidden">Hapus</span></th>
</tr>
</thead>
<tbody>
<?php foreach ($bands as $i => $max): ?>
<tr class="tier-row">
<th scope="row">
<div class="tier-range">
<span class="tier-from"></span>
<div class="input-affix is-idr tier-max-wrap">
<span>Rp</span>
<input name="bands[<?= $i ?>]" class="t-max" value="<?= e($i === $last ? '' : $max) ?>" inputmode="numeric" autocomplete="off" aria-label="Sampai dengan nominal, baris <?= $i + 1 ?>">
</div>
</div>
</th>
<?php foreach ($tenors as $t): ?>
<td data-label="<?= (int)$t ?> bulan">
<div class="input-affix tier-rate">
<input name="rates[<?= $i ?>][<?= (int)$t ?>]" data-tenor="<?= (int)$t ?>" class="t-rate" value="<?= e($rates[$i][$t] ?? '') ?>" inputmode="decimal" autocomplete="off" aria-label="Rate <?= (int)$t ?> bulan, baris <?= $i + 1 ?>">
<span>%</span>
</div>
</td>
<?php endforeach; ?>
<td class="tier-del-cell"><button type="button" class="icon-btn tier-del" aria-label="Hapus baris <?= $i + 1 ?>"><?= icon('trash') ?></button></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<button type="button" class="btn btn-ghost tier-add" data-target="depoMatrix"><?= icon('plus') ?><span>Tambah kelompok nominal</span></button>
</section>
<div class="form-actions">
<button class="btn btn-primary" type="submit"><?= icon('save') ?><span>Simpan rate deposito</span></button>
</div>
</form>
+57
View File
@@ -0,0 +1,57 @@
<?php
defined('SERUBI_ADMIN') || exit;
$rows = settings_rows();
$byGroup = [];
foreach ($rows as $key => $row) {
$byGroup[$row['grp']][$key] = $row;
}
$unitSuffix = ['pct' => '%', 'idr' => null, 'days' => 'hari'];
?>
<form method="post" class="param-form" id="paramForm" data-kind="settings" novalidate>
<?= csrf_field() ?>
<input type="hidden" name="action" value="save_settings">
<?php foreach (SERUBI_GROUPS as $grp => [$title, $desc]): if (empty($byGroup[$grp])) continue; ?>
<section class="panel">
<header class="panel-head"><h2><?= e($title) ?></h2><p><?= e($desc) ?></p></header>
<div class="setting-list">
<?php foreach ($byGroup[$grp] as $key => $row):
$val = $draft[$key] ?? fmt_setting($row);
$err = isset($errors["s.$key"]);
$id = 's-' . $key; ?>
<div class="setting<?= $err ? ' is-invalid' : '' ?>">
<div class="setting-text">
<label for="<?= e($id) ?>"><?= e($row['label']) ?></label>
<p id="<?= e($id) ?>-d"><?= e($row['description']) ?></p>
<?= field_error($errors, "s.$key") ?>
</div>
<div class="input-affix<?= $row['unit'] === 'idr' ? ' is-idr' : '' ?>">
<?php if ($row['unit'] === 'idr'): ?><span>Rp</span><?php endif; ?>
<input id="<?= e($id) ?>" name="s[<?= e($key) ?>]" value="<?= e($val) ?>" data-setting="<?= e($key) ?>" data-unit="<?= e($row['unit']) ?>"
inputmode="<?= $row['unit'] === 'idr' || $row['unit'] === 'days' ? 'numeric' : 'decimal' ?>" autocomplete="off"
aria-describedby="<?= e($id) ?>-d" <?= $err ? 'aria-invalid="true"' : '' ?>>
<?php if ($unitSuffix[$row['unit']] ?? null): ?><span><?= e($unitSuffix[$row['unit']]) ?></span><?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
</section>
<?php endforeach; ?>
<div class="form-actions">
<button class="btn btn-primary" type="submit"><?= icon('save') ?><span>Simpan parameter</span></button>
</div>
</form>
<details class="panel panel-danger"<?= isset($errors['reset']) ? ' open' : '' ?>>
<summary><h2>Kembalikan ke nilai file Excel</h2><?= icon('chevron', 'icon chev') ?></summary>
<form method="post" class="danger-body">
<?= csrf_field() ?>
<input type="hidden" name="action" value="reset_defaults">
<p>Semua parameter, rate produk, rate deposito, dan porsi cashback diganti dengan nilai dari <b>CALCULATOR CASHBACK SERUB KANTOR CABANG 22092026.xlsx</b>. Perubahan tercatat di riwayat.</p>
<div class="form-field">
<label for="confirmReset">Ketik <b>RESET</b> untuk konfirmasi</label>
<input id="confirmReset" name="confirm" autocomplete="off" autocapitalize="characters">
<?= field_error($errors, 'reset') ?>
</div>
<button class="btn btn-danger" type="submit"><?= icon('reset') ?><span>Kembalikan semua parameter</span></button>
</form>
</details>
+101
View File
@@ -0,0 +1,101 @@
<?php
defined('SERUBI_ADMIN') || exit;
$users = db()->query('SELECT id, username, full_name, role, is_active, otp_enabled, must_change_password, last_login_at, created_at FROM admins ORDER BY is_active DESC, username')->fetchAll();
$openId = (int)($errors['user_id'] ?? 0);
$super = is_super_admin($me);
// Every action on another account asks for the acting admin's own password.
$confirmField = function (string $id): string {
return '<div class="form-field"><label for="' . e($id) . '">Password Anda (konfirmasi)</label>'
. '<input id="' . e($id) . '" type="password" name="confirm_password" autocomplete="current-password" required></div>';
};
?>
<section class="panel">
<header class="panel-head">
<h2>Daftar admin</h2>
<p><?= $super
? 'Sebagai super admin, Anda dapat menambah, menonaktifkan, me-reset password atau OTP, dan mengubah peran admin lain.'
: 'Hanya super admin yang dapat mengelola akun admin lain.' ?></p>
</header>
<?= isset($errors['user']) && !$openId ? field_error($errors, 'user') : '' ?>
<ul class="user-list">
<?php foreach ($users as $u): $self = (int)$u['id'] === $me['id']; $uid = (int)$u['id']; ?>
<li class="user<?= (int)$u['is_active'] ? '' : ' is-off' ?>">
<div class="user-main">
<span class="user-name"><?= e($u['full_name']) ?><?= $self ? ' <span class="badge">Anda</span>' : '' ?>
<span class="badge"><?= $u['role'] === 'super_admin' ? 'Super admin' : 'Admin' ?></span>
<?= (int)$u['otp_enabled'] ? '' : ' <span class="badge badge-off">OTP belum aktif</span>' ?>
<?= (int)$u['must_change_password'] ? ' <span class="badge badge-off">Wajib ganti password</span>' : '' ?>
<?= (int)$u['is_active'] ? '' : ' <span class="badge badge-off">Nonaktif</span>' ?></span>
<span class="user-meta">@<?= e($u['username']) ?> · login terakhir <?= e(fmt_datetime($u['last_login_at'])) ?></span>
<?= $openId === $uid ? field_error($errors, 'user') : '' ?>
</div>
<?php if ($super && !$self): ?>
<div class="user-actions">
<details class="inline-details"<?= $openId === $uid ? ' open' : '' ?>>
<summary class="btn btn-ghost btn-sm"><?= icon('key') ?><span>Kelola</span></summary>
<form method="post" class="inline-form">
<?= csrf_field() ?>
<input type="hidden" name="action" value="user_password">
<input type="hidden" name="id" value="<?= $uid ?>">
<strong>Reset password</strong>
<div class="form-field"><label for="pw<?= $uid ?>">Password sementara</label><input id="pw<?= $uid ?>" type="password" name="password" autocomplete="new-password" required></div>
<div class="form-field"><label for="pc<?= $uid ?>">Ulangi password</label><input id="pc<?= $uid ?>" type="password" name="password_confirm" autocomplete="new-password" required></div>
<?= $confirmField('pa' . $uid) ?>
<p class="hint">Pemilik akun wajib mengganti password ini saat login berikutnya.</p>
<button class="btn btn-primary btn-sm" type="submit">Reset password</button>
</form>
<form method="post" class="inline-form">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= $uid ?>">
<strong>Akun</strong>
<?= $confirmField('pb' . $uid) ?>
<div class="inline-buttons">
<button class="btn btn-sm <?= (int)$u['is_active'] ? 'btn-ghost' : 'btn-primary' ?>" type="submit" name="action" value="user_toggle"><?= (int)$u['is_active'] ? 'Nonaktifkan' : 'Aktifkan' ?></button>
<button class="btn btn-ghost btn-sm" type="submit" name="action" value="user_role"><?= $u['role'] === 'super_admin' ? 'Jadikan admin' : 'Jadikan super admin' ?></button>
<?php if ((int)$u['otp_enabled']): ?>
<button class="btn btn-ghost btn-sm" type="submit" name="action" value="user_reset_otp">Reset OTP</button>
<?php endif; ?>
</div>
<p class="hint">Reset OTP bila ponsel hilang: authenticator baru dipasang saat login berikutnya.</p>
</form>
</details>
</div>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
</section>
<div class="two-col">
<?php if ($super): ?>
<section class="panel">
<header class="panel-head"><h2>Tambah admin</h2></header>
<form method="post" class="stack-form" novalidate>
<?= csrf_field() ?>
<input type="hidden" name="action" value="user_create">
<?= field_error($errors, 'user_new') ?>
<div class="form-field"><label for="nu">Username</label><input id="nu" name="username" autocapitalize="none" spellcheck="false" autocomplete="off" value="<?= e($draft['username'] ?? '') ?>" required><p class="hint">Huruf kecil, angka, titik, minus, garis bawah.</p></div>
<div class="form-field"><label for="nn">Nama lengkap</label><input id="nn" name="full_name" autocomplete="off" value="<?= e($draft['full_name'] ?? '') ?>" required></div>
<div class="form-field"><label for="nr">Peran</label>
<select id="nr" name="role"><option value="admin">Admin (ubah parameter)</option><option value="super_admin">Super admin (juga kelola akun)</option></select></div>
<div class="form-field"><label for="np">Password awal</label><input id="np" type="password" name="password" autocomplete="new-password" required><p class="hint">Minimal 10 karakter, berisi huruf, angka, dan simbol. Wajib diganti saat login pertama.</p></div>
<div class="form-field"><label for="nc">Ulangi password</label><input id="nc" type="password" name="password_confirm" autocomplete="new-password" required></div>
<?= $confirmField('nx') ?>
<button class="btn btn-primary" type="submit"><?= icon('plus') ?><span>Tambah admin</span></button>
</form>
</section>
<?php endif; ?>
<section class="panel">
<header class="panel-head"><h2>Password Anda</h2></header>
<form method="post" class="stack-form" novalidate>
<?= csrf_field() ?>
<input type="hidden" name="action" value="own_password">
<?= field_error($errors, 'own') ?>
<div class="form-field"><label for="oc">Password saat ini</label><input id="oc" type="password" name="current" autocomplete="current-password" required></div>
<div class="form-field"><label for="op">Password baru</label><input id="op" type="password" name="password" autocomplete="new-password" required></div>
<div class="form-field"><label for="oq">Ulangi password baru</label><input id="oq" type="password" name="password_confirm" autocomplete="new-password" required></div>
<button class="btn btn-primary" type="submit"><?= icon('key') ?><span>Ganti password</span></button>
</form>
</section>
</div>
+27
View File
@@ -0,0 +1,27 @@
<?php
defined('SERUBI_ADMIN') || exit;
// Rows as display strings: from the failed POST, or from the database.
$tierRows = [];
foreach (SERUBI_PRODUCTS as $key => $label) {
if ($draft !== null) {
$tierRows[$key] = array_values((array)($draft[$key] ?? []));
} else {
$tierRows[$key] = array_map(fn($t) => ['max' => fmt_idr($t['max']), 'rate' => fmt_pct($t['rate'])], $params['productRates'][$key]);
}
}
?>
<form method="post" id="paramForm" data-kind="products" novalidate>
<?= csrf_field() ?>
<input type="hidden" name="action" value="save_products">
<p class="hint-block"><?= icon('info') ?><span>Tingkat dibaca dari atas: nominal sampai dengan batas pertama memakai rate pertama, dan seterusnya. Baris terakhir berlaku untuk semua nominal di atasnya.</span></p>
<?php foreach (SERUBI_PRODUCTS as $key => $label): ?>
<section class="panel">
<header class="panel-head"><h2><?= e($label) ?></h2></header>
<?= field_error($errors, "tiers.$key") ?>
<?php $prefix = "tiers[$key]"; $rows = $tierRows[$key]; $tierId = $key; require __DIR__ . '/_tiers.php'; ?>
</section>
<?php endforeach; ?>
<div class="form-actions">
<button class="btn btn-primary" type="submit"><?= icon('save') ?><span>Simpan rate produk</span></button>
</div>
</form>
+69
View File
@@ -0,0 +1,69 @@
<?php
defined('SERUBI_ADMIN') || exit;
$perPage = 30;
$page = max(1, (int)($_GET['p'] ?? 1));
$total = (int)db()->query('SELECT COUNT(*) FROM audit_log')->fetchColumn();
$pages = max(1, (int)ceil($total / $perPage));
$page = min($page, $pages);
$st = db()->prepare('SELECT * FROM audit_log ORDER BY id DESC LIMIT ? OFFSET ?');
$st->bindValue(1, $perPage, PDO::PARAM_INT);
$st->bindValue(2, ($page - 1) * $perPage, PDO::PARAM_INT);
$st->execute();
$logs = $st->fetchAll();
$fmtVal = function ($v, string $unit): string {
return $unit === 'pct' ? fmt_pct((float)$v) . '%' : ($unit === 'idr' ? 'Rp' . fmt_idr((float)$v) : (string)$v);
};
/** One line per tier: "≤ Rp100.000.000: 0,75%". */
$fmtTiers = function (array $tiers): string {
return implode(' · ', array_map(fn($t) => ($t['max'] === null ? 'di atas' : '≤ Rp' . fmt_idr((float)$t['max'])) . ': ' . fmt_pct((float)$t['rate']) . '%', $tiers));
};
?>
<section class="panel">
<?php if (!$logs): ?>
<p class="empty">Belum ada aktivitas. Setiap perubahan parameter, login, dan pengelolaan admin akan tercatat di sini.</p>
<?php else: ?>
<ol class="log-list">
<?php foreach ($logs as $log): $ch = $log['changes'] ? json_decode($log['changes'], true) : null; ?>
<li class="log">
<div class="log-head">
<span class="log-what"><?= e($log['summary']) ?></span>
<span class="log-meta"><?= e(fmt_datetime($log['created_at'])) ?> · <?= $log['username'] ? '@' . e($log['username']) : 'sistem' ?></span>
</div>
<?php if ($ch && $log['action'] === 'param.settings'): ?>
<ul class="log-diff">
<?php foreach ($ch as $d): ?>
<li><?= e($d['label']) ?>: <del><?= e($fmtVal($d['old'], $d['unit'])) ?></del> → <ins><?= e($fmtVal($d['new'], $d['unit'])) ?></ins></li>
<?php endforeach; ?>
</ul>
<?php elseif ($ch && $log['action'] === 'param.produk'): ?>
<ul class="log-diff">
<?php foreach ($ch as $prod => $d): ?>
<li><b><?= e(SERUBI_PRODUCTS[$prod] ?? $prod) ?></b><br><del><?= e($fmtTiers($d['old'])) ?></del><br><ins><?= e($fmtTiers($d['new'])) ?></ins></li>
<?php endforeach; ?>
</ul>
<?php elseif ($ch && $log['action'] === 'param.deposito'): ?>
<ul class="log-diff">
<?php foreach ($ch['new'] as $tenor => $tiers): if (json_encode($tiers) === json_encode($ch['old'][$tenor] ?? null)) continue; ?>
<li><b><?= (int)$tenor ?> bulan</b><br><del><?= e($fmtTiers($ch['old'][$tenor] ?? [])) ?></del><br><ins><?= e($fmtTiers($tiers)) ?></ins></li>
<?php endforeach; ?>
</ul>
<?php elseif ($ch && $log['action'] === 'param.cashback'): ?>
<ul class="log-diff">
<?php foreach (['old' => 'del', 'new' => 'ins'] as $k => $tag): ?>
<li><<?= $tag ?>>Tenor <?= e(implode(', ', $ch[$k]['tenors'])) ?> bln · <?= e(implode(' · ', array_map(fn($t) => '≥ Rp' . fmt_idr((float)$t['min']) . ': ' . fmt_pct((float)$t['nasabah']) . '%', $ch[$k]['tiers']))) ?></<?= $tag ?>></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ol>
<?php if ($pages > 1): ?>
<nav class="pager" aria-label="Halaman riwayat">
<?php if ($page > 1): ?><a class="btn btn-ghost btn-sm" href="?tab=riwayat&p=<?= $page - 1 ?>">Lebih baru</a><?php endif; ?>
<span>Halaman <?= $page ?> dari <?= $pages ?></span>
<?php if ($page < $pages): ?><a class="btn btn-ghost btn-sm" href="?tab=riwayat&p=<?= $page + 1 ?>">Lebih lama</a><?php endif; ?>
</nav>
<?php endif; ?>
<?php endif; ?>
</section>