JustPaste
HomeCategoriesAboutDonateContactTerms of UsePrivacy Policy
JustPaste

Free online notepad — write and share instantly

Navigate

  • Home
  • Timeline
  • Categories

Info

  • About
  • Donate
  • Contact

Legal

  • Terms of Use
  • Privacy Policy

© 2026 JustPaste.app. All rights reserved.

Made with ♥ by JustPaste

Agenda Jawaban | JustPaste.app
22 days ago1 views
👨‍💻Programming

Agenda Jawaban

# 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 &bull; ' + 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#.
← Back to timeline