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¶
- eRaport bekerja end-to-end tanpa LMS/CBT — guru input nilai manual, workflow approval berjalan, parent bisa unduh PDF
- UX bersih dari artefak LMS/CBT — tidak ada tombol, link, atau pesan yang merujuk ke fitur yang non-aktif
- 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_cbtflag 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:
- Validate akses (teacher untuk mapel yang diampu, atau admin)
- Untuk setiap entry:
a. Find atau create
scola.student.reportuntuk student + term b. Find atau createscola.student.report.lineuntuk subject c. Find atau createscola.student.report.componentuntuk component d. Update score, set source =manual - 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:
- Feature flags otomatis berubah:
scola_lms: true,scola_cbt: true - Existing data preserved — semua
scola.student.report.componentdengan sourcemanualtetap ada - LMS routes muncul — menu Gradebook, Assignment, CBT tampil di sidebar
- Mixed mode — guru bisa tetap input manual DAN publish dari LMS. Saat publish dari LMS,
academic.gradeterbuat. Saat eRaport di-refresh, komponen yang sudah punyaacademic.gradeakan ter-update, yang manual tetap manual. - 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 |