feat: implementasi Kodeva - sistem manajemen kursus coding

Fitur lengkap:
- Auth JWT + RBAC (Admin/Teacher) + halaman login
- Shell 3 kolom (left nav, topbar, right rail dashboard)
- CRUD siswa + follow up kanban + status calon/student/ex
- Dashboard: siswa aktif, kehadiran, bar sumber lead, perlu follow up
- Pembayaran prorata per pertemuan (cash/transfer/qris)
- Kelas, enrollment, generate sesi, kehadiran bulk
- Jurnal aktivitas + upload multi-foto
- Raport + placement test (tampil di detail siswa)
- Pengaturan & seed data contoh

Stack: Next.js 16, TypeScript, Tailwind v4, Prisma (SQLite), Recharts, Zod
This commit is contained in:
Amadea
2026-09-18 19:24:59 +07:00
commit 61ce9d7926
86 changed files with 13392 additions and 0 deletions
+281
View File
@@ -0,0 +1,281 @@
// Kodeva — Prisma schema
// DEV: SQLite (tanpa Docker/Postgres). Untuk PRODUKSI ganti provider ke "postgresql"
// dan ubah String enum/list kembali menjadi enum + String[] (lihat AGENTS.md §6).
//
// Nilai enum disimpan sebagai String agar kompatibel SQLite. Konstanta TypeScript
// tersedia di src/lib/constants.ts.
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
name String
email String @unique
passwordHash String
role String @default("TEACHER") // ADMIN | 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 String? // MALE | FEMALE
address String?
leadSource String // WHATSAPP | REFERRAL | BANNER | SOCIAL_MEDIA
status String @default("CALON_STUDENT") // CALON_STUDENT | STUDENT | EX_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[]
@@index([status])
@@index([leadSource])
}
model Class {
id String @id @default(cuid())
name String
code String @unique
description String?
level String // BEGINNER | INTERMEDIATE | ADVANCED
teacherId String?
teacher User? @relation("ClassTeacher", fields: [teacherId], references: [id])
totalMeetings Int @default(10)
packagePrice Int @default(0)
pricePerMeeting Int?
capacity Int @default(12)
schedule String?
startDate DateTime?
endDate DateTime?
status String @default("ACTIVE") // DRAFT | ACTIVE | COMPLETED | ARCHIVED
color String?
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 String @default("ACTIVE") // ACTIVE | COMPLETED | DROPPED
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 String // PRESENT | LATE | EXCUSED | SICK | ABSENT
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
method String // CASH | TRANSFER | QRIS
status String @default("PAID") // UNPAID | PARTIAL | PAID | REFUNDED
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 @default("") // comma separated
authorId String
author User @relation(fields: [authorId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
photos JournalPhoto[]
students JournalStudent[]
}
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
teacherId String
teacher User @relation(fields: [teacherId], references: [id])
attendanceSummary String?
finalScore Int?
grade String?
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
score Int
note String?
}
model PlacementTest {
id String @id @default(cuid())
studentId String
student Student @relation(fields: [studentId], references: [id], onDelete: Cascade)
testDate DateTime
levelResult String // BEGINNER | INTERMEDIATE | ADVANCED
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
score Int
}
model FollowUp {
id String @id @default(cuid())
studentId String
student Student @relation(fields: [studentId], references: [id], onDelete: Cascade)
status String @default("NEW") // NEW | CONTACTED | TRIAL | OFFER_SENT | ENROLLED | LOST
picId String?
pic User? @relation("FollowUpPIC", fields: [picId], references: [id])
nextFollowUpAt DateTime?
lastContactedAt DateTime?
channel String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
activities FollowUpActivity[]
@@index([status])
@@index([nextFollowUpAt])
}
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 String // JSON string (SQLite)
}