feat: implementasi Kodeva - sistem manajemen kursus coding
Fitur lengkap: - Auth JWT + RBAC (Admin/Teacher) + halaman login - Shell 3 kolom (left nav, topbar, right rail dashboard) - CRUD siswa + follow up kanban + status calon/student/ex - Dashboard: siswa aktif, kehadiran, bar sumber lead, perlu follow up - Pembayaran prorata per pertemuan (cash/transfer/qris) - Kelas, enrollment, generate sesi, kehadiran bulk - Jurnal aktivitas + upload multi-foto - Raport + placement test (tampil di detail siswa) - Pengaturan & seed data contoh Stack: Next.js 16, TypeScript, Tailwind v4, Prisma (SQLite), Recharts, Zod
This commit is contained in:
@@ -0,0 +1,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)
|
||||
}
|
||||
Reference in New Issue
Block a user