migration to typescript
This commit is contained in:
parent
924000b084
commit
9fde77468a
41 changed files with 11952 additions and 10164 deletions
|
|
@ -1,406 +0,0 @@
|
|||
-- CreateEnum
|
||||
CREATE TYPE "CourseStatus" AS ENUM ('DRAFT', 'PENDING_APPROVAL', 'APPROVED', 'REJECTED', 'ARCHIVED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "LessonType" AS ENUM ('VIDEO', 'TEXT', 'PDF', 'QUIZ');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ScorePolicy" AS ENUM ('HIGHEST', 'LATEST', 'AVERAGE');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "roles" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"code" VARCHAR(50) NOT NULL,
|
||||
"name" JSONB NOT NULL,
|
||||
"description" JSONB,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "roles_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "users" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"username" VARCHAR(50) NOT NULL,
|
||||
"email" VARCHAR(255) NOT NULL,
|
||||
"password" VARCHAR(255) NOT NULL,
|
||||
"role_id" INTEGER NOT NULL,
|
||||
"is_active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "profiles" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"user_id" INTEGER NOT NULL,
|
||||
"first_name" VARCHAR(100),
|
||||
"last_name" VARCHAR(100),
|
||||
"phone" VARCHAR(20),
|
||||
"avatar_url" VARCHAR(500),
|
||||
"bio" JSONB,
|
||||
"birth_date" TIMESTAMP(3),
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "profiles_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "categories" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"code" VARCHAR(50) NOT NULL,
|
||||
"name" JSONB NOT NULL,
|
||||
"description" JSONB,
|
||||
"icon_url" VARCHAR(500),
|
||||
"is_active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "categories_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "courses" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"title" JSONB NOT NULL,
|
||||
"description" JSONB NOT NULL,
|
||||
"thumbnail_url" VARCHAR(500),
|
||||
"price" DECIMAL(10,2) NOT NULL DEFAULT 0,
|
||||
"is_free" BOOLEAN NOT NULL DEFAULT false,
|
||||
"have_certificate" BOOLEAN NOT NULL DEFAULT false,
|
||||
"status" "CourseStatus" NOT NULL DEFAULT 'DRAFT',
|
||||
"category_id" INTEGER NOT NULL,
|
||||
"created_by" INTEGER NOT NULL,
|
||||
"rejection_reason" TEXT,
|
||||
"is_deleted" BOOLEAN NOT NULL DEFAULT false,
|
||||
"deleted_at" TIMESTAMP(3),
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "courses_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "course_instructors" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"course_id" INTEGER NOT NULL,
|
||||
"user_id" INTEGER NOT NULL,
|
||||
"is_primary" BOOLEAN NOT NULL DEFAULT false,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "course_instructors_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "chapters" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"course_id" INTEGER NOT NULL,
|
||||
"title" JSONB NOT NULL,
|
||||
"description" JSONB,
|
||||
"order" INTEGER NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "chapters_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "lessons" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"chapter_id" INTEGER NOT NULL,
|
||||
"title" JSONB NOT NULL,
|
||||
"description" JSONB,
|
||||
"type" "LessonType" NOT NULL,
|
||||
"content" JSONB,
|
||||
"video_url" VARCHAR(500),
|
||||
"video_duration" INTEGER,
|
||||
"order" INTEGER NOT NULL,
|
||||
"is_preview" BOOLEAN NOT NULL DEFAULT false,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "lessons_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "lesson_prerequisites" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"lesson_id" INTEGER NOT NULL,
|
||||
"prerequisite_lesson_id" INTEGER NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "lesson_prerequisites_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "attachments" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"lesson_id" INTEGER NOT NULL,
|
||||
"filename" VARCHAR(255) NOT NULL,
|
||||
"original_name" VARCHAR(255) NOT NULL,
|
||||
"file_url" VARCHAR(500) NOT NULL,
|
||||
"file_size" BIGINT NOT NULL,
|
||||
"mime_type" VARCHAR(100) NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "attachments_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "quizzes" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"lesson_id" INTEGER NOT NULL,
|
||||
"title" JSONB NOT NULL,
|
||||
"description" JSONB,
|
||||
"passing_score" INTEGER NOT NULL DEFAULT 70,
|
||||
"time_limit" INTEGER,
|
||||
"max_attempts" INTEGER NOT NULL DEFAULT 3,
|
||||
"cooldown_hours" INTEGER NOT NULL DEFAULT 24,
|
||||
"score_policy" "ScorePolicy" NOT NULL DEFAULT 'HIGHEST',
|
||||
"shuffle_questions" BOOLEAN NOT NULL DEFAULT true,
|
||||
"shuffle_choices" BOOLEAN NOT NULL DEFAULT true,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "quizzes_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "quiz_questions" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"quiz_id" INTEGER NOT NULL,
|
||||
"question" JSONB NOT NULL,
|
||||
"choices" JSONB NOT NULL,
|
||||
"points" INTEGER NOT NULL DEFAULT 1,
|
||||
"order" INTEGER NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "quiz_questions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "quiz_attempts" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"quiz_id" INTEGER NOT NULL,
|
||||
"user_id" INTEGER NOT NULL,
|
||||
"answers" JSONB NOT NULL,
|
||||
"score" INTEGER NOT NULL,
|
||||
"passed" BOOLEAN NOT NULL,
|
||||
"time_spent" INTEGER,
|
||||
"started_at" TIMESTAMP(3) NOT NULL,
|
||||
"completed_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "quiz_attempts_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "enrollments" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"user_id" INTEGER NOT NULL,
|
||||
"course_id" INTEGER NOT NULL,
|
||||
"enrolled_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"completed_at" TIMESTAMP(3),
|
||||
"progress_percent" INTEGER NOT NULL DEFAULT 0,
|
||||
"last_accessed_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "enrollments_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "lesson_progress" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"user_id" INTEGER NOT NULL,
|
||||
"lesson_id" INTEGER NOT NULL,
|
||||
"is_completed" BOOLEAN NOT NULL DEFAULT false,
|
||||
"video_progress" INTEGER NOT NULL DEFAULT 0,
|
||||
"completed_at" TIMESTAMP(3),
|
||||
"last_watched_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "lesson_progress_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "certificates" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"user_id" INTEGER NOT NULL,
|
||||
"course_id" INTEGER NOT NULL,
|
||||
"certificate_url" VARCHAR(500) NOT NULL,
|
||||
"issued_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "certificates_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "roles_code_key" ON "roles"("code");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "users_username_key" ON "users"("username");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "users_email_idx" ON "users"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "users_role_id_idx" ON "users"("role_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "profiles_user_id_key" ON "profiles"("user_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "categories_code_key" ON "categories"("code");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "courses_category_id_idx" ON "courses"("category_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "courses_status_idx" ON "courses"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "courses_created_by_idx" ON "courses"("created_by");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "courses_created_at_idx" ON "courses"("created_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "course_instructors_course_id_idx" ON "course_instructors"("course_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "course_instructors_user_id_idx" ON "course_instructors"("user_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "course_instructors_course_id_user_id_key" ON "course_instructors"("course_id", "user_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "chapters_course_id_idx" ON "chapters"("course_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "chapters_order_idx" ON "chapters"("order");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "lessons_chapter_id_idx" ON "lessons"("chapter_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "lessons_order_idx" ON "lessons"("order");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "lesson_prerequisites_lesson_id_idx" ON "lesson_prerequisites"("lesson_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "lesson_prerequisites_prerequisite_lesson_id_idx" ON "lesson_prerequisites"("prerequisite_lesson_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "lesson_prerequisites_lesson_id_prerequisite_lesson_id_key" ON "lesson_prerequisites"("lesson_id", "prerequisite_lesson_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "attachments_lesson_id_idx" ON "attachments"("lesson_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "quizzes_lesson_id_key" ON "quizzes"("lesson_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "quiz_questions_quiz_id_idx" ON "quiz_questions"("quiz_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "quiz_questions_order_idx" ON "quiz_questions"("order");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "quiz_attempts_quiz_id_idx" ON "quiz_attempts"("quiz_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "quiz_attempts_user_id_idx" ON "quiz_attempts"("user_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "quiz_attempts_completed_at_idx" ON "quiz_attempts"("completed_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "enrollments_user_id_idx" ON "enrollments"("user_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "enrollments_course_id_idx" ON "enrollments"("course_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "enrollments_user_id_course_id_key" ON "enrollments"("user_id", "course_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "lesson_progress_user_id_idx" ON "lesson_progress"("user_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "lesson_progress_lesson_id_idx" ON "lesson_progress"("lesson_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "lesson_progress_user_id_lesson_id_key" ON "lesson_progress"("user_id", "lesson_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "certificates_user_id_idx" ON "certificates"("user_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "certificates_user_id_course_id_key" ON "certificates"("user_id", "course_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "users" ADD CONSTRAINT "users_role_id_fkey" FOREIGN KEY ("role_id") REFERENCES "roles"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "profiles" ADD CONSTRAINT "profiles_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "courses" ADD CONSTRAINT "courses_category_id_fkey" FOREIGN KEY ("category_id") REFERENCES "categories"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "courses" ADD CONSTRAINT "courses_created_by_fkey" FOREIGN KEY ("created_by") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "course_instructors" ADD CONSTRAINT "course_instructors_course_id_fkey" FOREIGN KEY ("course_id") REFERENCES "courses"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "course_instructors" ADD CONSTRAINT "course_instructors_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "chapters" ADD CONSTRAINT "chapters_course_id_fkey" FOREIGN KEY ("course_id") REFERENCES "courses"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "lessons" ADD CONSTRAINT "lessons_chapter_id_fkey" FOREIGN KEY ("chapter_id") REFERENCES "chapters"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "lesson_prerequisites" ADD CONSTRAINT "lesson_prerequisites_lesson_id_fkey" FOREIGN KEY ("lesson_id") REFERENCES "lessons"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "lesson_prerequisites" ADD CONSTRAINT "lesson_prerequisites_prerequisite_lesson_id_fkey" FOREIGN KEY ("prerequisite_lesson_id") REFERENCES "lessons"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "attachments" ADD CONSTRAINT "attachments_lesson_id_fkey" FOREIGN KEY ("lesson_id") REFERENCES "lessons"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "quizzes" ADD CONSTRAINT "quizzes_lesson_id_fkey" FOREIGN KEY ("lesson_id") REFERENCES "lessons"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "quiz_questions" ADD CONSTRAINT "quiz_questions_quiz_id_fkey" FOREIGN KEY ("quiz_id") REFERENCES "quizzes"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "quiz_attempts" ADD CONSTRAINT "quiz_attempts_quiz_id_fkey" FOREIGN KEY ("quiz_id") REFERENCES "quizzes"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "quiz_attempts" ADD CONSTRAINT "quiz_attempts_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "enrollments" ADD CONSTRAINT "enrollments_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "enrollments" ADD CONSTRAINT "enrollments_course_id_fkey" FOREIGN KEY ("course_id") REFERENCES "courses"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "lesson_progress" ADD CONSTRAINT "lesson_progress_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "lesson_progress" ADD CONSTRAINT "lesson_progress_lesson_id_fkey" FOREIGN KEY ("lesson_id") REFERENCES "lessons"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "certificates" ADD CONSTRAINT "certificates_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
|
@ -17,113 +17,149 @@ datasource db {
|
|||
model Role {
|
||||
id Int @id @default(autoincrement())
|
||||
code String @unique @db.VarChar(50)
|
||||
name Json // { th: "", en: "" }
|
||||
name Json // { th: "", en: "" }
|
||||
description Json?
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
|
||||
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
|
||||
is_active Boolean @default(true)
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
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?
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime? @updatedAt
|
||||
|
||||
role Role @relation(fields: [role_id], references: [id])
|
||||
profile Profile?
|
||||
role Role @relation(fields: [role_id], references: [id], onDelete: Restrict)
|
||||
profile UserProfile?
|
||||
|
||||
// Relations
|
||||
created_courses Course[] @relation("CourseCreator")
|
||||
instructor_courses CourseInstructor[]
|
||||
enrollments Enrollment[]
|
||||
lesson_progress LessonProgress[]
|
||||
quiz_attempts QuizAttempt[]
|
||||
certificates Certificate[]
|
||||
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 Profile {
|
||||
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())
|
||||
user_id Int @unique
|
||||
first_name String? @db.VarChar(100)
|
||||
last_name String? @db.VarChar(100)
|
||||
phone String? @db.VarChar(20)
|
||||
avatar_url String? @db.VarChar(500)
|
||||
bio Json? // { th: "", en: "" }
|
||||
birth_date DateTime?
|
||||
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())
|
||||
updated_at DateTime @updatedAt
|
||||
created_by Int
|
||||
updated_at DateTime? @updatedAt
|
||||
updated_by Int?
|
||||
|
||||
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
||||
courses Course[]
|
||||
creator User @relation("CategoryCreator", fields: [created_by], references: [id], onDelete: Restrict)
|
||||
updater User? @relation("CategoryUpdater", fields: [updated_by], references: [id])
|
||||
|
||||
@@map("profiles")
|
||||
@@index([slug])
|
||||
@@index([is_active])
|
||||
@@index([sort_order])
|
||||
@@map("categories")
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Course Management
|
||||
// Course Structure
|
||||
// ============================================
|
||||
|
||||
enum CourseStatus {
|
||||
DRAFT
|
||||
PENDING_APPROVAL
|
||||
PENDING
|
||||
APPROVED
|
||||
REJECTED
|
||||
ARCHIVED
|
||||
}
|
||||
|
||||
model Category {
|
||||
id Int @id @default(autoincrement())
|
||||
code String @unique @db.VarChar(50)
|
||||
name Json // { th: "", en: "" }
|
||||
description Json?
|
||||
icon_url String? @db.VarChar(500)
|
||||
is_active Boolean @default(true)
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
|
||||
courses Course[]
|
||||
|
||||
@@map("categories")
|
||||
}
|
||||
|
||||
model Course {
|
||||
id Int @id @default(autoincrement())
|
||||
title Json // { th: "", en: "" }
|
||||
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)
|
||||
category_id Int
|
||||
created_by Int
|
||||
rejection_reason String? @db.Text
|
||||
is_deleted Boolean @default(false)
|
||||
deleted_at DateTime?
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
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])
|
||||
creator User @relation("CourseCreator", fields: [created_by], references: [id])
|
||||
chapters Chapter[]
|
||||
instructors CourseInstructor[]
|
||||
enrollments Enrollment[]
|
||||
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([created_at])
|
||||
@@index([status, is_free])
|
||||
@@index([category_id, status])
|
||||
@@map("courses")
|
||||
}
|
||||
|
||||
|
|
@ -132,7 +168,7 @@ model CourseInstructor {
|
|||
course_id Int
|
||||
user_id Int
|
||||
is_primary Boolean @default(false)
|
||||
created_at DateTime @default(now())
|
||||
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)
|
||||
|
|
@ -140,23 +176,55 @@ model CourseInstructor {
|
|||
@@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
|
||||
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?
|
||||
order Int
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
id Int @id @default(autoincrement())
|
||||
course_id Int
|
||||
title Json // { th: "", en: "" }
|
||||
description Json?
|
||||
sort_order Int @default(0)
|
||||
is_published Boolean @default(false)
|
||||
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([order])
|
||||
@@index([course_id, sort_order])
|
||||
@@map("chapters")
|
||||
}
|
||||
|
||||
|
|
@ -166,169 +234,184 @@ model Chapter {
|
|||
|
||||
enum LessonType {
|
||||
VIDEO
|
||||
TEXT
|
||||
PDF
|
||||
QUIZ
|
||||
}
|
||||
|
||||
model Lesson {
|
||||
id Int @id @default(autoincrement())
|
||||
chapter_id Int
|
||||
title Json // { th: "", en: "" }
|
||||
description Json?
|
||||
type LessonType
|
||||
content Json? // For TEXT type
|
||||
video_url String? @db.VarChar(500)
|
||||
video_duration Int? // in seconds
|
||||
order Int
|
||||
is_preview Boolean @default(false)
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
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(false)
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime? @updatedAt
|
||||
|
||||
chapter Chapter @relation(fields: [chapter_id], references: [id], onDelete: Cascade)
|
||||
attachments Attachment[]
|
||||
prerequisites LessonPrerequisite[] @relation("LessonPrerequisites")
|
||||
required_for LessonPrerequisite[] @relation("RequiredForLessons")
|
||||
quiz Quiz?
|
||||
progress LessonProgress[]
|
||||
chapter Chapter @relation(fields: [chapter_id], references: [id], onDelete: Cascade)
|
||||
attachments LessonAttachment[]
|
||||
quiz Quiz?
|
||||
progress LessonProgress[]
|
||||
|
||||
@@index([chapter_id])
|
||||
@@index([order])
|
||||
@@index([chapter_id, sort_order])
|
||||
@@index([type])
|
||||
@@map("lessons")
|
||||
}
|
||||
|
||||
model LessonPrerequisite {
|
||||
id Int @id @default(autoincrement())
|
||||
lesson_id Int
|
||||
prerequisite_lesson_id Int
|
||||
created_at DateTime @default(now())
|
||||
|
||||
lesson Lesson @relation("LessonPrerequisites", fields: [lesson_id], references: [id], onDelete: Cascade)
|
||||
prerequisite_lesson Lesson @relation("RequiredForLessons", fields: [prerequisite_lesson_id], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([lesson_id, prerequisite_lesson_id])
|
||||
@@index([lesson_id])
|
||||
@@index([prerequisite_lesson_id])
|
||||
@@map("lesson_prerequisites")
|
||||
}
|
||||
|
||||
model Attachment {
|
||||
id Int @id @default(autoincrement())
|
||||
lesson_id Int
|
||||
filename String @db.VarChar(255)
|
||||
original_name String @db.VarChar(255)
|
||||
file_url String @db.VarChar(500)
|
||||
file_size BigInt
|
||||
mime_type String @db.VarChar(100)
|
||||
created_at DateTime @default(now())
|
||||
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])
|
||||
@@map("attachments")
|
||||
@@index([lesson_id, sort_order])
|
||||
@@map("lesson_attachments")
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Quiz System
|
||||
// ============================================
|
||||
|
||||
enum ScorePolicy {
|
||||
HIGHEST
|
||||
LATEST
|
||||
AVERAGE
|
||||
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(70)
|
||||
time_limit Int? // in minutes
|
||||
max_attempts Int @default(3)
|
||||
cooldown_hours Int @default(24)
|
||||
score_policy ScorePolicy @default(HIGHEST)
|
||||
shuffle_questions Boolean @default(true)
|
||||
shuffle_choices Boolean @default(true)
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
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)
|
||||
questions QuizQuestion[]
|
||||
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 QuizQuestion {
|
||||
id Int @id @default(autoincrement())
|
||||
quiz_id Int
|
||||
question Json // { th: "", en: "" }
|
||||
choices Json // [{ th: "", en: "", is_correct: boolean }]
|
||||
points Int @default(1)
|
||||
order Int
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
|
||||
quiz Quiz @relation(fields: [quiz_id], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([quiz_id])
|
||||
@@index([order])
|
||||
@@map("quiz_questions")
|
||||
}
|
||||
|
||||
model QuizAttempt {
|
||||
id Int @id @default(autoincrement())
|
||||
model Question {
|
||||
id Int @id @default(autoincrement())
|
||||
quiz_id Int
|
||||
user_id Int
|
||||
answers Json // [{ question_id: int, choice_index: int }]
|
||||
score Int
|
||||
passed Boolean
|
||||
time_spent Int? // in seconds
|
||||
started_at DateTime
|
||||
completed_at DateTime @default(now())
|
||||
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)
|
||||
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
||||
quiz Quiz @relation(fields: [quiz_id], references: [id], onDelete: Cascade)
|
||||
choices Choice[]
|
||||
|
||||
@@index([quiz_id])
|
||||
@@index([user_id])
|
||||
@@index([completed_at])
|
||||
@@map("quiz_attempts")
|
||||
@@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")
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Progress Tracking
|
||||
// Student Progress
|
||||
// ============================================
|
||||
|
||||
enum EnrollmentStatus {
|
||||
ENROLLED
|
||||
IN_PROGRESS
|
||||
COMPLETED
|
||||
DROPPED
|
||||
}
|
||||
|
||||
model Enrollment {
|
||||
id Int @id @default(autoincrement())
|
||||
user_id Int
|
||||
course_id Int
|
||||
enrolled_at DateTime @default(now())
|
||||
completed_at DateTime?
|
||||
progress_percent Int @default(0)
|
||||
last_accessed_at DateTime @default(now())
|
||||
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)
|
||||
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])
|
||||
@@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)
|
||||
video_progress Int @default(0) // seconds watched
|
||||
completed_at DateTime?
|
||||
last_watched_at DateTime @default(now())
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
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)
|
||||
|
|
@ -336,19 +419,190 @@ model LessonProgress {
|
|||
@@unique([user_id, lesson_id])
|
||||
@@index([user_id])
|
||||
@@index([lesson_id])
|
||||
@@index([last_watched_at])
|
||||
@@map("lesson_progress")
|
||||
}
|
||||
|
||||
model Certificate {
|
||||
id Int @id @default(autoincrement())
|
||||
user_id Int
|
||||
course_id Int
|
||||
certificate_url String @db.VarChar(500)
|
||||
issued_at DateTime @default(now())
|
||||
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)
|
||||
|
||||
@@unique([user_id, course_id])
|
||||
@@index([user_id])
|
||||
@@map("certificates")
|
||||
@@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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,25 +7,32 @@ async function main() {
|
|||
console.log('🌱 Starting database seeding...');
|
||||
|
||||
// Clear existing data (in development only)
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('🗑️ Clearing existing data...');
|
||||
await prisma.quizAttempt.deleteMany();
|
||||
await prisma.quizQuestion.deleteMany();
|
||||
await prisma.quiz.deleteMany();
|
||||
await prisma.lessonProgress.deleteMany();
|
||||
await prisma.attachment.deleteMany();
|
||||
await prisma.lessonPrerequisite.deleteMany();
|
||||
await prisma.lesson.deleteMany();
|
||||
await prisma.chapter.deleteMany();
|
||||
await prisma.enrollment.deleteMany();
|
||||
await prisma.courseInstructor.deleteMany();
|
||||
await prisma.course.deleteMany();
|
||||
await prisma.category.deleteMany();
|
||||
await prisma.certificate.deleteMany();
|
||||
await prisma.profile.deleteMany();
|
||||
await prisma.user.deleteMany();
|
||||
await prisma.role.deleteMany();
|
||||
}
|
||||
// if (process.env.NODE_ENV === 'development') {
|
||||
// console.log('🗑️ Clearing existing data...');
|
||||
// await prisma.quizAttempt.deleteMany();
|
||||
// await prisma.choice.deleteMany();
|
||||
// await prisma.question.deleteMany();
|
||||
// await prisma.quiz.deleteMany();
|
||||
// await prisma.lessonProgress.deleteMany();
|
||||
// await prisma.lessonAttachment.deleteMany();
|
||||
// await prisma.lesson.deleteMany();
|
||||
// await prisma.chapter.deleteMany();
|
||||
// await prisma.announcementAttachment.deleteMany();
|
||||
// await prisma.announcement.deleteMany();
|
||||
// await prisma.certificate.deleteMany();
|
||||
// await prisma.enrollment.deleteMany();
|
||||
// await prisma.courseInstructor.deleteMany();
|
||||
// await prisma.course.deleteMany();
|
||||
// await prisma.category.deleteMany();
|
||||
// await prisma.payment.deleteMany();
|
||||
// await prisma.orderItem.deleteMany();
|
||||
// await prisma.order.deleteMany();
|
||||
// await prisma.withdrawalRequest.deleteMany();
|
||||
// await prisma.instructorBalance.deleteMany();
|
||||
// await prisma.userProfile.deleteMany();
|
||||
// await prisma.user.deleteMany();
|
||||
// await prisma.role.deleteMany();
|
||||
// }
|
||||
|
||||
// Seed Roles
|
||||
console.log('👥 Seeding roles...');
|
||||
|
|
@ -63,11 +70,12 @@ async function main() {
|
|||
email: 'admin@elearning.local',
|
||||
password: hashedPassword,
|
||||
role_id: roles[0].id,
|
||||
email_verified_at: new Date(),
|
||||
profile: {
|
||||
create: {
|
||||
prefix: { th: 'นาย', en: 'Mr.' },
|
||||
first_name: 'Admin',
|
||||
last_name: 'User',
|
||||
bio: { th: 'ผู้ดูแลระบบ', en: 'System Administrator' }
|
||||
last_name: 'User'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -79,11 +87,12 @@ async function main() {
|
|||
email: 'instructor@elearning.local',
|
||||
password: hashedPassword,
|
||||
role_id: roles[1].id,
|
||||
email_verified_at: new Date(),
|
||||
profile: {
|
||||
create: {
|
||||
prefix: { th: 'นาย', en: 'Mr.' },
|
||||
first_name: 'John',
|
||||
last_name: 'Doe',
|
||||
bio: { th: 'ผู้สอนมืออาชีพ', en: 'Professional Instructor' }
|
||||
last_name: 'Doe'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -95,11 +104,12 @@ async function main() {
|
|||
email: 'student@elearning.local',
|
||||
password: hashedPassword,
|
||||
role_id: roles[2].id,
|
||||
email_verified_at: new Date(),
|
||||
profile: {
|
||||
create: {
|
||||
prefix: { th: 'นางสาว', en: 'Ms.' },
|
||||
first_name: 'Jane',
|
||||
last_name: 'Smith',
|
||||
bio: { th: 'นักเรียนที่กระตือรือร้น', en: 'Eager learner' }
|
||||
last_name: 'Smith'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -110,23 +120,32 @@ async function main() {
|
|||
const categories = await Promise.all([
|
||||
prisma.category.create({
|
||||
data: {
|
||||
code: 'PROGRAMMING',
|
||||
name: { th: 'การเขียนโปรแกรม', en: 'Programming' },
|
||||
description: { th: 'เรียนรู้การเขียนโปรแกรมและพัฒนาซอฟต์แวร์', en: 'Learn programming and software development' }
|
||||
slug: 'programming',
|
||||
description: { th: 'เรียนรู้การเขียนโปรแกรมและพัฒนาซอฟต์แวร์', en: 'Learn programming and software development' },
|
||||
icon: 'code',
|
||||
sort_order: 1,
|
||||
created_by: admin.id
|
||||
}
|
||||
}),
|
||||
prisma.category.create({
|
||||
data: {
|
||||
code: 'DESIGN',
|
||||
name: { th: 'การออกแบบ', en: 'Design' },
|
||||
description: { th: 'เรียนรู้การออกแบบกราฟิกและ UI/UX', en: 'Learn graphic design and UI/UX' }
|
||||
slug: 'design',
|
||||
description: { th: 'เรียนรู้การออกแบบกราฟิกและ UI/UX', en: 'Learn graphic design and UI/UX' },
|
||||
icon: 'palette',
|
||||
sort_order: 2,
|
||||
created_by: admin.id
|
||||
}
|
||||
}),
|
||||
prisma.category.create({
|
||||
data: {
|
||||
code: 'BUSINESS',
|
||||
name: { th: 'ธุรกิจ', en: 'Business' },
|
||||
description: { th: 'เรียนรู้การบริหารธุรกิจและการตลาด', en: 'Learn business management and marketing' }
|
||||
slug: 'business',
|
||||
description: { th: 'เรียนรู้การบริหารธุรกิจและการตลาด', en: 'Learn business management and marketing' },
|
||||
icon: 'briefcase',
|
||||
sort_order: 3,
|
||||
created_by: admin.id
|
||||
}
|
||||
})
|
||||
]);
|
||||
|
|
@ -136,9 +155,10 @@ async function main() {
|
|||
const course = await prisma.course.create({
|
||||
data: {
|
||||
title: { th: 'พื้นฐาน JavaScript', en: 'JavaScript Fundamentals' },
|
||||
slug: 'javascript-fundamentals',
|
||||
description: {
|
||||
th: 'เรียนรู้พื้นฐาน JavaScript ตั้งแต่เริ่มต้น',
|
||||
en: 'Learn JavaScript fundamentals from scratch'
|
||||
th: 'เรียนรู้พื้นฐาน JavaScript ตั้งแต่เริ่มต้น รวมถึงตัวแปร ฟังก์ชัน และการจัดการ DOM',
|
||||
en: 'Learn JavaScript fundamentals from scratch including variables, functions, and DOM manipulation'
|
||||
},
|
||||
price: 0,
|
||||
is_free: true,
|
||||
|
|
@ -146,6 +166,8 @@ async function main() {
|
|||
status: 'APPROVED',
|
||||
category_id: categories[0].id,
|
||||
created_by: instructor.id,
|
||||
approved_by: admin.id,
|
||||
approved_at: new Date(),
|
||||
instructors: {
|
||||
create: {
|
||||
user_id: instructor.id,
|
||||
|
|
@ -156,43 +178,177 @@ async function main() {
|
|||
create: [
|
||||
{
|
||||
title: { th: 'บทที่ 1: เริ่มต้น', en: 'Chapter 1: Getting Started' },
|
||||
description: { th: 'แนะนำ JavaScript', en: 'Introduction to JavaScript' },
|
||||
order: 1,
|
||||
description: { th: 'แนะนำ JavaScript และการตั้งค่าสภาพแวดล้อม', en: 'Introduction to JavaScript and environment setup' },
|
||||
sort_order: 1,
|
||||
is_published: true,
|
||||
lessons: {
|
||||
create: [
|
||||
{
|
||||
title: { th: 'JavaScript คืออะไร', en: 'What is JavaScript' },
|
||||
description: { th: 'เรียนรู้ว่า JavaScript คืออะไร', en: 'Learn what JavaScript is' },
|
||||
content: {
|
||||
th: 'JavaScript เป็นภาษาโปรแกรมที่ใช้ในการพัฒนาเว็บไซต์',
|
||||
en: 'JavaScript is a programming language used for web development'
|
||||
},
|
||||
type: 'VIDEO',
|
||||
order: 1,
|
||||
is_preview: true
|
||||
duration_minutes: 15,
|
||||
sort_order: 1,
|
||||
is_published: true
|
||||
},
|
||||
{
|
||||
title: { th: 'ตัวแปรและชนิดข้อมูล', en: 'Variables and Data Types' },
|
||||
description: { th: 'เรียนรู้เกี่ยวกับตัวแปร', en: 'Learn about variables' },
|
||||
content: {
|
||||
th: 'เรียนรู้เกี่ยวกับตัวแปร let, const และชนิดข้อมูลต่างๆ',
|
||||
en: 'Learn about let, const variables and different data types'
|
||||
},
|
||||
type: 'VIDEO',
|
||||
order: 2
|
||||
duration_minutes: 20,
|
||||
sort_order: 2,
|
||||
is_published: true
|
||||
},
|
||||
{
|
||||
title: { th: 'แบบทดสอบบทที่ 1', en: 'Chapter 1 Quiz' },
|
||||
type: 'QUIZ',
|
||||
duration_minutes: 10,
|
||||
sort_order: 3,
|
||||
is_published: true,
|
||||
require_pass_quiz: true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
title: { th: 'บทที่ 2: ฟังก์ชัน', en: 'Chapter 2: Functions' },
|
||||
description: { th: 'เรียนรู้เกี่ยวกับฟังก์ชัน', en: 'Learn about functions' },
|
||||
order: 2,
|
||||
description: { th: 'เรียนรู้เกี่ยวกับฟังก์ชันและการใช้งาน', en: 'Learn about functions and their usage' },
|
||||
sort_order: 2,
|
||||
is_published: true,
|
||||
lessons: {
|
||||
create: [
|
||||
{
|
||||
title: { th: 'การสร้างฟังก์ชัน', en: 'Creating Functions' },
|
||||
description: { th: 'เรียนรู้วิธีสร้างฟังก์ชัน', en: 'Learn how to create functions' },
|
||||
content: {
|
||||
th: 'เรียนรู้วิธีสร้างและเรียกใช้ฟังก์ชัน',
|
||||
en: 'Learn how to create and call functions'
|
||||
},
|
||||
type: 'VIDEO',
|
||||
order: 1
|
||||
duration_minutes: 25,
|
||||
sort_order: 1,
|
||||
is_published: true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
include: {
|
||||
chapters: {
|
||||
include: {
|
||||
lessons: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Create a quiz for the quiz lesson
|
||||
const quizLesson = await prisma.lesson.findFirst({
|
||||
where: {
|
||||
title: {
|
||||
path: ['en'],
|
||||
equals: 'Chapter 1 Quiz'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (quizLesson) {
|
||||
console.log('📝 Creating quiz...');
|
||||
await prisma.quiz.create({
|
||||
data: {
|
||||
lesson_id: quizLesson.id,
|
||||
title: { th: 'แบบทดสอบบทที่ 1', en: 'Chapter 1 Quiz' },
|
||||
description: { th: 'ทดสอบความเข้าใจเกี่ยวกับพื้นฐาน JavaScript', en: 'Test your understanding of JavaScript basics' },
|
||||
passing_score: 70,
|
||||
time_limit: 10,
|
||||
shuffle_questions: true,
|
||||
shuffle_choices: true,
|
||||
created_by: instructor.id,
|
||||
questions: {
|
||||
create: [
|
||||
{
|
||||
question: {
|
||||
th: 'JavaScript ใช้สำหรับอะไร?',
|
||||
en: 'What is JavaScript used for?'
|
||||
},
|
||||
question_type: 'MULTIPLE_CHOICE',
|
||||
score: 1,
|
||||
sort_order: 1,
|
||||
choices: {
|
||||
create: [
|
||||
{
|
||||
text: { th: 'พัฒนาเว็บไซต์', en: 'Web development' },
|
||||
is_correct: true,
|
||||
sort_order: 1
|
||||
},
|
||||
{
|
||||
text: { th: 'ทำกาแฟ', en: 'Making coffee' },
|
||||
is_correct: false,
|
||||
sort_order: 2
|
||||
},
|
||||
{
|
||||
text: { th: 'ขับรถ', en: 'Driving cars' },
|
||||
is_correct: false,
|
||||
sort_order: 3
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
question: {
|
||||
th: 'ตัวแปรใน JavaScript ประกาศด้วยคำสั่งใด?',
|
||||
en: 'Which keyword is used to declare variables in JavaScript?'
|
||||
},
|
||||
question_type: 'MULTIPLE_CHOICE',
|
||||
score: 1,
|
||||
sort_order: 2,
|
||||
choices: {
|
||||
create: [
|
||||
{
|
||||
text: { th: 'let และ const', en: 'let and const' },
|
||||
is_correct: true,
|
||||
sort_order: 1
|
||||
},
|
||||
{
|
||||
text: { th: 'int และ float', en: 'int and float' },
|
||||
is_correct: false,
|
||||
sort_order: 2
|
||||
},
|
||||
{
|
||||
text: { th: 'variable', en: 'variable' },
|
||||
is_correct: false,
|
||||
sort_order: 3
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Create announcement
|
||||
console.log('📢 Creating announcement...');
|
||||
await prisma.announcement.create({
|
||||
data: {
|
||||
course_id: course.id,
|
||||
title: { th: 'ยินดีต้อนรับสู่คอร์ส', en: 'Welcome to the Course' },
|
||||
content: {
|
||||
th: 'ยินดีต้อนรับทุกคนสู่คอร์ส JavaScript Fundamentals! เราจะเริ่มเรียนในสัปดาห์หน้า',
|
||||
en: 'Welcome everyone to JavaScript Fundamentals! We will start next week'
|
||||
},
|
||||
status: 'PUBLISHED',
|
||||
is_pinned: true,
|
||||
published_at: new Date(),
|
||||
created_by: instructor.id
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -202,6 +358,10 @@ async function main() {
|
|||
console.log(`- Users: 3 (admin, instructor, student)`);
|
||||
console.log(`- Categories: ${categories.length}`);
|
||||
console.log(`- Courses: 1`);
|
||||
console.log(`- Chapters: 2`);
|
||||
console.log(`- Lessons: 4`);
|
||||
console.log(`- Quizzes: 1 (with 2 questions)`);
|
||||
console.log(`- Announcements: 1`);
|
||||
console.log('\n🔑 Test Credentials:');
|
||||
console.log('Admin: admin / admin123');
|
||||
console.log('Instructor: instructor / admin123');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue