From 9bfc621d3f8739af6709e1b7cbf29a0089de7ad7 Mon Sep 17 00:00:00 2001 From: SpecialX <47072643+wangxiner55@users.noreply.github.com> Date: Wed, 14 Jan 2026 13:59:11 +0800 Subject: [PATCH] feat(classes): optimize teacher dashboard ui and implement grade management --- .../003_frontend_engineering_standards.md | 3 + .../002_teacher_dashboard_implementation.md | 66 + .../003_textbooks_module_implementation.md | 51 +- docs/design/005_exam_module_implementation.md | 32 +- .../006_homework_module_implementation.md | 36 + docs/design/design_system.md | 5 +- docs/work_log.md | 78 + drizzle/0007_add_class_invitation_code.sql | 3 - drizzle/0007_talented_bromley.sql | 6 + drizzle/0008_add_user_profile_fields.sql | 17 - drizzle/meta/0007_snapshot.json | 3064 +++++++++++++++++ drizzle/meta/0009_snapshot.json | 3009 ++++++++++++++++ drizzle/meta/_journal.json | 13 +- package-lock.json | 936 ++++- package.json | 7 + scripts/seed.ts | 16 +- .../management/grade/classes/page.tsx | 31 + .../grade}/insights/page.tsx | 2 +- src/app/(dashboard)/profile/page.tsx | 149 +- src/app/(dashboard)/settings/page.tsx | 14 +- .../assignments/[assignmentId]/page.tsx | 18 + .../student/learning/assignments/page.tsx | 20 + .../student/learning/textbooks/[id]/page.tsx | 30 +- .../student/learning/textbooks/page.tsx | 2 +- .../teacher/classes/my/[id]/page.tsx | 484 +-- .../(dashboard)/teacher/classes/my/page.tsx | 12 +- .../teacher/classes/students/page.tsx | 4 +- .../teacher/exams/[id]/build/page.tsx | 13 +- .../(dashboard)/teacher/exams/all/page.tsx | 7 - .../(dashboard)/teacher/exams/create/page.tsx | 31 +- .../homework/assignments/[id]/page.tsx | 146 +- .../submissions/[submissionId]/page.tsx | 2 + .../(dashboard)/teacher/textbooks/page.tsx | 4 +- src/modules/classes/actions.ts | 210 +- .../classes/components/grade-classes-view.tsx | 455 +++ .../classes/components/my-classes-grid.tsx | 331 +- .../classes/components/students-filters.tsx | 17 +- .../classes/components/students-table.tsx | 203 +- src/modules/classes/data-access.ts | 191 +- src/modules/classes/types.ts | 73 +- .../student-dashboard-header.tsx | 36 +- .../student-dashboard-view.tsx | 18 +- .../student-dashboard/student-grades-card.tsx | 124 +- .../student-ranking-card.tsx | 47 - .../student-dashboard/student-stats-grid.tsx | 61 +- .../student-upcoming-assignments-card.tsx | 82 +- .../teacher-classes-card.tsx | 1 - .../teacher-dashboard/teacher-stats.tsx | 3 + src/modules/exams/actions.ts | 113 +- .../assembly/exam-paper-preview.tsx | 78 +- .../assembly/question-bank-list.tsx | 31 +- src/modules/exams/components/exam-actions.tsx | 164 +- .../exams/components/exam-assembly.tsx | 224 +- src/modules/exams/components/exam-card.tsx | 103 + src/modules/exams/components/exam-columns.tsx | 161 +- .../exams/components/exam-data-table.tsx | 40 +- src/modules/exams/components/exam-filters.tsx | 86 +- src/modules/exams/components/exam-form.tsx | 414 ++- src/modules/exams/components/exam-grid.tsx | 16 + src/modules/exams/data-access.ts | 29 +- src/modules/homework/actions.ts | 2 +- .../homework-assignment-exam-content-card.tsx | 2 +- ...rk-assignment-exam-error-explorer-lazy.tsx | 69 +- ...omework-assignment-exam-error-explorer.tsx | 2 +- .../homework-assignment-exam-preview-pane.tsx | 22 +- ...assignment-question-error-detail-panel.tsx | 100 +- ...assignment-question-error-details-card.tsx | 49 - ...ssignment-question-error-overview-card.tsx | 179 +- .../components/homework-grading-view.tsx | 565 +-- .../components/homework-take-view.tsx | 284 +- .../student-homework-review-view.tsx | 320 ++ src/modules/homework/data-access.ts | 62 +- src/modules/homework/types.ts | 7 +- src/modules/layout/components/site-header.tsx | 51 +- src/modules/layout/config/navigation.ts | 12 +- src/modules/questions/actions.ts | 34 +- src/modules/questions/data-access.ts | 4 +- .../components/admin-settings-view.tsx | 58 +- .../components/profile-settings-form.tsx | 199 ++ .../components/student-settings-view.tsx | 41 +- .../components/teacher-settings-view.tsx | 41 +- .../components/student-courses-view.tsx | 208 +- src/modules/textbooks/actions.ts | 20 +- .../textbooks/components/chapter-list.tsx | 2 +- .../components/chapter-sidebar-list.tsx | 389 ++- .../components/create-chapter-dialog.tsx | 3 +- .../components/knowledge-point-panel.tsx | 66 +- .../textbooks/components/textbook-card.tsx | 126 +- .../components/textbook-content-layout.tsx | 66 +- .../textbooks/components/textbook-filters.tsx | 23 +- .../components/textbook-form-dialog.tsx | 6 +- .../textbooks/components/textbook-reader.tsx | 2 +- src/modules/textbooks/data-access.ts | 36 +- src/modules/users/actions.ts | 44 + src/modules/users/data-access.ts | 45 + src/shared/components/ui/alert-dialog.tsx | 8 +- src/shared/components/ui/chart.tsx | 12 +- src/shared/components/ui/dialog.tsx | 10 +- src/shared/components/ui/progress.tsx | 28 + src/shared/components/ui/radio-group.tsx | 40 + src/shared/components/ui/rich-text-editor.tsx | 208 ++ src/shared/components/ui/sheet.tsx | 2 +- src/shared/db/relations.ts | 13 +- src/shared/db/schema.ts | 20 +- 104 files changed, 12793 insertions(+), 2309 deletions(-) create mode 100644 docs/work_log.md delete mode 100644 drizzle/0007_add_class_invitation_code.sql create mode 100644 drizzle/0007_talented_bromley.sql delete mode 100644 drizzle/0008_add_user_profile_fields.sql create mode 100644 drizzle/meta/0007_snapshot.json create mode 100644 drizzle/meta/0009_snapshot.json create mode 100644 src/app/(dashboard)/management/grade/classes/page.tsx rename src/app/(dashboard)/{teacher/grades => management/grade}/insights/page.tsx (98%) create mode 100644 src/modules/classes/components/grade-classes-view.tsx delete mode 100644 src/modules/dashboard/components/student-dashboard/student-ranking-card.tsx create mode 100644 src/modules/exams/components/exam-card.tsx create mode 100644 src/modules/exams/components/exam-grid.tsx delete mode 100644 src/modules/homework/components/homework-assignment-question-error-details-card.tsx create mode 100644 src/modules/homework/components/student-homework-review-view.tsx create mode 100644 src/modules/settings/components/profile-settings-form.tsx create mode 100644 src/modules/users/actions.ts create mode 100644 src/modules/users/data-access.ts create mode 100644 src/shared/components/ui/progress.tsx create mode 100644 src/shared/components/ui/radio-group.tsx create mode 100644 src/shared/components/ui/rich-text-editor.tsx diff --git a/docs/architecture/003_frontend_engineering_standards.md b/docs/architecture/003_frontend_engineering_standards.md index 288fbbe..a2167ed 100644 --- a/docs/architecture/003_frontend_engineering_standards.md +++ b/docs/architecture/003_frontend_engineering_standards.md @@ -91,6 +91,9 @@ - 若现有基础组件无法满足需求: 1. 优先通过 Composition 在业务模块里封装“业务组件” 2. 仅在存在 bug 或需要全局一致性调整时,才考虑改动 `src/shared/components/ui/*`(并在 PR 中明确影响面) +- **图表库**:统一使用 `Recharts`,禁止引入其他图表库(Chart.js / ECharts 等)。 + - 使用 `src/shared/components/ui/chart.tsx` 进行封装。 + - 遵循 Shadcn/UI Chart 规范。 ### 2.4 Client Component 引用边界(强制) diff --git a/docs/design/002_teacher_dashboard_implementation.md b/docs/design/002_teacher_dashboard_implementation.md index 3cc9393..69a5f8b 100644 --- a/docs/design/002_teacher_dashboard_implementation.md +++ b/docs/design/002_teacher_dashboard_implementation.md @@ -234,3 +234,69 @@ Seed 脚本已覆盖班级相关数据,以便在开发环境快速验证页面 ### 7.4 技术细节 - 引入 `recharts` 替换手写 SVG 图表,统一图表风格。 - 优化 Grid 布局响应式表现 (`lg:grid-cols-12`)。 +- **New Components**: + - `TeacherGradeTrends`: 基于 Recharts 的趋势图组件。 + - `Chart`: 基于 Shadcn/UI 规范的通用图表包装器 (`src/shared/components/ui/chart.tsx`)。 + +### 7.5 代码管理 +- **Branch**: `ui_opt` +- **Scope**: `src/modules/dashboard`, `src/shared/components/ui/chart.tsx` +- **Commit**: "feat(dashboard): optimize teacher dashboard ui and layout" + +--- + +## 8. 班级详情页与学生管理优化 (2026-01-14) + +**目标**: 提升班级管理效率与信息可视化程度,优化大班级场景下的性能与体验。 + +### 8.1 学生管理列表优化 (Students Table) +- **分页 (Pagination)**: 引入客户端分页(每页 10 条),解决大班级列表渲染性能问题。 +- **信息增强**: + - 增加学生头像 (Avatar)、性别、加入时间展示。 + - 增加可视化状态徽章 (Status Badge):Active (Emerald) / Inactive (Muted)。 +- **筛选能力**: + - 新增状态筛选器 (Active/Inactive),支持服务端过滤。 +- **涉及组件**: + - `src/modules/classes/components/students-table.tsx` + - `src/modules/classes/components/students-filters.tsx` + +### 8.2 班级详情页重构 (Class Detail Dashboard) +- **布局重构**: 采用响应式双栏布局 (Main Content + Sidebar),提升空间利用率。 +- **核心指标 (Key Metrics)**: 顶部增加 4 卡片统计网格: + - **Total Students**: 活跃/非活跃人数细分。 + - **Schedule Items**: 每周课程数。 + - **Active Assignments**: 活跃作业数与逾期数。 + - **Class Average**: 基于已评分作业的平均分。 +- **侧边栏小部件**: + - **Class Schedule**: 快速查看近期课程。 + - **Homework History**: 快速查看历史作业状态。 +- **涉及页面**: + - `src/app/(dashboard)/teacher/classes/my/[id]/page.tsx` + +### 8.3 数据访问层更新 +- **getClassStudents**: 扩展查询字段(头像、性别、加入时间),支持 `status` 过滤参数。 + +### 8.4 权限与流程调整 (Role Separation) +- **教师端变更**: + - 移除了“创建班级”入口,教师不再直接创建班级。 + - 新增“加入班级” (Join Class) 功能,通过 6 位邀请码加入已由管理员创建的班级。 + - 涉及组件:`src/modules/classes/components/my-classes-grid.tsx` + +## 9. 年级管理端班级模块 (2026-01-14) + +**目标**: 实现年级维度的班级集中管理,支持年级组长与管理员统一创建与维护班级。 + +### 9.1 路由与入口 +- **路由**: `src/app/(dashboard)/management/grade/classes/page.tsx` +- **权限**: 仅限拥有年级管理权限的角色(Grade Director / Teaching Head / Admin)。 + +### 9.2 功能特性 +- **GradeClassesView**: + - 展示用户所管理年级的所有班级列表。 + - 支持按年级筛选。 + - **CRUD**: 提供创建、编辑、删除班级的完整能力。 + - **RBAC**: 操作前校验用户对目标年级的管理权限。 + +### 9.3 核心变更 +- **Data Access**: 新增 `getManagedGrades` 与 `getGradeManagedClasses`。 +- **Actions**: 新增 `createGradeClassAction` 等带权限校验的 Server Actions。 diff --git a/docs/design/003_textbooks_module_implementation.md b/docs/design/003_textbooks_module_implementation.md index a15977b..9e1f701 100644 --- a/docs/design/003_textbooks_module_implementation.md +++ b/docs/design/003_textbooks_module_implementation.md @@ -1,7 +1,7 @@ # Textbooks Module Implementation Details **Date**: 2025-12-23 -**Updated**: 2025-12-31 +**Updated**: 2026-01-13 **Author**: DevOps Architect **Module**: Textbooks (`src/modules/textbooks`) @@ -143,6 +143,51 @@ src/ * 通过 `npm run lint / typecheck / build`。 ## 8. 后续计划 (Next Steps) -* [ ] **富文本编辑器**: 集成编辑器替换当前 Markdown Textarea,提升编辑体验。 -* [ ] **拖拽排序**: 实现章节树拖拽排序与持久化。 +* [x] **富文本编辑器**: 已集成 Tiptap 富文本编辑器,支持 Markdown 读写、即时预览与工具栏操作。 +* [x] **拖拽排序**: 实现章节树拖拽排序与持久化。 * [ ] **知识点能力增强**: 支持编辑、排序、分层(如需要)。 + +--- + +## 9. 界面与交互优化 (2026-01-12) + +**目标**: 提升教师端教材管理的视觉质感与操作体验,对齐 "International Typographic Style" 设计语言。 + +### 9.1 卡片与列表 (Textbook Card & Filters) +* **Dynamic Covers**: 卡片封面采用动态渐变色,根据科目 (Subject) 自动映射不同色系(如数学蓝、物理紫、生物绿),提升识别度。 +* **Information Density**: 增加元数据展示(Grade, Publisher, Chapter Count),并优化排版层级。 +* **Quick Actions**: 在卡片底部增加 "Edit Content" / "Delete" 快捷下拉菜单。 +* **Filters**: 简化筛选栏设计,移除厚重的容器背景,使其更轻量融入页面。 + +### 9.2 详情页工作台 (Detail Workbench) +* **Immersive Layout**: + * **Full Height**: 采用 `h-[calc(100vh-8rem)]` 撑满剩余空间,移除多余滚动条。 + * **Sticky Header**: 章节标题与操作栏吸顶,内容区独立滚动。 + * **Typography**: 引入 `prose-zinc` 与优化的字体排版,提升阅读舒适度。 +* **Sidebar Refinement**: + * **Chapter Tree**: 增加左侧边框线与层级缩进,选中态更明显;操作按钮(添加/删除)仅在 Hover 时显示,减少视觉干扰。 + * **Knowledge Points**: 改为卡片式列表,Hover 显示删除按钮;增加空状态引导。 + * **Drag & Drop**: 集成 `@dnd-kit` 实现章节拖拽排序,支持同级拖动并实时持久化到数据库。 + +### 9.3 富文本编辑器 (Rich Text Editor) +* **Tiptap Integration**: 引入 `@tiptap/react` 替换原有的 Textarea。 +* **Markdown Support**: 支持 Markdown 源码读写,保持数据格式兼容性。 +* **Toolbar**: 实现悬浮工具栏,支持 Bold, Italic, Headings, Lists, Blockquote 等常用格式。 +* **SSR Fix**: 解决 Tiptap 在 Next.js 中的 Hydration Mismatch 问题 (`immediatelyRender: false`)。 + +### 9.4 系统组件优化 (UI Components) +* **Dialog**: + * 优化遮罩层 (`backdrop-blur`) 与弹窗阴影,提升通透感。 + * 调整动画时长 (`duration-200`) 与缓动,移除位移动画,改为纯净的 Fade + Zoom 效果。 + * 增加内部间距 (`gap-6`) 与圆角 (`rounded-xl`),使排版更现代。 + * **Create Chapter Dialog**: 优化触发按钮样式,增加 `sr-only` 辅助文本,修复点击区域过小的问题。 + +--- + +## 10. 近期改进 (2026-01-13) + +### 10.1 导航体验 (Navigation) +* **Dynamic Breadcrumbs**: 接入全局动态面包屑系统。 + * 支持从路由路径(e.g., `/teacher/textbooks/123`)自动生成层级导航。 + * 解决了深层嵌套页面(如教材详情页)缺乏上下文回退路径的问题。 + diff --git a/docs/design/005_exam_module_implementation.md b/docs/design/005_exam_module_implementation.md index 186e5d5..0ff03c5 100644 --- a/docs/design/005_exam_module_implementation.md +++ b/docs/design/005_exam_module_implementation.md @@ -153,7 +153,37 @@ type ExamNode = { - `getExams(params)`: 支持按 `q/status` 在数据库侧过滤;`difficulty` 因当前存储在 `description` JSON 中,采用内存过滤 - `getExamById(id)`: 查询 exam 及其 `exam_questions`,并返回 `structure` 以用于构建器 Hydrate -## 7. 变更记录(合并 Homework) +### 6.5 `getExamPreviewAction` (新增) +- **入参**: `examId` (string) +- **行为**: + - 查询指定 exam 及其关联的 questions (通过 `exam_questions` 关系)。 + - 返回完整的 `structure` (JSON 树) 和扁平化的 `questions` 列表。 + - 用于预览弹窗的数据加载。 + +## 7. 变更记录 + +**日期**:2026-01-12 (当前) + +- **列表页优化 (`/teacher/exams/all`)**: + - 移除了冗余的 "All Exams" 页面标题和描述。 + - 重构了表格列 (`ExamColumns`): + - 合并标题、标签、科目、年级为 "Exam Info" 列。 + - 合并题目数、总分、时长为 "Stats" 列。 + - 合并创建时间和预定时间为 "Date" 列。 + - 优化了状态 (Status) 和难度 (Difficulty) 的视觉样式 (Badge, Progress bar)。 + - 优化了表格分页和布局 (`ExamDataTable`)。 + +- **预览功能增强**: + - 新增直接预览功能:在操作列添加了 "View" (眼睛图标) 按钮。 + - 点击 "View" 触发 `getExamPreviewAction` 获取完整试卷数据。 + - 弹窗 (`Dialog`) 直接展示试卷内容 (`ExamPaperPreview`),移除了冗余的头部描述,优化了滚动体验。 + - 修复了可访问性问题 (DialogTitle)。 + +- **组卷页面升级 (`/teacher/exams/[id]/build`)**: + - **布局重构**: 扩展工作区高度,调整左右面板比例 (2:1),优化头部信息展示和进度可视化。 + - **题库增强**: 实现了基于 Server Action (`getQuestionsAction`) 的分页加载和服务器端筛选,提升大数据量下的性能;优化了搜索和筛选器 UI。 + - **预览优化**: 移除了内联预览,改为通过 "Preview" 按钮触发弹窗预览,避免干扰编辑流。 + - **视觉降噪**: 移除了页面顶部冗余的标题和描述。 **日期**:2025-12-31 diff --git a/docs/design/006_homework_module_implementation.md b/docs/design/006_homework_module_implementation.md index 50df14b..a1f86db 100644 --- a/docs/design/006_homework_module_implementation.md +++ b/docs/design/006_homework_module_implementation.md @@ -268,3 +268,39 @@ - `npm run lint`: 通过 - `npm run typecheck`: 通过 - `npm run build`: 通过 + +--- + +## 12. UI/UX 优化更新(2026-01-12) + +### 12.1 教师端作业列表 (`/teacher/homework/assignments`) + +- **表格重构**: 从简单的卡片列表升级为功能丰富的数据表格(Table)。 +- **信息增强**: 合并展示标题/时间,使用 Badge 区分状态,清晰展示截止日期(含 Late 标记),可视化提交进度。 +- **操作便捷**: 每行增加操作菜单(Actions),支持快速跳转详情或提交列表。 + +### 12.2 作业详情页 (`/teacher/homework/assignments/[id]`) + +- **布局重构**: + - **Sticky Header**: 头部信息栏(标题、状态、面包屑)随滚动吸顶,但后续优化为随页面滚动(移除 Sticky)以节省空间。 + - **关键指标**: 将截止日期、目标数、提交数、已批改数整合到头部下方,使用图标增强可读性。 + - **双栏布局**: 主体内容分为“Performance Analytics”(分析)和“Assignment Content”(内容)两部分。 +- **图表升级**: + - 重构 `HomeworkAssignmentQuestionErrorOverviewCard`,废弃 SVG,改用 **Recharts** 实现柱状图(BarChart)。 + - 增强交互:支持 Tooltip 悬停查看具体题目错误率和人数。 +- **详情面板优化**: + - 移除了冗余的 `HomeworkAssignmentQuestionErrorDetailsCard`。 + - 深度优化 `HomeworkAssignmentQuestionErrorDetailPanel`: + - 增加饼图展示单题错误率。 + - 错误答案列表卡片化,清晰展示每个错误答案的内容及选择人数。 + - 整合预览面板与详情面板,提供更连贯的“左侧选题-右侧分析”体验。 + +--- + +## 13. Bug 修复与完善 (2026-01-13) + +### 13.1 批改视图 (Grading View) +- **Type Safety Fix**: 修复了 `HomeworkGradingView` 组件中的 TypeScript 类型错误。 + - 问题:`isCorrect` 字段可能为 `boolean | null`,直接用于 JSX 渲染导致 "Type 'unknown' is not assignable to type 'ReactNode'" 错误。 + - 修复:增加显式布尔值检查 `opt.isCorrect === true`,确保 React 条件渲染接收到合法的 boolean 值。 + diff --git a/docs/design/design_system.md b/docs/design/design_system.md index 1dd4dab..b802c85 100644 --- a/docs/design/design_system.md +++ b/docs/design/design_system.md @@ -114,7 +114,10 @@ Next_Edu 旨在对抗教育系统常见的信息过载。我们的设计风格 * **Height**: `64px` (h-16). * **Layout**: `flex items-center justify-between px-6 border-b`. * **Components**: - 1. **Breadcrumb**: 显示当前路径,层级清晰。 + 1. **Breadcrumb**: 动态路径导航 (Dynamic Breadcrumb). + * **Implementation**: 基于 `usePathname()` 自动解析路由段。 + * **Mapping**: 通过 `NAV_CONFIG` 或 `BREADCRUMB_MAP` 映射路径到友好标题 (e.g., `/teacher/textbooks` -> "Textbooks"). + * **Filtering**: 自动过滤根角色路径 (e.g., `/teacher`) 以保持简洁。 2. **Global Search**: `Cmd+K` 触发,居中或靠右。 3. **User Nav**: 头像 + 下拉菜单。 diff --git a/docs/work_log.md b/docs/work_log.md new file mode 100644 index 0000000..058dd09 --- /dev/null +++ b/docs/work_log.md @@ -0,0 +1,78 @@ +# Work Log + +## 2026-01-14 + +### 1. Class Management Refactoring (Role Separation) +* **Separation of Duties**: + * Moved class creation and management responsibilities from the generic Teacher view to a dedicated Management view. + * Created **Grade Management Page** at `src/app/(dashboard)/management/grade/classes/page.tsx` for Grade Directors and Admins. + * Teachers can now only **Join Classes** (via code) or view their assigned classes in "My Classes". + +* **New Components & Pages**: + * `GradeClassesView` (`src/modules/classes/components/grade-classes-view.tsx`): A comprehensive table view for managing classes within specific grades, supporting creation, editing, and deletion. + * `GradeClassesPage`: Server Component that fetches managed grades and classes using strict RBAC (Role-Based Access Control). + +* **Teacher "My Classes" Update (`my-classes-grid.tsx`)**: + * Removed the "Create Class" button/dialog. + * Added a **"Join Class"** dialog that accepts a 6-digit invitation code. + * Updated styling to use standard design system colors (`bg-card`, `border-border`) instead of hardcoded gradients. + +### 2. Backend & Logic Updates +* **Data Access (`data-access.ts`)**: + * Implemented `getGradeManagedClasses`: Fetches classes for grades where the user is either a Grade Head or Teaching Head. + * Implemented `getManagedGrades`: Fetches the list of grades managed by the user for the creation dropdown. + * Updated `getTeacherClasses`: Now returns both **owned classes** (assigned by admin) and **enrolled classes** (joined via code). + * Fixed a SQL syntax error in `getGradeManagedClasses` (unescaped backticks in template literal). + +* **Server Actions (`actions.ts`)**: + * Added `createGradeClassAction`, `updateGradeClassAction`, `deleteGradeClassAction`: These actions enforce that the user manages the target grade before performing operations. + * Updated `joinClassByInvitationCodeAction`: Expanded to allow Teachers (role `teacher`) to join classes, not just Students. + +### 3. Verification +* **RBAC**: Verified that users can only manage classes for grades they are assigned to. +* **Flow**: Verified Teacher "Join Class" flow correctly redirects and updates the list. +* **Syntax**: Fixed TypeScript/SQL syntax errors in the new data access functions. + +### 4. Class UI/UX Optimization +* **Students Management Interface (`students-table.tsx`, `students-filters.tsx`)**: + * **Enhanced Table**: Added student avatars, gender display, and join date. + * **Pagination**: Implemented client-side pagination (10 items per page) to handle larger classes gracefully. + * **Status Filtering**: Added "Active/Inactive" filter with visual status badges (Emerald for active, muted for inactive). + * **Data Access**: Updated `getClassStudents` to fetch extended user profile data and support server-side status filtering. + +* **Class Detail Dashboard (`/teacher/classes/my/[id]/page.tsx`)**: + * **Dashboard Layout**: Refactored into a responsive two-column layout (Main Content + Sidebar). + * **Key Metrics**: Added a 4-card stats grid at the top displaying critical insights: + * Total Students (Active/Inactive breakdown) + * Schedule Items (Weekly sessions) + * Active Assignments (Overdue count) + * Class Average (Based on graded submissions) + * **Sidebar Widgets**: Added "Class Schedule" and "Homework History" widgets for quick access to temporal data. + * **Visual Polish**: Integrated `lucide-react` icons throughout for better information scanning. + +## 2026-01-13 + +### 1. Navigation & Layout Improvements +* **Dynamic Breadcrumbs (`site-header.tsx`)**: + * Replaced hardcoded "Dashboard > Overview" breadcrumbs with a dynamic system. + * Implemented a path-to-title lookup using `NAV_CONFIG` from `src/modules/layout/config/navigation.ts`. + * Added logic to filter out root role segments (admin/teacher/student/parent) for cleaner paths. + * Added fallback capitalization for segments not found in the config. + * Refactored `SiteHeader` to use `usePathname` for real-time route updates. + +### 2. Code Quality & Bug Fixes +* **Type Safety (`homework-grading-view.tsx`)**: + * Fixed a TypeScript error where a boolean expression was returning `boolean | undefined` which is not a valid React node (implicit `true` check added). + * Resolved "Calling setState synchronously within an effect" React warning by initializing state lazily instead of using `useEffect`. + * Fixed implicit `any` type errors in map functions. +* **Linting**: + * Cleaned up unused imports across multiple files (`exam-actions.tsx`, `exam-assembly.tsx`, `textbook-reader.tsx`, etc.). + * Fixed unescaped HTML entities in `student-dashboard-header.tsx` and others. + * Removed unused variables to clear ESLint warnings. +* **Refactoring**: + * Updated `TextbookCard` to support `hideActions` prop for better reuse in student views. + * Added missing `Progress` component to `src/shared/components/ui/progress.tsx`. + +### 3. Verification +* Ran `npm run typecheck`: **Passed** (0 errors). +* Ran `npm run lint`: **Passed** (0 errors, 28 warnings remaining for unused vars/components that may be needed later). diff --git a/drizzle/0007_add_class_invitation_code.sql b/drizzle/0007_add_class_invitation_code.sql deleted file mode 100644 index 033267b..0000000 --- a/drizzle/0007_add_class_invitation_code.sql +++ /dev/null @@ -1,3 +0,0 @@ -ALTER TABLE `classes` ADD `invitation_code` varchar(6); ---> statement-breakpoint -ALTER TABLE `classes` ADD CONSTRAINT `classes_invitation_code_unique` UNIQUE(`invitation_code`); diff --git a/drizzle/0007_talented_bromley.sql b/drizzle/0007_talented_bromley.sql new file mode 100644 index 0000000..e6d0af1 --- /dev/null +++ b/drizzle/0007_talented_bromley.sql @@ -0,0 +1,6 @@ +ALTER TABLE `exams` ADD `subject_id` varchar(128);--> statement-breakpoint +ALTER TABLE `exams` ADD `grade_id` varchar(128);--> statement-breakpoint +ALTER TABLE `exams` ADD CONSTRAINT `exams_subject_id_subjects_id_fk` FOREIGN KEY (`subject_id`) REFERENCES `subjects`(`id`) ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `exams` ADD CONSTRAINT `exams_grade_id_grades_id_fk` FOREIGN KEY (`grade_id`) REFERENCES `grades`(`id`) ON DELETE no action ON UPDATE no action;--> statement-breakpoint +CREATE INDEX `exams_subject_idx` ON `exams` (`subject_id`);--> statement-breakpoint +CREATE INDEX `exams_grade_idx` ON `exams` (`grade_id`); \ No newline at end of file diff --git a/drizzle/0008_add_user_profile_fields.sql b/drizzle/0008_add_user_profile_fields.sql deleted file mode 100644 index 56effd0..0000000 --- a/drizzle/0008_add_user_profile_fields.sql +++ /dev/null @@ -1,17 +0,0 @@ -ALTER TABLE `users` ADD `phone` varchar(30); ---> statement-breakpoint -ALTER TABLE `users` ADD `address` varchar(255); ---> statement-breakpoint -ALTER TABLE `users` ADD `gender` varchar(20); ---> statement-breakpoint -ALTER TABLE `users` ADD `age` int; ---> statement-breakpoint -ALTER TABLE `users` ADD `grade_id` varchar(128); ---> statement-breakpoint -ALTER TABLE `users` ADD `department_id` varchar(128); ---> statement-breakpoint -ALTER TABLE `users` ADD `onboarded_at` timestamp; ---> statement-breakpoint -CREATE INDEX `users_grade_id_idx` ON `users` (`grade_id`); ---> statement-breakpoint -CREATE INDEX `users_department_id_idx` ON `users` (`department_id`); diff --git a/drizzle/meta/0007_snapshot.json b/drizzle/meta/0007_snapshot.json new file mode 100644 index 0000000..3c5305c --- /dev/null +++ b/drizzle/meta/0007_snapshot.json @@ -0,0 +1,3064 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "a6d95d47-4400-464e-bc53-45735dd6e3e3", + "prevId": "5eaf9185-8a1e-4e35-8144-553aec7ff31f", + "tables": { + "academic_years": { + "name": "academic_years", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "start_date": { + "name": "start_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "end_date": { + "name": "end_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "academic_years_name_idx": { + "name": "academic_years_name_idx", + "columns": [ + "name" + ], + "isUnique": false + }, + "academic_years_active_idx": { + "name": "academic_years_active_idx", + "columns": [ + "is_active" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "academic_years_id": { + "name": "academic_years_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "academic_years_name_unique": { + "name": "academic_years_name_unique", + "columns": [ + "name" + ] + } + }, + "checkConstraint": {} + }, + "accounts": { + "name": "accounts", + "columns": { + "userId": { + "name": "userId", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_type": { + "name": "token_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_state": { + "name": "session_state", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "accounts_userId_users_id_fk": { + "name": "accounts_userId_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "accounts_provider_providerAccountId_pk": { + "name": "accounts_provider_providerAccountId_pk", + "columns": [ + "provider", + "providerAccountId" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "chapters": { + "name": "chapters", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "textbook_id": { + "name": "textbook_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "parent_id": { + "name": "parent_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "textbook_idx": { + "name": "textbook_idx", + "columns": [ + "textbook_id" + ], + "isUnique": false + }, + "parent_id_idx": { + "name": "parent_id_idx", + "columns": [ + "parent_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "chapters_textbook_id_textbooks_id_fk": { + "name": "chapters_textbook_id_textbooks_id_fk", + "tableFrom": "chapters", + "tableTo": "textbooks", + "columnsFrom": [ + "textbook_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "chapters_id": { + "name": "chapters_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "class_enrollments": { + "name": "class_enrollments", + "columns": { + "class_id": { + "name": "class_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "student_id": { + "name": "student_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "class_enrollment_status": { + "name": "class_enrollment_status", + "type": "enum('active','inactive')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "class_enrollments_class_idx": { + "name": "class_enrollments_class_idx", + "columns": [ + "class_id" + ], + "isUnique": false + }, + "class_enrollments_student_idx": { + "name": "class_enrollments_student_idx", + "columns": [ + "student_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ce_c_fk": { + "name": "ce_c_fk", + "tableFrom": "class_enrollments", + "tableTo": "classes", + "columnsFrom": [ + "class_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ce_s_fk": { + "name": "ce_s_fk", + "tableFrom": "class_enrollments", + "tableTo": "users", + "columnsFrom": [ + "student_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "class_enrollments_class_id_student_id_pk": { + "name": "class_enrollments_class_id_student_id_pk", + "columns": [ + "class_id", + "student_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "class_schedule": { + "name": "class_schedule", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "class_id": { + "name": "class_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "weekday": { + "name": "weekday", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "start_time": { + "name": "start_time", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "end_time": { + "name": "end_time", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "course": { + "name": "course", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "location": { + "name": "location", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "class_schedule_class_idx": { + "name": "class_schedule_class_idx", + "columns": [ + "class_id" + ], + "isUnique": false + }, + "class_schedule_class_day_idx": { + "name": "class_schedule_class_day_idx", + "columns": [ + "class_id", + "weekday" + ], + "isUnique": false + } + }, + "foreignKeys": { + "cs_c_fk": { + "name": "cs_c_fk", + "tableFrom": "class_schedule", + "tableTo": "classes", + "columnsFrom": [ + "class_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "class_schedule_id": { + "name": "class_schedule_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "class_subject_teachers": { + "name": "class_subject_teachers", + "columns": { + "class_id": { + "name": "class_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "enum('语文','数学','英语','美术','体育','科学','社会','音乐')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "teacher_id": { + "name": "teacher_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "class_subject_teachers_class_idx": { + "name": "class_subject_teachers_class_idx", + "columns": [ + "class_id" + ], + "isUnique": false + }, + "class_subject_teachers_teacher_idx": { + "name": "class_subject_teachers_teacher_idx", + "columns": [ + "teacher_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "class_subject_teachers_teacher_id_users_id_fk": { + "name": "class_subject_teachers_teacher_id_users_id_fk", + "tableFrom": "class_subject_teachers", + "tableTo": "users", + "columnsFrom": [ + "teacher_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cst_c_fk": { + "name": "cst_c_fk", + "tableFrom": "class_subject_teachers", + "tableTo": "classes", + "columnsFrom": [ + "class_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "class_subject_teachers_class_id_subject_pk": { + "name": "class_subject_teachers_class_id_subject_pk", + "columns": [ + "class_id", + "subject" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "classes": { + "name": "classes", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "school_name": { + "name": "school_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "school_id": { + "name": "school_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "grade": { + "name": "grade", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "grade_id": { + "name": "grade_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "homeroom": { + "name": "homeroom", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "room": { + "name": "room", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invitation_code": { + "name": "invitation_code", + "type": "varchar(6)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "teacher_id": { + "name": "teacher_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "classes_teacher_idx": { + "name": "classes_teacher_idx", + "columns": [ + "teacher_id" + ], + "isUnique": false + }, + "classes_grade_idx": { + "name": "classes_grade_idx", + "columns": [ + "grade" + ], + "isUnique": false + }, + "classes_school_idx": { + "name": "classes_school_idx", + "columns": [ + "school_id" + ], + "isUnique": false + }, + "classes_grade_id_idx": { + "name": "classes_grade_id_idx", + "columns": [ + "grade_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "classes_teacher_id_users_id_fk": { + "name": "classes_teacher_id_users_id_fk", + "tableFrom": "classes", + "tableTo": "users", + "columnsFrom": [ + "teacher_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "c_s_fk": { + "name": "c_s_fk", + "tableFrom": "classes", + "tableTo": "schools", + "columnsFrom": [ + "school_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "c_g_fk": { + "name": "c_g_fk", + "tableFrom": "classes", + "tableTo": "grades", + "columnsFrom": [ + "grade_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "classes_id": { + "name": "classes_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "classes_invitation_code_unique": { + "name": "classes_invitation_code_unique", + "columns": [ + "invitation_code" + ] + } + }, + "checkConstraint": {} + }, + "classrooms": { + "name": "classrooms", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "building": { + "name": "building", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "floor": { + "name": "floor", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "capacity": { + "name": "capacity", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "classrooms_name_idx": { + "name": "classrooms_name_idx", + "columns": [ + "name" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "classrooms_id": { + "name": "classrooms_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "classrooms_name_unique": { + "name": "classrooms_name_unique", + "columns": [ + "name" + ] + } + }, + "checkConstraint": {} + }, + "departments": { + "name": "departments", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "departments_name_idx": { + "name": "departments_name_idx", + "columns": [ + "name" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "departments_id": { + "name": "departments_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "departments_name_unique": { + "name": "departments_name_unique", + "columns": [ + "name" + ] + } + }, + "checkConstraint": {} + }, + "exam_questions": { + "name": "exam_questions", + "columns": { + "exam_id": { + "name": "exam_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "question_id": { + "name": "question_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "score": { + "name": "score", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "order": { + "name": "order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "exam_questions_exam_id_exams_id_fk": { + "name": "exam_questions_exam_id_exams_id_fk", + "tableFrom": "exam_questions", + "tableTo": "exams", + "columnsFrom": [ + "exam_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "exam_questions_question_id_questions_id_fk": { + "name": "exam_questions_question_id_questions_id_fk", + "tableFrom": "exam_questions", + "tableTo": "questions", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "exam_questions_exam_id_question_id_pk": { + "name": "exam_questions_exam_id_question_id_pk", + "columns": [ + "exam_id", + "question_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "exam_submissions": { + "name": "exam_submissions", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "exam_id": { + "name": "exam_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "student_id": { + "name": "student_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "score": { + "name": "score", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'started'" + }, + "submitted_at": { + "name": "submitted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "exam_student_idx": { + "name": "exam_student_idx", + "columns": [ + "exam_id", + "student_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "exam_submissions_exam_id_exams_id_fk": { + "name": "exam_submissions_exam_id_exams_id_fk", + "tableFrom": "exam_submissions", + "tableTo": "exams", + "columnsFrom": [ + "exam_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "exam_submissions_student_id_users_id_fk": { + "name": "exam_submissions_student_id_users_id_fk", + "tableFrom": "exam_submissions", + "tableTo": "users", + "columnsFrom": [ + "student_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "exam_submissions_id": { + "name": "exam_submissions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "exams": { + "name": "exams", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "structure": { + "name": "structure", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "creator_id": { + "name": "creator_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_id": { + "name": "subject_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "grade_id": { + "name": "grade_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "start_time": { + "name": "start_time", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "end_time": { + "name": "end_time", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'draft'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "exams_subject_idx": { + "name": "exams_subject_idx", + "columns": [ + "subject_id" + ], + "isUnique": false + }, + "exams_grade_idx": { + "name": "exams_grade_idx", + "columns": [ + "grade_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "exams_creator_id_users_id_fk": { + "name": "exams_creator_id_users_id_fk", + "tableFrom": "exams", + "tableTo": "users", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "exams_subject_id_subjects_id_fk": { + "name": "exams_subject_id_subjects_id_fk", + "tableFrom": "exams", + "tableTo": "subjects", + "columnsFrom": [ + "subject_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "exams_grade_id_grades_id_fk": { + "name": "exams_grade_id_grades_id_fk", + "tableFrom": "exams", + "tableTo": "grades", + "columnsFrom": [ + "grade_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "exams_id": { + "name": "exams_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "grades": { + "name": "grades", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "school_id": { + "name": "school_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "grade_head_id": { + "name": "grade_head_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "teaching_head_id": { + "name": "teaching_head_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "grades_school_idx": { + "name": "grades_school_idx", + "columns": [ + "school_id" + ], + "isUnique": false + }, + "grades_school_name_uniq": { + "name": "grades_school_name_uniq", + "columns": [ + "school_id", + "name" + ], + "isUnique": false + }, + "grades_grade_head_idx": { + "name": "grades_grade_head_idx", + "columns": [ + "grade_head_id" + ], + "isUnique": false + }, + "grades_teaching_head_idx": { + "name": "grades_teaching_head_idx", + "columns": [ + "teaching_head_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "g_s_fk": { + "name": "g_s_fk", + "tableFrom": "grades", + "tableTo": "schools", + "columnsFrom": [ + "school_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "g_gh_fk": { + "name": "g_gh_fk", + "tableFrom": "grades", + "tableTo": "users", + "columnsFrom": [ + "grade_head_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "g_th_fk": { + "name": "g_th_fk", + "tableFrom": "grades", + "tableTo": "users", + "columnsFrom": [ + "teaching_head_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "grades_id": { + "name": "grades_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "homework_answers": { + "name": "homework_answers", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "submission_id": { + "name": "submission_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "question_id": { + "name": "question_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "answer_content": { + "name": "answer_content", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "score": { + "name": "score", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "hw_answer_submission_idx": { + "name": "hw_answer_submission_idx", + "columns": [ + "submission_id" + ], + "isUnique": false + }, + "hw_answer_submission_question_idx": { + "name": "hw_answer_submission_question_idx", + "columns": [ + "submission_id", + "question_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "hw_ans_sub_fk": { + "name": "hw_ans_sub_fk", + "tableFrom": "homework_answers", + "tableTo": "homework_submissions", + "columnsFrom": [ + "submission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "hw_ans_q_fk": { + "name": "hw_ans_q_fk", + "tableFrom": "homework_answers", + "tableTo": "questions", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homework_answers_id": { + "name": "homework_answers_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "homework_assignment_questions": { + "name": "homework_assignment_questions", + "columns": { + "assignment_id": { + "name": "assignment_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "question_id": { + "name": "question_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "score": { + "name": "score", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "order": { + "name": "order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "hw_assignment_questions_assignment_idx": { + "name": "hw_assignment_questions_assignment_idx", + "columns": [ + "assignment_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "hw_aq_a_fk": { + "name": "hw_aq_a_fk", + "tableFrom": "homework_assignment_questions", + "tableTo": "homework_assignments", + "columnsFrom": [ + "assignment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "hw_aq_q_fk": { + "name": "hw_aq_q_fk", + "tableFrom": "homework_assignment_questions", + "tableTo": "questions", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homework_assignment_questions_assignment_id_question_id_pk": { + "name": "homework_assignment_questions_assignment_id_question_id_pk", + "columns": [ + "assignment_id", + "question_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "homework_assignment_targets": { + "name": "homework_assignment_targets", + "columns": { + "assignment_id": { + "name": "assignment_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "student_id": { + "name": "student_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "hw_assignment_targets_assignment_idx": { + "name": "hw_assignment_targets_assignment_idx", + "columns": [ + "assignment_id" + ], + "isUnique": false + }, + "hw_assignment_targets_student_idx": { + "name": "hw_assignment_targets_student_idx", + "columns": [ + "student_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "hw_at_a_fk": { + "name": "hw_at_a_fk", + "tableFrom": "homework_assignment_targets", + "tableTo": "homework_assignments", + "columnsFrom": [ + "assignment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "hw_at_s_fk": { + "name": "hw_at_s_fk", + "tableFrom": "homework_assignment_targets", + "tableTo": "users", + "columnsFrom": [ + "student_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homework_assignment_targets_assignment_id_student_id_pk": { + "name": "homework_assignment_targets_assignment_id_student_id_pk", + "columns": [ + "assignment_id", + "student_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "homework_assignments": { + "name": "homework_assignments", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_exam_id": { + "name": "source_exam_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "structure": { + "name": "structure", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'draft'" + }, + "creator_id": { + "name": "creator_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "available_at": { + "name": "available_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "due_at": { + "name": "due_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "allow_late": { + "name": "allow_late", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "late_due_at": { + "name": "late_due_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "max_attempts": { + "name": "max_attempts", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "hw_assignment_creator_idx": { + "name": "hw_assignment_creator_idx", + "columns": [ + "creator_id" + ], + "isUnique": false + }, + "hw_assignment_source_exam_idx": { + "name": "hw_assignment_source_exam_idx", + "columns": [ + "source_exam_id" + ], + "isUnique": false + }, + "hw_assignment_status_idx": { + "name": "hw_assignment_status_idx", + "columns": [ + "status" + ], + "isUnique": false + } + }, + "foreignKeys": { + "hw_asg_exam_fk": { + "name": "hw_asg_exam_fk", + "tableFrom": "homework_assignments", + "tableTo": "exams", + "columnsFrom": [ + "source_exam_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "hw_asg_creator_fk": { + "name": "hw_asg_creator_fk", + "tableFrom": "homework_assignments", + "tableTo": "users", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homework_assignments_id": { + "name": "homework_assignments_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "homework_submissions": { + "name": "homework_submissions", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "assignment_id": { + "name": "assignment_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "student_id": { + "name": "student_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "attempt_no": { + "name": "attempt_no", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "score": { + "name": "score", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'started'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "submitted_at": { + "name": "submitted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_late": { + "name": "is_late", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "hw_assignment_student_idx": { + "name": "hw_assignment_student_idx", + "columns": [ + "assignment_id", + "student_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "hw_sub_a_fk": { + "name": "hw_sub_a_fk", + "tableFrom": "homework_submissions", + "tableTo": "homework_assignments", + "columnsFrom": [ + "assignment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "hw_sub_student_fk": { + "name": "hw_sub_student_fk", + "tableFrom": "homework_submissions", + "tableTo": "users", + "columnsFrom": [ + "student_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homework_submissions_id": { + "name": "homework_submissions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "knowledge_points": { + "name": "knowledge_points", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_id": { + "name": "parent_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "chapter_id": { + "name": "chapter_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "level": { + "name": "level", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "order": { + "name": "order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "parent_id_idx": { + "name": "parent_id_idx", + "columns": [ + "parent_id" + ], + "isUnique": false + }, + "kp_chapter_id_idx": { + "name": "kp_chapter_id_idx", + "columns": [ + "chapter_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "knowledge_points_id": { + "name": "knowledge_points_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "questions": { + "name": "questions", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "json", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "enum('single_choice','multiple_choice','text','judgment','composite')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "difficulty": { + "name": "difficulty", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 1 + }, + "parent_id": { + "name": "parent_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "author_id": { + "name": "author_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "parent_id_idx": { + "name": "parent_id_idx", + "columns": [ + "parent_id" + ], + "isUnique": false + }, + "author_id_idx": { + "name": "author_id_idx", + "columns": [ + "author_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "questions_author_id_users_id_fk": { + "name": "questions_author_id_users_id_fk", + "tableFrom": "questions", + "tableTo": "users", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "questions_id": { + "name": "questions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "questions_to_knowledge_points": { + "name": "questions_to_knowledge_points", + "columns": { + "question_id": { + "name": "question_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "knowledge_point_id": { + "name": "knowledge_point_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "kp_idx": { + "name": "kp_idx", + "columns": [ + "knowledge_point_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "q_kp_qid_fk": { + "name": "q_kp_qid_fk", + "tableFrom": "questions_to_knowledge_points", + "tableTo": "questions", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "q_kp_kpid_fk": { + "name": "q_kp_kpid_fk", + "tableFrom": "questions_to_knowledge_points", + "tableTo": "knowledge_points", + "columnsFrom": [ + "knowledge_point_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "questions_to_knowledge_points_question_id_knowledge_point_id_pk": { + "name": "questions_to_knowledge_points_question_id_knowledge_point_id_pk", + "columns": [ + "question_id", + "knowledge_point_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "roles_id": { + "name": "roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ] + } + }, + "checkConstraint": {} + }, + "schools": { + "name": "schools", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "schools_name_idx": { + "name": "schools_name_idx", + "columns": [ + "name" + ], + "isUnique": false + }, + "schools_code_idx": { + "name": "schools_code_idx", + "columns": [ + "code" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "schools_id": { + "name": "schools_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "schools_name_unique": { + "name": "schools_name_unique", + "columns": [ + "name" + ] + }, + "schools_code_unique": { + "name": "schools_code_unique", + "columns": [ + "code" + ] + } + }, + "checkConstraint": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "sessionToken": { + "name": "sessionToken", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_userId_users_id_fk": { + "name": "sessions_userId_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sessions_sessionToken": { + "name": "sessions_sessionToken", + "columns": [ + "sessionToken" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "subjects": { + "name": "subjects", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "subjects_name_idx": { + "name": "subjects_name_idx", + "columns": [ + "name" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "subjects_id": { + "name": "subjects_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "subjects_name_unique": { + "name": "subjects_name_unique", + "columns": [ + "name" + ] + }, + "subjects_code_unique": { + "name": "subjects_code_unique", + "columns": [ + "code" + ] + } + }, + "checkConstraint": {} + }, + "submission_answers": { + "name": "submission_answers", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "submission_id": { + "name": "submission_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "question_id": { + "name": "question_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "answer_content": { + "name": "answer_content", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "score": { + "name": "score", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "submission_idx": { + "name": "submission_idx", + "columns": [ + "submission_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "submission_answers_submission_id_exam_submissions_id_fk": { + "name": "submission_answers_submission_id_exam_submissions_id_fk", + "tableFrom": "submission_answers", + "tableTo": "exam_submissions", + "columnsFrom": [ + "submission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "submission_answers_question_id_questions_id_fk": { + "name": "submission_answers_question_id_questions_id_fk", + "tableFrom": "submission_answers", + "tableTo": "questions", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "submission_answers_id": { + "name": "submission_answers_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "textbooks": { + "name": "textbooks", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "grade": { + "name": "grade", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "publisher": { + "name": "publisher", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "textbooks_id": { + "name": "textbooks_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "emailVerified": { + "name": "emailVerified", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'student'" + }, + "password": { + "name": "password", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phone": { + "name": "phone", + "type": "varchar(30)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address": { + "name": "address", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "gender": { + "name": "gender", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "age": { + "name": "age", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "grade_id": { + "name": "grade_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "department_id": { + "name": "department_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "onboarded_at": { + "name": "onboarded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "email_idx": { + "name": "email_idx", + "columns": [ + "email" + ], + "isUnique": false + }, + "users_grade_id_idx": { + "name": "users_grade_id_idx", + "columns": [ + "grade_id" + ], + "isUnique": false + }, + "users_department_id_idx": { + "name": "users_department_id_idx", + "columns": [ + "department_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "columns": [ + "email" + ] + } + }, + "checkConstraint": {} + }, + "users_to_roles": { + "name": "users_to_roles", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "user_id_idx": { + "name": "user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "users_to_roles_user_id_users_id_fk": { + "name": "users_to_roles_user_id_users_id_fk", + "tableFrom": "users_to_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "users_to_roles_role_id_roles_id_fk": { + "name": "users_to_roles_role_id_roles_id_fk", + "tableFrom": "users_to_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "users_to_roles_user_id_role_id_pk": { + "name": "users_to_roles_user_id_role_id_pk", + "columns": [ + "user_id", + "role_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "verificationTokens": { + "name": "verificationTokens", + "columns": { + "identifier": { + "name": "identifier", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationTokens_identifier_token_pk": { + "name": "verificationTokens_identifier_token_pk", + "columns": [ + "identifier", + "token" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0009_snapshot.json b/drizzle/meta/0009_snapshot.json new file mode 100644 index 0000000..b4ed61d --- /dev/null +++ b/drizzle/meta/0009_snapshot.json @@ -0,0 +1,3009 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "5eaf9185-8a1e-4e35-8144-553aec7ff31f", + "prevId": "3b23e056-3d79-4ea9-a03e-d1b5d56bafda", + "tables": { + "academic_years": { + "name": "academic_years", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "start_date": { + "name": "start_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "end_date": { + "name": "end_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "academic_years_name_idx": { + "name": "academic_years_name_idx", + "columns": [ + "name" + ], + "isUnique": false + }, + "academic_years_active_idx": { + "name": "academic_years_active_idx", + "columns": [ + "is_active" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "academic_years_id": { + "name": "academic_years_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "academic_years_name_unique": { + "name": "academic_years_name_unique", + "columns": [ + "name" + ] + } + }, + "checkConstraint": {} + }, + "accounts": { + "name": "accounts", + "columns": { + "userId": { + "name": "userId", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_type": { + "name": "token_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_state": { + "name": "session_state", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "accounts_userId_users_id_fk": { + "name": "accounts_userId_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "accounts_provider_providerAccountId_pk": { + "name": "accounts_provider_providerAccountId_pk", + "columns": [ + "provider", + "providerAccountId" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "chapters": { + "name": "chapters", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "textbook_id": { + "name": "textbook_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "parent_id": { + "name": "parent_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "textbook_idx": { + "name": "textbook_idx", + "columns": [ + "textbook_id" + ], + "isUnique": false + }, + "parent_id_idx": { + "name": "parent_id_idx", + "columns": [ + "parent_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "chapters_textbook_id_textbooks_id_fk": { + "name": "chapters_textbook_id_textbooks_id_fk", + "tableFrom": "chapters", + "tableTo": "textbooks", + "columnsFrom": [ + "textbook_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "chapters_id": { + "name": "chapters_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "class_enrollments": { + "name": "class_enrollments", + "columns": { + "class_id": { + "name": "class_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "student_id": { + "name": "student_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "class_enrollment_status": { + "name": "class_enrollment_status", + "type": "enum('active','inactive')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "class_enrollments_class_idx": { + "name": "class_enrollments_class_idx", + "columns": [ + "class_id" + ], + "isUnique": false + }, + "class_enrollments_student_idx": { + "name": "class_enrollments_student_idx", + "columns": [ + "student_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ce_c_fk": { + "name": "ce_c_fk", + "tableFrom": "class_enrollments", + "tableTo": "classes", + "columnsFrom": [ + "class_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ce_s_fk": { + "name": "ce_s_fk", + "tableFrom": "class_enrollments", + "tableTo": "users", + "columnsFrom": [ + "student_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "class_enrollments_class_id_student_id_pk": { + "name": "class_enrollments_class_id_student_id_pk", + "columns": [ + "class_id", + "student_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "class_schedule": { + "name": "class_schedule", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "class_id": { + "name": "class_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "weekday": { + "name": "weekday", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "start_time": { + "name": "start_time", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "end_time": { + "name": "end_time", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "course": { + "name": "course", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "location": { + "name": "location", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "class_schedule_class_idx": { + "name": "class_schedule_class_idx", + "columns": [ + "class_id" + ], + "isUnique": false + }, + "class_schedule_class_day_idx": { + "name": "class_schedule_class_day_idx", + "columns": [ + "class_id", + "weekday" + ], + "isUnique": false + } + }, + "foreignKeys": { + "cs_c_fk": { + "name": "cs_c_fk", + "tableFrom": "class_schedule", + "tableTo": "classes", + "columnsFrom": [ + "class_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "class_schedule_id": { + "name": "class_schedule_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "class_subject_teachers": { + "name": "class_subject_teachers", + "columns": { + "class_id": { + "name": "class_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "enum('语文','数学','英语','美术','体育','科学','社会','音乐')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "teacher_id": { + "name": "teacher_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "class_subject_teachers_class_idx": { + "name": "class_subject_teachers_class_idx", + "columns": [ + "class_id" + ], + "isUnique": false + }, + "class_subject_teachers_teacher_idx": { + "name": "class_subject_teachers_teacher_idx", + "columns": [ + "teacher_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "class_subject_teachers_teacher_id_users_id_fk": { + "name": "class_subject_teachers_teacher_id_users_id_fk", + "tableFrom": "class_subject_teachers", + "tableTo": "users", + "columnsFrom": [ + "teacher_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cst_c_fk": { + "name": "cst_c_fk", + "tableFrom": "class_subject_teachers", + "tableTo": "classes", + "columnsFrom": [ + "class_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "class_subject_teachers_class_id_subject_pk": { + "name": "class_subject_teachers_class_id_subject_pk", + "columns": [ + "class_id", + "subject" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "classes": { + "name": "classes", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "school_name": { + "name": "school_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "school_id": { + "name": "school_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "grade": { + "name": "grade", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "grade_id": { + "name": "grade_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "homeroom": { + "name": "homeroom", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "room": { + "name": "room", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invitation_code": { + "name": "invitation_code", + "type": "varchar(6)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "teacher_id": { + "name": "teacher_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "classes_teacher_idx": { + "name": "classes_teacher_idx", + "columns": [ + "teacher_id" + ], + "isUnique": false + }, + "classes_grade_idx": { + "name": "classes_grade_idx", + "columns": [ + "grade" + ], + "isUnique": false + }, + "classes_school_idx": { + "name": "classes_school_idx", + "columns": [ + "school_id" + ], + "isUnique": false + }, + "classes_grade_id_idx": { + "name": "classes_grade_id_idx", + "columns": [ + "grade_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "classes_teacher_id_users_id_fk": { + "name": "classes_teacher_id_users_id_fk", + "tableFrom": "classes", + "tableTo": "users", + "columnsFrom": [ + "teacher_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "c_s_fk": { + "name": "c_s_fk", + "tableFrom": "classes", + "tableTo": "schools", + "columnsFrom": [ + "school_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "c_g_fk": { + "name": "c_g_fk", + "tableFrom": "classes", + "tableTo": "grades", + "columnsFrom": [ + "grade_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "classes_id": { + "name": "classes_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "classes_invitation_code_unique": { + "name": "classes_invitation_code_unique", + "columns": [ + "invitation_code" + ] + } + }, + "checkConstraint": {} + }, + "classrooms": { + "name": "classrooms", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "building": { + "name": "building", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "floor": { + "name": "floor", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "capacity": { + "name": "capacity", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "classrooms_name_idx": { + "name": "classrooms_name_idx", + "columns": [ + "name" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "classrooms_id": { + "name": "classrooms_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "classrooms_name_unique": { + "name": "classrooms_name_unique", + "columns": [ + "name" + ] + } + }, + "checkConstraint": {} + }, + "departments": { + "name": "departments", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "departments_name_idx": { + "name": "departments_name_idx", + "columns": [ + "name" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "departments_id": { + "name": "departments_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "departments_name_unique": { + "name": "departments_name_unique", + "columns": [ + "name" + ] + } + }, + "checkConstraint": {} + }, + "exam_questions": { + "name": "exam_questions", + "columns": { + "exam_id": { + "name": "exam_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "question_id": { + "name": "question_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "score": { + "name": "score", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "order": { + "name": "order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "exam_questions_exam_id_exams_id_fk": { + "name": "exam_questions_exam_id_exams_id_fk", + "tableFrom": "exam_questions", + "tableTo": "exams", + "columnsFrom": [ + "exam_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "exam_questions_question_id_questions_id_fk": { + "name": "exam_questions_question_id_questions_id_fk", + "tableFrom": "exam_questions", + "tableTo": "questions", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "exam_questions_exam_id_question_id_pk": { + "name": "exam_questions_exam_id_question_id_pk", + "columns": [ + "exam_id", + "question_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "exam_submissions": { + "name": "exam_submissions", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "exam_id": { + "name": "exam_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "student_id": { + "name": "student_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "score": { + "name": "score", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'started'" + }, + "submitted_at": { + "name": "submitted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "exam_student_idx": { + "name": "exam_student_idx", + "columns": [ + "exam_id", + "student_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "exam_submissions_exam_id_exams_id_fk": { + "name": "exam_submissions_exam_id_exams_id_fk", + "tableFrom": "exam_submissions", + "tableTo": "exams", + "columnsFrom": [ + "exam_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "exam_submissions_student_id_users_id_fk": { + "name": "exam_submissions_student_id_users_id_fk", + "tableFrom": "exam_submissions", + "tableTo": "users", + "columnsFrom": [ + "student_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "exam_submissions_id": { + "name": "exam_submissions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "exams": { + "name": "exams", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "structure": { + "name": "structure", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "creator_id": { + "name": "creator_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "start_time": { + "name": "start_time", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "end_time": { + "name": "end_time", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'draft'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": { + "exams_creator_id_users_id_fk": { + "name": "exams_creator_id_users_id_fk", + "tableFrom": "exams", + "tableTo": "users", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "exams_id": { + "name": "exams_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "grades": { + "name": "grades", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "school_id": { + "name": "school_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "grade_head_id": { + "name": "grade_head_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "teaching_head_id": { + "name": "teaching_head_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "grades_school_idx": { + "name": "grades_school_idx", + "columns": [ + "school_id" + ], + "isUnique": false + }, + "grades_school_name_uniq": { + "name": "grades_school_name_uniq", + "columns": [ + "school_id", + "name" + ], + "isUnique": false + }, + "grades_grade_head_idx": { + "name": "grades_grade_head_idx", + "columns": [ + "grade_head_id" + ], + "isUnique": false + }, + "grades_teaching_head_idx": { + "name": "grades_teaching_head_idx", + "columns": [ + "teaching_head_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "g_s_fk": { + "name": "g_s_fk", + "tableFrom": "grades", + "tableTo": "schools", + "columnsFrom": [ + "school_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "g_gh_fk": { + "name": "g_gh_fk", + "tableFrom": "grades", + "tableTo": "users", + "columnsFrom": [ + "grade_head_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "g_th_fk": { + "name": "g_th_fk", + "tableFrom": "grades", + "tableTo": "users", + "columnsFrom": [ + "teaching_head_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "grades_id": { + "name": "grades_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "homework_answers": { + "name": "homework_answers", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "submission_id": { + "name": "submission_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "question_id": { + "name": "question_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "answer_content": { + "name": "answer_content", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "score": { + "name": "score", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "hw_answer_submission_idx": { + "name": "hw_answer_submission_idx", + "columns": [ + "submission_id" + ], + "isUnique": false + }, + "hw_answer_submission_question_idx": { + "name": "hw_answer_submission_question_idx", + "columns": [ + "submission_id", + "question_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "hw_ans_sub_fk": { + "name": "hw_ans_sub_fk", + "tableFrom": "homework_answers", + "tableTo": "homework_submissions", + "columnsFrom": [ + "submission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "hw_ans_q_fk": { + "name": "hw_ans_q_fk", + "tableFrom": "homework_answers", + "tableTo": "questions", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homework_answers_id": { + "name": "homework_answers_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "homework_assignment_questions": { + "name": "homework_assignment_questions", + "columns": { + "assignment_id": { + "name": "assignment_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "question_id": { + "name": "question_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "score": { + "name": "score", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "order": { + "name": "order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "hw_assignment_questions_assignment_idx": { + "name": "hw_assignment_questions_assignment_idx", + "columns": [ + "assignment_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "hw_aq_a_fk": { + "name": "hw_aq_a_fk", + "tableFrom": "homework_assignment_questions", + "tableTo": "homework_assignments", + "columnsFrom": [ + "assignment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "hw_aq_q_fk": { + "name": "hw_aq_q_fk", + "tableFrom": "homework_assignment_questions", + "tableTo": "questions", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homework_assignment_questions_assignment_id_question_id_pk": { + "name": "homework_assignment_questions_assignment_id_question_id_pk", + "columns": [ + "assignment_id", + "question_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "homework_assignment_targets": { + "name": "homework_assignment_targets", + "columns": { + "assignment_id": { + "name": "assignment_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "student_id": { + "name": "student_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "hw_assignment_targets_assignment_idx": { + "name": "hw_assignment_targets_assignment_idx", + "columns": [ + "assignment_id" + ], + "isUnique": false + }, + "hw_assignment_targets_student_idx": { + "name": "hw_assignment_targets_student_idx", + "columns": [ + "student_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "hw_at_a_fk": { + "name": "hw_at_a_fk", + "tableFrom": "homework_assignment_targets", + "tableTo": "homework_assignments", + "columnsFrom": [ + "assignment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "hw_at_s_fk": { + "name": "hw_at_s_fk", + "tableFrom": "homework_assignment_targets", + "tableTo": "users", + "columnsFrom": [ + "student_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homework_assignment_targets_assignment_id_student_id_pk": { + "name": "homework_assignment_targets_assignment_id_student_id_pk", + "columns": [ + "assignment_id", + "student_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "homework_assignments": { + "name": "homework_assignments", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_exam_id": { + "name": "source_exam_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "structure": { + "name": "structure", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'draft'" + }, + "creator_id": { + "name": "creator_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "available_at": { + "name": "available_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "due_at": { + "name": "due_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "allow_late": { + "name": "allow_late", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "late_due_at": { + "name": "late_due_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "max_attempts": { + "name": "max_attempts", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "hw_assignment_creator_idx": { + "name": "hw_assignment_creator_idx", + "columns": [ + "creator_id" + ], + "isUnique": false + }, + "hw_assignment_source_exam_idx": { + "name": "hw_assignment_source_exam_idx", + "columns": [ + "source_exam_id" + ], + "isUnique": false + }, + "hw_assignment_status_idx": { + "name": "hw_assignment_status_idx", + "columns": [ + "status" + ], + "isUnique": false + } + }, + "foreignKeys": { + "hw_asg_exam_fk": { + "name": "hw_asg_exam_fk", + "tableFrom": "homework_assignments", + "tableTo": "exams", + "columnsFrom": [ + "source_exam_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "hw_asg_creator_fk": { + "name": "hw_asg_creator_fk", + "tableFrom": "homework_assignments", + "tableTo": "users", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homework_assignments_id": { + "name": "homework_assignments_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "homework_submissions": { + "name": "homework_submissions", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "assignment_id": { + "name": "assignment_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "student_id": { + "name": "student_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "attempt_no": { + "name": "attempt_no", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "score": { + "name": "score", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'started'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "submitted_at": { + "name": "submitted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_late": { + "name": "is_late", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "hw_assignment_student_idx": { + "name": "hw_assignment_student_idx", + "columns": [ + "assignment_id", + "student_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "hw_sub_a_fk": { + "name": "hw_sub_a_fk", + "tableFrom": "homework_submissions", + "tableTo": "homework_assignments", + "columnsFrom": [ + "assignment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "hw_sub_student_fk": { + "name": "hw_sub_student_fk", + "tableFrom": "homework_submissions", + "tableTo": "users", + "columnsFrom": [ + "student_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homework_submissions_id": { + "name": "homework_submissions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "knowledge_points": { + "name": "knowledge_points", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_id": { + "name": "parent_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "chapter_id": { + "name": "chapter_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "level": { + "name": "level", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "order": { + "name": "order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "parent_id_idx": { + "name": "parent_id_idx", + "columns": [ + "parent_id" + ], + "isUnique": false + }, + "kp_chapter_id_idx": { + "name": "kp_chapter_id_idx", + "columns": [ + "chapter_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "knowledge_points_id": { + "name": "knowledge_points_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "questions": { + "name": "questions", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "json", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "enum('single_choice','multiple_choice','text','judgment','composite')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "difficulty": { + "name": "difficulty", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 1 + }, + "parent_id": { + "name": "parent_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "author_id": { + "name": "author_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "parent_id_idx": { + "name": "parent_id_idx", + "columns": [ + "parent_id" + ], + "isUnique": false + }, + "author_id_idx": { + "name": "author_id_idx", + "columns": [ + "author_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "questions_author_id_users_id_fk": { + "name": "questions_author_id_users_id_fk", + "tableFrom": "questions", + "tableTo": "users", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "questions_id": { + "name": "questions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "questions_to_knowledge_points": { + "name": "questions_to_knowledge_points", + "columns": { + "question_id": { + "name": "question_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "knowledge_point_id": { + "name": "knowledge_point_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "kp_idx": { + "name": "kp_idx", + "columns": [ + "knowledge_point_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "q_kp_qid_fk": { + "name": "q_kp_qid_fk", + "tableFrom": "questions_to_knowledge_points", + "tableTo": "questions", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "q_kp_kpid_fk": { + "name": "q_kp_kpid_fk", + "tableFrom": "questions_to_knowledge_points", + "tableTo": "knowledge_points", + "columnsFrom": [ + "knowledge_point_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "questions_to_knowledge_points_question_id_knowledge_point_id_pk": { + "name": "questions_to_knowledge_points_question_id_knowledge_point_id_pk", + "columns": [ + "question_id", + "knowledge_point_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "roles_id": { + "name": "roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ] + } + }, + "checkConstraint": {} + }, + "schools": { + "name": "schools", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "schools_name_idx": { + "name": "schools_name_idx", + "columns": [ + "name" + ], + "isUnique": false + }, + "schools_code_idx": { + "name": "schools_code_idx", + "columns": [ + "code" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "schools_id": { + "name": "schools_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "schools_name_unique": { + "name": "schools_name_unique", + "columns": [ + "name" + ] + }, + "schools_code_unique": { + "name": "schools_code_unique", + "columns": [ + "code" + ] + } + }, + "checkConstraint": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "sessionToken": { + "name": "sessionToken", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_userId_users_id_fk": { + "name": "sessions_userId_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sessions_sessionToken": { + "name": "sessions_sessionToken", + "columns": [ + "sessionToken" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "subjects": { + "name": "subjects", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "subjects_name_idx": { + "name": "subjects_name_idx", + "columns": [ + "name" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "subjects_id": { + "name": "subjects_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "subjects_name_unique": { + "name": "subjects_name_unique", + "columns": [ + "name" + ] + }, + "subjects_code_unique": { + "name": "subjects_code_unique", + "columns": [ + "code" + ] + } + }, + "checkConstraint": {} + }, + "submission_answers": { + "name": "submission_answers", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "submission_id": { + "name": "submission_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "question_id": { + "name": "question_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "answer_content": { + "name": "answer_content", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "score": { + "name": "score", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "submission_idx": { + "name": "submission_idx", + "columns": [ + "submission_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "submission_answers_submission_id_exam_submissions_id_fk": { + "name": "submission_answers_submission_id_exam_submissions_id_fk", + "tableFrom": "submission_answers", + "tableTo": "exam_submissions", + "columnsFrom": [ + "submission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "submission_answers_question_id_questions_id_fk": { + "name": "submission_answers_question_id_questions_id_fk", + "tableFrom": "submission_answers", + "tableTo": "questions", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "submission_answers_id": { + "name": "submission_answers_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "textbooks": { + "name": "textbooks", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "grade": { + "name": "grade", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "publisher": { + "name": "publisher", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "textbooks_id": { + "name": "textbooks_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "emailVerified": { + "name": "emailVerified", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'student'" + }, + "password": { + "name": "password", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phone": { + "name": "phone", + "type": "varchar(30)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address": { + "name": "address", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "gender": { + "name": "gender", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "age": { + "name": "age", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "grade_id": { + "name": "grade_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "department_id": { + "name": "department_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "onboarded_at": { + "name": "onboarded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "email_idx": { + "name": "email_idx", + "columns": [ + "email" + ], + "isUnique": false + }, + "users_grade_id_idx": { + "name": "users_grade_id_idx", + "columns": [ + "grade_id" + ], + "isUnique": false + }, + "users_department_id_idx": { + "name": "users_department_id_idx", + "columns": [ + "department_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "columns": [ + "email" + ] + } + }, + "checkConstraint": {} + }, + "users_to_roles": { + "name": "users_to_roles", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "user_id_idx": { + "name": "user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "users_to_roles_user_id_users_id_fk": { + "name": "users_to_roles_user_id_users_id_fk", + "tableFrom": "users_to_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "users_to_roles_role_id_roles_id_fk": { + "name": "users_to_roles_role_id_roles_id_fk", + "tableFrom": "users_to_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "users_to_roles_user_id_role_id_pk": { + "name": "users_to_roles_user_id_role_id_pk", + "columns": [ + "user_id", + "role_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "verificationTokens": { + "name": "verificationTokens", + "columns": { + "identifier": { + "name": "identifier", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationTokens_identifier_token_pk": { + "name": "verificationTokens_identifier_token_pk", + "columns": [ + "identifier", + "token" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index c0cad15..76ca167 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -54,16 +54,9 @@ { "idx": 7, "version": "5", - "when": 1767782500000, - "tag": "0007_add_class_invitation_code", - "breakpoints": true - }, - { - "idx": 8, - "version": "5", - "when": 1767941300000, - "tag": "0008_add_user_profile_fields", + "when": 1768205524480, + "tag": "0007_talented_bromley", "breakpoints": true } ] -} +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index cde4674..6cd1e2e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,8 @@ "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-progress": "^1.1.8", + "@radix-ui/react-radio-group": "^1.3.8", "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.8", @@ -28,6 +30,10 @@ "@t3-oss/env-nextjs": "^0.13.10", "@tanstack/react-query": "^5.90.12", "@tanstack/react-table": "^8.21.3", + "@tiptap/extension-placeholder": "^3.15.3", + "@tiptap/pm": "^3.15.3", + "@tiptap/react": "^3.15.3", + "@tiptap/starter-kit": "^3.15.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "drizzle-orm": "^0.45.1", @@ -47,6 +53,7 @@ "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", "tailwindcss-animate": "^1.0.7", + "tiptap-markdown": "^0.9.0", "zod": "^4.2.1", "zustand": "^5.0.9" }, @@ -3465,6 +3472,118 @@ } } }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.8.tgz", + "integrity": "sha512-+gISHcSPUJ7ktBy9RnTqbdKW78bcGke3t6taawyZ71pio1JewwGSJizycs7rLhGTvMJYCQB1DBK4KQsxs7U8dA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.3", + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", + "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-roving-focus": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", @@ -4215,6 +4334,12 @@ "url": "https://opencollective.com/immer" } }, + "node_modules/@remirror/core-constants": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz", + "integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==", + "license": "MIT" + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -4641,6 +4766,453 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tiptap/core": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.15.3.tgz", + "integrity": "sha512-bmXydIHfm2rEtGju39FiQNfzkFx9CDvJe+xem1dgEZ2P6Dj7nQX9LnA1ZscW7TuzbBRkL5p3dwuBIi3f62A66A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/pm": "^3.15.3" + } + }, + "node_modules/@tiptap/extension-blockquote": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.15.3.tgz", + "integrity": "sha512-13x5UsQXtttFpoS/n1q173OeurNxppsdWgP3JfsshzyxIghhC141uL3H6SGYQLPU31AizgDs2OEzt6cSUevaZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.15.3" + } + }, + "node_modules/@tiptap/extension-bold": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.15.3.tgz", + "integrity": "sha512-I8JYbkkUTNUXbHd/wCse2bR0QhQtJD7+0/lgrKOmGfv5ioLxcki079Nzuqqay3PjgYoJLIJQvm3RAGxT+4X91w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.15.3" + } + }, + "node_modules/@tiptap/extension-bubble-menu": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.15.3.tgz", + "integrity": "sha512-e88DG1bTy6hKxrt7iPVQhJnH5/EOrnKpIyp09dfRDgWrrW88fE0Qjys7a/eT8W+sXyXM3z10Ye7zpERWsrLZDg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.15.3", + "@tiptap/pm": "^3.15.3" + } + }, + "node_modules/@tiptap/extension-bullet-list": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.15.3.tgz", + "integrity": "sha512-MGwEkNT7ltst6XaWf0ObNgpKQ4PvuuV3igkBrdYnQS+qaAx9IF4isygVPqUc9DvjYC306jpyKsNqNrENIXcosA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "^3.15.3" + } + }, + "node_modules/@tiptap/extension-code": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.15.3.tgz", + "integrity": "sha512-x6LFt3Og6MFINYpsMzrJnz7vaT9Yk1t4oXkbJsJRSavdIWBEBcoRudKZ4sSe/AnsYlRJs8FY2uR76mt9e+7xAQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.15.3" + } + }, + "node_modules/@tiptap/extension-code-block": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.15.3.tgz", + "integrity": "sha512-q1UB9icNfdJppTqMIUWfoRKkx5SSdWIpwZoL2NeOI5Ah3E20/dQKVttIgLhsE521chyvxCYCRaHD5tMNGKfhyw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.15.3", + "@tiptap/pm": "^3.15.3" + } + }, + "node_modules/@tiptap/extension-document": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.15.3.tgz", + "integrity": "sha512-AC72nI2gnogBuETCKbZekn+h6t5FGGcZG2abPGKbz/x9rwpb6qV2hcbAQ30t6M7H6cTOh2/Ut8bEV2MtMB15sw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.15.3" + } + }, + "node_modules/@tiptap/extension-dropcursor": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.15.3.tgz", + "integrity": "sha512-jGI5XZpdo8GSYQFj7HY15/oEwC2m2TqZz0/Fln5qIhY32XlZhWrsMuMI6WbUJrTH16es7xO6jmRlDsc6g+vJWg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "^3.15.3" + } + }, + "node_modules/@tiptap/extension-floating-menu": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.15.3.tgz", + "integrity": "sha512-+3DVBleKKffadEJEdLYxmYAJOjHjLSqtiSFUE3RABT4V2ka1ODy2NIpyKX0o1SvQ5N1jViYT9Q+yUbNa6zCcDw==", + "license": "MIT", + "optional": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@floating-ui/dom": "^1.0.0", + "@tiptap/core": "^3.15.3", + "@tiptap/pm": "^3.15.3" + } + }, + "node_modules/@tiptap/extension-gapcursor": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.15.3.tgz", + "integrity": "sha512-Kaw0sNzP0bQI/xEAMSfIpja6xhsu9WqqAK/puzOIS1RKWO47Wps/tzqdSJ9gfslPIb5uY5mKCfy8UR8Xgiia8w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "^3.15.3" + } + }, + "node_modules/@tiptap/extension-hard-break": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.15.3.tgz", + "integrity": "sha512-8HjxmeRbBiXW+7JKemAJtZtHlmXQ9iji398CPQ0yYde68WbIvUhHXjmbJE5pxFvvQTJ/zJv1aISeEOZN2bKBaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.15.3" + } + }, + "node_modules/@tiptap/extension-heading": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.15.3.tgz", + "integrity": "sha512-G1GG6iN1YXPS+75arDpo+bYRzhr3dNDw99c7D7na3aDawa9Qp7sZ/bVrzFUUcVEce0cD6h83yY7AooBxEc67hA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.15.3" + } + }, + "node_modules/@tiptap/extension-horizontal-rule": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.15.3.tgz", + "integrity": "sha512-FYkN7L6JsfwwNEntmLklCVKvgL0B0N47OXMacRk6kYKQmVQ4Nvc7q/VJLpD9sk4wh4KT1aiCBfhKEBTu5pv1fg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.15.3", + "@tiptap/pm": "^3.15.3" + } + }, + "node_modules/@tiptap/extension-italic": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.15.3.tgz", + "integrity": "sha512-6XeuPjcWy7OBxpkgOV7bD6PATO5jhIxc8SEK4m8xn8nelGTBIbHGqK37evRv+QkC7E0MUryLtzwnmmiaxcKL0Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.15.3" + } + }, + "node_modules/@tiptap/extension-link": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.15.3.tgz", + "integrity": "sha512-PdDXyBF9Wco9U1x6e+b7tKBWG+kqBDXDmaYXHkFm/gYuQCQafVJ5mdrDdKgkHDWVnJzMWZXBcZjT9r57qtlLWg==", + "license": "MIT", + "dependencies": { + "linkifyjs": "^4.3.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.15.3", + "@tiptap/pm": "^3.15.3" + } + }, + "node_modules/@tiptap/extension-list": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.15.3.tgz", + "integrity": "sha512-n7y/MF9lAM5qlpuH5IR4/uq+kJPEJpe9NrEiH+NmkO/5KJ6cXzpJ6F4U17sMLf2SNCq+TWN9QK8QzoKxIn50VQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.15.3", + "@tiptap/pm": "^3.15.3" + } + }, + "node_modules/@tiptap/extension-list-item": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.15.3.tgz", + "integrity": "sha512-CCxL5ek1p0lO5e8aqhnPzIySldXRSigBFk2fP9OLgdl5qKFLs2MGc19jFlx5+/kjXnEsdQTFbGY1Sizzt0TVDw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "^3.15.3" + } + }, + "node_modules/@tiptap/extension-list-keymap": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.15.3.tgz", + "integrity": "sha512-UxqnTEEAKrL+wFQeSyC9z0mgyUUVRS2WTcVFoLZCE6/Xus9F53S4bl7VKFadjmqI4GpDk5Oe2IOUc72o129jWg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "^3.15.3" + } + }, + "node_modules/@tiptap/extension-ordered-list": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.15.3.tgz", + "integrity": "sha512-/8uhw528Iy0c9wF6tHCiIn0ToM0Ml6Ll2c/3iPRnKr4IjXwx2Lr994stUFihb+oqGZwV1J8CPcZJ4Ufpdqi4Dw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "^3.15.3" + } + }, + "node_modules/@tiptap/extension-paragraph": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.15.3.tgz", + "integrity": "sha512-lc0Qu/1AgzcEfS67NJMj5tSHHhH6NtA6uUpvppEKGsvJwgE2wKG1onE4isrVXmcGRdxSMiCtyTDemPNMu6/ozQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.15.3" + } + }, + "node_modules/@tiptap/extension-placeholder": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-3.15.3.tgz", + "integrity": "sha512-XcHHnojT186hKIoOgcPBesXk89+caNGVUdMtc171Vcr/5s0dpnr4q5LfE+YRC+S85CpCxCRRnh84Ou+XRtOqrw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "^3.15.3" + } + }, + "node_modules/@tiptap/extension-strike": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.15.3.tgz", + "integrity": "sha512-Y1P3eGNY7RxQs2BcR6NfLo9VfEOplXXHAqkOM88oowWWOE7dMNeFFZM9H8HNxoQgXJ7H0aWW9B7ZTWM9hWli2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.15.3" + } + }, + "node_modules/@tiptap/extension-text": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.15.3.tgz", + "integrity": "sha512-MhkBz8ZvrqOKtKNp+ZWISKkLUlTrDR7tbKZc2OnNcUTttL9dz0HwT+cg91GGz19fuo7ttDcfsPV6eVmflvGToA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.15.3" + } + }, + "node_modules/@tiptap/extension-underline": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.15.3.tgz", + "integrity": "sha512-r/IwcNN0W366jGu4Y0n2MiFq9jGa4aopOwtfWO4d+J0DyeS2m7Go3+KwoUqi0wQTiVU74yfi4DF6eRsMQ9/iHQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.15.3" + } + }, + "node_modules/@tiptap/extensions": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.15.3.tgz", + "integrity": "sha512-ycx/BgxR4rc9tf3ZyTdI98Z19yKLFfqM3UN+v42ChuIwkzyr9zyp7kG8dB9xN2lNqrD+5y/HyJobz/VJ7T90gA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.15.3", + "@tiptap/pm": "^3.15.3" + } + }, + "node_modules/@tiptap/pm": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.15.3.tgz", + "integrity": "sha512-Zm1BaU1TwFi3CQiisxjgnzzIus+q40bBKWLqXf6WEaus8Z6+vo1MT2pU52dBCMIRaW9XNDq3E5cmGtMc1AlveA==", + "license": "MIT", + "dependencies": { + "prosemirror-changeset": "^2.3.0", + "prosemirror-collab": "^1.3.1", + "prosemirror-commands": "^1.6.2", + "prosemirror-dropcursor": "^1.8.1", + "prosemirror-gapcursor": "^1.3.2", + "prosemirror-history": "^1.4.1", + "prosemirror-inputrules": "^1.4.0", + "prosemirror-keymap": "^1.2.2", + "prosemirror-markdown": "^1.13.1", + "prosemirror-menu": "^1.2.4", + "prosemirror-model": "^1.24.1", + "prosemirror-schema-basic": "^1.2.3", + "prosemirror-schema-list": "^1.5.0", + "prosemirror-state": "^1.4.3", + "prosemirror-tables": "^1.6.4", + "prosemirror-trailing-node": "^3.0.0", + "prosemirror-transform": "^1.10.2", + "prosemirror-view": "^1.38.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, + "node_modules/@tiptap/react": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.15.3.tgz", + "integrity": "sha512-XvouB+Hrqw8yFmZLPEh+HWlMeRSjZfHSfWfWuw5d8LSwnxnPeu3Bg/rjHrRrdwb+7FumtzOnNWMorpb/PSOttQ==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "fast-equals": "^5.3.3", + "use-sync-external-store": "^1.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "optionalDependencies": { + "@tiptap/extension-bubble-menu": "^3.15.3", + "@tiptap/extension-floating-menu": "^3.15.3" + }, + "peerDependencies": { + "@tiptap/core": "^3.15.3", + "@tiptap/pm": "^3.15.3", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tiptap/starter-kit": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.15.3.tgz", + "integrity": "sha512-ia+eQr9Mt1ln2UO+kK4kFTJOrZK4GhvZXFjpCCYuHtco3rhr2fZAIxEEY4cl/vo5VO5WWyPqxhkFeLcoWmNjSw==", + "license": "MIT", + "dependencies": { + "@tiptap/core": "^3.15.3", + "@tiptap/extension-blockquote": "^3.15.3", + "@tiptap/extension-bold": "^3.15.3", + "@tiptap/extension-bullet-list": "^3.15.3", + "@tiptap/extension-code": "^3.15.3", + "@tiptap/extension-code-block": "^3.15.3", + "@tiptap/extension-document": "^3.15.3", + "@tiptap/extension-dropcursor": "^3.15.3", + "@tiptap/extension-gapcursor": "^3.15.3", + "@tiptap/extension-hard-break": "^3.15.3", + "@tiptap/extension-heading": "^3.15.3", + "@tiptap/extension-horizontal-rule": "^3.15.3", + "@tiptap/extension-italic": "^3.15.3", + "@tiptap/extension-link": "^3.15.3", + "@tiptap/extension-list": "^3.15.3", + "@tiptap/extension-list-item": "^3.15.3", + "@tiptap/extension-list-keymap": "^3.15.3", + "@tiptap/extension-ordered-list": "^3.15.3", + "@tiptap/extension-paragraph": "^3.15.3", + "@tiptap/extension-strike": "^3.15.3", + "@tiptap/extension-text": "^3.15.3", + "@tiptap/extension-underline": "^3.15.3", + "@tiptap/extensions": "^3.15.3", + "@tiptap/pm": "^3.15.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -4762,6 +5334,22 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "license": "MIT" + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", @@ -4771,6 +5359,12 @@ "@types/unist": "*" } }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "license": "MIT" + }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", @@ -4800,7 +5394,6 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" @@ -5422,7 +6015,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/aria-hidden": { @@ -5988,6 +6580,12 @@ "dev": true, "license": "MIT" }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -6540,6 +7138,18 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/error-causes": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/error-causes/-/error-causes-3.0.2.tgz", @@ -6802,7 +7412,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -7264,6 +7873,15 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-equals": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", + "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/fast-glob": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", @@ -8780,6 +9398,21 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/linkifyjs": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz", + "integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==", + "license": "MIT" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -8876,6 +9509,29 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it-task-lists": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/markdown-it-task-lists/-/markdown-it-task-lists-2.1.1.tgz", + "integrity": "sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA==", + "license": "ISC" + }, "node_modules/markdown-table": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", @@ -9192,6 +9848,12 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "license": "MIT" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -10192,6 +10854,12 @@ "node": ">= 0.8.0" } }, + "node_modules/orderedmap": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz", + "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==", + "license": "MIT" + }, "node_modules/own-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", @@ -10525,6 +11193,201 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/prosemirror-changeset": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.3.1.tgz", + "integrity": "sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ==", + "license": "MIT", + "dependencies": { + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-collab": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz", + "integrity": "sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0" + } + }, + "node_modules/prosemirror-commands": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz", + "integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.10.2" + } + }, + "node_modules/prosemirror-dropcursor": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz", + "integrity": "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0", + "prosemirror-view": "^1.1.0" + } + }, + "node_modules/prosemirror-gapcursor": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.0.tgz", + "integrity": "sha512-z00qvurSdCEWUIulij/isHaqu4uLS8r/Fi61IbjdIPJEonQgggbJsLnstW7Lgdk4zQ68/yr6B6bf7sJXowIgdQ==", + "license": "MIT", + "dependencies": { + "prosemirror-keymap": "^1.0.0", + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-view": "^1.0.0" + } + }, + "node_modules/prosemirror-history": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz", + "integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.2.2", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.31.0", + "rope-sequence": "^1.3.0" + } + }, + "node_modules/prosemirror-inputrules": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz", + "integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-keymap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz", + "integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "w3c-keyname": "^2.2.0" + } + }, + "node_modules/prosemirror-markdown": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.2.tgz", + "integrity": "sha512-FPD9rHPdA9fqzNmIIDhhnYQ6WgNoSWX9StUZ8LEKapaXU9i6XgykaHKhp6XMyXlOWetmaFgGDS/nu/w9/vUc5g==", + "license": "MIT", + "dependencies": { + "@types/markdown-it": "^14.0.0", + "markdown-it": "^14.0.0", + "prosemirror-model": "^1.25.0" + } + }, + "node_modules/prosemirror-menu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.2.5.tgz", + "integrity": "sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ==", + "license": "MIT", + "dependencies": { + "crelt": "^1.0.0", + "prosemirror-commands": "^1.0.0", + "prosemirror-history": "^1.0.0", + "prosemirror-state": "^1.0.0" + } + }, + "node_modules/prosemirror-model": { + "version": "1.25.4", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz", + "integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==", + "license": "MIT", + "dependencies": { + "orderedmap": "^2.0.0" + } + }, + "node_modules/prosemirror-schema-basic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.4.tgz", + "integrity": "sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.25.0" + } + }, + "node_modules/prosemirror-schema-list": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz", + "integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.7.3" + } + }, + "node_modules/prosemirror-state": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", + "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.27.0" + } + }, + "node_modules/prosemirror-tables": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz", + "integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==", + "license": "MIT", + "dependencies": { + "prosemirror-keymap": "^1.2.3", + "prosemirror-model": "^1.25.4", + "prosemirror-state": "^1.4.4", + "prosemirror-transform": "^1.10.5", + "prosemirror-view": "^1.41.4" + } + }, + "node_modules/prosemirror-trailing-node": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz", + "integrity": "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==", + "license": "MIT", + "dependencies": { + "@remirror/core-constants": "3.0.0", + "escape-string-regexp": "^4.0.0" + }, + "peerDependencies": { + "prosemirror-model": "^1.22.1", + "prosemirror-state": "^1.4.2", + "prosemirror-view": "^1.33.8" + } + }, + "node_modules/prosemirror-transform": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.10.5.tgz", + "integrity": "sha512-RPDQCxIDhIBb1o36xxwsaeAvivO8VLJcgBtzmOwQ64bMtsVFh5SSuJ6dWSxO1UsHTiTXPCgQm3PDJt7p6IOLbw==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.21.0" + } + }, + "node_modules/prosemirror-view": { + "version": "1.41.4", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.4.tgz", + "integrity": "sha512-WkKgnyjNncri03Gjaz3IFWvCAE94XoiEgvtr0/r2Xw7R8/IjK3sKLSiDoCHWcsXSAinVaKlGRZDvMCsF1kbzjA==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.20.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -10535,6 +11398,15 @@ "node": ">=6" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -10946,6 +11818,12 @@ "node": ">=0.10.0" } }, + "node_modules/rope-sequence": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz", + "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==", + "license": "MIT" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -11648,6 +12526,46 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tiptap-markdown": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/tiptap-markdown/-/tiptap-markdown-0.9.0.tgz", + "integrity": "sha512-dKLQ9iiuGNgrlGVjrNauF/UBzWu4LYOx5pkD0jNkmQt/GOwfCJsBuzZTsf1jZ204ANHOm572mZ9PYvGh1S7tpQ==", + "license": "MIT", + "workspaces": [ + "example" + ], + "dependencies": { + "@types/markdown-it": "^13.0.7", + "markdown-it": "^14.1.0", + "markdown-it-task-lists": "^2.1.1", + "prosemirror-markdown": "^1.11.1" + }, + "peerDependencies": { + "@tiptap/core": "^3.0.1" + } + }, + "node_modules/tiptap-markdown/node_modules/@types/linkify-it": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.5.tgz", + "integrity": "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==", + "license": "MIT" + }, + "node_modules/tiptap-markdown/node_modules/@types/markdown-it": { + "version": "13.0.9", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-13.0.9.tgz", + "integrity": "sha512-1XPwR0+MgXLWfTn9gCsZ55AHOKW1WN+P9vr0PaQh5aerR9LLQXUbjfEAFhjmEmyoYFWAyuN2Mqkn40MZ4ukjBw==", + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^3", + "@types/mdurl": "^1" + } + }, + "node_modules/tiptap-markdown/node_modules/@types/mdurl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.5.tgz", + "integrity": "sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==", + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -11855,6 +12773,12 @@ "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -12153,6 +13077,12 @@ "d3-timer": "^3.0.1" } }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index a9866db..3dc78e3 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,8 @@ "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-progress": "^1.1.8", + "@radix-ui/react-radio-group": "^1.3.8", "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.8", @@ -33,6 +35,10 @@ "@t3-oss/env-nextjs": "^0.13.10", "@tanstack/react-query": "^5.90.12", "@tanstack/react-table": "^8.21.3", + "@tiptap/extension-placeholder": "^3.15.3", + "@tiptap/pm": "^3.15.3", + "@tiptap/react": "^3.15.3", + "@tiptap/starter-kit": "^3.15.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "drizzle-orm": "^0.45.1", @@ -52,6 +58,7 @@ "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", "tailwindcss-animate": "^1.0.7", + "tiptap-markdown": "^0.9.0", "zod": "^4.2.1", "zustand": "^5.0.9" }, diff --git a/scripts/seed.ts b/scripts/seed.ts index 8efbec0..a815e5e 100644 --- a/scripts/seed.ts +++ b/scripts/seed.ts @@ -12,7 +12,8 @@ import { textbooks, chapters, schools, grades, - classes, classEnrollments, classSchedule + classes, classEnrollments, classSchedule, + subjects } from "../src/shared/db/schema"; import { createId } from "@paralleldrive/cuid2"; import { faker } from "@faker-js/faker"; @@ -43,7 +44,7 @@ async function seed() { "submission_answers", "exam_submissions", "exam_questions", "exams", "questions_to_knowledge_points", "questions", "knowledge_points", "chapters", "textbooks", - "grades", "schools", + "grades", "schools", "subjects", "users_to_roles", "roles", "users", "accounts", "sessions" ]; for (const table of tables) { @@ -133,6 +134,17 @@ async function seed() { { id: "school_demo_2", name: "Demo School No.2", code: "DEMO2" }, ]) + // --- Seeding Subjects --- + await db.insert(subjects).values([ + { id: createId(), name: "Mathematics", code: "MATH", order: 1 }, + { id: createId(), name: "Physics", code: "PHYS", order: 2 }, + { id: createId(), name: "Chemistry", code: "CHEM", order: 3 }, + { id: createId(), name: "English", code: "ENG", order: 4 }, + { id: createId(), name: "History", code: "HIST", order: 5 }, + { id: createId(), name: "Geography", code: "GEO", order: 6 }, + { id: createId(), name: "Biology", code: "BIO", order: 7 }, + ]) + await db.insert(grades).values([ { id: grade10Id, diff --git a/src/app/(dashboard)/management/grade/classes/page.tsx b/src/app/(dashboard)/management/grade/classes/page.tsx new file mode 100644 index 0000000..c8ea6c1 --- /dev/null +++ b/src/app/(dashboard)/management/grade/classes/page.tsx @@ -0,0 +1,31 @@ +import { auth } from "@/auth" +import { getGradeManagedClasses, getManagedGrades, getTeacherOptions } from "@/modules/classes/data-access" +import { GradeClassesClient } from "@/modules/classes/components/grade-classes-view" + +export const dynamic = "force-dynamic" + +export default async function GradeClassesPage() { + const session = await auth() + const userId = session?.user?.id ?? "" + + const [classes, teachers, managedGrades] = await Promise.all([ + getGradeManagedClasses(userId), + getTeacherOptions(), + getManagedGrades(userId), + ]) + + return ( +
+
+
+

Class Management

+

+ Manage classes for your grades. +

+
+
+ + +
+ ) +} \ No newline at end of file diff --git a/src/app/(dashboard)/teacher/grades/insights/page.tsx b/src/app/(dashboard)/management/grade/insights/page.tsx similarity index 98% rename from src/app/(dashboard)/teacher/grades/insights/page.tsx rename to src/app/(dashboard)/management/grade/insights/page.tsx index 655fdcc..9f0b867 100644 --- a/src/app/(dashboard)/teacher/grades/insights/page.tsx +++ b/src/app/(dashboard)/management/grade/insights/page.tsx @@ -65,7 +65,7 @@ export default async function TeacherGradeInsightsPage({ searchParams }: { searc -
+ + + + + + {managedGrades.map((g) => ( + + {g.name} ({g.schoolName}) + + ))} + + + + + + + + + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + +
+
+ + + + + +
+ + + + { + if (isWorking) return + if (!open) setEditItem(null) + }} + > + + + Edit class + + {editItem ? ( +
+
+ +
+ + + + + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + +
+
+ +
+
任课老师
+
+ {DEFAULT_CLASS_SUBJECTS.map((subject) => { + const selected = editSubjectTeachers.find((x) => x.subject === subject)?.teacherId ?? null + return ( +
+ +
+ +
+
+ ) + })} +
+ +
+ + + + + +
+ ) : null} +
+
+ + { + if (!open) setDeleteItem(null) + }} + > + + + Delete class + This will permanently delete {deleteItem?.name || "this class"}. + + + Cancel + + Delete + + + + + + ) +} \ No newline at end of file diff --git a/src/modules/classes/components/my-classes-grid.tsx b/src/modules/classes/components/my-classes-grid.tsx index 6769d87..cb1aec1 100644 --- a/src/modules/classes/components/my-classes-grid.tsx +++ b/src/modules/classes/components/my-classes-grid.tsx @@ -3,11 +3,24 @@ import Link from "next/link" import { useMemo, useState } from "react" import { useRouter } from "next/navigation" -import { Calendar, Copy, MoreHorizontal, Pencil, Plus, RefreshCw, Trash2, Users } from "lucide-react" +import { + Calendar, + Copy, + MoreHorizontal, + Pencil, + Plus, + RefreshCw, + Search, + Trash2, + Users, + GraduationCap, + MapPin, + ChartBar, +} from "lucide-react" import { toast } from "sonner" import { parseAsString, useQueryState } from "nuqs" -import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card" +import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/shared/components/ui/card" import { Button } from "@/shared/components/ui/button" import { Badge } from "@/shared/components/ui/badge" import { EmptyState } from "@/shared/components/ui/empty-state" @@ -41,6 +54,7 @@ import { import { Input } from "@/shared/components/ui/input" import { Label } from "@/shared/components/ui/label" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/shared/components/ui/select" +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/shared/components/ui/tooltip" import type { TeacherClass } from "../types" import { createTeacherClassAction, @@ -48,12 +62,25 @@ import { ensureClassInvitationCodeAction, regenerateClassInvitationCodeAction, updateTeacherClassAction, + joinClassByInvitationCodeAction, } from "../actions" +const GRADIENTS = [ + "bg-card border-border", + "bg-card border-border", + "bg-card border-border", + "bg-card border-border", + "bg-card border-border", +] + +function getClassGradient(id: string) { + return "bg-card border-border shadow-sm hover:shadow-md" +} + export function MyClassesGrid({ classes, canCreateClass }: { classes: TeacherClass[]; canCreateClass: boolean }) { const router = useRouter() const [isWorking, setIsWorking] = useState(false) - const [createOpen, setCreateOpen] = useState(false) + const [joinOpen, setJoinOpen] = useState(false) const [q, setQ] = useQueryState("q", parseAsString.withDefault("")) const [grade, setGrade] = useQueryState("grade", parseAsString.withDefault("all")) @@ -75,41 +102,44 @@ export function MyClassesGrid({ classes, canCreateClass }: { classes: TeacherCla const defaultGrade = useMemo(() => (grade !== "all" ? grade : classes[0]?.grade ?? ""), [classes, grade]) - const handleCreate = async (formData: FormData) => { + const handleJoin = async (formData: FormData) => { setIsWorking(true) try { - const res = await createTeacherClassAction(null, formData) + const res = await joinClassByInvitationCodeAction(null, formData) if (res.success) { - toast.success(res.message) - setCreateOpen(false) + toast.success(res.message || "Joined class successfully") + setJoinOpen(false) router.refresh() } else { - toast.error(res.message || "Failed to create class") + toast.error(res.message || "Failed to join class") } } catch { - toast.error("Failed to create class") + toast.error("Failed to join class") } finally { setIsWorking(false) } } return ( -
-
+
+ {/* Filter Bar */} +
-
+
+ setQ(e.target.value || null)} + className="pl-9 bg-background" />
-
-
- - -
-
- -
-
- - -
-
- - -
@@ -204,34 +207,33 @@ export function MyClassesGrid({ classes, canCreateClass }: { classes: TeacherCla
-
+ {/* Grid */} +
{classes.length === 0 ? ( setCreateOpen(true) } : undefined} - className="h-[360px] bg-card sm:col-span-2 lg:col-span-3" + action={{ label: "Join class", onClick: () => setJoinOpen(true) }} + className="h-[360px] bg-card border-dashed sm:col-span-2 lg:col-span-3 xl:col-span-4" /> ) : filteredClasses.length === 0 ? ( { - setQ(null) - setGrade(null) - }}} - className="h-[360px] bg-card sm:col-span-2 lg:col-span-3" + icon={Search} + action={{ + label: "Clear filters", + onClick: () => { + setQ(null) + setGrade(null) + }, + }} + className="h-[360px] bg-card border-dashed sm:col-span-2 lg:col-span-3 xl:col-span-4" /> ) : ( filteredClasses.map((c) => ( - + )) )}
@@ -334,92 +336,131 @@ function ClassCard({ } return ( - - + +
-
- +
+ {c.name} -
- {c.room ? `Room: ${c.room}` : "Room: Not set"} +
+ + {c.grade} + + {c.homeroom && ( + + {c.homeroom} + + )}
- -
- {c.grade} - - - - - - setShowEdit(true)}> - - Edit - - - setShowDelete(true)} - > - - Delete - - - -
+ + + + + + setShowEdit(true)}> + + Edit Class + + + setShowDelete(true)} + > + + Delete Class + + +
- -
-
{c.studentCount} students
- {c.homeroom ? {c.homeroom} : null} -
-
-
-
Invitation code
-
{c.invitationCode ?? "-"}
+ +
+
+ Students +
+ + {c.studentCount} +
-
+
+ Room +
+ + {c.room || "—"} +
+
+
+ +
+
+ Invite Code + {c.invitationCode || "—"} +
+
{c.invitationCode ? ( - <> - - - + + + + + + Copy Code + + + + + + Regenerate + + ) : ( - )}
-
- - -
+ + + + + + + {/* Dialogs */} { @@ -495,7 +536,7 @@ function ClassCard({
@@ -524,7 +565,7 @@ function ClassCard({ onClick={handleDelete} disabled={isWorking} > - {isWorking ? "Deleting..." : "Delete"} + {isWorking ? "Deleting..." : "Delete Class"} diff --git a/src/modules/classes/components/students-filters.tsx b/src/modules/classes/components/students-filters.tsx index 787ed1d..ae2f5db 100644 --- a/src/modules/classes/components/students-filters.tsx +++ b/src/modules/classes/components/students-filters.tsx @@ -31,6 +31,7 @@ import { enrollStudentByEmailAction } from "../actions" export function StudentsFilters({ classes }: { classes: TeacherClass[] }) { const [search, setSearch] = useQueryState("q", parseAsString.withDefault("")) const [classId, setClassId] = useQueryState("classId", parseAsString.withDefault("all")) + const [status, setStatus] = useQueryState("status", parseAsString.withDefault("all")) const router = useRouter() const [open, setOpen] = useState(false) @@ -76,7 +77,7 @@ export function StudentsFilters({ classes }: { classes: TeacherClass[] }) {
- {(search || classId !== "all") && ( + + + {(search || classId !== "all" || status !== "all") && ( + + + {s.status !== "active" ? ( + setStatus(s, "active")} disabled={workingKey !== null}> + + Set active + + ) : ( + setStatus(s, "inactive")} disabled={workingKey !== null}> + + Set inactive + + )} + + setRemoveTarget(s)} + className="text-destructive focus:text-destructive" + disabled={s.status === "inactive" || workingKey !== null} + > + + Remove from class + + + + + + ))} + + + + {totalPages > 1 && ( + +
+ Showing {startIndex + 1}- + {Math.min(startIndex + ITEMS_PER_PAGE, students.length)} of{" "} + {students.length} students +
+
+ +
+ {page} / {totalPages} +
+ +
+
+ )} + { try { + const ownedIds = await db + .select({ id: classes.id }) + .from(classes) + .where(eq(classes.teacherId, teacherId)) + + const enrolledIds = await db + .select({ id: classEnrollments.classId }) + .from(classEnrollments) + .where(and(eq(classEnrollments.studentId, teacherId), eq(classEnrollments.status, "active"))) + + const allIds = Array.from(new Set([...ownedIds.map((x) => x.id), ...enrolledIds.map((x) => x.id)])) + + if (allIds.length === 0) return [] + return await db .select({ id: classes.id, @@ -135,26 +149,11 @@ export const getTeacherClasses = cache(async (params?: { teacherId?: string }): }) .from(classes) .leftJoin(classEnrollments, eq(classEnrollments.classId, classes.id)) - .where(eq(classes.teacherId, teacherId)) + .where(inArray(classes.id, allIds)) .groupBy(classes.id, classes.schoolName, classes.name, classes.grade, classes.homeroom, classes.room, classes.invitationCode) .orderBy(asc(classes.schoolName), asc(classes.grade), asc(classes.name), asc(classes.homeroom), asc(classes.room)) } catch { - return await db - .select({ - id: classes.id, - schoolName: sql`NULL`.as("schoolName"), - name: classes.name, - grade: classes.grade, - homeroom: classes.homeroom, - room: classes.room, - invitationCode: sql`NULL`.as("invitationCode"), - studentCount: sql`COALESCE(SUM(CASE WHEN ${classEnrollments.status} = 'active' THEN 1 ELSE 0 END), 0)`, - }) - .from(classes) - .leftJoin(classEnrollments, eq(classEnrollments.classId, classes.id)) - .where(eq(classes.teacherId, teacherId)) - .groupBy(classes.id, classes.name, classes.grade, classes.homeroom, classes.room) - .orderBy(asc(classes.grade), asc(classes.name), asc(classes.homeroom), asc(classes.room)) + return [] } })() @@ -331,6 +330,143 @@ export const getAdminClasses = cache(async (): Promise => return list }) +export const getGradeManagedClasses = cache(async (userId: string): Promise => { + const managedGradeIds = await db + .select({ id: grades.id }) + .from(grades) + .where(or(eq(grades.gradeHeadId, userId), eq(grades.teachingHeadId, userId))) + + if (managedGradeIds.length === 0) return [] + + const gradeIds = managedGradeIds.map((g) => g.id) + + const [rows, subjectRows] = await Promise.all([ + (async () => { + try { + return await db + .select({ + id: classes.id, + schoolName: classes.schoolName, + schoolId: classes.schoolId, + name: classes.name, + grade: classes.grade, + gradeId: classes.gradeId, + homeroom: classes.homeroom, + room: classes.room, + invitationCode: classes.invitationCode, + teacherId: users.id, + teacherName: users.name, + teacherEmail: users.email, + studentCount: sql`COALESCE(SUM(CASE WHEN ${classEnrollments.status} = 'active' THEN 1 ELSE 0 END), 0)`, + createdAt: classes.createdAt, + updatedAt: classes.updatedAt, + }) + .from(classes) + .innerJoin(users, eq(users.id, classes.teacherId)) + .leftJoin(classEnrollments, eq(classEnrollments.classId, classes.id)) + .where(inArray(classes.gradeId, gradeIds)) + .groupBy( + classes.id, + classes.schoolName, + classes.schoolId, + classes.name, + classes.grade, + classes.gradeId, + classes.homeroom, + classes.room, + classes.invitationCode, + users.id, + users.name, + users.email, + classes.createdAt, + classes.updatedAt + ) + .orderBy( + asc(classes.schoolName), + asc(classes.grade), + asc(classes.name), + asc(classes.homeroom), + asc(classes.room) + ) + } catch { + return [] + } + })(), + db + .select({ + classId: classSubjectTeachers.classId, + subject: classSubjectTeachers.subject, + teacherId: users.id, + teacherName: users.name, + teacherEmail: users.email, + }) + .from(classSubjectTeachers) + .innerJoin(classes, eq(classes.id, classSubjectTeachers.classId)) + .leftJoin(users, eq(users.id, classSubjectTeachers.teacherId)) + .where(inArray(classes.gradeId, gradeIds)) + .orderBy(asc(classSubjectTeachers.classId), asc(classSubjectTeachers.subject)), + ]) + + const subjectsByClassId = new Map>() + for (const r of subjectRows) { + const subject = r.subject as ClassSubject + if (!DEFAULT_CLASS_SUBJECTS.includes(subject)) continue + const teacher = + typeof r.teacherId === "string" && r.teacherId.length > 0 + ? { id: r.teacherId, name: r.teacherName ?? "Unnamed", email: r.teacherEmail ?? "" } + : null + const bySubject = subjectsByClassId.get(r.classId) ?? new Map() + bySubject.set(subject, teacher) + subjectsByClassId.set(r.classId, bySubject) + } + + const list = rows.map((r) => { + const bySubject = subjectsByClassId.get(r.id) + const subjectTeachers: ClassSubjectTeacherAssignment[] = DEFAULT_CLASS_SUBJECTS.map((subject) => ({ + subject, + teacher: bySubject?.get(subject) ?? null, + })) + + return { + id: r.id, + schoolName: r.schoolName, + schoolId: r.schoolId, + name: r.name, + grade: r.grade, + gradeId: r.gradeId, + homeroom: r.homeroom, + room: r.room, + invitationCode: r.invitationCode ?? null, + teacher: { + id: r.teacherId, + name: r.teacherName ?? "Unnamed", + email: r.teacherEmail, + }, + subjectTeachers, + studentCount: Number(r.studentCount ?? 0), + createdAt: r.createdAt.toISOString(), + updatedAt: r.updatedAt.toISOString(), + } + }) + + list.sort(compareClassLike) + return list +}) + +export const getManagedGrades = cache(async (userId: string) => { + return await db + .select({ + id: grades.id, + name: grades.name, + schoolId: grades.schoolId, + schoolName: schools.name, + }) + .from(grades) + .innerJoin(schools, eq(schools.id, grades.schoolId)) + .where(or(eq(grades.gradeHeadId, userId), eq(grades.teachingHeadId, userId))) + .orderBy(asc(schools.name), asc(grades.name)) +}) + export const getStudentClasses = cache(async (studentId: string): Promise => { const id = studentId.trim() if (!id) return [] @@ -345,9 +481,12 @@ export const getStudentClasses = cache(async (studentId: string): Promise => { + async (params?: { classId?: string; q?: string; status?: string; teacherId?: string }): Promise => { const teacherId = params?.teacherId ?? (await getDefaultTeacherId()) if (!teacherId) return [] const classId = params?.classId?.trim() const q = params?.q?.trim().toLowerCase() + const status = params?.status?.trim().toLowerCase() const conditions: SQL[] = [eq(classes.teacherId, teacherId)] @@ -427,6 +572,10 @@ export const getClassStudents = cache( conditions.push(eq(classes.id, classId)) } + if (status === "active" || status === "inactive") { + conditions.push(eq(classEnrollments.status, status)) + } + if (q && q.length > 0) { const needle = `%${q}%` conditions.push( @@ -439,9 +588,12 @@ export const getClassStudents = cache( id: users.id, name: users.name, email: users.email, + image: users.image, + gender: users.gender, classId: classes.id, className: classes.name, status: classEnrollments.status, + joinedAt: classEnrollments.createdAt, }) .from(classEnrollments) .innerJoin(classes, eq(classes.id, classEnrollments.classId)) @@ -453,9 +605,12 @@ export const getClassStudents = cache( id: r.id, name: r.name ?? "Unnamed", email: r.email, + image: r.image, + gender: r.gender, classId: r.classId, className: r.className, status: r.status, + joinedAt: r.joinedAt, })) } ) diff --git a/src/modules/classes/types.ts b/src/modules/classes/types.ts index d93062d..f502c31 100644 --- a/src/modules/classes/types.ts +++ b/src/modules/classes/types.ts @@ -65,9 +65,12 @@ export type ClassStudent = { id: string name: string email: string + image?: string | null + gender?: string | null classId: string className: string status: "active" | "inactive" + joinedAt: Date } export type ClassScheduleItem = { @@ -80,26 +83,6 @@ export type ClassScheduleItem = { location?: string | null } -export type StudentEnrolledClass = { - id: string - schoolName?: string | null - name: string - grade: string - homeroom?: string | null - room?: string | null -} - -export type StudentScheduleItem = { - id: string - classId: string - className: string - weekday: 1 | 2 | 3 | 4 | 5 | 6 | 7 - startTime: string - endTime: string - course: string - location?: string | null -} - export type CreateClassScheduleItemInput = { classId: string weekday: 1 | 2 | 3 | 4 | 5 | 6 | 7 @@ -118,13 +101,26 @@ export type UpdateClassScheduleItemInput = { location?: string | null } -export type ClassBasicInfo = { +export type StudentEnrolledClass = { id: string + schoolName?: string | null name: string grade: string homeroom?: string | null room?: string | null - invitationCode?: string | null + teacherName?: string | null + teacherEmail?: string | null +} + +export type StudentScheduleItem = { + id: string + classId: string + className: string + weekday: 1 | 2 | 3 | 4 | 5 | 6 | 7 + startTime: string + endTime: string + course: string + location?: string | null } export type ScoreStats = { @@ -151,24 +147,23 @@ export type ClassHomeworkAssignmentStats = { } export type ClassHomeworkInsights = { - class: ClassBasicInfo - studentCounts: { - total: number - active: number - inactive: number + class: { + id: string + name: string + grade: string + homeroom?: string | null + room?: string | null + invitationCode?: string | null } + studentCounts: { total: number; active: number; inactive: number } assignments: ClassHomeworkAssignmentStats[] latest: ClassHomeworkAssignmentStats | null overallScores: ScoreStats } export type GradeHomeworkClassSummary = { - class: ClassBasicInfo - studentCounts: { - total: number - active: number - inactive: number - } + class: { id: string; name: string; grade: string; homeroom?: string | null; room?: string | null } + studentCounts: { total: number; active: number; inactive: number } latestAvg: number | null prevAvg: number | null deltaAvg: number | null @@ -176,17 +171,9 @@ export type GradeHomeworkClassSummary = { } export type GradeHomeworkInsights = { - grade: { - id: string - name: string - school: { id: string; name: string } - } + grade: { id: string; name: string; school: { id: string; name: string } } classCount: number - studentCounts: { - total: number - active: number - inactive: number - } + studentCounts: { total: number; active: number; inactive: number } assignments: ClassHomeworkAssignmentStats[] latest: ClassHomeworkAssignmentStats | null overallScores: ScoreStats diff --git a/src/modules/dashboard/components/student-dashboard/student-dashboard-header.tsx b/src/modules/dashboard/components/student-dashboard/student-dashboard-header.tsx index f70329e..5832550 100644 --- a/src/modules/dashboard/components/student-dashboard/student-dashboard-header.tsx +++ b/src/modules/dashboard/components/student-dashboard/student-dashboard-header.tsx @@ -1,17 +1,45 @@ +"use client" + import Link from "next/link" +import { CalendarDays, BookOpen, PenTool } from "lucide-react" import { Button } from "@/shared/components/ui/button" export function StudentDashboardHeader({ studentName }: { studentName: string }) { + const hour = new Date().getHours() + let greeting = "Welcome back" + if (hour < 12) greeting = "Good morning" + else if (hour < 18) greeting = "Good afternoon" + else greeting = "Good evening" + return (

Dashboard

-
Welcome back, {studentName}.
+
+ {greeting}, {studentName}. Here's what's happening today. +
+
+
+ + +
-
) } diff --git a/src/modules/dashboard/components/student-dashboard/student-dashboard-view.tsx b/src/modules/dashboard/components/student-dashboard/student-dashboard-view.tsx index 40c13af..d45ca16 100644 --- a/src/modules/dashboard/components/student-dashboard/student-dashboard-view.tsx +++ b/src/modules/dashboard/components/student-dashboard/student-dashboard-view.tsx @@ -2,7 +2,6 @@ import type { StudentDashboardProps } from "@/modules/dashboard/types" import { StudentDashboardHeader } from "./student-dashboard-header" import { StudentGradesCard } from "./student-grades-card" -import { StudentRankingCard } from "./student-ranking-card" import { StudentStatsGrid } from "./student-stats-grid" import { StudentTodayScheduleCard } from "./student-today-schedule-card" import { StudentUpcomingAssignmentsCard } from "./student-upcoming-assignments-card" @@ -26,16 +25,17 @@ export function StudentDashboard({ dueSoonCount={dueSoonCount} overdueCount={overdueCount} gradedCount={gradedCount} + ranking={grades.ranking} /> -
- - -
- -
- - +
+
+ + +
+
+ +
) diff --git a/src/modules/dashboard/components/student-dashboard/student-grades-card.tsx b/src/modules/dashboard/components/student-dashboard/student-grades-card.tsx index 2b49eb6..cc2ee23 100644 --- a/src/modules/dashboard/components/student-dashboard/student-grades-card.tsx +++ b/src/modules/dashboard/components/student-dashboard/student-grades-card.tsx @@ -1,9 +1,13 @@ +"use client" + import Link from "next/link" import { BarChart3 } from "lucide-react" +import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts" import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card" import { EmptyState } from "@/shared/components/ui/empty-state" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/shared/components/ui/table" +import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/shared/components/ui/chart" import { formatDate } from "@/shared/lib/utils" import type { StudentDashboardGradeProps } from "@/modules/homework/types" @@ -11,6 +15,24 @@ export function StudentGradesCard({ grades }: { grades: StudentDashboardGradePro const hasGradeTrend = grades.trend.length > 0 const hasRecentGrades = grades.recent.length > 0 + const chartData = grades.trend.map((item) => ({ + title: item.assignmentTitle, + score: Math.round(item.percentage), + fullTitle: item.assignmentTitle, + submittedAt: formatDate(item.submittedAt), + rawScore: item.score, + maxScore: item.maxScore, + })) + + const chartConfig = { + score: { + label: "Score (%)", + color: "hsl(var(--primary))", + }, + } + + const latestGrade = grades.trend[grades.trend.length - 1] + return ( @@ -30,37 +52,79 @@ export function StudentGradesCard({ grades }: { grades: StudentDashboardGradePro ) : (
- - { - const t = grades.trend.length > 1 ? i / (grades.trend.length - 1) : 0 - const x = t * 100 - const v = Number.isFinite(p.percentage) ? Math.max(0, Math.min(100, p.percentage)) : 0 - const y = 40 - (v / 100) * 40 - return `${x},${y}` - }) - .join(" ")} - className="text-primary" - /> - -
-
- Latest:{" "} - - {Math.round(grades.trend[grades.trend.length - 1]?.percentage ?? 0)}% - + + + + value.slice(0, 10) + (value.length > 10 ? "..." : "")} + /> + `${value}%`} + width={30} + /> + + } + /> + + + + + {latestGrade && ( +
+
+ Latest:{" "} + + {Math.round(latestGrade.percentage)}% + +
+
+ Points:{" "} + + {latestGrade.score}/{latestGrade.maxScore} + +
-
- Points:{" "} - - {grades.trend[grades.trend.length - 1]?.score ?? 0}/{grades.trend[grades.trend.length - 1]?.maxScore ?? 0} - -
-
+ )}
{!hasRecentGrades ? null : ( diff --git a/src/modules/dashboard/components/student-dashboard/student-ranking-card.tsx b/src/modules/dashboard/components/student-dashboard/student-ranking-card.tsx deleted file mode 100644 index 3f0b780..0000000 --- a/src/modules/dashboard/components/student-dashboard/student-ranking-card.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { Trophy } from "lucide-react" - -import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card" -import { EmptyState } from "@/shared/components/ui/empty-state" -import type { StudentRanking } from "@/modules/homework/types" - -export function StudentRankingCard({ ranking }: { ranking: StudentRanking | null }) { - return ( - - - - - Ranking - - - - {!ranking ? ( - - ) : ( -
-
-
-
Class Rank
-
- {ranking.rank}/{ranking.classSize} -
-
-
-
Overall
-
{Math.round(ranking.percentage)}%
-
- {ranking.totalScore}/{ranking.totalMaxScore} pts -
-
-
-
Based on latest graded submissions per assignment for your class.
-
- )} -
-
- ) -} diff --git a/src/modules/dashboard/components/student-dashboard/student-stats-grid.tsx b/src/modules/dashboard/components/student-dashboard/student-stats-grid.tsx index 5c24ab9..1e81a47 100644 --- a/src/modules/dashboard/components/student-dashboard/student-stats-grid.tsx +++ b/src/modules/dashboard/components/student-dashboard/student-stats-grid.tsx @@ -1,12 +1,17 @@ -import { BookOpen, CheckCircle2, PenTool, TriangleAlert } from "lucide-react" +import Link from "next/link" +import { BookOpen, PenTool, TriangleAlert, Trophy, TrendingUp } from "lucide-react" import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card" +import { cn } from "@/shared/lib/utils" +import type { StudentRanking } from "@/modules/homework/types" type Stat = { title: string value: string description: string icon: typeof BookOpen + href: string + color?: string } export function StudentStatsGrid({ @@ -14,52 +19,64 @@ export function StudentStatsGrid({ dueSoonCount, overdueCount, gradedCount, + ranking, }: { enrolledClassCount: number dueSoonCount: number overdueCount: number gradedCount: number + ranking: StudentRanking | null }) { - const stats: readonly Stat[] = [ + const stats: Stat[] = [ { - title: "My Classes", - value: String(enrolledClassCount), - description: "Enrolled classes", - icon: BookOpen, + title: "Average Score", + value: ranking ? `${Math.round(ranking.percentage)}%` : "-", + description: ranking ? "Overall performance" : "No grades yet", + icon: TrendingUp, + href: "/student/learning/assignments", + color: "text-blue-500", + }, + { + title: "Class Rank", + value: ranking ? `${ranking.rank}/${ranking.classSize}` : "-", + description: ranking ? "Current position" : "No ranking yet", + icon: Trophy, + href: "/student/learning/assignments", + color: "text-purple-500", }, { title: "Due Soon", value: String(dueSoonCount), description: "Next 7 days", icon: PenTool, + href: "/student/learning/assignments", + color: dueSoonCount > 0 ? "text-orange-500" : undefined, }, { title: "Overdue", value: String(overdueCount), description: "Needs attention", icon: TriangleAlert, - }, - { - title: "Graded", - value: String(gradedCount), - description: "With score", - icon: CheckCircle2, + href: "/student/learning/assignments", + color: overdueCount > 0 ? "text-red-500" : undefined, }, ] return (
{stats.map((stat) => ( - - - {stat.title} - - - -
{stat.value}
-
{stat.description}
-
-
+ + + + {stat.title} + + + +
{stat.value}
+
{stat.description}
+
+
+ ))}
) diff --git a/src/modules/dashboard/components/student-dashboard/student-upcoming-assignments-card.tsx b/src/modules/dashboard/components/student-dashboard/student-upcoming-assignments-card.tsx index 8d51bb2..37b5c0c 100644 --- a/src/modules/dashboard/components/student-dashboard/student-upcoming-assignments-card.tsx +++ b/src/modules/dashboard/components/student-dashboard/student-upcoming-assignments-card.tsx @@ -6,7 +6,7 @@ import { Button } from "@/shared/components/ui/button" import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card" import { EmptyState } from "@/shared/components/ui/empty-state" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/shared/components/ui/table" -import { formatDate } from "@/shared/lib/utils" +import { formatDate, cn } from "@/shared/lib/utils" import type { StudentHomeworkAssignmentListItem } from "@/modules/homework/types" const getStatusVariant = (status: string): "default" | "secondary" | "outline" => { @@ -23,6 +23,30 @@ const getStatusLabel = (status: string) => { return "Not started" } +const getActionLabel = (status: string) => { + if (status === "graded") return "Review" + if (status === "submitted") return "View" + if (status === "in_progress") return "Continue" + return "Start" +} + +const getActionVariant = (status: string): "default" | "secondary" | "outline" => { + if (status === "graded" || status === "submitted") return "outline" + return "default" +} + +const getDueUrgency = (dueAt: string | null) => { + if (!dueAt) return null + const now = new Date() + const due = new Date(dueAt) + const diffHours = (due.getTime() - now.getTime()) / (1000 * 60 * 60) + + if (diffHours < 0) return "overdue" + if (diffHours < 48) return "urgent" // 2 days + if (diffHours < 120) return "warning" // 5 days + return "normal" +} + export function StudentUpcomingAssignmentsCard({ upcomingAssignments }: { upcomingAssignments: StudentHomeworkAssignmentListItem[] }) { const hasAssignments = upcomingAssignments.length > 0 @@ -54,25 +78,49 @@ export function StudentUpcomingAssignmentsCard({ upcomingAssignments }: { upcomi Status Due Score + Action - {upcomingAssignments.map((a) => ( - - - - {a.title} - - - - - {getStatusLabel(a.progressStatus)} - - - {a.dueAt ? formatDate(a.dueAt) : "-"} - {a.latestScore ?? "-"} - - ))} + {upcomingAssignments.map((a) => { + const urgency = getDueUrgency(a.dueAt) + const isGraded = a.progressStatus === "graded" + + return ( + + +
+ + {a.title} + + {!isGraded && urgency === "overdue" && ( + Late + )} +
+
+ + + {getStatusLabel(a.progressStatus)} + + + + {a.dueAt ? formatDate(a.dueAt) : "-"} + + {a.latestScore ?? "-"} + + + +
+ ) + })}
diff --git a/src/modules/dashboard/components/teacher-dashboard/teacher-classes-card.tsx b/src/modules/dashboard/components/teacher-dashboard/teacher-classes-card.tsx index 3c86998..5425e77 100644 --- a/src/modules/dashboard/components/teacher-dashboard/teacher-classes-card.tsx +++ b/src/modules/dashboard/components/teacher-dashboard/teacher-classes-card.tsx @@ -1,7 +1,6 @@ import Link from "next/link" import { Users } from "lucide-react" -import { Badge } from "@/shared/components/ui/badge" import { Button } from "@/shared/components/ui/button" import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card" import { EmptyState } from "@/shared/components/ui/empty-state" diff --git a/src/modules/dashboard/components/teacher-dashboard/teacher-stats.tsx b/src/modules/dashboard/components/teacher-dashboard/teacher-stats.tsx index 9c9a898..d38b72b 100644 --- a/src/modules/dashboard/components/teacher-dashboard/teacher-stats.tsx +++ b/src/modules/dashboard/components/teacher-dashboard/teacher-stats.tsx @@ -54,6 +54,7 @@ export function TeacherStats({ description: "Published and ongoing", icon: PenTool, href: "/teacher/homework/assignments?status=published", + highlight: false, color: "text-blue-500", }, { @@ -62,6 +63,7 @@ export function TeacherStats({ description: "Across recent assignments", icon: TrendingUp, href: "#grade-trends", + highlight: false, color: "text-emerald-500", }, { @@ -70,6 +72,7 @@ export function TeacherStats({ description: "Overall completion rate", icon: BarChart, href: "#grade-trends", + highlight: false, color: "text-purple-500", }, ] as const; diff --git a/src/modules/exams/actions.ts b/src/modules/exams/actions.ts index 21bf4d4..e89410b 100644 --- a/src/modules/exams/actions.ts +++ b/src/modules/exams/actions.ts @@ -5,8 +5,9 @@ import { ActionState } from "@/shared/types/action-state" import { z } from "zod" import { createId } from "@paralleldrive/cuid2" import { db } from "@/shared/db" -import { exams, examQuestions } from "@/shared/db/schema" +import { exams, examQuestions, subjects, grades } from "@/shared/db/schema" import { eq } from "drizzle-orm" +import { omitScheduledAtFromDescription } from "./data-access" const ExamCreateSchema = z.object({ title: z.string().min(1), @@ -56,9 +57,17 @@ export async function createExamAction( const examId = createId() const scheduled = input.scheduledAt || undefined + // Retrieve names for JSON description (to maintain compatibility) + const subjectRecord = await db.query.subjects.findFirst({ + where: eq(subjects.id, input.subject), + }) + const gradeRecord = await db.query.grades.findFirst({ + where: eq(grades.id, input.grade), + }) + const meta = { - subject: input.subject, - grade: input.grade, + subject: subjectRecord?.name ?? input.subject, + grade: gradeRecord?.name ?? input.grade, difficulty: input.difficulty, totalScore: input.totalScore, durationMin: input.durationMin, @@ -71,11 +80,14 @@ export async function createExamAction( id: examId, title: input.title, description: JSON.stringify(meta), - creatorId: user?.id ?? "user_teacher_123", + creatorId: user?.id ?? "user_teacher_math", + subjectId: input.subject, + gradeId: input.grade, startTime: scheduled ? new Date(scheduled) : null, status: "draft", }) - } catch { + } catch (error) { + console.error("Failed to create exam:", error) return { success: false, message: "Database error: Failed to create exam", @@ -215,19 +227,6 @@ const ExamDuplicateSchema = z.object({ examId: z.string().min(1), }) -const omitScheduledAtFromDescription = (description: string | null) => { - if (!description) return null - try { - const parsed: unknown = JSON.parse(description) - if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return description - const meta = parsed as Record - if ("scheduledAt" in meta) delete meta.scheduledAt - return JSON.stringify(meta) - } catch { - return description - } -} - export async function duplicateExamAction( prevState: ActionState | null, formData: FormData @@ -271,7 +270,7 @@ export async function duplicateExamAction( id: newExamId, title: `${source.title} (Copy)`, description: omitScheduledAtFromDescription(source.description), - creatorId: user?.id ?? "user_teacher_123", + creatorId: user?.id ?? "user_teacher_math", startTime: null, endTime: null, status: "draft", @@ -305,6 +304,78 @@ export async function duplicateExamAction( } } -async function getCurrentUser() { - return { id: "user_teacher_123", role: "teacher" } +export async function getExamPreviewAction(examId: string) { + try { + const exam = await db.query.exams.findFirst({ + where: eq(exams.id, examId), + with: { + questions: { + orderBy: (examQuestions, { asc }) => [asc(examQuestions.order)], + with: { + question: true + } + } + } + }) + + if (!exam) { + return { success: false, message: "Exam not found" } + } + + // Extract questions from the relation + const questions = exam.questions.map(eq => eq.question) + + return { + success: true, + data: { + structure: exam.structure, + questions: questions + } + } +} catch (error) { + console.error(error) + return { success: false, message: "Failed to load exam preview" } +} +} + +export async function getSubjectsAction(): Promise> { + try { + const allSubjects = await db.query.subjects.findMany({ + orderBy: (subjects, { asc }) => [asc(subjects.order), asc(subjects.name)], + }) + + return { + success: true, + data: allSubjects.map((s) => ({ id: s.id, name: s.name })), + } + } catch (error) { + console.error("Failed to fetch subjects:", error) + return { + success: false, + message: "Failed to load subjects", + } + } +} + +export async function getGradesAction(): Promise> { + try { + const allGrades = await db.query.grades.findMany({ + orderBy: (grades, { asc }) => [asc(grades.order), asc(grades.name)], + }) + + return { + success: true, + data: allGrades.map((g) => ({ id: g.id, name: g.name })), + } + } catch (error) { + console.error("Failed to fetch grades:", error) + return { + success: false, + message: "Failed to load grades", + } + } +} + +async function getCurrentUser() { + return { id: "user_teacher_math", role: "teacher" } } diff --git a/src/modules/exams/components/assembly/exam-paper-preview.tsx b/src/modules/exams/components/assembly/exam-paper-preview.tsx index 13df658..07aef66 100644 --- a/src/modules/exams/components/assembly/exam-paper-preview.tsx +++ b/src/modules/exams/components/assembly/exam-paper-preview.tsx @@ -1,9 +1,5 @@ "use client" -import { ScrollArea } from "@/shared/components/ui/scroll-area" -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/shared/components/ui/dialog" -import { Button } from "@/shared/components/ui/button" -import { Eye, Printer } from "lucide-react" import type { ExamNode } from "./selected-question-list" type ChoiceOption = { @@ -86,55 +82,33 @@ export function ExamPaperPreview({ title, subject, grade, durationMin, totalScor } return ( - - - - - - -
- Exam Preview - -
-
- - -
- {/* Header */} -
-

{title}

-
- Subject: {subject} - Grade: {grade} - Time: {durationMin} mins - Total: {totalScore} pts -
-
-
Class:
-
Name:
-
No.:
-
-
+
+ {/* Header */} +
+

{title}

+
+ Subject: {subject} + Grade: {grade} + Time: {durationMin} mins + Total: {totalScore} pts +
+
+
Class:
+
Name:
+
No.:
+
+
- {/* Content */} -
- {nodes.length === 0 ? ( -
- Empty Exam Paper -
- ) : ( - nodes.map(node => renderNode(node)) - )} -
+ {/* Content */} +
+ {nodes.length === 0 ? ( +
+ Empty Exam Paper
- - -
+ ) : ( + nodes.map(node => renderNode(node)) + )} +
+
) } diff --git a/src/modules/exams/components/assembly/question-bank-list.tsx b/src/modules/exams/components/assembly/question-bank-list.tsx index c13059a..1ec59a2 100644 --- a/src/modules/exams/components/assembly/question-bank-list.tsx +++ b/src/modules/exams/components/assembly/question-bank-list.tsx @@ -10,10 +10,13 @@ type QuestionBankListProps = { questions: Question[] onAdd: (question: Question) => void isAdded: (id: string) => boolean + onLoadMore?: () => void + hasMore?: boolean + isLoading?: boolean } -export function QuestionBankList({ questions, onAdd, isAdded }: QuestionBankListProps) { - if (questions.length === 0) { +export function QuestionBankList({ questions, onAdd, isAdded, onLoadMore, hasMore, isLoading }: QuestionBankListProps) { + if (questions.length === 0 && !isLoading) { return (
No questions found matching your filters. @@ -22,7 +25,7 @@ export function QuestionBankList({ questions, onAdd, isAdded }: QuestionBankList } return ( -
+
{questions.map((q) => { const added = isAdded(q.id) const content = q.content as { text?: string } @@ -60,6 +63,28 @@ export function QuestionBankList({ questions, onAdd, isAdded }: QuestionBankList ) })} + + {hasMore && ( +
+ +
+ )} + + {isLoading && questions.length === 0 && ( +
+ {[1,2,3].map(i => ( +
+ ))} +
+ )}
) } diff --git a/src/modules/exams/components/exam-actions.tsx b/src/modules/exams/components/exam-actions.tsx index 1009fa9..ecb6c15 100644 --- a/src/modules/exams/components/exam-actions.tsx +++ b/src/modules/exams/components/exam-actions.tsx @@ -6,6 +6,7 @@ import { MoreHorizontal, Eye, Pencil, Trash, Archive, UploadCloud, Undo2, Copy } import { toast } from "sonner" import { Button } from "@/shared/components/ui/button" +import { ScrollArea } from "@/shared/components/ui/scroll-area" import { DropdownMenu, DropdownMenuContent, @@ -27,13 +28,14 @@ import { import { Dialog, DialogContent, - DialogDescription, - DialogHeader, DialogTitle, } from "@/shared/components/ui/dialog" -import { deleteExamAction, duplicateExamAction, updateExamAction } from "../actions" +import { deleteExamAction, duplicateExamAction, updateExamAction, getExamPreviewAction } from "../actions" import { Exam } from "../types" +import { ExamPaperPreview } from "./assembly/exam-paper-preview" +import type { ExamNode } from "./assembly/selected-question-list" +import type { Question } from "@/modules/questions/types" interface ExamActionsProps { exam: Exam @@ -44,6 +46,46 @@ export function ExamActions({ exam }: ExamActionsProps) { const [showViewDialog, setShowViewDialog] = useState(false) const [showDeleteDialog, setShowDeleteDialog] = useState(false) const [isWorking, setIsWorking] = useState(false) + const [previewNodes, setPreviewNodes] = useState(null) + const [loadingPreview, setLoadingPreview] = useState(false) + + const handleView = async () => { + setLoadingPreview(true) + setShowViewDialog(true) + try { + const result = await getExamPreviewAction(exam.id) + if (result.success && result.data) { + const { structure, questions } = result.data + const questionById = new Map() + for (const q of questions) questionById.set(q.id, q as unknown as Question) + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const hydrate = (nodes: any[]): ExamNode[] => { + return nodes.map((node) => { + if (node.type === "question") { + const q = node.questionId ? questionById.get(node.questionId) : undefined + return { ...node, question: q } + } + if (node.type === "group") { + return { ...node, children: hydrate(node.children || []) } + } + return node + }) + } + + const nodes = Array.isArray(structure) ? hydrate(structure) : [] + setPreviewNodes(nodes) + } else { + toast.error("Failed to load exam preview") + setShowViewDialog(false) + } + } catch (e) { + toast.error("Failed to load exam preview") + setShowViewDialog(false) + } finally { + setLoadingPreview(false) + } + } const copyId = () => { navigator.clipboard.writeText(exam.id) @@ -112,25 +154,35 @@ export function ExamActions({ exam }: ExamActionsProps) { return ( <> - - - - - - Actions - - Copy ID - - - setShowViewDialog(true)}> - View - - router.push(`/teacher/exams/${exam.id}/build`)}> - Edit - +
+ + + + + + + Actions + + Copy ID + + + router.push(`/teacher/exams/${exam.id}/build`)}> + Edit + router.push(`/teacher/exams/${exam.id}/build`)}> Build @@ -166,49 +218,21 @@ export function ExamActions({ exam }: ExamActionsProps) { - - - - - Exam Details - ID: {exam.id} - -
-
- Title: - {exam.title} -
-
- Subject: - {exam.subject} -
-
- Grade: - {exam.grade} -
-
- Total Score: - {exam.totalScore} -
-
- Duration: - {exam.durationMin} min -
-
-
-
- +
+ - Delete exam? + Are you absolutely sure? - This action cannot be undone. This will permanently delete the exam. + This action cannot be undone. This will permanently delete the exam + "{exam.title}" and remove all associated data. Cancel { e.preventDefault() handleDelete() @@ -220,6 +244,34 @@ export function ExamActions({ exam }: ExamActionsProps) { + + + +
+ {exam.title} +
+ + {loadingPreview ? ( +
Loading preview...
+ ) : previewNodes && previewNodes.length > 0 ? ( +
+ +
+ ) : ( +
+ No questions in this exam. +
+ )} +
+
+
) } diff --git a/src/modules/exams/components/exam-assembly.tsx b/src/modules/exams/components/exam-assembly.tsx index 19f4997..1cafa6b 100644 --- a/src/modules/exams/components/exam-assembly.tsx +++ b/src/modules/exams/components/exam-assembly.tsx @@ -1,19 +1,20 @@ "use client" -import { useDeferredValue, useMemo, useState } from "react" +import { useDeferredValue, useMemo, useState, useTransition, useEffect, useRef } from "react" import { useFormStatus } from "react-dom" import { useRouter } from "next/navigation" import { toast } from "sonner" -import { Search } from "lucide-react" +import { Search, Eye } from "lucide-react" import { Card, CardHeader, CardTitle } from "@/shared/components/ui/card" import { Button } from "@/shared/components/ui/button" import { Input } from "@/shared/components/ui/input" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/shared/components/ui/select" import { ScrollArea } from "@/shared/components/ui/scroll-area" -import { Separator } from "@/shared/components/ui/separator" +import { Dialog, DialogContent, DialogTitle, DialogTrigger } from "@/shared/components/ui/dialog" import type { Question } from "@/modules/questions/types" import { updateExamAction } from "@/modules/exams/actions" +import { getQuestionsAction } from "@/modules/questions/actions" import { StructureEditor } from "./assembly/structure-editor" import { QuestionBankList } from "./assembly/question-bank-list" import type { ExamNode } from "./assembly/selected-question-list" @@ -49,6 +50,12 @@ export function ExamAssembly(props: ExamAssemblyProps) { const [difficultyFilter, setDifficultyFilter] = useState("all") const deferredSearch = useDeferredValue(search) + // Bank state + const [bankQuestions, setBankQuestions] = useState(props.questionOptions) + const [page, setPage] = useState(1) + const [hasMore, setHasMore] = useState(props.questionOptions.length >= 20) + const [isBankLoading, startBankTransition] = useTransition() + // Initialize structure state const [structure, setStructure] = useState(() => { const questionById = new Map() @@ -76,26 +83,47 @@ export function ExamAssembly(props: ExamAssemblyProps) { return [] }) - const filteredQuestions = useMemo(() => { - let list: Question[] = [...props.questionOptions] - - if (deferredSearch) { - const lower = deferredSearch.toLowerCase() - list = list.filter(q => { - const content = q.content as { text?: string } - return content.text?.toLowerCase().includes(lower) - }) - } + const fetchQuestions = (reset: boolean = false) => { + startBankTransition(async () => { + const nextPage = reset ? 1 : page + 1 + try { + const result = await getQuestionsAction({ + q: deferredSearch, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + type: typeFilter === 'all' ? undefined : typeFilter as any, + difficulty: difficultyFilter === 'all' ? undefined : parseInt(difficultyFilter), + page: nextPage, + pageSize: 20 + }) + + if (result && result.data) { + setBankQuestions(prev => { + if (reset) return result.data + // Deduplicate just in case + const existingIds = new Set(prev.map(q => q.id)) + const newQuestions = result.data.filter(q => !existingIds.has(q.id)) + return [...prev, ...newQuestions] + }) + setHasMore(result.data.length === 20) + setPage(nextPage) + } + } catch (error) { + toast.error("Failed to load questions") + } + }) + } - if (typeFilter !== "all") { - list = list.filter((q) => q.type === (typeFilter as Question["type"])) + const isFirstRender = useRef(true) + + useEffect(() => { + if (isFirstRender.current) { + isFirstRender.current = false + if (deferredSearch === "" && typeFilter === "all" && difficultyFilter === "all") { + return + } } - if (difficultyFilter !== "all") { - const d = parseInt(difficultyFilter) - list = list.filter((q) => q.difficulty === d) - } - return list - }, [deferredSearch, typeFilter, difficultyFilter, props.questionOptions]) + fetchQuestions(true) + }, [deferredSearch, typeFilter, difficultyFilter]) // Recursively calculate total score const assignedTotal = useMemo(() => { @@ -231,6 +259,8 @@ export function ExamAssembly(props: ExamAssemblyProps) { return clean(structure) } + const [previewOpen, setPreviewOpen] = useState(false) + const handleSave = async (formData: FormData) => { formData.set("examId", props.examId) formData.set("questionsJson", JSON.stringify(getFlatQuestions())) @@ -238,7 +268,7 @@ export function ExamAssembly(props: ExamAssemblyProps) { const result = await updateExamAction(null, formData) if (result.success) { - toast.success("Saved draft") + toast.success("Exam draft saved") } else { toast.error(result.message || "Save failed") } @@ -260,47 +290,76 @@ export function ExamAssembly(props: ExamAssemblyProps) { } return ( -
- {/* Left: Preview (3 cols) */} - - +
+ {/* Left: Preview (8 cols) */} + +
- Exam Structure - -
-
-
- {assignedTotal} / {props.totalScore} - Total Score + Exam Structure +
+
+ {props.subject} + + {props.grade} + + {props.durationMin} min
-
-
props.totalScore ? "bg-destructive" : "bg-primary" - }`} - style={{ width: `${progress}%` }} - /> +
+
+ + + + + +
+ {props.title} +
+
+ +
+ +
+
+
+
+
+ +
+
+
+ props.totalScore ? "text-destructive" : "text-primary"}`}> + {assignedTotal} + + / {props.totalScore} +
+ Total Score +
+
+
props.totalScore ? "bg-destructive" : "bg-primary" + }`} + style={{ height: `${Math.min(progress, 100)}%` }} + /> +
- -
-
-
{props.subject}
-
{props.grade}
-
Duration: {props.durationMin} min
-
- + +
-
-
- +
+
+ {structure.length === 0 ? "Start by adding questions from the right panel" : `${structure.length} items in structure`} +
+ + -
- + +
- {/* Right: Question Bank (2 cols) */} - - - Question Bank + {/* Right: Question Bank (4 cols) */} + + +
+ Question Bank + + {bankQuestions.length}{hasMore ? "+" : ""} loaded + +
- + setSearch(e.target.value)} />
setSearch(e.target.value || null)} />
- +
+ - + - {(search || (status && status !== "all") || (difficulty && difficulty !== "all")) && ( - - )} + {(search || (status && status !== "all") || (difficulty && difficulty !== "all")) && ( + + )} +
) } diff --git a/src/modules/exams/components/exam-form.tsx b/src/modules/exams/components/exam-form.tsx index 87c1472..841abaa 100644 --- a/src/modules/exams/components/exam-form.tsx +++ b/src/modules/exams/components/exam-form.tsx @@ -1,99 +1,357 @@ "use client" -import { useState } from "react" -import { useFormStatus } from "react-dom" -import { toast } from "sonner" +import { useTransition, useEffect, useState } from "react" import { useRouter } from "next/navigation" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import * as z from "zod" +import { toast } from "sonner" +import { Loader2, Sparkles, BookOpen } from "lucide-react" -import { Card, CardContent, CardHeader, CardTitle, CardFooter } from "@/shared/components/ui/card" +import { cn } from "@/shared/lib/utils" import { Button } from "@/shared/components/ui/button" +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/shared/components/ui/form" import { Input } from "@/shared/components/ui/input" -import { Label } from "@/shared/components/ui/label" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/shared/components/ui/select" -import { createExamAction } from "../actions" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/shared/components/ui/select" +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/shared/components/ui/card" +import { createExamAction, getSubjectsAction, getGradesAction } from "../actions" -function SubmitButton() { - const { pending } = useFormStatus() - return ( - - ) +export const formSchema = z.object({ + title: z.string().min(2, "Title must be at least 2 characters."), + subject: z.string().min(1, "Subject is required."), + grade: z.string().min(1, "Grade is required."), + difficulty: z.string(), + totalScore: z.coerce.number().min(1, "Total score must be at least 1."), + durationMin: z.coerce.number().min(10, "Duration must be at least 10 minutes."), + scheduledAt: z.string().optional(), + mode: z.enum(["manual", "ai"]), +}) + +type ExamFormValues = z.infer + +const defaultValues: Partial = { + title: "", + subject: "", + grade: "", + difficulty: "3", + totalScore: 100, + durationMin: 90, + mode: "manual", + scheduledAt: "", } export function ExamForm() { const router = useRouter() - const [difficulty, setDifficulty] = useState("3") + const [isPending, startTransition] = useTransition() + const [subjects, setSubjects] = useState<{ id: string; name: string }[]>([]) + const [loadingSubjects, setLoadingSubjects] = useState(true) + const [grades, setGrades] = useState<{ id: string; name: string }[]>([]) + const [loadingGrades, setLoadingGrades] = useState(true) - const handleSubmit = async (formData: FormData) => { - const result = await createExamAction(null, formData) - if (result.success) { - toast.success(result.message) - if (result.data) { - router.push(`/teacher/exams/${result.data}/build`) + const form = useForm({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + resolver: zodResolver(formSchema) as any, + defaultValues: defaultValues as unknown as ExamFormValues, + }) + + useEffect(() => { + const fetchMetadata = async () => { + try { + const [subjectsResult, gradesResult] = await Promise.all([ + getSubjectsAction(), + getGradesAction() + ]) + + if (subjectsResult.success && subjectsResult.data) { + setSubjects(subjectsResult.data) + } else { + toast.error("Failed to load subjects") + } + + if (gradesResult.success && gradesResult.data) { + setGrades(gradesResult.data) + } else { + toast.error("Failed to load grades") + } + } catch (error) { + console.error(error) + toast.error("Failed to load form data") + } finally { + setLoadingSubjects(false) + setLoadingGrades(false) } - } else { - toast.error(result.message) } + fetchMetadata() + }, []) + + function onSubmit(data: ExamFormValues) { + const formData = new FormData() + formData.append("title", data.title) + formData.append("subject", data.subject) + formData.append("grade", data.grade) + formData.append("difficulty", data.difficulty) + formData.append("totalScore", data.totalScore.toString()) + formData.append("durationMin", data.durationMin.toString()) + if (data.scheduledAt) { + formData.append("scheduledAt", data.scheduledAt) + } + + startTransition(async () => { + const result = await createExamAction(null, formData) + + if (result.success && result.data) { + toast.success("Exam draft created", { + description: "Redirecting to exam builder...", + }) + router.push(`/teacher/exams/${result.data}/build`) + } else { + toast.error(result.message || "Failed to create exam") + } + }) + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const handleSubmit = (e: any) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + form.handleSubmit(onSubmit as any)(e); } return ( - - - Exam Creator - - -
-
-
- - -
-
- - -
-
- - -
-
- - - -
-
- - -
-
- - -
-
- - -
-
+ + + {/* Left Column: Exam Details */} +
+ + + Exam Details + + Define the core information for your exam. + + + + ( + + Title + + + + + + )} + /> + +
+ ( + + Subject + + + + )} + /> + ( + + Grade Level + + + + )} + /> +
- +
+ ( + + Difficulty + + + + )} + /> + ( + + Total Score + + + + + + )} + /> + ( + + Duration (min) + + + + + + )} + /> +
- - - - -
-
+ ( + + Schedule Start Time (Optional) + + + + + If set, this exam will be scheduled for a specific time. + + + + )} + /> + + +
+ + {/* Right Column: Mode & Actions */} +
+ + + Assembly Mode + + Choose how to build the exam structure. + + + + ( + + +
+ {/* Manual Mode */} +
field.onChange("manual")} + > +
+ + Manual Assembly +
+ + Manually select questions from the bank and organize structure. + +
+ + {/* AI Mode (Disabled) */} +
+
+ + AI Generation +
+ + Automatically generate exam structure based on topics. (Coming Soon) + +
+
+
+ +
+ )} + /> +
+ + + +
+
+ + ) } diff --git a/src/modules/exams/components/exam-grid.tsx b/src/modules/exams/components/exam-grid.tsx new file mode 100644 index 0000000..c5eb19c --- /dev/null +++ b/src/modules/exams/components/exam-grid.tsx @@ -0,0 +1,16 @@ +import { Exam } from "../types" +import { ExamCard } from "./exam-card" + +interface ExamGridProps { + exams: Exam[] +} + +export function ExamGrid({ exams }: ExamGridProps) { + return ( +
+ {exams.map((exam) => ( + + ))} +
+ ) +} diff --git a/src/modules/exams/data-access.ts b/src/modules/exams/data-access.ts index 53ce28c..d002eae 100644 --- a/src/modules/exams/data-access.ts +++ b/src/modules/exams/data-access.ts @@ -68,6 +68,10 @@ export const getExams = cache(async (params: GetExamsParams) => { const data = await db.query.exams.findMany({ where: conditions.length ? and(...conditions) : undefined, orderBy: [desc(exams.createdAt)], + with: { + subject: true, + gradeEntity: true, + } }) // Transform and Filter (especially for JSON fields) @@ -78,8 +82,8 @@ export const getExams = cache(async (params: GetExamsParams) => { id: exam.id, title: exam.title, status: (exam.status as ExamStatus) || "draft", - subject: getString(meta, "subject") || "General", - grade: getString(meta, "grade") || "General", + subject: exam.subject?.name ?? getString(meta, "subject") ?? "General", + grade: exam.gradeEntity?.name ?? getString(meta, "grade") ?? "General", difficulty: toExamDifficulty(getNumber(meta, "difficulty")), totalScore: getNumber(meta, "totalScore") || 100, durationMin: getNumber(meta, "durationMin") || 60, @@ -103,6 +107,8 @@ export const getExamById = cache(async (id: string) => { const exam = await db.query.exams.findFirst({ where: eq(exams.id, id), with: { + subject: true, + gradeEntity: true, questions: { orderBy: (examQuestions, { asc }) => [asc(examQuestions.order)], with: { @@ -120,8 +126,8 @@ export const getExamById = cache(async (id: string) => { id: exam.id, title: exam.title, status: (exam.status as ExamStatus) || "draft", - subject: getString(meta, "subject") || "General", - grade: getString(meta, "grade") || "General", + subject: exam.subject?.name ?? getString(meta, "subject") ?? "General", + grade: exam.gradeEntity?.name ?? getString(meta, "grade") ?? "General", difficulty: toExamDifficulty(getNumber(meta, "difficulty")), totalScore: getNumber(meta, "totalScore") || 100, durationMin: getNumber(meta, "durationMin") || 60, @@ -137,3 +143,18 @@ export const getExamById = cache(async (id: string) => { })), } }) + +export const omitScheduledAtFromDescription = (description: string | null): string => { + if (!description) return "{}" + try { + const meta = JSON.parse(description) + if (typeof meta === "object" && meta !== null) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { scheduledAt, ...rest } = meta as any + return JSON.stringify(rest) + } + return description + } catch { + return description || "{}" + } +} diff --git a/src/modules/homework/actions.ts b/src/modules/homework/actions.ts index 9f8eb5d..81eb139 100644 --- a/src/modules/homework/actions.ts +++ b/src/modules/homework/actions.ts @@ -46,7 +46,7 @@ async function getCurrentUser() { if (anyUser) return { id: anyUser.id, role: roleHint } - return { id: "user_teacher_123", role: roleHint } + return { id: "user_teacher_math", role: roleHint } } async function ensureTeacher() { diff --git a/src/modules/homework/components/homework-assignment-exam-content-card.tsx b/src/modules/homework/components/homework-assignment-exam-content-card.tsx index 5842901..6c78e62 100644 --- a/src/modules/homework/components/homework-assignment-exam-content-card.tsx +++ b/src/modules/homework/components/homework-assignment-exam-content-card.tsx @@ -17,7 +17,7 @@ export function HomeworkAssignmentExamContentCard({ Exam Content - + -
-
题目
-
- - - - - +
+
+
+ Question Preview +
+
+ +
+ + + +
+
+ + + +
-
-
-
错题详情
-
- -
-
- - -
-
- - -
-
- - -
+
+
+
Error Analysis
+
+
+
+ +
+ + +
+
+ +
+
Wrong Answers
+
+ + +
-
-
- - - -
diff --git a/src/modules/homework/components/homework-assignment-exam-error-explorer.tsx b/src/modules/homework/components/homework-assignment-exam-error-explorer.tsx index f124c9a..c2d9248 100644 --- a/src/modules/homework/components/homework-assignment-exam-error-explorer.tsx +++ b/src/modules/homework/components/homework-assignment-exam-error-explorer.tsx @@ -26,7 +26,7 @@ export function HomeworkAssignmentExamErrorExplorer({ }, [questions, selectedQuestionId]) return ( -
+
({ diff --git a/src/modules/homework/components/homework-assignment-exam-preview-pane.tsx b/src/modules/homework/components/homework-assignment-exam-preview-pane.tsx index 4b87522..4da9c63 100644 --- a/src/modules/homework/components/homework-assignment-exam-preview-pane.tsx +++ b/src/modules/homework/components/homework-assignment-exam-preview-pane.tsx @@ -20,15 +20,19 @@ export function HomeworkAssignmentExamPreviewPane({ onQuestionSelect: (questionId: string) => void }) { return ( -
-
题目
- - +
+
+ Question Preview +
+ +
+ +
) diff --git a/src/modules/homework/components/homework-assignment-question-error-detail-panel.tsx b/src/modules/homework/components/homework-assignment-question-error-detail-panel.tsx index 3ed5bef..76cfa5f 100644 --- a/src/modules/homework/components/homework-assignment-question-error-detail-panel.tsx +++ b/src/modules/homework/components/homework-assignment-question-error-detail-panel.tsx @@ -92,59 +92,63 @@ export function HomeworkAssignmentQuestionErrorDetailPanel({ const errorRate = selected?.errorRate ?? 0 return ( -
-
-
错题详情
- {selected ? ( -
-
- -
-
-
- 错误人数 - {errorCount} -
-
- 错误率 - {(errorRate * 100).toFixed(1)}% -
-
- 统计样本 - {gradedSampleCount} -
-
-
- ) : ( -
请选择左侧题目
- )} +
+
+
Error Analysis
- -
- - {!selected ? ( -
暂无数据
- ) : wrongAnswers.length === 0 ? ( -
暂无错误答案
- ) : ( -
-
错误答案列表(可滚动)
-
- {wrongAnswers.map((item, idx) => ( -
-
-
{item.studentName}
-
- {formatAnswer(item.answerContent, selected)} -
-
+ +
+ {selected ? ( + <> +
+
+ +
+
+
+ Question + Q{selected.questionId.slice(-4)}
- ))} +
+ Errors + + {errorCount} / {gradedSampleCount} + +
+
+ +
+
Wrong Answers ({wrongAnswers.length})
+ {wrongAnswers.length === 0 ? ( +
+ No wrong answers recorded. +
+ ) : ( +
+ {wrongAnswers.map((wa, i) => ( +
+
+ Student Answer + {wa.count ?? 1} student{(wa.count ?? 1) > 1 ? "s" : ""} +
+
+ {formatAnswer(wa.answerContent, selected)} +
+
+ ))} +
+ )} +
+ + ) : ( +
+

Select a question from the left

+

to view error analysis

)} - -
+
+
) } diff --git a/src/modules/homework/components/homework-assignment-question-error-details-card.tsx b/src/modules/homework/components/homework-assignment-question-error-details-card.tsx deleted file mode 100644 index 7fe5cfb..0000000 --- a/src/modules/homework/components/homework-assignment-question-error-details-card.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import type { HomeworkAssignmentQuestionAnalytics } from "@/modules/homework/types" -import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card" -import { ScrollArea } from "@/shared/components/ui/scroll-area" -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/shared/components/ui/table" - -export function HomeworkAssignmentQuestionErrorDetailsCard({ - questions, - gradedSampleCount, -}: { - questions: HomeworkAssignmentQuestionAnalytics[] - gradedSampleCount: number -}) { - return ( - - - Question Error Details - - - {questions.length === 0 || gradedSampleCount === 0 ? ( -
No data available.
- ) : ( - - - - - Question - Error Count - Error Rate - - - - {questions.map((q, index) => ( - - -
Q{index + 1}
-
- {q.errorCount} - {(q.errorRate * 100).toFixed(1)}% -
- ))} -
-
-
- )} -
-
- ) -} - diff --git a/src/modules/homework/components/homework-assignment-question-error-overview-card.tsx b/src/modules/homework/components/homework-assignment-question-error-overview-card.tsx index 6a6f366..0c7b033 100644 --- a/src/modules/homework/components/homework-assignment-question-error-overview-card.tsx +++ b/src/modules/homework/components/homework-assignment-question-error-overview-card.tsx @@ -1,103 +1,8 @@ +"use client" + import type { HomeworkAssignmentQuestionAnalytics } from "@/modules/homework/types" import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card" - -function ErrorRateChart({ - questions, - gradedSampleCount, -}: { - questions: HomeworkAssignmentQuestionAnalytics[] - gradedSampleCount: number -}) { - const w = 100 - const h = 60 - const padL = 10 - const padR = 3 - const padT = 4 - const padB = 10 - const plotW = w - padL - padR - const plotH = h - padT - padB - const n = questions.length - - const clamp01 = (v: number) => Math.max(0, Math.min(1, v)) - const xFor = (i: number) => padL + (n <= 1 ? 0 : (i / (n - 1)) * plotW) - const yFor = (rate: number) => padT + (1 - clamp01(rate)) * plotH - - const points = questions.map((q, i) => `${xFor(i)},${yFor(q.errorRate)}`).join(" ") - const areaD = - n === 0 - ? "" - : `M ${padL} ${padT + plotH} L ${points.split(" ").join(" L ")} L ${padL + plotW} ${padT + plotH} Z` - - const gridYs = [ - { v: 1, label: "100%" }, - { v: 0.5, label: "50%" }, - { v: 0, label: "0%" }, - ] - - return ( - - {gridYs.map((g) => { - const y = yFor(g.v) - return ( - - - - {g.label} - - - ) - })} - - - - - {n >= 2 ? : null} - - - {questions.map((q, i) => { - const cx = xFor(i) - const cy = yFor(q.errorRate) - const label = `Q${i + 1}` - return ( - - - {`${label}: ${(q.errorRate * 100).toFixed(1)}% (${q.errorCount} / ${gradedSampleCount})`} - - ) - })} - - {questions.map((q, i) => { - if (n > 12 && i % 2 === 1) return null - const x = xFor(i) - return ( - - {i + 1} - - ) - })} - - ) -} +import { Bar, BarChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts" export function HomeworkAssignmentQuestionErrorOverviewCard({ questions, @@ -106,26 +11,78 @@ export function HomeworkAssignmentQuestionErrorOverviewCard({ questions: HomeworkAssignmentQuestionAnalytics[] gradedSampleCount: number }) { + const data = questions.map((q, index) => ({ + name: `Q${index + 1}`, + errorRate: q.errorRate * 100, + errorCount: q.errorCount, + total: gradedSampleCount, + })) + return ( - Question Error Overview + Error Rate Overview - + {questions.length === 0 || gradedSampleCount === 0 ? ( -
- No graded submissions yet. Error analytics will appear here after grading. +
+ No graded submissions yet.
) : ( -
-
- Graded students - {gradedSampleCount} -
-
- -
-
+ + + + + `${value}%`} + domain={[0, 100]} + /> + { + if (active && payload && payload.length) { + const d = payload[0].payload + return ( +
+
+
+ Question + {d.name} +
+
+ Error Rate + {d.errorRate.toFixed(1)}% +
+
+ Errors + + {d.errorCount} / {d.total} + +
+
+
+ ) + } + return null + }} + /> + +
+
)} diff --git a/src/modules/homework/components/homework-grading-view.tsx b/src/modules/homework/components/homework-grading-view.tsx index 67dab12..d1b12a0 100644 --- a/src/modules/homework/components/homework-grading-view.tsx +++ b/src/modules/homework/components/homework-grading-view.tsx @@ -3,10 +3,20 @@ import { useState } from "react" import { useRouter } from "next/navigation" import { toast } from "sonner" -import { Check, MessageSquarePlus, X } from "lucide-react" +import { + Check, + MessageSquarePlus, + X, + ChevronLeft, + ChevronRight, + Save, + User, + AlertCircle, + Clock +} from "lucide-react" import { Badge } from "@/shared/components/ui/badge" import { Button } from "@/shared/components/ui/button" -import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card" +import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/shared/components/ui/card" import { Input } from "@/shared/components/ui/input" import { Label } from "@/shared/components/ui/label" import { Textarea } from "@/shared/components/ui/textarea" @@ -39,20 +49,48 @@ type HomeworkGradingViewProps = { status: string totalScore: number | null answers: Answer[] + prevSubmissionId?: string | null + nextSubmissionId?: string | null } export function HomeworkGradingView({ submissionId, answers: initialAnswers, + prevSubmissionId, + nextSubmissionId, + studentName, + assignmentTitle, + submittedAt, }: HomeworkGradingViewProps) { const router = useRouter() const [answers, setAnswers] = useState(() => applyAutoGrades(initialAnswers)) const [isSubmitting, setIsSubmitting] = useState(false) - const [showFeedbackByAnswerId, setShowFeedbackByAnswerId] = useState>({}) + + // Initialize feedback visibility for answers that already have feedback + const [showFeedbackByAnswerId, setShowFeedbackByAnswerId] = useState>(() => { + const initialVisibility: Record = {} + if (initialAnswers) { + initialAnswers.forEach(a => { + if (a.feedback && a.feedback.trim().length > 0) { + initialVisibility[a.id] = true + } + }) + } + return initialVisibility + }) const handleManualScoreChange = (id: string, val: string) => { const parsed = val === "" ? 0 : Number(val) - const nextScore = Number.isFinite(parsed) ? parsed : 0 + // Clamp score between 0 and maxScore? Or allow extra credit? + // Usually maxScore is the limit, but let's just ensure it's a number. + // Ideally we should clamp it to [0, maxScore] to avoid errors, but sometimes teachers want to give 0 for invalid input. + const targetAnswer = answers.find(a => a.id === id) + const max = targetAnswer?.maxScore ?? 100 + + let nextScore = Number.isFinite(parsed) ? parsed : 0 + if (nextScore > max) nextScore = max + if (nextScore < 0) nextScore = 0 + setAnswers((prev) => prev.map((a) => (a.id === id ? { ...a, score: nextScore } : a))) } @@ -69,10 +107,12 @@ export function HomeworkGradingView({ } const currentTotal = answers.reduce((sum, a) => sum + (a.score || 0), 0) - const binaryAnswers = answers.filter(shouldUseBinaryGrading) - const correctCount = binaryAnswers.reduce((sum, a) => sum + (a.score === a.maxScore ? 1 : 0), 0) - const incorrectCount = binaryAnswers.reduce((sum, a) => sum + (a.score === 0 ? 1 : 0), 0) - const ungradedCount = binaryAnswers.length - correctCount - incorrectCount + const maxTotal = answers.reduce((sum, a) => sum + a.maxScore, 0) + const progressPercent = maxTotal > 0 ? (currentTotal / maxTotal) * 100 : 0 + + const correctCount = answers.reduce((sum, a) => sum + (a.score === a.maxScore ? 1 : 0), 0) + const incorrectCount = answers.reduce((sum, a) => sum + (a.score === 0 ? 1 : 0), 0) + const partialCount = answers.reduce((sum, a) => sum + (a.score !== null && a.score > 0 && a.score < a.maxScore ? 1 : 0), 0) const handleSubmit = async () => { setIsSubmitting(true) @@ -89,177 +129,357 @@ export function HomeworkGradingView({ const result = await gradeHomeworkSubmissionAction(null, formData) if (result.success) { - toast.success("Grading saved") - router.push("/teacher/homework/submissions") + toast.success("Grading saved successfully") + // Optionally redirect or stay + router.refresh() } else { - toast.error(result.message || "Failed to save") + toast.error(result.message || "Failed to save grading") } setIsSubmitting(false) } + const handleScrollToQuestion = (id: string) => { + const el = document.getElementById(`question-card-${id}`) + if (el) { + el.scrollIntoView({ behavior: "smooth", block: "start" }) + } + } + return ( -
-
-
-

Student Response

-
- -
+
+ {/* Main Content: Questions List */} +
+ +
{answers.map((ans, index) => ( -
-
-
- Question {index + 1} -
{ans.questionContent?.text}
+ 0 ? "border-l-4 border-l-red-500" : "border-l-4 border-l-muted" + }`}> + +
+
+
+ + {index + 1} + + + {ans.questionType.replace("_", " ")} + + {isAutoGradable(ans) && ( + Auto-graded + )} +
+ + {ans.questionContent?.text || "No question text"} + +
+
+ + {ans.score ?? 0} / {ans.maxScore} pts + +
- Max: {ans.maxScore} -
- -
- -

- {formatStudentAnswer(ans.studentAnswer)} -

-
- + + -
+ + + {/* Student Answer Display */} +
+ + +
+ {(ans.questionType === "single_choice" || ans.questionType === "multiple_choice") && + Array.isArray(ans.questionContent?.options) ? ( +
+ {(ans.questionContent.options as ChoiceOption[]).map((opt: ChoiceOption) => { + const isSelected = Array.isArray(extractAnswerValue(ans.studentAnswer)) + ? (extractAnswerValue(ans.studentAnswer) as string[]).includes(opt.id as string) + : extractAnswerValue(ans.studentAnswer) === opt.id + + const isCorrect = opt.isCorrect === true + + // Visual logic: + // If selected and correct -> Green + Check + // If selected and wrong -> Red + X + // If not selected but correct -> Green outline (show missed correct answer) + + let containerClass = "border-transparent hover:bg-muted/50" + let indicatorClass = "border-muted-foreground/30 text-muted-foreground" + + if (isSelected) { + if (isCorrect) { + containerClass = "border-emerald-500 bg-emerald-50/50 dark:bg-emerald-950/20" + indicatorClass = "border-emerald-500 bg-emerald-500 text-white" + } else { + containerClass = "border-red-500 bg-red-50/50 dark:bg-red-950/20" + indicatorClass = "border-red-500 bg-red-500 text-white" + } + } else if (isCorrect) { + containerClass = "border-emerald-200 bg-emerald-50/30 dark:border-emerald-800 dark:bg-emerald-950/10" + indicatorClass = "border-emerald-500 text-emerald-600 dark:text-emerald-400" + } + + return ( +
+
+ {opt.id as string} +
+ {opt.text} + {isCorrect && } + {isSelected && !isCorrect && } +
+ ) + })} +
+ ) : ( +

+ {formatStudentAnswer(ans.studentAnswer)} +

+ )} +
+
+ + {/* Reference Answer (for text/non-choice questions) */} + {ans.questionType === "text" && ( +
+ +
+ {getTextCorrectAnswers(ans.questionContent).join(" / ") || "No reference answer provided."} +
+
+ )} +
+ + +
+ {/* Grading Controls */} +
+
+ + +
+ + + +
+ + handleManualScoreChange(ans.id, e.target.value)} + /> + / {ans.maxScore} +
+
+ + {/* Feedback Toggle */} + +
+ + {/* Feedback Textarea */} + {showFeedbackByAnswerId[ans.id] && ( +
+