Lewati ke isi

Rencana Implementasi Skenario 2: Tier 1 (Starter) — LMS & CBT Non-Aktif

Rencana implementasi lengkap agar Scola berfungsi optimal untuk sekolah Tier 1 (Starter) dimana siswa bukan user, LMS dan CBT dinonaktifkan, dan input nilai dilakukan secara manual oleh guru/admin.

Created: 2026-03-21
P0 Implemented: 2026-03-21
P1 Implemented: 2026-03-21
P2 Implemented: 2026-03-21
Prereq doc: architecture/score-pipeline.md
Tier reference: architecture/platform-tiers.md


1. Konteks & Tujuan

1.1 Profil Sekolah Tier 1

  • Sekolah kecil, madrasah, pesantren, sekolah swasta baru
  • Siswa tidak punya akun — semua input dari guru/admin
  • LMS (scola_lms) dan CBT (scola_cbt) dinonaktifkan
  • Fitur aktif: akademik dasar, absensi manual, rapor inti, SPMB dasar, tagihan dasar

1.2 Tujuan Implementasi

  1. eRaport bekerja end-to-end tanpa LMS/CBT — guru input nilai manual, workflow approval berjalan, parent bisa unduh PDF
  2. UX bersih dari artefak LMS/CBT — tidak ada tombol, link, atau pesan yang merujuk ke fitur yang non-aktif
  3. Tidak ada breaking change untuk Tier 2/3 — perubahan bersifat conditional berdasarkan feature flag

1.3 Prinsip Implementasi

  • Feature flag driven — semua perubahan dikondisikan oleh scola_lms / scola_cbt flag status
  • Minimal upstream change — prefer hide/show di FE, bukan refactor model BE
  • Backward compatible — Tier 2/3 behavior tidak berubah
  • Progressive enhancement — Tier 1 bisa upgrade ke Tier 2 tanpa migrasi data

2. Inventaris Perubahan

2.1 Overview

Area Jumlah Perubahan Prioritas
Frontend — eRaport Views 7 file P0
Frontend — Config/Guard 2 file P0
Backend — API Response 1 file P0
Backend — Model Behavior 1 file P1
Frontend — Bulk Manual Input (Fitur Baru) 3 file P1
Backend — Bulk Manual Input API (Fitur Baru) 1 file P1
Documentation 2 file P2

2.2 Estimasi Effort

Fase Effort Deskripsi
P0 — Tier-Aware UX 2–3 hari Conditional UI berdasarkan feature flag
P1 — Bulk Manual Input 3–5 hari Fitur baru: CSV/batch input nilai
P2 — Polish & Docs 1–2 hari Testing, dokumentasi, edge cases
Total 6–10 hari

3. P0 — Tier-Aware eRaport UX

3.1 Backend: Expose Module Status di Filter Response

File: scola_report_card/controllers/report_card_api.py

Perubahan: Tambahkan module_status ke response endpoint GET /api/v1/report-card/filters.

# Di method get_report_card_filters(), tambahkan ke response:
'module_status': {
    'lms_active': bool(request.env['ir.module.module'].sudo().search([
        ('name', '=', 'scola_lms'), ('state', '=', 'installed')
    ], limit=1)),
    'cbt_active': bool(request.env['ir.module.module'].sudo().search([
        ('name', '=', 'scola_cbt'), ('state', '=', 'installed')
    ], limit=1)),
}

Alternatif (lebih ringan): Gunakan ir.config_parameter:

'module_status': {
    'lms_active': request.env['ir.config_parameter'].sudo().get_param(
        'scola.feature.lms_enabled', 'true') == 'true',
    'cbt_active': request.env['ir.config_parameter'].sudo().get_param(
        'scola.feature.cbt_enabled', 'true') == 'true',
}

Rationale: FE sudah punya featureFlags.js, tapi eRaport views belum consume flag tersebut untuk conditional rendering. Dengan menambahkan di filter response, views yang sudah fetch filters bisa langsung pakai tanpa import tambahan.


3.2 Frontend: Composable untuk Module Status

File baru: src/composables/useModuleStatus.js

import { computed } from 'vue'
import { isFeatureEnabled } from '@/config/featureFlags'

export function useModuleStatus() {
  const isLmsActive = computed(() => isFeatureEnabled('scola_lms'))
  const isCbtActive = computed(() => isFeatureEnabled('scola_cbt'))
  const hasExternalScoreSource = computed(() => isLmsActive.value || isCbtActive.value)

  return { isLmsActive, isCbtActive, hasExternalScoreSource }
}

Rationale: Satu composable reusable, dipakai di semua eRaport views. Menggunakan featureFlags.js yang sudah di-sync dari backend via session.


3.3 Frontend: ReportLineEditModal.vue

File: src/views/ReportCardManagement/Admin/ReportLineEditModal.vue

Perubahan:

Elemen Kondisi Behavior Tier 1 Behavior Tier 2+
Tombol "Refresh Nilai dari Sumber" per komponen hasExternalScoreSource Hidden Visible (existing)
Label source komponen component.source !== 'manual' Tampil "Manual" Tampil source label (existing)
Info banner atas modal !hasExternalScoreSource Tampil: "Mode input manual — nilai diisi langsung oleh guru" Hidden

Detail implementasi:

<script setup>
import { useModuleStatus } from '@/composables/useModuleStatus'
const { hasExternalScoreSource } = useModuleStatus()
</script>

<!-- Tambah banner di atas form -->
<div v-if="!hasExternalScoreSource" class="mb-4 p-3 bg-blue-50 ... rounded-lg">
  <p class="text-sm text-blue-700">
    Mode input manual — nilai diisi langsung oleh guru.
  </p>
</div>

<!-- Sembunyikan tombol refresh per komponen -->
<button v-if="hasExternalScoreSource" @click="refreshScore(comp)">
  Refresh dari Sumber
</button>

3.4 Frontend: ReportLineDetailModal.vue

File: src/views/ReportCardManagement/Admin/ReportLineDetailModal.vue

Perubahan:

Elemen Kondisi Behavior Tier 1
Source reference link (ke assignment/exam) hasExternalScoreSource && component.source_ref Hidden
Source badge ("dari LMS", "dari CBT") hasExternalScoreSource Hidden — semua tampil "Manual"

3.5 Frontend: AdminReportDetail.vue

File: src/views/ReportCardManagement/Admin/AdminReportDetail.vue

Perubahan:

Elemen Kondisi Behavior Tier 1
Link/button "Buka Gradebook" isLmsActive Hidden
Bulk refresh action (jika ada) hasExternalScoreSource Hidden

3.6 Frontend: HomeroomReportDetail.vue

File: src/views/ReportCardManagement/Faculty/HomeroomReportDetail.vue

Perubahan:

Elemen Kondisi Behavior Tier 1
Link ke Gradebook isLmsActive Hidden
Info statistik "Score dari LMS" hasExternalScoreSource Hidden

3.7 Frontend: TeacherReportLineList.vue

File: src/views/ReportCardManagement/Faculty/TeacherReportLineList.vue

Perubahan:

Elemen Kondisi Behavior Tier 1
Info banner !hasExternalScoreSource Tampil: "Masukkan nilai secara manual untuk setiap mapel yang Anda ampu"
Kolom "Source" di tabel hasExternalScoreSource Hidden — tidak relevan, semua manual

3.8 Frontend: GenerateReportWizard.vue

File: src/views/ReportCardManagement/Admin/GenerateReportWizard.vue

Perubahan:

Elemen Kondisi Behavior Tier 1
Step/checkbox "Auto-refresh dari sumber LMS" hasExternalScoreSource Hidden/skipped
Deskripsi wizard !hasExternalScoreSource Tambah note: "Setelah generate, guru perlu mengisi nilai manual per mapel"

3.9 Frontend: StudentReportList.vue / StudentReportDetail.vue

Tidak perlu diubah. Views ini hanya muncul jika student punya akun dan login. Di Tier 1 student tidak punya akun, jadi views ini tidak akan ter-akses. Route guard (requiresAuth) sudah mencegah akses.


4. P1 — Bulk Manual Input (Fitur Baru)

4.1 Problem Statement

Di Tier 1, guru harus input nilai satu per satu via ReportLineEditModal. Untuk sekolah dengan 30+ siswa × 10+ mapel × 3+ komponen = 900+ input per semester. Ini tidak praktis.

4.2 Solusi: Batch Score Input

4.2.1 Frontend — BatchScoreInput.vue (Fitur Baru)

File baru: src/views/ReportCardManagement/Faculty/BatchScoreInput.vue

Deskripsi: Grid input mirip spreadsheet — satu halaman per mapel, semua siswa × semua komponen.

Wireframe:

┌──────────────────────────────────────────────────────────────────┐
│  Batch Input Nilai — Matematika (Kelas 7A, Semester 1)           │
│                                                                    │
│  ┌─────────────┬──────┬──────┬──────┬──────────────┐             │
│  │ Nama Siswa  │  PH  │  PTS │  PAS │ Nilai Akhir  │             │
│  ├─────────────┼──────┼──────┼──────┼──────────────┤             │
│  │ Ahmad       │ [85] │ [78] │ [82] │    81.7      │             │
│  │ Budi        │ [90] │ [85] │ [88] │    87.7      │             │
│  │ Citra       │ [75] │ [70] │ [72] │    72.3      │             │
│  │ ...         │      │      │      │              │             │
│  └─────────────┴──────┴──────┴──────┴──────────────┘             │
│                                                                    │
│  [Simpan Draft]  [Simpan & Finalisasi]  [Export Template CSV]     │
│  [Import dari CSV]                                                 │
└──────────────────────────────────────────────────────────────────┘

Spesifikasi:

Aspek Detail
Akses Teacher (mapel yang diampu) + Admin (semua mapel)
Filter Batch (kelas), Subject (mapel), Term (semester)
Grid Rows = siswa, Columns = assessment components dari curriculum aktif
Computed Nilai akhir auto-compute berdasarkan weight komponen
Save Batch save — semua perubahan dikirim sekaligus
CSV Import Upload CSV dengan format: student_id, component_code, score
CSV Export Download template CSV pre-filled dengan daftar siswa
Validasi Score range (0–100), required components, weight check

Route baru:

{
  path: '/faculty/report-card/batch-input',
  name: 'BatchScoreInput',
  component: () => import('@/views/ReportCardManagement/Faculty/BatchScoreInput.vue'),
  meta: { requiresAuth: true, capability: 'academics.report_card.manage' }
}

4.2.2 Frontend — CSV Utilities

File baru: src/utils/scoreImportExport.js

/**
 * Generate CSV template for batch score input.
 * @param {Array} students - [{ id, name }]
 * @param {Array} components - [{ code, name }]
 * @returns {string} CSV content
 */
export function generateScoreTemplate(students, components) { ... }

/**
 * Parse CSV file into score entries.
 * @param {File} file
 * @returns {Promise<Array<{ student_id, component_code, score }>>}
 */
export function parseScoreCsv(file) { ... }

/**
 * Validate parsed scores against expected components and score ranges.
 * @param {Array} entries
 * @param {Array} validComponents
 * @returns {{ valid: Array, errors: Array }}
 */
export function validateScoreEntries(entries, validComponents) { ... }

4.2.3 Backend — Batch Save API

File: scola_report_card/controllers/report_card_api.py

Endpoint baru: POST /api/v1/report-card/batch-scores

@http.route('/api/v1/report-card/batch-scores', type='json', auth='user', methods=['POST'])
def save_batch_scores(self, **kwargs):
    """
    Batch save scores for multiple students × components.

    Payload:
    {
        "batch_id": 123,
        "subject_id": 456,
        "academic_term_id": 789,
        "scores": [
            {
                "student_id": 1,
                "component_code": "PH",
                "score": 85.0
            },
            ...
        ]
    }

    Returns:
    {
        "success": true,
        "saved_count": 90,
        "skipped_count": 0,
        "errors": []
    }
    """

Logic:

  1. Validate akses (teacher untuk mapel yang diampu, atau admin)
  2. Untuk setiap entry: a. Find atau create scola.student.report untuk student + term b. Find atau create scola.student.report.line untuk subject c. Find atau create scola.student.report.component untuk component d. Update score, set source = manual
  3. Return summary (saved_count, errors)

4.2.4 Frontend — Service Layer

Tambahan di: src/services/reportCard/reportCard.service.js

export async function saveBatchScores({ batchId, subjectId, termId, scores }) {
  const response = await apiClient.post('/api/v1/report-card/batch-scores', {
    jsonrpc: '2.0',
    method: 'call',
    params: { batch_id: batchId, subject_id: subjectId, academic_term_id: termId, scores }
  })
  return response.data?.result
}

export async function getScoreTemplate({ batchId, subjectId, termId }) {
  // Fetch students + components for template generation
  const response = await apiClient.post('/api/v1/report-card/score-template', {
    jsonrpc: '2.0',
    method: 'call',
    params: { batch_id: batchId, subject_id: subjectId, academic_term_id: termId }
  })
  return response.data?.result
}

5. P2 — Polish & Edge Cases

5.1 Backend: Graceful Empty Source

File: scola_report_card/models/student_report.py

Perubahan di _fetch_score_from_source():

def _fetch_score_from_source(self):
    # Check if LMS module is active
    lms_installed = self.env['ir.module.module'].sudo().search([
        ('name', '=', 'scola_lms'), ('state', '=', 'installed')
    ], limit=1)

    if not lms_installed:
        self.source = 'manual'
        self.source_ref = 'LMS module tidak aktif — gunakan input manual'
        return

    # ... existing logic

Rationale: Pesan yang jelas lebih baik daripada silent empty result.

5.2 Menu Registry Update

File: src/config/menuRegistry.js

Pastikan menu item "Batch Input Nilai" hanya muncul untuk role yang sesuai:

{
  id: 'batch-score-input',
  label: 'Input Nilai Batch',
  icon: 'TableProperties',
  path: '/faculty/report-card/batch-input',
  capability: 'academics.report_card.manage',
  // Visible for all tiers — ini fitur inti rapor, bukan LMS
}

5.3 Parent raportList.vue

Tidak perlu diubah. View ini sudah bekerja dengan baik — hanya menampilkan report yang sudah published terlepas dari sumber nilai.

5.4 Regression Testing Checklist

# Test Case Tier 1 Tier 2+
1 Generate report cards untuk satu kelas ✅ Berhasil tanpa LMS data ✅ Berhasil (existing)
2 Teacher input nilai manual via ReportLineEditModal ✅ Tombol refresh hidden ✅ Tombol refresh visible
3 Teacher batch input nilai via BatchScoreInput ✅ Grid input works ✅ Grid input works
4 CSV import nilai ✅ Import & validate ✅ Import & validate
5 Homeroom submit rapor ✅ No Gradebook link ✅ Gradebook link visible
6 Admin approve & publish ✅ Normal workflow ✅ Normal workflow
7 Parent download PDF ✅ PDF generated ✅ PDF generated
8 Student view (jika ada akun) N/A — no student user ✅ Shows published reports
9 Refresh dari sumber (modal) ✅ Button hidden ✅ Fetches from academic.grade
10 Upgrade Tier 1 → Tier 2 ✅ LMS/CBT muncul, existing manual data preserved N/A

6. Urutan Eksekusi

Week 1 (P0 — Tier-Aware UX): ✅ DONE
├── ✅ Backend — module_status di filter response
├── ✅ Backend — source tracking on manual save + graceful _fetch_score_from_source
├── ✅ Frontend — useModuleStatus composable
├── ✅ Frontend — ReportLineEditModal (conditional refresh + info banner)
├── ✅ Frontend — ReportLineDetailModal (conditional source ref)
├── ✅ Frontend — TeacherReportLineList (info banner)
└── ✅ Frontend — GenerateReportWizard (Tier 1 info note)

Week 2 (P1 — Bulk Manual Input): ✅ DONE
├── ✅ Backend — batch-scores/template + batch-scores/save API endpoints
├── ✅ Frontend — BatchScoreInput.vue (grid UI with keyboard nav)
├── ✅ Frontend — CSV import/export utilities (scoreImportExport.js)
├── ✅ Frontend — reportCard.service.js (getScoreTemplate + saveBatchScores)
├── ✅ Frontend — Route (/faculty/report-card/batch-input) + menu registry
└── ✅ Backend — _fetch_score_from_source graceful handling (done in P0)

Week 3 (P2 — Polish): ✅ DONE
├── ✅ Regression testing — 14 new tests added, 0 regressions (pre-existing 23 failures unchanged)
├── ✅ Unit tests: useModuleStatus composable (5 tests)
├── ✅ Unit tests: scoreImportExport CSV utilities (9 tests)
└── ✅ Documentation update + commit

7. Migration & Upgrade Path

7.1 Tier 1 → Tier 2 Upgrade

Ketika sekolah upgrade dari Starter ke Professional:

  1. Feature flags otomatis berubah: scola_lms: true, scola_cbt: true
  2. Existing data preserved — semua scola.student.report.component dengan source manual tetap ada
  3. LMS routes muncul — menu Gradebook, Assignment, CBT tampil di sidebar
  4. Mixed mode — guru bisa tetap input manual DAN publish dari LMS. Saat publish dari LMS, academic.grade terbuat. Saat eRaport di-refresh, komponen yang sudah punya academic.grade akan ter-update, yang manual tetap manual.
  5. Tidak ada migrasi data yang diperlukan

7.2 Data Consistency

Situasi Behavior
Komponen sudah ada nilai manual, lalu LMS di-publish _fetch_score_from_source() akan overwrite hanya jika user klik "Refresh" — tidak otomatis
Komponen belum ada nilai, lalu LMS di-publish _fetch_score_from_source() akan mengisi dari academic.grade
LMS dinonaktifkan kembali (downgrade) Nilai manual tetap ada. Nilai dari LMS tetap tersimpan di academic.grade tapi tidak di-fetch lagi

8. Keputusan Arsitektural

8.1 Mengapa Feature Flag, Bukan Modul Terpisah?

Keputusan: Gunakan existing feature flag system (featureFlags.js + platformTiers.js), bukan buat modul eRaport-Lite terpisah.

Alasan: - Infrastruktur sudah ada dan proven - Satu codebase — tidak ada fork maintenance - Upgrade path seamless (toggle flag, bukan install module) - Konsisten dengan keputusan tier gating di platform-tiers.md

8.2 Mengapa Batch Input di eRaport, Bukan di Gradebook?

Keputusan: BatchScoreInput masuk ke ReportCardManagement/, bukan LearningManagement/.

Alasan: - Gradebook adalah fitur LMS (Tier 2) — di-gate oleh scola_lms - Batch input adalah fitur rapor inti — harus tersedia di Tier 1 - Data langsung ke scola.student.report.component, bukan melalui academic.grade - Teacher yang input manual tidak perlu tau tentang Gradebook LMS

8.3 Mengapa Tidak Melalui academic.grade untuk Manual Input?

Keputusan: Manual input dari Batch/Modal langsung ke scola.student.report.component, bypass academic.grade.

Alasan: - academic.grade adalah bridge table untuk LMS → eRaport - Di Tier 1, LMS non-aktif — menambah data ke bridge table yang tidak akan ter-consume hanya menambah noise - Jika suatu saat perlu konsistensi, bisa ditambah optional write-through ke academic.grade (backward compatible)

8.4 Mengapa featureFlags.js dan platformTiers.js Tetap Terpisah?

Keputusan: Dua file tetap terpisah, tidak di-merge.

Alasan (Single Responsibility Principle): - featureFlags.js (116 baris) = state management — definisi flag, aliases, normalization, runtime mutation, isFeatureEnabled() query - platformTiers.js (227 baris) = business policy — tier hierarchy, tier comparison, capability→flag inference, route→flag inference - Hanya 3 consumer import keduanya (auth.store, router/index, useMenuV2). 5 observability services hanya butuh isFeatureEnabled — merge akan memaksa mereka import policy code yang tidak relevan - Tidak ada circular dependency

8.5 Tier 2 Coexistence: LMS as Default, Manual Override Always Possible

Keputusan: Di Tier 2 (LMS+CBT aktif), nilai dari LMS/CBT menjadi default, tapi teacher selalu bisa override manual.

Mekanisme (mengikuti pattern Canvas LMS / Google Classroom):

Aturan Detail
Assessment component source_type Dikonfigurasi di curriculum (exam/assignment/manual) — menentukan default source
Auto-fetch on generate/refresh Komponen non-manual otomatis fetch dari academic.grade
Teacher always can edit Semua score selalu editable via ReportLineEditModal
Manual override tracking Saat teacher edit komponen non-manual, backend set source='manual' + source_ref='Override manual oleh [nama]'
"Refresh dari Sumber" Mengembalikan ke nilai LMS/CBT, overwrite manual edit. Source kembali ke exam/assignment
Visual clarity Badge menunjukkan source aktual: "Manual", "Dari Ujian", "Dari Tugas"
Tidak tumpang tindih Hanya 1 nilai per komponen per siswa per mapel. Last-write wins dengan source tracking
Tidak ada model change Existing source field di scola.student.report.component sudah cukup

UX Flow Tier 2:

Generate raport → auto-fetch dari LMS (source: exam/assignment)
  → Teacher review → bisa edit manual (source berubah ke manual)
  → Klik "Refresh" → restore ke LMS value (source kembali ke exam/assignment)


9. File Reference

File yang Dimodifikasi

File Tipe Perubahan
scola_report_card/controllers/report_card_api.py Tambah module_status di filter response + batch-scores endpoint
scola_report_card/models/student_report.py Graceful handling di _fetch_score_from_source()
src/views/ReportCardManagement/Admin/ReportLineEditModal.vue Conditional refresh button + info banner
src/views/ReportCardManagement/Admin/ReportLineDetailModal.vue Conditional source ref links
src/views/ReportCardManagement/Admin/AdminReportDetail.vue Conditional Gradebook link
src/views/ReportCardManagement/Admin/GenerateReportWizard.vue Skip auto-refresh step
src/views/ReportCardManagement/Faculty/HomeroomReportDetail.vue Conditional Gradebook link
src/views/ReportCardManagement/Faculty/TeacherReportLineList.vue Info banner
src/services/reportCard/reportCard.service.js Tambah saveBatchScores() + getScoreTemplate()
src/config/menuRegistry.js Tambah menu item batch input

File Baru

File Deskripsi
src/composables/useModuleStatus.js Composable untuk cek LMS/CBT active status
src/views/ReportCardManagement/Faculty/BatchScoreInput.vue Grid batch input nilai
src/utils/scoreImportExport.js CSV template generation, parsing, validation

Route Baru

Route View Capability
/faculty/report-card/batch-input BatchScoreInput.vue academics.report_card.manage