elearning/Backend/prisma/schema.prisma
JakkrapartXD 18b8f4501f feat: add is_skippable field to Quiz model with default value true
Add is_skippable boolean field to Quiz schema, update quiz creation and update logic to handle the new field, and include it in student course content responses. Update seed data and type definitions accordingly.
2026-02-02 11:17:20 +07:00

610 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)
is_skippable 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")
}