elearning/Backend/prisma/schema.prisma

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