609 lines
19 KiB
Text
609 lines
19 KiB
Text
// This is your Prisma schema file,
|
|
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
|
|
|
generator client {
|
|
provider = "prisma-client-js"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
url = env("DATABASE_URL")
|
|
}
|
|
|
|
// ============================================
|
|
// User Management
|
|
// ============================================
|
|
|
|
model Role {
|
|
id Int @id @default(autoincrement())
|
|
code String @unique @db.VarChar(50)
|
|
name Json // { th: "", en: "" }
|
|
description Json?
|
|
created_at DateTime @default(now())
|
|
|
|
users User[]
|
|
|
|
@@index([code])
|
|
@@map("roles")
|
|
}
|
|
|
|
model User {
|
|
id Int @id @default(autoincrement())
|
|
username String @unique @db.VarChar(50)
|
|
email String @unique @db.VarChar(255)
|
|
password String @db.VarChar(255)
|
|
role_id Int
|
|
email_verified_at DateTime?
|
|
is_deactivated Boolean @default(false)
|
|
created_at DateTime @default(now())
|
|
updated_at DateTime? @updatedAt
|
|
|
|
role Role @relation(fields: [role_id], references: [id], onDelete: Restrict)
|
|
profile UserProfile?
|
|
|
|
// Relations
|
|
created_courses Course[] @relation("CourseCreator")
|
|
approved_courses Course[] @relation("CourseApprover")
|
|
instructor_courses CourseInstructor[]
|
|
enrollments Enrollment[]
|
|
lesson_progress LessonProgress[]
|
|
quiz_attempts QuizAttempt[]
|
|
certificates Certificate[]
|
|
created_categories Category[] @relation("CategoryCreator")
|
|
updated_categories Category[] @relation("CategoryUpdater")
|
|
updated_profiles UserProfile[] @relation("ProfileUpdater")
|
|
created_quizzes Quiz[] @relation("QuizCreator")
|
|
updated_quizzes Quiz[] @relation("QuizUpdater")
|
|
created_announcements Announcement[] @relation("AnnouncementCreator")
|
|
updated_announcements Announcement[] @relation("AnnouncementUpdater")
|
|
updated_courses Course[] @relation("CourseUpdater")
|
|
orders Order[]
|
|
instructor_balance InstructorBalance?
|
|
withdrawal_requests WithdrawalRequest[] @relation("WithdrawalInstructor")
|
|
approved_withdrawals WithdrawalRequest[] @relation("WithdrawalApprover")
|
|
updated_withdrawals WithdrawalRequest[] @relation("WithdrawalUpdater")
|
|
submitted_approvals CourseApproval[] @relation("ApprovalSubmitter")
|
|
reviewed_approvals CourseApproval[] @relation("ApprovalReviewer")
|
|
|
|
@@index([username])
|
|
@@index([email])
|
|
@@index([role_id])
|
|
@@map("users")
|
|
}
|
|
|
|
model UserProfile {
|
|
id Int @id @default(autoincrement())
|
|
user_id Int @unique
|
|
prefix Json? // { th: "นาย", en: "Mr." }
|
|
first_name String @db.VarChar(100)
|
|
last_name String @db.VarChar(100)
|
|
phone String? @db.VarChar(20)
|
|
avatar_url String? @db.VarChar(500)
|
|
birth_date DateTime?
|
|
created_at DateTime @default(now())
|
|
updated_at DateTime? @updatedAt
|
|
updated_by Int?
|
|
|
|
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
|
updater User? @relation("ProfileUpdater", fields: [updated_by], references: [id])
|
|
|
|
@@index([user_id])
|
|
@@map("user_profiles")
|
|
}
|
|
|
|
model Category {
|
|
id Int @id @default(autoincrement())
|
|
name Json // { th: "", en: "" }
|
|
slug String @unique @db.VarChar(100)
|
|
description Json?
|
|
icon String? @db.VarChar(100)
|
|
sort_order Int @default(0)
|
|
is_active Boolean @default(true)
|
|
created_at DateTime @default(now())
|
|
created_by Int
|
|
updated_at DateTime? @updatedAt
|
|
updated_by Int?
|
|
|
|
courses Course[]
|
|
creator User @relation("CategoryCreator", fields: [created_by], references: [id], onDelete: Restrict)
|
|
updater User? @relation("CategoryUpdater", fields: [updated_by], references: [id])
|
|
|
|
@@index([slug])
|
|
@@index([is_active])
|
|
@@index([sort_order])
|
|
@@map("categories")
|
|
}
|
|
|
|
// ============================================
|
|
// Course Structure
|
|
// ============================================
|
|
|
|
enum CourseStatus {
|
|
DRAFT
|
|
PENDING
|
|
APPROVED
|
|
REJECTED
|
|
ARCHIVED
|
|
}
|
|
|
|
model Course {
|
|
id Int @id @default(autoincrement())
|
|
category_id Int?
|
|
title Json // { th: "", en: "" }
|
|
slug String @unique @db.VarChar(200)
|
|
description Json
|
|
thumbnail_url String? @db.VarChar(500)
|
|
price Decimal @default(0) @db.Decimal(10, 2)
|
|
is_free Boolean @default(false)
|
|
have_certificate Boolean @default(false)
|
|
status CourseStatus @default(DRAFT)
|
|
approved_by Int?
|
|
approved_at DateTime?
|
|
rejection_reason String? @db.Text
|
|
created_at DateTime @default(now())
|
|
created_by Int
|
|
updated_at DateTime? @updatedAt
|
|
updated_by Int?
|
|
|
|
category Category? @relation(fields: [category_id], references: [id], onDelete: SetNull)
|
|
creator User @relation("CourseCreator", fields: [created_by], references: [id], onDelete: Restrict)
|
|
approver User? @relation("CourseApprover", fields: [approved_by], references: [id])
|
|
updater User? @relation("CourseUpdater", fields: [updated_by], references: [id])
|
|
chapters Chapter[]
|
|
instructors CourseInstructor[]
|
|
enrollments Enrollment[]
|
|
announcements Announcement[]
|
|
courseApprovals CourseApproval[]
|
|
|
|
@@index([category_id])
|
|
@@index([slug])
|
|
@@index([status])
|
|
@@index([created_by])
|
|
@@index([status, is_free])
|
|
@@index([category_id, status])
|
|
@@map("courses")
|
|
}
|
|
|
|
model CourseInstructor {
|
|
id Int @id @default(autoincrement())
|
|
course_id Int
|
|
user_id Int
|
|
is_primary Boolean @default(false)
|
|
joined_at DateTime @default(now())
|
|
|
|
course Course @relation(fields: [course_id], references: [id], onDelete: Cascade)
|
|
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([course_id, user_id])
|
|
@@index([course_id])
|
|
@@index([user_id])
|
|
@@index([is_primary])
|
|
@@map("course_instructors")
|
|
}
|
|
|
|
enum ApprovalAction {
|
|
SUBMITTED
|
|
APPROVED
|
|
REJECTED
|
|
}
|
|
|
|
model CourseApproval {
|
|
id Int @id @default(autoincrement())
|
|
course_id Int
|
|
submitted_by Int
|
|
reviewed_by Int?
|
|
action ApprovalAction @default(SUBMITTED)
|
|
previous_status CourseStatus?
|
|
new_status CourseStatus?
|
|
comment String? @db.Text
|
|
created_at DateTime @default(now())
|
|
|
|
course Course @relation(fields: [course_id], references: [id], onDelete: Cascade)
|
|
submitter User @relation("ApprovalSubmitter", fields: [submitted_by], references: [id], onDelete: Restrict)
|
|
reviewer User? @relation("ApprovalReviewer", fields: [reviewed_by], references: [id])
|
|
|
|
@@index([course_id])
|
|
@@index([submitted_by])
|
|
@@index([reviewed_by])
|
|
@@index([action])
|
|
@@index([created_at])
|
|
@@index([course_id, created_at])
|
|
@@map("course_approvals")
|
|
}
|
|
|
|
model Chapter {
|
|
id Int @id @default(autoincrement())
|
|
course_id Int
|
|
title Json // { th: "", en: "" }
|
|
description Json?
|
|
sort_order Int @default(0)
|
|
is_published Boolean @default(true)
|
|
created_at DateTime @default(now())
|
|
updated_at DateTime? @updatedAt
|
|
|
|
course Course @relation(fields: [course_id], references: [id], onDelete: Cascade)
|
|
lessons Lesson[]
|
|
|
|
@@index([course_id])
|
|
@@index([course_id, sort_order])
|
|
@@map("chapters")
|
|
}
|
|
|
|
// ============================================
|
|
// Lessons
|
|
// ============================================
|
|
|
|
enum LessonType {
|
|
VIDEO
|
|
QUIZ
|
|
}
|
|
|
|
model Lesson {
|
|
id Int @id @default(autoincrement())
|
|
chapter_id Int
|
|
title Json // { th: "", en: "" }
|
|
content Json? // multi-language lesson content
|
|
type LessonType
|
|
duration_minutes Int?
|
|
sort_order Int @default(0)
|
|
is_sequential Boolean @default(true)
|
|
prerequisite_lesson_ids Json? // array of lesson IDs
|
|
require_pass_quiz Boolean @default(false)
|
|
is_published Boolean @default(true)
|
|
created_at DateTime @default(now())
|
|
updated_at DateTime? @updatedAt
|
|
|
|
chapter Chapter @relation(fields: [chapter_id], references: [id], onDelete: Cascade)
|
|
attachments LessonAttachment[]
|
|
quiz Quiz?
|
|
progress LessonProgress[]
|
|
|
|
@@index([chapter_id])
|
|
@@index([chapter_id, sort_order])
|
|
@@index([type])
|
|
@@map("lessons")
|
|
}
|
|
|
|
model LessonAttachment {
|
|
id Int @id @default(autoincrement())
|
|
lesson_id Int
|
|
file_name String @db.VarChar(255)
|
|
file_path String @db.VarChar(500)
|
|
file_size Int
|
|
mime_type String @db.VarChar(100)
|
|
description Json?
|
|
sort_order Int @default(0)
|
|
created_at DateTime @default(now())
|
|
|
|
lesson Lesson @relation(fields: [lesson_id], references: [id], onDelete: Cascade)
|
|
|
|
@@index([lesson_id])
|
|
@@index([lesson_id, sort_order])
|
|
@@map("lesson_attachments")
|
|
}
|
|
|
|
// ============================================
|
|
// Quiz System
|
|
// ============================================
|
|
|
|
enum QuestionType {
|
|
MULTIPLE_CHOICE
|
|
TRUE_FALSE
|
|
SHORT_ANSWER
|
|
}
|
|
|
|
model Quiz {
|
|
id Int @id @default(autoincrement())
|
|
lesson_id Int @unique
|
|
title Json // { th: "", en: "" }
|
|
description Json?
|
|
passing_score Int @default(60)
|
|
time_limit Int? // in minutes
|
|
shuffle_questions Boolean @default(false)
|
|
shuffle_choices Boolean @default(false)
|
|
show_answers_after_completion Boolean @default(true)
|
|
created_at DateTime @default(now())
|
|
created_by Int
|
|
updated_at DateTime? @updatedAt
|
|
updated_by Int?
|
|
|
|
lesson Lesson @relation(fields: [lesson_id], references: [id], onDelete: Cascade)
|
|
creator User @relation("QuizCreator", fields: [created_by], references: [id], onDelete: Restrict)
|
|
updater User? @relation("QuizUpdater", fields: [updated_by], references: [id])
|
|
questions Question[]
|
|
attempts QuizAttempt[]
|
|
|
|
@@index([lesson_id])
|
|
@@map("quizzes")
|
|
}
|
|
|
|
model Question {
|
|
id Int @id @default(autoincrement())
|
|
quiz_id Int
|
|
question Json // { th: "", en: "" }
|
|
explanation Json? // answer explanation
|
|
question_type QuestionType @default(MULTIPLE_CHOICE)
|
|
score Int @default(1)
|
|
sort_order Int @default(0)
|
|
created_at DateTime @default(now())
|
|
updated_at DateTime? @updatedAt
|
|
|
|
quiz Quiz @relation(fields: [quiz_id], references: [id], onDelete: Cascade)
|
|
choices Choice[]
|
|
|
|
@@index([quiz_id])
|
|
@@index([quiz_id, sort_order])
|
|
@@map("questions")
|
|
}
|
|
|
|
model Choice {
|
|
id Int @id @default(autoincrement())
|
|
question_id Int
|
|
text Json // { th: "", en: "" }
|
|
is_correct Boolean @default(false)
|
|
sort_order Int @default(0)
|
|
|
|
question Question @relation(fields: [question_id], references: [id], onDelete: Cascade)
|
|
|
|
@@index([question_id])
|
|
@@map("choices")
|
|
}
|
|
|
|
// ============================================
|
|
// Student Progress
|
|
// ============================================
|
|
|
|
enum EnrollmentStatus {
|
|
ENROLLED
|
|
IN_PROGRESS
|
|
COMPLETED
|
|
DROPPED
|
|
}
|
|
|
|
model Enrollment {
|
|
id Int @id @default(autoincrement())
|
|
user_id Int
|
|
course_id Int
|
|
status EnrollmentStatus @default(ENROLLED)
|
|
progress_percentage Int @default(0)
|
|
enrolled_at DateTime @default(now())
|
|
started_at DateTime?
|
|
completed_at DateTime?
|
|
last_accessed_at DateTime?
|
|
|
|
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
|
course Course @relation(fields: [course_id], references: [id], onDelete: Cascade)
|
|
certificates Certificate[]
|
|
|
|
@@unique([user_id, course_id], name: "unique_enrollment")
|
|
@@index([user_id])
|
|
@@index([course_id])
|
|
@@index([status])
|
|
@@index([last_accessed_at])
|
|
@@map("enrollments")
|
|
}
|
|
|
|
model Certificate {
|
|
id Int @id @default(autoincrement())
|
|
user_id Int
|
|
course_id Int
|
|
enrollment_id Int @unique
|
|
file_path String @db.VarChar(500)
|
|
issued_at DateTime @default(now())
|
|
|
|
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
|
enrollment Enrollment @relation(fields: [enrollment_id], references: [id], onDelete: Cascade)
|
|
|
|
@@index([user_id])
|
|
@@index([course_id])
|
|
@@index([enrollment_id])
|
|
@@index([user_id, course_id])
|
|
@@map("certificates")
|
|
}
|
|
|
|
model LessonProgress {
|
|
id Int @id @default(autoincrement())
|
|
user_id Int
|
|
lesson_id Int
|
|
is_completed Boolean @default(false)
|
|
completed_at DateTime?
|
|
video_progress_seconds Int @default(0)
|
|
video_duration_seconds Int?
|
|
video_progress_percentage Decimal? @db.Decimal(5, 2)
|
|
last_watched_at DateTime?
|
|
created_at DateTime @default(now())
|
|
updated_at DateTime? @updatedAt
|
|
|
|
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
|
lesson Lesson @relation(fields: [lesson_id], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([user_id, lesson_id])
|
|
@@index([user_id])
|
|
@@index([lesson_id])
|
|
@@index([last_watched_at])
|
|
@@map("lesson_progress")
|
|
}
|
|
|
|
model QuizAttempt {
|
|
id Int @id @default(autoincrement())
|
|
user_id Int
|
|
quiz_id Int
|
|
score Int @default(0)
|
|
total_questions Int
|
|
correct_answers Int @default(0)
|
|
is_passed Boolean @default(false)
|
|
attempt_number Int @default(1)
|
|
answers Json? // student answers for review
|
|
started_at DateTime @default(now())
|
|
completed_at DateTime?
|
|
|
|
quiz Quiz @relation(fields: [quiz_id], references: [id], onDelete: Cascade)
|
|
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
|
|
|
@@index([user_id])
|
|
@@index([quiz_id])
|
|
@@index([user_id, quiz_id])
|
|
@@index([user_id, quiz_id, attempt_number])
|
|
@@map("quiz_attempts")
|
|
}
|
|
|
|
// ============================================
|
|
// Communication
|
|
// ============================================
|
|
|
|
enum AnnouncementStatus {
|
|
DRAFT
|
|
PUBLISHED
|
|
ARCHIVED
|
|
}
|
|
|
|
model Announcement {
|
|
id Int @id @default(autoincrement())
|
|
course_id Int
|
|
title Json // { th: "", en: "" }
|
|
content Json // { th: "", en: "" }
|
|
status AnnouncementStatus @default(DRAFT)
|
|
is_pinned Boolean @default(false)
|
|
published_at DateTime?
|
|
created_at DateTime @default(now())
|
|
created_by Int
|
|
updated_at DateTime? @updatedAt
|
|
updated_by Int?
|
|
|
|
course Course @relation(fields: [course_id], references: [id], onDelete: Cascade)
|
|
creator User @relation("AnnouncementCreator", fields: [created_by], references: [id], onDelete: Restrict)
|
|
updater User? @relation("AnnouncementUpdater", fields: [updated_by], references: [id])
|
|
attachments AnnouncementAttachment[]
|
|
|
|
@@index([course_id])
|
|
@@index([created_by])
|
|
@@index([status])
|
|
@@index([course_id, status, is_pinned, published_at])
|
|
@@map("announcements")
|
|
}
|
|
|
|
model AnnouncementAttachment {
|
|
id Int @id @default(autoincrement())
|
|
announcement_id Int
|
|
file_name String @db.VarChar(255)
|
|
file_path String @db.VarChar(500)
|
|
file_size Int
|
|
mime_type String @db.VarChar(100)
|
|
created_at DateTime @default(now())
|
|
|
|
announcement Announcement @relation(fields: [announcement_id], references: [id], onDelete: Cascade)
|
|
|
|
@@index([announcement_id])
|
|
@@map("announcement_attachments")
|
|
}
|
|
|
|
// ============================================
|
|
// Payment System
|
|
// ============================================
|
|
|
|
enum OrderStatus {
|
|
PENDING
|
|
PAID
|
|
CANCELLED
|
|
REFUNDED
|
|
}
|
|
|
|
enum PaymentStatus {
|
|
PENDING
|
|
SUCCESS
|
|
FAILED
|
|
}
|
|
|
|
enum WithdrawalStatus {
|
|
PENDING
|
|
APPROVED
|
|
REJECTED
|
|
PAID
|
|
}
|
|
|
|
model Order {
|
|
id Int @id @default(autoincrement())
|
|
user_id Int
|
|
total_amount Decimal @default(0) @db.Decimal(10, 2)
|
|
status OrderStatus @default(PENDING)
|
|
created_at DateTime @default(now())
|
|
updated_at DateTime? @updatedAt
|
|
|
|
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
|
items OrderItem[]
|
|
payments Payment[]
|
|
|
|
@@index([user_id])
|
|
@@index([status])
|
|
@@index([user_id, status])
|
|
@@map("orders")
|
|
}
|
|
|
|
model OrderItem {
|
|
id Int @id @default(autoincrement())
|
|
order_id Int
|
|
course_id Int
|
|
price Decimal @db.Decimal(10, 2)
|
|
created_at DateTime @default(now())
|
|
|
|
order Order @relation(fields: [order_id], references: [id], onDelete: Cascade)
|
|
|
|
@@index([order_id])
|
|
@@index([course_id])
|
|
@@map("order_items")
|
|
}
|
|
|
|
model Payment {
|
|
id Int @id @default(autoincrement())
|
|
order_id Int
|
|
provider String @db.VarChar(50)
|
|
transaction_id String? @unique @db.VarChar(255)
|
|
amount Decimal @db.Decimal(10, 2)
|
|
status PaymentStatus @default(PENDING)
|
|
paid_at DateTime?
|
|
created_at DateTime @default(now())
|
|
updated_at DateTime? @updatedAt
|
|
|
|
order Order @relation(fields: [order_id], references: [id], onDelete: Cascade)
|
|
|
|
@@index([order_id])
|
|
@@index([transaction_id])
|
|
@@index([status])
|
|
@@map("payments")
|
|
}
|
|
|
|
model InstructorBalance {
|
|
id Int @id @default(autoincrement())
|
|
instructor_id Int @unique
|
|
available_amount Decimal @default(0) @db.Decimal(10, 2)
|
|
withdrawn_amount Decimal @default(0) @db.Decimal(10, 2)
|
|
created_at DateTime @default(now())
|
|
updated_at DateTime? @updatedAt
|
|
|
|
instructor User @relation(fields: [instructor_id], references: [id], onDelete: Cascade)
|
|
|
|
@@index([instructor_id])
|
|
@@map("instructor_balances")
|
|
}
|
|
|
|
model WithdrawalRequest {
|
|
id Int @id @default(autoincrement())
|
|
instructor_id Int
|
|
amount Decimal @db.Decimal(10, 2)
|
|
status WithdrawalStatus @default(PENDING)
|
|
approved_by Int?
|
|
approved_at DateTime?
|
|
rejected_reason String? @db.Text
|
|
created_at DateTime @default(now())
|
|
updated_at DateTime? @updatedAt
|
|
updated_by Int?
|
|
|
|
instructor User @relation("WithdrawalInstructor", fields: [instructor_id], references: [id], onDelete: Cascade)
|
|
approver User? @relation("WithdrawalApprover", fields: [approved_by], references: [id])
|
|
updater User? @relation("WithdrawalUpdater", fields: [updated_by], references: [id])
|
|
|
|
@@index([instructor_id])
|
|
@@index([status])
|
|
@@index([instructor_id, status])
|
|
@@map("withdrawal_requests")
|
|
}
|