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.
610 lines
19 KiB
Text
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")
|
|
}
|