feat: implementasi Kodeva - sistem manajemen kursus coding
Fitur lengkap: - Auth JWT + RBAC (Admin/Teacher) + halaman login - Shell 3 kolom (left nav, topbar, right rail dashboard) - CRUD siswa + follow up kanban + status calon/student/ex - Dashboard: siswa aktif, kehadiran, bar sumber lead, perlu follow up - Pembayaran prorata per pertemuan (cash/transfer/qris) - Kelas, enrollment, generate sesi, kehadiran bulk - Jurnal aktivitas + upload multi-foto - Raport + placement test (tampil di detail siswa) - Pengaturan & seed data contoh Stack: Next.js 16, TypeScript, Tailwind v4, Prisma (SQLite), Recharts, Zod
This commit is contained in:
@@ -0,0 +1,608 @@
|
||||
# AGENTS.md — Kodeva
|
||||
|
||||
> **Kodeva** — Sistem Manajemen Kursus Coding (Coding Course Management System).
|
||||
> Dokumen ini adalah kontrak kerja untuk AI coding agent & developer. Baca sampai habis sebelum menulis kode.
|
||||
> Untuk aturan visual/UX, lihat [`DESIGN.md`](./DESIGN.md). Untuk contoh data, lihat [`seed.example.ts`](./seed.example.ts).
|
||||
|
||||
---
|
||||
|
||||
## 1. Ringkasan Produk
|
||||
|
||||
Kodeva membantu akademi/bimbel coding mengelola siklus hidup siswa dari **calon student → student → ex student**, lengkap dengan:
|
||||
|
||||
| # | Fitur | Ringkas |
|
||||
|---|-------|---------|
|
||||
| 1 | **CRUD Follow Up Siswa** | Data siswa: nama, no. telepon, email, foto profil, nama orang tua, sumber lead (WhatsApp / Referral / Banner / Social Media), status siswa |
|
||||
| 2 | **Dashboard Right Rail** | Grafik siswa aktif, kehadiran, bar chart sumber lead |
|
||||
| 3 | **Pembayaran Prorata** | Bayar per pertemuan, tanpa payment gateway, metode Cash / Transfer / QRIS |
|
||||
| 4 | **Jurnal & Raport & Kehadiran** | Guru mengisi kehadiran, jurnal kelas, dan raport siswa |
|
||||
| 5 | **Konfigurasi Kelas** | Kelas dapat dikonfigurasi; siswa dipilihkan kelas saat pendaftaran |
|
||||
| 6 | **Jurnal Aktivitas** | Upload foto + aktivitas kelas |
|
||||
| 7 | **Dashboard Ringkasan** | Berapa yang harus di-follow up, berapa ex student |
|
||||
| 8 | **Placement Test** | Hasil tes penempatan tampil di detail siswa |
|
||||
|
||||
**Pengguna:** Admin (pemilik/operator), Teacher (tutor). Bahasa UI: **Bahasa Indonesia**.
|
||||
|
||||
---
|
||||
|
||||
## 2. Tech Stack (Default)
|
||||
|
||||
```
|
||||
Runtime : Node.js 20+ / Bun
|
||||
Framework : Next.js 15 (App Router, React Server Components)
|
||||
Language : TypeScript (strict)
|
||||
Styling : Tailwind CSS v4 + CSS variables (lihat DESIGN.md)
|
||||
UI Kit : shadcn/ui + Radix Primitives + lucide-react
|
||||
Charts : Recharts
|
||||
Forms : React Hook Form + Zod (zodResolver)
|
||||
Tables : TanStack Table
|
||||
Drag & Drop : dnd-kit (kanban follow-up)
|
||||
ORM : Prisma
|
||||
Database : PostgreSQL 16 (dev cepat boleh SQLite, tapi schema final PostgreSQL)
|
||||
Auth : Auth.js (NextAuth v5) — Credentials + RBAC
|
||||
Upload : API route lokal `/api/uploads` → `public/uploads` (atau S3-compatible)
|
||||
Dates : date-fns (+ locale `id`)
|
||||
Testing : Vitest (unit) + Playwright (e2e)
|
||||
Lint/Format : ESLint + Prettier
|
||||
Package Mgr : pnpm
|
||||
```
|
||||
|
||||
> Jika user memilih stack lain (mis. Laravel/Vue), **pertahankan domain model, business rules, dan DESIGN.md**, hanya ganti implementasi. Tanyakan dulu sebelum migrasi stack.
|
||||
|
||||
---
|
||||
|
||||
## 3. Setup & Perintah
|
||||
|
||||
```bash
|
||||
pnpm install # install dependency
|
||||
cp .env.example .env # siapkan env
|
||||
pnpm db:push # prisma db push (dev)
|
||||
pnpm db:seed # isi contoh data (seed.example.ts)
|
||||
pnpm dev # jalankan dev server :3000
|
||||
pnpm build && pnpm start # production build
|
||||
|
||||
pnpm lint # eslint
|
||||
pnpm typecheck # tsc --noEmit
|
||||
pnpm test # vitest
|
||||
pnpm test:e2e # playwright
|
||||
pnpm format # prettier --write .
|
||||
```
|
||||
|
||||
**Definition of Done** setiap task: `pnpm lint && pnpm typecheck && pnpm test` hijau, tidak ada `any` liar, tidak ada secret ter-commit.
|
||||
|
||||
---
|
||||
|
||||
## 4. Struktur Folder
|
||||
|
||||
```
|
||||
kodeva/
|
||||
├─ AGENTS.md
|
||||
├─ DESIGN.md
|
||||
├─ seed.example.ts
|
||||
├─ prisma/
|
||||
│ ├─ schema.prisma
|
||||
│ └─ migrations/
|
||||
├─ public/
|
||||
│ └─ uploads/ # foto siswa, jurnal, bukti bayar
|
||||
├─ src/
|
||||
│ ├─ app/
|
||||
│ │ ├─ (auth)/login/page.tsx
|
||||
│ │ ├─ (app)/
|
||||
│ │ │ ├─ layout.tsx # shell: left nav + main + right rail
|
||||
│ │ │ ├─ dashboard/page.tsx
|
||||
│ │ │ ├─ follow-up/page.tsx # kanban
|
||||
│ │ │ ├─ students/
|
||||
│ │ │ │ ├─ page.tsx
|
||||
│ │ │ │ ├─ new/page.tsx
|
||||
│ │ │ │ └─ [id]/page.tsx # detail: profil, kelas, kehadiran, bayar, raport, placement
|
||||
│ │ │ ├─ classes/page.tsx
|
||||
│ │ │ ├─ classes/[id]/page.tsx
|
||||
│ │ │ ├─ attendance/page.tsx
|
||||
│ │ │ ├─ payments/page.tsx
|
||||
│ │ │ ├─ journals/page.tsx
|
||||
│ │ │ ├─ report-cards/page.tsx
|
||||
│ │ │ ├─ placements/page.tsx
|
||||
│ │ │ └─ settings/page.tsx
|
||||
│ │ └─ api/
|
||||
│ │ ├─ students/route.ts
|
||||
│ │ ├─ students/[id]/route.ts
|
||||
│ │ ├─ ... (lihat §8)
|
||||
│ │ └─ uploads/route.ts
|
||||
│ ├─ components/
|
||||
│ │ ├─ ui/ # shadcn primitives
|
||||
│ │ ├─ layout/ # AppSidebar, RightRail, Topbar
|
||||
│ │ ├─ students/ # StudentForm, StudentCard, StatusBadge
|
||||
│ │ ├─ charts/ # ActiveStudentsChart, AttendanceChart, LeadSourceBar
|
||||
│ │ ├─ payments/ # PaymentForm, ProrataSummary
|
||||
│ │ └─ shared/ # EmptyState, StatCard, DataTable
|
||||
│ ├─ lib/
|
||||
│ │ ├─ prisma.ts
|
||||
│ │ ├─ auth.ts
|
||||
│ │ ├─ rbac.ts
|
||||
│ │ ├─ validators/ # zod schemas per entitas
|
||||
│ │ ├─ prorata.ts # kalkulasi pembayaran (PURE FUNCTION + unit test)
|
||||
│ │ ├─ follow-up.ts # query due follow up
|
||||
│ │ └─ utils.ts # cn(), formatIDR(), formatDateID()
|
||||
│ ├─ hooks/
|
||||
│ └─ types/
|
||||
└─ tests/
|
||||
```
|
||||
|
||||
**Aturan:** Server Components untuk baca data, Server Actions / Route Handlers untuk mutasi. Jangan fetch API internal dari Server Component — query Prisma langsung.
|
||||
|
||||
---
|
||||
|
||||
## 5. Konvensi Kode
|
||||
|
||||
- **TypeScript strict.** Dilarang `any`; pakai `unknown` + narrowing. `import type` untuk tipe.
|
||||
- **Naming:** komponen `PascalCase`, file komponen `PascalCase.tsx`, util `camelCase.ts`, route segment `kebab-case`.
|
||||
- **Validasi:** semua input user divalidasi Zod di boundary (form + route handler) memakai schema yang sama.
|
||||
- **Uang:** simpan sebagai `Int` dalam **rupiah penuh** (tanpa sen). Jangan pakai `Float`. Format tampilan lewat `formatIDR()` → `Rp 2.000.000`.
|
||||
- **Tanggal:** simpan UTC di DB, tampilkan dengan `date-fns` locale `id` (mis. `Senin, 18 Sep 2026`). Timezone default `Asia/Jakarta`.
|
||||
- **ID:** `cuid()`. Jangan auto-increment untuk entitas publik.
|
||||
- **Soft delete:** entitas siswa/kelas pakai `archivedAt`; jangan hard delete.
|
||||
- **Query:** selalu scope query ke authorization. Jangan pernah mengembalikan data lintas-tenant tanpa cek role.
|
||||
- **Error:** route handler mengembalikan `{ data }` atau `{ error: { code, message } }`; jangan bocorkan stack trace.
|
||||
- **Commit:** Conventional Commits (`feat(students): ...`, `fix(prorata): ...`).
|
||||
|
||||
---
|
||||
|
||||
## 6. Domain Model (Prisma)
|
||||
|
||||
```prisma
|
||||
generator client { provider = "prisma-client-js" }
|
||||
datasource db { provider = "postgresql"; url = env("DATABASE_URL") }
|
||||
|
||||
enum Role { ADMIN TEACHER }
|
||||
enum StudentStatus { CALON_STUDENT STUDENT EX_STUDENT }
|
||||
enum LeadSource { WHATSAPP REFERRAL BANNER SOCIAL_MEDIA }
|
||||
enum FollowUpStatus { NEW CONTACTED TRIAL OFFER_SENT ENROLLED LOST }
|
||||
enum Gender { MALE FEMALE }
|
||||
enum ClassStatus { DRAFT ACTIVE COMPLETED ARCHIVED }
|
||||
enum EnrollmentStatus { ACTIVE COMPLETED DROPPED }
|
||||
enum AttendanceStatus { PRESENT LATE EXCUSED SICK ABSENT }
|
||||
enum PaymentMethod { CASH TRANSFER QRIS }
|
||||
enum PaymentStatus { UNPAID PARTIAL PAID REFUNDED }
|
||||
enum PlacementLevel { BEGINNER INTERMEDIATE ADVANCED }
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
email String @unique
|
||||
passwordHash String
|
||||
role Role @default(TEACHER)
|
||||
avatarUrl String?
|
||||
phone String?
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
classesTaught Class[] @relation("ClassTeacher")
|
||||
sessions Session[]
|
||||
journals Journal[]
|
||||
reportCards ReportCard[]
|
||||
placementTests PlacementTest[] @relation("Examiner")
|
||||
followUpsOwned FollowUp[] @relation("FollowUpPIC")
|
||||
}
|
||||
|
||||
model Student {
|
||||
id String @id @default(cuid())
|
||||
fullName String
|
||||
phone String
|
||||
email String? @unique
|
||||
photoUrl String?
|
||||
parentName String?
|
||||
parentPhone String?
|
||||
birthDate DateTime?
|
||||
gender Gender?
|
||||
address String?
|
||||
leadSource LeadSource
|
||||
status StudentStatus @default(CALON_STUDENT)
|
||||
notes String?
|
||||
joinedAt DateTime?
|
||||
archivedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
enrollments Enrollment[]
|
||||
attendances Attendance[]
|
||||
payments Payment[]
|
||||
reportCards ReportCard[]
|
||||
placementTests PlacementTest[]
|
||||
followUps FollowUp[]
|
||||
journalStudents JournalStudent[]
|
||||
}
|
||||
|
||||
model Class {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
code String @unique
|
||||
description String?
|
||||
level PlacementLevel
|
||||
teacherId String?
|
||||
teacher User? @relation("ClassTeacher", fields: [teacherId], references: [id])
|
||||
totalMeetings Int @default(10)
|
||||
packagePrice Int @default(0) // harga paket penuh (IDR)
|
||||
pricePerMeeting Int? // override opsional
|
||||
capacity Int @default(12)
|
||||
schedule String? // "Senin & Rabu 16:00-17:30"
|
||||
startDate DateTime?
|
||||
endDate DateTime?
|
||||
status ClassStatus @default(ACTIVE)
|
||||
color String? // hex warna kartu kelas
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
enrollments Enrollment[]
|
||||
sessions Session[]
|
||||
payments Payment[]
|
||||
journals Journal[]
|
||||
reportCards ReportCard[]
|
||||
}
|
||||
|
||||
model Enrollment {
|
||||
id String @id @default(cuid())
|
||||
studentId String
|
||||
student Student @relation(fields: [studentId], references: [id], onDelete: Cascade)
|
||||
classId String
|
||||
class Class @relation(fields: [classId], references: [id], onDelete: Cascade)
|
||||
status EnrollmentStatus @default(ACTIVE)
|
||||
joinedAt DateTime @default(now())
|
||||
leftAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
@@unique([studentId, classId])
|
||||
}
|
||||
|
||||
model Session {
|
||||
id String @id @default(cuid())
|
||||
classId String
|
||||
class Class @relation(fields: [classId], references: [id], onDelete: Cascade)
|
||||
meetingNumber Int
|
||||
date DateTime
|
||||
topic String?
|
||||
teacherId String?
|
||||
teacher User? @relation(fields: [teacherId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
attendances Attendance[]
|
||||
journals Journal[]
|
||||
@@unique([classId, meetingNumber])
|
||||
}
|
||||
|
||||
model Attendance {
|
||||
id String @id @default(cuid())
|
||||
sessionId String
|
||||
session Session @relation(fields: [sessionId], references: [id], onDelete: Cascade)
|
||||
studentId String
|
||||
student Student @relation(fields: [studentId], references: [id], onDelete: Cascade)
|
||||
status AttendanceStatus
|
||||
note String?
|
||||
recordedById String?
|
||||
createdAt DateTime @default(now())
|
||||
@@unique([sessionId, studentId])
|
||||
}
|
||||
|
||||
model Payment {
|
||||
id String @id @default(cuid())
|
||||
invoiceNumber String @unique
|
||||
studentId String
|
||||
student Student @relation(fields: [studentId], references: [id])
|
||||
classId String
|
||||
class Class @relation(fields: [classId], references: [id])
|
||||
meetingsPaid Int
|
||||
pricePerMeeting Int
|
||||
amount Int // meetingsPaid * pricePerMeeting
|
||||
method PaymentMethod
|
||||
status PaymentStatus @default(PAID)
|
||||
paidAt DateTime
|
||||
note String?
|
||||
proofUrl String?
|
||||
createdById String?
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model Journal {
|
||||
id String @id @default(cuid())
|
||||
classId String
|
||||
class Class @relation(fields: [classId], references: [id], onDelete: Cascade)
|
||||
sessionId String?
|
||||
session Session? @relation(fields: [sessionId], references: [id])
|
||||
title String
|
||||
description String?
|
||||
activityDate DateTime
|
||||
tags String[]
|
||||
photos JournalPhoto[]
|
||||
students JournalStudent[]
|
||||
authorId String
|
||||
author User @relation(fields: [authorId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model JournalPhoto {
|
||||
id String @id @default(cuid())
|
||||
journalId String
|
||||
journal Journal @relation(fields: [journalId], references: [id], onDelete: Cascade)
|
||||
url String
|
||||
caption String?
|
||||
sortOrder Int @default(0)
|
||||
}
|
||||
|
||||
model JournalStudent {
|
||||
journalId String
|
||||
studentId String
|
||||
journal Journal @relation(fields: [journalId], references: [id], onDelete: Cascade)
|
||||
student Student @relation(fields: [studentId], references: [id], onDelete: Cascade)
|
||||
@@id([journalId, studentId])
|
||||
}
|
||||
|
||||
model ReportCard {
|
||||
id String @id @default(cuid())
|
||||
studentId String
|
||||
student Student @relation(fields: [studentId], references: [id], onDelete: Cascade)
|
||||
classId String
|
||||
class Class @relation(fields: [classId], references: [id])
|
||||
period String // "2026-Term1"
|
||||
teacherId String
|
||||
teacher User @relation(fields: [teacherId], references: [id])
|
||||
attendanceSummary String?
|
||||
finalScore Int?
|
||||
grade String? // A / B / C / D
|
||||
teacherNotes String?
|
||||
publishedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
scores ReportScore[]
|
||||
}
|
||||
|
||||
model ReportScore {
|
||||
id String @id @default(cuid())
|
||||
reportCardId String
|
||||
reportCard ReportCard @relation(fields: [reportCardId], references: [id], onDelete: Cascade)
|
||||
aspect String // "Logika", "Computational Thinking", "Problem Solving", "Kreativitas", "Kolaborasi"
|
||||
score Int // 0-100
|
||||
note String?
|
||||
}
|
||||
|
||||
model PlacementTest {
|
||||
id String @id @default(cuid())
|
||||
studentId String
|
||||
student Student @relation(fields: [studentId], references: [id], onDelete: Cascade)
|
||||
testDate DateTime
|
||||
levelResult PlacementLevel
|
||||
recommendedClassId String?
|
||||
totalScore Int
|
||||
examinerId String?
|
||||
examiner User? @relation("Examiner", fields: [examinerId], references: [id])
|
||||
notes String?
|
||||
createdAt DateTime @default(now())
|
||||
scores PlacementScore[]
|
||||
}
|
||||
|
||||
model PlacementScore {
|
||||
id String @id @default(cuid())
|
||||
placementTestId String
|
||||
placementTest PlacementTest @relation(fields: [placementTestId], references: [id], onDelete: Cascade)
|
||||
category String // "Logika", "Dasar Coding", "Problem Solving", "Kreativitas"
|
||||
score Int // 0-100
|
||||
}
|
||||
|
||||
model FollowUp {
|
||||
id String @id @default(cuid())
|
||||
studentId String
|
||||
student Student @relation(fields: [studentId], references: [id], onDelete: Cascade)
|
||||
status FollowUpStatus @default(NEW)
|
||||
picId String?
|
||||
pic User? @relation("FollowUpPIC", fields: [picId], references: [id])
|
||||
nextFollowUpAt DateTime?
|
||||
lastContactedAt DateTime?
|
||||
channel LeadSource?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
activities FollowUpActivity[]
|
||||
}
|
||||
|
||||
model FollowUpActivity {
|
||||
id String @id @default(cuid())
|
||||
followUpId String
|
||||
followUp FollowUp @relation(fields: [followUpId], references: [id], onDelete: Cascade)
|
||||
type String // call | wa | email | meeting
|
||||
note String
|
||||
authorId String?
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model Setting {
|
||||
key String @id
|
||||
value Json
|
||||
}
|
||||
```
|
||||
|
||||
**Relasi penting:** `Student 1—N Enrollment N—1 Class`. Satu siswa bisa ikut beberapa kelas. Kehadiran terikat ke `Session` (pertemuan), bukan langsung ke kelas — ini yang membuat prorata per pertemuan akurat.
|
||||
|
||||
---
|
||||
|
||||
## 7. Business Rules (WAJIB, sertakan unit test)
|
||||
|
||||
### 7.1 Prorata Pembayaran — `src/lib/prorata.ts`
|
||||
Pembayaran dihitung **per pertemuan**, bukan per bulan.
|
||||
|
||||
```ts
|
||||
pricePerMeeting = klass.pricePerMeeting ?? Math.round(klass.packagePrice / klass.totalMeetings)
|
||||
amount = meetingsPaid * pricePerMeeting
|
||||
```
|
||||
|
||||
Contoh: paket **Rp 2.000.000 / 10 pertemuan** → `pricePerMeeting = 200.000`.
|
||||
- Bayar 10 pertemuan → `amount = 2.000.000` (PAID penuh).
|
||||
- Bayar 5 pertemuan → `amount = 1.000.000` (PAID, sisa 5 sesi).
|
||||
- Cicilan: `Payment.status = PARTIAL` bila akumulasi `meetingsPaid` < komitmen, lalu naik ke `PAID`.
|
||||
|
||||
**Nilai prorata saat siswa berhenti pindah/refund:**
|
||||
```ts
|
||||
usedMeetings = count(Attendance WHERE studentId = X AND status IN (PRESENT, LATE))
|
||||
paidMeetings = sum(Payment.meetingsPaid WHERE status != REFUNDED)
|
||||
remainingMeetings = max(0, paidMeetings - usedMeetings)
|
||||
proratedValue = remainingMeetings * pricePerMeeting // nilai sisa / refund
|
||||
```
|
||||
|
||||
**Aturan sesi:** 1 pembayaran = N pertemuan. Kuota sesi siswa = total `meetingsPaid`. Sesi terpakai = kehadiran `PRESENT`/`LATE`. Jika sesi terpakai > kuota → tandai `UNPAID` & tampilkan badge **"Perlu Perpanjang"**.
|
||||
|
||||
### 7.2 Follow Up Due — `src/lib/follow-up.ts`
|
||||
```ts
|
||||
followUpDue = FollowUp.findMany({
|
||||
where: {
|
||||
status: { notIn: ["ENROLLED", "LOST"] },
|
||||
nextFollowUpAt: { lte: endOfToday() },
|
||||
},
|
||||
})
|
||||
```
|
||||
- Angka **"Perlu Follow Up"** di dashboard = `count(followUpDue)`.
|
||||
- Angka **"Ex Student"** = `count(Student where status = EX_STUDENT)`.
|
||||
- Kanban follow-up: kolom `NEW → CONTACTED → TRIAL → OFFER_SENT → ENROLLED`; `LOST` di kolom terpisah.
|
||||
- Saat follow-up diubah ke `ENROLLED`, tawarkan form: ubah `Student.status` jadi `STUDENT`, pilih kelas, buat `Enrollment`, lalu arahkan ke input `Payment`.
|
||||
|
||||
### 7.3 Kehadiran
|
||||
- `attendanceRate = (PRESENT + LATE) / totalSesiTercatat * 100`.
|
||||
- `LATE` dihitung hadir tapi ditandai kuning.
|
||||
- Guru mengisi kehadiran per `Session` (halaman `attendance`), bisa bulk (Hadir semua) lalu ubah pengecualian.
|
||||
|
||||
### 7.4 Status Siswa
|
||||
```
|
||||
CALON_STUDENT --(enroll + bayar)--> STUDENT --(selesai kelas / berhenti)--> EX_STUDENT
|
||||
^ |
|
||||
+---------- (daftar lagi) ----------------+
|
||||
```
|
||||
Perubahan status manual oleh Admin harus tercatat di activity/audit.
|
||||
|
||||
### 7.5 Placement Test
|
||||
- Menyimpan skor per kategori (0–100) → `levelResult` (`BEGINNER`/`INTERMEDIATE`/`ADVANCED`) + rekomendasi kelas.
|
||||
- Hasil **selalu tampil** di tab "Placement Test" pada halaman detail siswa dan sebagai badge di header profil.
|
||||
|
||||
### 7.6 Kelas
|
||||
- Admin bisa CRUD kelas: nama, kode unik, level, guru, jumlah pertemuan, harga paket, kapasitas, jadwal, warna.
|
||||
- Siswa baru **wajib memilih kelas** (minimal 1) saat diubah menjadi `STUDENT`. Validasi kapasitas: tolak jika `enrollments.active >= capacity`.
|
||||
- Generate `Session` otomatis sebanyak `totalMeetings` dengan interval jadwal saat kelas diaktifkan (boleh manual).
|
||||
|
||||
---
|
||||
|
||||
## 8. API Endpoints
|
||||
|
||||
| Method | Route | Fungsi | Role |
|
||||
|--------|-------|--------|------|
|
||||
| GET/POST | `/api/students` | list (filter status/source/q) & create | ADMIN, TEACHER |
|
||||
| GET/PATCH/DELETE | `/api/students/[id]` | detail, update, archive | ADMIN, TEACHER |
|
||||
| POST | `/api/students/[id]/enroll` | enroll ke kelas | ADMIN |
|
||||
| GET/POST | `/api/follow-ups` | list (due) & create | ADMIN, TEACHER |
|
||||
| PATCH | `/api/follow-ups/[id]` | ubah status / jadwal / PIC | ADMIN, TEACHER |
|
||||
| POST | `/api/follow-ups/[id]/activities` | catat aktivitas follow up | ADMIN, TEACHER |
|
||||
| GET/POST | `/api/classes` | list & create kelas | ADMIN |
|
||||
| GET/PATCH/DELETE | `/api/classes/[id]` | detail/update/archive | ADMIN |
|
||||
| POST | `/api/classes/[id]/sessions` | generate/atur pertemuan | ADMIN, TEACHER |
|
||||
| GET/POST | `/api/attendance` | per session & bulk upsert | ADMIN, TEACHER |
|
||||
| GET/POST | `/api/payments` | list & buat pembayaran (prorata) | ADMIN |
|
||||
| PATCH | `/api/payments/[id]` | update status / refund | ADMIN |
|
||||
| GET/POST | `/api/journals` | feed & create (multipart foto) | ADMIN, TEACHER |
|
||||
| GET/PATCH/DELETE | `/api/journals/[id]` | detail/update/hapus | ADMIN, TEACHER |
|
||||
| GET/POST | `/api/report-cards` | list & create | ADMIN, TEACHER |
|
||||
| PATCH | `/api/report-cards/[id]/publish` | publish ke siswa | ADMIN, TEACHER |
|
||||
| GET/POST | `/api/placements` | list & create hasil placement | ADMIN, TEACHER |
|
||||
| GET | `/api/dashboard/summary` | angka + data grafik right rail | ADMIN, TEACHER |
|
||||
| POST | `/api/uploads` | upload file → URL | ADMIN, TEACHER |
|
||||
|
||||
Semua route pakai `withAuth(handler, { roles })` dari `src/lib/rbac.ts`. Validasi body dengan Zod. Response sukses `{ data }`, error `{ error: { code, message } }`.
|
||||
|
||||
---
|
||||
|
||||
## 9. RBAC
|
||||
|
||||
| Area | ADMIN | TEACHER |
|
||||
|------|:-----:|:-------:|
|
||||
| Dashboard & right rail | ✅ | ✅ |
|
||||
| CRUD siswa | ✅ | ✅ (tanpa archive) |
|
||||
| CRUD follow up | ✅ | ✅ |
|
||||
| CRUD kelas & setting | ✅ | 👁 baca saja |
|
||||
| Input kehadiran | ✅ | ✅ (kelas sendiri) |
|
||||
| Input jurnal & foto | ✅ | ✅ (kelas sendiri) |
|
||||
| Input raport | ✅ | ✅ (kelas sendiri) |
|
||||
| Input placement test | ✅ | ✅ |
|
||||
| Pembayaran | ✅ | 👁 baca saja |
|
||||
| Manajemen user | ✅ | ❌ |
|
||||
|
||||
---
|
||||
|
||||
## 10. Environment Variables
|
||||
|
||||
```env
|
||||
DATABASE_URL="postgresql://user:pass@localhost:5432/kodeva"
|
||||
AUTH_SECRET="ganti-dengan-random-32-char"
|
||||
AUTH_TRUST_HOST=true
|
||||
NEXT_PUBLIC_APP_NAME="Kodeva"
|
||||
NEXT_PUBLIC_APP_URL="http://localhost:3000"
|
||||
UPLOAD_DIR="./public/uploads"
|
||||
MAX_UPLOAD_MB=5
|
||||
TZ="Asia/Jakarta"
|
||||
```
|
||||
|
||||
Jangan pernah commit `.env`. Sediakan `.env.example`.
|
||||
|
||||
---
|
||||
|
||||
## 11. Contoh Data
|
||||
|
||||
Dataset contoh lengkap ada di [`seed.example.ts`](./seed.example.ts): 3 user (1 admin, 2 teacher), 5 kelas, 10 siswa (campuran `CALON_STUDENT` / `STUDENT` / `EX_STUDENT`), enrollment, sesi, kehadiran, pembayaran prorata, jurnal + foto, raport, placement test, dan follow up.
|
||||
|
||||
Ringkasan untuk konteks cepat:
|
||||
|
||||
**Kelas (5):**
|
||||
| Kode | Nama | Level | Pertemuan | Harga Paket | Rp/Pertemuan |
|
||||
|------|------|-------|-----------|-------------|--------------|
|
||||
| KOD-SCR-01 | Scratch Junior | Beginner | 8 | Rp 1.200.000 | Rp 150.000 |
|
||||
| KOD-PYT-01 | Python Dasar | Beginner | 10 | Rp 2.000.000 | Rp 200.000 |
|
||||
| KOD-WEB-01 | Web Dev: HTML & CSS | Beginner | 12 | Rp 2.400.000 | Rp 200.000 |
|
||||
| KOD-JS-01 | JavaScript Intermediate | Intermediate | 10 | Rp 2.500.000 | Rp 250.000 |
|
||||
| KOD-ARD-01 | Arduino & Robotics | Intermediate | 8 | Rp 1.800.000 | Rp 225.000 |
|
||||
|
||||
**Siswa (10):** Andi Pratama (STUDENT), Citra Lestari (CALON_STUDENT), Dimas Anggara (STUDENT), Elsa Maharani (CALON_STUDENT), Fajar Nugroho (STUDENT), Gita Permata (EX_STUDENT), Hadi Susanto (STUDENT), Intan Puspita (CALON_STUDENT), Joko Wijaya (STUDENT), Kirana Dewi (EX_STUDENT).
|
||||
|
||||
**Contoh prorata:** Andi bayar Rp 2.000.000 untuk 10 pertemuan Python Dasar → Rp 200.000/pertemuan. Hadir 6 → terpakai Rp 1.200.000, sisa 4 pertemuan (Rp 800.000).
|
||||
|
||||
---
|
||||
|
||||
## 12. Testing & Quality
|
||||
|
||||
- **Unit wajib** untuk `prorata.ts`, `follow-up.ts`, `utils` format uang/tanggal, dan semua Zod schema.
|
||||
- **Integration** untuk route handler (happy path + validasi gagal + unauthorized).
|
||||
- **E2E** minimal: login → tambah calon siswa → follow up → enroll + bayar prorata → isi kehadiran → isi jurnal → isi raport → lihat placement test di detail siswa.
|
||||
- Sertakan test case batas: `totalMeetings = 0`, `meetingsPaid > totalMeetings`, pembayaran parsial, kehadiran melebihi kuota.
|
||||
|
||||
---
|
||||
|
||||
## 13. Do / Don't untuk Agent
|
||||
|
||||
**DO**
|
||||
- Baca `DESIGN.md` sebelum membuat/mengubah UI. Pakai token warna & komponen yang sudah ada, jangan hardcode hex baru.
|
||||
- Jalankan `prisma generate` setelah mengubah schema.
|
||||
- Buat komponen kecil & reusable; jangan file 800 baris.
|
||||
- Tangani 4 state: **loading (skeleton), empty, error, success**.
|
||||
- Aksesibel: label form, `aria-*`, fokus terlihat, kontras AA.
|
||||
- Format uang & tanggal lewat util, jangan inline.
|
||||
|
||||
**DON'T**
|
||||
- Jangan tambahkan payment gateway / integrasi pihak ketiga pembayaran.
|
||||
- Jangan pakai `Float` untuk uang.
|
||||
- Jangan fetch internal API dari Server Component.
|
||||
- Jangan hapus data keras; gunakan archive.
|
||||
- Jangan ubah `DESIGN.md`/palet tanpa persetujuan user.
|
||||
- Jangan taruh logika prorata di komponen; selalu di `lib/prorata.ts` + test.
|
||||
|
||||
---
|
||||
|
||||
## 14. Roadmap
|
||||
|
||||
- [ ] **M1** Auth + shell layout (left nav, right rail) + Prisma schema + seed
|
||||
- [ ] **M2** CRUD Siswa + Follow Up kanban + status
|
||||
- [ ] **M3** CRUD Kelas + Enrollment + Sesi/Pertemuan
|
||||
- [ ] **M4** Kehadiran + Pembayaran prorata
|
||||
- [ ] **M5** Jurnal + upload foto
|
||||
- [ ] **M6** Raport + Placement Test
|
||||
- [ ] **M7** Dashboard right rail (semua grafik) + polish
|
||||
- [ ] **M8** Export, notifikasi WhatsApp manual, audit log
|
||||
Reference in New Issue
Block a user