# Redesain Modal Setting Alokasi Soal (card per Jenis Soal)
Hanya 2 hal yang diganti: **CSS** di `create-agenda.html` dan fungsi `_generateSmartAlokasi` di `distribusi-soal.js`. Fungsi `_recalculateAlokasi`, `_refreshAlokasiVisuals`, `_saveAlokasi`, dst. tidak perlu diubah.
## Tampilan
- Ringkasan di atas dalam bentuk chip: target soal per peserta, jumlah jenis soal, dan jumlah kategori.
- Satu card untuk tiap jenis soal. Header card berisi nomor, nama jenis, info tersedia, input persen dari total, dan jumlah soal.
- Kategori berada di dalam card jenisnya, diberi garis aksen kiri dan label kolom "% dari jenis".
- Input yang jumlahnya melebihi soal tersedia ditandai merah (logika ini sudah ada di `_refreshAlokasiVisuals`).
- Total persentase menempel di bagian bawah modal, jadi selalu terlihat saat scroll.
---
## 1. create-agenda.html (blok `<style>`)
**HAPUS** semua rule lama dari `#modalAlokasi .alokasi-intro` sampai `#modalAlokasi .alokasi-total-bar` (termasuk `.alokasi-col-labels`, `.alokasi-row`, `.alokasi-kategori-block`, `.alokasi-bar`, input, `.alokasi-persen-wrap`, `.alokasi-jumlah-wrap`).
**UBAH** `max-width: 640px` menjadi `780px` pada `#modalAlokasi .modal-content`.
**TAMBAH** (letakkan setelah rule `#modalAlokasi .modal-footer`):
```css
#modalAlokasi .modal-body {
padding: 20px 24px;
max-height: 64vh;
overflow-y: auto;
background: var(--bg-soft);
}
/* ---------- Ringkasan ---------- */
#modalAlokasi .alokasi-summary {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 10px;
}
#modalAlokasi .alokasi-chip {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border-radius: 999px;
background: var(--surface);
border: 1px solid var(--border);
font-size: 12px;
color: var(--ink-600);
}
#modalAlokasi .alokasi-chip b { color: var(--ink-800); }
#modalAlokasi .alokasi-hint {
font-size: 12px;
line-height: 1.5;
color: var(--ink-500);
margin: 0 0 16px;
}
/* ---------- Card Jenis Soal ---------- */
#modalAlokasi .alokasi-group {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 14px;
margin-bottom: 14px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,.04);
transition: box-shadow .2s ease, border-color .2s ease;
}
#modalAlokasi .alokasi-group:hover {
border-color: var(--brand);
box-shadow: 0 8px 20px rgba(0,0,0,.08);
}
#modalAlokasi .alokasi-jenis-row {
display: grid;
grid-template-columns: minmax(0, 1fr) 120px 90px;
gap: 10px 12px;
align-items: end;
padding: 14px 16px;
background: linear-gradient(135deg, rgba(130,36,51,.09) 0%, rgba(130,36,51,.02) 100%);
border-bottom: 1px solid var(--border);
}
#modalAlokasi .alokasi-jenis-info {
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
}
#modalAlokasi .alokasi-jenis-badge {
flex: 0 0 auto;
width: 32px;
height: 32px;
border-radius: 10px;
background: var(--brand);
color: #fff;
font-weight: 700;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 10px rgba(130,36,51,.3);
}
#modalAlokasi .alokasi-jenis-title {
font-weight: 700;
font-size: 14px;
color: var(--ink-800);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#modalAlokasi .alokasi-meta {
font-size: 11px;
color: var(--ink-400);
margin-top: 2px;
}
#modalAlokasi .alokasi-field {
display: flex;
flex-direction: column;
gap: 4px;
}
#modalAlokasi .alokasi-field-label {
font-size: 10px;
text-transform: uppercase;
letter-spacing: .05em;
font-weight: 700;
color: var(--ink-500);
text-align: center;
}
/* ---------- Input ---------- */
#modalAlokasi .alokasi-group input[type="number"] {
width: 100%;
box-sizing: border-box;
text-align: center;
border: 1px solid var(--border);
border-radius: 8px;
padding: 9px 8px;
font-size: 13px;
font-weight: 600;
background: var(--surface);
transition: border-color .2s, box-shadow .2s;
-moz-appearance: textfield;
}
#modalAlokasi .alokasi-group input[type="number"]::-webkit-outer-spin-button,
#modalAlokasi .alokasi-group input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
#modalAlokasi .alokasi-group input[type="number"]:focus {
outline: none;
border-color: var(--brand);
box-shadow: 0 0 0 3px rgba(130,36,51,.12);
}
#modalAlokasi .alokasi-group input[disabled] {
background: var(--bg-soft);
color: var(--ink-600);
cursor: not-allowed;
}
#modalAlokasi .alokasi-persen-wrap { position: relative; }
#modalAlokasi .alokasi-persen-wrap input { padding-right: 24px; }
#modalAlokasi .alokasi-persen-wrap .suffix {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
font-size: 11px;
color: var(--ink-400);
pointer-events: none;
}
/* ---------- Progress bar ---------- */
#modalAlokasi .alokasi-bar {
grid-column: 1 / -1;
height: 6px;
border-radius: 4px;
background: var(--border);
overflow: hidden;
}
#modalAlokasi .alokasi-bar-fill {
height: 100%;
border-radius: 4px;
background: var(--brand);
transition: width .3s ease;
}
/* ---------- Daftar Kategori (di dalam card jenis) ---------- */
#modalAlokasi .alokasi-kat-list {
padding: 12px 16px 6px 28px;
}
#modalAlokasi .alokasi-kat-head {
display: grid;
grid-template-columns: minmax(0, 1fr) 120px 90px;
gap: 12px;
padding: 0 12px 6px;
font-size: 10px;
text-transform: uppercase;
letter-spacing: .05em;
font-weight: 700;
color: var(--ink-500);
}
#modalAlokasi .alokasi-kat-head span:not(:first-child) { text-align: center; }
#modalAlokasi .alokasi-kat-row {
display: grid;
grid-template-columns: minmax(0, 1fr) 120px 90px;
gap: 8px 12px;
align-items: center;
padding: 10px 12px;
margin-bottom: 8px;
background: var(--bg-soft);
border: 1px solid var(--border);
border-left: 3px solid var(--brand);
border-radius: 10px;
transition: border-color .2s, background-color .2s;
}
#modalAlokasi .alokasi-kat-row:hover {
border-color: var(--brand);
background: var(--surface);
}
#modalAlokasi .alokasi-kat-name {
font-weight: 600;
font-size: 13px;
color: var(--ink-800);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#modalAlokasi .alokasi-kat-row .alokasi-bar { height: 4px; }
/* ---------- Total (menempel di bawah) ---------- */
#modalAlokasi .alokasi-total-bar {
position: sticky;
bottom: 0;
margin-top: 6px;
padding: 12px 16px;
border-radius: 12px;
background: var(--surface);
border: 1px dashed var(--ink-300);
box-shadow: 0 -6px 14px rgba(0,0,0,.05);
display: flex;
justify-content: space-between;
align-items: center;
font-size: 13px;
font-weight: 600;
}
@media (max-width: 640px) {
#modalAlokasi .alokasi-jenis-row,
#modalAlokasi .alokasi-kat-head,
#modalAlokasi .alokasi-kat-row {
grid-template-columns: minmax(0, 1fr) 90px 70px;
}
#modalAlokasi .alokasi-kat-list { padding-left: 16px; }
}
```
---
## 2. distribusi-soal.js
**GANTI** `_generateSmartAlokasi` di `CreateAgendaApp` (logika hitung sama seperti sebelumnya, yang berubah hanya markup HTML):
```js
_generateSmartAlokasi: function(jumlahSoalPeserta) {
var container = document.getElementById("alokasiKategoriContainer");
container.innerHTML = "";
var groups = this._getAlokasiGroups();
if (groups.length === 0) { container.innerHTML = '<p style="color:var(--danger); text-align:center;">Tidak ada kategori.</p>'; return; }
var existing = this.state.alokasiData || [];
var restored = existing.length > 0;
var counts = groups.map(function(g) {
return g.kategori.map(function(k) {
var found = existing.filter(function(a) {
return String(a.JenisSoalID) === String(g.jenisId) && a.KategoriSoal === k.nama;
})[0];
if (!found) restored = false;
return found ? found.JumlahSoalDialokasikan : 0;
});
});
if (!restored) {
var jenisCaps = groups.map(function(g) { return g.kategori.reduce(function(s, k) { return s + k.available; }, 0); });
var jenisCounts = dsDistributeEqual(jumlahSoalPeserta, jenisCaps);
counts = groups.map(function(g, gi) {
return dsDistributeEqual(jenisCounts[gi], g.kategori.map(function(k) { return k.available; }));
});
}
var fmt = function(n) { return +(n).toFixed(2); };
var totalKategori = groups.reduce(function(s, g) { return s + g.kategori.length; }, 0);
var html =
'<div class="alokasi-summary">' +
'<span class="alokasi-chip">Target per peserta: <b>' + jumlahSoalPeserta + ' soal</b></span>' +
'<span class="alokasi-chip">Jenis soal: <b>' + groups.length + '</b></span>' +
'<span class="alokasi-chip">Kategori: <b>' + totalKategori + '</b></span>' +
'</div>' +
'<p class="alokasi-hint">Total 100% dibagi ke tiap Jenis Soal, lalu setiap Jenis Soal dibagi ke Kategori di dalamnya. ' +
'Persentase kategori dihitung dari jenis soalnya.</p>';
groups.forEach(function(g, gi) {
var jenisCount = counts[gi].reduce(function(a, b) { return a + b; }, 0);
var jenisAvail = g.kategori.reduce(function(s, k) { return s + k.available; }, 0);
var jenisPct = jumlahSoalPeserta > 0 ? (jenisCount / jumlahSoalPeserta) * 100 : 0;
var jenisDisabled = groups.length === 1 ? " disabled" : "";
html += '<div class="alokasi-group" data-jenis="' + (g.jenisId === null ? "" : dsEscapeHtml(g.jenisId)) + '" data-nama="' + dsEscapeHtml(g.nama) + '">' +
'<div class="alokasi-jenis-row">' +
'<div class="alokasi-jenis-info">' +
'<span class="alokasi-jenis-badge">' + (gi + 1) + '</span>' +
'<div style="min-width:0;">' +
'<div class="alokasi-jenis-title" title="' + dsEscapeHtml(g.nama) + '">' + dsEscapeHtml(g.nama) + '</div>' +
'<div class="alokasi-meta">Tersedia ' + jenisAvail + ' soal • ' + g.kategori.length + ' kategori</div>' +
'</div>' +
'</div>' +
'<div class="alokasi-field">' +
'<span class="alokasi-field-label">% dari total</span>' +
'<div class="alokasi-persen-wrap"><input type="number" class="alokasi-jenis-persen" data-available="' + jenisAvail + '" value="' + fmt(jenisPct) + '" min="0" max="100" step="0.01"' + jenisDisabled + '><span class="suffix">%</span></div>' +
'</div>' +
'<div class="alokasi-field">' +
'<span class="alokasi-field-label">Jumlah</span>' +
'<div class="alokasi-jumlah-wrap"><input type="number" class="alokasi-jumlah" value="' + jenisCount + '" disabled></div>' +
'</div>' +
'<div class="alokasi-bar"><div class="alokasi-bar-fill" style="width:' + fmt(jenisPct) + '%"></div></div>' +
'</div>' +
'<div class="alokasi-kat-list">' +
'<div class="alokasi-kat-head"><span>Kategori</span><span>% dari jenis</span><span>Jumlah</span></div>';
g.kategori.forEach(function(k, ki) {
var c = counts[gi][ki];
var katPct = jenisCount > 0 ? (c / jenisCount) * 100 : 0;
var katDisabled = g.kategori.length === 1 ? " disabled" : "";
html += '<div class="alokasi-kat-row">' +
'<div style="min-width:0;">' +
'<div class="alokasi-kat-name" title="' + dsEscapeHtml(k.nama) + '">' + dsEscapeHtml(k.nama) + '</div>' +
'<div class="alokasi-meta">Tersedia: ' + k.available + ' soal</div>' +
'</div>' +
'<div class="alokasi-persen-wrap"><input type="number" class="alokasi-kat-persen" data-kategori="' + dsEscapeHtml(k.nama) + '" data-available="' + k.available + '" value="' + fmt(katPct) + '" min="0" max="100" step="0.01"' + katDisabled + '><span class="suffix">%</span></div>' +
'<div class="alokasi-jumlah-wrap"><input type="number" class="alokasi-jumlah" value="' + c + '" disabled></div>' +
'<div class="alokasi-bar"><div class="alokasi-bar-fill" style="width:' + fmt(katPct) + '%"></div></div>' +
'</div>';
});
html += '</div></div>';
});
html += '<div class="alokasi-total-bar"><span>Total Persentase Jenis Soal</span><span id="alokasiTotalPersenDisplay">100%</span></div>';
container.innerHTML = html;
this._refreshAlokasiVisuals();
},
```
Tidak ada perubahan HTML modal (`modalAlokasi`) dan tidak ada perubahan di C#.1 views