Files
Amadea 61ce9d7926 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
2026-09-18 19:24:59 +07:00

24 KiB
Raw Permalink Blame History

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. Untuk contoh data, lihat 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

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)

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.

pricePerMeeting = klass.pricePerMeeting ?? Math.round(klass.packagePrice / klass.totalMeetings)
amount          = meetingsPaid * pricePerMeeting

Contoh: paket Rp 2.000.000 / 10 pertemuanpricePerMeeting = 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:

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

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 (0100) → 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

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: 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