From 61ce9d7926fe03bb9feb48320a6a53bdd4930808 Mon Sep 17 00:00:00 2001 From: Amadea Date: Fri, 18 Sep 2026 19:24:59 +0700 Subject: [PATCH] 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 --- .env.example | 8 + .gitignore | 51 + AGENTS.md | 608 ++ DESIGN.md | 458 ++ PLAN.md | 360 ++ README.md | 34 + eslint.config.mjs | 18 + next.config.ts | 7 + package.json | 48 + pnpm-lock.yaml | 5291 +++++++++++++++++ pnpm-workspace.yaml | 7 + postcss.config.mjs | 7 + prisma/schema.prisma | 281 + prisma/seed.example.ts | 329 + prisma/seed.ts | 279 + src/app/(app)/attendance/actions.ts | 37 + src/app/(app)/attendance/page.tsx | 133 + src/app/(app)/classes/[id]/edit/page.tsx | 45 + src/app/(app)/classes/[id]/page.tsx | 153 + src/app/(app)/classes/actions.ts | 118 + src/app/(app)/classes/new/page.tsx | 18 + src/app/(app)/classes/page.tsx | 91 + src/app/(app)/dashboard/page.tsx | 213 + src/app/(app)/follow-up/actions.ts | 87 + src/app/(app)/follow-up/page.tsx | 125 + src/app/(app)/journals/actions.ts | 55 + src/app/(app)/journals/new/page.tsx | 34 + src/app/(app)/journals/page.tsx | 98 + src/app/(app)/layout.tsx | 33 + src/app/(app)/payments/actions.ts | 79 + src/app/(app)/payments/new/page.tsx | 22 + src/app/(app)/payments/page.tsx | 129 + src/app/(app)/placements/actions.ts | 61 + src/app/(app)/placements/new/page.tsx | 24 + src/app/(app)/placements/page.tsx | 69 + src/app/(app)/report-cards/actions.ts | 67 + src/app/(app)/report-cards/new/page.tsx | 19 + src/app/(app)/report-cards/page.tsx | 93 + src/app/(app)/settings/page.tsx | 111 + src/app/(app)/students/[id]/edit/page.tsx | 48 + src/app/(app)/students/[id]/page.tsx | 504 ++ src/app/(app)/students/actions.ts | 129 + src/app/(app)/students/new/page.tsx | 18 + src/app/(app)/students/page.tsx | 232 + src/app/api/uploads/route.ts | 35 + src/app/globals.css | 171 + src/app/layout.tsx | 21 + src/app/login/actions.ts | 35 + src/app/login/login-form.tsx | 38 + src/app/login/page.tsx | 57 + src/app/page.tsx | 7 + .../attendance/attendance-sheet.tsx | 99 + .../charts/active-students-chart.tsx | 28 + src/components/charts/attendance-donut.tsx | 43 + src/components/charts/lead-source-bar.tsx | 34 + src/components/classes/class-form.tsx | 170 + src/components/follow-up/status-form.tsx | 34 + src/components/journals/journal-form.tsx | 129 + src/components/layout/sidebar.tsx | 138 + src/components/layout/topbar.tsx | 49 + src/components/payments/payment-form.tsx | 154 + src/components/placements/placement-form.tsx | 113 + src/components/reports/report-form.tsx | 99 + src/components/students/badges.tsx | 56 + src/components/students/student-form.tsx | 167 + src/components/ui/avatar.tsx | 34 + src/components/ui/badge.tsx | 41 + src/components/ui/button.tsx | 34 + src/components/ui/card.tsx | 31 + src/components/ui/empty-state.tsx | 28 + src/components/ui/form.tsx | 57 + src/components/ui/multi-upload.tsx | 76 + src/components/ui/page-header.tsx | 24 + src/components/ui/radio-pills.tsx | 44 + src/components/ui/stat-card.tsx | 39 + src/components/ui/submit-button.tsx | 15 + src/components/ui/upload-input.tsx | 87 + src/lib/auth.ts | 61 + src/lib/constants.ts | 101 + src/lib/prisma.ts | 11 + src/lib/prorata.ts | 74 + src/lib/queries.ts | 106 + src/lib/rbac.ts | 18 + src/lib/utils.ts | 52 + src/lib/validators.ts | 117 + tsconfig.json | 34 + 86 files changed, 13392 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 DESIGN.md create mode 100644 PLAN.md create mode 100644 README.md create mode 100644 eslint.config.mjs create mode 100644 next.config.ts create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 postcss.config.mjs create mode 100644 prisma/schema.prisma create mode 100644 prisma/seed.example.ts create mode 100644 prisma/seed.ts create mode 100644 src/app/(app)/attendance/actions.ts create mode 100644 src/app/(app)/attendance/page.tsx create mode 100644 src/app/(app)/classes/[id]/edit/page.tsx create mode 100644 src/app/(app)/classes/[id]/page.tsx create mode 100644 src/app/(app)/classes/actions.ts create mode 100644 src/app/(app)/classes/new/page.tsx create mode 100644 src/app/(app)/classes/page.tsx create mode 100644 src/app/(app)/dashboard/page.tsx create mode 100644 src/app/(app)/follow-up/actions.ts create mode 100644 src/app/(app)/follow-up/page.tsx create mode 100644 src/app/(app)/journals/actions.ts create mode 100644 src/app/(app)/journals/new/page.tsx create mode 100644 src/app/(app)/journals/page.tsx create mode 100644 src/app/(app)/layout.tsx create mode 100644 src/app/(app)/payments/actions.ts create mode 100644 src/app/(app)/payments/new/page.tsx create mode 100644 src/app/(app)/payments/page.tsx create mode 100644 src/app/(app)/placements/actions.ts create mode 100644 src/app/(app)/placements/new/page.tsx create mode 100644 src/app/(app)/placements/page.tsx create mode 100644 src/app/(app)/report-cards/actions.ts create mode 100644 src/app/(app)/report-cards/new/page.tsx create mode 100644 src/app/(app)/report-cards/page.tsx create mode 100644 src/app/(app)/settings/page.tsx create mode 100644 src/app/(app)/students/[id]/edit/page.tsx create mode 100644 src/app/(app)/students/[id]/page.tsx create mode 100644 src/app/(app)/students/actions.ts create mode 100644 src/app/(app)/students/new/page.tsx create mode 100644 src/app/(app)/students/page.tsx create mode 100644 src/app/api/uploads/route.ts create mode 100644 src/app/globals.css create mode 100644 src/app/layout.tsx create mode 100644 src/app/login/actions.ts create mode 100644 src/app/login/login-form.tsx create mode 100644 src/app/login/page.tsx create mode 100644 src/app/page.tsx create mode 100644 src/components/attendance/attendance-sheet.tsx create mode 100644 src/components/charts/active-students-chart.tsx create mode 100644 src/components/charts/attendance-donut.tsx create mode 100644 src/components/charts/lead-source-bar.tsx create mode 100644 src/components/classes/class-form.tsx create mode 100644 src/components/follow-up/status-form.tsx create mode 100644 src/components/journals/journal-form.tsx create mode 100644 src/components/layout/sidebar.tsx create mode 100644 src/components/layout/topbar.tsx create mode 100644 src/components/payments/payment-form.tsx create mode 100644 src/components/placements/placement-form.tsx create mode 100644 src/components/reports/report-form.tsx create mode 100644 src/components/students/badges.tsx create mode 100644 src/components/students/student-form.tsx create mode 100644 src/components/ui/avatar.tsx create mode 100644 src/components/ui/badge.tsx create mode 100644 src/components/ui/button.tsx create mode 100644 src/components/ui/card.tsx create mode 100644 src/components/ui/empty-state.tsx create mode 100644 src/components/ui/form.tsx create mode 100644 src/components/ui/multi-upload.tsx create mode 100644 src/components/ui/page-header.tsx create mode 100644 src/components/ui/radio-pills.tsx create mode 100644 src/components/ui/stat-card.tsx create mode 100644 src/components/ui/submit-button.tsx create mode 100644 src/components/ui/upload-input.tsx create mode 100644 src/lib/auth.ts create mode 100644 src/lib/constants.ts create mode 100644 src/lib/prisma.ts create mode 100644 src/lib/prorata.ts create mode 100644 src/lib/queries.ts create mode 100644 src/lib/rbac.ts create mode 100644 src/lib/utils.ts create mode 100644 src/lib/validators.ts create mode 100644 tsconfig.json diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..141df84 --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +DATABASE_URL="file:./dev.db" +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" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e2a0d0a --- /dev/null +++ b/.gitignore @@ -0,0 +1,51 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* +!.env.example + +# database (local SQLite) +prisma/*.db +prisma/*.db-journal +*.db +*.db-journal + +# uploaded files +public/uploads/ + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..003d75a --- /dev/null +++ b/AGENTS.md @@ -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 diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..2e925b5 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,458 @@ +# DESIGN.md — Kodeva Design System + +> Panduan visual & UX untuk **Kodeva**. Tema: **Colorful · Ed-Tech · Playful · Rapi (bukan childish)**. +> Bayangkan antarmuka produk SaaS modern untuk akademi coding — ceria, berenergi, tapi tetap kredibel dan enak dipakai kerja 8 jam. + +--- + +## 1. Brand & Personality + +| Atribut | Nilai | +|---------|-------| +| Nama | **Kodeva** | +| Tagline | *"Kelola kelas coding, tumbuhkan pembuat masa depan."* | +| Karakter | Ceria, cerdas, hangat, terpercaya, sedikit jenaka | +| Bukan | Kaku korporat, norak, kekanak-kanakan (tanpa kartun balita / font comic) | +| Metafora | Papan tulis digital yang penuh warna, blok kode berwarna, sticky notes | + +**Referensi rasa:** Linear (kerapian) × Duolingo (keceriaan) × Notion (keruang) × Figma (warna). + +--- + +## 2. Design Principles + +1. **Warna membawa makna, bukan sekadar hiasan.** Tiap warna punya peran semantik (status, sumber lead, kehadiran). Konsisten di seluruh app. +2. **Playful lewat bentuk & gerak, bukan lewat font kekanak-kanakan.** Sudut membulat, gradient halus, blob dekoratif tipis, micro-animation. +3. **Data dulu.** Angka penting (perlu follow up, siswa aktif) langsung terlihat tanpa klik. +4. **Satu layar, satu fokus.** Hierarki jelas: judul → aksi utama → konten → detail. +5. **Hangat & manusiawi.** Foto profil bulat, sapaan personal ("Hai, Sari 👋"), bahasa Indonesia santai-profesional. + +--- + +## 3. Color System + +### 3.1 Brand Palette + +| Token | Hex | Preview | Pemakaian | +|-------|-----|---------|-----------| +| `--brand-50` | `#F2EFFE` | 🟪 | background lembut | +| `--brand-100` | `#E4DEFD` | 🟪 | chip, hover | +| `--brand-300` | `#B9A9FB` | 🟪 | border aktif | +| `--brand-500` | `#7C5CFC` | 🟪 | **primary**, tombol, ikon aktif | +| `--brand-600` | `#6A46EF` | 🟪 | hover primary | +| `--brand-700` | `#5533CE` | 🟪 | pressed / teks di bg terang | + +### 3.2 Accent & Semantic + +| Token | Hex | Makna | +|-------|-----|-------| +| `--coral-500` | `#FF7A59` | aksi sekunder, highlight, CTA promo | +| `--amber-500` | `#FFB020` | warning, `CALON_STUDENT`, banner | +| `--mint-500` | `#12B886` | success, `STUDENT` aktif, hadir | +| `--sky-500` | `#2AA9FF` | info, social media, link | +| `--pink-500` | `#F65FA7` | aksen dekoratif, tag kreatif | +| `--danger-500` | `#F04438` | error, alpa, hapus | +| `--violet-soft` | `#A78BFA` | grafik pelengkap | + +### 3.3 Neutral + +| Token | Hex | Pemakaian | +|-------|-----|-----------| +| `--ink-900` | `#131A2B` | teks utama, heading | +| `--ink-700` | `#3A4560` | teks sekunder | +| `--ink-500` | `#6B7590` | placeholder, caption | +| `--ink-300` | `#C4CAD8` | border | +| `--ink-100` | `#EEF1F7` | divider, bg hover | +| `--surface` | `#FFFFFF` | kartu | +| `--canvas` | `#F7F8FC` | background halaman | +| `--canvas-tint` | linear-gradient(180deg,#F7F8FC 0%,#F1F0FE 100%) | background app | + +### 3.4 Gradient (signature) + +```css +--grad-primary: linear-gradient(135deg, #7C5CFC 0%, #2AA9FF 100%); /* tombol utama, hero */ +--grad-coral: linear-gradient(135deg, #FF7A59 0%, #FFB020 100%); /* kartu highlight */ +--grad-mint: linear-gradient(135deg, #12B886 0%, #2AA9FF 100%); /* stat positif */ +--grad-pink: linear-gradient(135deg, #F65FA7 0%, #7C5CFC 100%); /* dekorasi */ +``` + +Gunakan gradient **hanya** untuk: tombol primary, avatar ring, header kartu statistik, dan 1 blob dekoratif per halaman. Jangan gradient di teks panjang. + +### 3.5 Warna Mapping Semantik (WAJIB konsisten) + +**Status Siswa** +| Status | Warna | Badge | +|--------|-------|-------| +| `CALON_STUDENT` | Amber `#FFB020` | bg `#FFF4E0`, teks `#9A6100` | +| `STUDENT` | Mint `#12B886` | bg `#E4F8F1`, teks `#0A7A58` | +| `EX_STUDENT` | Slate `#6B7590` | bg `#EEF1F7`, teks `#4A5470` | + +**Sumber Lead** (dipakai di bar chart & chip) +| Sumber | Warna | Ikon | +|--------|-------|------| +| `WHATSAPP` | `#22C55E` | `MessageCircle` | +| `REFERRAL` | `#7C5CFC` | `Users` | +| `BANNER` | `#FFB020` | `Flag` | +| `SOCIAL_MEDIA` | `#2AA9FF` | `Instagram` | + +**Kehadiran** +| Status | Warna | Label | +|--------|-------|-------| +| `PRESENT` | Mint `#12B886` | Hadir | +| `LATE` | Amber `#FFB020` | Terlambat | +| `EXCUSED` | Sky `#2AA9FF` | Izin | +| `SICK` | Violet `#A78BFA` | Sakit | +| `ABSENT` | Danger `#F04438` | Alpa | + +**Follow Up** : NEW `#94A3B8` · CONTACTED `#2AA9FF` · TRIAL `#7C5CFC` · OFFER_SENT `#FFB020` · ENROLLED `#12B886` · LOST `#F04438` + +**Metode Bayar** : CASH `#12B886` · TRANSFER `#2AA9FF` · QRIS `#7C5CFC` + +--- + +## 4. Typography + +| Peran | Font | Weight | Size | +|-------|------|--------|------| +| Display / H1 | **Plus Jakarta Sans** | 800 | 30–36px, tracking `-0.02em` | +| H2 | Plus Jakarta Sans | 700 | 24px | +| H3 / Card title | Plus Jakarta Sans | 600 | 18px | +| Body | **Inter** | 400/500 | 14–15px, line-height 1.6 | +| Caption / label | Inter | 500 | 12–13px, tracking `0.01em` | +| Angka dashboard | **Sora** | 700 | 28–32px, tabular-nums | +| Kode / invoice | **JetBrains Mono** | 500 | 13px | + +``` +font-display : "Plus Jakarta Sans", Inter, system-ui, sans-serif +font-body : "Inter", system-ui, sans-serif +font-num : "Sora", Inter, sans-serif +font-mono : "JetBrains Mono", ui-monospace, monospace +``` + +Aturan: heading **selalu** gelap (`--ink-900`), jangan pakai warna brand untuk heading panjang. Warna brand untuk **aksen kata** (mis. "3 siswa **perlu follow up**"). + +--- + +## 5. Spacing · Radius · Shadow + +- **Spacing scale:** 4 · 8 · 12 · 16 · 20 · 24 · 32 · 40 · 48 · 64 (kelipatan 4). +- **Radius:** `sm 8px` (input kecil) · `md 12px` (button, input) · `lg 16px` (dropdown) · `xl 20px` (card) · `2xl 24px` (panel besar) · `full` (pill, avatar). +- **Shadow:** +```css +--shadow-xs: 0 1px 2px rgba(19,26,43,.06); +--shadow-sm: 0 2px 8px rgba(19,26,43,.06); +--shadow-md: 0 8px 24px rgba(19,26,43,.08); +--shadow-lg: 0 16px 40px rgba(19,26,43,.10); +--shadow-brand: 0 8px 20px rgba(124,92,252,.28); /* tombol primary */ +``` +- **Border:** 1px `--ink-100`; kartu aktif 1.5px `--brand-300`. +- **Focus ring:** `0 0 0 3px rgba(124,92,252,.28)`. + +--- + +## 6. Iconography & Illustration + +- **Ikon:** `lucide-react`, stroke 2, ukuran 18/20/24. Selalu dalam container bulat saat jadi aksi. +- **Ilustrasi/dekorasi:** bentuk blob, lingkaran, blok kode melayang — **opacity 6–12%**, gradient brand. Maksimal 1–2 elemen per halaman, jangan menghalangi konten. +- **Avatar:** bulat penuh, ukuran 32/40/56/96px, ring gradient `--grad-primary` untuk siswa aktif. +- **Emoji:** boleh untuk sapaan & empty state yang ramah, maksimal 1 per layar. Jangan di tombol/heading formal. + +--- + +## 7. Layout & Navigasi + +### 7.1 Shell Desktop (≥1280px) — 3 kolom + +``` +┌──────────────┬────────────────────────────────────────────┬───────────────────────┐ +│ LEFT NAV │ MAIN CONTENT │ RIGHT RAIL │ +│ 248px │ flex-1 (min 640px) │ 340px (sticky) │ +│ │ │ │ +│ ✦ Kodeva │ Topbar: breadcrumb · search · +Aksi · 🔔 │ ┌─────────────────┐ │ +│ │ │ │ Siswa Aktif 128│ │ +│ ▸ Dashboard │ ┌──────────────────────────────────────┐ │ ├─────────────────┤ │ +│ ▸ Follow Up │ │ Konten halaman / tabel / kanban │ │ │ Perlu Follow 12 │ │ +│ (12) 🔴 │ │ │ │ ├─────────────────┤ │ +│ ▸ Siswa │ │ │ │ │ Ex Student 7 │ │ +│ ▸ Kelas │ │ │ │ └─────────────────┘ │ +│ ▸ Kehadiran │ └──────────────────────────────────────┘ │ ┌─────────────────┐ │ +│ ▸ Pembayaran│ │ │ Kehadiran (donut)│ │ +│ ▸ Jurnal │ │ └─────────────────┘ │ +│ ▸ Raport │ │ ┌─────────────────┐ │ +│ ▸ Placement │ │ │ Siswa Aktif (line)│ │ +│ ▸ Pengaturan│ │ └─────────────────┘ │ +│ │ │ ┌─────────────────┐ │ +│ 👤 Sari W. │ │ │ Sumber Lead (bar)│ │ +│ Admin │ │ └─────────────────┘ │ +└──────────────┴────────────────────────────────────────────┴───────────────────────┘ +``` + +- **Left nav:** background `--surface`, lebar 248px, item aktif = pill gradient brand dengan teks putih + `--shadow-brand`. Badge angka merah untuk follow up due. Dapat collapse jadi 72px (ikon saja). +- **Right rail ("Insight Panel"):** hanya muncul penuh di **Dashboard**; di halaman lain bisa dibuka lewat tombol toggle (`PanelRight`), default tersembunyi agar konten lega. Sticky, scroll independen, tiap widget = kartu `radius xl`. +- **Topbar:** tinggi 64px, sticky, blur backdrop `rgba(247,248,252,.8)`. + +### 7.2 Breakpoint + +| Breakpoint | Perilaku | +|-----------|----------| +| `≥1440px` | 3 kolom penuh, rail 340px | +| `1280–1439px` | 3 kolom, rail 300px | +| `1024–1279px` | rail jadi drawer kanan (default tutup) | +| `768–1023px` | left nav collapse ikon; rail drawer | +| `<768px` | bottom nav 5 item utama; tabel → kartu; rail = sheet | + +### 7.3 Bottom Nav (mobile) +`Dashboard · Siswa · Follow Up · Kelas · Lainnya` + +--- + +## 8. Komponen + +### 8.1 Tombol +| Varian | Style | +|--------|-------| +| Primary | `--grad-primary`, teks putih, radius md, `--shadow-brand`, hover naik 1px | +| Secondary | bg `--brand-50`, teks `--brand-700` | +| Outline | border `--ink-300`, teks `--ink-900` | +| Ghost | transparan, hover `--ink-100` | +| Danger | bg `#F04438`, teks putih | +| Pill/Chip | radius full, padding 6×12 | + +Ukuran: `sm 32px` · `md 40px` · `lg 48px`. Sertakan ikon kiri untuk aksi utama. + +### 8.2 Kartu +- **Stat Card:** ikon bulat gradient + label caption + angka besar (`font-num`) + delta (`+8%` mint / `-3%` coral). Tinggi 112px. +- **Content Card:** header (judul + aksi "Lihat semua") + body + padding 20–24px. +- **Student Card:** avatar + nama + badge status + chip sumber + info kelas; hover shadow-md, cursor pointer. +- **Class Card:** strip warna kelas di kiri, nama, level chip, guru, `6/12 siswa` progress bar, jadwal. + +### 8.3 Form +- Label di atas, 13px `--ink-700`, wajib ada `*` coral untuk required. +- Input tinggi 40px, radius md, border `--ink-300`, fokus ring brand. +- **Upload foto profil:** dropzone lingkaran dengan preview + tombol kamera; maks 5MB, JPG/PNG/WebP. +- Pilihan sumber lead & status: **segmented control** berikon (bukan select biasa) agar playful. +- Validasi inline: border danger + pesan 12px di bawah field. + +### 8.4 Badge & Chip +- Status siswa pakai badge pill (dot + label). +- Sumber lead pakai chip berikon dengan warna mapping §3.5. +- Level kelas: Beginner (mint), Intermediate (sky), Advanced (violet). + +### 8.5 Tabel +Header `--ink-100`, teks uppercase 12px `--ink-500`. Baris hover `--brand-50/50`, tinggi 56px. Kolom aksi sticky kanan. Di mobile berubah jadi kartu bertumpuk. + +### 8.6 Kanban Follow Up +Kolom berisi header warna status + count + total value; kartu bisa drag (dnd-kit), sudut membulat, tampilkan avatar, sumber, "due 2 hari lalu" merah. + +### 8.7 Empty · Loading · Error +- **Empty:** ilustrasi blob + judul ramah ("Belum ada calon siswa nih") + CTA. +- **Loading:** skeleton shimmer bentuk sama dengan konten, bukan spinner penuh layar. +- **Error:** kartu coral lembut + pesan jelas + tombol "Coba lagi". + +--- + +## 9. Charts & Data Viz + +Semua chart pakai **Recharts**, tinggi 180–220px di rail, kartu putih, grid horizontal tipis `--ink-100`, tanpa garis sumbu Y penuh. + +| Chart | Tipe | Warna | Isi | +|-------|------|-------|-----| +| **Siswa Aktif** | Area/donut mini | `--grad-primary` | Tren 6 bulan; angka besar di tengah | +| **Kehadiran** | Donut / radial | PRESENT mint, LATE amber, ABSENT danger | % kehadiran hari ini + ring | +| **Sumber Lead** | Bar horizontal | mapping §3.5 | Jumlah per WhatsApp/Referral/Banner/Social | +| **Siswa per Kelas** | Bar vertikal | `--violet-soft` | Kapasitas terpakai | +| **Pendapatan** | Line/area | `--mint-500` | Opsional di halaman pembayaran | + +Aturan chart: +- Tooltip rounded, shadow-md, label Bahasa Indonesia. +- Angka `font-num`, animasi masuk 400ms ease-out. +- Selalu sediakan legend berwarna + label teks (jangan andalkan warna saja). +- Bar chart sumber lead: tampilkan angka di ujung bar. + +### 9.1 Right Rail Widgets (Dashboard) +1. **Stat stack:** Siswa Aktif · Perlu Follow Up · Ex Student (3 kartu mini). +2. **Kehadiran Hari Ini:** donut + `86%` + rincian Hadir/Terlambat/Alpa. +3. **Tren Siswa Aktif:** area chart 6 bulan. +4. **Sumber Lead:** bar chart 4 kategori. +5. **Perlu Follow Up Hari Ini:** list 4 nama + tombol WhatsApp; link "Lihat semua". +6. **Kelas Hari Ini:** sesi + jam + guru. + +--- + +## 10. Spesifikasi Halaman + +### 10.1 Dashboard +Hero sapaan + 4 stat card besar (Siswa Aktif, Perlu Follow Up, Ex Student, Kehadiran %). Bawahnya: grafik Tren Siswa Aktif & Sumber Lead (2 kolom). Right rail = widget §9.1. + +### 10.2 Halaman Siswa (list) +- Filter segmented: **Semua · Calon Student · Student · Ex Student**. +- Search nama/email/telepon, filter sumber lead, filter kelas. +- Toggle tampilan **tabel ⇄ kartu**. +- Tombol "+ Tambah Siswa" primary. +- Kolom: Siswa (avatar+nama+email) · Telepon · Orang Tua · Kelas · Sumber · Status · Aksi. + +### 10.3 Form Tambah/Edit Siswa +Grid 2 kolom: +- Kiri: Foto profil (dropzone), Nama lengkap*, No. telepon*, Email, Nama orang tua, No. telepon orang tua, Tanggal lahir, Gender. +- Kanan: **Tahu dari mana*** (segmented: WhatsApp/Referral/Banner/Social Media), **Status*** (segmented: Calon/Student/Ex), **Pilih Kelas** (multi-select, hanya aktif bila status Student), Catatan. +- Sticky footer: Batal · Simpan. + +### 10.4 Detail Siswa +Header profil: avatar besar ring gradient, nama, badge status, chip sumber, tombol WhatsApp/Call/Edit. Tab: **Ringkasan · Kelas & Kehadiran · Pembayaran · Placement Test · Raport · Jurnal**. +- **Placement Test** selalu tampil sebagai kartu: level hasil, skor per kategori (bar), rekomendasi kelas, tanggal, pemeriksa. +- **Pembayaran**: ringkasan prorata — `Kuota 10 sesi · Terpakai 6 · Sisa 4 · Nilai sisa Rp 800.000`. + +### 10.5 Kelas (CRUD) +Grid kartu kelas dengan strip warna + progress kapasitas. Form: nama, kode, level, guru, total pertemuan, harga paket, kapasitas, jadwal, warna, status. Preview `Rp per pertemuan` otomatis saat mengisi harga & jumlah pertemuan. + +### 10.6 Kehadiran +Pilih kelas → pilih pertemuan → daftar siswa dengan tombol status (segmented H/T/I/S/A) + tombol "Hadir Semua". Progress "12/12 terisi". Simpan. + +### 10.7 Pembayaran +Form: pilih siswa → pilih kelas (auto ambil harga) → **jumlah pertemuan dibayar** (stepper) → tampil kalkulasi prorata besar: `10 × Rp 200.000 = Rp 2.000.000` → metode (Cash/Transfer/QRIS segmented) → tanggal → upload bukti (opsional). List pembayaran: invoice mono, siswa, kelas, sesi, nominal, metode chip, status. + +### 10.8 Jurnal Aktivitas +Feed masonry/card: cover foto, judul, kelas, tanggal, tag, author, jumlah siswa. Tombol "+ Buat Jurnal": upload multi-foto (drag reorder), judul, deskripsi, kelas, sesi, tag, tandai siswa yang terlibat. + +### 10.9 Raport +Pilih kelas → pilih siswa → form aspek penilaian (slider/input 0–100): Logika, Computational Thinking, Problem Solving, Kreativitas, Kolaborasi → kesimpulan guru, grade otomatis, ringkasan kehadiran. Preview raport bertema (header gradient, tanda tangan guru). + +### 10.10 Placement Test +Form: siswa, tanggal, pemeriksa, skor kategori (bar input), level hasil otomatis dari rata-rata + rekomendasi kelas. Riwayat list. + +### 10.11 Pengaturan +Sumber lead (label + warna + aktif), daftar guru/user, info akademi (nama, logo, alamat, kontak), preferensi (mata uang, timezone). + +--- + +## 11. Motion + +| Aksi | Durasi | Easing | +|------|--------|--------| +| Hover kartu/tombol | 150ms | ease-out | +| Masuk halaman (fade+up 8px) | 250ms | cubic-bezier(.2,.8,.2,1) | +| Chart render | 400ms | ease-out | +| Drawer/sheet | 240ms | cubic-bezier(.2,.8,.2,1) | +| Konfeti saat siswa enroll 🎉 | 800ms | sekali, opsional | + +Hormati `prefers-reduced-motion`: matikan animasi non-esensial. + +--- + +## 12. Aksesibilitas + +- Kontras teks minimal **AA** (4.5:1 body, 3:1 besar). Jangan taruh teks putih di amber/kuning. +- Fokus terlihat di semua elemen interaktif (ring brand). +- Jangan sampaikan status **hanya** lewat warna — selalu ada label/ikon. +- Semua input punya `