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
609 lines
24 KiB
Markdown
609 lines
24 KiB
Markdown
# 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
|